From 2ce12a8634dd6cba739d8f720fd13ee17e4b81d7 Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Fri, 13 Dec 2024 11:20:00 +0100 Subject: [PATCH] Release 4.0.1 --- README.md | 48 +++++++++++++++++++++++++++++++++++------- finder.js | 40 +++++++++++++++++++++++------------ finder.ts | 59 +++++++++++++++++++++++++++++++++++++++------------- jsr.json | 1 + package.json | 6 ++---- 5 files changed, 115 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 806a732..b034a1b 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ ## Features -* Generates **shortest** CSS selectors. -* **Unique** CSS selectors per page. -* Stable and **robust** CSS selectors. -* Size: **1.5kb** (minified & gzipped). +* Generates **shortest** CSS selectors +* **Unique** CSS selectors per page +* Stable and **robust** CSS selectors +* Size: **1.5kb** (minified & gzipped) ## Install @@ -42,9 +42,7 @@ An example of a generated selector: ```js const selector = finder(event.target, { root: document.body, - timeoutMs: 1000, - seedMinLength: 3, - optimizedMinLength: 2, + timeoutMs: 1000, }); ``` @@ -56,6 +54,42 @@ Defines the root of the search. Defaults to `document.body`. Timeout to search for a selector. Defaults to `1000ms`. After the timeout, finder fallbacks to `nth-child` selectors. +### className + +Function that determines if a class name may be used in a selector. Defaults to a word-like class names. + +You can extend the default behaviour wrapping the `className` function: + +```js +import { finder, className } from '@medv/finder'; + +finder(event.target, { + className: name => className(name) || name.startsWith('my-class-'), +}); +``` + +### tagName + +Function that determines if a tag name may be used in a selector. Defaults to `() => true`. + +### attr + +Function that determines if an attribute may be used in a selector. Defaults to a word-like attribute names and values. + +You can extend the default behaviour wrapping the `attr` function: + +```js +import { finder, attr } from '@medv/finder'; + +finder(event.target, { + attr: (name, value) => attr(name, value) || name.startsWith('data-my-attr-'), +}); +``` + +### idName + +Function that determines if an id name may be used in a selector. Defaults to a word-like id names. + ### seedMinLength Minimum length of levels in fining selector. Defaults to `3`. diff --git a/finder.js b/finder.js index 0efed35..cb118d9 100644 --- a/finder.js +++ b/finder.js @@ -1,6 +1,28 @@ // License: MIT // Author: Anton Medvedev // Source: https://github.com/antonmedv/finder +const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href']); +/** Check if attribute name and value are word-like. */ +export function attr(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; +} +/** Check if id name is word-like. */ +export function idName(name) { + return wordLike(name); +} +/** Check if class name is word-like. */ +export function className(name) { + return wordLike(name); +} +/** Check if tag name is word-like. */ +export function tagName(name) { + return true; +} +/** Finds unique CSS selectors for the given element. */ export function finder(input, options) { if (input.nodeType !== Node.ELEMENT_NODE) { throw new Error(`Can't generate CSS selector for non-element node type.`); @@ -10,10 +32,10 @@ export function finder(input, options) { } const defaults = { root: document.body, - idName: wordLike, - className: wordLike, - tagName: (name) => true, - attr: useAttr, + idName: idName, + className: className, + tagName: tagName, + attr: attr, timeoutMs: 1000, seedMinLength: 3, optimizedMinLength: 2, @@ -79,7 +101,7 @@ function* search(input, config, rootDocument) { yield candidate; } } -export function wordLike(name) { +function wordLike(name) { if (/^[a-z0-9\-]{3,}$/i.test(name)) { const words = name.split(/-|[A-Z]/); for (const word of words) { @@ -94,14 +116,6 @@ 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 tie(element, config) { const level = []; const elementId = element.getAttribute('id'); diff --git a/finder.ts b/finder.ts index 939f97b..3e75ddc 100644 --- a/finder.ts +++ b/finder.ts @@ -8,18 +8,57 @@ type Knot = { level?: number } +const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href']) + +/** Check if attribute name and value are word-like. */ +export function attr(name: string, value: string): boolean { + 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 +} + +/** Check if id name is word-like. */ +export function idName(name: string): boolean { + return wordLike(name) +} + +/** Check if class name is word-like. */ +export function className(name: string): boolean { + return wordLike(name) +} + +/** Check if tag name is word-like. */ +export function tagName(name: string): boolean { + return true +} + +/** Configuration options for the finder. */ export type Options = { + /** The root element to start the search from. */ root: Element + /** Function that determines if an id name may be used in a selector. */ idName: (name: string) => boolean + /** Function that determines if a class name may be used in a selector. */ className: (name: string) => boolean + /** Function that determines if a tag name may be used in a selector. */ tagName: (name: string) => boolean + /** Function that determines if an attribute may be used in a selector. */ attr: (name: string, value: string) => boolean + /** Timeout to search for a selector. */ timeoutMs: number + /** Minimum length of levels in fining selector. */ seedMinLength: number + /** Minimum length for optimising selector. */ optimizedMinLength: number + /** Maximum number of path checks. */ maxNumberOfPathChecks: number } +/** Finds unique CSS selectors for the given element. */ export function finder(input: Element, options?: Partial): string { if (input.nodeType !== Node.ELEMENT_NODE) { throw new Error(`Can't generate CSS selector for non-element node type.`) @@ -29,10 +68,10 @@ export function finder(input: Element, options?: Partial): string { } const defaults: Options = { root: document.body, - idName: wordLike, - className: wordLike, - tagName: (name: string) => true, - attr: useAttr, + idName: idName, + className: className, + tagName: tagName, + attr: attr, timeoutMs: 1000, seedMinLength: 3, optimizedMinLength: 2, @@ -115,7 +154,7 @@ function* search( } } -export function wordLike(name: string): boolean { +function wordLike(name: string): boolean { if (/^[a-z0-9\-]{3,}$/i.test(name)) { const words = name.split(/-|[A-Z]/) for (const word of words) { @@ -131,16 +170,6 @@ 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 tie(element: Element, config: Options): Knot[] { const level: Knot[] = [] diff --git a/jsr.json b/jsr.json index 0f6511b..4058098 100644 --- a/jsr.json +++ b/jsr.json @@ -1,5 +1,6 @@ { "name": "@medv/finder", + "description": "CSS Selector Generator", "version": "3.2.0", "exports": "./finder.ts" } diff --git a/package.json b/package.json index ddf3f95..0062170 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@medv/finder", - "version": "4.0.0", + "version": "4.0.1", "description": "CSS Selector Generator", "type": "module", "main": "finder.js", @@ -13,14 +13,12 @@ "fmt": "prettier --write finder.ts", "fmt:check": "prettier --check finder.ts", "build": "tsc", - "test": "tsc && vitest", - "release": "release-it --access public" + "test": "tsc && vitest" }, "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" },