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 = `
+
+
+ Foo
+
+
+
+ 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 = `
+
+
+ Foo
+
+
+
+ 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 = `
+
+
+ Foo
+
+
+
+ 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 = `
+
+
+ Foo
+
+
+
+ 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 = `
+
+
+ Foo
+
+
+
+ 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 = `