@medv/finder
Advanced tools
Comparing version 2.1.0 to 3.0.0
@@ -1,2 +0,2 @@ | ||
export declare type Options = { | ||
export type Options = { | ||
root: Element; | ||
@@ -3,0 +3,0 @@ idName: (name: string) => boolean; |
@@ -1,9 +0,4 @@ | ||
var Limit; | ||
(function (Limit) { | ||
Limit[Limit["All"] = 0] = "All"; | ||
Limit[Limit["Two"] = 1] = "Two"; | ||
Limit[Limit["One"] = 2] = "One"; | ||
})(Limit || (Limit = {})); | ||
let config; | ||
let rootDocument; | ||
let uniqueCache; | ||
export function finder(input, options) { | ||
@@ -13,4 +8,4 @@ if (input.nodeType !== Node.ELEMENT_NODE) { | ||
} | ||
if ("html" === input.tagName.toLowerCase()) { | ||
return "html"; | ||
if ('html' === input.tagName.toLowerCase()) { | ||
return 'html'; | ||
} | ||
@@ -28,5 +23,6 @@ const defaults = { | ||
}; | ||
config = Object.assign(Object.assign({}, defaults), options); | ||
config = { ...defaults, ...options }; | ||
rootDocument = findRootDocument(config.root, defaults); | ||
let path = bottomUpSearch(input, Limit.All, () => bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One))); | ||
uniqueCache = new Map(); | ||
let path = bottomUpSearch(input, 'all', () => bottomUpSearch(input, 'two', () => bottomUpSearch(input, 'one', () => bottomUpSearch(input, 'none')))); | ||
if (path) { | ||
@@ -57,3 +53,3 @@ const optimized = sort(optimize(path, input)); | ||
let i = 0; | ||
while (current && current !== config.root.parentElement) { | ||
while (current) { | ||
let level = maybe(id(current)) || | ||
@@ -64,3 +60,3 @@ maybe(...attr(current)) || | ||
const nth = index(current); | ||
if (limit === Limit.All) { | ||
if (limit == 'all') { | ||
if (nth) { | ||
@@ -70,3 +66,3 @@ level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))); | ||
} | ||
else if (limit === Limit.Two) { | ||
else if (limit == 'two') { | ||
level = level.slice(0, 1); | ||
@@ -77,3 +73,3 @@ if (nth) { | ||
} | ||
else if (limit === Limit.One) { | ||
else if (limit == 'one') { | ||
const [node] = (level = level.slice(0, 1)); | ||
@@ -84,2 +80,8 @@ if (nth && dispensableNth(node)) { | ||
} | ||
else if (limit == 'none') { | ||
level = [any()]; | ||
if (nth) { | ||
level = [nthChild(level[0], nth)]; | ||
} | ||
} | ||
for (let node of level) { | ||
@@ -101,2 +103,5 @@ node.level = i; | ||
} | ||
if (!path && fallback) { | ||
return fallback(); | ||
} | ||
return path; | ||
@@ -135,8 +140,11 @@ } | ||
function unique(path) { | ||
switch (rootDocument.querySelectorAll(selector(path)).length) { | ||
const css = selector(path); | ||
switch (rootDocument.querySelectorAll(css).length) { | ||
case 0: | ||
throw new Error(`Can't select any node with this selector: ${selector(path)}`); | ||
throw new Error(`Can't select any node with this selector: ${css}`); | ||
case 1: | ||
uniqueCache.set(css, true); | ||
return true; | ||
default: | ||
uniqueCache.set(css, false); | ||
return false; | ||
@@ -146,6 +154,6 @@ } | ||
function id(input) { | ||
const elementId = input.getAttribute("id"); | ||
const elementId = input.getAttribute('id'); | ||
if (elementId && config.idName(elementId)) { | ||
return { | ||
name: "#" + cssesc(elementId, { isIdentifier: true }), | ||
name: '#' + cssesc(elementId, { isIdentifier: true }), | ||
penalty: 0, | ||
@@ -159,3 +167,3 @@ }; | ||
return attrs.map((attr) => ({ | ||
name: "[" + | ||
name: '[' + | ||
cssesc(attr.name, { isIdentifier: true }) + | ||
@@ -171,3 +179,3 @@ '="' + | ||
return names.map((name) => ({ | ||
name: "." + cssesc(name, { isIdentifier: true }), | ||
name: '.' + cssesc(name, { isIdentifier: true }), | ||
penalty: 1, | ||
@@ -188,3 +196,3 @@ })); | ||
return { | ||
name: "*", | ||
name: '*', | ||
penalty: 3, | ||
@@ -221,3 +229,3 @@ }; | ||
function dispensableNth(node) { | ||
return node.name !== "html" && !node.name.startsWith("#"); | ||
return node.name !== 'html' && !node.name.startsWith('#'); | ||
} | ||
@@ -245,3 +253,3 @@ function maybe(...level) { | ||
function sort(paths) { | ||
return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); | ||
return [...paths].sort((a, b) => penalty(a) - penalty(b)); | ||
} | ||
@@ -281,14 +289,14 @@ function* optimize(path, input, scope = { | ||
isIdentifier: false, | ||
quotes: "single", | ||
quotes: 'single', | ||
wrap: false, | ||
}; | ||
function cssesc(string, opt = {}) { | ||
const options = Object.assign(Object.assign({}, defaultOptions), opt); | ||
if (options.quotes != "single" && options.quotes != "double") { | ||
options.quotes = "single"; | ||
const options = { ...defaultOptions, ...opt }; | ||
if (options.quotes != 'single' && options.quotes != 'double') { | ||
options.quotes = 'single'; | ||
} | ||
const quote = options.quotes == "double" ? '"' : "'"; | ||
const quote = options.quotes == 'double' ? '"' : '\''; | ||
const isIdentifier = options.isIdentifier; | ||
const firstChar = string.charAt(0); | ||
let output = ""; | ||
let output = ''; | ||
let counter = 0; | ||
@@ -315,3 +323,3 @@ const length = string.length; | ||
} | ||
value = "\\" + codePoint.toString(16).toUpperCase() + " "; | ||
value = '\\' + codePoint.toString(16).toUpperCase() + ' '; | ||
} | ||
@@ -321,17 +329,17 @@ else { | ||
if (regexAnySingleEscape.test(character)) { | ||
value = "\\" + character; | ||
value = '\\' + character; | ||
} | ||
else { | ||
value = "\\" + codePoint.toString(16).toUpperCase() + " "; | ||
value = '\\' + codePoint.toString(16).toUpperCase() + ' '; | ||
} | ||
} | ||
else if (/[\t\n\f\r\x0B]/.test(character)) { | ||
value = "\\" + codePoint.toString(16).toUpperCase() + " "; | ||
value = '\\' + codePoint.toString(16).toUpperCase() + ' '; | ||
} | ||
else if (character == "\\" || | ||
else if (character == '\\' || | ||
(!isIdentifier && | ||
((character == '"' && quote == character) || | ||
(character == "'" && quote == character))) || | ||
(character == '\'' && quote == character))) || | ||
(isIdentifier && regexSingleEscape.test(character))) { | ||
value = "\\" + character; | ||
value = '\\' + character; | ||
} | ||
@@ -346,6 +354,6 @@ else { | ||
if (/^-[-\d]/.test(output)) { | ||
output = "\\-" + output.slice(1); | ||
output = '\\-' + output.slice(1); | ||
} | ||
else if (/\d/.test(firstChar)) { | ||
output = "\\3" + firstChar + " " + output.slice(1); | ||
output = '\\3' + firstChar + ' ' + output.slice(1); | ||
} | ||
@@ -362,3 +370,3 @@ } | ||
// Strip the space. | ||
return ($1 || "") + $2; | ||
return ($1 || '') + $2; | ||
}); | ||
@@ -365,0 +373,0 @@ if (!isIdentifier && options.wrap) { |
395
finder.ts
@@ -1,39 +0,35 @@ | ||
type Node = { | ||
name: string; | ||
penalty: number; | ||
level?: number; | ||
}; | ||
// License: MIT | ||
// Author: Anton Medvedev <anton@medv.io> | ||
// Source: https://github.com/antonmedv/finder | ||
type Path = Node[]; | ||
enum Limit { | ||
All, | ||
Two, | ||
One, | ||
type Knot = { | ||
name: string | ||
penalty: number | ||
level?: number | ||
} | ||
type Path = Knot[] | ||
export type Options = { | ||
root: Element; | ||
idName: (name: string) => boolean; | ||
className: (name: string) => boolean; | ||
tagName: (name: string) => boolean; | ||
attr: (name: string, value: string) => boolean; | ||
seedMinLength: number; | ||
optimizedMinLength: number; | ||
threshold: number; | ||
maxNumberOfTries: number; | ||
}; | ||
root: Element | ||
idName: (name: string) => boolean | ||
className: (name: string) => boolean | ||
tagName: (name: string) => boolean | ||
attr: (name: string, value: string) => boolean | ||
seedMinLength: number | ||
optimizedMinLength: number | ||
threshold: number | ||
maxNumberOfTries: number | ||
} | ||
let config: Options; | ||
let config: Options | ||
let rootDocument: Document | Element | ||
let rootDocument: Document | Element; | ||
export function finder(input: Element, options?: Partial<Options>) { | ||
if (input.nodeType !== Node.ELEMENT_NODE) { | ||
throw new Error(`Can't generate CSS selector for non-element node type.`); | ||
throw new Error(`Can't generate CSS selector for non-element node type.`) | ||
} | ||
if ("html" === input.tagName.toLowerCase()) { | ||
return "html"; | ||
if ('html' === input.tagName.toLowerCase()) { | ||
return 'html' | ||
} | ||
const defaults: Options = { | ||
@@ -49,22 +45,21 @@ root: document.body, | ||
maxNumberOfTries: 10000, | ||
}; | ||
} | ||
config = { ...defaults, ...options }; | ||
config = {...defaults, ...options} | ||
rootDocument = findRootDocument(config.root, defaults) | ||
rootDocument = findRootDocument(config.root, defaults); | ||
let path = | ||
bottomUpSearch(input, 'all', | ||
() => bottomUpSearch(input, 'two', | ||
() => bottomUpSearch(input, 'one', | ||
() => bottomUpSearch(input, 'none')))) | ||
let path = bottomUpSearch(input, Limit.All, () => | ||
bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)) | ||
); | ||
if (path) { | ||
const optimized = sort(optimize(path, input)); | ||
const optimized = sort(optimize(path, input)) | ||
if (optimized.length > 0) { | ||
path = optimized[0]; | ||
path = optimized[0] | ||
} | ||
return selector(path); | ||
return selector(path) | ||
} else { | ||
throw new Error(`Selector was not found.`); | ||
throw new Error(`Selector was not found.`) | ||
} | ||
@@ -75,8 +70,8 @@ } | ||
if (rootNode.nodeType === Node.DOCUMENT_NODE) { | ||
return rootNode; | ||
return rootNode | ||
} | ||
if (rootNode === defaults.root) { | ||
return rootNode.ownerDocument as Document; | ||
return rootNode.ownerDocument as Document | ||
} | ||
return rootNode; | ||
return rootNode | ||
} | ||
@@ -86,138 +81,130 @@ | ||
input: Element, | ||
limit: Limit, | ||
limit: 'all' | 'two' | 'one' | 'none', | ||
fallback?: () => Path | null | ||
): Path | null { | ||
let path: Path | null = null; | ||
let stack: Node[][] = []; | ||
let current: Element | null = input; | ||
let i = 0; | ||
while (current && current !== config.root.parentElement) { | ||
let level: Node[] = maybe(id(current)) || | ||
let path: Path | null = null | ||
let stack: Knot[][] = [] | ||
let current: Element | null = input | ||
let i = 0 | ||
while (current) { | ||
let level: Knot[] = maybe(id(current)) || | ||
maybe(...attr(current)) || | ||
maybe(...classNames(current)) || | ||
maybe(tagName(current)) || [any()]; | ||
const nth = index(current); | ||
if (limit === Limit.All) { | ||
maybe(tagName(current)) || [any()] | ||
const nth = index(current) | ||
if (limit == 'all') { | ||
if (nth) { | ||
level = level.concat( | ||
level.filter(dispensableNth).map((node) => nthChild(node, nth)) | ||
); | ||
) | ||
} | ||
} else if (limit === Limit.Two) { | ||
level = level.slice(0, 1); | ||
} else if (limit == 'two') { | ||
level = level.slice(0, 1) | ||
if (nth) { | ||
level = level.concat( | ||
level.filter(dispensableNth).map((node) => nthChild(node, nth)) | ||
); | ||
) | ||
} | ||
} else if (limit === Limit.One) { | ||
const [node] = (level = level.slice(0, 1)); | ||
} else if (limit == 'one') { | ||
const [node] = (level = level.slice(0, 1)) | ||
if (nth && dispensableNth(node)) { | ||
level = [nthChild(node, nth)]; | ||
level = [nthChild(node, nth)] | ||
} | ||
} else if (limit == 'none') { | ||
level = [any()] | ||
if (nth) { | ||
level = [nthChild(level[0], nth)] | ||
} | ||
} | ||
for (let node of level) { | ||
node.level = i; | ||
node.level = i | ||
} | ||
stack.push(level); | ||
stack.push(level) | ||
if (stack.length >= config.seedMinLength) { | ||
path = findUniquePath(stack, fallback); | ||
path = findUniquePath(stack, fallback) | ||
if (path) { | ||
break; | ||
break | ||
} | ||
} | ||
current = current.parentElement; | ||
i++; | ||
current = current.parentElement | ||
i++ | ||
} | ||
if (!path) { | ||
path = findUniquePath(stack, fallback); | ||
path = findUniquePath(stack, fallback) | ||
} | ||
return path; | ||
if (!path && fallback) { | ||
return fallback() | ||
} | ||
return path | ||
} | ||
function findUniquePath( | ||
stack: Node[][], | ||
stack: Knot[][], | ||
fallback?: () => Path | null | ||
): Path | null { | ||
const paths = sort(combinations(stack)); | ||
const paths = sort(combinations(stack)) | ||
if (paths.length > config.threshold) { | ||
return fallback ? fallback() : null; | ||
return fallback ? fallback() : null | ||
} | ||
for (let candidate of paths) { | ||
if (unique(candidate)) { | ||
return candidate; | ||
return candidate | ||
} | ||
} | ||
return null; | ||
return null | ||
} | ||
function selector(path: Path): string { | ||
let node = path[0]; | ||
let query = node.name; | ||
let node = path[0] | ||
let query = node.name | ||
for (let i = 1; i < path.length; i++) { | ||
const level = path[i].level || 0; | ||
const level = path[i].level || 0 | ||
if (node.level === level - 1) { | ||
query = `${path[i].name} > ${query}`; | ||
query = `${path[i].name} > ${query}` | ||
} else { | ||
query = `${path[i].name} ${query}`; | ||
query = `${path[i].name} ${query}` | ||
} | ||
node = path[i]; | ||
node = path[i] | ||
} | ||
return query; | ||
return query | ||
} | ||
function penalty(path: Path): number { | ||
return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0); | ||
return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0) | ||
} | ||
function unique(path: Path) { | ||
switch (rootDocument.querySelectorAll(selector(path)).length) { | ||
const css = selector(path) | ||
switch (rootDocument.querySelectorAll(css).length) { | ||
case 0: | ||
throw new Error( | ||
`Can't select any node with this selector: ${selector(path)}` | ||
); | ||
`Can't select any node with this selector: ${css}` | ||
) | ||
case 1: | ||
return true; | ||
return true | ||
default: | ||
return false; | ||
return false | ||
} | ||
} | ||
function id(input: Element): Node | null { | ||
const elementId = input.getAttribute("id"); | ||
function id(input: Element): Knot | null { | ||
const elementId = input.getAttribute('id') | ||
if (elementId && config.idName(elementId)) { | ||
return { | ||
name: "#" + cssesc(elementId, { isIdentifier: true }), | ||
name: '#' + cssesc(elementId, {isIdentifier: true}), | ||
penalty: 0, | ||
}; | ||
} | ||
} | ||
return null; | ||
return null | ||
} | ||
function attr(input: Element): Node[] { | ||
function attr(input: Element): Knot[] { | ||
const attrs = Array.from(input.attributes).filter((attr) => | ||
config.attr(attr.name, attr.value) | ||
); | ||
) | ||
return attrs.map( | ||
(attr): Node => ({ | ||
(attr): Knot => ({ | ||
name: | ||
"[" + | ||
cssesc(attr.name, { isIdentifier: true }) + | ||
'[' + | ||
cssesc(attr.name, {isIdentifier: true}) + | ||
'="' + | ||
@@ -228,18 +215,17 @@ cssesc(attr.value) + | ||
}) | ||
); | ||
) | ||
} | ||
function classNames(input: Element): Node[] { | ||
const names = Array.from(input.classList).filter(config.className); | ||
function classNames(input: Element): Knot[] { | ||
const names = Array.from(input.classList).filter(config.className) | ||
return names.map( | ||
(name): Node => ({ | ||
name: "." + cssesc(name, { isIdentifier: true }), | ||
(name): Knot => ({ | ||
name: '.' + cssesc(name, {isIdentifier: true}), | ||
penalty: 1, | ||
}) | ||
); | ||
) | ||
} | ||
function tagName(input: Element): Node | null { | ||
const name = input.tagName.toLowerCase(); | ||
function tagName(input: Element): Knot | null { | ||
const name = input.tagName.toLowerCase() | ||
if (config.tagName(name)) { | ||
@@ -249,71 +235,66 @@ return { | ||
penalty: 2, | ||
}; | ||
} | ||
} | ||
return null; | ||
return null | ||
} | ||
function any(): Node { | ||
function any(): Knot { | ||
return { | ||
name: "*", | ||
name: '*', | ||
penalty: 3, | ||
}; | ||
} | ||
} | ||
function index(input: Element): number | null { | ||
const parent = input.parentNode; | ||
const parent = input.parentNode | ||
if (!parent) { | ||
return null; | ||
return null | ||
} | ||
let child = parent.firstChild; | ||
let child = parent.firstChild | ||
if (!child) { | ||
return null; | ||
return null | ||
} | ||
let i = 0; | ||
let i = 0 | ||
while (child) { | ||
if (child.nodeType === Node.ELEMENT_NODE) { | ||
i++; | ||
i++ | ||
} | ||
if (child === input) { | ||
break; | ||
break | ||
} | ||
child = child.nextSibling; | ||
child = child.nextSibling | ||
} | ||
return i; | ||
return i | ||
} | ||
function nthChild(node: Node, i: number): Node { | ||
function nthChild(node: Knot, i: number): Knot { | ||
return { | ||
name: node.name + `:nth-child(${i})`, | ||
penalty: node.penalty + 1, | ||
}; | ||
} | ||
} | ||
function dispensableNth(node: Node) { | ||
return node.name !== "html" && !node.name.startsWith("#"); | ||
function dispensableNth(node: Knot) { | ||
return node.name !== 'html' && !node.name.startsWith('#') | ||
} | ||
function maybe(...level: (Node | null)[]): Node[] | null { | ||
const list = level.filter(notEmpty); | ||
function maybe(...level: (Knot | null)[]): Knot[] | null { | ||
const list = level.filter(notEmpty) | ||
if (list.length > 0) { | ||
return list; | ||
return list | ||
} | ||
return null; | ||
return null | ||
} | ||
function notEmpty<T>(value: T | null | undefined): value is T { | ||
return value !== null && value !== undefined; | ||
return value !== null && value !== undefined | ||
} | ||
function* combinations(stack: Node[][], path: Node[] = []): Generator<Node[]> { | ||
function* combinations(stack: Knot[][], path: Knot[] = []): Generator<Knot[]> { | ||
if (stack.length > 0) { | ||
for (let node of stack[0]) { | ||
yield* combinations(stack.slice(1, stack.length), path.concat(node)); | ||
yield* combinations(stack.slice(1, stack.length), path.concat(node)) | ||
} | ||
} else { | ||
yield path; | ||
yield path | ||
} | ||
@@ -323,9 +304,9 @@ } | ||
function sort(paths: Iterable<Path>): Path[] { | ||
return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); | ||
return [...paths].sort((a, b) => penalty(a) - penalty(b)) | ||
} | ||
type Scope = { | ||
counter: number; | ||
visited: Map<string, boolean>; | ||
}; | ||
counter: number | ||
visited: Map<string, boolean> | ||
} | ||
@@ -339,19 +320,19 @@ function* optimize( | ||
} | ||
): Generator<Node[]> { | ||
): Generator<Knot[]> { | ||
if (path.length > 2 && path.length > config.optimizedMinLength) { | ||
for (let i = 1; i < path.length - 1; i++) { | ||
if (scope.counter > config.maxNumberOfTries) { | ||
return; // Okay At least I tried! | ||
return // Okay At least I tried! | ||
} | ||
scope.counter += 1; | ||
const newPath = [...path]; | ||
newPath.splice(i, 1); | ||
const newPathKey = selector(newPath); | ||
scope.counter += 1 | ||
const newPath = [...path] | ||
newPath.splice(i, 1) | ||
const newPathKey = selector(newPath) | ||
if (scope.visited.has(newPathKey)) { | ||
return; | ||
return | ||
} | ||
if (unique(newPath) && same(newPath, input)) { | ||
yield newPath; | ||
scope.visited.set(newPathKey, true); | ||
yield* optimize(newPath, input, scope); | ||
yield newPath | ||
scope.visited.set(newPathKey, true) | ||
yield* optimize(newPath, input, scope) | ||
} | ||
@@ -363,9 +344,9 @@ } | ||
function same(path: Path, input: Element) { | ||
return rootDocument.querySelector(selector(path)) === input; | ||
return rootDocument.querySelector(selector(path)) === input | ||
} | ||
const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/; | ||
const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/; | ||
const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/ | ||
const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/ | ||
const regexExcessiveSpaces = | ||
/(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; | ||
/(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g | ||
@@ -375,22 +356,21 @@ const defaultOptions = { | ||
isIdentifier: false, | ||
quotes: "single", | ||
quotes: 'single', | ||
wrap: false, | ||
}; | ||
} | ||
function cssesc(string: string, opt: Partial<typeof defaultOptions> = {}) { | ||
const options = { ...defaultOptions, ...opt }; | ||
if (options.quotes != "single" && options.quotes != "double") { | ||
options.quotes = "single"; | ||
const options = {...defaultOptions, ...opt} | ||
if (options.quotes != 'single' && options.quotes != 'double') { | ||
options.quotes = 'single' | ||
} | ||
const quote = options.quotes == "double" ? '"' : "'"; | ||
const isIdentifier = options.isIdentifier; | ||
const firstChar = string.charAt(0); | ||
let output = ""; | ||
let counter = 0; | ||
const length = string.length; | ||
const quote = options.quotes == 'double' ? '"' : '\'' | ||
const isIdentifier = options.isIdentifier | ||
const firstChar = string.charAt(0) | ||
let output = '' | ||
let counter = 0 | ||
const length = string.length | ||
while (counter < length) { | ||
const character = string.charAt(counter++); | ||
let codePoint = character.charCodeAt(0); | ||
let value: string | undefined = void 0; | ||
const character = string.charAt(counter++) | ||
let codePoint = character.charCodeAt(0) | ||
let value: string | undefined = void 0 | ||
// If it’s not a printable ASCII character… | ||
@@ -400,45 +380,43 @@ if (codePoint < 0x20 || codePoint > 0x7e) { | ||
// It’s a high surrogate, and there is a next character. | ||
const extra = string.charCodeAt(counter++); | ||
const extra = string.charCodeAt(counter++) | ||
if ((extra & 0xfc00) == 0xdc00) { | ||
// next character is low surrogate | ||
codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; | ||
codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000 | ||
} else { | ||
// It’s an unmatched surrogate; only append this code unit, in case | ||
// the next code unit is the high surrogate of a surrogate pair. | ||
counter--; | ||
counter-- | ||
} | ||
} | ||
value = "\\" + codePoint.toString(16).toUpperCase() + " "; | ||
value = '\\' + codePoint.toString(16).toUpperCase() + ' ' | ||
} else { | ||
if (options.escapeEverything) { | ||
if (regexAnySingleEscape.test(character)) { | ||
value = "\\" + character; | ||
value = '\\' + character | ||
} else { | ||
value = "\\" + codePoint.toString(16).toUpperCase() + " "; | ||
value = '\\' + codePoint.toString(16).toUpperCase() + ' ' | ||
} | ||
} else if (/[\t\n\f\r\x0B]/.test(character)) { | ||
value = "\\" + codePoint.toString(16).toUpperCase() + " "; | ||
value = '\\' + codePoint.toString(16).toUpperCase() + ' ' | ||
} else if ( | ||
character == "\\" || | ||
character == '\\' || | ||
(!isIdentifier && | ||
((character == '"' && quote == character) || | ||
(character == "'" && quote == character))) || | ||
(character == '\'' && quote == character))) || | ||
(isIdentifier && regexSingleEscape.test(character)) | ||
) { | ||
value = "\\" + character; | ||
value = '\\' + character | ||
} else { | ||
value = character; | ||
value = character | ||
} | ||
} | ||
output += value; | ||
output += value | ||
} | ||
if (isIdentifier) { | ||
if (/^-[-\d]/.test(output)) { | ||
output = "\\-" + output.slice(1); | ||
output = '\\-' + output.slice(1) | ||
} else if (/\d/.test(firstChar)) { | ||
output = "\\3" + firstChar + " " + output.slice(1); | ||
output = '\\3' + firstChar + ' ' + output.slice(1) | ||
} | ||
} | ||
// Remove spaces after `\HEX` escapes that are not followed by a hex digit, | ||
@@ -450,12 +428,11 @@ // since they’re redundant. Note that this is only possible if the escape | ||
// It’s not safe to remove the space, so don’t. | ||
return $0; | ||
return $0 | ||
} | ||
// Strip the space. | ||
return ($1 || "") + $2; | ||
}); | ||
return ($1 || '') + $2 | ||
}) | ||
if (!isIdentifier && options.wrap) { | ||
return quote + output + quote; | ||
return quote + output + quote | ||
} | ||
return output; | ||
return output | ||
} |
{ | ||
"name": "@medv/finder", | ||
"version": "3.0.0", | ||
"description": "CSS Selector Generator", | ||
"type": "module", | ||
"version": "2.1.0", | ||
"description": "CSS Selector Generator", | ||
"repository": "antonmedv/finder", | ||
"homepage": "https://github.com/antonmedv/finder", | ||
"author": "Anton Medvedev <anton@medv.io>", | ||
"license": "MIT", | ||
"keywords": [ | ||
"css", | ||
"selector", | ||
"generator" | ||
], | ||
"main": "finder.js", | ||
@@ -22,33 +13,21 @@ "types": "finder.d.ts", | ||
"scripts": { | ||
"start": "tsc -w", | ||
"test": "tsc && ava", | ||
"prepare": "tsc", | ||
"release": "release-it --access public", | ||
"size": "minify finder.js --sourceType module | gzip-size" | ||
"build": "tsc", | ||
"test": "tsc && uvu", | ||
"release": "release-it --access public" | ||
}, | ||
"devDependencies": { | ||
"ava": "^3.15.0", | ||
"babel-minify": "*", | ||
"browser-env": "^3.3.0", | ||
"esm": "^3.2.25", | ||
"gzip-size-cli": "*", | ||
"release-it": "^13.6.1", | ||
"ts-node": "^10.2.1", | ||
"typescript": "3.9.3" | ||
"jsdom": "^21.1.0", | ||
"release-it": "^15.7.0", | ||
"typescript": "4.9.5", | ||
"uvu": "^0.5.6" | ||
}, | ||
"ava": { | ||
"require": [ | ||
"./test/helpers/setup-browser-env.js" | ||
], | ||
"extensions": { | ||
"ts": "module", | ||
"js": true | ||
}, | ||
"nonSemVerExperiments": { | ||
"configurableModuleFormat": true | ||
}, | ||
"nodeArguments": [ | ||
"--loader=ts-node/esm" | ||
] | ||
} | ||
"author": "Anton Medvedev <anton@medv.io>", | ||
"license": "MIT", | ||
"homepage": "https://github.com/antonmedv/finder", | ||
"repository": "antonmedv/finder", | ||
"keywords": [ | ||
"css", | ||
"selector", | ||
"generator" | ||
] | ||
} |
@@ -5,14 +5,14 @@ ![finder](https://medv.io/assets/finder.png) | ||
[![npm](https://img.shields.io/npm/v/@medv/finder?color=grightgreen)](https://www.npmjs.com/package/@medv/finder) | ||
[![Build status](https://img.shields.io/travis/antonmedv/finder)](https://travis-ci.org/antonmedv/finder) | ||
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@medv/finder?label=size)](https://bundlephobia.com/result?p=@medv/finder) | ||
[![Version](https://img.shields.io/npm/v/@medv/finder?color=grightgreen)](https://www.npmjs.com/package/@medv/finder) | ||
[![Test](https://github.com/antonmedv/finder/actions/workflows/test.yml/badge.svg)](https://github.com/antonmedv/finder/actions/workflows/test.yml) | ||
[![Size](https://img.shields.io/bundlephobia/minzip/@medv/finder?label=size)](https://bundlephobia.com/result?p=@medv/finder) | ||
> CSS Selector Generator | ||
**The CSS Selector Generator** | ||
## Features | ||
* Generates **shortest** selectors | ||
* Generates the **shortest** selector | ||
* **Unique** selectors per page | ||
* Stable and **robust** selectors | ||
* **2.1 kB** gzip and minify size | ||
* **2kB** minified + gzipped | ||
@@ -25,10 +25,2 @@ ## Install | ||
Finder can be used via modules: | ||
```html | ||
<script type="module"> | ||
import {finder} from 'https://medv.io/finder/finder.js' | ||
</script> | ||
``` | ||
## Usage | ||
@@ -47,3 +39,3 @@ | ||
Example of generated selector: | ||
An example of a generated selector: | ||
@@ -56,11 +48,10 @@ ```css | ||
`finder` takes configuration object as second parameters. Here is example of all params with default values: | ||
```js | ||
const selector = finder(event.target, { | ||
root: document.body, | ||
className: (name) => true, | ||
tagName: (name) => true, | ||
attr: (name, value) => false, | ||
seedMinLength: 1, | ||
root: document.body, // Root of search, defaults to document.body. | ||
idName: (name) => true, // Check if this ID can be used. | ||
className: (name) => true, // Check if this class name can be used. | ||
tagName: (name) => true, // Check if tag name can be used. | ||
attr: (name, value) => false, // Check if attr name can be used. | ||
seedMinLength: 1, | ||
optimizedMinLength: 2, | ||
@@ -72,66 +63,28 @@ threshold: 1000, | ||
#### `root: Element` | ||
### seedMinLength | ||
Root of search, defaults to `document.body`. | ||
#### `idName: (name: string) => boolean` | ||
Check if this ID can be used. For example you can restrict using framework specific IDs: | ||
```js | ||
const selector = finder(event.target, { | ||
idName: name => !name.startsWith('ember') | ||
}) | ||
``` | ||
#### `className: (name: string) => boolean` | ||
Check if this class name can be used. For example you can restrict using _is-*_ class names: | ||
```js | ||
const selector = finder(event.target, { | ||
className: name => !name.startsWith('is-') | ||
}) | ||
``` | ||
#### `tagName: (name: string) => boolean` | ||
Check if tag name can be used, same as `className`. | ||
#### `attr: (name: string, value: string) => boolean` | ||
Check if attr name can be used. | ||
#### `seedMinLength: number` | ||
Minimum length of levels in fining selector. Starts from `1`. | ||
For more robust selectors give this param value around 4-5 depending on depth of you DOM tree. | ||
If `finder` hits `root` this param is ignored. | ||
For more robust selectors give this param value around 4-5 depending on depth of | ||
you DOM tree. If `finder` hits `root` this param is ignored. | ||
#### `optimizedMinLength: number` | ||
### optimizedMinLength | ||
Minimum length for optimising selector. Starts from `2`. | ||
For example selector `body > div > div > p` can be optimized to `body p`. | ||
For example selector `body > div > div > p` can be optimised to `body p`. | ||
#### `threshold: number` | ||
### threshold | ||
Max number of selectors to check before falling into `nth-child` usage. | ||
Checking for uniqueness of selector is very costs operation, if you have DOM tree depth of 5, with 5 classes on each level, | ||
that gives you more than 3k selectors to check. | ||
`finder` uses two step approach so it's reaching this threshold in some cases twice. | ||
Default `1000` is good enough in most cases. | ||
Checking for uniqueness of selector is very costs operation, if you have DOM | ||
tree depth of 5, with 5 classes on each level, that gives you more than 3k | ||
selectors to check. Finder uses two-step approach,Ï so it's reaching this | ||
threshold in some cases twice. Default `1000` is good enough in most cases. | ||
#### `maxNumberOfTries: number` | ||
### maxNumberOfTries | ||
Max number of tries when we do the optimization. It is a trade-off between optimization and efficiency. | ||
Default `10_000` is good enough in most cases. | ||
Max number of tries when we do the optimization. It is a trade-off between | ||
optimization and efficiency. Default `10_000` is good enough in most cases. | ||
### Google Chrome Extension | ||
![Chrome Extension](https://user-images.githubusercontent.com/141232/36737287-4a999d84-1c0d-11e8-8a14-43bcf9baf7ca.png) | ||
Generate the unique selectors in your browser by using [Chrome Extension](https://chrome.google.com/webstore/detail/get-unique-css-selector/lkfaghhbdebclkklgjhhonadomejckai) | ||
## License | ||
[MIT](LICENSE) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
4
764
26429
86