feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,8 @@
export default interface ISelectorAttribute {
name: string;
operator: string | null;
value: string | null;
modifier: 's' | 'i' | null;
regExp: RegExp | null;
}
//# sourceMappingURL=ISelectorAttribute.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ISelectorAttribute.d.ts","sourceRoot":"","sources":["../../src/query-selector/ISelectorAttribute.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,WAAW,kBAAkB;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ISelectorAttribute.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ISelectorAttribute.js","sourceRoot":"","sources":["../../src/query-selector/ISelectorAttribute.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,4 @@
export default interface ISelectorMatch {
priorityWeight: number;
}
//# sourceMappingURL=ISelectorMatch.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ISelectorMatch.d.ts","sourceRoot":"","sources":["../../src/query-selector/ISelectorMatch.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,WAAW,cAAc;IACtC,cAAc,EAAE,MAAM,CAAC;CACvB"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ISelectorMatch.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ISelectorMatch.js","sourceRoot":"","sources":["../../src/query-selector/ISelectorMatch.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,8 @@
import SelectorItem from './SelectorItem.js';
export default interface ISelectorPseudo {
name: string;
arguments: string | null;
selectorItems: SelectorItem[] | null;
nthFunction: ((n: number) => boolean) | null;
}
//# sourceMappingURL=ISelectorPseudo.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ISelectorPseudo.d.ts","sourceRoot":"","sources":["../../src/query-selector/ISelectorPseudo.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,mBAAmB,CAAC;AAE7C,MAAM,CAAC,OAAO,WAAW,eAAe;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACrC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7C"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ISelectorPseudo.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ISelectorPseudo.js","sourceRoot":"","sources":["../../src/query-selector/ISelectorPseudo.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,109 @@
import Element from '../nodes/element/Element.js';
import NodeList from '../nodes/node/NodeList.js';
import Document from '../nodes/document/Document.js';
import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js';
import ISelectorMatch from './ISelectorMatch.js';
import IHTMLElementTagNameMap from '../config/IHTMLElementTagNameMap.js';
import ISVGElementTagNameMap from '../config/ISVGElementTagNameMap.js';
/**
* Utility for query selection in an HTML element.
*
* @class QuerySelector
*/
export default class QuerySelector {
/**
* Finds elements based on a query selector.
*
* @param node Node to search in.
* @param selector Selector.
* @returns HTML elements.
*/
static querySelectorAll<K extends keyof IHTMLElementTagNameMap>(node: Element | Document | DocumentFragment, selector: K): NodeList<IHTMLElementTagNameMap[K]>;
/**
* Finds elements based on a query selector.
*
* @param node Node to search in.
* @param selector Selector.
* @returns HTML elements.
*/
static querySelectorAll<K extends keyof ISVGElementTagNameMap>(node: Element | Document | DocumentFragment, selector: K): NodeList<ISVGElementTagNameMap[K]>;
/**
* Finds elements based on a query selector.
*
* @param node Node to search in.
* @param selector Selector.
* @returns HTML elements.
*/
static querySelectorAll(node: Element | Document | DocumentFragment, selector: string): NodeList<Element>;
/**
* Finds an element based on a query selector.
*
* @param node Node to search in.
* @param selector Selector.
* @returns HTML element.
*/
static querySelector<K extends keyof IHTMLElementTagNameMap>(node: Element | Document | DocumentFragment, selector: K): IHTMLElementTagNameMap[K] | null;
/**
* Finds an element based on a query selector.
*
* @param node Node to search in.
* @param selector Selector.
* @returns HTML element.
*/
static querySelector<K extends keyof ISVGElementTagNameMap>(node: Element | Document | DocumentFragment, selector: K): ISVGElementTagNameMap[K] | null;
/**
* Finds an element based on a query selector.
*
* @param node Node to search in.
* @param selector Selector.
* @returns HTML element.
*/
static querySelector(node: Element | Document | DocumentFragment, selector: string): Element | null;
/**
* Checks if an element matches a selector and returns priority weight.
*
* @param element Element to match.
* @param selector Selector to match with.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Result.
*/
static matches(element: Element, selector: string, options?: {
ignoreErrors?: boolean;
}): ISelectorMatch | null;
/**
* Checks if a node matches a selector.
*
* @param element Target element.
* @param currentElement
* @param selectorItems Selector items.
* @param cachedItem Cached item.
* @param [previousSelectorItem] Previous selector item.
* @param [priorityWeight] Priority weight.
* @returns Result.
*/
private static matchSelector;
/**
* Finds elements based on a query selector for a part of a list of selectors separated with comma.
*
* @param rootElement Root element.
* @param children Child elements.
* @param selectorItems Selector items.
* @param cachedItem Cached item.
* @param [documentPosition] Document position of the element.
* @returns Document position and element map.
*/
private static findAll;
/**
* Finds an element based on a query selector for a part of a list of selectors separated with comma.
*
* @param rootElement Root element.
* @param children Child elements.
* @param selectorItems Selector items.
* @param cachedItem Cached item.
* @param [documentPosition] Document position of the element.
* @returns Document position and element map.
*/
private static findFirst;
}
//# sourceMappingURL=QuerySelector.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"QuerySelector.d.ts","sourceRoot":"","sources":["../../src/query-selector/QuerySelector.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,6BAA6B,CAAC;AAGlD,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AAGjD,OAAO,QAAQ,MAAM,+BAA+B,CAAC;AACrD,OAAO,gBAAgB,MAAM,gDAAgD,CAAC;AAE9E,OAAO,cAAc,MAAM,qBAAqB,CAAC;AACjD,OAAO,sBAAsB,MAAM,qCAAqC,CAAC;AACzE,OAAO,qBAAqB,MAAM,oCAAoC,CAAC;AAevE;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,aAAa;IACjC;;;;;;OAMG;WACW,gBAAgB,CAAC,CAAC,SAAS,MAAM,sBAAsB,EACpE,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,EAC3C,QAAQ,EAAE,CAAC,GACT,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;IAEtC;;;;;;OAMG;WACW,gBAAgB,CAAC,CAAC,SAAS,MAAM,qBAAqB,EACnE,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,EAC3C,QAAQ,EAAE,CAAC,GACT,QAAQ,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAErC;;;;;;OAMG;WACW,gBAAgB,CAC7B,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,EAC3C,QAAQ,EAAE,MAAM,GACd,QAAQ,CAAC,OAAO,CAAC;IAwFpB;;;;;;OAMG;WACW,aAAa,CAAC,CAAC,SAAS,MAAM,sBAAsB,EACjE,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,EAC3C,QAAQ,EAAE,CAAC,GACT,sBAAsB,CAAC,CAAC,CAAC,GAAG,IAAI;IAEnC;;;;;;OAMG;WACW,aAAa,CAAC,CAAC,SAAS,MAAM,qBAAqB,EAChE,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,EAC3C,QAAQ,EAAE,CAAC,GACT,qBAAqB,CAAC,CAAC,CAAC,GAAG,IAAI;IAElC;;;;;;OAMG;WACW,aAAa,CAC1B,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,EAC3C,QAAQ,EAAE,MAAM,GACd,OAAO,GAAG,IAAI;IAqFjB;;;;;;;;OAQG;WACW,OAAO,CACpB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAClC,cAAc,GAAG,IAAI;IA6ExB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAqG5B;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;IA6EtB;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;CAyFxB"}

