Skip to content

Commit

Permalink
Release 4.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv committed Dec 13, 2024
1 parent 761a84f commit 2ce12a8
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 39 deletions.
48 changes: 41 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
});
```

Expand All @@ -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`.
Expand Down
40 changes: 27 additions & 13 deletions finder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
// License: MIT
// Author: Anton Medvedev <[email protected]>
// 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.`);
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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');
Expand Down
59 changes: 44 additions & 15 deletions finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Options>): string {
if (input.nodeType !== Node.ELEMENT_NODE) {
throw new Error(`Can't generate CSS selector for non-element node type.`)
Expand All @@ -29,10 +68,10 @@ export function finder(input: Element, options?: Partial<Options>): 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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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[] = []

Expand Down
1 change: 1 addition & 0 deletions jsr.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@medv/finder",
"description": "CSS Selector Generator",
"version": "3.2.0",
"exports": "./finder.ts"
}
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@medv/finder",
"version": "4.0.0",
"version": "4.0.1",
"description": "CSS Selector Generator",
"type": "module",
"main": "finder.js",
Expand All @@ -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"
},
Expand Down

0 comments on commit 2ce12a8

Please sign in to comment.