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 9c8e379 commit 575e699
Show file tree
Hide file tree
Showing 10 changed files with 7,772 additions and 2,626 deletions.
75 changes: 49 additions & 26 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) => false,
attr: (name, value) => wordLike(name) && wordLike(value),
seedMinLength: 2,
optimizedMinLength: 2,
timeoutMs: 1000,
Expand All @@ -26,7 +26,7 @@ export function finder(input, options) {
let foundPath;
let current = input;
let i = 0;
while (current) {
while (current && current !== rootDocument) {
const level = tie(current, config);
for (let node of level) {
node.level = i;
Expand All @@ -36,35 +36,51 @@ export function finder(input, options) {
i++;
paths.push(...combinations(stack));
if (i >= config.seedMinLength) {
foundPath = search(paths, config, rootDocument, startTime);
foundPath = search(paths, config, input, rootDocument, startTime);
if (foundPath) {
break;
}
paths = [];
}
}
if (paths.length > 0) {
foundPath = search(paths, config, rootDocument, startTime);
foundPath = search(paths, config, input, rootDocument, startTime);
}
if (!foundPath) {
throw new Error(`Selector was not found.`);
}
const optimized = [...optimize(foundPath, input, config, rootDocument, startTime)];
const optimized = [
...optimize(foundPath, input, config, rootDocument, startTime),
];
optimized.sort(byPenalty);
if (optimized.length > 0) {
return selector(optimized[0]);
}
return selector(foundPath);
}
function search(paths, config, rootDocument, startTime) {
export function wordLike(name) {
if (/^[a-z0-9\-]{3,}$/i.test(name)) {
const words = name.split(/-|[A-Z]/);
for (const word of words) {
if (word.length <= 2) {
return false;
}
if (/[^aeiou]{4,}/i.test(word)) {
return false;
}
}
return true;
}
return false;
}
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) {
for (let i = paths.length - 1; i >= paths.length - 10 && i >= 0; i--) {
if (unique(paths[i], rootDocument)) {
return paths[i];
}
const path = fallbackToNthChild(input, rootDocument);
if (path) {
return path;
}
throw new Error(`Timeout: Can't find a unique selector after ${elapsedTimeMs}ms`);
}
Expand All @@ -73,20 +89,26 @@ function search(paths, config, rootDocument, startTime) {
}
}
}
function wordLike(name) {
if (/^[a-z0-9\-]{3,}$/i.test(name)) {
const words = name.split(/-|[A-Z]/);
for (const word of words) {
if (word.length <= 2) {
return false;
}
if (/[^aeiou]{4,}/i.test(word)) {
return false;
}
function fallbackToNthChild(input, rootDocument) {
let i = 0;
let current = input;
const path = [];
while (current && current !== rootDocument) {
const index = indexOf(current);
if (index === undefined) {
return;
}
return true;
path.push({
name: `:nth-child(${index})`,
penalty: 0,
level: i,
});
current = current.parentElement;
i++;
}
if (unique(path, rootDocument)) {
return path;
}
return false;
}
function tie(element, config) {
const level = [];
Expand Down Expand Up @@ -170,9 +192,9 @@ function indexOf(input, tagName) {
}
let i = 0;
while (child) {
if (child.nodeType === Node.ELEMENT_NODE
&& (tagName === undefined
|| child.tagName.toLowerCase() === tagName)) {
if (child.nodeType === Node.ELEMENT_NODE &&
(tagName === undefined ||
child.tagName.toLowerCase() === tagName)) {
i++;
}
if (child === input) {
Expand Down Expand Up @@ -221,7 +243,8 @@ function* optimize(path, input, config, rootDocument, startTime) {
}
const newPath = [...path];
newPath.splice(i, 1);
if (unique(newPath, rootDocument) && rootDocument.querySelector(selector(newPath)) === input) {
if (unique(newPath, rootDocument) &&
rootDocument.querySelector(selector(newPath)) === input) {
yield newPath;
yield* optimize(newPath, input, config, rootDocument, startTime);
}
Expand Down
96 changes: 63 additions & 33 deletions finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@ export function finder(input: Element, options?: Partial<Options>): string {
idName: wordLike,
className: wordLike,
tagName: (name: string) => true,
attr: (name: string, value: string) => false,
attr: (name: string, value: string) => wordLike(name) && wordLike(value),
seedMinLength: 2,
optimizedMinLength: 2,
timeoutMs: 1000,
}

const config = {...defaults, ...options}
const config = { ...defaults, ...options }
const rootDocument = findRootDocument(config.root, defaults)

const stack: Knot[][] = []
let paths: Knot[][] = []
let foundPath: Knot[] | undefined
let current: Element | null = input
let i = 0
while (current) {
while (current && current !== rootDocument) {
const level = tie(current, config)
for (let node of level) {
node.level = i
Expand All @@ -58,7 +58,7 @@ export function finder(input: Element, options?: Partial<Options>): string {
paths.push(...combinations(stack))

if (i >= config.seedMinLength) {
foundPath = search(paths, config, rootDocument, startTime)
foundPath = search(paths, config, input, rootDocument, startTime)
if (foundPath) {
break
}
Expand All @@ -67,53 +67,84 @@ export function finder(input: Element, options?: Partial<Options>): string {
}

if (paths.length > 0) {
foundPath = search(paths, config, rootDocument, startTime)
foundPath = search(paths, config, input, rootDocument, startTime)
}

if (!foundPath) {
throw new Error(`Selector was not found.`)
}

const optimized = [...optimize(foundPath, input, config, rootDocument, startTime)]
const optimized = [
...optimize(foundPath, input, config, rootDocument, startTime),
]
optimized.sort(byPenalty)
if (optimized.length > 0) {
return selector(optimized[0])
}
return selector(foundPath)
}

function search(paths: Knot[][], config: Options, rootDocument: Element | Document, startTime: Date) {
export function wordLike(name: string): boolean {
if (/^[a-z0-9\-]{3,}$/i.test(name)) {
const words = name.split(/-|[A-Z]/)
for (const word of words) {
if (word.length <= 2) {
return false
}
if (/[^aeiou]{4,}/i.test(word)) {
return false
}
}
return true
}
return false
}

function search(
paths: Knot[][],
config: Options,
input: Element,
rootDocument: Element | Document,
startTime: Date,
) {
paths.sort(byPenalty)
for (const candidate of paths) {
const elapsedTimeMs = new Date().getTime() - startTime.getTime()
if (elapsedTimeMs > config.timeoutMs) {
for (let i = paths.length - 1; i >= paths.length - 10 && i >= 0; i--) {
if (unique(paths[i], rootDocument)) {
return paths[i]
}
const path = fallbackToNthChild(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 ${elapsedTimeMs}ms`,
)
}
if (unique(candidate, rootDocument)) {
return candidate
}
}
}

function wordLike(name: string) {
if (/^[a-z0-9\-]{3,}$/i.test(name)) {
const words = name.split(/-|[A-Z]/)
for (const word of words) {
if (word.length <= 2) {
return false
}
if (/[^aeiou]{4,}/i.test(word)) {
return false
}
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
}
return true
path.push({
name: `:nth-child(${index})`,
penalty: 0,
level: i,
})
current = current.parentElement
i++
}
if (unique(path, rootDocument)) {
return path
}
return false
}

function tie(element: Element, config: Options): Knot[] {
Expand Down Expand Up @@ -209,11 +240,9 @@ function indexOf(input: Element, tagName?: string): number | undefined {
let i = 0
while (child) {
if (
child.nodeType === Node.ELEMENT_NODE
&& (
tagName === undefined
|| (child as Element).tagName.toLowerCase() === tagName
)
child.nodeType === Node.ELEMENT_NODE &&
(tagName === undefined ||
(child as Element).tagName.toLowerCase() === tagName)
) {
i++
}
Expand Down Expand Up @@ -249,9 +278,7 @@ function unique(path: Knot[], rootDocument: Element | Document) {
const css = selector(path)
switch (rootDocument.querySelectorAll(css).length) {
case 0:
throw new Error(
`Can't select any node with this selector: ${css}`,
)
throw new Error(`Can't select any node with this selector: ${css}`)
case 1:
return true
default:
Expand All @@ -274,7 +301,10 @@ function* optimize(
}
const newPath = [...path]
newPath.splice(i, 1)
if (unique(newPath, rootDocument) && rootDocument.querySelector(selector(newPath)) === input) {
if (
unique(newPath, rootDocument) &&
rootDocument.querySelector(selector(newPath)) === input
) {
yield newPath
yield* optimize(newPath, input, config, rootDocument, startTime)
}
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,25 @@
"*.js"
],
"scripts": {
"fmt": "prettier --write **/*.{js,ts}",
"fmt:check": "prettier --check **/*.{js,ts}",
"build": "tsc",
"test": "tsc && vitest",
"release": "release-it --access public"
},
"devDependencies": {
"css.escape": "^1.5.1",
"jsdom": "^25.0.1",
"prettier": "^3.4.2",
"release-it": "^17.10.0",
"typescript": "5.7.2",
"vitest": "^2.1.8"
},
"prettier": {
"semi": false,
"singleQuote": true,
"endOfLine": "lf"
},
"author": "Anton Medvedev <[email protected]>",
"license": "MIT",
"homepage": "https://github.com/antonmedv/finder",
Expand Down
6 changes: 6 additions & 0 deletions tests/__snapshots__/finder.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,12 @@ exports[`github 1`] = `
]
`;

exports[`google 1`] = `
[
":nth-child(1) > :nth-child(2) > :nth-child(6) > :nth-child(1) > :nth-child(14) > :nth-child(1) > :nth-child(1) > :nth-child(2) > :nth-child(2) > :nth-child(1) > :nth-child(2) > :nth-child(10) > :nth-child(1) > :nth-child(1) > :nth-child(1) > :nth-child(1) > :nth-child(1) > :nth-child(1) > :nth-child(1)",
]
`;

exports[`stripe 1`] = `
[
"html",
Expand Down
Loading

0 comments on commit 575e699

Please sign in to comment.