clarity-js 0.6.32 → 0.6.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-js",
3
- "version": "0.6.32",
3
+ "version": "0.6.33",
4
4
  "description": "An analytics library that uses web page interactions to generate aggregated insights",
5
5
  "author": "Microsoft Corp.",
6
6
  "license": "MIT",
@@ -9,8 +9,7 @@ let config: Config = {
9
9
  mask: [],
10
10
  unmask: [],
11
11
  regions: [],
12
- metrics: [],
13
- dimensions: [],
12
+ extract: [],
14
13
  cookies: [],
15
14
  report: null,
16
15
  upload: null,
@@ -1,2 +1,2 @@
1
- let version = "0.6.32";
1
+ let version = "0.6.33";
2
2
  export default version;
@@ -9,6 +9,7 @@ import * as ping from "@src/data/ping";
9
9
  import * as summary from "@src/data/summary";
10
10
  import * as upgrade from "@src/data/upgrade";
11
11
  import * as variable from "@src/data/variable";
12
+ import * as extract from "@src/data/extract";
12
13
  import { queue, track } from "./upload";
13
14
 
14
15
  export default function(event: Event): void {
@@ -105,5 +106,13 @@ export default function(event: Event): void {
105
106
  queue(tokens, false);
106
107
  }
107
108
  break;
109
+ case Event.Extract:
110
+ let extractKeys = extract.keys;
111
+ for (let e of extractKeys) {
112
+ tokens.push(e);
113
+ tokens.push(extract.data[e]);
114
+ }
115
+ extract.reset();
116
+ queue(tokens, false);
108
117
  }
109
118
  }
