fix(tracker): put vendor lib inside src for the cjs compilation
This commit is contained in:
parent
0920f17009
commit
57ff3574b5
9 changed files with 999 additions and 3 deletions
2
tracker/tracker/package-lock.json
generated
2
tracker/tracker/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { finder } from '@medv/finder';
|
||||
import { finder } from '../vendors/finder/finder';
|
||||
import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils';
|
||||
import App from '../app';
|
||||
import { MouseMove, MouseClick } from '../../messages';
|
||||
|
|
|
|||
21
tracker/tracker/src/main/vendors/finder/LICENSE
vendored
Normal file
21
tracker/tracker/src/main/vendors/finder/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018–2020 Anton Medvedev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
132
tracker/tracker/src/main/vendors/finder/README.md
vendored
Normal file
132
tracker/tracker/src/main/vendors/finder/README.md
vendored
Normal file
|
|
@ -0,0 +1,132 @@
|
|||

|
||||
|
||||
# finder
|
||||
|
||||
[](https://www.npmjs.com/package/@medv/finder)
|
||||
[](https://travis-ci.org/antonmedv/finder)
|
||||
[](https://bundlephobia.com/result?p=@medv/finder)
|
||||
|
||||
> CSS Selector Generator
|
||||
|
||||
## Features
|
||||
|
||||
* Generates **shortest** selectors
|
||||
* **Unique** selectors per page
|
||||
* Stable and **robust** selectors
|
||||
* **2.1 kB** gzip and minify size
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @medv/finder
|
||||
```
|
||||
|
||||
Finder can be used via modules:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import {finder} from 'https://medv.io/finder/finder.js'
|
||||
</script>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import {finder} from '@medv/finder'
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
const selector = finder(event.target)
|
||||
console.log(selector)
|
||||
})
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Example of generated selector:
|
||||
|
||||
```css
|
||||
.blog > article:nth-child(3) .add-comment
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`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,
|
||||
optimizedMinLength: 2,
|
||||
threshold: 1000,
|
||||
maxNumberOfTries: 10_000,
|
||||
})
|
||||
```
|
||||
|
||||
#### `root: Element`
|
||||
|
||||
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.
|
||||
|
||||
#### `optimizedMinLength: number`
|
||||
|
||||
Minimum length for optimising selector. Starts from `2`.
|
||||
For example selector `body > div > div > p` can be optimized to `body p`.
|
||||
|
||||
#### `threshold: number`
|
||||
|
||||
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.
|
||||
|
||||
#### `maxNumberOfTries: number`
|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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)
|
||||
12
tracker/tracker/src/main/vendors/finder/finder.d.ts
vendored
Normal file
12
tracker/tracker/src/main/vendors/finder/finder.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export declare 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;
|
||||
};
|
||||
export declare function finder(input: Element, options?: Partial<Options>): string;
|
||||
339
tracker/tracker/src/main/vendors/finder/finder.js
vendored
Normal file
339
tracker/tracker/src/main/vendors/finder/finder.js
vendored
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
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;
|
||||
export function finder(input, options) {
|
||||
if (input.nodeType !== Node.ELEMENT_NODE) {
|
||||
throw new Error(`Can't generate CSS selector for non-element node type.`);
|
||||
}
|
||||
if ("html" === input.tagName.toLowerCase()) {
|
||||
return "html";
|
||||
}
|
||||
const defaults = {
|
||||
root: document.body,
|
||||
idName: (name) => true,
|
||||
className: (name) => true,
|
||||
tagName: (name) => true,
|
||||
attr: (name, value) => false,
|
||||
seedMinLength: 1,
|
||||
optimizedMinLength: 2,
|
||||
threshold: 1000,
|
||||
maxNumberOfTries: 10000,
|
||||
};
|
||||
config = Object.assign(Object.assign({}, defaults), options);
|
||||
rootDocument = findRootDocument(config.root, defaults);
|
||||
let path = bottomUpSearch(input, Limit.All, () => bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)));
|
||||
if (path) {
|
||||
const optimized = sort(optimize(path, input));
|
||||
if (optimized.length > 0) {
|
||||
path = optimized[0];
|
||||
}
|
||||
return selector(path);
|
||||
}
|
||||
else {
|
||||
throw new Error(`Selector was not found.`);
|
||||
}
|
||||
}
|
||||
function findRootDocument(rootNode, defaults) {
|
||||
if (rootNode.nodeType === Node.DOCUMENT_NODE) {
|
||||
return rootNode;
|
||||
}
|
||||
if (rootNode === defaults.root) {
|
||||
return rootNode.ownerDocument;
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
function bottomUpSearch(input, limit, fallback) {
|
||||
let path = null;
|
||||
let stack = [];
|
||||
let current = input;
|
||||
let i = 0;
|
||||
while (current && current !== config.root.parentElement) {
|
||||
let level = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()];
|
||||
const nth = index(current);
|
||||
if (limit === 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);
|
||||
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);
|
||||
if (nth && dispensableNth(node)) {
|
||||
level = [nthChild(node, nth)];
|
||||
}
|
||||
}
|
||||
for (let node of level) {
|
||||
node.level = i;
|
||||
}
|
||||
stack.push(level);
|
||||
if (stack.length >= config.seedMinLength) {
|
||||
path = findUniquePath(stack, fallback);
|
||||
if (path) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
current = current.parentElement;
|
||||
i++;
|
||||
}
|
||||
if (!path) {
|
||||
path = findUniquePath(stack, fallback);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
function findUniquePath(stack, fallback) {
|
||||
const paths = sort(combinations(stack));
|
||||
if (paths.length > config.threshold) {
|
||||
return fallback ? fallback() : null;
|
||||
}
|
||||
for (let candidate of paths) {
|
||||
if (unique(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function selector(path) {
|
||||
let node = path[0];
|
||||
let query = node.name;
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const level = path[i].level || 0;
|
||||
if (node.level === level - 1) {
|
||||
query = `${path[i].name} > ${query}`;
|
||||
}
|
||||
else {
|
||||
query = `${path[i].name} ${query}`;
|
||||
}
|
||||
node = path[i];
|
||||
}
|
||||
return query;
|
||||
}
|
||||
function penalty(path) {
|
||||
return path.map(node => node.penalty).reduce((acc, i) => acc + i, 0);
|
||||
}
|
||||
function unique(path) {
|
||||
switch (rootDocument.querySelectorAll(selector(path)).length) {
|
||||
case 0:
|
||||
throw new Error(`Can't select any node with this selector: ${selector(path)}`);
|
||||
case 1:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function id(input) {
|
||||
const elementId = input.getAttribute("id");
|
||||
if (elementId && config.idName(elementId)) {
|
||||
return {
|
||||
name: "#" + cssesc(elementId, { isIdentifier: true }),
|
||||
penalty: 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function attr(input) {
|
||||
const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value));
|
||||
return attrs.map((attr) => ({
|
||||
name: "[" + cssesc(attr.name, { isIdentifier: true }) + "=\"" + cssesc(attr.value) + "\"]",
|
||||
penalty: 0.5
|
||||
}));
|
||||
}
|
||||
function classNames(input) {
|
||||
const names = Array.from(input.classList)
|
||||
.filter(config.className);
|
||||
return names.map((name) => ({
|
||||
name: "." + cssesc(name, { isIdentifier: true }),
|
||||
penalty: 1
|
||||
}));
|
||||
}
|
||||
function tagName(input) {
|
||||
const name = input.tagName.toLowerCase();
|
||||
if (config.tagName(name)) {
|
||||
return {
|
||||
name,
|
||||
penalty: 2
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function any() {
|
||||
return {
|
||||
name: "*",
|
||||
penalty: 3
|
||||
};
|
||||
}
|
||||
function index(input) {
|
||||
const parent = input.parentNode;
|
||||
if (!parent) {
|
||||
return null;
|
||||
}
|
||||
let child = parent.firstChild;
|
||||
if (!child) {
|
||||
return null;
|
||||
}
|
||||
let i = 0;
|
||||
while (child) {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
i++;
|
||||
}
|
||||
if (child === input) {
|
||||
break;
|
||||
}
|
||||
child = child.nextSibling;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
function nthChild(node, i) {
|
||||
return {
|
||||
name: node.name + `:nth-child(${i})`,
|
||||
penalty: node.penalty + 1
|
||||
};
|
||||
}
|
||||
function dispensableNth(node) {
|
||||
return node.name !== "html" && !node.name.startsWith("#");
|
||||
}
|
||||
function maybe(...level) {
|
||||
const list = level.filter(notEmpty);
|
||||
if (list.length > 0) {
|
||||
return list;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function notEmpty(value) {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
function* combinations(stack, path = []) {
|
||||
if (stack.length > 0) {
|
||||
for (let node of stack[0]) {
|
||||
yield* combinations(stack.slice(1, stack.length), path.concat(node));
|
||||
}
|
||||
}
|
||||
else {
|
||||
yield path;
|
||||
}
|
||||
}
|
||||
function sort(paths) {
|
||||
return Array.from(paths).sort((a, b) => penalty(a) - penalty(b));
|
||||
}
|
||||
function* optimize(path, input, scope = {
|
||||
counter: 0,
|
||||
visited: new Map()
|
||||
}) {
|
||||
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!
|
||||
}
|
||||
scope.counter += 1;
|
||||
const newPath = [...path];
|
||||
newPath.splice(i, 1);
|
||||
const newPathKey = selector(newPath);
|
||||
if (scope.visited.has(newPathKey)) {
|
||||
return;
|
||||
}
|
||||
if (unique(newPath) && same(newPath, input)) {
|
||||
yield newPath;
|
||||
scope.visited.set(newPathKey, true);
|
||||
yield* optimize(newPath, input, scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function same(path, input) {
|
||||
return rootDocument.querySelector(selector(path)) === input;
|
||||
}
|
||||
const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/;
|
||||
const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/;
|
||||
const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g;
|
||||
const defaultOptions = {
|
||||
"escapeEverything": false,
|
||||
"isIdentifier": false,
|
||||
"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 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 = void 0;
|
||||
// If it’s not a printable ASCII character…
|
||||
if (codePoint < 0x20 || codePoint > 0x7E) {
|
||||
if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) {
|
||||
// It’s a high surrogate, and there is a next character.
|
||||
const extra = string.charCodeAt(counter++);
|
||||
if ((extra & 0xFC00) == 0xDC00) {
|
||||
// next character is low surrogate
|
||||
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--;
|
||||
}
|
||||
}
|
||||
value = "\\" + codePoint.toString(16).toUpperCase() + " ";
|
||||
}
|
||||
else {
|
||||
if (options.escapeEverything) {
|
||||
if (regexAnySingleEscape.test(character)) {
|
||||
value = "\\" + character;
|
||||
}
|
||||
else {
|
||||
value = "\\" + codePoint.toString(16).toUpperCase() + " ";
|
||||
}
|
||||
}
|
||||
else if (/[\t\n\f\r\x0B]/.test(character)) {
|
||||
value = "\\" + codePoint.toString(16).toUpperCase() + " ";
|
||||
}
|
||||
else if (character == "\\" || !isIdentifier && (character == "\"" && quote == character || character == "'" && quote == character) || isIdentifier && regexSingleEscape.test(character)) {
|
||||
value = "\\" + character;
|
||||
}
|
||||
else {
|
||||
value = character;
|
||||
}
|
||||
}
|
||||
output += value;
|
||||
}
|
||||
if (isIdentifier) {
|
||||
if (/^-[-\d]/.test(output)) {
|
||||
output = "\\-" + output.slice(1);
|
||||
}
|
||||
else if (/\d/.test(firstChar)) {
|
||||
output = "\\3" + firstChar + " " + output.slice(1);
|
||||
}
|
||||
}
|
||||
// Remove spaces after `\HEX` escapes that are not followed by a hex digit,
|
||||
// since they’re redundant. Note that this is only possible if the escape
|
||||
// sequence isn’t preceded by an odd number of backslashes.
|
||||
output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
|
||||
if ($1 && $1.length % 2) {
|
||||
// It’s not safe to remove the space, so don’t.
|
||||
return $0;
|
||||
}
|
||||
// Strip the space.
|
||||
return ($1 || "") + $2;
|
||||
});
|
||||
if (!isIdentifier && options.wrap) {
|
||||
return quote + output + quote;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
414
tracker/tracker/src/main/vendors/finder/finder.ts
vendored
Normal file
414
tracker/tracker/src/main/vendors/finder/finder.ts
vendored
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
type Node = {
|
||||
name: string
|
||||
penalty: number
|
||||
level?: number
|
||||
}
|
||||
|
||||
type Path = Node[]
|
||||
|
||||
enum Limit {
|
||||
All,
|
||||
Two,
|
||||
One,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
let config: Options
|
||||
|
||||
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.`)
|
||||
}
|
||||
|
||||
if ("html" === input.tagName.toLowerCase()) {
|
||||
return "html"
|
||||
}
|
||||
|
||||
const defaults: Options = {
|
||||
root: document.body,
|
||||
idName: (name: string) => true,
|
||||
className: (name: string) => true,
|
||||
tagName: (name: string) => true,
|
||||
attr: (name: string, value: string) => false,
|
||||
seedMinLength: 1,
|
||||
optimizedMinLength: 2,
|
||||
threshold: 1000,
|
||||
maxNumberOfTries: 10000,
|
||||
}
|
||||
|
||||
config = {...defaults, ...options}
|
||||
|
||||
rootDocument = findRootDocument(config.root, defaults)
|
||||
|
||||
let path =
|
||||
bottomUpSearch(input, Limit.All, () =>
|
||||
bottomUpSearch(input, Limit.Two, () =>
|
||||
bottomUpSearch(input, Limit.One)))
|
||||
|
||||
if (path) {
|
||||
const optimized = sort(optimize(path, input))
|
||||
|
||||
if (optimized.length > 0) {
|
||||
path = optimized[0]
|
||||
}
|
||||
|
||||
return selector(path)
|
||||
} else {
|
||||
throw new Error(`Selector was not found.`)
|
||||
}
|
||||
}
|
||||
|
||||
function findRootDocument(rootNode: Element | Document, defaults: Options) {
|
||||
if (rootNode.nodeType === Node.DOCUMENT_NODE) {
|
||||
return rootNode
|
||||
}
|
||||
if (rootNode === defaults.root) {
|
||||
return rootNode.ownerDocument as Document
|
||||
}
|
||||
return rootNode
|
||||
}
|
||||
|
||||
function bottomUpSearch(input: Element, limit: Limit, 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)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()]
|
||||
|
||||
const nth = index(current)
|
||||
|
||||
if (limit === 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)
|
||||
|
||||
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)
|
||||
|
||||
if (nth && dispensableNth(node)) {
|
||||
level = [nthChild(node, nth)]
|
||||
}
|
||||
}
|
||||
|
||||
for (let node of level) {
|
||||
node.level = i
|
||||
}
|
||||
|
||||
stack.push(level)
|
||||
|
||||
if (stack.length >= config.seedMinLength) {
|
||||
path = findUniquePath(stack, fallback)
|
||||
if (path) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parentElement
|
||||
i++
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
path = findUniquePath(stack, fallback)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
function findUniquePath(stack: Node[][], fallback?: () => Path | null): Path | null {
|
||||
const paths = sort(combinations(stack))
|
||||
|
||||
if (paths.length > config.threshold) {
|
||||
return fallback ? fallback() : null
|
||||
}
|
||||
|
||||
for (let candidate of paths) {
|
||||
if (unique(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function selector(path: Path): string {
|
||||
let node = path[0]
|
||||
let query = node.name
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const level = path[i].level || 0
|
||||
|
||||
if (node.level === level - 1) {
|
||||
query = `${path[i].name} > ${query}`
|
||||
} else {
|
||||
query = `${path[i].name} ${query}`
|
||||
}
|
||||
|
||||
node = path[i]
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
function penalty(path: Path): number {
|
||||
return path.map(node => node.penalty).reduce((acc, i) => acc + i, 0)
|
||||
}
|
||||
|
||||
function unique(path: Path) {
|
||||
switch (rootDocument.querySelectorAll(selector(path)).length) {
|
||||
case 0:
|
||||
throw new Error(`Can't select any node with this selector: ${selector(path)}`)
|
||||
case 1:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function id(input: Element): Node | null {
|
||||
const elementId = input.getAttribute("id")
|
||||
if (elementId && config.idName(elementId)) {
|
||||
return {
|
||||
name: "#" + cssesc(elementId, {isIdentifier: true}),
|
||||
penalty: 0,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function attr(input: Element): Node[] {
|
||||
const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value))
|
||||
|
||||
return attrs.map((attr): Node => ({
|
||||
name: "[" + cssesc(attr.name, {isIdentifier: true}) + "=\"" + cssesc(attr.value) + "\"]",
|
||||
penalty: 0.5
|
||||
}))
|
||||
}
|
||||
|
||||
function classNames(input: Element): Node[] {
|
||||
const names = Array.from(input.classList)
|
||||
.filter(config.className)
|
||||
|
||||
return names.map((name): Node => ({
|
||||
name: "." + cssesc(name, {isIdentifier: true}),
|
||||
penalty: 1
|
||||
}))
|
||||
}
|
||||
|
||||
function tagName(input: Element): Node | null {
|
||||
const name = input.tagName.toLowerCase()
|
||||
if (config.tagName(name)) {
|
||||
return {
|
||||
name,
|
||||
penalty: 2
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function any(): Node {
|
||||
return {
|
||||
name: "*",
|
||||
penalty: 3
|
||||
}
|
||||
}
|
||||
|
||||
function index(input: Element): number | null {
|
||||
const parent = input.parentNode
|
||||
if (!parent) {
|
||||
return null
|
||||
}
|
||||
|
||||
let child = parent.firstChild
|
||||
if (!child) {
|
||||
return null
|
||||
}
|
||||
|
||||
let i = 0
|
||||
while (child) {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
i++
|
||||
}
|
||||
|
||||
if (child === input) {
|
||||
break
|
||||
}
|
||||
|
||||
child = child.nextSibling
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
function nthChild(node: Node, i: number): Node {
|
||||
return {
|
||||
name: node.name + `:nth-child(${i})`,
|
||||
penalty: node.penalty + 1
|
||||
}
|
||||
}
|
||||
|
||||
function dispensableNth(node: Node) {
|
||||
return node.name !== "html" && !node.name.startsWith("#")
|
||||
}
|
||||
|
||||
function maybe(...level: (Node | null)[]): Node[] | null {
|
||||
const list = level.filter(notEmpty)
|
||||
if (list.length > 0) {
|
||||
return list
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function notEmpty<T>(value: T | null | undefined): value is T {
|
||||
return value !== null && value !== undefined
|
||||
}
|
||||
|
||||
function* combinations(stack: Node[][], path: Node[] = []): Generator<Node[]> {
|
||||
if (stack.length > 0) {
|
||||
for (let node of stack[0]) {
|
||||
yield* combinations(stack.slice(1, stack.length), path.concat(node))
|
||||
}
|
||||
} else {
|
||||
yield path
|
||||
}
|
||||
}
|
||||
|
||||
function sort(paths: Iterable<Path>): Path[] {
|
||||
return Array.from(paths).sort((a, b) => penalty(a) - penalty(b))
|
||||
}
|
||||
|
||||
type Scope = {
|
||||
counter: number
|
||||
visited: Map<string, boolean>
|
||||
}
|
||||
|
||||
function* optimize(path: Path, input: Element, scope: Scope = {
|
||||
counter: 0,
|
||||
visited: new Map<string, boolean>()
|
||||
}): Generator<Node[]> {
|
||||
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!
|
||||
}
|
||||
scope.counter += 1
|
||||
const newPath = [...path]
|
||||
newPath.splice(i, 1)
|
||||
const newPathKey = selector(newPath)
|
||||
if (scope.visited.has(newPathKey)) {
|
||||
return
|
||||
}
|
||||
if (unique(newPath) && same(newPath, input)) {
|
||||
yield newPath
|
||||
scope.visited.set(newPathKey, true)
|
||||
yield* optimize(newPath, input, scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function same(path: Path, input: Element) {
|
||||
return rootDocument.querySelector(selector(path)) === input
|
||||
}
|
||||
|
||||
const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/
|
||||
const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/
|
||||
const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g
|
||||
|
||||
const defaultOptions = {
|
||||
"escapeEverything": false,
|
||||
"isIdentifier": false,
|
||||
"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 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
|
||||
// If it’s not a printable ASCII character…
|
||||
if (codePoint < 0x20 || codePoint > 0x7E) {
|
||||
if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) {
|
||||
// It’s a high surrogate, and there is a next character.
|
||||
const extra = string.charCodeAt(counter++)
|
||||
if ((extra & 0xFC00) == 0xDC00) {
|
||||
// next character is low surrogate
|
||||
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--
|
||||
}
|
||||
}
|
||||
value = "\\" + codePoint.toString(16).toUpperCase() + " "
|
||||
} else {
|
||||
if (options.escapeEverything) {
|
||||
if (regexAnySingleEscape.test(character)) {
|
||||
value = "\\" + character
|
||||
} else {
|
||||
value = "\\" + codePoint.toString(16).toUpperCase() + " "
|
||||
}
|
||||
} else if (/[\t\n\f\r\x0B]/.test(character)) {
|
||||
value = "\\" + codePoint.toString(16).toUpperCase() + " "
|
||||
} else if (character == "\\" || !isIdentifier && (character == "\"" && quote == character || character == "'" && quote == character) || isIdentifier && regexSingleEscape.test(character)) {
|
||||
value = "\\" + character
|
||||
} else {
|
||||
value = character
|
||||
}
|
||||
}
|
||||
output += value
|
||||
}
|
||||
|
||||
if (isIdentifier) {
|
||||
if (/^-[-\d]/.test(output)) {
|
||||
output = "\\-" + output.slice(1)
|
||||
} else if (/\d/.test(firstChar)) {
|
||||
output = "\\3" + firstChar + " " + output.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove spaces after `\HEX` escapes that are not followed by a hex digit,
|
||||
// since they’re redundant. Note that this is only possible if the escape
|
||||
// sequence isn’t preceded by an odd number of backslashes.
|
||||
output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
|
||||
if ($1 && $1.length % 2) {
|
||||
// It’s not safe to remove the space, so don’t.
|
||||
return $0
|
||||
}
|
||||
// Strip the space.
|
||||
return ($1 || "") + $2
|
||||
})
|
||||
|
||||
if (!isIdentifier && options.wrap) {
|
||||
return quote + output + quote
|
||||
}
|
||||
return output
|
||||
}
|
||||
78
tracker/tracker/src/main/vendors/finder/package.json
vendored
Normal file
78
tracker/tracker/src/main/vendors/finder/package.json
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"_from": "@medv/finder",
|
||||
"_id": "@medv/finder@2.0.0",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-gV4jOsGpiWNDGd8Dw7tod1Fc9Gc7StaOT4oZ/6srHRWtsHU+HYWzmkYsa3Qy/z0e9tY1WpJ9wWdBFGskfbzoug==",
|
||||
"_location": "/@medv/finder",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "@medv/finder",
|
||||
"name": "@medv/finder",
|
||||
"escapedName": "@medv%2ffinder",
|
||||
"scope": "@medv",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/@medv/finder/-/finder-2.0.0.tgz",
|
||||
"_shasum": "699b7141393aa815f120b38f54f92ad212225902",
|
||||
"_spec": "@medv/finder",
|
||||
"_where": "/Users/shikhu/work/openreplay/tracker/tracker",
|
||||
"author": {
|
||||
"name": "Anton Medvedev",
|
||||
"email": "anton@medv.io"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
"esm",
|
||||
"./test/helpers/setup-browser-env.js"
|
||||
]
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/antonmedv/finder/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"description": "CSS Selector Generator",
|
||||
"devDependencies": {
|
||||
"ava": "^3.8.2",
|
||||
"babel-minify": "*",
|
||||
"browser-env": "^3.3.0",
|
||||
"esm": "^3.2.25",
|
||||
"gzip-size-cli": "*",
|
||||
"release-it": "^13.6.1",
|
||||
"typescript": "3.9.3"
|
||||
},
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.js"
|
||||
],
|
||||
"homepage": "https://github.com/antonmedv/finder",
|
||||
"keywords": [
|
||||
"css",
|
||||
"selector",
|
||||
"generator"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "finder.js",
|
||||
"name": "@medv/finder",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/antonmedv/finder.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "tsc",
|
||||
"release": "release-it --access public",
|
||||
"size": "minify finder.js --sourceType module | gzip-size",
|
||||
"start": "tsc -w",
|
||||
"test": "tsc && ava"
|
||||
},
|
||||
"types": "finder.d.ts",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue