clarity-js 0.6.28 → 0.6.32
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/build/clarity.js +108 -78
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +108 -78
- package/package.json +1 -1
- package/src/core/api.ts +8 -0
- package/src/core/event.ts +4 -3
- package/src/core/version.ts +1 -1
- package/src/data/metadata.ts +1 -1
- package/src/data/upload.ts +11 -8
- package/src/diagnostic/internal.ts +1 -14
- package/src/layout/dom.ts +28 -13
- package/src/layout/mutation.ts +14 -12
- package/src/layout/node.ts +1 -0
- package/src/layout/selector.ts +14 -3
- package/test/helper.ts +10 -2
- package/types/core.d.ts +7 -0
- package/types/data.d.ts +3 -0
- package/types/layout.d.ts +1 -2
package/src/data/upload.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function queue(tokens: Token[], transmit: boolean = true): void {
|
|
|
42
42
|
let now = time();
|
|
43
43
|
let type = tokens.length > 1 ? tokens[1] : null;
|
|
44
44
|
let event = JSON.stringify(tokens);
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
switch (type) {
|
|
47
47
|
case Event.Discover:
|
|
48
48
|
discoverBytes += event.length;
|
|
@@ -118,12 +118,12 @@ async function upload(final: boolean = false): Promise<void> {
|
|
|
118
118
|
|
|
119
119
|
let p = sendPlaybackBytes ? `[${playback.join()}]` : Constant.Empty;
|
|
120
120
|
let encoded: EncodedPayload = {e, a, p};
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
// Get the payload ready for sending over the wire
|
|
123
123
|
// We also attempt to compress the payload if it is not the last payload and the browser supports it
|
|
124
124
|
// In all other cases, we continue to send back string value
|
|
125
125
|
let payload = stringify(encoded);
|
|
126
|
-
let zipped = last ? null : await compress(payload)
|
|
126
|
+
let zipped = last ? null : await compress(payload)
|
|
127
127
|
metric.sum(Metric.TotalBytes, zipped ? zipped.length : payload.length);
|
|
128
128
|
send(payload, zipped, envelope.data.sequence, last);
|
|
129
129
|
|
|
@@ -151,8 +151,11 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo
|
|
|
151
151
|
// However, we don't want to rely on it for every payload, since we have no ability to retry if the upload failed.
|
|
152
152
|
// Also, in case of sendBeacon, we do not have a way to alter HTTP headers and therefore can't send compressed payload
|
|
153
153
|
if (beacon && "sendBeacon" in navigator) {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
try {
|
|
155
|
+
// Navigator needs to be bound to sendBeacon before it is used to avoid errors in some browsers
|
|
156
|
+
dispatched = navigator.sendBeacon.bind(navigator)(url, payload);
|
|
157
|
+
if (dispatched) { done(sequence); }
|
|
158
|
+
} catch { /* do nothing - and we will automatically fallback to XHR below */ }
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
// Before initiating XHR upload, we check if the data has already been uploaded using sendBeacon
|
|
@@ -189,7 +192,7 @@ function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
|
189
192
|
if (xhr && xhr.readyState === XMLReadyState.Done && transitData) {
|
|
190
193
|
// Attempt send payload again (as configured in settings) if we do not receive a success (2XX) response code back from the server
|
|
191
194
|
if ((xhr.status < 200 || xhr.status > 208) && transitData.attempts <= Setting.RetryLimit) {
|
|
192
|
-
// We re-attempt in all cases except when server explicitly rejects our request with 4XX error
|
|
195
|
+
// We re-attempt in all cases except when server explicitly rejects our request with 4XX error
|
|
193
196
|
if (xhr.status >= 400 && xhr.status < 500) {
|
|
194
197
|
// In case of a 4XX response from the server, we bail out instead of trying again
|
|
195
198
|
limit.trigger(Check.Server);
|
|
@@ -212,7 +215,7 @@ function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
|
212
215
|
// Handle response if it was a 200 response with a valid body
|
|
213
216
|
if (xhr.status === 200 && xhr.responseText) { response(xhr.responseText); }
|
|
214
217
|
// If we exhausted our retries then trigger Clarity's shutdown for this page since the data will be incomplete
|
|
215
|
-
if (xhr.status === 0) {
|
|
218
|
+
if (xhr.status === 0) {
|
|
216
219
|
// And, right before we terminate the session, we will attempt one last time to see if we can use
|
|
217
220
|
// different transport option (sendBeacon vs. XHR) to get this data to the server for analysis purposes
|
|
218
221
|
send(transitData.data, null, sequence, true);
|
|
@@ -233,7 +236,7 @@ function done(sequence: number): void {
|
|
|
233
236
|
|
|
234
237
|
function delay(): number {
|
|
235
238
|
// Progressively increase delay as we continue to send more payloads from the client to the server
|
|
236
|
-
// If we are not uploading data to a server, and instead invoking UploadCallback, in that case keep returning configured value
|
|
239
|
+
// If we are not uploading data to a server, and instead invoking UploadCallback, in that case keep returning configured value
|
|
237
240
|
let gap = config.lean === false && discoverBytes > 0 ? Setting.MinUploadDelay : envelope.data.sequence * config.delay;
|
|
238
241
|
return typeof config.upload === Constant.String ? Math.max(Math.min(gap, Setting.MaxUploadDelay), Setting.MinUploadDelay) : config.delay;
|
|
239
242
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { Code,
|
|
1
|
+
import { Code, Event, Severity } from "@clarity-types/data";
|
|
2
2
|
import { LogData } from "@clarity-types/diagnostic";
|
|
3
|
-
import config from "@src/core/config";
|
|
4
|
-
import { bind } from "@src/core/event";
|
|
5
3
|
import encode from "./encode";
|
|
6
4
|
|
|
7
5
|
let history: { [key: number]: string[] } = {};
|
|
@@ -9,7 +7,6 @@ export let data: LogData;
|
|
|
9
7
|
|
|
10
8
|
export function start(): void {
|
|
11
9
|
history = {};
|
|
12
|
-
bind(document, "securitypolicyviolation", csp);
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
export function log(code: Code, severity: Severity, name: string = null, message: string = null, stack: string = null): void {
|
|
@@ -26,16 +23,6 @@ export function log(code: Code, severity: Severity, name: string = null, message
|
|
|
26
23
|
encode(Event.Log);
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
function csp(e: SecurityPolicyViolationEvent): void {
|
|
30
|
-
let upload = config.upload as string;
|
|
31
|
-
let parts = upload ? upload.substr(0, upload.indexOf("/", Constant.HTTPS.length)).split(Constant.Dot) : []; // Look for first "/" starting after initial "https://" string
|
|
32
|
-
let domain = parts.length >= 2 ? parts.splice(-2).join(Constant.Dot) : null;
|
|
33
|
-
// Capture content security policy violation only if disposition value is not explicitly set to "report"
|
|
34
|
-
if (domain && e.blockedURI && e.blockedURI.indexOf(domain) >= 0 && e["disposition"] !== Constant.Report) {
|
|
35
|
-
log(Code.ContentSecurityPolicy, Severity.Warning, e.blockedURI);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
26
|
export function stop(): void {
|
|
40
27
|
history = {};
|
|
41
28
|
}
|
package/src/layout/dom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Privacy } from "@clarity-types/core";
|
|
2
2
|
import { Code, Setting, Severity } from "@clarity-types/data";
|
|
3
|
-
import { Constant, NodeInfo, NodeValue, SelectorInput, Source } from "@clarity-types/layout";
|
|
3
|
+
import { Constant, NodeInfo, NodeValue, Selector, SelectorInput, Source } from "@clarity-types/layout";
|
|
4
4
|
import config from "@src/core/config";
|
|
5
5
|
import hash from "@src/core/hash";
|
|
6
6
|
import * as internal from "@src/diagnostic/internal";
|
|
@@ -12,13 +12,15 @@ let index: number = 1;
|
|
|
12
12
|
|
|
13
13
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#%3Cinput%3E_types
|
|
14
14
|
const DISALLOWED_TYPES = ["password", "hidden", "email", "tel"];
|
|
15
|
-
const DISALLOWED_NAMES = ["addr", "cell", "code", "dob", "email", "mob", "name", "phone", "secret", "social", "ssn", "tel", "zip", "pass"];
|
|
15
|
+
const DISALLOWED_NAMES = ["addr", "cell", "code", "dob", "email", "mob", "name", "phone", "secret", "social", "ssn", "tel", "zip", "pass", "card", "account", "cvv", "ccv"];
|
|
16
16
|
const DISALLOWED_MATCH = ["address", "password", "contact"];
|
|
17
17
|
|
|
18
18
|
let nodes: Node[] = [];
|
|
19
19
|
let values: NodeValue[] = [];
|
|
20
20
|
let updateMap: number[] = [];
|
|
21
21
|
let hashMap: { [hash: string]: number } = {};
|
|
22
|
+
let override = [];
|
|
23
|
+
let unmask = [];
|
|
22
24
|
|
|
23
25
|
// The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
|
|
24
26
|
let idMap: WeakMap<Node, number> = null; // Maps node => id.
|
|
@@ -27,7 +29,7 @@ let privacyMap: WeakMap<Node, Privacy> = null; // Maps node => Privacy (enum)
|
|
|
27
29
|
|
|
28
30
|
export function start(): void {
|
|
29
31
|
reset();
|
|
30
|
-
parse(document);
|
|
32
|
+
parse(document, true);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function stop(): void {
|
|
@@ -40,6 +42,8 @@ function reset(): void {
|
|
|
40
42
|
values = [];
|
|
41
43
|
updateMap = [];
|
|
42
44
|
hashMap = {};
|
|
45
|
+
override = [];
|
|
46
|
+
unmask = [];
|
|
43
47
|
idMap = new WeakMap();
|
|
44
48
|
iframeMap = new WeakMap();
|
|
45
49
|
privacyMap = new WeakMap();
|
|
@@ -47,10 +51,13 @@ function reset(): void {
|
|
|
47
51
|
|
|
48
52
|
// We parse new root nodes for any regions or masked nodes in the beginning (document) and
|
|
49
53
|
// later whenever there are new additions or modifications to DOM (mutations)
|
|
50
|
-
export function parse(root: ParentNode): void {
|
|
54
|
+
export function parse(root: ParentNode, init: boolean = false): void {
|
|
51
55
|
// Wrap selectors in a try / catch block.
|
|
52
56
|
// It's possible for script to receive invalid selectors, e.g. "'#id'" with extra quotes, and cause the code below to fail
|
|
53
57
|
try {
|
|
58
|
+
// Parse unmask configuration into separate query selectors and override tokens as part of initialization
|
|
59
|
+
if (init) { config.unmask.forEach(x => x.indexOf(Constant.Bang) < 0 ? unmask.push(x) : override.push(x.substr(1))); }
|
|
60
|
+
|
|
54
61
|
// Since mutations may happen on leaf nodes too, e.g. text nodes, which may not support all selector APIs.
|
|
55
62
|
// We ensure that the root note supports querySelectorAll API before executing the code below to identify new regions.
|
|
56
63
|
if ("querySelectorAll" in root) {
|
|
@@ -58,7 +65,7 @@ export function parse(root: ParentNode): void {
|
|
|
58
65
|
extract.metrics(root, config.metrics);
|
|
59
66
|
extract.dimensions(root, config.dimensions);
|
|
60
67
|
config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
|
|
61
|
-
|
|
68
|
+
unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
|
|
62
69
|
}
|
|
63
70
|
} catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
|
|
64
71
|
}
|
|
@@ -80,19 +87,17 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
|
|
|
80
87
|
let previousId = getPreviousId(node);
|
|
81
88
|
let privacy = config.content ? Privacy.Sensitive : Privacy.Text;
|
|
82
89
|
let parentValue = null;
|
|
83
|
-
let parentTag = Constant.Empty;
|
|
84
90
|
let regionId = region.exists(node) ? id : null;
|
|
85
91
|
|
|
86
92
|
if (parentId >= 0 && values[parentId]) {
|
|
87
93
|
parentValue = values[parentId];
|
|
88
|
-
parentTag = parentValue.data.tag;
|
|
89
94
|
parentValue.children.push(id);
|
|
90
95
|
regionId = regionId === null ? parentValue.region : regionId;
|
|
91
96
|
privacy = parentValue.metadata.privacy;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
// Check to see if this particular node should be masked or not
|
|
95
|
-
privacy = getPrivacy(node, data,
|
|
100
|
+
privacy = getPrivacy(node, data, parentValue, privacy);
|
|
96
101
|
|
|
97
102
|
// If there's an explicit region attribute set on the element, use it to mark a region on the page
|
|
98
103
|
if (data.attributes && Constant.RegionData in data.attributes) {
|
|
@@ -198,13 +203,27 @@ export function iframe(node: Node): HTMLIFrameElement {
|
|
|
198
203
|
return doc && iframeMap.has(doc) ? iframeMap.get(doc) : null;
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
function getPrivacy(node: Node, data: NodeInfo,
|
|
206
|
+
function getPrivacy(node: Node, data: NodeInfo, parent: NodeValue, privacy: Privacy): Privacy {
|
|
202
207
|
let attributes = data.attributes;
|
|
203
208
|
let tag = data.tag.toUpperCase();
|
|
204
209
|
|
|
205
210
|
// If this node was explicitly configured to contain sensitive content, use that information and return the value
|
|
206
211
|
if (privacyMap.has(node)) { return privacyMap.get(node); }
|
|
207
212
|
|
|
213
|
+
// If it's a text node belonging to a STYLE or TITLE tag;
|
|
214
|
+
// Or, the text node belongs to one of SCRUB_EXCEPTIONS
|
|
215
|
+
// then reset the privacy setting to ensure we capture the content
|
|
216
|
+
if (tag === Constant.TextTag && parent && parent.data) {
|
|
217
|
+
let path = parent.selector ? parent.selector[Selector.Stable] : Constant.Empty;
|
|
218
|
+
privacy = parent.data.tag === Constant.StyleTag || parent.data.tag === Constant.TitleTag ? Privacy.None : privacy;
|
|
219
|
+
for (let entry of override) {
|
|
220
|
+
if (path.indexOf(entry) >= 0) {
|
|
221
|
+
privacy = Privacy.None;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
// Do not proceed if attributes are missing for the node
|
|
209
228
|
if (attributes === null || attributes === undefined) { return privacy; }
|
|
210
229
|
|
|
@@ -243,10 +262,6 @@ function getPrivacy(node: Node, data: NodeInfo, parentTag: string, privacy: Priv
|
|
|
243
262
|
if (Constant.MaskData in attributes) { privacy = Privacy.TextImage; }
|
|
244
263
|
if (Constant.UnmaskData in attributes) { privacy = Privacy.None; }
|
|
245
264
|
|
|
246
|
-
// If it's a text node belonging to a STYLE or TITLE tag; then reset the privacy setting to ensure we capture the content
|
|
247
|
-
let cTag = tag === Constant.TextTag ? parentTag : tag;
|
|
248
|
-
if (cTag === Constant.StyleTag || cTag === Constant.TitleTag) { privacy = Privacy.None; }
|
|
249
|
-
|
|
250
265
|
return privacy;
|
|
251
266
|
}
|
|
252
267
|
|
package/src/layout/mutation.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Priority, Task, Timer } from "@clarity-types/core";
|
|
2
2
|
import { Code, Event, Metric, Severity } from "@clarity-types/data";
|
|
3
3
|
import { Constant, MutationHistory, MutationQueue, Setting, Source } from "@clarity-types/layout";
|
|
4
|
+
import api from "@src/core/api";
|
|
4
5
|
import { bind } from "@src/core/event";
|
|
5
6
|
import measure from "@src/core/measure";
|
|
6
7
|
import * as task from "@src/core/task";
|
|
@@ -36,7 +37,7 @@ export function start(): void {
|
|
|
36
37
|
|
|
37
38
|
if (insertRule === null) { insertRule = CSSStyleSheet.prototype.insertRule; }
|
|
38
39
|
if (deleteRule === null) { deleteRule = CSSStyleSheet.prototype.deleteRule; }
|
|
39
|
-
if (attachShadow === null) { attachShadow =
|
|
40
|
+
if (attachShadow === null) { attachShadow = Element.prototype.attachShadow; }
|
|
40
41
|
|
|
41
42
|
// Some popular open source libraries, like styled-components, optimize performance
|
|
42
43
|
// by injecting CSS using insertRule API vs. appending text node. A side effect of
|
|
@@ -56,7 +57,7 @@ export function start(): void {
|
|
|
56
57
|
// In case we are unable to add a hook and browser throws an exception,
|
|
57
58
|
// reset attachShadow variable and resume processing like before
|
|
58
59
|
try {
|
|
59
|
-
|
|
60
|
+
Element.prototype.attachShadow = function (): ShadowRoot {
|
|
60
61
|
return schedule(attachShadow.apply(this, arguments)) as ShadowRoot;
|
|
61
62
|
}
|
|
62
63
|
} catch { attachShadow = null; }
|
|
@@ -68,11 +69,8 @@ export function observe(node: Node): void {
|
|
|
68
69
|
// For this reason, we need to wire up mutations every time we see a new shadow dom.
|
|
69
70
|
// Also, wrap it inside a try / catch. In certain browsers (e.g. legacy Edge), observer on shadow dom can throw errors
|
|
70
71
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// As a temporary work around, ensuring Clarity can invoke MutationObserver outside of Zone (and use native implementation instead)
|
|
74
|
-
let api: string = window[Constant.Zone] && Constant.Symbol in window[Constant.Zone] ? window[Constant.Zone][Constant.Symbol](Constant.MutationObserver) : Constant.MutationObserver;
|
|
75
|
-
let observer = api in window ? new window[api](measure(handle) as MutationCallback) : null;
|
|
72
|
+
let m = api(Constant.MutationObserver);
|
|
73
|
+
let observer = m in window ? new window[m](measure(handle) as MutationCallback) : null;
|
|
76
74
|
if (observer) {
|
|
77
75
|
observer.observe(node, { attributes: true, childList: true, characterData: true, subtree: true });
|
|
78
76
|
observers.push(observer);
|
|
@@ -107,7 +105,7 @@ export function stop(): void {
|
|
|
107
105
|
|
|
108
106
|
// Restoring original attachShadow
|
|
109
107
|
if (attachShadow != null) {
|
|
110
|
-
|
|
108
|
+
Element.prototype.attachShadow = attachShadow;
|
|
111
109
|
attachShadow = null;
|
|
112
110
|
}
|
|
113
111
|
|
|
@@ -145,6 +143,7 @@ async function process(): Promise<void> {
|
|
|
145
143
|
let target = mutation.target;
|
|
146
144
|
let type = track(mutation, timer);
|
|
147
145
|
if (type && target && target.ownerDocument) { dom.parse(target.ownerDocument); }
|
|
146
|
+
if (type && target && target.nodeType == Node.DOCUMENT_FRAGMENT_NODE && (target as ShadowRoot).host) { dom.parse(target as ShadowRoot); }
|
|
148
147
|
switch (type) {
|
|
149
148
|
case Constant.Attributes:
|
|
150
149
|
processNode(target, Source.Attributes);
|
|
@@ -234,10 +233,13 @@ function schedule(node: Node): Node {
|
|
|
234
233
|
|
|
235
234
|
function trigger(): void {
|
|
236
235
|
for (let node of queue) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
236
|
+
// Generate a mutation for this node only if it still exists
|
|
237
|
+
if (node) {
|
|
238
|
+
let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
|
239
|
+
// Skip re-processing shadowRoot if it was already discovered
|
|
240
|
+
if (shadowRoot && dom.has(node)) { continue; }
|
|
241
|
+
generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
|
|
242
|
+
}
|
|
241
243
|
}
|
|
242
244
|
queue = [];
|
|
243
245
|
}
|
package/src/layout/node.ts
CHANGED
|
@@ -46,6 +46,7 @@ export default function (node: Node, source: Source): Node {
|
|
|
46
46
|
case Node.DOCUMENT_FRAGMENT_NODE:
|
|
47
47
|
let shadowRoot = (node as ShadowRoot);
|
|
48
48
|
if (shadowRoot.host) {
|
|
49
|
+
dom.parse(shadowRoot);
|
|
49
50
|
let type = typeof (shadowRoot.constructor);
|
|
50
51
|
if (type === Constant.Function && shadowRoot.constructor.toString().indexOf(Constant.NativeCode) >= 0) {
|
|
51
52
|
observe(shadowRoot);
|
package/src/layout/selector.ts
CHANGED
|
@@ -27,12 +27,10 @@ export default function(input: SelectorInput, beta: boolean = false): string {
|
|
|
27
27
|
// In beta mode, update selector to use "id" field when available. There are two exceptions:
|
|
28
28
|
// (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
|
|
29
29
|
// (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
|
|
30
|
-
let shadowStart = prefix.lastIndexOf(Constant.ShadowDomTag);
|
|
31
|
-
let shadowEnd = prefix.indexOf(">", shadowStart) + 1;
|
|
32
30
|
let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
|
|
33
31
|
classes = input.tag !== Constant.BodyTag && classes ? classes.filter(c => !hasDigits(c)) : [];
|
|
34
32
|
selector = classes.length > 0 ? `${prefix}${input.tag}.${classes.join(".")}${suffix}` : selector;
|
|
35
|
-
selector = id && hasDigits(id) === false ?
|
|
33
|
+
selector = id && hasDigits(id) === false ? `${getDomPrefix(prefix)}#${id}` : selector;
|
|
36
34
|
} else {
|
|
37
35
|
// Otherwise, fallback to stable mode, where we include class names as part of the selector
|
|
38
36
|
selector = classes ? `${prefix}${input.tag}.${classes.join(".")}${suffix}` : selector;
|
|
@@ -41,6 +39,19 @@ export default function(input: SelectorInput, beta: boolean = false): string {
|
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
41
|
|
|
42
|
+
function getDomPrefix(prefix: string): string {
|
|
43
|
+
const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
|
|
44
|
+
const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
|
|
45
|
+
const domStart = Math.max(shadowDomStart, iframeDomStart);
|
|
46
|
+
|
|
47
|
+
if (domStart < 0) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const domEnd = prefix.indexOf(">", domStart) + 1;
|
|
52
|
+
return prefix.substr(0, domEnd);
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
// Check if the given input string has digits or not
|
|
45
56
|
function hasDigits(value: string): boolean {
|
|
46
57
|
for (let i = 0; i < value.length; i++) {
|
package/test/helper.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Core, Data, Layout } from "clarity-decode";
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
+
import * as url from 'url';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import { Browser, Page, chromium } from 'playwright';
|
|
5
6
|
|
|
@@ -8,7 +9,13 @@ export async function launch(): Promise<Browser> {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export async function markup(page: Page, file: string, override: Core.Config = null): Promise<string[]> {
|
|
11
|
-
const
|
|
12
|
+
const htmlPath = path.resolve(__dirname, `./html/${file}`);
|
|
13
|
+
const htmlFileUrl = url.pathToFileURL(htmlPath).toString();
|
|
14
|
+
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
15
|
+
await Promise.all([
|
|
16
|
+
page.goto(htmlFileUrl),
|
|
17
|
+
page.waitForNavigation()
|
|
18
|
+
]);
|
|
12
19
|
await page.setContent(html.replace("</body>", `
|
|
13
20
|
<script>
|
|
14
21
|
window.payloads = [];
|
|
@@ -17,6 +24,7 @@ export async function markup(page: Page, file: string, override: Core.Config = n
|
|
|
17
24
|
</script>
|
|
18
25
|
</body>
|
|
19
26
|
`));
|
|
27
|
+
await page.hover("#two");
|
|
20
28
|
await page.waitForFunction("payloads && payloads.length > 1");
|
|
21
29
|
return await page.evaluate('payloads');
|
|
22
30
|
}
|
|
@@ -34,7 +42,7 @@ export function node(decoded: Data.DecodedPayload[], key: string, value: string
|
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
// Walking over the decoded payload to find the right match
|
|
37
|
-
for (let i = decoded.length - 1; i
|
|
45
|
+
for (let i = decoded.length - 1; i >= 0; i--) {
|
|
38
46
|
if (decoded[i].dom) {
|
|
39
47
|
for (let j = 0; j < decoded[i].dom.length; j++) {
|
|
40
48
|
if (decoded[i].dom[j].data) {
|
package/types/core.d.ts
CHANGED
|
@@ -127,3 +127,10 @@ export interface Config {
|
|
|
127
127
|
fallback?: string;
|
|
128
128
|
upgrade?: (key: string) => void;
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
export const enum Constant {
|
|
132
|
+
Zone = "Zone",
|
|
133
|
+
Symbol = "__symbol__",
|
|
134
|
+
AddEventListener = "addEventListener",
|
|
135
|
+
RemoveEventListener = "removeEventListener"
|
|
136
|
+
}
|
package/types/data.d.ts
CHANGED
package/types/layout.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export const enum Constant {
|
|
|
40
40
|
Src = "src",
|
|
41
41
|
Srcset = "srcset",
|
|
42
42
|
Box = "#",
|
|
43
|
+
Bang = "!",
|
|
43
44
|
Period = ".",
|
|
44
45
|
MaskData = "data-clarity-mask",
|
|
45
46
|
UnmaskData = "data-clarity-unmask",
|
|
@@ -74,8 +75,6 @@ export const enum Constant {
|
|
|
74
75
|
BorderBox = "border-box",
|
|
75
76
|
Value = "value",
|
|
76
77
|
MutationObserver = "MutationObserver",
|
|
77
|
-
Zone = "Zone",
|
|
78
|
-
Symbol = "__symbol__",
|
|
79
78
|
JsonLD = "application/ld+json",
|
|
80
79
|
String = "string",
|
|
81
80
|
Number = "number",
|