Commit 12e718ec authored by David Sveningsson's avatar David Sveningsson

feat(dom): add `generateSelector`

parent d2687c2e
......@@ -137,4 +137,12 @@ describe("DOMNode", () => {
expect(node.ruleEnabled("my-rule")).toBeTruthy();
});
});
describe("generateSelector()", () => {
it("should default to return null", () => {
expect.assertions(1);
const node = new DOMNode(NodeType.TEXT_NODE, "#text");
expect(node.generateSelector()).toBeNull();
});
});
});
......@@ -117,4 +117,8 @@ export class DOMNode {
public ruleEnabled(ruleId: string): boolean {
return !this.disabledRules.has(ruleId);
}
public generateSelector(): string | null {
return null;
}
}
......@@ -387,6 +387,54 @@ describe("HtmlElement", () => {
});
});
describe("generateSelector()", () => {
let parser: Parser;
beforeAll(() => {
parser = new Parser(Config.empty());
});
it("should generate a unique selector", () => {
expect.assertions(1);
const document = parser.parseHtml(`
<div>
<i>a</i>
<p>b</p>
<i>c</i>
<p>d</p>
</div>
`);
const el = document.querySelector("div").childElements[3];
expect(el.generateSelector()).toEqual("div > p:nth-child(4)");
});
it("should use id if a unique id is present", () => {
expect.assertions(1);
const document = parser.parseHtml(`
<div>
<div id="foo">
<p></p>
</div>
</div>
`);
const el = document.querySelector("p");
expect(el.generateSelector()).toEqual("#foo > p");
});
it("should normalize tagnames", () => {
expect.assertions(1);
const document = parser.parseHtml(`<dIV></DIv>`);
const el = document.querySelector("div");
expect(el.generateSelector()).toEqual("div");
});
it("root element should not receive selector", () => {
expect.assertions(1);
const el = HtmlElement.rootNode(null);
expect(el.generateSelector()).toBeNull();
});
});
describe("is()", () => {
it("should match tagname", () => {
const el = new HtmlElement("foo");
......
......@@ -134,6 +134,52 @@ export class HtmlElement extends DOMNode {
return null;
}
/**
* Generate a DOM selector for this element. The returned selector will be
* unique inside the current document.
*/
public generateSelector(): string | null {
/* root element cannot have a selector as it isn't a proper element */
if (this.isRootElement()) {
return null;
}
const parts = [];
let root: HtmlElement;
for (root = this; root.parent; root = root.parent) {
/* .. */
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
for (let cur: HtmlElement = this; cur.parent; cur = cur.parent) {
/* if a unique id is present, use it and short-circuit */
if (cur.id) {
const matches = root.querySelectorAll(`#${cur.id}`);
if (matches.length === 1) {
parts.push(`#${cur.id}`);
break;
}
}
const parent = cur.parent;
const child = parent.childElements;
const index = child.findIndex(it => it.unique === cur.unique);
const numOfType = child.filter(it => it.is(cur.tagName)).length;
const solo = numOfType === 1;
/* if this is the only tagName in this level of siblings nth-child isn't needed */
if (solo) {
parts.push(cur.tagName.toLowerCase());
continue;
}
/* this will generate the worst kind of selector but at least it will be accurate (optimizations welcome) */
parts.push(`${cur.tagName.toLowerCase()}:nth-child(${index + 1})`);
}
return parts.reverse().join(" > ");
}
/**
* Tests if this element has given tagname.
*
......@@ -302,7 +348,7 @@ export class HtmlElement extends DOMNode {
* @param {string} key - Attribute name
* @return Attribute value or null.
*/
public getAttributeValue(key: string): string {
public getAttributeValue(key: string): string | null {
const attr = this.getAttribute(key);
if (attr) {
return attr.value !== null ? attr.value.toString() : null;
......@@ -336,7 +382,10 @@ export class HtmlElement extends DOMNode {
return new DOMTokenList(classes);
}
public get id(): string {
/**
* Get element ID if present.
*/
public get id(): string | null {
return this.getAttributeValue("id");
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment