<commit_before>﻿import {Node, Declaration, Changes, ChangeMap} from "ui/styling/css-selector";
import {isNullOrUndefined} from "utils/types";
import {escapeRegexSymbols} from "utils/utils";

import * as cssParser from "css";
import * as selectorParser from "./css-selector-parser";

const enum Specificity {
    Inline =        0x01000000,
    Id =            0x00010000,
    Attribute =     0x00000100,
    Class =         0x00000100,
    PseudoClass =   0x00000100,
    Type =          0x00000001,
    Universal =     0x00000000,
    Invalid =       0x00000000
}

const enum Rarity {
    Invalid = 4,
    Id = 3,
    Class = 2,
    Type = 1,
    PseudoClass = 0,
    Attribute = 0,
    Universal = 0,
    Inline = 0
}

interface LookupSorter {
    sortById(id: string, sel: SelectorCore);
    sortByClass(cssClass: string, sel: SelectorCore);
    sortByType(cssType: string, sel: SelectorCore);
    sortAsUniversal(sel: SelectorCore);
}

namespace Match {
    /**
     * Depends on attributes or pseudoclasses state;
     */
    export var Dynamic = true;
    /**
     * Depends only on the tree structure.
     */
    export var Static = false;
}

function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: boolean = false): ClassDecorator {
    return cls => {
        cls.prototype.specificity = specificity;
        cls.prototype.rarity = rarity;
        cls.prototype.combinator = "";
        cls.prototype.dynamic = dynamic;
        return cls;
    }
}

declare type Combinator = "+" | ">" | "~" | " ";
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
export abstract class SelectorCore {
    public specificity: number;
    public rarity: Rarity;
    public combinator: Combinator;
    public ruleset: RuleSet;
    public dynamic: boolean;
    public abstract match(node: Node): boolean;
    /**
     * If the selector is static returns if it matches the node.
     * If the selector is dynamic returns if it may match the node, and accumulates any changes that may affect its state.
     */
    public abstract accumulateChanges(node: Node, map: ChangeAccumulator): boolean;
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortAsUniversal(base || this); }
}

export abstract class SimpleSelector extends SelectorCore {
    public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
        if (!this.dynamic) {
            return this.match(node);
        } else if (this.mayMatch(node)) {
            this.trackChanges(node, map);
            return true;
        }
        return false;
    }
    public mayMatch(node: Node): boolean { return this.match(node); }
    public trackChanges(node: Node, map: ChangeAccumulator): void {
        // No-op, silence the tslint 'block is empty'.
        // Some derived classes (dynamic) will actually fill the map with stuff here, some (static) won't do anything.
    }
}

function wrap(text: string): string {
    return text ? ` ${text} ` : "";
}

@SelectorProperties(Specificity.Invalid, Rarity.Invalid, Match.Static)
export class InvalidSelector extends SimpleSelector {
    constructor(public e: Error) { super(); }
    public toString(): string { return `<error: ${this.e}>`; }
    public match(node: Node): boolean { return false; }
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
        // No-op, silence the tslint 'block is empty'.
        // It feels like tslint has problems with simple polymorphism...
        // This selector is invalid and will never match so we won't bother sorting it to further appear in queries.
    }
}

@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
export class UniversalSelector extends SimpleSelector {
    public toString(): string { return `*${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return true; }
}

@SelectorProperties(Specificity.Id, Rarity.Id, Match.Static)
export class IdSelector extends SimpleSelector {
    constructor(public id: string) { super(); }
    public toString(): string { return `#${this.id}${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return node.id === this.id; }
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortById(this.id, base || this); }
}

@SelectorProperties(Specificity.Type, Rarity.Type, Match.Static)
export class TypeSelector extends SimpleSelector {
    constructor(public cssType: string) { super(); }
    public toString(): string { return `${this.cssType}${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return node.cssType === this.cssType; }
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByType(this.cssType, base || this); }
}

@SelectorProperties(Specificity.Class, Rarity.Class, Match.Static)
export class ClassSelector extends SimpleSelector {
    constructor(public cssClass: string) { super(); }
    public toString(): string { return `.${this.cssClass}${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return node.cssClasses && node.cssClasses.has(this.cssClass); }
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByClass(this.cssClass, base || this); }
}

