diff options
Diffstat (limited to 'vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js')
| -rw-r--r-- | vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js | 431 |
1 files changed, 431 insertions, 0 deletions
diff --git a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js new file mode 100644 index 0000000..bf06d9f --- /dev/null +++ b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/parser.js @@ -0,0 +1,431 @@ +/** + * parser.js + */ + +/* import */ +import * as cssTree from 'css-tree'; +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, + HEX, + ID_SELECTOR, + KEYS_LOGICAL, + NTH, + PS_CLASS_SELECTOR, + PS_ELEMENT_SELECTOR, + SELECTOR, + SYNTAX_ERR, + TYPE_SELECTOR +} from './constant.js'; +const AST_SORT_ORDER = new Map([ + [PS_ELEMENT_SELECTOR, BIT_01], + [ID_SELECTOR, BIT_02], + [CLASS_SELECTOR, BIT_04], + [TYPE_SELECTOR, BIT_08], + [ATTR_SELECTOR, BIT_16], + [PS_CLASS_SELECTOR, BIT_32] +]); +const KEYS_PS_CLASS_STATE = new Set([ + 'checked', + 'closed', + 'disabled', + 'empty', + 'enabled', + 'in-range', + 'indeterminate', + 'invalid', + 'open', + 'out-of-range', + 'placeholder-shown', + 'read-only', + 'read-write', + 'valid' +]); +const KEYS_SHADOW_HOST = new Set(['host', 'host-context']); +const REG_EMPTY_PS_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'; + +/** + * Unescapes a CSS selector string. + * @param {string} selector - The CSS selector to unescape. + * @returns {string} The unescaped selector string. + */ +export const unescapeSelector = (selector = '') => { + if (typeof selector === 'string' && selector.indexOf('\\', 0) >= 0) { + const arr = selector.split('\\'); + const selectorItems = [arr[0]]; + const l = arr.length; + for (let i = 1; i < l; i++) { + const item = arr[i]; + if (item === '' && i === l - 1) { + selectorItems.push(U_FFFD); + } else { + const hexExists = /^([\da-f]{1,6}\s?)/i.exec(item); + if (hexExists) { + const [, hex] = hexExists; + let str; + try { + const low = parseInt('D800', HEX); + const high = parseInt('DFFF', HEX); + const deci = parseInt(hex, HEX); + if (deci === 0 || (deci >= low && deci <= high)) { + str = U_FFFD; + } else { + str = String.fromCodePoint(deci); + } + } catch (e) { + str = U_FFFD; + } + let postStr = ''; + if (item.length > hex.length) { + postStr = item.substring(hex.length); + } + selectorItems.push(`${str}${postStr}`); + // whitespace + } else if (/^[\n\r\f]/.test(item)) { + selectorItems.push(`\\${item}`); + } else { + selectorItems.push(item); + } + } + } + return selectorItems.join(''); + } + return selector; +}; + +/** + * Preprocesses a selector string according to the specification. + * @see https://drafts.csswg.org/css-syntax-3/#input-preprocessing + * @param {string} value - The value to preprocess. + * @returns {string} The preprocessed selector string. + */ +export const preprocess = value => { + // Non-string values will be converted to string. + if (typeof value !== 'string') { + if (value === undefined || value === null) { + return getType(value).toLowerCase(); + } else if (Array.isArray(value)) { + return value.join(','); + } else if (Object.hasOwn(value, 'toString')) { + return value.toString(); + } else { + throw new DOMException(`Invalid selector ${value}`, SYNTAX_ERR); + } + } + let selector = value; + let index = 0; + while (index >= 0) { + // @see https://drafts.csswg.org/selectors/#id-selectors + index = selector.indexOf('#', index); + if (index < 0) { + break; + } + const preHash = selector.substring(0, index + 1); + let postHash = selector.substring(index + 1); + const codePoint = postHash.codePointAt(0); + if (codePoint > BIT_FFFF) { + const str = `\\${codePoint.toString(HEX)} `; + if (postHash.length === DUO) { + postHash = str; + } else { + postHash = `${str}${postHash.substring(DUO)}`; + } + } + selector = `${preHash}${postHash}`; + index++; + } + return selector + .replace(/\f|\r\n?/g, '\n') + .replace(/[\0\uD800-\uDFFF]|\\$/g, U_FFFD) + .replace(/\x26/g, ':scope'); +}; + +/** + * Creates an Abstract Syntax Tree (AST) from a CSS selector string. + * @param {string} sel - The CSS selector string. + * @returns {object} The parsed AST object. + */ +export const parseSelector = sel => { + const selector = preprocess(sel); + // invalid selectors + if (/^$|^\s*>|,\s*$/.test(selector)) { + throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR); + } + try { + const ast = cssTree.parse(selector, { + context: 'selectorList', + parseCustomProperty: true + }); + return cssTree.toPlainObject(ast); + } catch (e) { + const { message } = e; + if ( + /^(?:"\]"|Attribute selector [()\s,=~^$*|]+) is expected$/.test( + message + ) && + !selector.endsWith(']') + ) { + const index = selector.lastIndexOf('['); + const selPart = selector.substring(index); + if (selPart.includes('"')) { + const quotes = selPart.match(/"/g).length; + if (quotes % 2) { + return parseSelector(`${selector}"]`); + } + return parseSelector(`${selector}]`); + } + return parseSelector(`${selector}]`); + } else if (message === '")" is expected') { + // workaround for https://github.com/csstree/csstree/issues/283 + if (REG_EMPTY_PS_FUNC.test(selector)) { + return parseSelector(`${selector.replaceAll(REG_EMPTY_PS_FUNC, '()')}`); + } else if (!selector.endsWith(')')) { + return parseSelector(`${selector})`); + } else { + throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR); + } + } else { + throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR); + } + } +}; + +/** + * Walks the provided AST to collect selector branches and gather information + * about its contents. + * @param {object} ast - The AST to traverse. + * @returns {{branches: Array<object>, info: object}} An object containing the selector branches and info. + */ +export const walkAST = (ast = {}) => { + const branches = new Set(); + const info = { + hasForgivenPseudoFunc: false, + hasHasPseudoFunc: false, + hasLogicalPseudoFunc: false, + hasNotPseudoFunc: false, + hasNthChildOfSelector: false, + hasNestedSelector: false, + hasStatePseudoClass: false + }; + const opt = { + enter(node) { + switch (node.type) { + case CLASS_SELECTOR: { + if (/^-?\d/.test(node.name)) { + throw new DOMException( + `Invalid selector .${node.name}`, + SYNTAX_ERR + ); + } + break; + } + case ID_SELECTOR: { + if (/^-?\d/.test(node.name)) { + throw new DOMException( + `Invalid selector #${node.name}`, + SYNTAX_ERR + ); + } + break; + } + case PS_CLASS_SELECTOR: { + if (KEYS_LOGICAL.has(node.name)) { + info.hasNestedSelector = true; + info.hasLogicalPseudoFunc = true; + if (node.name === 'has') { + info.hasHasPseudoFunc = true; + } else if (node.name === 'not') { + info.hasNotPseudoFunc = true; + } else { + info.hasForgivenPseudoFunc = true; + } + } else if (KEYS_PS_CLASS_STATE.has(node.name)) { + info.hasStatePseudoClass = true; + } else if ( + KEYS_SHADOW_HOST.has(node.name) && + Array.isArray(node.children) && + node.children.length + ) { + info.hasNestedSelector = true; + } + break; + } + case PS_ELEMENT_SELECTOR: { + if (REG_SHADOW_PS_ELEMENT.test(node.name)) { + info.hasNestedSelector = true; + } + break; + } + case NTH: { + if (node.selector) { + info.hasNestedSelector = true; + info.hasNthChildOfSelector = true; + } + break; + } + case SELECTOR: { + branches.add(node.children); + break; + } + default: + } + } + }; + cssTree.walk(ast, opt); + if (info.hasNestedSelector === true) { + cssTree.findAll(ast, (node, item, list) => { + if (list) { + if (node.type === PS_CLASS_SELECTOR && KEYS_LOGICAL.has(node.name)) { + const itemList = list.filter(i => { + const { name, type } = i; + return type === PS_CLASS_SELECTOR && KEYS_LOGICAL.has(name); + }); + for (const { children } of itemList) { + // SelectorList + for (const { children: grandChildren } of children) { + // Selector + for (const { children: greatGrandChildren } of grandChildren) { + if (branches.has(greatGrandChildren)) { + branches.delete(greatGrandChildren); + } + } + } + } + } else if ( + node.type === PS_CLASS_SELECTOR && + KEYS_SHADOW_HOST.has(node.name) && + Array.isArray(node.children) && + node.children.length + ) { + const itemList = list.filter(i => { + const { children, name, type } = i; + const res = + type === PS_CLASS_SELECTOR && + KEYS_SHADOW_HOST.has(name) && + Array.isArray(children) && + children.length; + return res; + }); + for (const { children } of itemList) { + // Selector + for (const { children: grandChildren } of children) { + if (branches.has(grandChildren)) { + branches.delete(grandChildren); + } + } + } + } else if ( + node.type === PS_ELEMENT_SELECTOR && + REG_SHADOW_PS_ELEMENT.test(node.name) + ) { + const itemList = list.filter(i => { + const { name, type } = i; + const res = + type === PS_ELEMENT_SELECTOR && REG_SHADOW_PS_ELEMENT.test(name); + return res; + }); + for (const { children } of itemList) { + // Selector + for (const { children: grandChildren } of children) { + if (branches.has(grandChildren)) { + branches.delete(grandChildren); + } + } + } + } else if (node.type === NTH && node.selector) { + const itemList = list.filter(i => { + const { selector, type } = i; + const res = type === NTH && selector; + return res; + }); + for (const { selector } of itemList) { + const { children } = selector; + // Selector + for (const { children: grandChildren } of children) { + if (branches.has(grandChildren)) { + branches.delete(grandChildren); + } + } + } + } + } + }); + } + return { + info, + branches: [...branches] + }; +}; + +/** + * Comparison function for sorting AST nodes based on specificity. + * @param {object} a - The first AST node. + * @param {object} b - The second AST node. + * @returns {number} -1, 0 or 1, depending on the sort order. + */ +export const compareASTNodes = (a, b) => { + const bitA = AST_SORT_ORDER.get(a.type); + const bitB = AST_SORT_ORDER.get(b.type); + if (bitA === bitB) { + return 0; + } else if (bitA > bitB) { + return 1; + } else { + return -1; + } +}; + +/** + * Sorts a collection of AST nodes based on CSS specificity rules. + * @param {Array<object>} asts - A collection of AST nodes to sort. + * @returns {Array<object>} A new array containing the sorted AST nodes. + */ +export const sortAST = asts => { + const arr = [...asts]; + if (arr.length > 1) { + arr.sort(compareASTNodes); + } + return arr; +}; + +/** + * Parses a type selector's name, which may include a namespace prefix. + * @param {string} selector - The type selector name (e.g., 'ns|E' or 'E'). + * @returns {{prefix: string, localName: string}} An object with `prefix` and + * `localName` properties. + */ +export const parseAstName = selector => { + let prefix; + let localName; + if (selector && typeof selector === 'string') { + if (selector.indexOf('|') > -1) { + [prefix, localName] = selector.split('|'); + } else { + prefix = '*'; + localName = selector; + } + } else { + throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR); + } + return { + prefix, + localName + }; +}; + +/* Re-exported from css-tree. */ +export { find as findAST, generate as generateCSS } from 'css-tree'; |