@@ -0,0 +1,143 @@
1
+ import { ExtractSource, Syntax, Type } from "@clarity-types/core";
2
+ import { Event, Setting, ExtractData } from "@clarity-types/data";
3
+ import config from "@src/core/config";
4
+ import encode from "./encode";
5
+ import * as internal from "@src/diagnostic/internal";
6
+ import { Code, Constant, Severity } from "@clarity-types/data";
7
+
8
+ export let data: ExtractData = {};
9
+ export let keys: (number | string)[] = [];
10
+
11
+ let variables : { [key: number]: Syntax[] } = {};
12
+ let selectors : { [key: number]: string } = {};
13
+ export let fragments: string[] = [];
14
+
15
+ export function start(): void {
16
+ try {
17
+ let e = config.extract;
18
+ if (!e) { return; }
19
+ for (let i = 0; i < e.length; i+=3) {
20
+ let source = e[i] as ExtractSource;
21
+ let key = e[i+1] as number;
22
+ switch (source) {
23
+ case ExtractSource.Javascript:
24
+ let variable = e[i+2] as string;
25
+ variables[key] = parse(variable);
26
+ break;
27
+ case ExtractSource.Cookie:
28
+ /*Todo: Add cookie extract logic*/
29
+ break;
30
+ case ExtractSource.Text:
31
+ let match = e[i+2] as string;
32
+ selectors[key] = match;
33
+ break;
34
+ case ExtractSource.Fragment:
35
+ fragments = e[i+2] as string[];
36
+ break;
37
+ }
38
+ }
39
+ }
40
+ catch(e) {
41
+ internal.log(Code.Config, Severity.Warning, e ? e.name : null);
42
+ }
43
+ }
44
+
45
+ export function clone(v: Syntax[]): Syntax[] {
46
+ return JSON.parse(JSON.stringify(v));
47
+ }
48
+
49
+ export function compute(): void {
50
+ try {
51
+ for (let v in variables) {
52
+ let value = str(evaluate(clone(variables[v])));
53
+ if (value) { update(v, value); }
54
+ }
55
+
56
+ for (let s in selectors) {
57
+ let node = document.querySelector(selectors[s] as string) as HTMLElement;
58
+ if (node) { update(s, node.innerText); }
59
+ }
60
+ }
61
+ catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
62
+
63
+ encode(Event.Extract);
64
+ }
65
+
66
+ export function reset(): void {
67
+ keys = [];
68
+ }
69
+
70
+ export function update(key: string, value: string | number, force: boolean = false): void {
71
+ if (!(key in data) || (key in data && data[key] !== value) || force ) {
72
+ data[key] = value;
73
+ keys.push(key);
74
+ }
75
+ }
76
+
77
+ export function stop(): void {
78
+ data = {};
79
+ keys = [];
80
+ variables = {};
81
+ selectors = {};
82
+ }
83
+
84
+ function parse(variable: string): Syntax[] {
85
+ let syntax: Syntax[] = [];
86
+ let parts = variable.split(Constant.Dot);
87
+ while (parts.length > 0) {
88
+ let part = parts.shift();
89
+ let arrayStart = part.indexOf(Constant.ArrayStart);
90
+ let conditionStart = part.indexOf(Constant.ConditionStart);
91
+ let conditionEnd = part.indexOf(Constant.ConditionEnd);
92
+ syntax.push({
93
+ name : arrayStart > 0 ? part.substring(0, arrayStart) : (conditionStart > 0 ? part.substring(0, conditionStart) : part),
94
+ type : arrayStart > 0 ? Type.Array : (conditionStart > 0 ? Type.Object : Type.Simple),
95
+ condition : conditionStart > 0 ? part.substring(conditionStart + 1, conditionEnd) : null
96
+ });
97
+ }
98
+
99
+ return syntax;
100
+ }
101
+
102
+ // The function below takes in a variable name in following format: "a.b.c" and safely evaluates its value in javascript context
103
+ // For instance, for a.b.c, it will first check window["a"]. If it exists, it will recursively look at: window["a"]["b"] and finally,
104
+ // return the value for window["a"]["b"]["c"].
105
+ function evaluate(variable: Syntax[], base: Object = window): any {
106
+ if (variable.length == 0) { return base; }
107
+ let part = variable.shift();
108
+ let output;
109
+ if (base && base[part.name]) {
110
+ let obj = base[part.name];
111
+ if (part.type !== Type.Array && match(obj, part.condition)) {
112
+ output = evaluate(variable, obj);
113
+ }
114
+ else if (Array.isArray(obj)) {
115
+ let filtered = [];
116
+ for (var value of obj) {
117
+ if (match(value, part.condition)) {
118
+ let op = evaluate(variable, value)
119
+ if (op) { filtered.push(op); }
120
+ }
121
+ }
122
+ output = filtered;
123
+ }
124
+
125
+ return output;
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ function str(input: string): string {
132
+ // Automatically trim string to max of Setting.ExtractLimit to avoid fetching long strings
133
+ return input ? JSON.stringify(input).substring(0, Setting.ExtractLimit) : input;
134
+ }
135
+
136
+ function match(base: Object, condition: string): boolean {
137
+ if (condition) {
138
+ let prop = condition.split(":");
139
+ return prop.length > 1 ? base[prop[0]] == prop[1] : base[prop[0]]
140
+ }
141
+
142
+ return true;
143
+ }
package/src/data/index.ts CHANGED
@@ -11,12 +11,13 @@ import * as summary from "@src/data/summary";
11
11
  import * as upgrade from "@src/data/upgrade";
12
12
  import * as upload from "@src/data/upload";
13
13
  import * as variable from "@src/data/variable";
14
+ import * as extract from "@src/data/extract";
14
15
  export { event } from "@src/data/custom";
15
16
  export { consent, metadata } from "@src/data/metadata";
16
17
  export { upgrade } from "@src/data/upgrade";
17
18
  export { set, identify } from "@src/data/variable";
18
19
 
19
- const modules: Module[] = [baseline, dimension, variable, limit, summary, metadata, envelope, upload, ping, upgrade];
20
+ const modules: Module[] = [baseline, dimension, variable, limit, summary, metadata, envelope, upload, ping, upgrade, extract];
20
21
 
21
22
  export function start(): void {
22
23
  // Metric needs to be initialized before we can start measuring. so metric is not wrapped in measure
@@ -40,4 +41,5 @@ export function compute(): void {
40
41
  metric.compute();
41
42
  summary.compute();
42
43
  limit.compute();
44
+ extract.compute();
43
45
  }
package/src/layout/dom.ts CHANGED
@@ -4,10 +4,10 @@ import { Constant, NodeInfo, NodeValue, Selector, SelectorInput, Source } from "
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";
7
- import * as extract from "@src/layout/extract";
8
7
  import * as region from "@src/layout/region";
9
8
  import selector from "@src/layout/selector";
10
-
9
+ import * as mutation from "@src/layout/mutation";
10
+ import * as extract from "@src/data/extract";
11
11
  let index: number = 1;
12
12
 
13
13
  // Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#%3Cinput%3E_types
@@ -21,6 +21,7 @@ let updateMap: number[] = [];
21
21
  let hashMap: { [hash: string]: number } = {};
22
22
  let override = [];
23
23
  let unmask = [];
24
+ let updatedFragments: { [fragment: number]: string } = {};
24
25
 
25
26
  // The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
26
27
  let idMap: WeakMap<Node, number> = null; // Maps node => id.
@@ -61,9 +62,7 @@ export function parse(root: ParentNode, init: boolean = false): void {
61
62
  // Since mutations may happen on leaf nodes too, e.g. text nodes, which may not support all selector APIs.
62
63
  // We ensure that the root note supports querySelectorAll API before executing the code below to identify new regions.
63
64
  if ("querySelectorAll" in root) {
64
- extract.regions(root, config.regions);
65
- extract.metrics(root, config.metrics);
66
- extract.dimensions(root, config.dimensions);
65
+ config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, `${x[0]}`))); // Regions
67
66
  config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
68
67
  unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
69
68
  }
@@ -88,11 +87,13 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
88
87
  let privacy = config.content ? Privacy.Sensitive : Privacy.Text;
89
88
  let parentValue = null;
90
89
  let regionId = region.exists(node) ? id : null;
90
+ let fragmentId = null;
91
91
 
92
92
  if (parentId >= 0 && values[parentId]) {
93
93
  parentValue = values[parentId];
94
94
  parentValue.children.push(id);
95
95
  regionId = regionId === null ? parentValue.region : regionId;
96
+ fragmentId = parentValue.fragment;
96
97
  privacy = parentValue.metadata.privacy;
97
98
  }
98
99
 
@@ -115,12 +116,13 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
115
116
  selector: null,
116
117
  hash: null,
117
118
  region: regionId,
118
- metadata: { active: true, suspend: false, privacy, position: null, size: null }
119
+ metadata: { active: true, suspend: false, privacy, position: null, size: null },
120
+ fragment: fragmentId,
119
121
  };
