Skip to content

Commit

Permalink
Update css-tree (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
asamuzaK authored Sep 14, 2024
1 parent 348fae3 commit 59f0662
Show file tree
Hide file tree
Showing 10 changed files with 959 additions and 333 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Returns **[Array][62]<([object][60] \| [undefined][63])>** array of matched n
|E\[foo\|="en"\]|| |
|E:defined|Partially supported|Matching with MathML is not yet supported.|
|E:dir(ltr)|| |
|E:lang(en)|Partially supported|Comma-separated list of language codes, e.g. `:lang(en, fr)`, is not yet supported.|
|E:lang(en)|| |
|E:any‑link|| |
|E:link|| |
|E:visited||Returns `false` or `null` to prevent fingerprinting.|
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"dependencies": {
"@asamuzakjp/nwsapi": "^2.2.16",
"bidi-js": "^1.0.3",
"css-tree": "^2.3.1",
"css-tree": "^3.0.0",
"is-potential-custom-element-name": "^1.0.1"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion src/js/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
export const ATTR_SELECTOR = 'AttributeSelector';
export const CLASS_SELECTOR = 'ClassSelector';
export const COMBINATOR = 'Combinator';
export const EMPTY = '__EMPTY__';
export const IDENT = 'Identifier';
export const ID_SELECTOR = 'IdSelector';
export const NOT_SUPPORTED_ERR = 'NotSupportedError';
export const NTH = 'Nth';
export const OPERATOR = 'Operator';
export const PS_CLASS_SELECTOR = 'PseudoClassSelector';
export const PS_ELEMENT_SELECTOR = 'PseudoElementSelector';
export const SELECTOR = 'Selector';
export const STRING = 'String';
export const SYNTAX_ERR = 'SyntaxError';
export const TARGET_ALL = 'all';
export const TARGET_FIRST = 'first';
Expand Down
53 changes: 36 additions & 17 deletions src/js/finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import {
/* constants */
import {
ATTR_SELECTOR, BIT_01, CLASS_SELECTOR, COMBINATOR, DOCUMENT_FRAGMENT_NODE,
DOCUMENT_NODE, ELEMENT_NODE, EMPTY, ID_SELECTOR, KEY_FORM_FOCUS,
KEY_INPUT_DATE, KEY_INPUT_EDIT, KEY_INPUT_TEXT, KEY_LOGICAL,
NOT_SUPPORTED_ERR, PS_CLASS_SELECTOR, PS_ELEMENT_SELECTOR, SHOW_ALL,
SYNTAX_ERR, TARGET_ALL, TARGET_FIRST, TARGET_LINEAL, TARGET_SELF, TEXT_NODE,
TYPE_SELECTOR, WALKER_FILTER
DOCUMENT_NODE, ELEMENT_NODE, ID_SELECTOR, KEY_FORM_FOCUS, KEY_INPUT_DATE,
KEY_INPUT_EDIT, KEY_INPUT_TEXT, KEY_LOGICAL, NOT_SUPPORTED_ERR,
PS_CLASS_SELECTOR, PS_ELEMENT_SELECTOR, SHOW_ALL, SYNTAX_ERR, TARGET_ALL,
TARGET_FIRST, TARGET_LINEAL, TARGET_SELF, TEXT_NODE, TYPE_SELECTOR,
WALKER_FILTER
} from './constant.js';
const DIR_NEXT = 'next';
const DIR_PREV = 'prev';
Expand Down Expand Up @@ -836,7 +836,11 @@ export class Finder {
} = opt;
const matched = new Set();
// :has(), :is(), :not(), :where()
if (KEY_LOGICAL.includes(astName)) {
if (Array.isArray(astChildren) && KEY_LOGICAL.includes(astName)) {
if (!astChildren.length && astName !== 'is' && astName !== 'where') {
const css = generateCSS(ast);
throw new DOMException(`Invalid selector ${css}`, SYNTAX_ERR);
}
let astData;
if (this.#astCache.has(ast)) {
astData = this.#astCache.get(ast);
Expand Down Expand Up @@ -911,26 +915,43 @@ export class Finder {
} else if (Array.isArray(astChildren)) {
// :nth-child(), :nth-last-child(), nth-of-type(), :nth-last-of-type()
if (/^nth-(?:last-)?(?:child|of-type)$/.test(astName)) {
if (astChildren.length !== 1) {
const css = generateCSS(ast);
throw new DOMException(`Invalid selector ${css}`, SYNTAX_ERR);
}
const [branch] = astChildren;
const nodes = this._matchAnPlusB(branch, node, astName, opt);
return nodes;
} else {
switch (astName) {
// :dir()
case 'dir': {
if (astChildren.length !== 1) {
const css = generateCSS(ast);
throw new DOMException(`Invalid selector ${css}`, SYNTAX_ERR);
}
const [astChild] = astChildren;
const res = matchDirectionPseudoClass(astChild, node);
if (res) {
matched.add(res);
matched.add(node);
}
break;
}
// :lang()
case 'lang': {
const [astChild] = astChildren;
const res = matchLanguagePseudoClass(astChild, node, opt);
if (res) {
matched.add(res);
if (!astChildren.length) {
const css = generateCSS(ast);
throw new DOMException(`Invalid selector ${css}`, SYNTAX_ERR);
}
let bool;
for (const astChild of astChildren) {
bool = matchLanguagePseudoClass(astChild, node);
if (bool) {
break;
}
}
if (bool) {
matched.add(node);
}
break;
}
Expand Down Expand Up @@ -1671,6 +1692,10 @@ export class Finder {
const { children: astChildren, name: astName } = ast;
let res;
if (Array.isArray(astChildren)) {
if (astChildren.length !== 1) {
const css = generateCSS(ast);
throw new DOMException(`Invalid selector ${css}`, SYNTAX_ERR);
}
const { branches } = walkAST(astChildren[0]);
const [branch] = branches;
const [...leaves] = branch;
Expand Down Expand Up @@ -1735,13 +1760,7 @@ export class Finder {
_matchSelector(ast, node, opt = {}) {
const { type: astType } = ast;
const matched = new Set();
if (ast.name === EMPTY) {
return matched;
}
const astName = unescapeSelector(ast.name);
if (typeof astName === 'string' && astName !== ast.name) {
ast.name = astName;
}
if (node.nodeType === ELEMENT_NODE) {
switch (astType) {
case ATTR_SELECTOR: {
Expand Down
51 changes: 27 additions & 24 deletions src/js/matcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { getDirectionality, getType, isNamespaceDeclared } from './utility.js';

/* constants */
import {
ALPHA_NUM, ELEMENT_NODE, EMPTY, LANG_PART, NOT_SUPPORTED_ERR,
PS_ELEMENT_SELECTOR, SYNTAX_ERR
ALPHA_NUM, ELEMENT_NODE, IDENT, LANG_PART, NOT_SUPPORTED_ERR,
PS_ELEMENT_SELECTOR, STRING, SYNTAX_ERR
} from './constant.js';

/**
Expand Down Expand Up @@ -73,45 +73,53 @@ export const matchPseudoElementSelector = (astName, astType, opt = {}) => {
* match directionality pseudo-class - :dir()
* @param {object} ast - AST
* @param {object} node - Element node
* @returns {?object} - matched node
* @returns {boolean} - result
*/
export const matchDirectionPseudoClass = (ast, node) => {
const dir = getDirectionality(node);
let res;
if (ast.name === dir) {
res = node;
if (!ast.name) {
let type;
if (ast.name === '') {
type = '(empty String)';
} else {
type = getType(ast.name);
}
throw new TypeError(`Unexpected ast type ${type}`);
}
return res ?? null;
const dir = getDirectionality(node);
return ast.name === dir;
};

/**
* match language pseudo-class - :lang()
* @see https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.1
* @param {object} ast - AST
* @param {object} node - Element node
* @returns {?object} - matched node
* @returns {boolean} - result
*/
export const matchLanguagePseudoClass = (ast, node) => {
if (ast.name === EMPTY) {
return null;
const { name, type, value } = ast;
let astName;
if (type === STRING && value) {
astName = value;
} else if (type === IDENT && name) {
astName = unescapeSelector(name);
}
const astName = unescapeSelector(ast.name);
if (typeof astName === 'string' && astName !== ast.name) {
ast.name = astName;
if (!astName) {
return false;
}
let res;
if (astName === '*') {
if (node.hasAttribute('lang')) {
if (node.getAttribute('lang')) {
res = node;
res = true;
}
} else {
let parent = node.parentNode;
while (parent) {
if (parent.nodeType === ELEMENT_NODE) {
if (parent.hasAttribute('lang')) {
if (parent.getAttribute('lang')) {
res = node;
res = true;
}
break;
}
Expand Down Expand Up @@ -147,18 +155,13 @@ export const matchLanguagePseudoClass = (ast, node) => {
regExtendedLang = new RegExp(`^${astName}${LANG_PART}$`, 'i');
}
if (node.hasAttribute('lang')) {
if (regExtendedLang.test(node.getAttribute('lang'))) {
res = node;
}
res = regExtendedLang.test(node.getAttribute('lang'));
} else {
let parent = node.parentNode;
while (parent) {
if (parent.nodeType === ELEMENT_NODE) {
if (parent.hasAttribute('lang')) {
const value = parent.getAttribute('lang');
if (regExtendedLang.test(value)) {
res = node;
}
res = regExtendedLang.test(parent.getAttribute('lang'));
break;
}
parent = parent.parentNode;
Expand All @@ -169,7 +172,7 @@ export const matchLanguagePseudoClass = (ast, node) => {
}
}
}
return res ?? null;
return !!res;
};

/**
Expand Down
44 changes: 18 additions & 26 deletions src/js/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import { getType } from './utility.js';
/* constants */
import {
ATTR_SELECTOR, BIT_01, BIT_02, BIT_04, BIT_08, BIT_16, BIT_32, BIT_FFFF,
CLASS_SELECTOR, DUO, EMPTY, HEX, HYPHEN, ID_SELECTOR, KEY_LOGICAL, NTH,
CLASS_SELECTOR, DUO, HEX, HYPHEN, ID_SELECTOR, KEY_LOGICAL, NTH,
PS_CLASS_SELECTOR, PS_ELEMENT_SELECTOR, SELECTOR, SYNTAX_ERR, TYPE_SELECTOR
} from './constant.js';
const REG_LANG_QUOTED = /(:lang\(\s*("[A-Za-z\d\-*]*")\s*\))/;
const REG_LOGICAL_EMPTY = /(:(is|where)\(\s*\))/;
const REG_SHADOW_PSEUDO = /^part|slotted$/;
const REG_EMPTY_PSEUDO_FUNC = /(?<=:(?:dir|has|host(?:-context)?|is|lang|not|nth-(?:last-)?(?:child|of-type)|where)\()\s+\)/g;
const REG_SHADOW_PS_ELEMENT = /^part|slotted$/;
const U_FFFD = '\uFFFD';

/**
Expand Down Expand Up @@ -140,23 +139,8 @@ export const parseSelector = selector => {
res = toPlainObject(ast);
} catch (e) {
const { message } = e;
// workaround for https://github.com/csstree/csstree/issues/265
if (message === 'Identifier is expected' &&
REG_LANG_QUOTED.test(selector)) {
const [, lang, range] = REG_LANG_QUOTED.exec(selector);
const escapedRange =
range.replaceAll('*', '\\*').replace(/^"/, '').replace(/"$/, '');
let escapedLang = lang.replace(range, escapedRange);
if (escapedLang === ':lang()') {
escapedLang = `:lang(${EMPTY})`;
}
res = parseSelector(selector.replace(lang, escapedLang));
} else if (/^(?:Identifier|Selector) is expected$/.test(message) &&
REG_LOGICAL_EMPTY.test(selector)) {
const [, sel, name] = REG_LOGICAL_EMPTY.exec(selector);
res = parseSelector(selector.replace(sel, `:${name}(${EMPTY})`));
} else if (/^(?:"\]"|Attribute selector [()\s,=~^$*|]+) is expected$/.test(message) &&
!selector.endsWith(']')) {
if (/^(?:"\]"|Attribute selector [()\s,=~^$*|]+) is expected$/.test(message) &&
!selector.endsWith(']')) {
const index = selector.lastIndexOf('[');
const sel = selector.substring(index);
if (sel.includes('"')) {
Expand All @@ -169,8 +153,16 @@ export const parseSelector = selector => {
} else {
res = parseSelector(`${selector}]`);
}
} else if (message === '")" is expected' && !selector.endsWith(')')) {
res = parseSelector(`${selector})`);
} else if (message === '")" is expected') {
// workaround for https://github.com/csstree/csstree/issues/283
if (REG_EMPTY_PSEUDO_FUNC.test(selector)) {
res =
parseSelector(`${selector.replaceAll(REG_EMPTY_PSEUDO_FUNC, ')')}`);
} else if (!selector.endsWith(')')) {
res = parseSelector(`${selector})`);
} else {
throw new DOMException(message, SYNTAX_ERR);
}
} else {
throw new DOMException(message, SYNTAX_ERR);
}
Expand Down Expand Up @@ -204,7 +196,7 @@ export const walkAST = (ast = {}) => {
break;
}
case PS_ELEMENT_SELECTOR: {
if (REG_SHADOW_PSEUDO.test(node.name)) {
if (REG_SHADOW_PS_ELEMENT.test(node.name)) {
info.set('hasNestedSelector', true);
}
break;
Expand Down Expand Up @@ -242,11 +234,11 @@ export const walkAST = (ast = {}) => {
}
}
} else if (node.type === PS_ELEMENT_SELECTOR &&
REG_SHADOW_PSEUDO.test(node.name)) {
REG_SHADOW_PS_ELEMENT.test(node.name)) {
const itemList = list.filter(i => {
const { name, type } = i;
const res =
type === PS_ELEMENT_SELECTOR && REG_SHADOW_PSEUDO.test(name);
type === PS_ELEMENT_SELECTOR && REG_SHADOW_PS_ELEMENT.test(name);
return res;
});
for (const { children } of itemList) {
Expand Down
Loading

0 comments on commit 59f0662

Please sign in to comment.