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 a1275b0 commit 2f21cad
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 88 deletions.
75 changes: 40 additions & 35 deletions finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Author: Anton Medvedev <[email protected]>
// Source: https://github.com/antonmedv/finder
export function finder(input, options) {
const startTime = new Date();
if (input.nodeType !== Node.ELEMENT_NODE) {
throw new Error(`Can't generate CSS selector for non-element node type.`);
}
Expand All @@ -15,15 +14,47 @@ export function finder(input, options) {
className: wordLike,
tagName: (name) => true,
attr: useAttr,
timeoutMs: 1000,
seedMinLength: 2,
optimizedMinLength: 2,
timeoutMs: 1000,
maxNumberOfPathChecks: Infinity,
};
const startTime = new Date();
const config = { ...defaults, ...options };
const rootDocument = findRootDocument(config.root, defaults);
let foundPath;
let count = 0;
for (const candidate of search(input, config, rootDocument)) {
const elapsedTimeMs = new Date().getTime() - startTime.getTime();
if (elapsedTimeMs > config.timeoutMs ||
count >= config.maxNumberOfPathChecks) {
const fPath = fallback(input, rootDocument);
if (!fPath) {
throw new Error(`Timeout: Can't find a unique selector after ${config.timeoutMs}ms`);
}
return selector(fPath);
}
count++;
if (unique(candidate, rootDocument)) {
foundPath = candidate;
break;
}
}
if (!foundPath) {
throw new Error(`Selector was not found.`);
}
const optimized = [
...optimize(foundPath, input, config, rootDocument, startTime),
];
optimized.sort(byPenalty);
if (optimized.length > 0) {
return selector(optimized[0]);
}
return selector(foundPath);
}
function* search(input, config, rootDocument) {
const stack = [];
let paths = [];
let foundPath;
let current = input;
let i = 0;
while (current && current !== rootDocument) {
Expand All @@ -36,27 +67,17 @@ export function finder(input, options) {
i++;
paths.push(...combinations(stack));
if (i >= config.seedMinLength) {
foundPath = search(paths, config, input, rootDocument, startTime);
if (foundPath) {
break;
paths.sort(byPenalty);
for (const candidate of paths) {
yield candidate;
}
paths = [];
}
}
if (paths.length > 0) {
foundPath = search(paths, config, input, rootDocument, startTime);
}
if (!foundPath) {
throw new Error(`Selector was not found.`);
}
const optimized = [
...optimize(foundPath, input, config, rootDocument, startTime),
];
optimized.sort(byPenalty);
if (optimized.length > 0) {
return selector(optimized[0]);
paths.sort(byPenalty);
for (const candidate of paths) {
yield candidate;
}
return selector(foundPath);
}
export function wordLike(name) {
if (/^[a-z0-9\-]{3,}$/i.test(name)) {
Expand All @@ -81,22 +102,6 @@ export function useAttr(name, value) {
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 = fallback(input, rootDocument);
if (path) {
return path;
}
throw new Error(`Timeout: Can't find a unique selector after ${config.timeoutMs}ms`);
}
if (unique(candidate, rootDocument)) {
return candidate;
}
}
}
function tie(element, config) {
const level = [];
const elementId = element.getAttribute('id');
Expand Down
100 changes: 53 additions & 47 deletions finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ export type Options = {
className: (name: string) => boolean
tagName: (name: string) => boolean
attr: (name: string, value: string) => boolean
timeoutMs: number
seedMinLength: number
optimizedMinLength: number
timeoutMs: number
maxNumberOfPathChecks: number
}

export function finder(input: Element, options?: Partial<Options>): string {
const startTime = new Date()
if (input.nodeType !== Node.ELEMENT_NODE) {
throw new Error(`Can't generate CSS selector for non-element node type.`)
}
Expand All @@ -33,17 +33,60 @@ export function finder(input: Element, options?: Partial<Options>): string {
className: wordLike,
tagName: (name: string) => true,
attr: useAttr,
timeoutMs: 1000,
seedMinLength: 2,
optimizedMinLength: 2,
timeoutMs: 1000,
maxNumberOfPathChecks: Infinity,
}

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

let foundPath: Knot[] | undefined
let count = 0
for (const candidate of search(input, config, rootDocument)) {
const elapsedTimeMs = new Date().getTime() - startTime.getTime()
if (
elapsedTimeMs > config.timeoutMs ||
count >= config.maxNumberOfPathChecks
) {
const fPath = fallback(input, rootDocument)
if (!fPath) {
throw new Error(
`Timeout: Can't find a unique selector after ${config.timeoutMs}ms`,
)
}
return selector(fPath)
}
count++
if (unique(candidate, rootDocument)) {
foundPath = candidate
break
}
}

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

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

function* search(
input: Element,
config: Options,
rootDocument: Element | Document,
): Generator<Knot[]> {
const stack: Knot[][] = []
let paths: Knot[][] = []
let foundPath: Knot[] | undefined
let current: Element | null = input
let i = 0
while (current && current !== rootDocument) {
Expand All @@ -58,30 +101,18 @@ export function finder(input: Element, options?: Partial<Options>): string {
paths.push(...combinations(stack))

if (i >= config.seedMinLength) {
foundPath = search(paths, config, input, rootDocument, startTime)
if (foundPath) {
break
paths.sort(byPenalty)
for (const candidate of paths) {
yield candidate
}
paths = []
}
}

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

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

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

export function wordLike(name: string): boolean {
Expand Down Expand Up @@ -110,31 +141,6 @@ export function useAttr(name: string, value: string) {
return nameIsOk && valueIsOk
}

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) {
const path = fallback(input, rootDocument)
if (path) {
return path
}
throw new Error(
`Timeout: Can't find a unique selector after ${config.timeoutMs}ms`,
)
}
if (unique(candidate, rootDocument)) {
return candidate
}
}
}

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

Expand Down
12 changes: 6 additions & 6 deletions tests/__snapshots__/finder.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -745,15 +745,15 @@ exports[`google 1`] = `
"div:nth-of-type(7) > a:nth-of-type(2)",
"div > a:nth-of-type(3)",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"div:nth-of-type(3) > .vt6azd a",
"div:nth-of-type(4) > .vt6azd a",
"div:nth-of-type(5) > .vt6azd a",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(4) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(5) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(6) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"div:nth-of-type(2) > div > div > div > div > div > .truncation-information",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(7) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"div:nth-of-type(8) > .vt6azd a",
"div:nth-of-type(9) > .vt6azd a",
"div:nth-of-type(10) a",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(8) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(9) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(10) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
"div:nth-child(2) > div > div > div > div > span > a",
"div:nth-of-type(3) > div > div > div > div > a",
"html > body:nth-of-type(1) > div:nth-of-type(3) > div:nth-of-type(1) > div:nth-of-type(13) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(12) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > div:nth-of-type(1) > span:nth-of-type(1) > a:nth-of-type(1)",
Expand Down
5 changes: 5 additions & 0 deletions tests/finder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

function check({ file, html, query }, config = {}) {
config = {
timeoutMs: Infinity,
maxNumberOfPathChecks: 2_000,
...config,
}
const dom = file
? new JSDOM(readFileSync(path.join(__dirname, file), 'utf8'))
: new JSDOM(html)
Expand Down

0 comments on commit 2f21cad

Please sign in to comment.