Skip to content

Commit 2ce12a8

Browse files
committed
Release 4.0.1
1 parent 761a84f commit 2ce12a8

File tree

5 files changed

+115
-39
lines changed

5 files changed

+115
-39
lines changed

README.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
## Features
1010

11-
* Generates **shortest** CSS selectors.
12-
* **Unique** CSS selectors per page.
13-
* Stable and **robust** CSS selectors.
14-
* Size: **1.5kb** (minified & gzipped).
11+
* Generates **shortest** CSS selectors
12+
* **Unique** CSS selectors per page
13+
* Stable and **robust** CSS selectors
14+
* Size: **1.5kb** (minified & gzipped)
1515

1616
## Install
1717

@@ -42,9 +42,7 @@ An example of a generated selector:
4242
```js
4343
const selector = finder(event.target, {
4444
root: document.body,
45-
timeoutMs: 1000,
46-
seedMinLength: 3,
47-
optimizedMinLength: 2,
45+
timeoutMs: 1000,
4846
});
4947
```
5048

@@ -56,6 +54,42 @@ Defines the root of the search. Defaults to `document.body`.
5654

5755
Timeout to search for a selector. Defaults to `1000ms`. After the timeout, finder fallbacks to `nth-child` selectors.
5856

57+
### className
58+
59+
Function that determines if a class name may be used in a selector. Defaults to a word-like class names.
60+
61+
You can extend the default behaviour wrapping the `className` function:
62+
63+
```js
64+
import { finder, className } from '@medv/finder';
65+
66+
finder(event.target, {
67+
className: name => className(name) || name.startsWith('my-class-'),
68+
});
69+
```
70+
71+
### tagName
72+
73+
Function that determines if a tag name may be used in a selector. Defaults to `() => true`.
74+
75+
### attr
76+
77+
Function that determines if an attribute may be used in a selector. Defaults to a word-like attribute names and values.
78+
79+
You can extend the default behaviour wrapping the `attr` function:
80+
81+
```js
82+
import { finder, attr } from '@medv/finder';
83+
84+
finder(event.target, {
85+
attr: (name, value) => attr(name, value) || name.startsWith('data-my-attr-'),
86+
});
87+
```
88+
89+
### idName
90+
91+
Function that determines if an id name may be used in a selector. Defaults to a word-like id names.
92+
5993
### seedMinLength
6094

6195
Minimum length of levels in fining selector. Defaults to `3`.

finder.js

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
// License: MIT
22
// Author: Anton Medvedev <anton@medv.io>
33
// Source: https://github.com/antonmedv/finder
4+
const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href']);
5+
/** Check if attribute name and value are word-like. */
6+
export function attr(name, value) {
7+
let nameIsOk = acceptedAttrNames.has(name);
8+
nameIsOk ||= name.startsWith('data-') && wordLike(name);
9+
let valueIsOk = wordLike(value) && value.length < 100;
10+
valueIsOk ||= value.startsWith('#') && wordLike(value.slice(1));
11+
return nameIsOk && valueIsOk;
12+
}
13+
/** Check if id name is word-like. */
14+
export function idName(name) {
15+
return wordLike(name);
16+
}
17+
/** Check if class name is word-like. */
18+
export function className(name) {
19+
return wordLike(name);
20+
}
21+
/** Check if tag name is word-like. */
22+
export function tagName(name) {
23+
return true;
24+
}
25+
/** Finds unique CSS selectors for the given element. */
426
export function finder(input, options) {
527
if (input.nodeType !== Node.ELEMENT_NODE) {
628
throw new Error(`Can't generate CSS selector for non-element node type.`);
@@ -10,10 +32,10 @@ export function finder(input, options) {
1032
}
1133
const defaults = {
1234
root: document.body,
13-
idName: wordLike,
14-
className: wordLike,
15-
tagName: (name) => true,
16-
attr: useAttr,
35+
idName: idName,
36+
className: className,
37+
tagName: tagName,
38+
attr: attr,
1739
timeoutMs: 1000,
1840
seedMinLength: 3,
1941
optimizedMinLength: 2,
@@ -79,7 +101,7 @@ function* search(input, config, rootDocument) {
79101
yield candidate;
80102
}
81103
}
82-
export function wordLike(name) {
104+
function wordLike(name) {
83105
if (/^[a-z0-9\-]{3,}$/i.test(name)) {
84106
const words = name.split(/-|[A-Z]/);
85107
for (const word of words) {
@@ -94,14 +116,6 @@ export function wordLike(name) {
94116
}
95117
return false;
96118
}
97-
const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href']);
98-
export function useAttr(name, value) {
99-
let nameIsOk = acceptedAttrNames.has(name);
100-
nameIsOk ||= name.startsWith('data-') && wordLike(name);
101-
let valueIsOk = wordLike(value) && value.length < 100;
102-
valueIsOk ||= value.startsWith('#') && wordLike(value.slice(1));
103-
return nameIsOk && valueIsOk;
104-
}
105119
function tie(element, config) {
106120
const level = [];
107121
const elementId = element.getAttribute('id');

finder.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,57 @@ type Knot = {
88
level?: number
99
}
1010

11+
const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href'])
12+
13+
/** Check if attribute name and value are word-like. */
14+
export function attr(name: string, value: string): boolean {
15+
let nameIsOk = acceptedAttrNames.has(name)
16+
nameIsOk ||= name.startsWith('data-') && wordLike(name)
17+
18+
let valueIsOk = wordLike(value) && value.length < 100
19+
valueIsOk ||= value.startsWith('#') && wordLike(value.slice(1))
20+
21+
return nameIsOk && valueIsOk
22+
}
23+
24+
/** Check if id name is word-like. */
25+
export function idName(name: string): boolean {
26+
return wordLike(name)
27+
}
28+
29+
/** Check if class name is word-like. */
30+
export function className(name: string): boolean {
31+
return wordLike(name)
32+
}
33+
34+
/** Check if tag name is word-like. */
35+
export function tagName(name: string): boolean {
36+
return true
37+
}
38+
39+
/** Configuration options for the finder. */
1140
export type Options = {
41+
/** The root element to start the search from. */
1242
root: Element
43+
/** Function that determines if an id name may be used in a selector. */
1344
idName: (name: string) => boolean
45+
/** Function that determines if a class name may be used in a selector. */
1446
className: (name: string) => boolean
47+
/** Function that determines if a tag name may be used in a selector. */
1548
tagName: (name: string) => boolean
49+
/** Function that determines if an attribute may be used in a selector. */
1650
attr: (name: string, value: string) => boolean
51+
/** Timeout to search for a selector. */
1752
timeoutMs: number
53+
/** Minimum length of levels in fining selector. */
1854
seedMinLength: number
55+
/** Minimum length for optimising selector. */
1956
optimizedMinLength: number
57+
/** Maximum number of path checks. */
2058
maxNumberOfPathChecks: number
2159
}
2260

61+
/** Finds unique CSS selectors for the given element. */
2362
export function finder(input: Element, options?: Partial<Options>): string {
2463
if (input.nodeType !== Node.ELEMENT_NODE) {
2564
throw new Error(`Can't generate CSS selector for non-element node type.`)
@@ -29,10 +68,10 @@ export function finder(input: Element, options?: Partial<Options>): string {
2968
}
3069
const defaults: Options = {
3170
root: document.body,
32-
idName: wordLike,
33-
className: wordLike,
34-
tagName: (name: string) => true,
35-
attr: useAttr,
71+
idName: idName,
72+
className: className,
73+
tagName: tagName,
74+
attr: attr,
3675
timeoutMs: 1000,
3776
seedMinLength: 3,
3877
optimizedMinLength: 2,
@@ -115,7 +154,7 @@ function* search(
115154
}
116155
}
117156

118-
export function wordLike(name: string): boolean {
157+
function wordLike(name: string): boolean {
119158
if (/^[a-z0-9\-]{3,}$/i.test(name)) {
120159
const words = name.split(/-|[A-Z]/)
121160
for (const word of words) {
@@ -131,16 +170,6 @@ export function wordLike(name: string): boolean {
131170
return false
132171
}
133172

134-
const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href'])
135-
136-
export function useAttr(name: string, value: string) {
137-
let nameIsOk = acceptedAttrNames.has(name)
138-
nameIsOk ||= name.startsWith('data-') && wordLike(name)
139-
let valueIsOk = wordLike(value) && value.length < 100
140-
valueIsOk ||= value.startsWith('#') && wordLike(value.slice(1))
141-
return nameIsOk && valueIsOk
142-
}
143-
144173
function tie(element: Element, config: Options): Knot[] {
145174
const level: Knot[] = []
146175

jsr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "@medv/finder",
3+
"description": "CSS Selector Generator",
34
"version": "3.2.0",
45
"exports": "./finder.ts"
56
}

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@medv/finder",
3-
"version": "4.0.0",
3+
"version": "4.0.1",
44
"description": "CSS Selector Generator",
55
"type": "module",
66
"main": "finder.js",
@@ -13,14 +13,12 @@
1313
"fmt": "prettier --write finder.ts",
1414
"fmt:check": "prettier --check finder.ts",
1515
"build": "tsc",
16-
"test": "tsc && vitest",
17-
"release": "release-it --access public"
16+
"test": "tsc && vitest"
1817
},
1918
"devDependencies": {
2019
"css.escape": "^1.5.1",
2120
"jsdom": "^25.0.1",
2221
"prettier": "^3.4.2",
23-
"release-it": "^17.10.0",
2422
"typescript": "5.7.2",
2523
"vitest": "^2.1.8"
2624
},

0 commit comments

Comments
 (0)