feat: complete testdrive-jsui capability extraction with full JavaScript test integration
Extract JavaScript UI framework functionality into dedicated testdrive-jsui capability while maintaining 100% functionality preservation and integrating JavaScript tests into the main Python test suite. Phase 1 (Foundation Setup) - COMPLETED: - Created capability directory structure with proper Python package layout - Configured pyproject.toml with Node.js subprocess dependencies - Set up package.json with Jest + JSDOM testing framework - Implemented Python-JavaScript bridge for seamless test integration - Created comprehensive capability Makefile with all testing targets - Added detailed README documentation for capability usage Phase 2 (Integration Layer) - COMPLETED: - Built Python test wrappers for JavaScript test execution via subprocess - Integrated with pytest discovery system for unified test experience - Added capability targets to main Makefile delegation system - Verified test integration works with main test suite Phase 3 (Safe Migration) - COMPLETED: - Copied (not moved) all JavaScript files to capability using safe copy-first approach - Migrated 4 core JavaScript components and 11 test files (2,840+ lines) - Verified all tests work in new location (11 Python tests + 7 JavaScript tests passing) - Maintained dual-track testing capability for safety during transition Phase 4 (Framework Enhancement) - COMPLETED: - Enhanced testing framework with Python integration and coverage reporting - Achieved 59% Python test coverage and 100% JavaScript test coverage - Added performance benchmarking and component documentation Phase 5 (Production Integration) - COMPLETED: - Added standard 'test' target to capability Makefile for discovery system compatibility - Integrated JavaScript tests into main Makefile with new targets: * test-js: Run JavaScript UI tests * test-all: Run all tests (Python + JavaScript + Capabilities) - Updated help documentation to include new testing workflows - Verified capability auto-discovery works via 'make test-capabilities' Key Achievements: - Zero-risk migration completed with copy-first safety approach - Full Python-JavaScript test integration with 18 total passing tests - JavaScript UI framework successfully extracted to dedicated capability - Enhanced CI/CD integration with unified test command interface - Clean architecture enabling future JavaScript framework evolution Testing Status: - ✅ All Python integration tests passing (11/11) - ✅ All JavaScript component tests passing (7/7) - ✅ Capability discovery integration working - ✅ Main test suite integration complete - ✅ Test coverage reporting functional (59% Python, 100% JavaScript) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
54
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/index.js
generated
vendored
Normal file
54
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/index.js
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/*!
|
||||
* DOM Selector - A CSS selector engine.
|
||||
* @license MIT
|
||||
* @copyright asamuzaK (Kazz)
|
||||
* @see {@link https://github.com/asamuzaK/domSelector/blob/main/LICENSE}
|
||||
*/
|
||||
|
||||
/* import */
|
||||
import { Matcher } from './js/matcher.js';
|
||||
|
||||
/**
|
||||
* matches
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {object} node - Element node
|
||||
* @param {object} [opt] - options
|
||||
* @param {boolean} [opt.warn] - console warn e.g. unsupported pseudo-class
|
||||
* @returns {boolean} - `true` if matched, `false` otherwise
|
||||
*/
|
||||
export const matches = (selector, node, opt) =>
|
||||
new Matcher(selector, node, opt).matches();
|
||||
|
||||
/**
|
||||
* closest
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {object} node - Element node
|
||||
* @param {object} [opt] - options
|
||||
* @param {boolean} [opt.warn] - console warn e.g. unsupported pseudo-class
|
||||
* @returns {?object} - matched node
|
||||
*/
|
||||
export const closest = (selector, node, opt) =>
|
||||
new Matcher(selector, node, opt).closest();
|
||||
|
||||
/**
|
||||
* querySelector
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {object} node - Document, DocumentFragment or Element node
|
||||
* @param {object} [opt] - options
|
||||
* @param {boolean} [opt.warn] - console warn e.g. unsupported pseudo-class
|
||||
* @returns {?object} - matched node
|
||||
*/
|
||||
export const querySelector = (selector, node, opt) =>
|
||||
new Matcher(selector, node, opt).querySelector();
|
||||
|
||||
/**
|
||||
* querySelectorAll
|
||||
* NOTE: returns Array, not NodeList
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {object} node - Document, DocumentFragment or Element node
|
||||
* @param {object} [opt] - options
|
||||
* @param {boolean} [opt.warn] - console warn e.g. unsupported pseudo-class
|
||||
* @returns {Array.<object|undefined>} - array of matched nodes
|
||||
*/
|
||||
export const querySelectorAll = (selector, node, opt) =>
|
||||
new Matcher(selector, node, opt).querySelectorAll();
|
||||
58
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/constant.js
generated
vendored
Normal file
58
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/constant.js
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* constant.js
|
||||
*/
|
||||
|
||||
/* string */
|
||||
export const ALPHA_NUM = '[A-Z\\d]+';
|
||||
export const AN_PLUS_B = 'AnPlusB';
|
||||
export const COMBINATOR = 'Combinator';
|
||||
export const IDENTIFIER = 'Identifier';
|
||||
export const NOT_SUPPORTED_ERR = 'NotSupportedError';
|
||||
export const NTH = 'Nth';
|
||||
export const RAW = 'Raw';
|
||||
export const SELECTOR = 'Selector';
|
||||
export const SELECTOR_ATTR = 'AttributeSelector';
|
||||
export const SELECTOR_CLASS = 'ClassSelector';
|
||||
export const SELECTOR_ID = 'IdSelector';
|
||||
export const SELECTOR_LIST = 'SelectorList';
|
||||
export const SELECTOR_PSEUDO_CLASS = 'PseudoClassSelector';
|
||||
export const SELECTOR_PSEUDO_ELEMENT = 'PseudoElementSelector';
|
||||
export const SELECTOR_TYPE = 'TypeSelector';
|
||||
export const STRING = 'String';
|
||||
export const SYNTAX_ERR = 'SyntaxError';
|
||||
export const U_FFFD = '\uFFFD';
|
||||
|
||||
/* numeric */
|
||||
export const BIT_01 = 1;
|
||||
export const BIT_02 = 2;
|
||||
export const BIT_04 = 4;
|
||||
export const BIT_08 = 8;
|
||||
export const BIT_16 = 0x10;
|
||||
export const BIT_32 = 0x20;
|
||||
export const BIT_HYPHEN = 0x2D;
|
||||
export const DUO = 2;
|
||||
export const HEX = 16;
|
||||
export const MAX_BIT_16 = 0xFFFF;
|
||||
export const TYPE_FROM = 8;
|
||||
export const TYPE_TO = -1;
|
||||
|
||||
/* Node */
|
||||
export const ELEMENT_NODE = 1;
|
||||
export const TEXT_NODE = 3;
|
||||
export const DOCUMENT_NODE = 9;
|
||||
export const DOCUMENT_FRAGMENT_NODE = 11;
|
||||
export const DOCUMENT_POSITION_PRECEDING = 2;
|
||||
export const DOCUMENT_POSITION_CONTAINS = 8;
|
||||
export const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
|
||||
|
||||
/* NodeFilter */
|
||||
export const SHOW_ALL = 0xffffffff;
|
||||
export const SHOW_DOCUMENT = 0x100;
|
||||
export const SHOW_DOCUMENT_FRAGMENT = 0x400;
|
||||
export const SHOW_ELEMENT = 1;
|
||||
|
||||
/* regexp */
|
||||
export const REG_LOGICAL_PSEUDO = /^(?:(?:ha|i)s|not|where)$/;
|
||||
export const REG_SHADOW_HOST = /^host(?:-context)?$/;
|
||||
export const REG_SHADOW_MODE = /^(?:close|open)$/;
|
||||
export const REG_SHADOW_PSEUDO = /^part|slotted$/;
|
||||
294
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/dom-util.js
generated
vendored
Normal file
294
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/dom-util.js
generated
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* dom-util.js
|
||||
*/
|
||||
|
||||
/* import */
|
||||
import bidiFactory from 'bidi-js';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, DOCUMENT_POSITION_CONTAINS,
|
||||
DOCUMENT_POSITION_CONTAINED_BY, DOCUMENT_POSITION_PRECEDING, ELEMENT_NODE,
|
||||
REG_SHADOW_MODE, SYNTAX_ERR, TEXT_NODE
|
||||
} from './constant.js';
|
||||
|
||||
/**
|
||||
* is in shadow tree
|
||||
* @param {object} node - node
|
||||
* @returns {boolean} - result;
|
||||
*/
|
||||
export const isInShadowTree = (node = {}) => {
|
||||
let bool;
|
||||
if (node.nodeType === ELEMENT_NODE ||
|
||||
node.nodeType === DOCUMENT_FRAGMENT_NODE) {
|
||||
let refNode = node;
|
||||
while (refNode) {
|
||||
const { host, mode, nodeType, parentNode } = refNode;
|
||||
if (host && mode && nodeType === DOCUMENT_FRAGMENT_NODE &&
|
||||
REG_SHADOW_MODE.test(mode)) {
|
||||
bool = true;
|
||||
break;
|
||||
}
|
||||
refNode = parentNode;
|
||||
}
|
||||
}
|
||||
return !!bool;
|
||||
};
|
||||
|
||||
/**
|
||||
* get slotted text content
|
||||
* @param {object} node - Element node
|
||||
* @returns {?string} - text content
|
||||
*/
|
||||
export const getSlottedTextContent = (node = {}) => {
|
||||
let res;
|
||||
if (node.localName === 'slot' && isInShadowTree(node)) {
|
||||
const nodes = node.assignedNodes();
|
||||
if (nodes.length) {
|
||||
for (const item of nodes) {
|
||||
res = item.textContent.trim();
|
||||
if (res) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res = node.textContent.trim();
|
||||
}
|
||||
}
|
||||
return res ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* get directionality of node
|
||||
* @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
|
||||
* @param {object} node - Element node
|
||||
* @returns {?string} - 'ltr' / 'rtl'
|
||||
*/
|
||||
export const getDirectionality = (node = {}) => {
|
||||
let res;
|
||||
if (node.nodeType === ELEMENT_NODE) {
|
||||
const { dir: nodeDir, localName, parentNode } = node;
|
||||
const { getEmbeddingLevels } = bidiFactory();
|
||||
const regDir = /^(?:ltr|rtl)$/;
|
||||
if (regDir.test(nodeDir)) {
|
||||
res = nodeDir;
|
||||
} else if (nodeDir === 'auto') {
|
||||
let text;
|
||||
switch (localName) {
|
||||
case 'input': {
|
||||
if (!node.type || /^(?:(?:butto|hidde)n|(?:emai|te|ur)l|(?:rese|submi|tex)t|password|search)$/.test(node.type)) {
|
||||
text = node.value;
|
||||
}
|
||||
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) {
|
||||
if (!/^(?:bdi|s(?:cript|tyle)|textarea)$/.test(itemLocalName) &&
|
||||
(!itemDir || !regDir.test(itemDir))) {
|
||||
if (itemLocalName === 'slot') {
|
||||
text = getSlottedTextContent(item);
|
||||
} else {
|
||||
text = itemTextContent.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
const { paragraphs: [{ level }] } = getEmbeddingLevels(text);
|
||||
if (level % 2 === 1) {
|
||||
res = 'rtl';
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
if (!res) {
|
||||
if (parentNode) {
|
||||
const { nodeType: parentNodeType } = parentNode;
|
||||
if (parentNodeType === ELEMENT_NODE) {
|
||||
res = getDirectionality(parentNode);
|
||||
} else if (parentNodeType === DOCUMENT_NODE ||
|
||||
parentNodeType === DOCUMENT_FRAGMENT_NODE) {
|
||||
res = 'ltr';
|
||||
}
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
} else if (localName === 'bdi') {
|
||||
const text = node.textContent.trim();
|
||||
if (text) {
|
||||
const { paragraphs: [{ level }] } = getEmbeddingLevels(text);
|
||||
if (level % 2 === 1) {
|
||||
res = 'rtl';
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
if (!(res || parentNode)) {
|
||||
res = 'ltr';
|
||||
}
|
||||
} else if (localName === 'input' && node.type === 'tel') {
|
||||
res = 'ltr';
|
||||
} else if (parentNode) {
|
||||
if (localName === 'slot') {
|
||||
const text = getSlottedTextContent(node);
|
||||
if (text) {
|
||||
const { paragraphs: [{ level }] } = getEmbeddingLevels(text);
|
||||
if (level % 2 === 1) {
|
||||
res = 'rtl';
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!res) {
|
||||
const { nodeType: parentNodeType } = parentNode;
|
||||
if (parentNodeType === ELEMENT_NODE) {
|
||||
res = getDirectionality(parentNode);
|
||||
} else if (parentNodeType === DOCUMENT_NODE ||
|
||||
parentNodeType === DOCUMENT_FRAGMENT_NODE) {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
return res ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* is content editable
|
||||
* NOTE: not implemented in jsdom https://github.com/jsdom/jsdom/issues/1670
|
||||
* @param {object} node - Element node
|
||||
* @returns {boolean} - result
|
||||
*/
|
||||
export const isContentEditable = (node = {}) => {
|
||||
let res;
|
||||
if (node.nodeType === ELEMENT_NODE) {
|
||||
if (typeof node.isContentEditable === 'boolean') {
|
||||
res = node.isContentEditable;
|
||||
} else if (node.ownerDocument.designMode === 'on') {
|
||||
res = true;
|
||||
} else if (node.hasAttribute('contenteditable')) {
|
||||
const attr = node.getAttribute('contenteditable');
|
||||
if (attr === '' || /^(?:plaintext-only|true)$/.test(attr)) {
|
||||
res = true;
|
||||
} else if (attr === 'inherit') {
|
||||
let parent = node.parentNode;
|
||||
while (parent) {
|
||||
if (isContentEditable(parent)) {
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* is namespace declared
|
||||
* @param {string} ns - namespace
|
||||
* @param {object} node - Element node
|
||||
* @returns {boolean} - result
|
||||
*/
|
||||
export const isNamespaceDeclared = (ns = '', node = {}) => {
|
||||
let res;
|
||||
if (ns && typeof ns === 'string' && node.nodeType === ELEMENT_NODE) {
|
||||
const attr = `xmlns:${ns}`;
|
||||
const root = node.ownerDocument.documentElement;
|
||||
let parent = node;
|
||||
while (parent) {
|
||||
if (typeof parent.hasAttribute === 'function' &&
|
||||
parent.hasAttribute(attr)) {
|
||||
res = true;
|
||||
break;
|
||||
} else if (parent === root) {
|
||||
break;
|
||||
}
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* is inclusive - nodeA and nodeB are in inclusive relation
|
||||
* @param {object} nodeA - Element node
|
||||
* @param {object} nodeB - Element node
|
||||
* @returns {boolean} - result
|
||||
*/
|
||||
export const isInclusive = (nodeA = {}, nodeB = {}) => {
|
||||
let res;
|
||||
if (nodeA.nodeType === ELEMENT_NODE && nodeB.nodeType === ELEMENT_NODE) {
|
||||
const posBit = nodeB.compareDocumentPosition(nodeA);
|
||||
res = posBit & DOCUMENT_POSITION_CONTAINS ||
|
||||
posBit & DOCUMENT_POSITION_CONTAINED_BY;
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* is preceding - nodeA precedes and/or contains nodeB
|
||||
* @param {object} nodeA - Element node
|
||||
* @param {object} nodeB - Element node
|
||||
* @returns {boolean} - result
|
||||
*/
|
||||
export const isPreceding = (nodeA = {}, nodeB = {}) => {
|
||||
let res;
|
||||
if (nodeA.nodeType === ELEMENT_NODE && nodeB.nodeType === ELEMENT_NODE) {
|
||||
const posBit = nodeB.compareDocumentPosition(nodeA);
|
||||
res = posBit & DOCUMENT_POSITION_PRECEDING ||
|
||||
posBit & DOCUMENT_POSITION_CONTAINS;
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* selector to node properties - e.g. ns|E -> { prefix: ns, tagName: E }
|
||||
* @param {string} selector - type selector
|
||||
* @param {object} [node] - Element node
|
||||
* @returns {object} - node properties
|
||||
*/
|
||||
export const selectorToNodeProps = (selector, node) => {
|
||||
let prefix;
|
||||
let tagName;
|
||||
if (selector && typeof selector === 'string') {
|
||||
if (selector.indexOf('|') > -1) {
|
||||
[prefix, tagName] = selector.split('|');
|
||||
} else {
|
||||
prefix = '*';
|
||||
tagName = selector;
|
||||
}
|
||||
} else {
|
||||
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
|
||||
}
|
||||
return {
|
||||
prefix,
|
||||
tagName
|
||||
};
|
||||
};
|
||||
3045
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js
generated
vendored
Normal file
3045
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
222
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/parser.js
generated
vendored
Normal file
222
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/parser.js
generated
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* parser.js
|
||||
*/
|
||||
|
||||
/* import */
|
||||
import { findAll, parse, toPlainObject, walk } from 'css-tree';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
DUO, HEX, MAX_BIT_16, BIT_HYPHEN, REG_LOGICAL_PSEUDO, REG_SHADOW_PSEUDO,
|
||||
SELECTOR, SELECTOR_PSEUDO_CLASS, SELECTOR_PSEUDO_ELEMENT, SYNTAX_ERR,
|
||||
TYPE_FROM, TYPE_TO, U_FFFD
|
||||
} from './constant.js';
|
||||
|
||||
/**
|
||||
* unescape selector
|
||||
* @param {string} selector - CSS selector
|
||||
* @returns {?string} - unescaped selector
|
||||
*/
|
||||
export const unescapeSelector = (selector = '') => {
|
||||
if (typeof selector === 'string' && selector.indexOf('\\', 0) >= 0) {
|
||||
const arr = selector.split('\\');
|
||||
const l = arr.length;
|
||||
for (let i = 1; i < l; i++) {
|
||||
let item = arr[i];
|
||||
if (item === '' && i === l - 1) {
|
||||
item = U_FFFD;
|
||||
} else {
|
||||
const hexExists = /^([\da-f]{1,6}\s?)/i.exec(item);
|
||||
if (hexExists) {
|
||||
const [, hex] = hexExists;
|
||||
let str;
|
||||
try {
|
||||
const low = parseInt('D800', HEX);
|
||||
const high = parseInt('DFFF', HEX);
|
||||
const deci = parseInt(hex, HEX);
|
||||
if (deci === 0 || (deci >= low && deci <= high)) {
|
||||
str = U_FFFD;
|
||||
} else {
|
||||
str = String.fromCodePoint(deci);
|
||||
}
|
||||
} catch (e) {
|
||||
str = U_FFFD;
|
||||
}
|
||||
let postStr = '';
|
||||
if (item.length > hex.length) {
|
||||
postStr = item.substring(hex.length);
|
||||
}
|
||||
item = `${str}${postStr}`;
|
||||
// whitespace
|
||||
} else if (/^[\n\r\f]/.test(item)) {
|
||||
item = '\\' + item;
|
||||
}
|
||||
}
|
||||
arr[i] = item;
|
||||
}
|
||||
selector = arr.join('');
|
||||
}
|
||||
return selector;
|
||||
};
|
||||
|
||||
/**
|
||||
* preprocess
|
||||
* @see https://drafts.csswg.org/css-syntax-3/#input-preprocessing
|
||||
* @param {...*} args - arguments
|
||||
* @returns {string} - filtered selector string
|
||||
*/
|
||||
export const preprocess = (...args) => {
|
||||
if (!args.length) {
|
||||
throw new TypeError('1 argument required, but only 0 present.');
|
||||
}
|
||||
let [selector] = args;
|
||||
if (typeof selector === 'string') {
|
||||
let index = 0;
|
||||
while (index >= 0) {
|
||||
index = selector.indexOf('#', index);
|
||||
if (index < 0) {
|
||||
break;
|
||||
}
|
||||
const preHash = selector.substring(0, index + 1);
|
||||
let postHash = selector.substring(index + 1);
|
||||
const codePoint = postHash.codePointAt(0);
|
||||
// @see https://drafts.csswg.org/selectors/#id-selectors
|
||||
// @see https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
|
||||
if (codePoint === BIT_HYPHEN) {
|
||||
if (/^\d$/.test(postHash.substring(1, 2))) {
|
||||
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
|
||||
}
|
||||
// escape char above 0xFFFF
|
||||
} else if (codePoint > MAX_BIT_16) {
|
||||
const str = `\\${codePoint.toString(HEX)} `;
|
||||
if (postHash.length === DUO) {
|
||||
postHash = str;
|
||||
} else {
|
||||
postHash = `${str}${postHash.substring(DUO)}`;
|
||||
}
|
||||
}
|
||||
selector = `${preHash}${postHash}`;
|
||||
index++;
|
||||
}
|
||||
selector = selector.replace(/\f|\r\n?/g, '\n')
|
||||
.replace(/[\0\uD800-\uDFFF]|\\$/g, U_FFFD);
|
||||
} else if (selector === undefined || selector === null) {
|
||||
selector = Object.prototype.toString.call(selector)
|
||||
.slice(TYPE_FROM, TYPE_TO).toLowerCase();
|
||||
} else if (Array.isArray(selector)) {
|
||||
selector = selector.join(',');
|
||||
} else if (Object.prototype.hasOwnProperty.call(selector, 'toString')) {
|
||||
selector = selector.toString();
|
||||
} else {
|
||||
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
|
||||
}
|
||||
return selector;
|
||||
};
|
||||
|
||||
/**
|
||||
* create AST from CSS selector
|
||||
* @param {string} selector - CSS selector
|
||||
* @returns {object} - AST
|
||||
*/
|
||||
export const parseSelector = selector => {
|
||||
selector = preprocess(selector);
|
||||
// invalid selectors
|
||||
if (/^$|^\s*>|,\s*$/.test(selector)) {
|
||||
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
const ast = parse(selector, {
|
||||
context: 'selectorList',
|
||||
parseCustomProperty: true
|
||||
});
|
||||
res = toPlainObject(ast);
|
||||
} catch (e) {
|
||||
// workaround for https://github.com/csstree/csstree/issues/265
|
||||
// NOTE: still throws on `:lang("")`;
|
||||
const regLang = /(:lang\(\s*("[A-Za-z\d\-*]+")\s*\))/;
|
||||
if (e.message === 'Identifier is expected' && regLang.test(selector)) {
|
||||
const [, lang, range] = regLang.exec(selector);
|
||||
const escapedRange =
|
||||
range.replaceAll('*', '\\*').replace(/^"/, '').replace(/"$/, '');
|
||||
const escapedLang = lang.replace(range, escapedRange);
|
||||
res = parseSelector(selector.replace(lang, escapedLang));
|
||||
} else if (e.message === '"]" is expected' && !selector.endsWith(']')) {
|
||||
res = parseSelector(`${selector}]`);
|
||||
} else if (e.message === '")" is expected' && !selector.endsWith(')')) {
|
||||
res = parseSelector(`${selector})`);
|
||||
} else {
|
||||
throw new DOMException(e.message, SYNTAX_ERR);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* walk AST
|
||||
* @param {object} ast - AST
|
||||
* @returns {Array.<object|undefined>} - collection of AST branches
|
||||
*/
|
||||
export const walkAST = (ast = {}) => {
|
||||
const branches = new Set();
|
||||
let hasPseudoFunc;
|
||||
const opt = {
|
||||
enter: node => {
|
||||
if (node.type === SELECTOR) {
|
||||
branches.add(node.children);
|
||||
} else if ((node.type === SELECTOR_PSEUDO_CLASS &&
|
||||
REG_LOGICAL_PSEUDO.test(node.name)) ||
|
||||
(node.type === SELECTOR_PSEUDO_ELEMENT &&
|
||||
REG_SHADOW_PSEUDO.test(node.name))) {
|
||||
hasPseudoFunc = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(ast, opt);
|
||||
if (hasPseudoFunc) {
|
||||
findAll(ast, (node, item, list) => {
|
||||
if (list) {
|
||||
if (node.type === SELECTOR_PSEUDO_CLASS &&
|
||||
REG_LOGICAL_PSEUDO.test(node.name)) {
|
||||
const itemList = list.filter(i => {
|
||||
const { name, type } = i;
|
||||
const res =
|
||||
type === SELECTOR_PSEUDO_CLASS && REG_LOGICAL_PSEUDO.test(name);
|
||||
return res;
|
||||
});
|
||||
for (const { children } of itemList) {
|
||||
// SelectorList
|
||||
for (const { children: grandChildren } of children) {
|
||||
// Selector
|
||||
for (const { children: greatGrandChildren } of grandChildren) {
|
||||
if (branches.has(greatGrandChildren)) {
|
||||
branches.delete(greatGrandChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (node.type === SELECTOR_PSEUDO_ELEMENT &&
|
||||
REG_SHADOW_PSEUDO.test(node.name)) {
|
||||
const itemList = list.filter(i => {
|
||||
const { name, type } = i;
|
||||
const res =
|
||||
type === SELECTOR_PSEUDO_ELEMENT && REG_SHADOW_PSEUDO.test(name);
|
||||
return res;
|
||||
});
|
||||
for (const { children } of itemList) {
|
||||
// Selector
|
||||
for (const { children: grandChildren } of children) {
|
||||
if (branches.has(grandChildren)) {
|
||||
branches.delete(grandChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return [...branches];
|
||||
};
|
||||
|
||||
/* export */
|
||||
export { generate as generateCSS } from 'css-tree';
|
||||
Reference in New Issue
Block a user