View File

@@ -0,0 +1,384 @@
import * as PropertySymbol from '../PropertySymbol.js';
import NodeList from '../nodes/node/NodeList.js';
import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js';
import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';
import SelectorParser from './SelectorParser.js';
/**
* Invalid Selector RegExp.
*/
const INVALID_SELECTOR_REGEXP = /^[.#\[]?\d|[.#]$/;
/**
* Utility for query selection in an HTML element.
*
* @class QuerySelector
*/
export default class QuerySelector {
/**
* Finds elements based on a query selector.
*
* @param node Node to search in.
* @param selector Selector.
* @returns HTML elements.
*/
static querySelectorAll(node, selector) {
const window = node[PropertySymbol.window];
if (selector === '') {
throw new window.DOMException(`Failed to execute 'querySelectorAll' on '${node.constructor.name}': The provided selector is empty.`);
}
if (typeof selector === 'function') {
throw new window.DOMException(`Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.`);
}
if (typeof selector === 'symbol') {
throw new window.TypeError(`Failed to execute 'querySelectorAll' on '${node.constructor.name}': Cannot convert a Symbol value to a string`);
}
selector = String(selector);
if (INVALID_SELECTOR_REGEXP.test(selector)) {
throw new window.DOMException(`Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.`);
}
const cache = node[PropertySymbol.cache].querySelectorAll;
const cachedResult = cache.get(selector);
if (cachedResult?.result) {
const result = cachedResult.result.deref();
if (result) {
return result;
}
}
const groups = SelectorParser.getSelectorGroups(selector);
const items = [];
const nodeList = new NodeList(PropertySymbol.illegalConstructor, items);
const matchesMap = new Map();
const matchedPositions = [];
const cachedItem = {
result: new WeakRef(nodeList)
};
node[PropertySymbol.cache].querySelectorAll.set(selector, cachedItem);
if (node[PropertySymbol.isConnected]) {
// Document is affected for the ":target" selector
(node[PropertySymbol.ownerDocument] || node)[PropertySymbol.affectsCache].push(cachedItem);
}
for (const items of groups) {
const matches = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode
? this.findAll(node, [node], items, cachedItem)
: this.findAll(null, node[PropertySymbol.elementArray], items, cachedItem);
for (const match of matches) {
if (!matchesMap.has(match.documentPosition)) {
matchesMap.set(match.documentPosition, match.element);
matchedPositions.push(match.documentPosition);
}
}
}
const keys = matchedPositions.sort();
for (let i = 0, max = keys.length; i < max; i++) {
items.push(matchesMap.get(keys[i]));
}
return nodeList;
}
/**
* Finds an element based on a query selector.
*
* @param node Node to search in.
* @param selector Selector.
* @returns HTML element.
*/
static querySelector(node, selector) {
const window = node[PropertySymbol.window];
if (selector === '') {
throw new window.DOMException(`Failed to execute 'querySelector' on '${node.constructor.name}': The provided selector is empty.`);
}
if (typeof selector === 'function') {
throw new window.DOMException(`Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.`);
}
if (typeof selector === 'symbol') {
throw new window.TypeError(`Failed to execute 'querySelector' on '${node.constructor.name}': Cannot convert a Symbol value to a string`);
}
selector = String(selector);
if (INVALID_SELECTOR_REGEXP.test(selector)) {
throw new window.DOMException(`Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.`);
}
const cachedResult = node[PropertySymbol.cache].querySelector.get(selector);
if (cachedResult?.result) {
const result = cachedResult.result.deref();
if (result) {
return result;
}
}
const cachedItem = {
result: {
deref: () => null
}
};
node[PropertySymbol.cache].querySelector.set(selector, cachedItem);
if (node[PropertySymbol.isConnected]) {
// Document is affected for the ":target" selector
(node[PropertySymbol.ownerDocument] || node)[PropertySymbol.affectsCache].push(cachedItem);
}
const matchesMap = new Map();
const matchedPositions = [];
for (const items of SelectorParser.getSelectorGroups(selector)) {
const match = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode
? this.findFirst(node, [node], items, cachedItem)
: this.findFirst(null, node[PropertySymbol.elementArray], items, cachedItem);
if (match && !matchesMap.has(match.documentPosition)) {
matchesMap.set(match.documentPosition, match.element);
matchedPositions.push(match.documentPosition);
}
}
if (matchedPositions.length > 0) {
const keys = matchedPositions.sort();
return matchesMap.get(keys[0]);
}
return null;
}
/**
* Checks if an element matches a selector and returns priority weight.
*
* @param element Element to match.
* @param selector Selector to match with.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Result.
*/
static matches(element, selector, options) {
const ignoreErrors = options?.ignoreErrors;
const window = element[PropertySymbol.window];
if (selector === '*') {
return {
priorityWeight: 1
};
}
if (selector === '') {
if (ignoreErrors) {
return null;
}
throw new window.DOMException(`Failed to execute 'matches' on '${element.constructor.name}': The provided selector is empty.`);
}
if (typeof selector === 'function') {
if (ignoreErrors) {
return null;
}
throw new window.DOMException(`Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.`);
}
if (typeof selector === 'symbol') {
if (ignoreErrors) {
return null;
}
throw new window.TypeError(`Cannot convert a Symbol value to a string`);
}
selector = String(selector);
if (INVALID_SELECTOR_REGEXP.test(selector)) {
if (ignoreErrors) {
return null;
}
throw new window.DOMException(`Failed to execute 'matches' on '${element.constructor.name}': '${selector}' is not a valid selector.`);
}
const cachedResult = element[PropertySymbol.cache].matches.get(selector);
if (cachedResult?.result) {
return cachedResult.result.match;
}
const cachedItem = {
result: { match: null }
};
element[PropertySymbol.cache].matches.set(selector, cachedItem);
if (element[PropertySymbol.isConnected]) {
// Document is affected for the ":target" selector
(element[PropertySymbol.ownerDocument] || element)[PropertySymbol.affectsCache].push(cachedItem);
}
for (const items of SelectorParser.getSelectorGroups(selector, options)) {
const result = this.matchSelector(element, items.reverse(), cachedItem);
if (result) {
cachedItem.result.match = result;
return result;
}
}
return null;
}
/**
* Checks if a node matches a selector.
*
* @param element Target element.
* @param currentElement
* @param selectorItems Selector items.
* @param cachedItem Cached item.
* @param [previousSelectorItem] Previous selector item.
* @param [priorityWeight] Priority weight.
* @returns Result.
*/
static matchSelector(element, selectorItems, cachedItem, previousSelectorItem = null, priorityWeight = 0) {
const selectorItem = selectorItems[0];
const result = selectorItem.match(element);
if (result) {
if (selectorItems.length === 1) {
return {
priorityWeight: priorityWeight + result.priorityWeight
};
}
switch (selectorItem.combinator) {
case SelectorCombinatorEnum.adjacentSibling:
const previousElementSibling = element.previousElementSibling;
if (previousElementSibling) {
previousElementSibling[PropertySymbol.affectsCache].push(cachedItem);
const match = this.matchSelector(previousElementSibling, selectorItems.slice(1), cachedItem, selectorItem, priorityWeight + result.priorityWeight);
if (match) {
return match;
}
}
break;
case SelectorCombinatorEnum.child:
case SelectorCombinatorEnum.descendant:
const parentElement = element.parentElement;
if (parentElement) {
parentElement[PropertySymbol.affectsCache].push(cachedItem);
const match = this.matchSelector(parentElement, selectorItems.slice(1), cachedItem, selectorItem, priorityWeight + result.priorityWeight);
if (match) {
return match;
}
}
break;
case SelectorCombinatorEnum.subsequentSibling:
const siblingParentElement = element.parentElement;
if (siblingParentElement) {
const siblings = siblingParentElement[PropertySymbol.elementArray];
const index = siblings.indexOf(element);
siblingParentElement[PropertySymbol.affectsCache].push(cachedItem);
for (let i = index - 1; i >= 0; i--) {
const sibling = siblings[i];
sibling[PropertySymbol.affectsCache].push(cachedItem);
const match = this.matchSelector(sibling, selectorItems.slice(1), cachedItem, selectorItem, priorityWeight + result.priorityWeight);
if (match) {
return match;
}
}
}
break;
}
}
if (previousSelectorItem?.combinator === SelectorCombinatorEnum.descendant) {
const parentElement = element.parentElement;
if (parentElement) {
return this.matchSelector(parentElement, selectorItems, cachedItem, previousSelectorItem, priorityWeight);
}
}
return null;
}
/**
* Finds elements based on a query selector for a part of a list of selectors separated with comma.
*
* @param rootElement Root element.
* @param children Child elements.
* @param selectorItems Selector items.
* @param cachedItem Cached item.
* @param [documentPosition] Document position of the element.
* @returns Document position and element map.
*/
static findAll(rootElement, children, selectorItems, cachedItem, documentPosition) {
const selectorItem = selectorItems[0];
const nextSelectorItem = selectorItems[1];
let matched = [];
for (let i = 0, max = children.length; i < max; i++) {
const child = children[i];
const childrenOfChild = child[PropertySymbol.elementArray];
const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i);
child[PropertySymbol.affectsCache].push(cachedItem);
if (selectorItem.match(child)) {
if (!nextSelectorItem) {
if (rootElement !== child) {
matched.push({
documentPosition: position,
element: child
});
}
}
else {
switch (nextSelectorItem.combinator) {
case SelectorCombinatorEnum.adjacentSibling:
const nextElementSibling = child.nextElementSibling;
if (nextElementSibling) {
matched = matched.concat(this.findAll(rootElement, [nextElementSibling], selectorItems.slice(1), cachedItem, position));
}
break;
case SelectorCombinatorEnum.descendant:
case SelectorCombinatorEnum.child:
matched = matched.concat(this.findAll(rootElement, childrenOfChild, selectorItems.slice(1), cachedItem, position));
break;
case SelectorCombinatorEnum.subsequentSibling:
const index = children.indexOf(child);
for (let j = index + 1; j < children.length; j++) {
const sibling = children[j];
matched = matched.concat(this.findAll(rootElement, [sibling], selectorItems.slice(1), cachedItem, position));
}
break;
}
}
}
if (selectorItem.combinator === SelectorCombinatorEnum.descendant && childrenOfChild.length) {
matched = matched.concat(this.findAll(rootElement, childrenOfChild, selectorItems, cachedItem, position));
}
}
return matched;
}
/**
* Finds an element based on a query selector for a part of a list of selectors separated with comma.
*
* @param rootElement Root element.
* @param children Child elements.
* @param selectorItems Selector items.
* @param cachedItem Cached item.
* @param [documentPosition] Document position of the element.
* @returns Document position and element map.
*/
static findFirst(rootElement, children, selectorItems, cachedItem, documentPosition) {
const selectorItem = selectorItems[0];
const nextSelectorItem = selectorItems[1];
for (let i = 0, max = children.length; i < max; i++) {
const child = children[i];
const childrenOfChild = child[PropertySymbol.elementArray];
const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i);
child[PropertySymbol.affectsCache].push(cachedItem);
if (selectorItem.match(child)) {
if (!nextSelectorItem) {
if (rootElement !== child) {
return { documentPosition: position, element: child };
}
}
else {
switch (nextSelectorItem.combinator) {
case SelectorCombinatorEnum.adjacentSibling:
const nextElementSibling = child.nextElementSibling;
if (nextElementSibling) {
const match = this.findFirst(rootElement, [nextElementSibling], selectorItems.slice(1), cachedItem, position);
if (match) {
return match;
}
}
break;
case SelectorCombinatorEnum.descendant:
case SelectorCombinatorEnum.child:
const match = this.findFirst(rootElement, childrenOfChild, selectorItems.slice(1), cachedItem, position);
if (match) {
return match;
}
break;
case SelectorCombinatorEnum.subsequentSibling:
const index = children.indexOf(child);
for (let i = index + 1; i < children.length; i++) {
const sibling = children[i];
const match = this.findFirst(rootElement, [sibling], selectorItems.slice(1), cachedItem, position);
if (match) {
return match;
}
}
break;
}
}
}
if (selectorItem.combinator === SelectorCombinatorEnum.descendant && childrenOfChild.length) {
const match = this.findFirst(rootElement, childrenOfChild, selectorItems, cachedItem, position);
if (match) {
return match;
}
}
}
return null;
}
}
//# sourceMappingURL=QuerySelector.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
declare enum SelectorCombinatorEnum {
descendant = "descendant",
child = "child",
adjacentSibling = "adjacentSibling",
subsequentSibling = "subsequentSibling"
}
export default SelectorCombinatorEnum;
//# sourceMappingURL=SelectorCombinatorEnum.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SelectorCombinatorEnum.d.ts","sourceRoot":"","sources":["../../src/query-selector/SelectorCombinatorEnum.ts"],"names":[],"mappings":"AAAA,aAAK,sBAAsB;IAC1B,UAAU,eAAe;IACzB,KAAK,UAAU;IACf,eAAe,oBAAoB;IACnC,iBAAiB,sBAAsB;CACvC;AAED,eAAe,sBAAsB,CAAC"}

