/**
* 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