Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv committed Dec 12, 2024
1 parent 575e699 commit e16a130
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 64 deletions.
84 changes: 54 additions & 30 deletions finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function finder(input, options) {
idName: wordLike,
className: wordLike,
tagName: (name) => true,
attr: (name, value) => wordLike(name) && wordLike(value),
attr: useAttr,
seedMinLength: 2,
optimizedMinLength: 2,
timeoutMs: 1000,
Expand Down Expand Up @@ -73,43 +73,30 @@ export function wordLike(name) {
}
return false;
}
const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href']);
export function useAttr(name, value) {
let nameIsOk = acceptedAttrNames.has(name);
nameIsOk ||= name.startsWith('data-') && wordLike(name);
let valueIsOk = wordLike(value) && value.length < 100;
valueIsOk ||= value.startsWith('#') && wordLike(value.slice(1));
return nameIsOk && valueIsOk;
}
function search(paths, config, input, rootDocument, startTime) {
paths.sort(byPenalty);
for (const candidate of paths) {
const elapsedTimeMs = new Date().getTime() - startTime.getTime();
if (elapsedTimeMs > config.timeoutMs) {
const path = fallbackToNthChild(input, rootDocument);
const path = fallback(input, rootDocument);
if (path) {
return path;
}
throw new Error(`Timeout: Can't find a unique selector after ${elapsedTimeMs}ms`);
throw new Error(`Timeout: Can't find a unique selector after ${config.timeoutMs}ms`);
}
if (unique(candidate, rootDocument)) {
return candidate;
}
}
}
function fallbackToNthChild(input, rootDocument) {
let i = 0;
let current = input;
const path = [];
while (current && current !== rootDocument) {
const index = indexOf(current);
if (index === undefined) {
return;
}
path.push({
name: `:nth-child(${index})`,
penalty: 0,
level: i,
});
current = current.parentElement;
i++;
}
if (unique(path, rootDocument)) {
return path;
}
}
function tie(element, config) {
const level = [];
const elementId = element.getAttribute('id');
Expand All @@ -121,6 +108,9 @@ function tie(element, config) {
}
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
if (attr.name === 'id' || attr.name === 'class') {
continue;
}
if (config.attr(attr.name, attr.value)) {
level.push({
name: `[${CSS.escape(attr.name)}="${CSS.escape(attr.value)}"]`,
Expand All @@ -133,29 +123,29 @@ function tie(element, config) {
if (config.className(name)) {
level.push({
name: '.' + CSS.escape(name),
penalty: 2,
penalty: 1,
});
}
}
const tagName = element.tagName.toLowerCase();
if (config.tagName(tagName)) {
level.push({
name: tagName,
penalty: 3,
penalty: 5,
});
const index = indexOf(element, tagName);
if (index !== undefined) {
level.push({
name: `${tagName}:nth-of-type(${index})`,
penalty: 4,
name: nthOfType(tagName, index),
penalty: 10,
});
}
}
const nth = indexOf(element);
if (nth !== undefined) {
level.push({
name: `:nth-child(${nth})`,
penalty: 9,
name: nthChild(tagName, nth),
penalty: 50,
});
}
return level;
Expand Down Expand Up @@ -204,6 +194,40 @@ function indexOf(input, tagName) {
}
return i;
}
function fallback(input, rootDocument) {
let i = 0;
let current = input;
const path = [];
while (current && current !== rootDocument) {
const tagName = current.tagName.toLowerCase();
const index = indexOf(current, tagName);
if (index === undefined) {
return;
}
path.push({
name: nthOfType(tagName, index),
penalty: NaN,
level: i,
});
current = current.parentElement;
i++;
}
if (unique(path, rootDocument)) {
return path;
}
}
function nthChild(tagName, index) {
if (tagName === 'html') {
return 'html';
}
return `${tagName}:nth-child(${index})`;
}
function nthOfType(tagName, index) {
if (tagName === 'html') {
return 'html';
}
return `${tagName}:nth-of-type(${index})`;
}
function* combinations(stack, path = []) {
if (stack.length > 0) {
for (let node of stack[0]) {
Expand Down
90 changes: 59 additions & 31 deletions finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function finder(input: Element, options?: Partial<Options>): string {
idName: wordLike,
className: wordLike,
tagName: (name: string) => true,
attr: (name: string, value: string) => wordLike(name) && wordLike(value),
attr: useAttr,
seedMinLength: 2,
optimizedMinLength: 2,
timeoutMs: 1000,
Expand Down Expand Up @@ -100,6 +100,16 @@ export function wordLike(name: string): boolean {
return false
}

const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href'])

export function useAttr(name: string, value: string) {
let nameIsOk = acceptedAttrNames.has(name)
nameIsOk ||= name.startsWith('data-') && wordLike(name)
let valueIsOk = wordLike(value) && value.length < 100
valueIsOk ||= value.startsWith('#') && wordLike(value.slice(1))
return nameIsOk && valueIsOk
}

function search(
paths: Knot[][],
config: Options,
Expand All @@ -111,12 +121,12 @@ function search(
for (const candidate of paths) {
const elapsedTimeMs = new Date().getTime() - startTime.getTime()
if (elapsedTimeMs > config.timeoutMs) {
const path = fallbackToNthChild(input, rootDocument)
const path = fallback(input, rootDocument)
if (path) {
return path
}
throw new Error(
`Timeout: Can't find a unique selector after ${elapsedTimeMs}ms`,
`Timeout: Can't find a unique selector after ${config.timeoutMs}ms`,
)
}
if (unique(candidate, rootDocument)) {
Expand All @@ -125,28 +135,6 @@ function search(
}
}

function fallbackToNthChild(input: Element, rootDocument: Element | Document) {
let i = 0
let current: Element | null = input
const path: Knot[] = []
while (current && current !== rootDocument) {
const index = indexOf(current)
if (index === undefined) {
return
}
path.push({
name: `:nth-child(${index})`,
penalty: 0,
level: i,
})
current = current.parentElement
i++
}
if (unique(path, rootDocument)) {
return path
}
}

function tie(element: Element, config: Options): Knot[] {
const level: Knot[] = []

Expand All @@ -160,6 +148,9 @@ function tie(element: Element, config: Options): Knot[] {

for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i]
if (attr.name === 'id' || attr.name === 'class') {
continue
}
if (config.attr(attr.name, attr.value)) {
level.push({
name: `[${CSS.escape(attr.name)}="${CSS.escape(attr.value)}"]`,
Expand All @@ -173,7 +164,7 @@ function tie(element: Element, config: Options): Knot[] {
if (config.className(name)) {
level.push({
name: '.' + CSS.escape(name),
penalty: 2,
penalty: 1,
})
}
}
Expand All @@ -182,23 +173,23 @@ function tie(element: Element, config: Options): Knot[] {
if (config.tagName(tagName)) {
level.push({
name: tagName,
penalty: 3,
penalty: 5,
})

const index = indexOf(element, tagName)
if (index !== undefined) {
level.push({
name: `${tagName}:nth-of-type(${index})`,
penalty: 4,
name: nthOfType(tagName, index),
penalty: 10,
})
}
}

const nth = indexOf(element)
if (nth !== undefined) {
level.push({
name: `:nth-child(${nth})`,
penalty: 9,
name: nthChild(tagName, nth),
penalty: 50,
})
}

Expand Down Expand Up @@ -254,6 +245,43 @@ function indexOf(input: Element, tagName?: string): number | undefined {
return i
}

function fallback(input: Element, rootDocument: Element | Document) {
let i = 0
let current: Element | null = input
const path: Knot[] = []
while (current && current !== rootDocument) {
const tagName = current.tagName.toLowerCase()
const index = indexOf(current, tagName)
if (index === undefined) {
return
}
path.push({
name: nthOfType(tagName, index),
penalty: NaN,
level: i,
})
current = current.parentElement
i++
}
if (unique(path, rootDocument)) {
return path
}
}

function nthChild(tagName: string, index: number) {
if (tagName === 'html') {
return 'html'
}
return `${tagName}:nth-child(${index})`
}

function nthOfType(tagName: string, index: number) {
if (tagName === 'html') {
return 'html'
}
return `${tagName}:nth-of-type(${index})`
}

function* combinations(stack: Knot[][], path: Knot[] = []): Generator<Knot[]> {
if (stack.length > 0) {
for (let node of stack[0]) {
Expand Down
6 changes: 3 additions & 3 deletions tests/finder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import 'css.escape'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

function check({ file, html, target }, config = {}) {
function check({ file, html, query }, config = {}) {
const dom = file
? new JSDOM(readFileSync(path.join(__dirname, file), 'utf8'))
: new JSDOM(html)
globalThis.document = dom.window.document
globalThis.Node = dom.window.Node
const selectors = []
for (let node of document.querySelectorAll(target ?? '*')) {
for (let node of document.querySelectorAll(query ?? '*')) {
let css
try {
css = finder(node, config)
Expand Down Expand Up @@ -61,7 +61,7 @@ test('tailwindcss', () => {
test('google', () => {
check({
file: 'pages/google.com.html',
target: '[href="https://github.com/antonmedv/finder"]',
query: '[href]',
})
})

Expand Down

0 comments on commit e16a130

Please sign in to comment.