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, 1107 insertions, 0 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
new file mode 100644
index 0000000..ce141a4
--- /dev/null
+++ b/vanilla/node_modules/@asamuzakjp/dom-selector/src/js/utility.js
@@ -0,0 +1,1107 @@
+/**
+ * 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;
+};