declare type AttributeTest = "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|=";
@SelectorProperties(Specificity.Attribute, Rarity.Attribute, Match.Dynamic)
export class AttributeSelector extends SimpleSelector {
    constructor(public attribute: string, public test?: AttributeTest, public value?: string) {
        super();

        if (!test) {
            // HasAttribute
            this.match = node => !isNullOrUndefined(node[attribute]);
            return;
        }

        if (!value) {
            this.match = node => false;
        }

        let escapedValue = escapeRegexSymbols(value);
        let regexp: RegExp = null;
        switch(test) {
            case "^=": // PrefixMatch
                regexp = new RegExp("^" + escapedValue);
                break;
            case "$=": // SuffixMatch
                regexp = new RegExp(escapedValue + "$");
                break;
            case "*=": // SubstringMatch
                regexp = new RegExp(escapedValue);
                break;
            case "=": // Equals
                regexp = new RegExp("^" + escapedValue + "$");
                break;
            case "~=": // Includes
                if (/\s/.test(value)) {
                    this.match = node => false;
                    return;
                }
                regexp = new RegExp("(^|\\s)" + escapedValue + "(\\s|$)");
                break;
            case "|=": // DashMatch
                regexp = new RegExp("^" + escapedValue + "(-|$)");
                break;
        }

        if (regexp) {
            this.match = node => regexp.test(node[attribute] + "");
            return;
        } else {
            this.match = node => false;
            return;
        }
    }
    public get rarity(): number { return Specificity.Attribute; }
    public toString(): string { return `[${this.attribute}${wrap(this.test)}${(this.test && this.value) || ''}]${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return false; }
    public mayMatch(node: Node): boolean { return true; }
    public trackChanges(node: Node, map: ChangeAccumulator): void { map.addAttribute(node, this.attribute); }
}

@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic)<commit_msg>Remove the rarity getter too<commit_after>﻿import {Node, Declaration, Changes, ChangeMap} from "ui/styling/css-selector";
import {isNullOrUndefined} from "utils/types";
import {escapeRegexSymbols} from "utils/utils";

import * as cssParser from "css";
import * as selectorParser from "./css-selector-parser";

const enum Specificity {
    Inline =        0x01000000,
    Id =            0x00010000,
    Attribute =     0x00000100,
    Class =         0x00000100,
    PseudoClass =   0x00000100,
    Type =          0x00000001,
    Universal =     0x00000000,
    Invalid =       0x00000000
}

const enum Rarity {
    Invalid = 4,
    Id = 3,
    Class = 2,
    Type = 1,
    PseudoClass = 0,
    Attribute = 0,
    Universal = 0,
    Inline = 0
}

interface LookupSorter {
    sortById(id: string, sel: SelectorCore);
    sortByClass(cssClass: string, sel: SelectorCore);
    sortByType(cssType: string, sel: SelectorCore);
    sortAsUniversal(sel: SelectorCore);
}

namespace Match {
    /**
     * Depends on attributes or pseudoclasses state;
     */
    export var Dynamic = true;
    /**
     * Depends only on the tree structure.
     */
    export var Static = false;
}

function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: boolean = false): ClassDecorator {
    return cls => {
        cls.prototype.specificity = specificity;
        cls.prototype.rarity = rarity;
        cls.prototype.combinator = "";
        cls.prototype.dynamic = dynamic;
        return cls;
    }
}

declare type Combinator = "+" | ">" | "~" | " ";
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
export abstract class SelectorCore {
    public specificity: number;
    public rarity: Rarity;
    public combinator: Combinator;
    public ruleset: RuleSet;
    public dynamic: boolean;
    public abstract match(node: Node): boolean;
    /**
     * If the selector is static returns if it matches the node.
     * If the selector is dynamic returns if it may match the node, and accumulates any changes that may affect its state.
     */
    public abstract accumulateChanges(node: Node, map: ChangeAccumulator): boolean;
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortAsUniversal(base || this); }
}

export abstract class SimpleSelector extends SelectorCore {
    public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
        if (!this.dynamic) {
            return this.match(node);
        } else if (this.mayMatch(node)) {
            this.trackChanges(node, map);
            return true;
        }
        return false;
    }
    public mayMatch(node: Node): boolean { return this.match(node); }
    public trackChanges(node: Node, map: ChangeAccumulator): void {
        // No-op, silence the tslint 'block is empty'.
        // Some derived classes (dynamic) will actually fill the map with stuff here, some (static) won't do anything.
    }
}

function wrap(text: string): string {
    return text ? ` ${text} ` : "";
}

@SelectorProperties(Specificity.Invalid, Rarity.Invalid, Match.Static)
export class InvalidSelector extends SimpleSelector {
    constructor(public e: Error) { super(); }
    public toString(): string { return `<error: ${this.e}>`; }
    public match(node: Node): boolean { return false; }
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
        // No-op, silence the tslint 'block is empty'.
        // It feels like tslint has problems with simple polymorphism...
        // This selector is invalid and will never match so we won't bother sorting it to further appear in queries.
    }
}

@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
export class UniversalSelector extends SimpleSelector {
    public toString(): string { return `*${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return true; }
}

@SelectorProperties(Specificity.Id, Rarity.Id, Match.Static)
export class IdSelector extends SimpleSelector {
    constructor(public id: string) { super(); }
    public toString(): string { return `#${this.id}${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return node.id === this.id; }
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortById(this.id, base || this); }
}

@SelectorProperties(Specificity.Type, Rarity.Type, Match.Static)
export class TypeSelector extends SimpleSelector {
    constructor(public cssType: string) { super(); }
    public toString(): string { return `${this.cssType}${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return node.cssType === this.cssType; }
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByType(this.cssType, base || this); }
}

@SelectorProperties(Specificity.Class, Rarity.Class, Match.Static)
export class ClassSelector extends SimpleSelector {
    constructor(public cssClass: string) { super(); }
    public toString(): string { return `.${this.cssClass}${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return node.cssClasses && node.cssClasses.has(this.cssClass); }
    public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByClass(this.cssClass, base || this); }
}

declare type AttributeTest = "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|=";
@SelectorProperties(Specificity.Attribute, Rarity.Attribute, Match.Dynamic)
export class AttributeSelector extends SimpleSelector {
    constructor(public attribute: string, public test?: AttributeTest, public value?: string) {
        super();

        if (!test) {
            // HasAttribute
            this.match = node => !isNullOrUndefined(node[attribute]);
            return;
        }

        if (!value) {
            this.match = node => false;
        }

        let escapedValue = escapeRegexSymbols(value);
        let regexp: RegExp = null;
        switch(test) {
            case "^=": // PrefixMatch
                regexp = new RegExp("^" + escapedValue);
                break;
            case "$=": // SuffixMatch
                regexp = new RegExp(escapedValue + "$");
                break;
            case "*=": // SubstringMatch
                regexp = new RegExp(escapedValue);
                break;
            case "=": // Equals
                regexp = new RegExp("^" + escapedValue + "$");
                break;
            case "~=": // Includes
                if (/\s/.test(value)) {
                    this.match = node => false;
                    return;
                }
                regexp = new RegExp("(^|\\s)" + escapedValue + "(\\s|$)");
                break;
            case "|=": // DashMatch
                regexp = new RegExp("^" + escapedValue + "(-|$)");
                break;
        }

        if (regexp) {
            this.match = node => regexp.test(node[attribute] + "");
            return;
        } else {
            this.match = node => false;
            return;
        }
    }
    public toString(): string { return `[${this.attribute}${wrap(this.test)}${(this.test && this.value) || ''}]${wrap(this.combinator)}`; }
    public match(node: Node): boolean { return false; }
    public mayMatch(node: Node): boolean { return true; }
    public trackChanges(node: Node, map: ChangeAccumulator): void { map.addAttribute(node, this.attribute); }
}

@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic)
{'fraction_non_alphanumeric': 0.1258109825429737, 'fraction_numerical': 0.010701625309343857, 'mean_word_length': 2.7586726998491704, 'pattern_counts': {'":': 12, '<': 5, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 6, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7310313', 'n_tokens_mistral': 4405, 'n_tokens_neox': 4173, 'n_words': 1165}