diff --git a/src/js/constant.js b/src/js/constant.js index 3601d932..8f09a9f7 100644 --- a/src/js/constant.js +++ b/src/js/constant.js @@ -107,3 +107,4 @@ export const KEY_MODIFIER = Object.freeze([ 'Alt', 'AltGraph', 'CapsLock', 'Control', 'Fn', 'FnLock', 'Hyper', 'Meta', 'NumLock', 'ScrollLock', 'Shift', 'Super', 'Symbol', 'SymbolLock' ]); +export const KEY_SHADOW_HOST = Object.freeze(['host', 'host-context']); diff --git a/src/js/finder.js b/src/js/finder.js index 4d33d640..73d3c9f8 100644 --- a/src/js/finder.js +++ b/src/js/finder.js @@ -746,8 +746,8 @@ export class Finder { */ _matchLogicalPseudoFunc(astData, node, opt) { const { astName, branches, twigBranches } = astData; - const isShadowRoot = opt.isShadowRoot || - (this.#shadow && node.nodeType === DOCUMENT_FRAGMENT_NODE); + const isShadowRoot = (opt.isShadowRoot || this.#shadow) && + node.nodeType === DOCUMENT_FRAGMENT_NODE; if (astName === 'has') { let bool; for (const leaves of branches) { @@ -1942,7 +1942,7 @@ export class Finder { const leafName = unescapeSelector(leaf.name); let nodes = new Set(); let pending = false; - if (this.#shadow) { + if (this.#shadow || baseNode.nodeType !== ELEMENT_NODE) { pending = true; } else { switch (leafType) { @@ -2427,11 +2427,37 @@ export class Finder { default: { if (targetType !== TARGET_LINEAL && (leafName === 'host' || leafName === 'host-context')) { + let shadowRoot; if (this.#shadow && this.#node.nodeType === DOCUMENT_FRAGMENT_NODE) { - const node = this._matchShadowHostPseudoClass(leaf, this.#node); - if (node) { - nodes.push(node); + shadowRoot = this._matchShadowHostPseudoClass(leaf, this.#node); + } else if (compound && this.#node.nodeType === ELEMENT_NODE) { + shadowRoot = + this._matchShadowHostPseudoClass(leaf, this.#node.shadowRoot); + } + if (shadowRoot) { + let bool; + if (compound) { + for (const item of filterLeaves) { + if (/^host(?:-context)?$/.test(item.name)) { + const node = + this._matchShadowHostPseudoClass(item, shadowRoot); + bool = node === shadowRoot; + } else if (item.name === 'has') { + bool = this._matchPseudoClassSelector(item, shadowRoot, {}) + .has(shadowRoot); + } else { + bool = false; + } + if (!bool) { + break; + } + } + } else { + bool = true; + } + if (bool) { + nodes.push(shadowRoot); filtered = true; } } diff --git a/src/js/parser.js b/src/js/parser.js index dc318fff..bb56fd34 100644 --- a/src/js/parser.js +++ b/src/js/parser.js @@ -9,8 +9,9 @@ import { getType } from './utility.js'; /* constants */ import { ATTR_SELECTOR, BIT_01, BIT_02, BIT_04, BIT_08, BIT_16, BIT_32, BIT_FFFF, - CLASS_SELECTOR, DUO, HEX, HYPHEN, ID_SELECTOR, KEY_LOGICAL, NTH, - PS_CLASS_SELECTOR, PS_ELEMENT_SELECTOR, SELECTOR, SYNTAX_ERR, TYPE_SELECTOR + CLASS_SELECTOR, DUO, HEX, HYPHEN, ID_SELECTOR, KEY_LOGICAL, KEY_SHADOW_HOST, + NTH, PS_CLASS_SELECTOR, PS_ELEMENT_SELECTOR, SELECTOR, SYNTAX_ERR, + TYPE_SELECTOR } from './constant.js'; const REG_EMPTY_PS_FUNC = /(?<=:(?:dir|has|host(?:-context)?|is|lang|not|nth-(?:last-)?(?:child|of-type)|where))\(\s+\)/g; const REG_SHADOW_PS_ELEMENT = /^part|slotted$/; @@ -191,6 +192,9 @@ export const walkAST = (ast = {}) => { if (node.name === 'has') { info.set('hasHasPseudoFunc', true); } + } else if (KEY_SHADOW_HOST.includes(node.name) && + Array.isArray(node.children) && node.children.length) { + info.set('hasNestedSelector', true); } break; } @@ -232,6 +236,24 @@ export const walkAST = (ast = {}) => { } } } + } else if (node.type === PS_CLASS_SELECTOR && + KEY_SHADOW_HOST.includes(node.name) && + Array.isArray(node.children) && node.children.length) { + const itemList = list.filter(i => { + const { children, name, type } = i; + const res = + type === PS_CLASS_SELECTOR && KEY_SHADOW_HOST.includes(name) && + Array.isArray(children) && children.length; + return res; + }); + for (const { children } of itemList) { + // Selector + for (const { children: grandChildren } of children) { + if (branches.has(grandChildren)) { + branches.delete(grandChildren); + } + } + } } else if (node.type === PS_ELEMENT_SELECTOR && REG_SHADOW_PS_ELEMENT.test(node.name)) { const itemList = list.filter(i => { diff --git a/test/finder.test.js b/test/finder.test.js index ec95339a..3c38209b 100644 --- a/test/finder.test.js +++ b/test/finder.test.js @@ -12509,6 +12509,187 @@ describe('Finder', () => { assert.isTrue(res.filtered, 'filtered'); assert.isFalse(res.pending, 'pending'); }); + + it('should get matched node', () => { + const html = ` + + + Qux + + `; + const container = document.getElementById('div0'); + container.innerHTML = html; + class MyElement extends window.HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({ mode: 'open' }); + const template = document.getElementById('template'); + shadowRoot.appendChild(template.content.cloneNode(true)); + } + } + window.customElements.define('my-element', MyElement); + const host = document.getElementById('baz'); + const node = host.shadowRoot; + const finder = new Finder(window); + finder.setup(':host(#baz) div', node); + finder._prepareQuerySelectorWalker(node); + const [[{ branch: [twig] }]] = finder._correspond(':host(#baz) div'); + const res = finder._findEntryNodes(twig, 'first'); + assert.deepEqual([...res.nodes], [ + node + ], 'nodes'); + assert.isFalse(res.compound, 'compound'); + assert.isTrue(res.filtered, 'filtered'); + assert.isFalse(res.pending, 'pending'); + }); + + it('should get matched node', () => { + const html = ` + + + Qux + + `; + const container = document.getElementById('div0'); + container.innerHTML = html; + class MyElement extends window.HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({ mode: 'open' }); + const template = document.getElementById('template'); + shadowRoot.appendChild(template.content.cloneNode(true)); + } + } + window.customElements.define('my-element', MyElement); + const host = document.getElementById('baz'); + const node = host.shadowRoot; + const finder = new Finder(window); + finder.setup(':host(#baz)', node); + finder._prepareQuerySelectorWalker(node); + const [[{ branch: [twig] }]] = finder._correspond(':host(#baz)'); + const res = finder._findEntryNodes(twig, 'first'); + assert.deepEqual([...res.nodes], [ + node + ], 'nodes'); + assert.isFalse(res.compound, 'compound'); + assert.isTrue(res.filtered, 'filtered'); + assert.isFalse(res.pending, 'pending'); + }); + + it('should not match', () => { + const html = ` + + + Qux + + `; + const container = document.getElementById('div0'); + container.innerHTML = html; + class MyElement extends window.HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({ mode: 'open' }); + const template = document.getElementById('template'); + shadowRoot.appendChild(template.content.cloneNode(true)); + } + } + window.customElements.define('my-element', MyElement); + const host = document.getElementById('baz'); + const node = host.shadowRoot; + const finder = new Finder(window); + finder.setup(':host:is(#baz)', node); + const [[{ branch: [twig] }]] = finder._correspond(':host:is(#baz)'); + const res = finder._findEntryNodes(twig, 'self'); + assert.deepEqual([...res.nodes], [], 'nodes'); + assert.isTrue(res.compound, 'compound'); + assert.isFalse(res.filtered, 'filtered'); + assert.isFalse(res.pending, 'pending'); + }); + + it('should get matched node', () => { + const html = ` + + + Qux + + `; + const container = document.getElementById('div0'); + container.innerHTML = html; + class MyElement extends window.HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({ mode: 'open' }); + const template = document.getElementById('template'); + shadowRoot.appendChild(template.content.cloneNode(true)); + } + } + window.customElements.define('my-element', MyElement); + const host = document.getElementById('baz'); + const node = host.shadowRoot; + const finder = new Finder(window); + finder.setup(':host:has(#foobar)', host); + const [[{ branch: [twig] }]] = finder._correspond(':host:has(#foobar)'); + const res = finder._findEntryNodes(twig, 'self'); + assert.deepEqual([...res.nodes], [ + node + ], 'nodes'); + assert.isTrue(res.compound, 'compound'); + assert.isTrue(res.filtered, 'filtered'); + assert.isFalse(res.pending, 'pending'); + }); + + it('should get matched node', () => { + const html = ` + + + Qux + + `; + const container = document.getElementById('div0'); + container.innerHTML = html; + class MyElement extends window.HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({ mode: 'open' }); + const template = document.getElementById('template'); + shadowRoot.appendChild(template.content.cloneNode(true)); + } + } + window.customElements.define('my-element', MyElement); + const host = document.getElementById('baz'); + const node = host.shadowRoot; + const finder = new Finder(window); + finder.setup(':host:has(#foobar):host-context(#div0)', host); + const [[{ branch: [twig] }]] = + finder._correspond(':host:has(#foobar):host-context(#div0)'); + const res = finder._findEntryNodes(twig, 'self'); + assert.deepEqual([...res.nodes], [ + node + ], 'nodes'); + assert.isTrue(res.compound, 'compound'); + assert.isTrue(res.filtered, 'filtered'); + assert.isFalse(res.pending, 'pending'); + }); }); describe('collect nodes', () => { diff --git a/test/parser.test.js b/test/parser.test.js index 156e2b77..6184d57d 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -7373,6 +7373,128 @@ describe('walk AST', () => { }, 'result'); }); + it('should get selectors and info', () => { + const ast = { + children: [ + { + children: [ + { + children: [ + { + children: [ + { + loc: null, + name: 'foo', + type: CLASS_SELECTOR + } + ], + loc: null, + type: SELECTOR + } + ], + loc: null, + name: 'host', + type: PS_CLASS_SELECTOR + } + ], + loc: null, + type: SELECTOR + } + ], + loc: null, + type: SELECTOR_LIST + }; + const res = func(ast); + assert.deepEqual(res, { + branches: [ + [ + { + children: [ + { + children: [ + { + loc: null, + name: 'foo', + type: CLASS_SELECTOR + } + ], + loc: null, + type: SELECTOR + } + ], + loc: null, + name: 'host', + type: PS_CLASS_SELECTOR + } + ] + ], + info: { + hasNestedSelector: true + } + }, 'result'); + }); + + it('should get selectors and info', () => { + const ast = { + children: [ + { + children: [ + { + children: [ + { + children: [ + { + loc: null, + name: 'foo', + type: CLASS_SELECTOR + } + ], + loc: null, + type: SELECTOR + } + ], + loc: null, + name: 'host-context', + type: PS_CLASS_SELECTOR + } + ], + loc: null, + type: SELECTOR + } + ], + loc: null, + type: SELECTOR_LIST + }; + const res = func(ast); + assert.deepEqual(res, { + branches: [ + [ + { + children: [ + { + children: [ + { + loc: null, + name: 'foo', + type: CLASS_SELECTOR + } + ], + loc: null, + type: SELECTOR + } + ], + loc: null, + name: 'host-context', + type: PS_CLASS_SELECTOR + } + ] + ], + info: { + hasNestedSelector: true + } + }, 'result'); + }); + it('should get selectors and info', () => { const ast = { children: [ diff --git a/test/wpt.test.js b/test/wpt.test.js index 8c6d0127..5fe2ac3b 100644 --- a/test/wpt.test.js +++ b/test/wpt.test.js @@ -3813,6 +3813,234 @@ describe('local wpt test cases', () => { }); }); + describe('css/selectors/invalidation/host-has-shadow-tree-element-at-nonsubject-position.html', () => { + it('should get matched node(s)', () => { + const html = ` +
+
+
+
+
+
+
+
+ `; + document.body.innerHTML = html; + const host = document.getElementById('host'); + const shadow = host.attachShadow({ mode: 'open' }); + shadow.innerHTML = ` + +
+
+
+
+ `; + const subject = shadow.getElementById('subject'); + const shadowChild = shadow.getElementById('shadow_child'); + const shadowDesc = shadow.getElementById('shadow_descendant'); + // Initial + assert.isFalse(subject.matches(':host:has(.descendant) .subject')); + assert.isFalse(subject.matches(':host:has(> .child) .subject')); + assert.isFalse(subject.matches(':host:has(~ .sibling) .subject')); + assert.isFalse(subject.matches(':host:has(:is(.ancestor .descendant)) .subject')); + assert.isFalse(subject.matches(':host:has(.descendant):has(> .child) .subject')); + assert.isFalse(subject.matches(':host-context(.host_context):has(> .child > .grand_child) .subject')); + assert.isFalse(subject.matches(':host:has(> .child > .grand_child):host(.host_context):has(> .child > .descendant) .subject')); + // Add .descendant to #shadow_child + shadowChild.classList.add('descendant'); + assert.isTrue(subject.matches(':host:has(.descendant) .subject')); + shadowChild.classList.remove('descendant'); + // Add .descendant to #shadow_descendant + shadowDesc.classList.add('descendant'); + assert.isTrue(subject.matches(':host:has(.descendant) .subject')); + // Add .ancestor to #shadow_child:has(.descendant) + shadowChild.classList.add('ancestor'); + assert.isTrue(subject.matches(':host:has(:is(.ancestor .descendant)) .subject')); + shadowChild.classList.remove('ancestor'); + // Add .child to #shadow_child:has(.descendant) + shadowChild.classList.add('child'); + assert.isTrue(subject.matches(':host:has(.descendant):has(> .child) .subject')); + shadowChild.classList.remove('child'); + shadowDesc.classList.remove('descendant'); + // Add .child to #shadow_child + shadowChild.classList.add('child'); + assert.isTrue(subject.matches(':host:has(> .child) .subject')); + // Add .grand_child to #shadow_descendant + shadowDesc.classList.add('grand_child'); + assert.isTrue(subject.matches(':host-context(.host_context):has(> .child > .grand_child) .subject')); + // Add .host_context to #host + host.classList.add('host_context'); + assert.isTrue(subject.matches(':host(.host_context):has(> .child > .grand_child) .subject')); + // Add .descendant to #shadow_descendant.grand_child + shadowDesc.classList.add('descendant'); + assert.isTrue(subject.matches(':host:has(> .child > .grand_child):host(.host_context):has(> .child > .descendant) .subject')); + shadowDesc.classList.remove('descendant'); + shadowDesc.classList.remove('grand_child'); + shadowChild.classList.remove('child'); + // Add .child to #shadow_descendant + shadowDesc.classList.add('child'); + assert.isFalse(subject.matches(':host:has(.descendant) .subject')); + assert.isFalse(subject.matches(':host:has(> .child) .subject')); + assert.isFalse(subject.matches(':host:has(~ .sibling) .subject')); + assert.isFalse(subject.matches(':host:has(:is(.ancestor .descendant)) .subject')); + assert.isFalse(subject.matches(':host:has(.descendant):has(> .child) .subject')); + assert.isFalse(subject.matches(':host-context(.host_context):has(> .child > .grand_child) .subject')); + assert.isFalse(subject.matches(':host:has(> .child > .grand_child):host(.host_context):has(> .child > .descendant) .subject')); + // Insert #first_child.descendant to shadow root + const div1 = document.createElement('div'); + div1.id = 'first_child'; + div1.classList.add('descendant'); + shadow.insertBefore(div1, shadow.firstChild); + assert.isTrue(subject.matches(':host:has(.descendant) .subject')); + div1.remove(); + // Insert #last_child.descendant to shadow root + const div2 = document.createElement('div'); + div2.id = 'last_child'; + div2.classList.add('descendant'); + shadow.insertBefore(div2, null); + assert.isTrue(subject.matches(':host:has(.descendant) .subject')); + div2.remove(); + // Insert #child_in_middle.descendant before #shadow_child + const div3 = document.createElement('div'); + div3.id = 'child_in_middle.descendant'; + div3.classList.add('descendant'); + shadow.insertBefore(div3, shadowChild); + assert.isTrue(subject.matches(':host:has(.descendant) .subject')); + div3.remove(); + // Insert #grand_child.descendant before #shadow_descendant + const div4 = document.createElement('div'); + div4.id = 'grand_child'; + div4.classList.add('descendant'); + shadowChild.insertBefore(div4, shadowDesc); + assert.isTrue(subject.matches(':host:has(.descendant) .subject')); + div4.remove(); + }); + }); + + describe('css/selectors/invalidation/host-has-shadow-tree-element-at-subject-position.html', () => { + it('should get matched node(s)', () => { + const html = ` +
+
+
+
+
+
+
+
+ `; + document.body.innerHTML = html; + const host = document.getElementById('host'); + const shadow = host.attachShadow({ mode: 'open' }); + shadow.innerHTML = ` + +
+
+
+ `; + const shadowChild = shadow.getElementById('shadow_child'); + const shadowDesc = shadow.getElementById('shadow_descendant'); + // Initial + assert.isFalse(host.matches(':host:has(.descendant)')); + assert.isFalse(host.matches(':host:has(> .child)')); + assert.isFalse(host.matches(':host:has(~ .sibling)')); + assert.isFalse(host.matches(':host:has(:is(.ancestor .descendant))')); + assert.isFalse(host.matches(':host:has(.descendant):has(> .child)')); + assert.isFalse(host.matches(':host-context(.host_context):has(> .child > .grand_child)')); + assert.isFalse(host.matches(':host(.host_context):has(> .child > .grand_child)')); + assert.isFalse(host.matches(':host:has(> .child > .grand_child):host(.host_context):has(> .child > .descendant)')); + // Add .descendant to #shadow_child + shadowChild.classList.add('descendant'); + assert.isTrue(host.matches(':host:has(.descendant)')); + shadowChild.classList.remove('descendant'); + // Add .descendant to #shadow_descendant + shadowDesc.classList.add('descendant'); + assert.isTrue(host.matches(':host:has(.descendant)')); + // Add .ancestor to #shadow_child:has(.descendant) + shadowChild.classList.add('ancestor'); + assert.isTrue(host.matches(':host:has(:is(.ancestor .descendant))')); + shadowChild.classList.remove('ancestor'); + // Add .child to #shadow_child:has(.descendant) + shadowChild.classList.add('child'); + assert.isTrue(host.matches(':host:has(.descendant):has(> .child)')); + shadowChild.classList.remove('child'); + shadowDesc.classList.remove('descendant'); + // Add .child to #shadow_child + shadowChild.classList.add('child'); + assert.isTrue(host.matches(':host:has(> .child)')); + // Add .grand_child to #shadow_descendant + shadowDesc.classList.add('grand_child'); + assert.isTrue(host.matches(':host-context(.host_context):has(> .child > .grand_child)')); + // Add .host_context to #host + host.classList.add('host_context'); + assert.isTrue(host.matches(':host(.host_context):has(> .child > .grand_child)')); + // Add .descendant to #shadow_descendant.grand_child + shadowDesc.classList.add('descendant'); + assert.isTrue(host.matches(':host:has(> .child > .grand_child):host(.host_context):has(> .child > .descendant)')); + shadowDesc.classList.remove('descendant'); + shadowDesc.classList.remove('grand_child'); + shadowChild.classList.remove('child'); + // Add .child to #shadow_descendant + shadowDesc.classList.add('child'); + assert.isFalse(host.matches(':host:has(.descendant)')); + assert.isFalse(host.matches(':host:has(> .child)')); + assert.isFalse(host.matches(':host:has(~ .sibling)')); + assert.isFalse(host.matches(':host:has(:is(.ancestor .descendant))')); + assert.isFalse(host.matches(':host:has(.descendant):has(> .child)')); + assert.isFalse(host.matches(':host-context(.host_context):has(> .child > .grand_child)')); + assert.isFalse(host.matches(':host(.host_context):has(> .child > .grand_child)')); + assert.isFalse(host.matches(':host:has(> .child > .grand_child):host(.host_context):has(> .child > .descendant)')); + shadowDesc.classList.remove('child'); + // Insert #first_child.descendant to shadow root + const div1 = document.createElement('div'); + div1.id = 'first_child'; + div1.classList.add('descendant'); + shadow.insertBefore(div1, shadow.firstChild); + assert.isTrue(host.matches(':host:has(.descendant)')); + div1.remove(); + // Insert #last_child.descendant to shadow root + const div2 = document.createElement('div'); + div2.id = 'last_child'; + div2.classList.add('descendant'); + shadow.insertBefore(div2, null); + assert.isTrue(host.matches(':host:has(.descendant)')); + div2.remove(); + // Insert #child_in_middle.descendant before #shadow_child + const div3 = document.createElement('div'); + div3.id = 'child_in_middle'; + div3.classList.add('descendant'); + shadow.insertBefore(div3, shadowChild); + assert.isTrue(host.matches(':host:has(.descendant)')); + div3.remove(); + // Insert #grand_child.descendant before #shadow_descendant + const div4 = document.createElement('div'); + div4.id = 'grand_child'; + div4.classList.add('descendant'); + shadowChild.insertBefore(div4, shadowDesc); + assert.isTrue(host.matches(':host:has(.descendant)')); + div4.remove(); + }); + }); + describe('css/selectors/invalidation/host-pseudo-class-in-has.html', () => { it('should get matched node(s)', () => { const html = `