aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js
diff options
context:
space:
mode:
Diffstat (limited to 'vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js')
-rw-r--r--vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js1107
1 files changed, 0 insertions, 1107 deletions
diff --git a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js
deleted file mode 100644
index ce141a4..0000000
--- a/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js
+++ /dev/null
@@ -1,1107 +0,0 @@
-/**
- * utility.js
- */
-
-/* import */
-import nwsapi from '@asamuzakjp/nwsapi';
-import bidiFactory from 'bidi-js';
-import * as cssTree from 'css-tree';
-import isCustomElementName from 'is-potential-custom-element-name';
-
-/* constants */
-import {
- ATRULE,
- COMBO,
- COMPOUND_I,
- DESCEND,
- DOCUMENT_FRAGMENT_NODE,
- DOCUMENT_NODE,
- DOCUMENT_POSITION_CONTAINS,
- DOCUMENT_POSITION_PRECEDING,
- ELEMENT_NODE,
- HAS_COMPOUND,
- INPUT_BUTTON,
- INPUT_EDIT,
- INPUT_LTR,
- INPUT_TEXT,
- KEYS_LOGICAL,
- LOGIC_COMPLEX,
- LOGIC_COMPOUND,
- N_TH,
- PSEUDO_CLASS,
- RULE,
- SCOPE,
- SELECTOR_LIST,
- SIBLING,
- TARGET_ALL,
- TARGET_FIRST,
- TEXT_NODE,
- TYPE_FROM,
- TYPE_TO
-} from './constant.js';
-const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
-const KEYS_DIR_LTR = new Set(INPUT_LTR);
-const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
-const KEYS_NODE_DIR_EXCLUDE = new Set(['bdi', 'script', 'style', 'textarea']);
-const KEYS_NODE_FOCUSABLE = new Set(['button', 'select', 'textarea']);
-const KEYS_NODE_FOCUSABLE_SVG = new Set([
- 'clipPath',
- 'defs',
- 'desc',
- 'linearGradient',
- 'marker',
- 'mask',
- 'metadata',
- 'pattern',
- 'radialGradient',
- 'script',
- 'style',
- 'symbol',
- 'title'
-]);
-const REG_EXCLUDE_BASIC =
- /[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)/;
-const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
-const REG_DESCEND = new RegExp(`${COMPOUND_I}${DESCEND}${COMPOUND_I}`, 'i');
-const REG_SIBLING = new RegExp(`${COMPOUND_I}${SIBLING}${COMPOUND_I}`, 'i');
-const REG_LOGIC_COMPLEX = new RegExp(
- `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
-);
-const REG_LOGIC_COMPOUND = new RegExp(
- `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
-);
-const REG_LOGIC_HAS_COMPOUND = new RegExp(
- `:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
-);
-const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
-const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
-const REG_IS_HTML = /^(?:application\/xhtml\+x|text\/ht)ml$/;
-const REG_IS_XML =
- /^(?:application\/(?:[\w\-.]+\+)?|image\/[\w\-.]+\+|text\/)xml$/;
-
-/**
- * Manages state for extracting nested selectors from a CSS AST.
- */
-class SelectorExtractor {
- constructor() {
- this.selectors = [];
- this.isScoped = false;
- }
-
- /**
- * Walker enter function.
- * @param {object} node - The AST node.
- */
- enter(node) {
- switch (node.type) {
- case ATRULE: {
- if (node.name === 'scope') {
- this.isScoped = true;
- }
- break;
- }
- case SCOPE: {
- const { children, type } = node.root;
- const arr = [];
- if (type === SELECTOR_LIST) {
- for (const child of children) {
- const selector = cssTree.generate(child);
- arr.push(selector);
- }
- this.selectors.push(arr);
- }
- break;
- }
- case RULE: {
- const { children, type } = node.prelude;
- const arr = [];
- if (type === SELECTOR_LIST) {
- let hasAmp = false;
- for (const child of children) {
- const selector = cssTree.generate(child);
- if (this.isScoped && !hasAmp) {
- hasAmp = /\x26/.test(selector);
- }
- arr.push(selector);
- }
- if (this.isScoped) {
- if (hasAmp) {
- this.selectors.push(arr);
- /* FIXME:
- } else {
- this.selectors = arr;
- this.isScoped = false;
- */
- }
- } else {
- this.selectors.push(arr);
- }
- }
- }
- }
- }
-
- /**
- * Walker leave function.
- * @param {object} node - The AST node.
- */
- leave(node) {
- if (node.type === ATRULE) {
- if (node.name === 'scope') {
- this.isScoped = false;
- }
- }
- }
-}
-
-/**
- * Get type of an object.
- * @param {object} o - Object to check.
- * @returns {string} - Type of the object.
- */
-export const getType = o =>
- Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO);
-
-/**
- * Verify array contents.
- * @param {Array} arr - The array.
- * @param {string} type - Expected type, e.g. 'String'.
- * @throws {TypeError} - Throws if array or its items are of unexpected type.
- * @returns {Array} - The verified array.
- */
-export const verifyArray = (arr, type) => {
- if (!Array.isArray(arr)) {
- throw new TypeError(`Unexpected type ${getType(arr)}`);
- }
- if (typeof type !== 'string') {
- throw new TypeError(`Unexpected type ${getType(type)}`);
- }
- for (const item of arr) {
- if (getType(item) !== type) {
- throw new TypeError(`Unexpected type ${getType(item)}`);
- }
- }
- return arr;
-};
-
-/**
- * Generate a DOMException.
- * @param {string} msg - The error message.
- * @param {string} name - The error name.
- * @param {object} globalObject - The global object (e.g., window).
- * @returns {DOMException} The generated DOMException object.
- */
-export const generateException = (msg, name, globalObject = globalThis) => {
- return new globalObject.DOMException(msg, name);
-};
-
-/**
- * Find a nested :has() pseudo-class.
- * @param {object} leaf - The AST leaf to check.
- * @returns {?object} The leaf if it's :has, otherwise null.
- */
-export const findNestedHas = leaf => {
- return leaf.name === 'has';
-};
-
-/**
- * Find a logical pseudo-class that contains a nested :has().
- * @param {object} leaf - The AST leaf to check.
- * @returns {?object} The leaf if it matches, otherwise null.
- */
-export const findLogicalWithNestedHas = leaf => {
- if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
- return leaf;
- }
- return null;
-};
-
-/**
- * Filter a list of nodes based on An+B logic
- * @param {Array.<object>} nodes - array of nodes to filter
- * @param {object} anb - An+B options
- * @param {number} anb.a - a
- * @param {number} anb.b - b
- * @param {boolean} [anb.reverse] - reverse order
- * @returns {Array.<object>} - array of matched nodes
- */
-export const filterNodesByAnB = (nodes, anb) => {
- const { a, b, reverse } = anb;
- const processedNodes = reverse ? [...nodes].reverse() : nodes;
- const l = nodes.length;
- const matched = [];
- if (a === 0) {
- if (b > 0 && b <= l) {
- matched.push(processedNodes[b - 1]);
- }
- return matched;
- }
- let startIndex = b - 1;
- if (a > 0) {
- while (startIndex < 0) {
- startIndex += a;
- }
- for (let i = startIndex; i < l; i += a) {
- matched.push(processedNodes[i]);
- }
- } else if (startIndex >= 0) {
- for (let i = startIndex; i >= 0; i += a) {
- matched.push(processedNodes[i]);
- }
- return matched.reverse();
- }
- return matched;
-};
-
-/**
- * Resolve content document, root node, and check if it's in a shadow DOM.
- * @param {object} node - Document, DocumentFragment, or Element node.
- * @returns {Array.<object|boolean>} - [document, root, isInShadow].
- */
-export const resolveContent = node => {
- if (!node?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(node)}`);
- }
- let document;
- let root;
- let shadow;
- switch (node.nodeType) {
- case DOCUMENT_NODE: {
- document = node;
- root = node;
- break;
- }
- case DOCUMENT_FRAGMENT_NODE: {
- const { host, mode, ownerDocument } = node;
- document = ownerDocument;
- root = node;
- shadow = host && (mode === 'close' || mode === 'open');
- break;
- }
- case ELEMENT_NODE: {
- document = node.ownerDocument;
- let refNode = node;
- while (refNode) {
- const { host, mode, nodeType, parentNode } = refNode;
- if (nodeType === DOCUMENT_FRAGMENT_NODE) {
- shadow = host && (mode === 'close' || mode === 'open');
- break;
- } else if (parentNode) {
- refNode = parentNode;
- } else {
- break;
- }
- }
- root = refNode;
- break;
- }
- default: {
- throw new TypeError(`Unexpected node ${node.nodeName}`);
- }
- }
- return [document, root, !!shadow];
-};
-
-/**
- * Traverse node tree with a TreeWalker.
- * @param {object} node - The target node.
- * @param {object} walker - The TreeWalker instance.
- * @param {boolean} [force] - Traverse only to the next node.
- * @returns {?object} - The current node if found, otherwise null.
- */
-export const traverseNode = (node, walker, force = false) => {
- if (!node?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(node)}`);
- }
- if (!walker) {
- return null;
- }
- let refNode = walker.currentNode;
- if (refNode === node) {
- return refNode;
- } else if (force || refNode.contains(node)) {
- refNode = walker.nextNode();
- while (refNode) {
- if (refNode === node) {
- break;
- }
- refNode = walker.nextNode();
- }
- return refNode;
- } else {
- if (refNode !== walker.root) {
- let bool;
- while (refNode) {
- if (refNode === node) {
- bool = true;
- break;
- } else if (refNode === walker.root || refNode.contains(node)) {
- break;
- }
- refNode = walker.parentNode();
- }
- if (bool) {
- return refNode;
- }
- }
- if (node.nodeType === ELEMENT_NODE) {
- let bool;
- while (refNode) {
- if (refNode === node) {
- bool = true;
- break;
- }
- refNode = walker.nextNode();
- }
- if (bool) {
- return refNode;
- }
- }
- }
- return null;
-};
-
-/**
- * Check if a node is a custom element.
- * @param {object} node - The Element node.
- * @param {object} [opt] - Options.
- * @returns {boolean} - True if it's a custom element.
- */
-export const isCustomElement = (node, opt = {}) => {
- if (!node?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(node)}`);
- }
- if (node.nodeType !== ELEMENT_NODE) {
- return false;
- }
- const { localName, ownerDocument } = node;
- const { formAssociated } = opt;
- const window = ownerDocument.defaultView;
- let elmConstructor;
- const attr = node.getAttribute('is');
- if (attr) {
- elmConstructor =
- isCustomElementName(attr) && window.customElements.get(attr);
- } else {
- elmConstructor =
- isCustomElementName(localName) && window.customElements.get(localName);
- }
- if (elmConstructor) {
- if (formAssociated) {
- return !!elmConstructor.formAssociated;
- }
- return true;
- }
- return false;
-};
-
-/**
- * Get slotted text content.
- * @param {object} node - The Element node (likely a <slot>).
- * @returns {?string} - The text content.
- */
-export const getSlottedTextContent = node => {
- if (!node?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(node)}`);
- }
- if (typeof node.assignedNodes !== 'function') {
- return null;
- }
- const nodes = node.assignedNodes();
- if (nodes.length) {
- let text = '';
- const l = nodes.length;
- for (let i = 0; i < l; i++) {
- const item = nodes[i];
- text = item.textContent.trim();
- if (text) {
- break;
- }
- }
- return text;
- }
- return node.textContent.trim();
-};
-
-/**
- * Get directionality of a node.
- * @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
- * @param {object} node - The Element node.
- * @returns {?string} - 'ltr' or 'rtl'.
- */
-export const getDirectionality = node => {
- if (!node?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(node)}`);
- }
- if (node.nodeType !== ELEMENT_NODE) {
- return null;
- }
- const { dir: dirAttr, localName, parentNode } = node;
- const { getEmbeddingLevels } = bidiFactory();
- if (dirAttr === 'ltr' || dirAttr === 'rtl') {
- return dirAttr;
- } else if (dirAttr === 'auto') {
- let text = '';
- switch (localName) {
- case 'input': {
- if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
- text = node.value;
- } else if (KEYS_DIR_LTR.has(node.type)) {
- return 'ltr';
- }
- break;
- }
- case 'slot': {
- text = getSlottedTextContent(node);
- break;
- }
- case 'textarea': {
- text = node.value;
- break;
- }
- default: {
- const items = [].slice.call(node.childNodes);
- for (const item of items) {
- const {
- dir: itemDir,
- localName: itemLocalName,
- nodeType: itemNodeType,
- textContent: itemTextContent
- } = item;
- if (itemNodeType === TEXT_NODE) {
- text = itemTextContent.trim();
- } else if (
- itemNodeType === ELEMENT_NODE &&
- !KEYS_NODE_DIR_EXCLUDE.has(itemLocalName) &&
- (!itemDir || (itemDir !== 'ltr' && itemDir !== 'rtl'))
- ) {
- if (itemLocalName === 'slot') {
- text = getSlottedTextContent(item);
- } else {
- text = itemTextContent.trim();
- }
- }
- if (text) {
- break;
- }
- }
- }
- }
- if (text) {
- const {
- paragraphs: [{ level }]
- } = getEmbeddingLevels(text);
- if (level % 2 === 1) {
- return 'rtl';
- }
- } else if (parentNode) {
- const { nodeType: parentNodeType } = parentNode;
- if (parentNodeType === ELEMENT_NODE) {
- return getDirectionality(parentNode);
- }
- }
- } else if (localName === 'input' && node.type === 'tel') {
- return 'ltr';
- } else if (localName === 'bdi') {
- const text = node.textContent.trim();
- if (text) {
- const {
- paragraphs: [{ level }]
- } = getEmbeddingLevels(text);
- if (level % 2 === 1) {
- return 'rtl';
- }
- }
- } else if (parentNode) {
- if (localName === 'slot') {
- const text = getSlottedTextContent(node);
- if (text) {
- const {
- paragraphs: [{ level }]
- } = getEmbeddingLevels(text);
- if (level % 2 === 1) {
- return 'rtl';
- }
- return 'ltr';
- }
- }
- const { nodeType: parentNodeType } = parentNode;
- if (parentNodeType === ELEMENT_NODE) {
- return getDirectionality(parentNode);
- }
- }
- return 'ltr';
-};
-
-/**
- * Traverses up the DOM tree to find the language attribute for a node.
- * It checks for 'lang' in HTML and 'xml:lang' in XML contexts.
- * @param {object} node - The starting element node.
- * @returns {string|null} The language attribute value, or null if not found.
- */
-export const getLanguageAttribute = node => {
- if (!node?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(node)}`);
- }
- if (node.nodeType !== ELEMENT_NODE) {
- return null;
- }
- const { contentType } = node.ownerDocument;
- const isHtml = REG_IS_HTML.test(contentType);
- const isXml = REG_IS_XML.test(contentType);
- let isShadow = false;
- // Traverse up from the current node to the root.
- let current = node;
- while (current) {
- // Check if the current node is an element.
- switch (current.nodeType) {
- case ELEMENT_NODE: {
- // Check for and return the language attribute if present.
- if (isHtml && current.hasAttribute('lang')) {
- return current.getAttribute('lang');
- } else if (isXml && current.hasAttribute('xml:lang')) {
- return current.getAttribute('xml:lang');
- }
- break;
- }
- case DOCUMENT_FRAGMENT_NODE: {
- // Continue traversal if the current node is a shadow root.
- if (current.host) {
- isShadow = true;
- }
- break;
- }
- case DOCUMENT_NODE:
- default: {
- // Stop if we reach the root document node.
- return null;
- }
- }
- if (isShadow) {
- current = current.host;
- isShadow = false;
- } else if (current.parentNode) {
- current = current.parentNode;
- } else {
- break;
- }
- }
- // No language attribute was found in the hierarchy.
- return null;
-};
-
-/**
- * Check if content is editable.
- * NOTE: Not implemented in jsdom https://github.com/jsdom/jsdom/issues/1670
- * @param {object} node - The Element node.
- * @returns {boolean} - True if content is editable.
- */
-export const isContentEditable = node => {
- if (!node?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(node)}`);
- }
- if (node.nodeType !== ELEMENT_NODE) {
- return false;
- }
- if (typeof node.isContentEditable === 'boolean') {
- return node.isContentEditable;
- } else if (node.ownerDocument.designMode === 'on') {
- return true;
- } else {
- let attr;
- if (node.hasAttribute('contenteditable')) {
- attr = node.getAttribute('contenteditable');
- } else {
- attr = 'inherit';
- }
- switch (attr) {
- case '':
- case 'true': {
- return true;
- }
- case 'plaintext-only': {
- // FIXME:
- // @see https://github.com/w3c/editing/issues/470
- // @see https://github.com/whatwg/html/issues/10651
- return true;
- }
- case 'false': {
- return false;
- }
- default: {
- if (node?.parentNode?.nodeType === ELEMENT_NODE) {
- return isContentEditable(node.parentNode);
- }
- return false;
- }
- }
- }
-};
-
-/**
- * Check if a node is visible.
- * @param {object} node - The Element node.
- * @returns {boolean} - True if the node is visible.
- */
-export const isVisible = node => {
- if (node?.nodeType !== ELEMENT_NODE) {
- return false;
- }
- const window = node.ownerDocument.defaultView;
- const { display, visibility } = window.getComputedStyle(node);
- if (display !== 'none' && visibility === 'visible') {
- return true;
- }
- return false;
-};
-
-/**
- * Check if focus is visible on the node.
- * @param {object} node - The Element node.
- * @returns {boolean} - True if focus is visible.
- */
-export const isFocusVisible = node => {
- if (node?.nodeType !== ELEMENT_NODE) {
- return false;
- }
- const { localName, type } = node;
- switch (localName) {
- case 'input': {
- if (!type || KEYS_INPUT_EDIT.has(type)) {
- return true;
- }
- return false;
- }
- case 'textarea': {
- return true;
- }
- default: {
- return isContentEditable(node);
- }
- }
-};
-
-/**
- * Check if an area is focusable.
- * @param {object} node - The Element node.
- * @returns {boolean} - True if the area is focusable.
- */
-export const isFocusableArea = node => {
- if (node?.nodeType !== ELEMENT_NODE) {
- return false;
- }
- if (!node.isConnected) {
- return false;
- }
- const window = node.ownerDocument.defaultView;
- if (node instanceof window.HTMLElement) {
- if (Number.isInteger(parseInt(node.getAttribute('tabindex')))) {
- return true;
- }
- if (isContentEditable(node)) {
- return true;
- }
- const { localName, parentNode } = node;
- switch (localName) {
- case 'a': {
- if (node.href || node.hasAttribute('href')) {
- return true;
- }
- return false;
- }
- case 'iframe': {
- return true;
- }
- case 'input': {
- if (
- node.disabled ||
- node.hasAttribute('disabled') ||
- node.hidden ||
- node.hasAttribute('hidden')
- ) {
- return false;
- }
- return true;
- }
- case 'summary': {
- if (parentNode.localName === 'details') {
- let child = parentNode.firstElementChild;
- let bool = false;
- while (child) {
- if (child.localName === 'summary') {
- bool = child === node;
- break;
- }
- child = child.nextElementSibling;
- }
- return bool;
- }
- return false;
- }
- default: {
- if (
- KEYS_NODE_FOCUSABLE.has(localName) &&
- !(node.disabled || node.hasAttribute('disabled'))
- ) {
- return true;
- }
- }
- }
- } else if (node instanceof window.SVGElement) {
- if (Number.isInteger(parseInt(node.getAttributeNS(null, 'tabindex')))) {
- const ns = 'http://www.w3.org/2000/svg';
- let bool;
- let refNode = node;
- while (refNode.namespaceURI === ns) {
- bool = KEYS_NODE_FOCUSABLE_SVG.has(refNode.localName);
- if (bool) {
- break;
- }
- if (refNode?.parentNode?.namespaceURI === ns) {
- refNode = refNode.parentNode;
- } else {
- break;
- }
- }
- if (bool) {
- return false;
- }
- return true;
- }
- if (
- node.localName === 'a' &&
- (node.href || node.hasAttributeNS(null, 'href'))
- ) {
- return true;
- }
- }
- return false;
-};
-
-/**
- * Check if a node is focusable.
- * NOTE: Not applied, needs fix in jsdom itself.
- * @see https://github.com/whatwg/html/pull/8392
- * @see https://phabricator.services.mozilla.com/D156219
- * @see https://github.com/jsdom/jsdom/issues/3029
- * @see https://github.com/jsdom/jsdom/issues/3464
- * @param {object} node - The Element node.
- * @returns {boolean} - True if the node is focusable.
- */
-export const isFocusable = node => {
- if (node?.nodeType !== ELEMENT_NODE) {
- return false;
- }
- const window = node.ownerDocument.defaultView;
- let refNode = node;
- let res = true;
- while (refNode) {
- if (refNode.disabled || refNode.hasAttribute('disabled')) {
- res = false;
- break;
- }
- if (refNode.hidden || refNode.hasAttribute('hidden')) {
- res = false;
- }
- const { contentVisibility, display, visibility } =
- window.getComputedStyle(refNode);
- if (
- display === 'none' ||
- visibility !== 'visible' ||
- (contentVisibility === 'hidden' && refNode !== node)
- ) {
- res = false;
- } else {
- res = true;
- }
- if (res && refNode?.parentNode?.nodeType === ELEMENT_NODE) {
- refNode = refNode.parentNode;
- } else {
- break;
- }
- }
- return res;
-};
-
-/**
- * Get namespace URI.
- * @param {string} ns - The namespace prefix.
- * @param {object} node - The Element node.
- * @returns {?string} - The namespace URI.
- */
-export const getNamespaceURI = (ns, node) => {
- if (typeof ns !== 'string') {
- throw new TypeError(`Unexpected type ${getType(ns)}`);
- } else if (!node?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(node)}`);
- }
- if (!ns || node.nodeType !== ELEMENT_NODE) {
- return null;
- }
- const { attributes } = node;
- let res;
- for (const attr of attributes) {
- const { name, namespaceURI, prefix, value } = attr;
- if (name === `xmlns:${ns}`) {
- res = value;
- } else if (prefix === ns) {
- res = namespaceURI;
- }
- if (res) {
- break;
- }
- }
- return res ?? null;
-};
-
-/**
- * Check if a namespace is declared.
- * @param {string} ns - The namespace.
- * @param {object} node - The Element node.
- * @returns {boolean} - True if the namespace is declared.
- */
-export const isNamespaceDeclared = (ns = '', node = {}) => {
- if (!ns || typeof ns !== 'string' || node?.nodeType !== ELEMENT_NODE) {
- return false;
- }
- if (node.lookupNamespaceURI(ns)) {
- return true;
- }
- const root = node.ownerDocument.documentElement;
- let parent = node;
- let res;
- while (parent) {
- res = getNamespaceURI(ns, parent);
- if (res || parent === root) {
- break;
- }
- parent = parent.parentNode;
- }
- return !!res;
-};
-
-/**
- * Check if nodeA precedes and/or contains nodeB.
- * @param {object} nodeA - The first Element node.
- * @param {object} nodeB - The second Element node.
- * @returns {boolean} - True if nodeA precedes nodeB.
- */
-export const isPreceding = (nodeA, nodeB) => {
- if (!nodeA?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(nodeA)}`);
- } else if (!nodeB?.nodeType) {
- throw new TypeError(`Unexpected type ${getType(nodeB)}`);
- }
- if (nodeA.nodeType !== ELEMENT_NODE || nodeB.nodeType !== ELEMENT_NODE) {
- return false;
- }
- const posBit = nodeB.compareDocumentPosition(nodeA);
- const res =
- posBit & DOCUMENT_POSITION_PRECEDING || posBit & DOCUMENT_POSITION_CONTAINS;
- return !!res;
-};
-
-/**
- * Comparison function for sorting nodes based on document position.
- * @param {object} a - The first node.
- * @param {object} b - The second node.
- * @returns {number} - Sort order.
- */
-export const compareNodes = (a, b) => {
- if (isPreceding(b, a)) {
- return 1;
- }
- return -1;
-};
-
-/**
- * Sort a collection of nodes.
- * @param {Array.<object>|Set.<object>} nodes - Collection of nodes.
- * @returns {Array.<object>} - Collection of sorted nodes.
- */
-export const sortNodes = (nodes = []) => {
- const arr = [...nodes];
- if (arr.length > 1) {
- arr.sort(compareNodes);
- }
- return arr;
-};
-
-/**
- * Concat an array of nested selectors into an equivalent single selector.
- * @param {Array.<Array.<string>>} selectors - [parents, children, ...].
- * @returns {string} - The concatenated selector.
- */
-export const concatNestedSelectors = selectors => {
- if (!Array.isArray(selectors)) {
- throw new TypeError(`Unexpected type ${getType(selectors)}`);
- }
- let selector = '';
- if (selectors.length) {
- const revSelectors = selectors.toReversed();
- let child = verifyArray(revSelectors.shift(), 'String');
- if (child.length === 1) {
- [child] = child;
- }
- while (revSelectors.length) {
- const parentArr = verifyArray(revSelectors.shift(), 'String');
- if (!parentArr.length) {
- continue;
- }
- let parent;
- if (parentArr.length === 1) {
- [parent] = parentArr;
- if (!/^[>~+]/.test(parent) && /[\s>~+]/.test(parent)) {
- parent = `:is(${parent})`;
- }
- } else {
- parent = `:is(${parentArr.join(', ')})`;
- }
- if (selector.includes('\x26')) {
- selector = selector.replace(/\x26/g, parent);
- }
- if (Array.isArray(child)) {
- const items = [];
- for (let item of child) {
- if (item.includes('\x26')) {
- if (/^[>~+]/.test(item)) {
- item = `${parent} ${item.replace(/\x26/g, parent)} ${selector}`;
- } else {
- item = `${item.replace(/\x26/g, parent)} ${selector}`;
- }
- } else {
- item = `${parent} ${item} ${selector}`;
- }
- items.push(item.trim());
- }
- selector = items.join(', ');
- } else if (revSelectors.length) {
- selector = `${child} ${selector}`;
- } else {
- if (child.includes('\x26')) {
- if (/^[>~+]/.test(child)) {
- selector = `${parent} ${child.replace(/\x26/g, parent)} ${selector}`;
- } else {
- selector = `${child.replace(/\x26/g, parent)} ${selector}`;
- }
- } else {
- selector = `${parent} ${child} ${selector}`;
- }
- }
- selector = selector.trim();
- if (revSelectors.length) {
- child = parentArr.length > 1 ? parentArr : parent;
- } else {
- break;
- }
- }
- selector = selector.replace(/\x26/g, ':scope').trim();
- }
- return selector;
-};
-
-/**
- * Extract nested selectors from CSSRule.cssText.
- * @param {string} css - CSSRule.cssText.
- * @returns {Array.<Array.<string>>} - Array of nested selectors.
- */
-export const extractNestedSelectors = css => {
- const ast = cssTree.parse(css, {
- context: 'rule'
- });
- const extractor = new SelectorExtractor();
- cssTree.walk(ast, {
- enter: extractor.enter.bind(extractor),
- leave: extractor.leave.bind(extractor)
- });
- return extractor.selectors;
-};
-
-/**
- * Initialize nwsapi.
- * @param {object} window - The Window object.
- * @param {object} document - The Document object.
- * @returns {object} - The nwsapi instance.
- */
-export const initNwsapi = (window, document) => {
- if (!window?.DOMException) {
- throw new TypeError(`Unexpected global object ${getType(window)}`);
- }
- if (document?.nodeType !== DOCUMENT_NODE) {
- document = window.document;
- }
- const nw = nwsapi({
- document,
- DOMException: window.DOMException
- });
- nw.configure({
- LOGERRORS: false
- });
- return nw;
-};
-
-/**
- * Filter a selector for use with nwsapi.
- * @param {string} selector - The selector string.
- * @param {string} target - The target type.
- * @returns {boolean} - True if the selector is valid for nwsapi.
- */
-export const filterSelector = (selector, target) => {
- const isQuerySelectorType = target === TARGET_FIRST || target === TARGET_ALL;
- if (
- !selector ||
- typeof selector !== 'string' ||
- /null|undefined/.test(selector)
- ) {
- return false;
- }
- // Exclude missing close square bracket.
- if (selector.includes('[')) {
- const index = selector.lastIndexOf('[');
- const sel = selector.substring(index);
- if (sel.indexOf(']') < 0) {
- return false;
- }
- }
- // Exclude various complex or unsupported selectors.
- // - selectors containing '/'
- // - namespaced selectors
- // - escaped selectors
- // - pseudo-element selectors
- // - selectors containing non-ASCII
- // - selectors containing control character other than whitespace
- // - attribute selectors with case flag, e.g. [attr i]
- // - attribute selectors with unclosed quotes
- // - empty :is() or :where()
- if (selector.includes('/') || REG_EXCLUDE_BASIC.test(selector)) {
- return false;
- }
- // Include pseudo-classes that are known to work correctly.
- if (selector.includes(':')) {
- let complex = false;
- if (target !== isQuerySelectorType) {
- complex = REG_COMPLEX.test(selector);
- }
- if (
- isQuerySelectorType &&
- REG_DESCEND.test(selector) &&
- !REG_SIBLING.test(selector)
- ) {
- return false;
- } else if (!isQuerySelectorType && /:has\(/.test(selector)) {
- if (!complex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
- return false;
- }
- return REG_END_WITH_HAS.test(selector);
- } else if (/:(?:is|not)\(/.test(selector)) {
- if (complex) {
- return !REG_LOGIC_COMPLEX.test(selector);
- } else {
- return !REG_LOGIC_COMPOUND.test(selector);
- }
- } else {
- return !REG_WO_LOGICAL.test(selector);
- }
- }
- return true;
-};