Complete Email Sortierer implementation with Appwrite and Stripe integration
This commit is contained in:
353
server/node_modules/@asamuzakjp/dom-selector/src/index.js
generated
vendored
Normal file
353
server/node_modules/@asamuzakjp/dom-selector/src/index.js
generated
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
/*!
|
||||
* DOM Selector - A CSS selector engine.
|
||||
* @license MIT
|
||||
* @copyright asamuzaK (Kazz)
|
||||
* @see {@link https://github.com/asamuzaK/domSelector/blob/main/LICENSE}
|
||||
*/
|
||||
|
||||
/* import */
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { Finder } from './js/finder.js';
|
||||
import { filterSelector, getType, initNwsapi } from './js/utility.js';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
DOCUMENT_NODE,
|
||||
DOCUMENT_FRAGMENT_NODE,
|
||||
ELEMENT_NODE,
|
||||
TARGET_ALL,
|
||||
TARGET_FIRST,
|
||||
TARGET_LINEAL,
|
||||
TARGET_SELF
|
||||
} from './js/constant.js';
|
||||
const MAX_CACHE = 1024;
|
||||
|
||||
/**
|
||||
* @typedef {object} CheckResult
|
||||
* @property {boolean} match - The match result.
|
||||
* @property {string?} pseudoElement - The pseudo-element, if any.
|
||||
*/
|
||||
|
||||
/* DOMSelector */
|
||||
export class DOMSelector {
|
||||
/* private fields */
|
||||
#window;
|
||||
#document;
|
||||
#finder;
|
||||
#idlUtils;
|
||||
#nwsapi;
|
||||
#cache;
|
||||
|
||||
/**
|
||||
* Creates an instance of DOMSelector.
|
||||
* @param {Window} window - The window object.
|
||||
* @param {Document} document - The document object.
|
||||
* @param {object} [opt] - Options.
|
||||
*/
|
||||
constructor(window, document, opt = {}) {
|
||||
const { idlUtils } = opt;
|
||||
this.#window = window;
|
||||
this.#document = document ?? window.document;
|
||||
this.#finder = new Finder(window);
|
||||
this.#idlUtils = idlUtils;
|
||||
this.#nwsapi = initNwsapi(window, document);
|
||||
this.#cache = new LRUCache({
|
||||
max: MAX_CACHE
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the internal cache of finder results.
|
||||
* @returns {void}
|
||||
*/
|
||||
clear = () => {
|
||||
this.#finder.clearResults(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an element matches a CSS selector.
|
||||
* @param {string} selector - The CSS selector to check against.
|
||||
* @param {Element} node - The element node to check.
|
||||
* @param {object} [opt] - Optional parameters.
|
||||
* @returns {CheckResult} An object containing the check result.
|
||||
*/
|
||||
check = (selector, node, opt = {}) => {
|
||||
if (!node?.nodeType) {
|
||||
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
|
||||
return this.#finder.onError(e, opt);
|
||||
} else if (node.nodeType !== ELEMENT_NODE) {
|
||||
const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
|
||||
return this.#finder.onError(e, opt);
|
||||
}
|
||||
const document = node.ownerDocument;
|
||||
if (
|
||||
document === this.#document &&
|
||||
document.contentType === 'text/html' &&
|
||||
document.documentElement &&
|
||||
node.parentNode
|
||||
) {
|
||||
const cacheKey = `check_${selector}`;
|
||||
let filterMatches = false;
|
||||
if (this.#cache.has(cacheKey)) {
|
||||
filterMatches = this.#cache.get(cacheKey);
|
||||
} else {
|
||||
filterMatches = filterSelector(selector, TARGET_SELF);
|
||||
this.#cache.set(cacheKey, filterMatches);
|
||||
}
|
||||
if (filterMatches) {
|
||||
try {
|
||||
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
|
||||
const match = this.#nwsapi.match(selector, n);
|
||||
return {
|
||||
match,
|
||||
pseudoElement: null
|
||||
};
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
if (this.#idlUtils) {
|
||||
node = this.#idlUtils.wrapperForImpl(node);
|
||||
}
|
||||
opt.check = true;
|
||||
opt.noexept = true;
|
||||
opt.warn = false;
|
||||
this.#finder.setup(selector, node, opt);
|
||||
res = this.#finder.find(TARGET_SELF);
|
||||
} catch (e) {
|
||||
this.#finder.onError(e, opt);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the element matches the selector.
|
||||
* @param {string} selector - The CSS selector to match against.
|
||||
* @param {Element} node - The element node to test.
|
||||
* @param {object} [opt] - Optional parameters.
|
||||
* @returns {boolean} `true` if the element matches, or `false` otherwise.
|
||||
*/
|
||||
matches = (selector, node, opt = {}) => {
|
||||
if (!node?.nodeType) {
|
||||
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
|
||||
return this.#finder.onError(e, opt);
|
||||
} else if (node.nodeType !== ELEMENT_NODE) {
|
||||
const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
|
||||
return this.#finder.onError(e, opt);
|
||||
}
|
||||
const document = node.ownerDocument;
|
||||
if (
|
||||
document === this.#document &&
|
||||
document.contentType === 'text/html' &&
|
||||
document.documentElement &&
|
||||
node.parentNode
|
||||
) {
|
||||
const cacheKey = `matches_${selector}`;
|
||||
let filterMatches = false;
|
||||
if (this.#cache.has(cacheKey)) {
|
||||
filterMatches = this.#cache.get(cacheKey);
|
||||
} else {
|
||||
filterMatches = filterSelector(selector, TARGET_SELF);
|
||||
this.#cache.set(cacheKey, filterMatches);
|
||||
}
|
||||
if (filterMatches) {
|
||||
try {
|
||||
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
|
||||
const res = this.#nwsapi.match(selector, n);
|
||||
return res;
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
if (this.#idlUtils) {
|
||||
node = this.#idlUtils.wrapperForImpl(node);
|
||||
}
|
||||
this.#finder.setup(selector, node, opt);
|
||||
const nodes = this.#finder.find(TARGET_SELF);
|
||||
res = nodes.size;
|
||||
} catch (e) {
|
||||
this.#finder.onError(e, opt);
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Traverses up the DOM tree to find the first node that matches the selector.
|
||||
* @param {string} selector - The CSS selector to match against.
|
||||
* @param {Element} node - The element from which to start traversing.
|
||||
* @param {object} [opt] - Optional parameters.
|
||||
* @returns {?Element} The first matching ancestor element, or `null`.
|
||||
*/
|
||||
closest = (selector, node, opt = {}) => {
|
||||
if (!node?.nodeType) {
|
||||
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
|
||||
return this.#finder.onError(e, opt);
|
||||
} else if (node.nodeType !== ELEMENT_NODE) {
|
||||
const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
|
||||
return this.#finder.onError(e, opt);
|
||||
}
|
||||
const document = node.ownerDocument;
|
||||
if (
|
||||
document === this.#document &&
|
||||
document.contentType === 'text/html' &&
|
||||
document.documentElement &&
|
||||
node.parentNode
|
||||
) {
|
||||
const cacheKey = `closest_${selector}`;
|
||||
let filterMatches = false;
|
||||
if (this.#cache.has(cacheKey)) {
|
||||
filterMatches = this.#cache.get(cacheKey);
|
||||
} else {
|
||||
filterMatches = filterSelector(selector, TARGET_LINEAL);
|
||||
this.#cache.set(cacheKey, filterMatches);
|
||||
}
|
||||
if (filterMatches) {
|
||||
try {
|
||||
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
|
||||
const res = this.#nwsapi.closest(selector, n);
|
||||
return res;
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
if (this.#idlUtils) {
|
||||
node = this.#idlUtils.wrapperForImpl(node);
|
||||
}
|
||||
this.#finder.setup(selector, node, opt);
|
||||
const nodes = this.#finder.find(TARGET_LINEAL);
|
||||
if (nodes.size) {
|
||||
let refNode = node;
|
||||
while (refNode) {
|
||||
if (nodes.has(refNode)) {
|
||||
res = refNode;
|
||||
break;
|
||||
}
|
||||
refNode = refNode.parentNode;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.#finder.onError(e, opt);
|
||||
}
|
||||
return res ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the first element within the subtree that matches the selector.
|
||||
* @param {string} selector - The CSS selector to match.
|
||||
* @param {Document|DocumentFragment|Element} node - The node to find within.
|
||||
* @param {object} [opt] - Optional parameters.
|
||||
* @returns {?Element} The first matching element, or `null`.
|
||||
*/
|
||||
querySelector = (selector, node, opt = {}) => {
|
||||
if (!node?.nodeType) {
|
||||
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
|
||||
return this.#finder.onError(e, opt);
|
||||
}
|
||||
/*
|
||||
const document =
|
||||
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
|
||||
if (
|
||||
document === this.#document &&
|
||||
document.contentType === 'text/html' &&
|
||||
document.documentElement &&
|
||||
(node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
|
||||
) {
|
||||
const cacheKey = `querySelector_${selector}`;
|
||||
let filterMatches = false;
|
||||
if (this.#cache.has(cacheKey)) {
|
||||
filterMatches = this.#cache.get(cacheKey);
|
||||
} else {
|
||||
filterMatches = filterSelector(selector, TARGET_FIRST);
|
||||
this.#cache.set(cacheKey, filterMatches);
|
||||
}
|
||||
if (filterMatches) {
|
||||
try {
|
||||
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
|
||||
const res = this.#nwsapi.first(selector, n);
|
||||
return res;
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
let res;
|
||||
try {
|
||||
if (this.#idlUtils) {
|
||||
node = this.#idlUtils.wrapperForImpl(node);
|
||||
}
|
||||
this.#finder.setup(selector, node, opt);
|
||||
const nodes = this.#finder.find(TARGET_FIRST);
|
||||
if (nodes.size) {
|
||||
[res] = [...nodes];
|
||||
}
|
||||
} catch (e) {
|
||||
this.#finder.onError(e, opt);
|
||||
}
|
||||
return res ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of elements within the subtree that match the selector.
|
||||
* Note: This method returns an Array, not a NodeList.
|
||||
* @param {string} selector - The CSS selector to match.
|
||||
* @param {Document|DocumentFragment|Element} node - The node to find within.
|
||||
* @param {object} [opt] - Optional parameters.
|
||||
* @returns {Array<Element>} An array of elements, or an empty array.
|
||||
*/
|
||||
querySelectorAll = (selector, node, opt = {}) => {
|
||||
if (!node?.nodeType) {
|
||||
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
|
||||
return this.#finder.onError(e, opt);
|
||||
}
|
||||
const document =
|
||||
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
|
||||
if (
|
||||
document === this.#document &&
|
||||
document.contentType === 'text/html' &&
|
||||
document.documentElement &&
|
||||
(node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
|
||||
) {
|
||||
const cacheKey = `querySelectorAll_${selector}`;
|
||||
let filterMatches = false;
|
||||
if (this.#cache.has(cacheKey)) {
|
||||
filterMatches = this.#cache.get(cacheKey);
|
||||
} else {
|
||||
filterMatches = filterSelector(selector, TARGET_ALL);
|
||||
this.#cache.set(cacheKey, filterMatches);
|
||||
}
|
||||
if (filterMatches) {
|
||||
try {
|
||||
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
|
||||
const res = this.#nwsapi.select(selector, n);
|
||||
return res;
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
if (this.#idlUtils) {
|
||||
node = this.#idlUtils.wrapperForImpl(node);
|
||||
}
|
||||
this.#finder.setup(selector, node, opt);
|
||||
const nodes = this.#finder.find(TARGET_ALL);
|
||||
if (nodes.size) {
|
||||
res = [...nodes];
|
||||
}
|
||||
} catch (e) {
|
||||
this.#finder.onError(e, opt);
|
||||
}
|
||||
return res ?? [];
|
||||
};
|
||||
}
|
||||
129
server/node_modules/@asamuzakjp/dom-selector/src/js/constant.js
generated
vendored
Normal file
129
server/node_modules/@asamuzakjp/dom-selector/src/js/constant.js
generated
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* constant.js
|
||||
*/
|
||||
|
||||
/* string */
|
||||
export const ATRULE = 'Atrule';
|
||||
export const ATTR_SELECTOR = 'AttributeSelector';
|
||||
export const CLASS_SELECTOR = 'ClassSelector';
|
||||
export const COMBINATOR = 'Combinator';
|
||||
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 RULE = 'Rule';
|
||||
export const SCOPE = 'Scope';
|
||||
export const SELECTOR = 'Selector';
|
||||
export const SELECTOR_LIST = 'SelectorList';
|
||||
export const STRING = 'String';
|
||||
export const SYNTAX_ERR = 'SyntaxError';
|
||||
export const TARGET_ALL = 'all';
|
||||
export const TARGET_FIRST = 'first';
|
||||
export const TARGET_LINEAL = 'lineal';
|
||||
export const TARGET_SELF = 'self';
|
||||
export const TYPE_SELECTOR = 'TypeSelector';
|
||||
|
||||
/* numeric */
|
||||
export const BIT_01 = 1;
|
||||
export const BIT_02 = 2;
|
||||
export const BIT_04 = 4;
|
||||
export const BIT_08 = 8;
|
||||
export const BIT_16 = 0x10;
|
||||
export const BIT_32 = 0x20;
|
||||
export const BIT_FFFF = 0xffff;
|
||||
export const DUO = 2;
|
||||
export const HEX = 16;
|
||||
export const TYPE_FROM = 8;
|
||||
export const TYPE_TO = -1;
|
||||
|
||||
/* Node */
|
||||
export const ELEMENT_NODE = 1;
|
||||
export const TEXT_NODE = 3;
|
||||
export const DOCUMENT_NODE = 9;
|
||||
export const DOCUMENT_FRAGMENT_NODE = 11;
|
||||
export const DOCUMENT_POSITION_PRECEDING = 2;
|
||||
export const DOCUMENT_POSITION_CONTAINS = 8;
|
||||
export const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
|
||||
|
||||
/* NodeFilter */
|
||||
export const SHOW_ALL = 0xffffffff;
|
||||
export const SHOW_CONTAINER = 0x501;
|
||||
export const SHOW_DOCUMENT = 0x100;
|
||||
export const SHOW_DOCUMENT_FRAGMENT = 0x400;
|
||||
export const SHOW_ELEMENT = 1;
|
||||
|
||||
/* selectors */
|
||||
export const ALPHA_NUM = '[A-Z\\d]+';
|
||||
export const CHILD_IDX = '(?:first|last|only)-(?:child|of-type)';
|
||||
export const DIGIT = '(?:0|[1-9]\\d*)';
|
||||
export const LANG_PART = `(?:-${ALPHA_NUM})*`;
|
||||
export const PSEUDO_CLASS = `(?:any-)?link|${CHILD_IDX}|checked|empty|indeterminate|read-(?:only|write)|target`;
|
||||
export const ANB = `[+-]?(?:${DIGIT}n?|n)|(?:[+-]?${DIGIT})?n\\s*[+-]\\s*${DIGIT}`;
|
||||
// combinators
|
||||
export const COMBO = '\\s?[\\s>~+]\\s?';
|
||||
export const DESCEND = '\\s?[\\s>]\\s?';
|
||||
export const SIBLING = '\\s?[+~]\\s?';
|
||||
// LOGIC_IS: :is()
|
||||
export const LOGIC_IS = `:is\\(\\s*[^)]+\\s*\\)`;
|
||||
// N_TH: excludes An+B with selector list, e.g. :nth-child(2n+1 of .foo)
|
||||
export const N_TH = `nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|${ANB})\\s*\\)`;
|
||||
// SUB_TYPE: attr, id, class, pseudo-class, note that [foo|=bar] is excluded
|
||||
export const SUB_TYPE = '\\[[^|\\]]+\\]|[#.:][\\w-]+';
|
||||
export const SUB_TYPE_WO_PSEUDO = '\\[[^|\\]]+\\]|[#.][\\w-]+';
|
||||
// TAG_TYPE: *, tag
|
||||
export const TAG_TYPE = '\\*|[A-Za-z][\\w-]*';
|
||||
export const TAG_TYPE_I = '\\*|[A-Z][\\w-]*';
|
||||
export const COMPOUND = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE})+)`;
|
||||
export const COMPOUND_L = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE}|${LOGIC_IS})+)`;
|
||||
export const COMPOUND_I = `(?:${TAG_TYPE_I}|(?:${TAG_TYPE_I})?(?:${SUB_TYPE})+)`;
|
||||
export const COMPOUND_WO_PSEUDO = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE_WO_PSEUDO})+)`;
|
||||
export const COMPLEX = `${COMPOUND}(?:${COMBO}${COMPOUND})*`;
|
||||
export const COMPLEX_L = `${COMPOUND_L}(?:${COMBO}${COMPOUND_L})*`;
|
||||
export const HAS_COMPOUND = `has\\([\\s>]?\\s*${COMPOUND_WO_PSEUDO}\\s*\\)`;
|
||||
export const LOGIC_COMPOUND = `(?:is|not)\\(\\s*${COMPOUND_L}(?:\\s*,\\s*${COMPOUND_L})*\\s*\\)`;
|
||||
export const LOGIC_COMPLEX = `(?:is|not)\\(\\s*${COMPLEX_L}(?:\\s*,\\s*${COMPLEX_L})*\\s*\\)`;
|
||||
|
||||
/* forms and input types */
|
||||
export const FORM_PARTS = Object.freeze([
|
||||
'button',
|
||||
'input',
|
||||
'select',
|
||||
'textarea'
|
||||
]);
|
||||
export const INPUT_BUTTON = Object.freeze(['button', 'reset', 'submit']);
|
||||
export const INPUT_CHECK = Object.freeze(['checkbox', 'radio']);
|
||||
export const INPUT_DATE = Object.freeze([
|
||||
'date',
|
||||
'datetime-local',
|
||||
'month',
|
||||
'time',
|
||||
'week'
|
||||
]);
|
||||
export const INPUT_TEXT = Object.freeze([
|
||||
'email',
|
||||
'password',
|
||||
'search',
|
||||
'tel',
|
||||
'text',
|
||||
'url'
|
||||
]);
|
||||
export const INPUT_EDIT = Object.freeze([
|
||||
...INPUT_DATE,
|
||||
...INPUT_TEXT,
|
||||
'number'
|
||||
]);
|
||||
export const INPUT_LTR = Object.freeze([
|
||||
...INPUT_CHECK,
|
||||
'color',
|
||||
'date',
|
||||
'image',
|
||||
'number',
|
||||
'range',
|
||||
'time'
|
||||
]);
|
||||
|
||||
/* logical combination pseudo-classes */
|
||||
export const KEYS_LOGICAL = new Set(['has', 'is', 'not', 'where']);
|
||||
3116
server/node_modules/@asamuzakjp/dom-selector/src/js/finder.js
generated
vendored
Normal file
3116
server/node_modules/@asamuzakjp/dom-selector/src/js/finder.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
587
server/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js
generated
vendored
Normal file
587
server/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js
generated
vendored
Normal file
@@ -0,0 +1,587 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
431
server/node_modules/@asamuzakjp/dom-selector/src/js/parser.js
generated
vendored
Normal file
431
server/node_modules/@asamuzakjp/dom-selector/src/js/parser.js
generated
vendored
Normal file
@@ -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';
|
||||
1107
server/node_modules/@asamuzakjp/dom-selector/src/js/utility.js
generated
vendored
Normal file
1107
server/node_modules/@asamuzakjp/dom-selector/src/js/utility.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user