From 2542ffbda874a7616451ae39f3bb566264bf6019 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Thu, 31 Mar 2022 17:41:19 +0900 Subject: [PATCH 01/11] Add CSS selector parser --- lib/utils/selector.js | 664 ++++++++++++++++++ package.json | 1 + .../utils/selector/attr-contains/result.json | 17 + .../utils/selector/attr-contains/source.vue | 10 + .../selector/attr-empty-value/result.json | 12 + .../selector/attr-empty-value/source.vue | 11 + .../utils/selector/attr-eq/result.json | 12 + .../utils/selector/attr-eq/source.vue | 11 + .../utils/selector/attr-has/result.json | 12 + .../utils/selector/attr-has/source.vue | 8 + .../selector/attr-insensitive/result.json | 7 + .../selector/attr-insensitive/source.vue | 10 + .../utils/selector/attr-lang/result.json | 17 + .../utils/selector/attr-lang/source.vue | 9 + .../selector/attr-not-insensitive/result.json | 1 + .../selector/attr-not-insensitive/source.vue | 11 + .../utils/selector/attr-prefixed/result.json | 12 + .../utils/selector/attr-prefixed/source.vue | 10 + .../utils/selector/attr-suffixed/result.json | 12 + .../utils/selector/attr-suffixed/source.vue | 10 + .../utils/selector/attr-words/result.json | 12 + .../utils/selector/attr-words/source.vue | 11 + .../fixtures/utils/selector/class/result.json | 12 + .../fixtures/utils/selector/class/source.vue | 8 + .../selector/combinator-adjacent/result.json | 12 + .../selector/combinator-adjacent/source.vue | 14 + .../selector/combinator-child01/result.json | 17 + .../selector/combinator-child01/source.vue | 14 + .../selector/combinator-child02/result.json | 22 + .../selector/combinator-child02/source.vue | 162 +++++ .../combinator-descendant01/result.json | 22 + .../combinator-descendant01/source.vue | 14 + .../combinator-descendant02/result.json | 42 ++ .../combinator-descendant02/source.vue | 162 +++++ .../selector/combinator-sibling/result.json | 17 + .../selector/combinator-sibling/source.vue | 14 + .../utils/selector/comment01/result.json | 7 + .../utils/selector/comment01/source.vue | 7 + .../utils/selector/comment02/result.json | 7 + .../utils/selector/comment02/source.vue | 13 + .../utils/selector/comment03/result.json | 7 + .../utils/selector/comment03/source.vue | 13 + tests/fixtures/utils/selector/id/result.json | 7 + tests/fixtures/utils/selector/id/source.vue | 8 + .../selector/invalid-attr-op/result.json | 5 + .../utils/selector/invalid-attr-op/source.vue | 4 + .../selector/invalid-combinators/result.json | 5 + .../selector/invalid-combinators/source.vue | 6 + .../utils/selector/invalid-empty/result.json | 5 + .../utils/selector/invalid-empty/source.vue | 6 + .../invalid-last-combinator/result.json | 5 + .../invalid-last-combinator/source.vue | 6 + .../selector/invalid-nesting/result.json | 5 + .../utils/selector/invalid-nesting/source.vue | 6 + .../utils/selector/invalid-nth/result.json | 5 + .../utils/selector/invalid-nth/source.vue | 4 + .../selector/invalid-selector/result.json | 5 + .../selector/invalid-selector/source.vue | 4 + .../utils/selector/invalid-string/result.json | 5 + .../utils/selector/invalid-string/source.vue | 6 + .../invalid-unknown-combinator/result.json | 5 + .../invalid-unknown-combinator/source.vue | 6 + .../invalid-unknown-pseudo/result.json | 5 + .../invalid-unknown-pseudo/source.vue | 6 + .../utils/selector/pseudo-empty/result.json | 12 + .../utils/selector/pseudo-empty/source.vue | 11 + .../selector/pseudo-first-child01/result.json | 7 + .../selector/pseudo-first-child01/source.vue | 14 + .../selector/pseudo-first-child02/result.json | 12 + .../selector/pseudo-first-child02/source.vue | 14 + .../pseudo-first-of-type01/result.json | 7 + .../pseudo-first-of-type01/source.vue | 8 + .../pseudo-first-of-type02/result.json | 12 + .../pseudo-first-of-type02/source.vue | 8 + .../selector/pseudo-has-complex01/result.json | 12 + .../selector/pseudo-has-complex01/source.vue | 31 + .../selector/pseudo-has-complex02/result.json | 17 + .../selector/pseudo-has-complex02/source.vue | 31 + .../utils/selector/pseudo-has01/result.json | 17 + .../utils/selector/pseudo-has01/source.vue | 25 + .../utils/selector/pseudo-has02/result.json | 12 + .../utils/selector/pseudo-has02/source.vue | 25 + .../utils/selector/pseudo-has03/result.json | 12 + .../utils/selector/pseudo-has03/source.vue | 13 + .../utils/selector/pseudo-has04/result.json | 17 + .../utils/selector/pseudo-has04/source.vue | 13 + .../utils/selector/pseudo-is/result.json | 32 + .../utils/selector/pseudo-is/source.vue | 17 + .../selector/pseudo-last-child/result.json | 12 + .../selector/pseudo-last-child/source.vue | 14 + .../pseudo-last-of-type01/result.json | 7 + .../selector/pseudo-last-of-type01/source.vue | 8 + .../pseudo-last-of-type02/result.json | 12 + .../selector/pseudo-last-of-type02/source.vue | 8 + .../utils/selector/pseudo-not/result.json | 17 + .../utils/selector/pseudo-not/source.vue | 8 + .../selector/pseudo-nth-child01/result.json | 27 + .../selector/pseudo-nth-child01/source.vue | 14 + .../selector/pseudo-nth-child02/result.json | 12 + .../selector/pseudo-nth-child02/source.vue | 14 + .../selector/pseudo-nth-child03/result.json | 7 + .../selector/pseudo-nth-child03/source.vue | 14 + .../pseudo-nth-last-child01/result.json | 17 + .../pseudo-nth-last-child01/source.vue | 21 + .../pseudo-nth-last-child02/result.json | 22 + .../pseudo-nth-last-child02/source.vue | 21 + .../pseudo-nth-last-child03/result.json | 7 + .../pseudo-nth-last-child03/source.vue | 21 + .../pseudo-nth-last-of-type01/result.json | 7 + .../pseudo-nth-last-of-type01/source.vue | 11 + .../pseudo-nth-last-of-type02/result.json | 7 + .../pseudo-nth-last-of-type02/source.vue | 11 + .../selector/pseudo-nth-of-type01/result.json | 12 + .../selector/pseudo-nth-of-type01/source.vue | 11 + .../selector/pseudo-nth-of-type02/result.json | 12 + .../selector/pseudo-nth-of-type02/source.vue | 11 + .../selector/pseudo-nth-of-type03/result.json | 7 + .../selector/pseudo-nth-of-type03/source.vue | 11 + .../selector/pseudo-nth-of-type04/result.json | 17 + .../selector/pseudo-nth-of-type04/source.vue | 11 + .../selector/pseudo-only-child/result.json | 7 + .../selector/pseudo-only-child/source.vue | 17 + .../selector/pseudo-only-of-type/result.json | 12 + .../selector/pseudo-only-of-type/source.vue | 13 + .../utils/selector/pseudo-where/result.json | 32 + .../utils/selector/pseudo-where/source.vue | 17 + .../fixtures/utils/selector/tag01/result.json | 17 + .../fixtures/utils/selector/tag01/source.vue | 9 + .../fixtures/utils/selector/tag02/result.json | 7 + .../fixtures/utils/selector/tag02/source.vue | 9 + .../utils/selector/universal/result.json | 32 + .../utils/selector/universal/source.vue | 9 + tests/lib/utils/selector.js | 86 +++ 133 files changed, 2642 insertions(+) create mode 100644 lib/utils/selector.js create mode 100644 tests/fixtures/utils/selector/attr-contains/result.json create mode 100644 tests/fixtures/utils/selector/attr-contains/source.vue create mode 100644 tests/fixtures/utils/selector/attr-empty-value/result.json create mode 100644 tests/fixtures/utils/selector/attr-empty-value/source.vue create mode 100644 tests/fixtures/utils/selector/attr-eq/result.json create mode 100644 tests/fixtures/utils/selector/attr-eq/source.vue create mode 100644 tests/fixtures/utils/selector/attr-has/result.json create mode 100644 tests/fixtures/utils/selector/attr-has/source.vue create mode 100644 tests/fixtures/utils/selector/attr-insensitive/result.json create mode 100644 tests/fixtures/utils/selector/attr-insensitive/source.vue create mode 100644 tests/fixtures/utils/selector/attr-lang/result.json create mode 100644 tests/fixtures/utils/selector/attr-lang/source.vue create mode 100644 tests/fixtures/utils/selector/attr-not-insensitive/result.json create mode 100644 tests/fixtures/utils/selector/attr-not-insensitive/source.vue create mode 100644 tests/fixtures/utils/selector/attr-prefixed/result.json create mode 100644 tests/fixtures/utils/selector/attr-prefixed/source.vue create mode 100644 tests/fixtures/utils/selector/attr-suffixed/result.json create mode 100644 tests/fixtures/utils/selector/attr-suffixed/source.vue create mode 100644 tests/fixtures/utils/selector/attr-words/result.json create mode 100644 tests/fixtures/utils/selector/attr-words/source.vue create mode 100644 tests/fixtures/utils/selector/class/result.json create mode 100644 tests/fixtures/utils/selector/class/source.vue create mode 100644 tests/fixtures/utils/selector/combinator-adjacent/result.json create mode 100644 tests/fixtures/utils/selector/combinator-adjacent/source.vue create mode 100644 tests/fixtures/utils/selector/combinator-child01/result.json create mode 100644 tests/fixtures/utils/selector/combinator-child01/source.vue create mode 100644 tests/fixtures/utils/selector/combinator-child02/result.json create mode 100644 tests/fixtures/utils/selector/combinator-child02/source.vue create mode 100644 tests/fixtures/utils/selector/combinator-descendant01/result.json create mode 100644 tests/fixtures/utils/selector/combinator-descendant01/source.vue create mode 100644 tests/fixtures/utils/selector/combinator-descendant02/result.json create mode 100644 tests/fixtures/utils/selector/combinator-descendant02/source.vue create mode 100644 tests/fixtures/utils/selector/combinator-sibling/result.json create mode 100644 tests/fixtures/utils/selector/combinator-sibling/source.vue create mode 100644 tests/fixtures/utils/selector/comment01/result.json create mode 100644 tests/fixtures/utils/selector/comment01/source.vue create mode 100644 tests/fixtures/utils/selector/comment02/result.json create mode 100644 tests/fixtures/utils/selector/comment02/source.vue create mode 100644 tests/fixtures/utils/selector/comment03/result.json create mode 100644 tests/fixtures/utils/selector/comment03/source.vue create mode 100644 tests/fixtures/utils/selector/id/result.json create mode 100644 tests/fixtures/utils/selector/id/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-attr-op/result.json create mode 100644 tests/fixtures/utils/selector/invalid-attr-op/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-combinators/result.json create mode 100644 tests/fixtures/utils/selector/invalid-combinators/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-empty/result.json create mode 100644 tests/fixtures/utils/selector/invalid-empty/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-last-combinator/result.json create mode 100644 tests/fixtures/utils/selector/invalid-last-combinator/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-nesting/result.json create mode 100644 tests/fixtures/utils/selector/invalid-nesting/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-nth/result.json create mode 100644 tests/fixtures/utils/selector/invalid-nth/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-selector/result.json create mode 100644 tests/fixtures/utils/selector/invalid-selector/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-string/result.json create mode 100644 tests/fixtures/utils/selector/invalid-string/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-unknown-combinator/result.json create mode 100644 tests/fixtures/utils/selector/invalid-unknown-combinator/source.vue create mode 100644 tests/fixtures/utils/selector/invalid-unknown-pseudo/result.json create mode 100644 tests/fixtures/utils/selector/invalid-unknown-pseudo/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-empty/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-empty/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-first-child01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-first-child01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-first-child02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-first-child02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-first-of-type01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-first-of-type01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-first-of-type02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-first-of-type02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-has-complex01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-has-complex01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-has-complex02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-has-complex02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-has01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-has01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-has02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-has02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-has03/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-has03/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-has04/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-has04/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-is/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-is/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-last-child/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-last-child/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-last-of-type01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-last-of-type01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-last-of-type02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-last-of-type02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-not/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-not/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-child01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-child01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-child02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-child02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-child03/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-child03/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-child01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-child01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-child02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-child02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-child03/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-child03/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-of-type01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-of-type01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-of-type02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-last-of-type02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-of-type01/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-of-type01/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-of-type02/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-of-type02/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-of-type03/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-of-type03/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-nth-of-type04/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-nth-of-type04/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-only-child/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-only-child/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-only-of-type/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-only-of-type/source.vue create mode 100644 tests/fixtures/utils/selector/pseudo-where/result.json create mode 100644 tests/fixtures/utils/selector/pseudo-where/source.vue create mode 100644 tests/fixtures/utils/selector/tag01/result.json create mode 100644 tests/fixtures/utils/selector/tag01/source.vue create mode 100644 tests/fixtures/utils/selector/tag02/result.json create mode 100644 tests/fixtures/utils/selector/tag02/source.vue create mode 100644 tests/fixtures/utils/selector/universal/result.json create mode 100644 tests/fixtures/utils/selector/universal/source.vue create mode 100644 tests/lib/utils/selector.js diff --git a/lib/utils/selector.js b/lib/utils/selector.js new file mode 100644 index 000000000..bb0bc144a --- /dev/null +++ b/lib/utils/selector.js @@ -0,0 +1,664 @@ +'use strict' + +const parser = require('postcss-selector-parser') +const { default: nthCheck } = require('nth-check') +const { getAttribute, isVElement } = require('.') + +/** + * @typedef {object} VElementSelector + * @property {(element: VElement)=>boolean} test + * @property {string|null} elementText + */ + +module.exports = { + parseSelector +} + +/** + * Parses CSS selectors and returns an object with a function that tests VElement. + * @param {string} selector CSS selector + * @param {RuleContext} context - The rule context. + * @returns {VElementSelector} + */ +function parseSelector(selector, context) { + let astSelector + try { + astSelector = parser().astSync(selector) + } catch (e) { + context.report({ + loc: { line: 0, column: 0 }, + message: `Cannot parse selector: ${selector}.` + }) + return { + elementText: null, + test: () => false + } + } + + try { + const selectorContext = new SelectorContext(selector) + const test = selectorsToVElementMatcher(astSelector.nodes, selectorContext) + + return { + test(element) { + return test(element, null) + }, + elementText: selectorContext.element.getText() + } + } catch (e) { + if (e instanceof SelectorError) { + context.report({ + loc: { line: 0, column: 0 }, + message: e.message + }) + return { + elementText: null, + test: () => false + } + } + throw e + } +} + +class SelectorElement { + constructor() { + /** @type {string | null} */ + this.tag = null + /** @type { { [key in string]: string | null } } */ + this.attrs = {} + this.unknown = false + } + + asUnknown() { + this.unknown = true + } + + /** + * @param {string} tag + */ + setTagName(tag) { + this.tag = tag + } + + /** + * @param {string} key + * @param {string} [value] + */ + addAttr(key, value) { + this.attrs[key] = value == null ? this.attrs[key] : value + } + + /** + * @param {string} className + */ + addClass(className) { + this.attrs.class = `${this.attrs.class || ''} ${className}` + .split(/\s+/g) + .filter((s) => s) + .join(' ') + } + + getText() { + if (this.unknown) { + return null + } + const attrText = Object.entries(this.attrs) + .map(([k, v]) => `${k}${v == null ? '' : `="${v}"`}`) + .join(' ') + + return `<${this.tag || '*'}${attrText ? ` ${attrText}` : ''}>` + } +} + +class SelectorContext { + /** + * @param {string} selector + */ + constructor(selector) { + this.element = new SelectorElement() + this.selector = selector + } +} + +class SelectorError extends Error {} + +/** + * @typedef {(element: VElement, subject: VElement | null )=>boolean} VElementMatcher + * @typedef {Exclude} ChildNode + */ + +/** + * Convert nodes to VElementMatcher + * @param {parser.Selector[]} selectorNodes + * @param {SelectorContext} context + * @returns {VElementMatcher} + */ +function selectorsToVElementMatcher(selectorNodes, context) { + const selectors = selectorNodes.map((n) => + selectorToVElementMatcher(cleanSelectorChildren(n), context) + ) + return (element, subject) => selectors.some((sel) => sel(element, subject)) +} + +/** + * Clean and get the selector child nodes. + * @param {parser.Selector} selector + * @returns {ChildNode[]} + */ +function cleanSelectorChildren(selector) { + /** @type {ChildNode[]} */ + const nodes = [] + /** @type {ChildNode|null} */ + let last = null + for (const node of selector.nodes) { + if (node.type === 'root') { + throw new Error('Unexpected state type=root') + } + if (node.type === 'comment') { + continue + } + if ( + (last == null || last.type === 'combinator') && + isDescendantCombinator(node) + ) { + // Ignore descendant combinator + continue + } + if (isDescendantCombinator(last) && node.type === 'combinator') { + // Replace combinator + nodes.pop() + } + nodes.push(node) + last = node + } + if (isDescendantCombinator(last)) { + nodes.pop() + } + return nodes + + /** + * @param {parser.Node|null} node + * @returns {node is parser.Combinator} + */ + function isDescendantCombinator(node) { + return Boolean(node && node.type === 'combinator' && !node.value.trim()) + } +} +/** + * Convert Selector child nodes to VElementMatcher + * @param {ChildNode[]} selectorChildren + * @param {SelectorContext} context + * @returns {VElementMatcher} + */ +function selectorToVElementMatcher(selectorChildren, context) { + const nodes = [...selectorChildren] + let node = nodes.shift() + /** + * @type {VElementMatcher | null} + */ + let result = null + while (node) { + if (node.type === 'combinator') { + context.element.asUnknown() + const combinator = node.value + node = nodes.shift() + if (!node) { + throw new SelectorError(`Expected selector after '${combinator}'.`) + } + if (node.type === 'combinator') { + throw new SelectorError(`Unexpected combinator '${node.value}'.`) + } + const right = nodeToVElementMatcher(node, context) + result = combination( + result || + // for :has() + ((element, subject) => element === subject), + combinator, + right + ) + } else { + const sel = nodeToVElementMatcher(node, context) + result = result ? compound(result, sel) : sel + } + node = nodes.shift() + } + if (!result) { + throw new SelectorError(`Unexpected empty selector.`) + } + return result +} + +/** + * @param {VElementMatcher} left + * @param {string} combinator + * @param {VElementMatcher} right + * @returns {VElementMatcher} + */ +function combination(left, combinator, right) { + if (!combinator.trim()) { + // descendant + return (element, subject) => { + if (right(element, null)) { + let parent = element.parent + while (parent.type === 'VElement') { + if (left(parent, subject)) { + return true + } + parent = parent.parent + } + } + return false + } + } + + if (combinator === '>') { + // child + return (element, subject) => { + if (right(element, null)) { + const parent = element.parent + if (parent.type === 'VElement') { + return left(parent, subject) + } + } + return false + } + } + + if (combinator === '+') { + // adjacent + return (element, subject) => { + if (right(element, null)) { + const before = getBeforeElement(element) + if (before) { + return left(before, subject) + } + } + return false + } + } + + if (combinator === '~') { + // sibling + return (element, subject) => { + if (right(element, null)) { + for (const before of getBeforeElements(element)) { + if (left(before, subject)) { + return true + } + } + } + return false + } + } + throw new SelectorError(`Unknown combinator: ${combinator}.`) +} + +/** + * Convert node to VElementMatcher + * @param {Exclude} selector + * @param {SelectorContext} context + * @returns {VElementMatcher} + */ +function nodeToVElementMatcher(selector, context) { + if (selector.type === 'attribute') { + return attributeNodeToVElementMatcher(selector, context) + } + if (selector.type === 'class') { + const className = selector.value + context.element.addClass(className) + return (element) => { + const attrValue = getAttributeValue(element, 'class') + if (attrValue == null) { + return false + } + return attrValue.split(/\s+/gu).includes(className) + } + } + if (selector.type === 'id') { + const id = selector.value + context.element.addAttr('id', id) + return (element) => { + const attrValue = getAttributeValue(element, 'id') + if (attrValue == null) { + return false + } + return attrValue === id + } + } + if (selector.type === 'tag') { + const name = selector.value + context.element.setTagName(name) + return (element) => element.rawName === name + } + if (selector.type === 'universal') { + return () => true + } + + // Element is unknown + context.element.asUnknown() + + if (selector.type === 'pseudo') { + const pseudo = selector.value + if (pseudo === ':not') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:not + const selectors = selectorsToVElementMatcher(selector.nodes, context) + return (element, subject) => { + return !selectors(element, subject) + } + } + if (pseudo === ':is' || pseudo === ':where') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:is + // https://developer.mozilla.org/en-US/docs/Web/CSS/:where + return selectorsToVElementMatcher(selector.nodes, context) + } + if (pseudo === ':has') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:has + return pseudoHasSelectorsToVElementMatcher(selector.nodes, context) + } + if (pseudo === ':empty') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:empty + return (element) => + element.children.every( + (child) => child.type === 'VText' && !child.value.trim() + ) + } + if (pseudo === ':nth-child') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child + const nth = parseNth(selector) + return buildPseudoNthVElementMatcher(nth) + } + if (pseudo === ':nth-last-child') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-child + const nth = parseNth(selector) + return buildPseudoNthVElementMatcher((index, length) => + nth(length - index - 1) + ) + } + if (pseudo === ':first-child') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child + return buildPseudoNthVElementMatcher((index) => index === 0) + } + if (pseudo === ':last-child') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child + return buildPseudoNthVElementMatcher( + (index, length) => index === length - 1 + ) + } + if (pseudo === ':only-child') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child + return buildPseudoNthVElementMatcher( + (index, length) => index === 0 && length === 1 + ) + } + + if (pseudo === ':nth-of-type') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type + const nth = parseNth(selector) + return buildPseudoNthOfTypeVElementMatcher(nth) + } + if (pseudo === ':nth-last-of-type') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type + const nth = parseNth(selector) + return buildPseudoNthOfTypeVElementMatcher((index, length) => + nth(length - index - 1) + ) + } + if (pseudo === ':first-of-type') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type + return buildPseudoNthOfTypeVElementMatcher((index) => index === 0) + } + if (pseudo === ':last-of-type') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type + return buildPseudoNthOfTypeVElementMatcher( + (index, length) => index === length - 1 + ) + } + if (pseudo === ':only-of-type') { + // https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type + return buildPseudoNthOfTypeVElementMatcher( + (index, length) => index === 0 && length === 1 + ) + } + throw new SelectorError(`Unsupported pseudo selector: ${pseudo}.`) + } + if (selector.type === 'nesting') { + throw new SelectorError('Unsupported nesting selector.') + } + if (selector.type === 'string') { + throw new SelectorError(`Unknown selector: ${selector.value}.`) + } + + throw new SelectorError( + `Unknown selector: ${/** @type {any}*/ (selector).value}.` + ) +} + +/** + * Convert Attribute node to VElementMatcher + * @param {parser.Attribute} selector + * @param {SelectorContext} context + * @returns {VElementMatcher} + */ +function attributeNodeToVElementMatcher(selector, context) { + const key = selector.attribute + if (!selector.operator) { + context.element.addAttr(key) + return (element) => getAttributeValue(element, key) != null + } + const value = selector.value || '' + if (selector.operator === '=') { + context.element.addAttr(key, value) + return buildVElementMatcher(value, (attr, val) => attr === val) + } + + context.element.asUnknown() + + if (selector.operator === '~=') { + // words + return buildVElementMatcher(value, (attr, val) => + attr.split(/\s+/gu).includes(val) + ) + } + if (selector.operator === '|=') { + // immediately followed by hyphen + return buildVElementMatcher( + value, + (attr, val) => attr === val || attr.startsWith(`${val}-`) + ) + } + if (selector.operator === '^=') { + // prefixed + return buildVElementMatcher(value, (attr, val) => attr.startsWith(val)) + } + if (selector.operator === '$=') { + // suffixed + return buildVElementMatcher(value, (attr, val) => attr.endsWith(val)) + } + if (selector.operator === '*=') { + // contains + return buildVElementMatcher(value, (attr, val) => attr.includes(val)) + } + + throw new SelectorError(`Unsupported operator: ${selector.operator}.`) + + /** + * @param {string} selectorValue + * @param {(attrValue:string, selectorValue: string)=>boolean} test + * @returns {VElementMatcher} + */ + function buildVElementMatcher(selectorValue, test) { + const val = selector.insensitive + ? selectorValue.toLowerCase() + : selectorValue + return (element) => { + const attrValue = getAttributeValue(element, key) + if (attrValue == null) { + return false + } + return test( + selector.insensitive ? attrValue.toLowerCase() : attrValue, + val + ) + } + } +} + +/** + * Convert :has() selector nodes to VElementMatcher + * @param {parser.Selector[]} selectorNodes + * @param {SelectorContext} context + * @returns {VElementMatcher} + */ +function pseudoHasSelectorsToVElementMatcher(selectorNodes, context) { + const selectors = selectorNodes.map((n) => + pseudoHasSelectorToVElementMatcher(n, context) + ) + return (element, subject) => selectors.some((sel) => sel(element, subject)) +} +/** + * Convert :has() selector node to VElementMatcher + * @param {parser.Selector} selector + * @param {SelectorContext} context + * @returns {VElementMatcher} + */ +function pseudoHasSelectorToVElementMatcher(selector, context) { + const nodes = cleanSelectorChildren(selector) + const selectors = selectorToVElementMatcher(nodes, context) + const firstNode = nodes[0] + if ( + firstNode.type === 'combinator' && + (firstNode.value === '+' || firstNode.value === '~') + ) { + // adjacent or sibling + return buildVElementMatcher(selectors, (element) => + getAfterElements(element) + ) + } + // descendant or child + return buildVElementMatcher(selectors, (element) => + element.children.filter(isVElement) + ) + + /** + * @param {VElementMatcher} selectors + * @param {(element: VElement) => VElement[]} getStartElements + * @returns {VElementMatcher} + */ + function buildVElementMatcher(selectors, getStartElements) { + return (element) => { + const elements = [...getStartElements(element)] + /** @type {VElement|undefined} */ + let curr + while ((curr = elements.shift())) { + const el = curr + if (selectors(el, element)) { + return true + } + elements.push(...el.children.filter(isVElement)) + } + return false + } + } +} + +/** + * Parse + * @param {parser.Pseudo} pseudoNode + * @returns {(index: number)=>boolean} + */ +function parseNth(pseudoNode) { + const argumentsText = pseudoNode + .toString() + .slice(pseudoNode.value.length) + .toLowerCase() + const openParenIndex = argumentsText.indexOf('(') + const closeParenIndex = argumentsText.lastIndexOf(')') + if (openParenIndex < 0 || closeParenIndex < 0) { + throw new SelectorError( + `Cannot parse An+B micro syntax (:nth-xxx() argument): ${argumentsText}.` + ) + } + + const argument = argumentsText + .slice(openParenIndex + 1, closeParenIndex) + .trim() + try { + return nthCheck(argument) + } catch (e) { + throw new SelectorError( + `Cannot parse An+B micro syntax (:nth-xxx() argument): '${argument}'.` + ) + } +} + +/** + * Build VElementMatcher for :nth-xxx() + * @param {(index: number, length: number)=>boolean} testIndex + * @returns {VElementMatcher} + */ +function buildPseudoNthVElementMatcher(testIndex) { + return (element) => { + const elements = element.parent.children.filter(isVElement) + return testIndex(elements.indexOf(element), elements.length) + } +} + +/** + * Build VElementMatcher for :nth-xxx-of-type() + * @param {(index: number, length: number)=>boolean} testIndex + * @returns {VElementMatcher} + */ +function buildPseudoNthOfTypeVElementMatcher(testIndex) { + return (element) => { + const elements = element.parent.children + .filter(isVElement) + .filter((e) => e.rawName === element.rawName) + return testIndex(elements.indexOf(element), elements.length) + } +} + +/** + * @param {VElement} element + */ +function getBeforeElement(element) { + return getBeforeElements(element).pop() || null +} +/** + * @param {VElement} element + */ +function getBeforeElements(element) { + const parent = element.parent + const index = parent.children.indexOf(element) + return parent.children.slice(0, index).filter(isVElement) +} + +/** + * @param {VElement} element + */ +function getAfterElements(element) { + const parent = element.parent + const index = parent.children.indexOf(element) + return parent.children.slice(index + 1).filter(isVElement) +} + +/** + * @param {VElementMatcher} a + * @param {VElementMatcher} b + * @returns {VElementMatcher} + */ +function compound(a, b) { + return (element, subject) => a(element, subject) && b(element, subject) +} + +/** + * Get attribute value from given element. + * @param {VElement} element The element node. + * @param {string} attribute The attribute name. + */ +function getAttributeValue(element, attribute) { + const attr = getAttribute(element, attribute) + if (attr) { + return (attr.value && attr.value.value) || '' + } + return null +} diff --git a/package.json b/package.json index 4bc07c92e..9ddbb72a4 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "dependencies": { "eslint-utils": "^3.0.0", "natural-compare": "^1.4.0", + "nth-check": "^2.0.1", "postcss-selector-parser": "^6.0.9", "semver": "^7.3.5", "vue-eslint-parser": "^8.0.1" diff --git a/tests/fixtures/utils/selector/attr-contains/result.json b/tests/fixtures/utils/selector/attr-contains/result.json new file mode 100644 index 000000000..80a3ea95a --- /dev/null +++ b/tests/fixtures/utils/selector/attr-contains/result.json @@ -0,0 +1,17 @@ +[ + { + "text": "", + "selector": "a[href*=\"example\"]", + "elementText": null + }, + { + "text": "", + "selector": "a[href*=\"example\"]", + "elementText": null + }, + { + "text": "", + "selector": "a[href*=\"example\"]", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-contains/source.vue b/tests/fixtures/utils/selector/attr-contains/source.vue new file mode 100644 index 000000000..6c9a89dde --- /dev/null +++ b/tests/fixtures/utils/selector/attr-contains/source.vue @@ -0,0 +1,10 @@ + + diff --git a/tests/fixtures/utils/selector/attr-empty-value/result.json b/tests/fixtures/utils/selector/attr-empty-value/result.json new file mode 100644 index 000000000..001cc55f4 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-empty-value/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "

", + "selector": "[foo=]", + "elementText": "<* foo=\"\">" + }, + { + "text": "

", + "selector": "[foo=]", + "elementText": "<* foo=\"\">" + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-empty-value/source.vue b/tests/fixtures/utils/selector/attr-empty-value/source.vue new file mode 100644 index 000000000..4a73c1519 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-empty-value/source.vue @@ -0,0 +1,11 @@ + + diff --git a/tests/fixtures/utils/selector/attr-eq/result.json b/tests/fixtures/utils/selector/attr-eq/result.json new file mode 100644 index 000000000..8cb8058a4 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-eq/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "
", + "selector": "[foo=foo]", + "elementText": "<* foo=\"foo\">" + }, + { + "text": "", + "selector": "[foo=foo]", + "elementText": "<* foo=\"foo\">" + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-eq/source.vue b/tests/fixtures/utils/selector/attr-eq/source.vue new file mode 100644 index 000000000..fdfe34e2e --- /dev/null +++ b/tests/fixtures/utils/selector/attr-eq/source.vue @@ -0,0 +1,11 @@ + + diff --git a/tests/fixtures/utils/selector/attr-has/result.json b/tests/fixtures/utils/selector/attr-has/result.json new file mode 100644 index 000000000..c2bc77712 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-has/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "
", + "selector": "[foo]", + "elementText": "<* foo>" + }, + { + "text": "

", + "selector": "[foo]", + "elementText": "<* foo>" + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-has/source.vue b/tests/fixtures/utils/selector/attr-has/source.vue new file mode 100644 index 000000000..aaf7b1ddf --- /dev/null +++ b/tests/fixtures/utils/selector/attr-has/source.vue @@ -0,0 +1,8 @@ + + diff --git a/tests/fixtures/utils/selector/attr-insensitive/result.json b/tests/fixtures/utils/selector/attr-insensitive/result.json new file mode 100644 index 000000000..64fa63c63 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-insensitive/result.json @@ -0,0 +1,7 @@ +[ + { + "text": "", + "selector": "a[href*=\"insensitive\" i]", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-insensitive/source.vue b/tests/fixtures/utils/selector/attr-insensitive/source.vue new file mode 100644 index 000000000..129fcab0d --- /dev/null +++ b/tests/fixtures/utils/selector/attr-insensitive/source.vue @@ -0,0 +1,10 @@ + + diff --git a/tests/fixtures/utils/selector/attr-lang/result.json b/tests/fixtures/utils/selector/attr-lang/result.json new file mode 100644 index 000000000..105ccca12 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-lang/result.json @@ -0,0 +1,17 @@ +[ + { + "text": "

", + "selector": "div[lang|=\"zh\"]", + "elementText": null + }, + { + "text": "
", + "selector": "div[lang|=\"zh\"]", + "elementText": null + }, + { + "text": "
", + "selector": "div[lang|=\"zh\"]", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-lang/source.vue b/tests/fixtures/utils/selector/attr-lang/source.vue new file mode 100644 index 000000000..fa88b8750 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-lang/source.vue @@ -0,0 +1,9 @@ + + diff --git a/tests/fixtures/utils/selector/attr-not-insensitive/result.json b/tests/fixtures/utils/selector/attr-not-insensitive/result.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/tests/fixtures/utils/selector/attr-not-insensitive/result.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-not-insensitive/source.vue b/tests/fixtures/utils/selector/attr-not-insensitive/source.vue new file mode 100644 index 000000000..2fb3e9037 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-not-insensitive/source.vue @@ -0,0 +1,11 @@ + + diff --git a/tests/fixtures/utils/selector/attr-prefixed/result.json b/tests/fixtures/utils/selector/attr-prefixed/result.json new file mode 100644 index 000000000..e4aeabed3 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-prefixed/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "", + "selector": "a[href^=\"#\"]", + "elementText": null + }, + { + "text": "", + "selector": "a[href^=\"#\"]", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-prefixed/source.vue b/tests/fixtures/utils/selector/attr-prefixed/source.vue new file mode 100644 index 000000000..2c1dc2eb0 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-prefixed/source.vue @@ -0,0 +1,10 @@ + + diff --git a/tests/fixtures/utils/selector/attr-suffixed/result.json b/tests/fixtures/utils/selector/attr-suffixed/result.json new file mode 100644 index 000000000..503182487 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-suffixed/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "", + "selector": "a[href$=\".org\"]", + "elementText": null + }, + { + "text": "", + "selector": "a[href$=\".org\"]", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-suffixed/source.vue b/tests/fixtures/utils/selector/attr-suffixed/source.vue new file mode 100644 index 000000000..7b27ad2e4 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-suffixed/source.vue @@ -0,0 +1,10 @@ + + diff --git a/tests/fixtures/utils/selector/attr-words/result.json b/tests/fixtures/utils/selector/attr-words/result.json new file mode 100644 index 000000000..ceab38e25 --- /dev/null +++ b/tests/fixtures/utils/selector/attr-words/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "
", + "selector": "div[lang~=\"en-us\"]", + "elementText": null + }, + { + "text": "
", + "selector": "div[lang~=\"en-us\"]", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/attr-words/source.vue b/tests/fixtures/utils/selector/attr-words/source.vue new file mode 100644 index 000000000..c9558519b --- /dev/null +++ b/tests/fixtures/utils/selector/attr-words/source.vue @@ -0,0 +1,11 @@ + + diff --git a/tests/fixtures/utils/selector/class/result.json b/tests/fixtures/utils/selector/class/result.json new file mode 100644 index 000000000..c870d0932 --- /dev/null +++ b/tests/fixtures/utils/selector/class/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "
", + "selector": ".foo", + "elementText": "<* class=\"foo\">" + }, + { + "text": "", + "selector": ".foo", + "elementText": "<* class=\"foo\">" + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/class/source.vue b/tests/fixtures/utils/selector/class/source.vue new file mode 100644 index 000000000..b9616a44b --- /dev/null +++ b/tests/fixtures/utils/selector/class/source.vue @@ -0,0 +1,8 @@ + + diff --git a/tests/fixtures/utils/selector/combinator-adjacent/result.json b/tests/fixtures/utils/selector/combinator-adjacent/result.json new file mode 100644 index 000000000..e3db942e1 --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-adjacent/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "
", + "selector": ".foo+.bar", + "elementText": null + }, + { + "text": "
", + "selector": ".foo+.bar", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/combinator-adjacent/source.vue b/tests/fixtures/utils/selector/combinator-adjacent/source.vue new file mode 100644 index 000000000..489c0d7ee --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-adjacent/source.vue @@ -0,0 +1,14 @@ + + diff --git a/tests/fixtures/utils/selector/combinator-child01/result.json b/tests/fixtures/utils/selector/combinator-child01/result.json new file mode 100644 index 000000000..a35bcda6f --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-child01/result.json @@ -0,0 +1,17 @@ +[ + { + "text": "
", + "selector": ".foo > .bar", + "elementText": null + }, + { + "text": "
", + "selector": ".foo > .bar", + "elementText": null + }, + { + "text": "
", + "selector": ".foo > .bar", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/combinator-child01/source.vue b/tests/fixtures/utils/selector/combinator-child01/source.vue new file mode 100644 index 000000000..5eaa8df61 --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-child01/source.vue @@ -0,0 +1,14 @@ + + diff --git a/tests/fixtures/utils/selector/combinator-child02/result.json b/tests/fixtures/utils/selector/combinator-child02/result.json new file mode 100644 index 000000000..1a7638d1c --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-child02/result.json @@ -0,0 +1,22 @@ +[ + { + "text": "
", + "selector": ".a > .b > .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a > .b > .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a > .b > .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a > .b > .c", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/combinator-child02/source.vue b/tests/fixtures/utils/selector/combinator-child02/source.vue new file mode 100644 index 000000000..cf2ede6ec --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-child02/source.vue @@ -0,0 +1,162 @@ + + diff --git a/tests/fixtures/utils/selector/combinator-descendant01/result.json b/tests/fixtures/utils/selector/combinator-descendant01/result.json new file mode 100644 index 000000000..d6853a405 --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-descendant01/result.json @@ -0,0 +1,22 @@ +[ + { + "text": "
", + "selector": ".foo .bar", + "elementText": null + }, + { + "text": "
", + "selector": ".foo .bar", + "elementText": null + }, + { + "text": "
", + "selector": ".foo .bar", + "elementText": null + }, + { + "text": "
", + "selector": ".foo .bar", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/combinator-descendant01/source.vue b/tests/fixtures/utils/selector/combinator-descendant01/source.vue new file mode 100644 index 000000000..b122dae66 --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-descendant01/source.vue @@ -0,0 +1,14 @@ + + diff --git a/tests/fixtures/utils/selector/combinator-descendant02/result.json b/tests/fixtures/utils/selector/combinator-descendant02/result.json new file mode 100644 index 000000000..dfde52575 --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-descendant02/result.json @@ -0,0 +1,42 @@ +[ + { + "text": "
", + "selector": ".a .b .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a .b .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a .b .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a .b .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a .b .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a .b .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a .b .c", + "elementText": null + }, + { + "text": "
", + "selector": ".a .b .c", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/combinator-descendant02/source.vue b/tests/fixtures/utils/selector/combinator-descendant02/source.vue new file mode 100644 index 000000000..d89b86220 --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-descendant02/source.vue @@ -0,0 +1,162 @@ + + diff --git a/tests/fixtures/utils/selector/combinator-sibling/result.json b/tests/fixtures/utils/selector/combinator-sibling/result.json new file mode 100644 index 000000000..efb4c7cfc --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-sibling/result.json @@ -0,0 +1,17 @@ +[ + { + "text": "
", + "selector": ".foo~.bar", + "elementText": null + }, + { + "text": "
", + "selector": ".foo~.bar", + "elementText": null + }, + { + "text": "
", + "selector": ".foo~.bar", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/combinator-sibling/source.vue b/tests/fixtures/utils/selector/combinator-sibling/source.vue new file mode 100644 index 000000000..381e6750d --- /dev/null +++ b/tests/fixtures/utils/selector/combinator-sibling/source.vue @@ -0,0 +1,14 @@ + + diff --git a/tests/fixtures/utils/selector/comment01/result.json b/tests/fixtures/utils/selector/comment01/result.json new file mode 100644 index 000000000..a16499c11 --- /dev/null +++ b/tests/fixtures/utils/selector/comment01/result.json @@ -0,0 +1,7 @@ +[ + { + "text": "
", + "selector": ".foo /* comment */ :is(.bar /* comment */)", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/comment01/source.vue b/tests/fixtures/utils/selector/comment01/source.vue new file mode 100644 index 000000000..d388a1b9a --- /dev/null +++ b/tests/fixtures/utils/selector/comment01/source.vue @@ -0,0 +1,7 @@ + + diff --git a/tests/fixtures/utils/selector/comment02/result.json b/tests/fixtures/utils/selector/comment02/result.json new file mode 100644 index 000000000..8f0a0e5d6 --- /dev/null +++ b/tests/fixtures/utils/selector/comment02/result.json @@ -0,0 +1,7 @@ +[ + { + "text": "
", + "selector": ".foo /* comment */ > /* comment */ /* comment */ .bar /* comment */", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/comment02/source.vue b/tests/fixtures/utils/selector/comment02/source.vue new file mode 100644 index 000000000..dc71c6bab --- /dev/null +++ b/tests/fixtures/utils/selector/comment02/source.vue @@ -0,0 +1,13 @@ + + diff --git a/tests/fixtures/utils/selector/comment03/result.json b/tests/fixtures/utils/selector/comment03/result.json new file mode 100644 index 000000000..585e12694 --- /dev/null +++ b/tests/fixtures/utils/selector/comment03/result.json @@ -0,0 +1,7 @@ +[ + { + "text": "
", + "selector": ".foo /* comment */ /* comment */ > /* comment */ .bar /* comment */", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/comment03/source.vue b/tests/fixtures/utils/selector/comment03/source.vue new file mode 100644 index 000000000..a8381a888 --- /dev/null +++ b/tests/fixtures/utils/selector/comment03/source.vue @@ -0,0 +1,13 @@ + + diff --git a/tests/fixtures/utils/selector/id/result.json b/tests/fixtures/utils/selector/id/result.json new file mode 100644 index 000000000..6c95c3176 --- /dev/null +++ b/tests/fixtures/utils/selector/id/result.json @@ -0,0 +1,7 @@ +[ + { + "text": "
", + "selector": "#foo", + "elementText": "<* id=\"foo\">" + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/id/source.vue b/tests/fixtures/utils/selector/id/source.vue new file mode 100644 index 000000000..88bdd7cb2 --- /dev/null +++ b/tests/fixtures/utils/selector/id/source.vue @@ -0,0 +1,8 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-attr-op/result.json b/tests/fixtures/utils/selector/invalid-attr-op/result.json new file mode 100644 index 000000000..fa8e37297 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-attr-op/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Unsupported operator: ==." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-attr-op/source.vue b/tests/fixtures/utils/selector/invalid-attr-op/source.vue new file mode 100644 index 000000000..dc39fc6b5 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-attr-op/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-combinators/result.json b/tests/fixtures/utils/selector/invalid-combinators/result.json new file mode 100644 index 000000000..f72eb511e --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-combinators/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Unexpected combinator '>'." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-combinators/source.vue b/tests/fixtures/utils/selector/invalid-combinators/source.vue new file mode 100644 index 000000000..2c768628d --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-combinators/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-empty/result.json b/tests/fixtures/utils/selector/invalid-empty/result.json new file mode 100644 index 000000000..fa3f0f3ef --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-empty/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Unexpected empty selector." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-empty/source.vue b/tests/fixtures/utils/selector/invalid-empty/source.vue new file mode 100644 index 000000000..c6444bdad --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-empty/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-last-combinator/result.json b/tests/fixtures/utils/selector/invalid-last-combinator/result.json new file mode 100644 index 000000000..d084beed0 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-last-combinator/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Expected selector after '>'." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-last-combinator/source.vue b/tests/fixtures/utils/selector/invalid-last-combinator/source.vue new file mode 100644 index 000000000..193c8fd1b --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-last-combinator/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-nesting/result.json b/tests/fixtures/utils/selector/invalid-nesting/result.json new file mode 100644 index 000000000..65e6151a5 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-nesting/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Unsupported nesting selector." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-nesting/source.vue b/tests/fixtures/utils/selector/invalid-nesting/source.vue new file mode 100644 index 000000000..69338be4f --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-nesting/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-nth/result.json b/tests/fixtures/utils/selector/invalid-nth/result.json new file mode 100644 index 000000000..4d432ce16 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-nth/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Cannot parse An+B micro syntax (:nth-xxx() argument): ''." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-nth/source.vue b/tests/fixtures/utils/selector/invalid-nth/source.vue new file mode 100644 index 000000000..4e2e356cd --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-nth/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-selector/result.json b/tests/fixtures/utils/selector/invalid-selector/result.json new file mode 100644 index 000000000..651d46a52 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-selector/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Cannot parse selector: :not(." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-selector/source.vue b/tests/fixtures/utils/selector/invalid-selector/source.vue new file mode 100644 index 000000000..cc6ae2d42 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-selector/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-string/result.json b/tests/fixtures/utils/selector/invalid-string/result.json new file mode 100644 index 000000000..dbc06a5b7 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-string/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Unknown selector: \".bar\"." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-string/source.vue b/tests/fixtures/utils/selector/invalid-string/source.vue new file mode 100644 index 000000000..3a51d6405 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-string/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-unknown-combinator/result.json b/tests/fixtures/utils/selector/invalid-unknown-combinator/result.json new file mode 100644 index 000000000..60fa222c2 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-unknown-combinator/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Unknown combinator: /unknown/." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-unknown-combinator/source.vue b/tests/fixtures/utils/selector/invalid-unknown-combinator/source.vue new file mode 100644 index 000000000..08d148f7c --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-unknown-combinator/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/selector/invalid-unknown-pseudo/result.json b/tests/fixtures/utils/selector/invalid-unknown-pseudo/result.json new file mode 100644 index 000000000..5e6b54358 --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-unknown-pseudo/result.json @@ -0,0 +1,5 @@ +[ + { + "error": "Unsupported pseudo selector: :unknown." + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/invalid-unknown-pseudo/source.vue b/tests/fixtures/utils/selector/invalid-unknown-pseudo/source.vue new file mode 100644 index 000000000..5d1449abe --- /dev/null +++ b/tests/fixtures/utils/selector/invalid-unknown-pseudo/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-empty/result.json b/tests/fixtures/utils/selector/pseudo-empty/result.json new file mode 100644 index 000000000..2b910e2a7 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-empty/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "
", + "selector": ".box:empty", + "elementText": null + }, + { + "text": "
", + "selector": ".box:empty", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-empty/source.vue b/tests/fixtures/utils/selector/pseudo-empty/source.vue new file mode 100644 index 000000000..9a47c2a52 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-empty/source.vue @@ -0,0 +1,11 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-first-child01/result.json b/tests/fixtures/utils/selector/pseudo-first-child01/result.json new file mode 100644 index 000000000..22d5931ce --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-first-child01/result.json @@ -0,0 +1,7 @@ +[ + { + "text": "

", + "selector": "p:first-child", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-first-child01/source.vue b/tests/fixtures/utils/selector/pseudo-first-child01/source.vue new file mode 100644 index 000000000..52ab2eb93 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-first-child01/source.vue @@ -0,0 +1,14 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-first-child02/result.json b/tests/fixtures/utils/selector/pseudo-first-child02/result.json new file mode 100644 index 000000000..cad998e48 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-first-child02/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "

  • ", + "selector": "ul li:first-child", + "elementText": null + }, + { + "text": "
  • ", + "selector": "ul li:first-child", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-first-child02/source.vue b/tests/fixtures/utils/selector/pseudo-first-child02/source.vue new file mode 100644 index 000000000..90c77ad95 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-first-child02/source.vue @@ -0,0 +1,14 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-first-of-type01/result.json b/tests/fixtures/utils/selector/pseudo-first-of-type01/result.json new file mode 100644 index 000000000..82ffdbbca --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-first-of-type01/result.json @@ -0,0 +1,7 @@ +[ + { + "text": "

    ", + "selector": "p:first-of-type", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-first-of-type01/source.vue b/tests/fixtures/utils/selector/pseudo-first-of-type01/source.vue new file mode 100644 index 000000000..c9a012a1f --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-first-of-type01/source.vue @@ -0,0 +1,8 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-first-of-type02/result.json b/tests/fixtures/utils/selector/pseudo-first-of-type02/result.json new file mode 100644 index 000000000..2d03e7df5 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-first-of-type02/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "

    ", + "selector": ".root > :first-of-type", + "elementText": null + }, + { + "text": "

    ", + "selector": ".root > :first-of-type", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-first-of-type02/source.vue b/tests/fixtures/utils/selector/pseudo-first-of-type02/source.vue new file mode 100644 index 000000000..515de7e99 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-first-of-type02/source.vue @@ -0,0 +1,8 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-has-complex01/result.json b/tests/fixtures/utils/selector/pseudo-has-complex01/result.json new file mode 100644 index 000000000..41022d32e --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has-complex01/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "

    ", + "selector": "div:has(>.foo > .foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(>.foo > .foo)", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-has-complex01/source.vue b/tests/fixtures/utils/selector/pseudo-has-complex01/source.vue new file mode 100644 index 000000000..abc928b81 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has-complex01/source.vue @@ -0,0 +1,31 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-has-complex02/result.json b/tests/fixtures/utils/selector/pseudo-has-complex02/result.json new file mode 100644 index 000000000..0b782d451 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has-complex02/result.json @@ -0,0 +1,17 @@ +[ + { + "text": "
    ", + "selector": "div:has(>.foo .foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(>.foo .foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(>.foo .foo)", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-has-complex02/source.vue b/tests/fixtures/utils/selector/pseudo-has-complex02/source.vue new file mode 100644 index 000000000..f0e1b8da4 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has-complex02/source.vue @@ -0,0 +1,31 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-has01/result.json b/tests/fixtures/utils/selector/pseudo-has01/result.json new file mode 100644 index 000000000..c93e7b71c --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has01/result.json @@ -0,0 +1,17 @@ +[ + { + "text": "
    ", + "selector": "div:has(.foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(.foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(.foo)", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-has01/source.vue b/tests/fixtures/utils/selector/pseudo-has01/source.vue new file mode 100644 index 000000000..11defe7d2 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has01/source.vue @@ -0,0 +1,25 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-has02/result.json b/tests/fixtures/utils/selector/pseudo-has02/result.json new file mode 100644 index 000000000..5e820dbac --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has02/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "
    ", + "selector": "div:has(>.foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(>.foo)", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-has02/source.vue b/tests/fixtures/utils/selector/pseudo-has02/source.vue new file mode 100644 index 000000000..7fcde7d5b --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has02/source.vue @@ -0,0 +1,25 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-has03/result.json b/tests/fixtures/utils/selector/pseudo-has03/result.json new file mode 100644 index 000000000..d4e744cb1 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has03/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "
    ", + "selector": "div:has(+.foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(+.foo)", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-has03/source.vue b/tests/fixtures/utils/selector/pseudo-has03/source.vue new file mode 100644 index 000000000..2157d5353 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has03/source.vue @@ -0,0 +1,13 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-has04/result.json b/tests/fixtures/utils/selector/pseudo-has04/result.json new file mode 100644 index 000000000..b640234d4 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has04/result.json @@ -0,0 +1,17 @@ +[ + { + "text": "
    ", + "selector": "div:has(~.foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(~.foo)", + "elementText": null + }, + { + "text": "
    ", + "selector": "div:has(~.foo)", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-has04/source.vue b/tests/fixtures/utils/selector/pseudo-has04/source.vue new file mode 100644 index 000000000..9eeebaa2f --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-has04/source.vue @@ -0,0 +1,13 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-is/result.json b/tests/fixtures/utils/selector/pseudo-is/result.json new file mode 100644 index 000000000..24738bdae --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-is/result.json @@ -0,0 +1,32 @@ +[ + { + "text": "

    ", + "selector": ":where(header, main, footer) p", + "elementText": null + }, + { + "text": "

    ", + "selector": ":where(header, main, footer) p", + "elementText": null + }, + { + "text": "

    ", + "selector": ":where(header, main, footer) p", + "elementText": null + }, + { + "text": "

    ", + "selector": ":where(header, main, footer) p", + "elementText": null + }, + { + "text": "

    ", + "selector": ":where(header, main, footer) p", + "elementText": null + }, + { + "text": "

    ", + "selector": ":where(header, main, footer) p", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-is/source.vue b/tests/fixtures/utils/selector/pseudo-is/source.vue new file mode 100644 index 000000000..944038e62 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-is/source.vue @@ -0,0 +1,17 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-last-child/result.json b/tests/fixtures/utils/selector/pseudo-last-child/result.json new file mode 100644 index 000000000..2eff3ab3c --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-last-child/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "

  • ", + "selector": "ul li:last-child", + "elementText": null + }, + { + "text": "
  • ", + "selector": "ul li:last-child", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-last-child/source.vue b/tests/fixtures/utils/selector/pseudo-last-child/source.vue new file mode 100644 index 000000000..04110e17a --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-last-child/source.vue @@ -0,0 +1,14 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-last-of-type01/result.json b/tests/fixtures/utils/selector/pseudo-last-of-type01/result.json new file mode 100644 index 000000000..3bb32de95 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-last-of-type01/result.json @@ -0,0 +1,7 @@ +[ + { + "text": "

    ", + "selector": "p:last-of-type", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-last-of-type01/source.vue b/tests/fixtures/utils/selector/pseudo-last-of-type01/source.vue new file mode 100644 index 000000000..eab097875 --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-last-of-type01/source.vue @@ -0,0 +1,8 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-last-of-type02/result.json b/tests/fixtures/utils/selector/pseudo-last-of-type02/result.json new file mode 100644 index 000000000..c8acee18f --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-last-of-type02/result.json @@ -0,0 +1,12 @@ +[ + { + "text": "

    ", + "selector": ".root > :last-of-type", + "elementText": null + }, + { + "text": "

    ", + "selector": ".root > :last-of-type", + "elementText": null + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/selector/pseudo-last-of-type02/source.vue b/tests/fixtures/utils/selector/pseudo-last-of-type02/source.vue new file mode 100644 index 000000000..06e2e0f5e --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-last-of-type02/source.vue @@ -0,0 +1,8 @@ + + diff --git a/tests/fixtures/utils/selector/pseudo-not/result.json b/tests/fixtures/utils/selector/pseudo-not/result.json new file mode 100644 index 000000000..ecb66bd4a --- /dev/null +++ b/tests/fixtures/utils/selector/pseudo-not/result.json @@ -0,0 +1,17 @@ +[ + { + "text": "