120
122
 
121
123
  updateSelector(values[id]);
122
124
  size(values[id], parentValue);
123
- track(id, source);
125
+ track(id, source, values[id].fragment);
124
126
  }
125
127
 
126
128
  export function update(node: Node, parent: Node, data: NodeInfo, source: Source): void {
@@ -174,9 +176,14 @@ export function update(node: Node, parent: Node, data: NodeInfo, source: Source)
174
176
  }
175
177
  }
176
178
 
179
+ // track node if it is a part of scheduled fragment mutation
180
+ if(value.fragment && updatedFragments[value.fragment]) {
181
+ changed = true;
182
+ }
183
+
177
184
  // Update selector
178
185
  updateSelector(value);
179
- track(id, source, changed, parentChanged);
186
+ track(id, source, values[id].fragment, changed, parentChanged);
180
187
  }
181
188
  }
182
189
 
@@ -296,6 +303,9 @@ function updateSelector(value: NodeValue): void {
296
303
  value.selector = [selector(s), selector(s, true)];
297
304
  value.hash = value.selector.map(x => x ? hash(x) : null) as [string, string];
298
305
  value.hash.forEach(h => hashMap[h] = value.id);
306
+ if (value.hash.some(h => extract.fragments.indexOf(h) !== -1)) {
307
+ value.fragment = value.id;
308
+ }
299
309
  }
300
310
 