View File

@@ -0,0 +1,9 @@
var SelectorCombinatorEnum;
(function (SelectorCombinatorEnum) {
SelectorCombinatorEnum["descendant"] = "descendant";
SelectorCombinatorEnum["child"] = "child";
SelectorCombinatorEnum["adjacentSibling"] = "adjacentSibling";
SelectorCombinatorEnum["subsequentSibling"] = "subsequentSibling";
})(SelectorCombinatorEnum || (SelectorCombinatorEnum = {}));
export default SelectorCombinatorEnum;
//# sourceMappingURL=SelectorCombinatorEnum.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SelectorCombinatorEnum.js","sourceRoot":"","sources":["../../src/query-selector/SelectorCombinatorEnum.ts"],"names":[],"mappings":"AAAA,IAAK,sBAKJ;AALD,WAAK,sBAAsB;IAC1B,mDAAyB,CAAA;IACzB,yCAAe,CAAA;IACf,6DAAmC,CAAA;IACnC,iEAAuC,CAAA;AACxC,CAAC,EALI,sBAAsB,KAAtB,sBAAsB,QAK1B;AAED,eAAe,sBAAsB,CAAC"}

View File

@@ -0,0 +1,92 @@
import Element from '../nodes/element/Element.js';
import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';
import ISelectorAttribute from './ISelectorAttribute.js';
import ISelectorMatch from './ISelectorMatch.js';
import ISelectorPseudo from './ISelectorPseudo.js';
/**
* Selector item.
*/
export default class SelectorItem {
tagName: string | null;
id: string | null;
classNames: string[] | null;
attributes: ISelectorAttribute[] | null;
pseudos: ISelectorPseudo[] | null;
isPseudoElement: boolean;
combinator: SelectorCombinatorEnum;
ignoreErrors: boolean;
/**
* Constructor.
*
* @param [options] Options.
* @param [options.combinator] Combinator.
* @param [options.tagName] Tag name.
* @param [options.id] ID.
* @param [options.classNames] Class names.
* @param [options.attributes] Attributes.
* @param [options.pseudos] Pseudos.
* @param [options.isPseudoElement] Is pseudo element.
* @param [options.ignoreErrors] Ignore errors.
*/
constructor(options?: {
tagName?: string;
id?: string;
classNames?: string[];
attributes?: ISelectorAttribute[];
pseudos?: ISelectorPseudo[];
isPseudoElement?: boolean;
combinator?: SelectorCombinatorEnum;
ignoreErrors?: boolean;
});
/**
* Matches a selector against an element.
*
* @param element HTML element.
* @returns Result.
*/
match(element: Element): ISelectorMatch | null;
/**
* Matches a pseudo selector.
*
* @param element Element.
* @returns Result.
*/
private matchPseudo;
/**
* Matches a pseudo selector.
*
* @param element Element.
* @param parentChildren Parent children.
* @param pseudo Pseudo.
*/
private matchPseudoItem;
/**
* Matches attribute.
*
* @param element Element.
* @returns Result.
*/
private matchAttributes;
/**
* Matches class.
*
* @param element Element.
* @returns Result.
*/
private matchClass;
/**
* Matches a selector item against children of an element.
*
* @param selectorItem Selector item.
* @param element Element.
* @returns Result.
*/
private matchChildOfElement;
/**
* Returns the selector string.
*
* @returns Selector string.
*/
private getSelectorString;
}
//# sourceMappingURL=SelectorItem.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SelectorItem.d.ts","sourceRoot":"","sources":["../../src/query-selector/SelectorItem.ts"],"names":[],"mappings":"AAEA,OAAO,OAAO,MAAM,6BAA6B,CAAC;AAElD,OAAO,sBAAsB,MAAM,6BAA6B,CAAC;AACjE,OAAO,kBAAkB,MAAM,yBAAyB,CAAC;AACzD,OAAO,cAAc,MAAM,qBAAqB,CAAC;AACjD,OAAO,eAAe,MAAM,sBAAsB,CAAC;AAInD;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,YAAY;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAC;IACxC,OAAO,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;IAClC,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,sBAAsB,CAAC;IACnC,YAAY,EAAE,OAAO,CAAC;IAE7B;;;;;;;;;;;;OAYG;gBACS,OAAO,CAAC,EAAE;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,UAAU,CAAC,EAAE,kBAAkB,EAAE,CAAC;QAClC,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;QAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,UAAU,CAAC,EAAE,sBAAsB,CAAC;QACpC,YAAY,CAAC,EAAE,OAAO,CAAC;KACvB;IAWD;;;;;OAKG;IACI,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI;IAqDrD;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAgEnB;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAyLvB;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA+BvB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAkBlB;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB;IAgB3B;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;CAsBzB"}

