Skip to content

Commit

Permalink
Support :host:has()
Browse files Browse the repository at this point in the history
Fix #138
  • Loading branch information
asamuzaK committed Oct 31, 2024
1 parent 845a07e commit 1e9a9ed
Show file tree
Hide file tree
Showing 6 changed files with 588 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/js/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
38 changes: 32 additions & 6 deletions src/js/finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
Expand Down
26 changes: 24 additions & 2 deletions src/js/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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$/;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 => {
Expand Down
181 changes: 181 additions & 0 deletions test/finder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12509,6 +12509,187 @@ describe('Finder', () => {
assert.isTrue(res.filtered, 'filtered');
assert.isFalse(res.pending, 'pending');
});

it('should get matched node', () => {
const html = `
<template id="template">
<div>
<slot id="foo" name="bar">Foo</slot>
</div>
</template>
<my-element id="baz">
<span id="qux" slot="foo">Qux</span>
</my-element>
`;
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 = `
<template id="template">
<div>
<slot id="foo" name="bar">Foo</slot>
</div>
</template>
<my-element id="baz">
<span id="qux" slot="foo">Qux</span>
</my-element>
`;
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 = `
<template id="template">
<div id="foobar">
<slot id="foo" name="bar">Foo</slot>
</div>
</template>
<my-element id="baz">
<span id="qux" slot="foo">Qux</span>
</my-element>
`;
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 = `
<template id="template">
<div id="foobar">
<slot id="foo" name="bar">Foo</slot>
</div>
</template>
<my-element id="baz">
<span id="qux" slot="foo">Qux</span>
</my-element>
`;
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 = `
<template id="template">
<div id="foobar">
<slot id="foo" name="bar">Foo</slot>
</div>
</template>
<my-element id="baz">
<span id="qux" slot="foo">Qux</span>
</my-element>
`;
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', () => {
Expand Down
Loading

0 comments on commit 1e9a9ed

Please sign in to comment.