diff options
Diffstat (limited to 'vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js')
| -rw-r--r-- | vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js | 587 |
1 files changed, 0 insertions, 587 deletions
diff --git a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js deleted file mode 100644 index 6395560..0000000 --- a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js +++ /dev/null @@ -1,587 +0,0 @@ -/** - * matcher.js - */ - -/* import */ -import { generateCSS, parseAstName, unescapeSelector } from './parser.js'; -import { - generateException, - getDirectionality, - getLanguageAttribute, - getType, - isContentEditable, - isCustomElement, - isNamespaceDeclared -} from './utility.js'; - -/* constants */ -import { - ALPHA_NUM, - FORM_PARTS, - IDENT, - INPUT_EDIT, - LANG_PART, - NOT_SUPPORTED_ERR, - PS_ELEMENT_SELECTOR, - STRING, - SYNTAX_ERR -} from './constant.js'; -const KEYS_FORM_PS_DISABLED = new Set([ - ...FORM_PARTS, - 'fieldset', - 'optgroup', - 'option' -]); -const KEYS_INPUT_EDIT = new Set(INPUT_EDIT); -const REG_LANG_VALID = new RegExp(`^(?:\\*-)?${ALPHA_NUM}${LANG_PART}$`, 'i'); -const REG_TAG_NAME = /[A-Z][\\w-]*/i; - -/** - * Validates a pseudo-element selector. - * @param {string} astName - The name of the pseudo-element from the AST. - * @param {string} astType - The type of the selector from the AST. - * @param {object} [opt] - Optional parameters. - * @param {boolean} [opt.forgive] - If true, ignores unknown pseudo-elements. - * @param {boolean} [opt.warn] - If true, throws an error for unsupported ones. - * @throws {DOMException} If the selector is invalid or unsupported. - * @returns {void} - */ -export const matchPseudoElementSelector = (astName, astType, opt = {}) => { - const { forgive, globalObject, warn } = opt; - if (astType !== PS_ELEMENT_SELECTOR) { - // Ensure the AST node is a pseudo-element selector. - throw new TypeError(`Unexpected ast type ${getType(astType)}`); - } - switch (astName) { - case 'after': - case 'backdrop': - case 'before': - case 'cue': - case 'cue-region': - case 'first-letter': - case 'first-line': - case 'file-selector-button': - case 'marker': - case 'placeholder': - case 'selection': - case 'target-text': { - // Warn if the pseudo-element is known but unsupported. - if (warn) { - throw generateException( - `Unsupported pseudo-element ::${astName}`, - NOT_SUPPORTED_ERR, - globalObject - ); - } - break; - } - case 'part': - case 'slotted': { - // Warn if the functional pseudo-element is known but unsupported. - if (warn) { - throw generateException( - `Unsupported pseudo-element ::${astName}()`, - NOT_SUPPORTED_ERR, - globalObject - ); - } - break; - } - default: { - // Handle vendor-prefixed or unknown pseudo-elements. - if (astName.startsWith('-webkit-')) { - if (warn) { - throw generateException( - `Unsupported pseudo-element ::${astName}`, - NOT_SUPPORTED_ERR, - globalObject - ); - } - // Throw an error for unknown pseudo-elements if not forgiven. - } else if (!forgive) { - throw generateException( - `Unknown pseudo-element ::${astName}`, - SYNTAX_ERR, - globalObject - ); - } - } - } -}; - -/** - * Matches the :dir() pseudo-class against an element's directionality. - * @param {object} ast - The AST object for the pseudo-class. - * @param {object} node - The element node to match against. - * @throws {TypeError} If the AST does not contain a valid direction value. - * @returns {boolean} - True if the directionality matches, otherwise false. - */ -export const matchDirectionPseudoClass = (ast, node) => { - const { name } = ast; - // The :dir() pseudo-class requires a direction argument (e.g., "ltr"). - if (!name) { - const type = name === '' ? '(empty String)' : getType(name); - throw new TypeError(`Unexpected ast type ${type}`); - } - // Get the computed directionality of the element. - const dir = getDirectionality(node); - // Compare the expected direction with the element's actual direction. - return name === dir; -}; - -/** - * Matches the :lang() pseudo-class against an element's language. - * @see https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.1 - * @param {object} ast - The AST object for the pseudo-class. - * @param {object} node - The element node to match against. - * @returns {boolean} - True if the language matches, otherwise false. - */ -export const matchLanguagePseudoClass = (ast, node) => { - const { name, type, value } = ast; - let langPattern; - // Determine the language pattern from the AST. - if (type === STRING && value) { - langPattern = value; - } else if (type === IDENT && name) { - langPattern = unescapeSelector(name); - } - // If no valid language pattern is provided, it cannot match. - if (typeof langPattern !== 'string') { - return false; - } - // Get the effective language attribute for the current node. - const elementLang = getLanguageAttribute(node); - // If the element has no language, it cannot match a specific pattern. - if (elementLang === null) { - return false; - } - // Handle the universal selector '*' for :lang. - if (langPattern === '*') { - // It matches any language unless attribute is not empty. - return elementLang !== ''; - } - // Validate the provided language pattern structure. - if (!REG_LANG_VALID.test(langPattern)) { - return false; - } - // Build a regex for extended language range matching. - let matcherRegex; - if (langPattern.indexOf('-') > -1) { - // Handle complex patterns with wildcards and sub-tags (e.g., '*-US'). - const [langMain, langSub, ...langRest] = langPattern.split('-'); - const extendedMain = - langMain === '*' ? `${ALPHA_NUM}${LANG_PART}` : `${langMain}${LANG_PART}`; - const extendedSub = `-${langSub}${LANG_PART}`; - let extendedRest = ''; - // Use a standard for loop for performance as per the rules. - for (let i = 0; i < langRest.length; i++) { - extendedRest += `-${langRest[i]}${LANG_PART}`; - } - matcherRegex = new RegExp( - `^${extendedMain}${extendedSub}${extendedRest}$`, - 'i' - ); - } else { - // Handle simple language patterns (e.g., 'en'). - matcherRegex = new RegExp(`^${langPattern}${LANG_PART}$`, 'i'); - } - // Test the element's language against the constructed regex. - return matcherRegex.test(elementLang); -}; - -/** - * Matches the :disabled and :enabled pseudo-classes. - * @param {string} astName - pseudo-class name - * @param {object} node - Element node - * @returns {boolean} - True if matched - */ -export const matchDisabledPseudoClass = (astName, node) => { - const { localName, parentNode } = node; - if ( - !KEYS_FORM_PS_DISABLED.has(localName) && - !isCustomElement(node, { formAssociated: true }) - ) { - return false; - } - let isDisabled = false; - if (node.disabled || node.hasAttribute('disabled')) { - isDisabled = true; - } else if (localName === 'option') { - if ( - parentNode && - parentNode.localName === 'optgroup' && - (parentNode.disabled || parentNode.hasAttribute('disabled')) - ) { - isDisabled = true; - } - } else if (localName !== 'optgroup') { - let current = parentNode; - while (current) { - if ( - current.localName === 'fieldset' && - (current.disabled || current.hasAttribute('disabled')) - ) { - // The first <legend> in a disabled <fieldset> is not disabled. - let legend; - let element = current.firstElementChild; - while (element) { - if (element.localName === 'legend') { - legend = element; - break; - } - element = element.nextElementSibling; - } - if (!legend || !legend.contains(node)) { - isDisabled = true; - } - // Found the containing fieldset, stop searching up. - break; - } - current = current.parentNode; - } - } - if (astName === 'disabled') { - return isDisabled; - } - return !isDisabled; -}; - -/** - * Match the :read-only and :read-write pseudo-classes - * @param {string} astName - pseudo-class name - * @param {object} node - Element node - * @returns {boolean} - True if matched - */ -export const matchReadOnlyPseudoClass = (astName, node) => { - const { localName } = node; - let isReadOnly = false; - switch (localName) { - case 'textarea': - case 'input': { - const isEditableInput = !node.type || KEYS_INPUT_EDIT.has(node.type); - if (localName === 'textarea' || isEditableInput) { - isReadOnly = - node.readOnly || - node.hasAttribute('readonly') || - node.disabled || - node.hasAttribute('disabled'); - } else { - // Non-editable input types are always read-only - isReadOnly = true; - } - break; - } - default: { - isReadOnly = !isContentEditable(node); - } - } - if (astName === 'read-only') { - return isReadOnly; - } - return !isReadOnly; -}; - -/** - * Matches an attribute selector against an element. - * This function handles various attribute matchers like '=', '~=', '^=', etc., - * and considers namespaces and case sensitivity based on document type. - * @param {object} ast - The AST for the attribute selector. - * @param {object} node - The element node to match against. - * @param {object} [opt] - Optional parameters. - * @param {boolean} [opt.check] - True if running in an internal check. - * @param {boolean} [opt.forgive] - True to forgive certain syntax errors. - * @returns {boolean} - True if the attribute selector matches, otherwise false. - */ -export const matchAttributeSelector = (ast, node, opt = {}) => { - const { - flags: astFlags, - matcher: astMatcher, - name: astName, - value: astValue - } = ast; - const { check, forgive, globalObject } = opt; - // Validate selector flags ('i' or 's'). - if (typeof astFlags === 'string' && !/^[is]$/i.test(astFlags) && !forgive) { - const css = generateCSS(ast); - throw generateException( - `Invalid selector ${css}`, - SYNTAX_ERR, - globalObject - ); - } - const { attributes } = node; - // An element with no attributes cannot match. - if (!attributes || !attributes.length) { - return false; - } - // Determine case sensitivity based on document type and flags. - const contentType = node.ownerDocument.contentType; - let caseInsensitive; - if (contentType === 'text/html') { - if (typeof astFlags === 'string' && /^s$/i.test(astFlags)) { - caseInsensitive = false; - } else { - caseInsensitive = true; - } - } else if (typeof astFlags === 'string' && /^i$/i.test(astFlags)) { - caseInsensitive = true; - } else { - caseInsensitive = false; - } - // Prepare the attribute name from the selector for matching. - let astAttrName = unescapeSelector(astName.name); - if (caseInsensitive) { - astAttrName = astAttrName.toLowerCase(); - } - // A set to store the values of attributes whose names match. - const attrValues = new Set(); - // Handle namespaced attribute names (e.g., [*|attr], [ns|attr]). - if (astAttrName.indexOf('|') > -1) { - const { prefix: astPrefix, localName: astLocalName } = - parseAstName(astAttrName); - for (const item of attributes) { - let { name: itemName, value: itemValue } = item; - if (caseInsensitive) { - itemName = itemName.toLowerCase(); - itemValue = itemValue.toLowerCase(); - } - switch (astPrefix) { - case '': { - if (astLocalName === itemName) { - attrValues.add(itemValue); - } - break; - } - case '*': { - if (itemName.indexOf(':') > -1) { - const [, ...restItemName] = itemName.split(':'); - const itemLocalName = restItemName.join(':').replace(/^:/, ''); - if (itemLocalName === astLocalName) { - attrValues.add(itemValue); - } - } else if (astLocalName === itemName) { - attrValues.add(itemValue); - } - break; - } - default: { - if (!check) { - if (forgive) { - return false; - } - const css = generateCSS(ast); - throw generateException( - `Invalid selector ${css}`, - SYNTAX_ERR, - globalObject - ); - } - if (itemName.indexOf(':') > -1) { - const [itemPrefix, ...restItemName] = itemName.split(':'); - const itemLocalName = restItemName.join(':').replace(/^:/, ''); - // Ignore the 'xml:lang' attribute. - if (itemPrefix === 'xml' && itemLocalName === 'lang') { - continue; - } else if ( - astPrefix === itemPrefix && - astLocalName === itemLocalName - ) { - const namespaceDeclared = isNamespaceDeclared(astPrefix, node); - if (namespaceDeclared) { - attrValues.add(itemValue); - } - } - } - } - } - } - // Handle non-namespaced attribute names. - } else { - for (let { name: itemName, value: itemValue } of attributes) { - if (caseInsensitive) { - itemName = itemName.toLowerCase(); - itemValue = itemValue.toLowerCase(); - } - if (itemName.indexOf(':') > -1) { - const [itemPrefix, ...restItemName] = itemName.split(':'); - const itemLocalName = restItemName.join(':').replace(/^:/, ''); - // The attribute is starting with ':'. - if (!itemPrefix && astAttrName === `:${itemLocalName}`) { - attrValues.add(itemValue); - // Ignore the 'xml:lang' attribute. - } else if (itemPrefix === 'xml' && itemLocalName === 'lang') { - continue; - } else if (astAttrName === itemLocalName) { - attrValues.add(itemValue); - } - } else if (astAttrName === itemName) { - attrValues.add(itemValue); - } - } - } - if (!attrValues.size) { - return false; - } - // Prepare the value from the selector's RHS for comparison. - const { name: astIdentValue, value: astStringValue } = astValue ?? {}; - let attrValue; - if (astIdentValue) { - if (caseInsensitive) { - attrValue = astIdentValue.toLowerCase(); - } else { - attrValue = astIdentValue; - } - } else if (astStringValue) { - if (caseInsensitive) { - attrValue = astStringValue.toLowerCase(); - } else { - attrValue = astStringValue; - } - } else if (astStringValue === '') { - attrValue = astStringValue; - } - // Perform the final match based on the specified matcher. - switch (astMatcher) { - case '=': { - return typeof attrValue === 'string' && attrValues.has(attrValue); - } - case '~=': { - if (attrValue && typeof attrValue === 'string') { - for (const value of attrValues) { - const item = new Set(value.split(/\s+/)); - if (item.has(attrValue)) { - return true; - } - } - } - return false; - } - case '|=': { - if (attrValue && typeof attrValue === 'string') { - for (const value of attrValues) { - if (value === attrValue || value.startsWith(`${attrValue}-`)) { - return true; - } - } - } - return false; - } - case '^=': { - if (attrValue && typeof attrValue === 'string') { - for (const value of attrValues) { - if (value.startsWith(`${attrValue}`)) { - return true; - } - } - } - return false; - } - case '$=': { - if (attrValue && typeof attrValue === 'string') { - for (const value of attrValues) { - if (value.endsWith(`${attrValue}`)) { - return true; - } - } - } - return false; - } - case '*=': { - if (attrValue && typeof attrValue === 'string') { - for (const value of attrValues) { - if (value.includes(`${attrValue}`)) { - return true; - } - } - } - return false; - } - case null: - default: { - // This case handles attribute existence checks (e.g., '[disabled]'). - return true; - } - } -}; - -/** - * match type selector - * @param {object} ast - AST - * @param {object} node - Element node - * @param {object} [opt] - options - * @param {boolean} [opt.check] - running in internal check() - * @param {boolean} [opt.forgive] - forgive undeclared namespace - * @returns {boolean} - result - */ -export const matchTypeSelector = (ast, node, opt = {}) => { - const astName = unescapeSelector(ast.name); - const { localName, namespaceURI, prefix } = node; - const { check, forgive, globalObject } = opt; - let { prefix: astPrefix, localName: astLocalName } = parseAstName( - astName, - node - ); - if ( - node.ownerDocument.contentType === 'text/html' && - (!namespaceURI || namespaceURI === 'http://www.w3.org/1999/xhtml') && - REG_TAG_NAME.test(localName) - ) { - astPrefix = astPrefix.toLowerCase(); - astLocalName = astLocalName.toLowerCase(); - } - let nodePrefix; - let nodeLocalName; - // just in case that the namespaced content is parsed as text/html - if (localName.indexOf(':') > -1) { - [nodePrefix, nodeLocalName] = localName.split(':'); - } else { - nodePrefix = prefix || ''; - nodeLocalName = localName; - } - switch (astPrefix) { - case '': { - if ( - !nodePrefix && - !namespaceURI && - (astLocalName === '*' || astLocalName === nodeLocalName) - ) { - return true; - } - return false; - } - case '*': { - if (astLocalName === '*' || astLocalName === nodeLocalName) { - return true; - } - return false; - } - default: { - if (!check) { - if (forgive) { - return false; - } - const css = generateCSS(ast); - throw generateException( - `Invalid selector ${css}`, - SYNTAX_ERR, - globalObject - ); - } - const astNS = node.lookupNamespaceURI(astPrefix); - const nodeNS = node.lookupNamespaceURI(nodePrefix); - if (astNS === nodeNS && astPrefix === nodePrefix) { - if (astLocalName === '*' || astLocalName === nodeLocalName) { - return true; - } - return false; - } else if (!forgive && !astNS) { - throw generateException( - `Undeclared namespace ${astPrefix}`, - SYNTAX_ERR, - globalObject - ); - } - return false; - } - } -}; |
