@openreplay/tracker 5.0.2-beta → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/app/guards.d.ts +21 -0
- package/cjs/app/guards.js +37 -0
- package/cjs/app/index.d.ts +118 -0
- package/cjs/app/index.js +438 -0
- package/cjs/app/logger.d.ts +26 -0
- package/cjs/app/logger.js +45 -0
- package/cjs/app/messages.gen.d.ts +63 -0
- package/cjs/app/messages.gen.js +551 -0
- package/cjs/app/nodes.d.ts +18 -0
- package/cjs/app/nodes.js +82 -0
- package/cjs/app/observer/iframe_observer.d.ts +4 -0
- package/cjs/app/observer/iframe_observer.js +23 -0
- package/cjs/app/observer/iframe_offsets.d.ts +8 -0
- package/cjs/app/observer/iframe_offsets.js +59 -0
- package/cjs/app/observer/observer.d.ts +23 -0
- package/cjs/app/observer/observer.js +340 -0
- package/cjs/app/observer/shadow_root_observer.d.ts +4 -0
- package/cjs/app/observer/shadow_root_observer.js +21 -0
- package/cjs/app/observer/top_observer.d.ts +24 -0
- package/cjs/app/observer/top_observer.js +113 -0
- package/cjs/app/sanitizer.d.ts +24 -0
- package/cjs/app/sanitizer.js +76 -0
- package/cjs/app/session.d.ts +38 -0
- package/cjs/app/session.js +114 -0
- package/cjs/app/ticker.d.ts +12 -0
- package/cjs/app/ticker.js +42 -0
- package/cjs/common/interaction.d.ts +24 -0
- package/cjs/common/interaction.js +2 -0
- package/cjs/common/messages.gen.d.ts +427 -0
- package/cjs/common/messages.gen.js +4 -0
- package/cjs/index.d.ts +47 -0
- package/cjs/index.js +254 -0
- package/cjs/modules/connection.d.ts +2 -0
- package/cjs/modules/connection.js +15 -0
- package/cjs/modules/console.d.ts +6 -0
- package/cjs/modules/console.js +119 -0
- package/cjs/modules/constructedStyleSheets.d.ts +4 -0
- package/cjs/modules/constructedStyleSheets.js +131 -0
- package/cjs/modules/cssrules.d.ts +2 -0
- package/cjs/modules/cssrules.js +99 -0
- package/cjs/modules/exception.d.ts +16 -0
- package/cjs/modules/exception.js +77 -0
- package/cjs/modules/focus.d.ts +2 -0
- package/cjs/modules/focus.js +45 -0
- package/cjs/modules/fonts.d.ts +2 -0
- package/cjs/modules/fonts.js +57 -0
- package/cjs/modules/img.d.ts +2 -0
- package/cjs/modules/img.js +110 -0
- package/cjs/modules/input.d.ts +16 -0
- package/cjs/modules/input.js +163 -0
- package/cjs/modules/mouse.d.ts +2 -0
- package/cjs/modules/mouse.js +148 -0
- package/cjs/modules/network.d.ts +28 -0
- package/cjs/modules/network.js +203 -0
- package/cjs/modules/performance.d.ts +7 -0
- package/cjs/modules/performance.js +53 -0
- package/cjs/modules/scroll.d.ts +2 -0
- package/cjs/modules/scroll.js +79 -0
- package/cjs/modules/timing.d.ts +7 -0
- package/cjs/modules/timing.js +160 -0
- package/cjs/modules/viewport.d.ts +2 -0
- package/cjs/modules/viewport.js +43 -0
- package/cjs/package.json +1 -0
- package/cjs/utils.d.ts +13 -0
- package/cjs/utils.js +71 -0
- package/cjs/vendors/finder/finder.d.ts +12 -0
- package/cjs/vendors/finder/finder.js +352 -0
- package/lib/app/guards.d.ts +21 -0
- package/lib/app/guards.js +26 -0
- package/lib/app/index.d.ts +118 -0
- package/lib/app/index.js +434 -0
- package/lib/app/logger.d.ts +26 -0
- package/lib/app/logger.js +41 -0
- package/lib/app/messages.gen.d.ts +63 -0
- package/lib/app/messages.gen.js +486 -0
- package/lib/app/nodes.d.ts +18 -0
- package/lib/app/nodes.js +79 -0
- package/lib/app/observer/iframe_observer.d.ts +4 -0
- package/lib/app/observer/iframe_observer.js +20 -0
- package/lib/app/observer/iframe_offsets.d.ts +8 -0
- package/lib/app/observer/iframe_offsets.js +56 -0
- package/lib/app/observer/observer.d.ts +23 -0
- package/lib/app/observer/observer.js +337 -0
- package/lib/app/observer/shadow_root_observer.d.ts +4 -0
- package/lib/app/observer/shadow_root_observer.js +18 -0
- package/lib/app/observer/top_observer.d.ts +24 -0
- package/lib/app/observer/top_observer.js +110 -0
- package/lib/app/sanitizer.d.ts +24 -0
- package/lib/app/sanitizer.js +72 -0
- package/lib/app/session.d.ts +38 -0
- package/lib/app/session.js +111 -0
- package/lib/app/ticker.d.ts +12 -0
- package/lib/app/ticker.js +39 -0
- package/lib/common/interaction.d.ts +24 -0
- package/lib/common/interaction.js +1 -0
- package/lib/common/messages.gen.d.ts +427 -0
- package/lib/common/messages.gen.js +3 -0
- package/lib/common/tsconfig.tsbuildinfo +1 -0
- package/lib/index.d.ts +47 -0
- package/lib/index.js +248 -0
- package/lib/modules/connection.d.ts +2 -0
- package/lib/modules/connection.js +12 -0
- package/lib/modules/console.d.ts +6 -0
- package/lib/modules/console.js +116 -0
- package/lib/modules/constructedStyleSheets.d.ts +4 -0
- package/lib/modules/constructedStyleSheets.js +126 -0
- package/lib/modules/cssrules.d.ts +2 -0
- package/lib/modules/cssrules.js +97 -0
- package/lib/modules/exception.d.ts +16 -0
- package/lib/modules/exception.js +71 -0
- package/lib/modules/focus.d.ts +2 -0
- package/lib/modules/focus.js +42 -0
- package/lib/modules/fonts.d.ts +2 -0
- package/lib/modules/fonts.js +54 -0
- package/lib/modules/img.d.ts +2 -0
- package/lib/modules/img.js +107 -0
- package/lib/modules/input.d.ts +16 -0
- package/lib/modules/input.js +158 -0
- package/lib/modules/mouse.d.ts +2 -0
- package/lib/modules/mouse.js +145 -0
- package/lib/modules/network.d.ts +28 -0
- package/lib/modules/network.js +200 -0
- package/lib/modules/performance.d.ts +7 -0
- package/lib/modules/performance.js +49 -0
- package/lib/modules/scroll.d.ts +2 -0
- package/lib/modules/scroll.js +76 -0
- package/lib/modules/timing.d.ts +7 -0
- package/lib/modules/timing.js +157 -0
- package/lib/modules/viewport.d.ts +2 -0
- package/lib/modules/viewport.js +40 -0
- package/lib/utils.d.ts +13 -0
- package/lib/utils.js +61 -0
- package/lib/vendors/finder/finder.d.ts +12 -0
- package/lib/vendors/finder/finder.js +348 -0
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { hasTag } from '../app/guards.js';
|
|
2
|
+
import { isURL, getTimeOrigin } from '../utils.js';
|
|
3
|
+
import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../app/messages.gen.js';
|
|
4
|
+
function getPaintBlocks(resources) {
|
|
5
|
+
const paintBlocks = [];
|
|
6
|
+
const elements = document.getElementsByTagName('*');
|
|
7
|
+
const styleURL = /url\(("[^"]*"|'[^']*'|[^)]*)\)/i;
|
|
8
|
+
for (let i = 0; i < elements.length; i++) {
|
|
9
|
+
const element = elements[i];
|
|
10
|
+
let src = '';
|
|
11
|
+
if (hasTag(element, 'img')) {
|
|
12
|
+
src = element.currentSrc || element.src;
|
|
13
|
+
}
|
|
14
|
+
if (!src) {
|
|
15
|
+
const backgroundImage = getComputedStyle(element).getPropertyValue('background-image');
|
|
16
|
+
if (backgroundImage) {
|
|
17
|
+
const matches = styleURL.exec(backgroundImage);
|
|
18
|
+
if (matches !== null) {
|
|
19
|
+
src = matches[1];
|
|
20
|
+
if (src.startsWith('"') || src.startsWith("'")) {
|
|
21
|
+
src = src.substr(1, src.length - 2);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!src)
|
|
27
|
+
continue;
|
|
28
|
+
const time = src.substr(0, 10) === 'data:image' ? 0 : resources[src];
|
|
29
|
+
if (time === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const rect = element.getBoundingClientRect();
|
|
32
|
+
const top = Math.max(rect.top, 0);
|
|
33
|
+
const left = Math.max(rect.left, 0);
|
|
34
|
+
const bottom = Math.min(rect.bottom, window.innerHeight ||
|
|
35
|
+
(document.documentElement && document.documentElement.clientHeight) ||
|
|
36
|
+
0);
|
|
37
|
+
const right = Math.min(rect.right, window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || 0);
|
|
38
|
+
if (bottom <= top || right <= left)
|
|
39
|
+
continue;
|
|
40
|
+
const area = (bottom - top) * (right - left);
|
|
41
|
+
paintBlocks.push({ time, area });
|
|
42
|
+
}
|
|
43
|
+
return paintBlocks;
|
|
44
|
+
}
|
|
45
|
+
function calculateSpeedIndex(firstContentfulPaint, paintBlocks) {
|
|
46
|
+
let a = (Math.max((document.documentElement && document.documentElement.clientWidth) || 0, window.innerWidth || 0) *
|
|
47
|
+
Math.max((document.documentElement && document.documentElement.clientHeight) || 0, window.innerHeight || 0)) /
|
|
48
|
+
10;
|
|
49
|
+
let s = a * firstContentfulPaint;
|
|
50
|
+
for (let i = 0; i < paintBlocks.length; i++) {
|
|
51
|
+
const { time, area } = paintBlocks[i];
|
|
52
|
+
a += area;
|
|
53
|
+
s += area * (time > firstContentfulPaint ? time : firstContentfulPaint);
|
|
54
|
+
}
|
|
55
|
+
return a === 0 ? 0 : s / a;
|
|
56
|
+
}
|
|
57
|
+
export default function (app, opts) {
|
|
58
|
+
const options = Object.assign({
|
|
59
|
+
captureResourceTimings: true,
|
|
60
|
+
capturePageLoadTimings: true,
|
|
61
|
+
capturePageRenderTimings: true,
|
|
62
|
+
}, opts);
|
|
63
|
+
if (!('PerformanceObserver' in window)) {
|
|
64
|
+
options.captureResourceTimings = false;
|
|
65
|
+
}
|
|
66
|
+
if (!options.captureResourceTimings) {
|
|
67
|
+
return;
|
|
68
|
+
} // Resources are necessary for all timings
|
|
69
|
+
let resources = {};
|
|
70
|
+
function resourceTiming(entry) {
|
|
71
|
+
if (entry.duration < 0 || !isURL(entry.name) || app.isServiceURL(entry.name))
|
|
72
|
+
return;
|
|
73
|
+
if (resources !== null) {
|
|
74
|
+
resources[entry.name] = entry.startTime + entry.duration;
|
|
75
|
+
}
|
|
76
|
+
app.send(ResourceTiming(entry.startTime + getTimeOrigin(), entry.duration, entry.responseStart && entry.startTime ? entry.responseStart - entry.startTime : 0, entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0, entry.encodedBodySize || 0, entry.decodedBodySize || 0, entry.name, entry.initiatorType));
|
|
77
|
+
}
|
|
78
|
+
const observer = new PerformanceObserver((list) => list.getEntries().forEach(resourceTiming));
|
|
79
|
+
let prevSessionID;
|
|
80
|
+
app.attachStartCallback(function ({ sessionID }) {
|
|
81
|
+
if (sessionID !== prevSessionID) {
|
|
82
|
+
// Send past page resources on a newly started session
|
|
83
|
+
performance.getEntriesByType('resource').forEach(resourceTiming);
|
|
84
|
+
prevSessionID = sessionID;
|
|
85
|
+
}
|
|
86
|
+
observer.observe({ entryTypes: ['resource'] });
|
|
87
|
+
});
|
|
88
|
+
app.attachStopCallback(function () {
|
|
89
|
+
observer.disconnect();
|
|
90
|
+
});
|
|
91
|
+
let firstPaint = 0, firstContentfulPaint = 0;
|
|
92
|
+
if (options.capturePageLoadTimings) {
|
|
93
|
+
let pageLoadTimingSent = false;
|
|
94
|
+
app.ticker.attach(() => {
|
|
95
|
+
if (pageLoadTimingSent) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (firstPaint === 0 || firstContentfulPaint === 0) {
|
|
99
|
+
performance.getEntriesByType('paint').forEach((entry) => {
|
|
100
|
+
const { name, startTime } = entry;
|
|
101
|
+
switch (name) {
|
|
102
|
+
case 'first-paint':
|
|
103
|
+
firstPaint = startTime;
|
|
104
|
+
break;
|
|
105
|
+
case 'first-contentful-paint':
|
|
106
|
+
firstContentfulPaint = startTime;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (performance.timing.loadEventEnd || performance.now() > 30000) {
|
|
112
|
+
pageLoadTimingSent = true;
|
|
113
|
+
const {
|
|
114
|
+
// should be ok to use here, (https://github.com/mdn/content/issues/4713)
|
|
115
|
+
// since it is compared with the values obtained on the page load (before any possible sleep state)
|
|
116
|
+
// deprecated though
|
|
117
|
+
navigationStart, requestStart, responseStart, responseEnd, domContentLoadedEventStart, domContentLoadedEventEnd, loadEventStart, loadEventEnd, } = performance.timing;
|
|
118
|
+
app.send(PageLoadTiming(requestStart - navigationStart || 0, responseStart - navigationStart || 0, responseEnd - navigationStart || 0, domContentLoadedEventStart - navigationStart || 0, domContentLoadedEventEnd - navigationStart || 0, loadEventStart - navigationStart || 0, loadEventEnd - navigationStart || 0, firstPaint, firstContentfulPaint));
|
|
119
|
+
}
|
|
120
|
+
}, 30);
|
|
121
|
+
}
|
|
122
|
+
if (options.capturePageRenderTimings) {
|
|
123
|
+
let visuallyComplete = 0, interactiveWindowStartTime = 0, interactiveWindowTickTime = 0, paintBlocks = null;
|
|
124
|
+
let pageRenderTimingSent = false;
|
|
125
|
+
app.ticker.attach(() => {
|
|
126
|
+
if (pageRenderTimingSent) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const time = performance.now();
|
|
130
|
+
if (resources !== null) {
|
|
131
|
+
visuallyComplete = Math.max.apply(null, Object.keys(resources).map((k) => resources[k]));
|
|
132
|
+
if (time - visuallyComplete > 1000) {
|
|
133
|
+
paintBlocks = getPaintBlocks(resources);
|
|
134
|
+
resources = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (interactiveWindowTickTime !== null) {
|
|
138
|
+
if (time - interactiveWindowTickTime > 50) {
|
|
139
|
+
interactiveWindowStartTime = time;
|
|
140
|
+
}
|
|
141
|
+
interactiveWindowTickTime = time - interactiveWindowStartTime > 5000 ? null : time;
|
|
142
|
+
}
|
|
143
|
+
if ((paintBlocks !== null && interactiveWindowTickTime === null) || time > 30000) {
|
|
144
|
+
pageRenderTimingSent = true;
|
|
145
|
+
resources = null;
|
|
146
|
+
const speedIndex = paintBlocks === null
|
|
147
|
+
? 0
|
|
148
|
+
: calculateSpeedIndex(firstContentfulPaint || firstPaint, paintBlocks);
|
|
149
|
+
const { domContentLoadedEventEnd, navigationStart } = performance.timing;
|
|
150
|
+
const timeToInteractive = interactiveWindowTickTime === null
|
|
151
|
+
? Math.max(interactiveWindowStartTime, firstContentfulPaint, domContentLoadedEventEnd - navigationStart || 0)
|
|
152
|
+
: 0;
|
|
153
|
+
app.send(PageRenderTiming(speedIndex, firstContentfulPaint > visuallyComplete ? firstContentfulPaint : visuallyComplete, timeToInteractive));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getTimeOrigin } from '../utils.js';
|
|
2
|
+
import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../app/messages.gen.js';
|
|
3
|
+
export default function (app) {
|
|
4
|
+
let url, width, height;
|
|
5
|
+
let navigationStart;
|
|
6
|
+
let referrer = document.referrer;
|
|
7
|
+
const sendSetPageLocation = app.safe(() => {
|
|
8
|
+
const { URL } = document;
|
|
9
|
+
if (URL !== url) {
|
|
10
|
+
url = URL;
|
|
11
|
+
app.send(SetPageLocation(url, referrer, navigationStart));
|
|
12
|
+
navigationStart = 0;
|
|
13
|
+
referrer = url;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const sendSetViewportSize = app.safe(() => {
|
|
17
|
+
const { innerWidth, innerHeight } = window;
|
|
18
|
+
if (innerWidth !== width || innerHeight !== height) {
|
|
19
|
+
width = innerWidth;
|
|
20
|
+
height = innerHeight;
|
|
21
|
+
app.send(SetViewportSize(width, height));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const sendSetPageVisibility = document.hidden === undefined
|
|
25
|
+
? Function.prototype
|
|
26
|
+
: app.safe(() => app.send(SetPageVisibility(document.hidden)));
|
|
27
|
+
app.attachStartCallback(() => {
|
|
28
|
+
url = '';
|
|
29
|
+
navigationStart = getTimeOrigin();
|
|
30
|
+
width = height = -1;
|
|
31
|
+
sendSetPageLocation();
|
|
32
|
+
sendSetViewportSize();
|
|
33
|
+
sendSetPageVisibility();
|
|
34
|
+
});
|
|
35
|
+
if (document.hidden !== undefined) {
|
|
36
|
+
app.attachEventListener(document, 'visibilitychange', sendSetPageVisibility, false, false);
|
|
37
|
+
}
|
|
38
|
+
app.ticker.attach(sendSetPageLocation, 1, false);
|
|
39
|
+
app.ticker.attach(sendSetViewportSize, 5, false);
|
|
40
|
+
}
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const IN_BROWSER: boolean;
|
|
2
|
+
export declare const IS_FIREFOX: false | RegExpMatchArray | null;
|
|
3
|
+
export declare const MAX_STR_LEN = 100000;
|
|
4
|
+
export declare function adjustTimeOrigin(): void;
|
|
5
|
+
export declare function getTimeOrigin(): number;
|
|
6
|
+
export declare const now: () => number;
|
|
7
|
+
export declare const stars: (str: string) => string;
|
|
8
|
+
export declare function normSpaces(str: string): string;
|
|
9
|
+
export declare function isURL(s: string): boolean;
|
|
10
|
+
export declare const DOCS_HOST = "https://docs.openreplay.com";
|
|
11
|
+
export declare function deprecationWarn(nameOfFeature: string, useInstead: string, docsPath?: string): void;
|
|
12
|
+
export declare function getLabelAttribute(e: Element): string | null;
|
|
13
|
+
export declare function hasOpenreplayAttribute(e: Element, attr: string): boolean;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const DEPRECATED_ATTRS = { htmlmasked: 'hidden', masked: 'obscured' };
|
|
2
|
+
export const IN_BROWSER = !(typeof window === 'undefined');
|
|
3
|
+
export const IS_FIREFOX = IN_BROWSER && navigator.userAgent.match(/firefox|fxios/i);
|
|
4
|
+
export const MAX_STR_LEN = 1e5;
|
|
5
|
+
// Buggy to use `performance.timeOrigin || performance.timing.navigationStart`
|
|
6
|
+
// https://github.com/mdn/content/issues/4713
|
|
7
|
+
// Maybe move to timer/ticker
|
|
8
|
+
let timeOrigin = IN_BROWSER ? Date.now() - performance.now() : 0;
|
|
9
|
+
export function adjustTimeOrigin() {
|
|
10
|
+
timeOrigin = Date.now() - performance.now();
|
|
11
|
+
}
|
|
12
|
+
export function getTimeOrigin() {
|
|
13
|
+
return timeOrigin;
|
|
14
|
+
}
|
|
15
|
+
export const now = IN_BROWSER && !!performance.now
|
|
16
|
+
? () => Math.round(performance.now() + timeOrigin)
|
|
17
|
+
: () => Date.now();
|
|
18
|
+
export const stars = 'repeat' in String.prototype
|
|
19
|
+
? (str) => '*'.repeat(str.length)
|
|
20
|
+
: (str) => str.replace(/./g, '*');
|
|
21
|
+
export function normSpaces(str) {
|
|
22
|
+
return str.trim().replace(/\s+/g, ' ');
|
|
23
|
+
}
|
|
24
|
+
// isAbsoluteUrl regexp: /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url)
|
|
25
|
+
export function isURL(s) {
|
|
26
|
+
return s.startsWith('https://') || s.startsWith('http://');
|
|
27
|
+
}
|
|
28
|
+
// TODO: JOIN IT WITH LOGGER somehow (use logging decorators?); Don't forget about index.js loggin when there is no logger instance.
|
|
29
|
+
export const DOCS_HOST = 'https://docs.openreplay.com';
|
|
30
|
+
const warnedFeatures = {};
|
|
31
|
+
export function deprecationWarn(nameOfFeature, useInstead, docsPath = '/') {
|
|
32
|
+
if (warnedFeatures[nameOfFeature]) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.warn(`OpenReplay: ${nameOfFeature} is deprecated. ${useInstead ? `Please, use ${useInstead} instead.` : ''} Visit ${DOCS_HOST}${docsPath} for more information.`);
|
|
36
|
+
warnedFeatures[nameOfFeature] = true;
|
|
37
|
+
}
|
|
38
|
+
export function getLabelAttribute(e) {
|
|
39
|
+
let value = e.getAttribute('data-openreplay-label');
|
|
40
|
+
if (value !== null) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
value = e.getAttribute('data-asayer-label');
|
|
44
|
+
if (value !== null) {
|
|
45
|
+
deprecationWarn('"data-asayer-label" attribute', '"data-openreplay-label" attribute', '/');
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
export function hasOpenreplayAttribute(e, attr) {
|
|
50
|
+
const newName = `data-openreplay-${attr}`;
|
|
51
|
+
if (e.hasAttribute(newName)) {
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
if (DEPRECATED_ATTRS[attr]) {
|
|
54
|
+
deprecationWarn(`"${newName}" attribute`,
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
`"${DEPRECATED_ATTRS[attr]}" attribute`, '/installation/sanitize-data');
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type Options = {
|
|
2
|
+
root: Element;
|
|
3
|
+
idName: (name: string) => boolean;
|
|
4
|
+
className: (name: string) => boolean;
|
|
5
|
+
tagName: (name: string) => boolean;
|
|
6
|
+
attr: (name: string, value: string) => boolean;
|
|
7
|
+
seedMinLength: number;
|
|
8
|
+
optimizedMinLength: number;
|
|
9
|
+
threshold: number;
|
|
10
|
+
maxNumberOfTries: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function finder(input: Element, options?: Partial<Options>): string;
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
var Limit;
|
|
2
|
+
(function (Limit) {
|
|
3
|
+
Limit[Limit["All"] = 0] = "All";
|
|
4
|
+
Limit[Limit["Two"] = 1] = "Two";
|
|
5
|
+
Limit[Limit["One"] = 2] = "One";
|
|
6
|
+
})(Limit || (Limit = {}));
|
|
7
|
+
let config;
|
|
8
|
+
let rootDocument;
|
|
9
|
+
export function finder(input, options) {
|
|
10
|
+
if (input.nodeType !== Node.ELEMENT_NODE) {
|
|
11
|
+
throw new Error("Can't generate CSS selector for non-element node type.");
|
|
12
|
+
}
|
|
13
|
+
if ('html' === input.tagName.toLowerCase()) {
|
|
14
|
+
return 'html';
|
|
15
|
+
}
|
|
16
|
+
const defaults = {
|
|
17
|
+
root: document.body,
|
|
18
|
+
idName: (name) => true,
|
|
19
|
+
className: (name) => true,
|
|
20
|
+
tagName: (name) => true,
|
|
21
|
+
attr: (name, value) => false,
|
|
22
|
+
seedMinLength: 1,
|
|
23
|
+
optimizedMinLength: 2,
|
|
24
|
+
threshold: 1000,
|
|
25
|
+
maxNumberOfTries: 10000,
|
|
26
|
+
};
|
|
27
|
+
config = Object.assign(Object.assign({}, defaults), options);
|
|
28
|
+
rootDocument = findRootDocument(config.root, defaults);
|
|
29
|
+
let path = bottomUpSearch(input, Limit.All, () => bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)));
|
|
30
|
+
if (path) {
|
|
31
|
+
const optimized = sort(optimize(path, input));
|
|
32
|
+
if (optimized.length > 0) {
|
|
33
|
+
path = optimized[0];
|
|
34
|
+
}
|
|
35
|
+
return selector(path);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw new Error('Selector was not found.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function findRootDocument(rootNode, defaults) {
|
|
42
|
+
if (rootNode.nodeType === Node.DOCUMENT_NODE) {
|
|
43
|
+
return rootNode;
|
|
44
|
+
}
|
|
45
|
+
if (rootNode === defaults.root) {
|
|
46
|
+
return rootNode.ownerDocument;
|
|
47
|
+
}
|
|
48
|
+
return rootNode;
|
|
49
|
+
}
|
|
50
|
+
function bottomUpSearch(input, limit, fallback) {
|
|
51
|
+
let path = null;
|
|
52
|
+
const stack = [];
|
|
53
|
+
let current = input;
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (current && current !== config.root.parentElement) {
|
|
56
|
+
let level = maybe(id(current)) ||
|
|
57
|
+
maybe(...attr(current)) ||
|
|
58
|
+
maybe(...classNames(current)) ||
|
|
59
|
+
maybe(tagName(current)) || [any()];
|
|
60
|
+
const nth = index(current);
|
|
61
|
+
if (limit === Limit.All) {
|
|
62
|
+
if (nth) {
|
|
63
|
+
level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth)));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (limit === Limit.Two) {
|
|
67
|
+
level = level.slice(0, 1);
|
|
68
|
+
if (nth) {
|
|
69
|
+
level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (limit === Limit.One) {
|
|
73
|
+
const [node] = (level = level.slice(0, 1));
|
|
74
|
+
if (nth && dispensableNth(node)) {
|
|
75
|
+
level = [nthChild(node, nth)];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const node of level) {
|
|
79
|
+
node.level = i;
|
|
80
|
+
}
|
|
81
|
+
stack.push(level);
|
|
82
|
+
if (stack.length >= config.seedMinLength) {
|
|
83
|
+
path = findUniquePath(stack, fallback);
|
|
84
|
+
if (path) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
current = current.parentElement;
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
if (!path) {
|
|
92
|
+
path = findUniquePath(stack, fallback);
|
|
93
|
+
}
|
|
94
|
+
return path;
|
|
95
|
+
}
|
|
96
|
+
function findUniquePath(stack, fallback) {
|
|
97
|
+
const paths = sort(combinations(stack));
|
|
98
|
+
if (paths.length > config.threshold) {
|
|
99
|
+
return fallback ? fallback() : null;
|
|
100
|
+
}
|
|
101
|
+
for (const candidate of paths) {
|
|
102
|
+
if (unique(candidate)) {
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function selector(path) {
|
|
109
|
+
let node = path[0];
|
|
110
|
+
let query = node.name;
|
|
111
|
+
for (let i = 1; i < path.length; i++) {
|
|
112
|
+
const level = path[i].level || 0;
|
|
113
|
+
if (node.level === level - 1) {
|
|
114
|
+
query = `${path[i].name} > ${query}`;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
query = `${path[i].name} ${query}`;
|
|
118
|
+
}
|
|
119
|
+
node = path[i];
|
|
120
|
+
}
|
|
121
|
+
return query;
|
|
122
|
+
}
|
|
123
|
+
function penalty(path) {
|
|
124
|
+
return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0);
|
|
125
|
+
}
|
|
126
|
+
function unique(path) {
|
|
127
|
+
switch (rootDocument.querySelectorAll(selector(path)).length) {
|
|
128
|
+
case 0:
|
|
129
|
+
throw new Error(`Can't select any node with this selector: ${selector(path)}`);
|
|
130
|
+
case 1:
|
|
131
|
+
return true;
|
|
132
|
+
default:
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function id(input) {
|
|
137
|
+
const elementId = input.getAttribute('id');
|
|
138
|
+
if (elementId && config.idName(elementId)) {
|
|
139
|
+
return {
|
|
140
|
+
name: '#' + cssesc(elementId, { isIdentifier: true }),
|
|
141
|
+
penalty: 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function attr(input) {
|
|
147
|
+
const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value));
|
|
148
|
+
return attrs.map((attr) => ({
|
|
149
|
+
name: '[' + cssesc(attr.name, { isIdentifier: true }) + '="' + cssesc(attr.value) + '"]',
|
|
150
|
+
penalty: 0.5,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
function classNames(input) {
|
|
154
|
+
const names = Array.from(input.classList).filter(config.className);
|
|
155
|
+
return names.map((name) => ({
|
|
156
|
+
name: '.' + cssesc(name, { isIdentifier: true }),
|
|
157
|
+
penalty: 1,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
function tagName(input) {
|
|
161
|
+
const name = input.tagName.toLowerCase();
|
|
162
|
+
if (config.tagName(name)) {
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
penalty: 2,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function any() {
|
|
171
|
+
return {
|
|
172
|
+
name: '*',
|
|
173
|
+
penalty: 3,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function index(input) {
|
|
177
|
+
const parent = input.parentNode;
|
|
178
|
+
if (!parent) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
let child = parent.firstChild;
|
|
182
|
+
if (!child) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
let i = 0;
|
|
186
|
+
while (child) {
|
|
187
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
if (child === input) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
child = child.nextSibling;
|
|
194
|
+
}
|
|
195
|
+
return i;
|
|
196
|
+
}
|
|
197
|
+
function nthChild(node, i) {
|
|
198
|
+
return {
|
|
199
|
+
name: node.name + `:nth-child(${i})`,
|
|
200
|
+
penalty: node.penalty + 1,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function dispensableNth(node) {
|
|
204
|
+
return node.name !== 'html' && !node.name.startsWith('#');
|
|
205
|
+
}
|
|
206
|
+
function maybe(...level) {
|
|
207
|
+
const list = level.filter(notEmpty);
|
|
208
|
+
if (list.length > 0) {
|
|
209
|
+
return list;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function notEmpty(value) {
|
|
214
|
+
return value !== null && value !== undefined;
|
|
215
|
+
}
|
|
216
|
+
function combinations(stack, path = []) {
|
|
217
|
+
const paths = [];
|
|
218
|
+
if (stack.length > 0) {
|
|
219
|
+
for (const node of stack[0]) {
|
|
220
|
+
paths.push(...combinations(stack.slice(1, stack.length), path.concat(node)));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
paths.push(path);
|
|
225
|
+
}
|
|
226
|
+
return paths;
|
|
227
|
+
}
|
|
228
|
+
function sort(paths) {
|
|
229
|
+
return Array.from(paths).sort((a, b) => penalty(a) - penalty(b));
|
|
230
|
+
}
|
|
231
|
+
function optimize(path, input, scope = {
|
|
232
|
+
counter: 0,
|
|
233
|
+
visited: new Map(),
|
|
234
|
+
}) {
|
|
235
|
+
const paths = [];
|
|
236
|
+
if (path.length > 2 && path.length > config.optimizedMinLength) {
|
|
237
|
+
for (let i = 1; i < path.length - 1; i++) {
|
|
238
|
+
if (scope.counter > config.maxNumberOfTries) {
|
|
239
|
+
return paths; // Okay At least I tried!
|
|
240
|
+
}
|
|
241
|
+
scope.counter += 1;
|
|
242
|
+
const newPath = [...path];
|
|
243
|
+
newPath.splice(i, 1);
|
|
244
|
+
const newPathKey = selector(newPath);
|
|
245
|
+
if (scope.visited.has(newPathKey)) {
|
|
246
|
+
return paths;
|
|
247
|
+
}
|
|
248
|
+
if (unique(newPath) && same(newPath, input)) {
|
|
249
|
+
paths.push(newPath);
|
|
250
|
+
scope.visited.set(newPathKey, true);
|
|
251
|
+
paths.push(...optimize(newPath, input, scope));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return paths;
|
|
256
|
+
}
|
|
257
|
+
function same(path, input) {
|
|
258
|
+
return rootDocument.querySelector(selector(path)) === input;
|
|
259
|
+
}
|
|
260
|
+
const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/;
|
|
261
|
+
const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/;
|
|
262
|
+
const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g;
|
|
263
|
+
const defaultOptions = {
|
|
264
|
+
escapeEverything: false,
|
|
265
|
+
isIdentifier: false,
|
|
266
|
+
quotes: 'single',
|
|
267
|
+
wrap: false,
|
|
268
|
+
};
|
|
269
|
+
function cssesc(string, opt = {}) {
|
|
270
|
+
const options = Object.assign(Object.assign({}, defaultOptions), opt);
|
|
271
|
+
if (options.quotes != 'single' && options.quotes != 'double') {
|
|
272
|
+
options.quotes = 'single';
|
|
273
|
+
}
|
|
274
|
+
const quote = options.quotes == 'double' ? '"' : "'";
|
|
275
|
+
const isIdentifier = options.isIdentifier;
|
|
276
|
+
const firstChar = string.charAt(0);
|
|
277
|
+
let output = '';
|
|
278
|
+
let counter = 0;
|
|
279
|
+
const length = string.length;
|
|
280
|
+
while (counter < length) {
|
|
281
|
+
const character = string.charAt(counter++);
|
|
282
|
+
let codePoint = character.charCodeAt(0);
|
|
283
|
+
let value = void 0;
|
|
284
|
+
// If it’s not a printable ASCII character…
|
|
285
|
+
if (codePoint < 0x20 || codePoint > 0x7e) {
|
|
286
|
+
if (codePoint >= 0xd800 && codePoint <= 0xdbff && counter < length) {
|
|
287
|
+
// It’s a high surrogate, and there is a next character.
|
|
288
|
+
const extra = string.charCodeAt(counter++);
|
|
289
|
+
if ((extra & 0xfc00) == 0xdc00) {
|
|
290
|
+
// next character is low surrogate
|
|
291
|
+
codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// It’s an unmatched surrogate; only append this code unit, in case
|
|
295
|
+
// the next code unit is the high surrogate of a surrogate pair.
|
|
296
|
+
counter--;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
value = '\\' + codePoint.toString(16).toUpperCase() + ' ';
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
if (options.escapeEverything) {
|
|
303
|
+
if (regexAnySingleEscape.test(character)) {
|
|
304
|
+
value = '\\' + character;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
value = '\\' + codePoint.toString(16).toUpperCase() + ' ';
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (/[\t\n\f\r\x0B]/.test(character)) {
|
|
311
|
+
value = '\\' + codePoint.toString(16).toUpperCase() + ' ';
|
|
312
|
+
}
|
|
313
|
+
else if (character == '\\' ||
|
|
314
|
+
(!isIdentifier &&
|
|
315
|
+
((character == '"' && quote == character) || (character == "'" && quote == character))) ||
|
|
316
|
+
(isIdentifier && regexSingleEscape.test(character))) {
|
|
317
|
+
value = '\\' + character;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
value = character;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
output += value;
|
|
324
|
+
}
|
|
325
|
+
if (isIdentifier) {
|
|
326
|
+
if (/^-[-\d]/.test(output)) {
|
|
327
|
+
output = '\\-' + output.slice(1);
|
|
328
|
+
}
|
|
329
|
+
else if (/\d/.test(firstChar)) {
|
|
330
|
+
output = '\\3' + firstChar + ' ' + output.slice(1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Remove spaces after `\HEX` escapes that are not followed by a hex digit,
|
|
334
|
+
// since they’re redundant. Note that this is only possible if the escape
|
|
335
|
+
// sequence isn’t preceded by an odd number of backslashes.
|
|
336
|
+
output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
|
|
337
|
+
if ($1 && $1.length % 2) {
|
|
338
|
+
// It’s not safe to remove the space, so don’t.
|
|
339
|
+
return $0;
|
|
340
|
+
}
|
|
341
|
+
// Strip the space.
|
|
342
|
+
return ($1 || '') + $2;
|
|
343
|
+
});
|
|
344
|
+
if (!isIdentifier && options.wrap) {
|
|
345
|
+
return quote + output + quote;
|
|
346
|
+
}
|
|
347
|
+
return output;
|
|
348
|
+
}
|