301
311
  export function getNode(id: number): Node {
@@ -331,6 +341,11 @@ export function updates(): NodeValue[] {
331
341
  if (id in values) { output.push(values[id]); }
332
342
  }
333
343
  updateMap = [];
344
+ for (let id in updatedFragments) {
345
+ extract.update(updatedFragments[id], id, true)
346
+ }
347
+
348
+ updatedFragments = {}
334
349
  return output;
335
350
  }
336
351
 
@@ -368,7 +383,19 @@ function getPreviousId(node: Node): number {
368
383
  return id;
369
384
  }
370
385
 
371
- function track(id: number, source: Source, changed: boolean = true, parentChanged: boolean = false): void {
386
+ function track(id: number, source: Source, fragment: number = null, changed: boolean = true, parentChanged: boolean = false): void {
387
+ // if updated node is a part of fragment and the fragment is not being tracked currently, schedule a mutation on the fragment node
388
+ if (fragment && !updatedFragments[fragment]) {
389
+ let node = getNode(fragment)
390
+ let value = getValue(fragment);
391
+ if (node && value) {
392
+ mutation.schedule(node, true);
393
+ value.hash.forEach(h => {
394
+ if(extract.fragments.indexOf(h) !== -1) { updatedFragments[fragment] = h;}
395
+ });
396
+ }
397
+ }
398
+
372
399
  // Keep track of the order in which mutations happened, they may not be sequential
373
400
  // Edge case: If an element is added later on, and pre-discovered element is moved as a child.
374
401
  // In that case, we need to reorder the pre-discovered element in the update list to keep visualization consistent.
@@ -218,7 +218,7 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer): Pr
218
218
  }
219
219
  }
220
220
 