View File

@@ -0,0 +1,419 @@
import DOMException from '../exception/DOMException.js';
import * as PropertySymbol from '../PropertySymbol.js';
import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';
const SPACE_REGEXP = /\s+/;
/**
* Selector item.
*/
export default class SelectorItem {
tagName;
id;
classNames;
attributes;
pseudos;
isPseudoElement;
combinator;
ignoreErrors;
/**
* Constructor.
*
* @param [options] Options.
* @param [options.combinator] Combinator.
* @param [options.tagName] Tag name.
* @param [options.id] ID.
* @param [options.classNames] Class names.
* @param [options.attributes] Attributes.
* @param [options.pseudos] Pseudos.
* @param [options.isPseudoElement] Is pseudo element.
* @param [options.ignoreErrors] Ignore errors.
*/
constructor(options) {
this.tagName = options?.tagName || null;
this.id = options?.id || null;
this.classNames = options?.classNames || null;
this.attributes = options?.attributes || null;
this.pseudos = options?.pseudos || null;
this.isPseudoElement = options?.isPseudoElement || false;
this.combinator = options?.combinator || SelectorCombinatorEnum.descendant;
this.ignoreErrors = options?.ignoreErrors || false;
}
/**
* Matches a selector against an element.
*
* @param element HTML element.
* @returns Result.
*/
match(element) {
let priorityWeight = 0;
if (this.isPseudoElement) {
return null;
}
// Tag name match
if (this.tagName) {
if (this.tagName !== '*' && this.tagName !== element[PropertySymbol.tagName].toUpperCase()) {
return null;
}
priorityWeight += 1;
}
// ID Match
if (this.id) {
if (this.id !== element.id) {
return null;
}
priorityWeight += 100;
}
// Class match
if (this.classNames) {
const result = this.matchClass(element);
if (!result) {
return null;
}
priorityWeight += result.priorityWeight;
}
// Attribute match
if (this.attributes) {
const result = this.matchAttributes(element);
if (!result) {
return null;
}
priorityWeight += result.priorityWeight;
}
// Pseudo match
if (this.pseudos) {
const result = this.matchPseudo(element);
if (!result) {
return null;
}
priorityWeight += result.priorityWeight;
}
return { priorityWeight };
}
/**
* Matches a pseudo selector.
*
* @param element Element.
* @returns Result.
*/
matchPseudo(element) {
const parent = element[PropertySymbol.parentNode];
const parentChildren = element[PropertySymbol.parentNode]
? element[PropertySymbol.parentNode][PropertySymbol.elementArray]
: [];
if (!this.pseudos) {
return { priorityWeight: 0 };
}
let priorityWeight = 0;
for (const pseudo of this.pseudos) {
// Validation
switch (pseudo.name) {
case 'not':
case 'nth-child':
case 'nth-of-type':
case 'nth-last-child':
case 'nth-last-of-type':
case 'is':
case 'where':
if (!pseudo.arguments) {
if (this.ignoreErrors) {
return null;
}
throw new DOMException(`Failed to execute 'matches' on '${element.constructor.name}': '${this.getSelectorString()}' is not a valid selector.`);
}
break;
}
// Check if parent exists
if (!parent) {
switch (pseudo.name) {
case 'first-child':
case 'last-child':
case 'only-child':
case 'first-of-type':
case 'last-of-type':
case 'only-of-type':
case 'nth-child':
case 'nth-of-type':
case 'nth-last-child':
case 'nth-last-of-type':
return null;
}
}
const selectorMatch = this.matchPseudoItem(element, parentChildren, pseudo);
if (!selectorMatch) {
return null;
}
priorityWeight += selectorMatch.priorityWeight;
}
return { priorityWeight };
}
/**
* Matches a pseudo selector.
*
* @param element Element.
* @param parentChildren Parent children.
* @param pseudo Pseudo.
*/
matchPseudoItem(element, parentChildren, pseudo) {
switch (pseudo.name) {
case 'first-child':
return parentChildren[0] === element ? { priorityWeight: 10 } : null;
case 'last-child':
return parentChildren.length && parentChildren[parentChildren.length - 1] === element
? { priorityWeight: 10 }
: null;
case 'only-child':
return parentChildren.length === 1 && parentChildren[0] === element
? { priorityWeight: 10 }
: null;
case 'first-of-type':
for (const child of parentChildren) {
if (child[PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
return child === element ? { priorityWeight: 10 } : null;
}
}
return null;
case 'last-of-type':
for (let i = parentChildren.length - 1; i >= 0; i--) {
const child = parentChildren[i];
if (child[PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
return child === element ? { priorityWeight: 10 } : null;
}
}
return null;
case 'only-of-type':
let isFound = false;
for (const child of parentChildren) {
if (child[PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
if (isFound || child !== element) {
return null;
}
isFound = true;
}
}
return isFound ? { priorityWeight: 10 } : null;
case 'checked':
return element[PropertySymbol.tagName] === 'INPUT' && element.checked
? { priorityWeight: 10 }
: null;
case 'disabled':
return 'disabled' in element && element.hasAttribute('disabled')
? { priorityWeight: 10 }
: null;
case 'empty':
return !element[PropertySymbol.elementArray].length
? { priorityWeight: 10 }
: null;
case 'root':
return element[PropertySymbol.tagName] === 'HTML' ? { priorityWeight: 10 } : null;
case 'not':
for (const selectorItem of pseudo.selectorItems) {
if (selectorItem.match(element)) {
return null;
}
}
return { priorityWeight: 10 };
case 'nth-child':
let nthChildIndex = -1;
if (pseudo.selectorItems[0] && !pseudo.selectorItems[0].match(element)) {
return null;
}
for (let i = 0, max = parentChildren.length; i < max; i++) {
if (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) {
nthChildIndex++;
}
if (parentChildren[i] === element) {
return nthChildIndex !== -1 && pseudo.nthFunction(nthChildIndex + 1)
? { priorityWeight: 10 }
: null;
}
}
return null;
case 'nth-of-type':
let nthOfTypeIndex = -1;
for (let i = 0, max = parentChildren.length; i < max; i++) {
if (parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
nthOfTypeIndex++;
}
if (parentChildren[i] === element) {
return nthOfTypeIndex !== -1 && pseudo.nthFunction(nthOfTypeIndex + 1)
? { priorityWeight: 10 }
: null;
}
}
return null;
case 'nth-last-child':
let nthLastChildIndex = -1;
if (pseudo.selectorItems[0] && !pseudo.selectorItems[0].match(element)) {
return null;
}
for (let i = parentChildren.length - 1; i >= 0; i--) {
if (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) {
nthLastChildIndex++;
}
if (parentChildren[i] === element) {
return nthLastChildIndex !== -1 && pseudo.nthFunction(nthLastChildIndex + 1)
? { priorityWeight: 10 }
: null;
}
}
return null;
case 'nth-last-of-type':
let nthLastOfTypeIndex = -1;
for (let i = parentChildren.length - 1; i >= 0; i--) {
if (parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
nthLastOfTypeIndex++;
}
if (parentChildren[i] === element) {
return nthLastOfTypeIndex !== -1 && pseudo.nthFunction(nthLastOfTypeIndex + 1)
? { priorityWeight: 10 }
: null;
}
}
return null;
case 'target':
const hash = element[PropertySymbol.ownerDocument].location.hash;
if (!hash) {
return null;
}
return element.isConnected && element.id === hash.slice(1) ? { priorityWeight: 10 } : null;
case 'is':
let priorityWeightForIs = 0;
for (const selectorItem of pseudo.selectorItems) {
const match = selectorItem.match(element);
if (match && priorityWeightForIs < match.priorityWeight) {
priorityWeightForIs = match.priorityWeight;
}
}
return priorityWeightForIs ? { priorityWeight: priorityWeightForIs } : null;
case 'where':
for (const selectorItem of pseudo.selectorItems) {
if (selectorItem.match(element)) {
return { priorityWeight: 0 };
}
}
return null;
case 'has':
let priorityWeightForHas = 0;
if (pseudo.arguments[0] === '+') {
const nextSibling = element.nextElementSibling;
if (!nextSibling) {
return null;
}
for (const selectorItem of pseudo.selectorItems) {
const match = selectorItem.match(nextSibling);
if (match && priorityWeightForHas < match.priorityWeight) {
priorityWeightForHas = match.priorityWeight;
}
}
}
else if (pseudo.arguments[0] === '>') {
for (const selectorItem of pseudo.selectorItems) {
for (const child of element[PropertySymbol.elementArray]) {
const match = selectorItem.match(child);
if (match && priorityWeightForHas < match.priorityWeight) {
priorityWeightForHas = match.priorityWeight;
break;
}
}
}
}
else {
for (const selectorItem of pseudo.selectorItems) {
const match = this.matchChildOfElement(selectorItem, element);
if (match && priorityWeightForHas < match.priorityWeight) {
priorityWeightForHas = match.priorityWeight;
}
}
}
return priorityWeightForHas ? { priorityWeight: priorityWeightForHas } : null;
case 'focus':
case 'focus-visible':
return element[PropertySymbol.ownerDocument].activeElement === element
? { priorityWeight: 10 }
: null;
default:
return null;
}
}
/**
* Matches attribute.
*
* @param element Element.
* @returns Result.
*/
matchAttributes(element) {
if (!this.attributes) {
return null;
}
let priorityWeight = 0;
for (const attribute of this.attributes) {
const elementAttribute = element[PropertySymbol.attributes].getNamedItem(attribute.name);
if (!elementAttribute) {
return null;
}
priorityWeight += 10;
if (attribute.value !== null &&
(elementAttribute[PropertySymbol.value] === null ||
(attribute.regExp && !attribute.regExp.test(elementAttribute[PropertySymbol.value])) ||
(!attribute.regExp && attribute.value !== elementAttribute[PropertySymbol.value]))) {
return null;
}
}
return { priorityWeight };
}
/**
* Matches class.
*
* @param element Element.
* @returns Result.
*/
matchClass(element) {
if (!this.classNames) {
return null;
}
const classList = element.className.split(SPACE_REGEXP);
let priorityWeight = 0;
for (const className of this.classNames) {
if (!classList.includes(className)) {
return null;
}
priorityWeight += 10;
}
return { priorityWeight };
}
/**
* Matches a selector item against children of an element.
*
* @param selectorItem Selector item.
* @param element Element.
* @returns Result.
*/
matchChildOfElement(selectorItem, element) {
for (const child of element[PropertySymbol.elementArray]) {
const match = selectorItem.match(child);
if (match) {
return match;
}
const childMatch = this.matchChildOfElement(selectorItem, child);
if (childMatch) {
return childMatch;
}
}
}
/**
* Returns the selector string.
*
* @returns Selector string.
*/
getSelectorString() {
return `${this.tagName ? this.tagName.toLowerCase() : ''}${this.id ? `#${this.id}` : ''}${this.classNames ? `.${this.classNames.join('.')}` : ''}${this.attributes
? this.attributes
.map((attribute) => `[${attribute.name}${attribute.value ? `${attribute.operator || ''}="${attribute.value}"` : ''}]`)
.join('')
: ''}${this.pseudos
? this.pseudos
.map((pseudo) => `:${pseudo.name}${pseudo.arguments ? `(${pseudo.arguments})` : ''}`)
.join('')
: ''}`;
}
}
//# sourceMappingURL=SelectorItem.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,59 @@
import SelectorItem from './SelectorItem.js';
/**
* Utility for parsing a selection string.
*/
export default class SelectorParser {
/**
* Parses a selector string and returns an instance of SelectorItem.
*
* @param selector Selector.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Selector item.
*/
static getSelectorItem(selector: string, options?: {
ignoreErrors?: boolean;
}): SelectorItem;
/**
* Parses a selector string and returns groups with SelectorItem instances.
*
* @param selector Selector.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Selector groups.
*/
static getSelectorGroups(selector: string, options?: {
ignoreErrors?: boolean;
}): Array<Array<SelectorItem>>;
/**
* Returns attribute RegExp.
*
* @param attribute Attribute.
* @param attribute.value Attribute value.
* @param attribute.operator Attribute operator.
* @param attribute.modifier Attribute modifier.
* @returns Attribute RegExp.
*/
private static getAttributeRegExp;
/**
* Returns pseudo.
*
* @param name Pseudo name.
* @param args Pseudo arguments.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Pseudo.
*/
private static getPseudo;
/**
* Returns pseudo nth function.
*
* Based on:
* https://github.com/dperini/nwsapi/blob/master/src/nwsapi.js
*
* @param args Pseudo arguments.
* @returns Pseudo nth function.
*/
private static getPseudoNthFunction;
}
//# sourceMappingURL=SelectorParser.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SelectorParser.d.ts","sourceRoot":"","sources":["../../src/query-selector/SelectorParser.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,mBAAmB,CAAC;AA8D7C;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,cAAc;IAClC;;;;;;;OAOG;WACW,eAAe,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAClC,YAAY;IAIf;;;;;;;OAOG;WACW,iBAAiB,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAClC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IA2I7B;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAmCjC;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAwFxB;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAC,oBAAoB;CA2CnC"}

View File

@@ -0,0 +1,389 @@
import SelectorItem from './SelectorItem.js';
import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';
import DOMException from '../exception/DOMException.js';
/**
* Selector RegExp.
*
* Group 1: All (e.g. "*")
* Group 2: Tag name (e.g. "div")
* Group 3: ID (e.g. "#id")
* Group 4: Class (e.g. ".class")
* Group 5: Attribute name when no value (e.g. "attr1")
* Group 6: Attribute name when there is a value using apostrophe (e.g. "attr1")
* Group 7: Attribute operator when using apostrophe (e.g. "~")
* Group 8: Attribute value when using apostrophe (e.g. "value1")
* Group 9: Attribute modifier when using apostrophe (e.g. "i" or "s")
* Group 10: Attribute name when threre is a value not using apostrophe (e.g. "attr1")
* Group 11: Attribute operator when not using apostrophe (e.g. "~")
* Group 12: Attribute value when notusing apostrophe (e.g. "value1")
* Group 13: Pseudo name when arguments (e.g. "nth-child")
* Group 14: Arguments of pseudo (e.g. "2n + 1")
* Group 15: Pseudo name when no arguments (e.g. "empty")
* Group 16: Pseudo element (e.g. "::after", "::-webkit-inner-spin-button").
* Group 17: Combinator.
*/
const SELECTOR_REGEXP = /(\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_«»]|\\.)+)|\.((?:[a-zA-Z0-9-_«»]|\\.)+)|\[([a-zA-Z0-9-_\\:]+)\]|\[([a-zA-Z0-9-_\\:]+)\s*([~|^$*]{0,1})\s*=\s*["']{1}([^"']*)["']{1}\s*(s|i){0,1}\]|\[([a-zA-Z0-9-_]+)\s*([~|^$*]{0,1})\s*=\s*([^\]]*)\]|:([a-zA-Z-]+)\s*\(((?:[^()]|\[[^\]]*\]|\([^()]*\))*)\){0,1}|:([a-zA-Z-]+)|::([a-zA-Z-]+)|([\s,+>~]*)/gm;
/**
* Escaped Character RegExp.
*/
const ESCAPED_CHARACTER_REGEXP = /\\/g;
/**
* Attribute Escape RegExp.
*/
const ATTRIBUTE_ESCAPE_REGEXP = /[.*+?^${}()|[\]\\]/g;
/**
* Nth Function.
*/
const NTH_FUNCTION = {
odd: (n) => (n + 1) % 2 === 0,
even: (n) => (n + 1) % 2 !== 0,
alwaysFalse: () => false
};
/**
* Space RegExp.
*/
const SPACE_REGEXP = / /g;
/**
* Simple Selector RegExp.
*
* Group 1: Tag name (e.g. "div")
* Group 2: Class (e.g. ".classA.classB")
* Group 3: ID (e.g. "#id")
*/
const SIMPLE_SELECTOR_REGEXP = /(^[a-zA-Z0-9-]+$)|(^\.[a-zA-Z0-9-_.]+$)|(^#[a-zA-Z0-9-_]+$)/;
/**
* Utility for parsing a selection string.
*/
export default class SelectorParser {
/**
* Parses a selector string and returns an instance of SelectorItem.
*
* @param selector Selector.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Selector item.
*/
static getSelectorItem(selector, options) {
return this.getSelectorGroups(selector, options)[0][0];
}
/**
* Parses a selector string and returns groups with SelectorItem instances.
*
* @param selector Selector.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Selector groups.
*/
static getSelectorGroups(selector, options) {
selector = selector.trim();
const ignoreErrors = options?.ignoreErrors;
if (selector === '*') {
return [[new SelectorItem({ tagName: '*', ignoreErrors })]];
}
const simpleMatch = selector.match(SIMPLE_SELECTOR_REGEXP);
if (simpleMatch) {
if (simpleMatch[1]) {
return [[new SelectorItem({ tagName: selector.toUpperCase(), ignoreErrors })]];
}
else if (simpleMatch[2]) {
return [
[new SelectorItem({ classNames: selector.replace('.', '').split('.'), ignoreErrors })]
];
}
else if (simpleMatch[3]) {
return [[new SelectorItem({ id: selector.replace('#', ''), ignoreErrors })]];
}
}
const regexp = new RegExp(SELECTOR_REGEXP);
let currentSelectorItem = new SelectorItem({
combinator: SelectorCombinatorEnum.descendant,
ignoreErrors
});
let currentGroup = [currentSelectorItem];
const groups = [currentGroup];
let isValid = false;
let match;
while ((match = regexp.exec(selector))) {
if (match[0]) {
isValid = true;
if (match[1]) {
currentSelectorItem.tagName = '*';
}
else if (match[2]) {
currentSelectorItem.tagName = match[2].toUpperCase();
}
else if (match[3]) {
currentSelectorItem.id = match[3].replace(ESCAPED_CHARACTER_REGEXP, '');
}
else if (match[4]) {
currentSelectorItem.classNames = currentSelectorItem.classNames || [];
currentSelectorItem.classNames.push(match[4].replace(ESCAPED_CHARACTER_REGEXP, ''));
}
else if (match[5]) {
currentSelectorItem.attributes = currentSelectorItem.attributes || [];
currentSelectorItem.attributes.push({
name: match[5].toLowerCase(),
operator: null,
value: null,
modifier: null,
regExp: null
});
}
else if (match[6] && match[8] !== undefined) {
currentSelectorItem.attributes = currentSelectorItem.attributes || [];
currentSelectorItem.attributes.push({
name: match[6].toLowerCase(),
operator: match[7] || null,
value: match[8].replace(ESCAPED_CHARACTER_REGEXP, ''),
modifier: match[9] || null,
regExp: this.getAttributeRegExp({
operator: match[7],
value: match[8],
modifier: match[9]
})
});
}
else if (match[10] && match[12] !== undefined) {
currentSelectorItem.attributes = currentSelectorItem.attributes || [];
currentSelectorItem.attributes.push({
name: match[10].toLowerCase(),
operator: match[11] || null,
value: match[12].replace(ESCAPED_CHARACTER_REGEXP, ''),
modifier: null,
regExp: this.getAttributeRegExp({ operator: match[11], value: match[12] })
});
}
else if (match[13] && match[14]) {
currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
currentSelectorItem.pseudos.push(this.getPseudo(match[13], match[14], options));
}
else if (match[15]) {
currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
currentSelectorItem.pseudos.push(this.getPseudo(match[15], null, options));
}
else if (match[16]) {
currentSelectorItem.isPseudoElement = true;
}
else if (match[17]) {
switch (match[17].trim()) {
case ',':
currentSelectorItem = new SelectorItem({
combinator: SelectorCombinatorEnum.descendant,
ignoreErrors
});
currentGroup = [currentSelectorItem];
groups.push(currentGroup);
break;
case '>':
currentSelectorItem = new SelectorItem({
combinator: SelectorCombinatorEnum.child,
ignoreErrors
});
currentGroup.push(currentSelectorItem);
break;
case '+':
currentSelectorItem = new SelectorItem({
combinator: SelectorCombinatorEnum.adjacentSibling,
ignoreErrors
});
currentGroup.push(currentSelectorItem);
break;
case '~':
currentSelectorItem = new SelectorItem({
combinator: SelectorCombinatorEnum.subsequentSibling,
ignoreErrors
});
currentGroup.push(currentSelectorItem);
break;
case '':
currentSelectorItem = new SelectorItem({
combinator: SelectorCombinatorEnum.descendant,
ignoreErrors
});
currentGroup.push(currentSelectorItem);
break;
}
}
}
else {
break;
}
}
if (!isValid) {
if (options?.ignoreErrors) {
return [];
}
throw new DOMException(`Invalid selector: "${selector}"`);
}
return groups;
}
/**
* Returns attribute RegExp.
*
* @param attribute Attribute.
* @param attribute.value Attribute value.
* @param attribute.operator Attribute operator.
* @param attribute.modifier Attribute modifier.
* @returns Attribute RegExp.
*/
static getAttributeRegExp(attribute) {
const modifier = attribute.modifier === 'i' ? 'i' : '';
if (!attribute.operator || !attribute.value) {
return null;
}
// Escape special regex characters in the value
const escapedValue = attribute.value.replace(ATTRIBUTE_ESCAPE_REGEXP, '\\$&');
switch (attribute.operator) {
// [attribute~="value"] - Contains a specified word.
case '~':
return new RegExp(`[- ]${escapedValue}|${escapedValue}[- ]|^${escapedValue}$`, modifier);
// [attribute|="value"] - Starts with the specified word.
case '|':
return new RegExp(`^${escapedValue}[- ]|^${escapedValue}$`, modifier);
// [attribute^="value"] - Begins with a specified value.
case '^':
return new RegExp(`^${escapedValue}`, modifier);
// [attribute$="value"] - Ends with a specified value.
case '$':
return new RegExp(`${escapedValue}$`, modifier);
// [attribute*="value"] - Contains a specified value.
case '*':
return new RegExp(`${escapedValue}`, modifier);
default:
return null;
}
}
/**
* Returns pseudo.
*
* @param name Pseudo name.
* @param args Pseudo arguments.
* @param [options] Options.
* @param [options.ignoreErrors] Ignores errors.
* @returns Pseudo.
*/
static getPseudo(name, args, options) {
const lowerName = name.toLowerCase();
if (args) {
args = args.trim();
}
if (!args) {
return { name: lowerName, arguments: null, selectorItems: null, nthFunction: null };
}
switch (lowerName) {
case 'nth-last-child':
case 'nth-child':
const nthOfIndex = args.indexOf(' of ');
const nthFunction = nthOfIndex !== -1 ? args.substring(0, nthOfIndex) : args;
const selectorItem = nthOfIndex !== -1
? this.getSelectorItem(args.substring(nthOfIndex + 4).trim(), options)
: null;
return {
name: lowerName,
arguments: args,
selectorItems: [selectorItem],
nthFunction: this.getPseudoNthFunction(nthFunction)
};
case 'nth-of-type':
case 'nth-last-of-type':
return {
name: lowerName,
arguments: args,
selectorItems: null,
nthFunction: this.getPseudoNthFunction(args)
};
case 'not':
const notSelectorItems = [];
for (const group of this.getSelectorGroups(args, options)) {
notSelectorItems.push(group[0]);
}
return {
name: lowerName,
arguments: args,
selectorItems: notSelectorItems,
nthFunction: null
};
case 'is':
case 'where':
const selectorItems = [];
for (const group of this.getSelectorGroups(args, options)) {
selectorItems.push(group[0]);
}
return {
name: lowerName,
arguments: args,
selectorItems,
nthFunction: null
};
case 'has':
const hasSelectorItems = [];
// The ":has()" pseudo selector doesn't allow for it to be nested inside another ":has()" pseudo selector, as it can lead to cyclic querying.
if (!args.includes(':has(')) {
let newArgs = args;
if (args[0] === '+') {
newArgs = args.replace('+', '');
}
else if (args[0] === '>') {
newArgs = args.replace('>', '');
}
for (const group of this.getSelectorGroups(newArgs, options)) {
hasSelectorItems.push(group[0]);
}
}
return {
name: lowerName,
arguments: args,
selectorItems: hasSelectorItems,
nthFunction: null
};
default:
return { name: lowerName, arguments: args, selectorItems: null, nthFunction: null };
}
}
/**
* Returns pseudo nth function.
*
* Based on:
* https://github.com/dperini/nwsapi/blob/master/src/nwsapi.js
*
* @param args Pseudo arguments.
* @returns Pseudo nth function.
*/
static getPseudoNthFunction(args) {
if (args === 'odd') {
return NTH_FUNCTION.odd;
}
else if (args === 'even') {
return NTH_FUNCTION.even;
}
const parts = args.replace(SPACE_REGEXP, '').split('n');
let partA = parseInt(parts[0], 10) || 0;
if (parts[0] == '-') {
partA = -1;
}
if (parts.length === 1) {
return (n) => n == partA;
}
let partB = parseInt(parts[1], 10) || 0;
if (parts[0] == '+') {
partB = 1;
}
if (partA >= 1 || partA <= -1) {
if (partA >= 1) {
if (Math.abs(partA) === 1) {
return (n) => n > partB - 1;
}
return (n) => n > partB - 1 && (n + -1 * partB) % partA === 0;
}
if (Math.abs(partA) === 1) {
return (n) => n < partB + 1;
}
return (n) => n < partB + 1 && (n + -1 * partB) % partA === 0;
}
if (parts[0]) {
return (n) => n === partB;
}
return (n) => n > partB - 1;
}
}
//# sourceMappingURL=SelectorParser.js.map

File diff suppressed because one or more lines are too long