221
- function schedule(node: Node): Node {
221
+ export function schedule(node: Node, fragment: boolean = false): Node {
222
222
  // Only schedule manual trigger for this node if it's not already in the queue
223
223
  if (queue.indexOf(node) < 0) { queue.push(node); }
224
224
 
@@ -226,19 +226,19 @@ function schedule(node: Node): Node {
226
226
  // It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
227
227
  // And in those cases we do not wish to monitor changes multiple times for the same node.
228
228
  if (timeout) { clearTimeout(timeout); }
229
- timeout = setTimeout(trigger, Setting.LookAhead);
229
+ timeout = setTimeout(() => { trigger(fragment) }, Setting.LookAhead);
230
230
 
231
231
  return node;
232
232
  }
233
233
 
234
- function trigger(): void {
234
+ function trigger(fragment: boolean): void {
235
235
  for (let node of queue) {
236
236
  // Generate a mutation for this node only if it still exists
237
237
  if (node) {
238
238
  let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
239
239
  // Skip re-processing shadowRoot if it was already discovered
240
240
  if (shadowRoot && dom.has(node)) { continue; }
241
- generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
241
+ generate(node, shadowRoot || fragment ? Constant.ChildList : Constant.CharacterData);
242
242
  }
243
243
  }
244
244
  queue = [];
package/types/core.d.ts CHANGED
@@ -3,9 +3,8 @@ import * as Data from "./data";
3
3
  type TaskFunction = () => Promise<void>;
4
4
  type TaskResolve = () => void;
5
5
  type UploadCallback = (data: string) => void;
6
- type Region = [number /* RegionId */, string /* Query Selector */, RegionFilter? /* Region Filter */, string? /* Filter Text */];
7
- type Metric = [Data.Metric /* MetricId */, Extract /* Extract Filter */, string /* Match Value */, number? /* Scale Factor */];
8
- type Dimension = [Data.Dimension /* DimensionId */, Extract /* Extract Filter */, string /* Match Value */];
6
+ type Region = [number /* RegionId */, string /* Query Selector */];
7
+ export type Extract = ExtractSource /* Extraction Source */ | number /* Extract Id */ | string | string[] /* Hash or Query Selector or String Token */;
9
8
 
10
9
  /* Enum */
11
10
 
@@ -21,7 +20,6 @@ export const enum Time {
21
20
  Day = 24 * 60 * 60 * 1000
22
21
  }
23
22
 
24
-
25
23
  export const enum Task {
26
24
  Wait = 0,
27
25
  Run = 1,
@@ -32,15 +30,23 @@ export const enum Setting {
32
30
  LongTask = 30, // 30ms
33
31
  }
34
32
 
35
- export const enum RegionFilter {
36
- Url = 0,
37
- Javascript = 1
33
+ export const enum ExtractSource {
34
+ Javascript = 0,
35
+ Cookie = 1,
36
+ Text = 2,
37
+ Fragment = 3
38
+ }
39
+
40
+ export const enum Type {
41
+ Array = 1,
42
+ Object = 2,
43
+ Simple = 3
38
44
  }
39
45
 
40
- export const enum Extract {
41
- Text = 0,
42
- Javascript = 1,
43
- Attribute = 2
46
+ export type Syntax = {
47
+ name: string,
48
+ type: Type,
49
+ condition: string
44
50
  }
45
51
 
46
52
  export const enum Privacy {
@@ -119,8 +125,7 @@ export interface Config {
119
125
  mask?: string[];
120
126
  unmask?: string[];
121
127
  regions?: Region[];
122
- metrics?: Metric[];
123
- dimensions?: Dimension[];
128
+ extract?: Extract[];
124
129
  cookies?: string[];
125
130
  report?: string;
126
131
  upload?: string | UploadCallback;
package/types/data.d.ts CHANGED
@@ -54,7 +54,8 @@ export const enum Event {
54
54
  Summary = 36,
55
55
  Box = 37,
56
56
  Clipboard = 38,
57
- Submit = 39
57
+ Submit = 39,
58
+ Extract = 40
58
59
  }
59
60
 
60
61
  export const enum Metric {
@@ -138,7 +139,8 @@ export const enum Code {
138
139
  /**
139
140
  * @deprecated No longer support ContentSecurityPolicy
140
141
  */
141
- ContentSecurityPolicy = 7
142
+ ContentSecurityPolicy = 7,
143
+ Config = 8
142
144
  }
143
145
 
144
146
  export const enum Severity {
@@ -187,7 +189,8 @@ export const enum Setting {
187
189
  MaxFirstPayloadBytes = 1 * 1024 * 1024, // 1MB: Cap the very first payload to a maximum of 1MB
188
190
  UploadFactor = 3, // Slow down sequence by specified factor
189
191
  MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
190
- MaxUploadDelay = 30 * Time.Second // Do flush out payload once every 30s
192
+ MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
193
+ ExtractLimit = 10000 // Do not extract more than 10000 characters
191
194
  }
192
195
 
193
196
  export const enum Character {
@@ -248,6 +251,9 @@ export const enum Constant {
248
251
  Accept = "Accept",
249
252
  ClarityGzip = "application/x-clarity-gzip",
250
253
  Tilde = "~",
254
+ ArrayStart = "[",
255
+ ConditionStart = "{",
256
+ ConditionEnd = "}"
251
257
  }
252
258
 
253
259
  export const enum XMLReadyState {
@@ -366,6 +372,10 @@ export interface UpgradeData {
366
372
  key: string;
367
373
  }
368
374
 
375
+ export interface ExtractData {
376
+ [key: string]: string | number;
377
+ }
378
+
369
379
  export interface UploadData {
370
380
  sequence: number;
371
381
  attempts: number;
package/types/layout.d.ts CHANGED
@@ -154,6 +154,7 @@ export interface NodeValue {
154
154
  hash: [string, string];
155
155
  region: number;
156
156
  metadata: NodeMeta;
157
+ fragment: number;
157
158
  }
158
159
 
159
160
  export interface NodeMeta {
@@ -1,94 +0,0 @@
1
- import { Dimension, Extract, Metric, Region, RegionFilter } from "@clarity-types/core";
2
- import { Constant, Setting } from "@clarity-types/data";
3
- import * as dimension from "@src/data/dimension";
4
- import * as metric from "@src/data/metric";
5
- import * as region from "@src/layout/region";
6
-
7
- const formatRegex = /1/g;
8
- const digitsRegex = /[^0-9\.]/g;
9
- const digitsWithCommaRegex = /[^0-9\.,]/g;
10
- const regexCache: {[key: string]: RegExp} = {};
11
-
12
- export function regions(root: ParentNode, value: Region[]): void {
13
- for (let v of value) {
14
- const [regionId, selector, filter, match] = v;
15
- let valid = true;
16
- switch (filter) {
17
- case RegionFilter.Url: valid = match && !!top.location.href.match(regex(match)); break;
18
- case RegionFilter.Javascript: valid = match && !!evaluate(match); break;
19
- }
20
- if (valid) { root.querySelectorAll(selector).forEach(e => region.observe(e, regionId.toString())); }
21
- }
22
- }
23
-
24
- export function metrics(root: ParentNode, value: Metric[]): void {
25
- for (let v of value) {
26
- const [metricId, source, match, scale] = v;
27
- if (match) {
28
- switch (source) {
29
- case Extract.Text: root.querySelectorAll(match).forEach(e => { metric.max(metricId, num((e as HTMLElement).innerText, scale)); }); break;
30
- case Extract.Attribute: root.querySelectorAll(`[${match}]`).forEach(e => { metric.max(metricId, num(e.getAttribute(match), scale, false)); }); break;
31
- case Extract.Javascript: metric.max(metricId, evaluate(match, Constant.Number) as number); break;
32
- }
33
- }
34
- }
35
- }
36
-
37
- export function dimensions(root: ParentNode, value: Dimension[]): void {
38
- for (let v of value) {
39
- const [dimensionId, source, match] = v;
40
- if (match) {
41
- switch (source) {
42
- case Extract.Text: root.querySelectorAll(match).forEach(e => { dimension.log(dimensionId, str((e as HTMLElement).innerText)); }); break;
43
- case Extract.Attribute: root.querySelectorAll(`[${match}]`).forEach(e => { dimension.log(dimensionId, str(e.getAttribute(match))); }); break;
44
- case Extract.Javascript: dimension.log(dimensionId, str(evaluate(match, Constant.String))); break;
45
- }
46
- }
47
- }
48
- }
49
-
50
- function regex(match: string): RegExp {
51
- regexCache[match] = match in regexCache ? regexCache[match] : new RegExp(match);
52
- return regexCache[match];
53
- }
54
-
55
- // The function below takes in a variable name in following format: "a.b.c" and safely evaluates its value in javascript context
56
- // For instance, for a.b.c, it will first check window["a"]. If it exists, it will recursively look at: window["a"]["b"] and finally,
57
- // return the value for window["a"]["b"]["c"].
58
- function evaluate(variable: string, type: string = null, base: Object = window): any {
59
- let parts = variable.split(Constant.Dot);
60
- let first = parts.shift();
61
- if (base && base[first]) {
62
- if (parts.length > 0) { return evaluate(parts.join(Constant.Dot), type, base[first]); }
63
- let output = type === null || type === typeof base[first] ? base[first] : null;
64
- return output;
65
- }
66
- return null;
67
- }
68
-
69
- function str(input: string): string {
70
- // Automatically trim string to max of Setting.DimensionLimit to avoid fetching long strings
71
- return input ? input.substr(0, Setting.DimensionLimit) : input;
72
- }
73
-
74
- function num(text: string, scale: number, localize: boolean = true): number {
75
- try {
76
- scale = scale || 1;
77
- // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
78
- let lang = document.documentElement.lang;
79
- if (Intl && Intl.NumberFormat && lang && localize) {
80
- text = text.replace(digitsWithCommaRegex, Constant.Empty);
81
- // Infer current group and decimal separator from current locale
82
- let group = Intl.NumberFormat(lang).format(11111).replace(formatRegex, Constant.Empty);
83
- let decimal = Intl.NumberFormat(lang).format(1.1).replace(formatRegex, Constant.Empty);
84
-
85
- // Parse number using inferred group and decimal separators
86
- return Math.round(parseFloat(text
87
- .replace(new RegExp('\\' + group, 'g'), Constant.Empty)
88
- .replace(new RegExp('\\' + decimal), Constant.Dot)
89
- ) * scale);
90
- }
91
- // Fallback to en locale
92
- return Math.round(parseFloat(text.replace(digitsRegex, Constant.Empty)) * scale);
93
- } catch { return null; }
94
- }