Setting the file. One moment.
Source Location · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page function isCanonical
— line 122
This file
Number 45.149
Position 149 of 182
Type JavaScript
Size 11 KB
Lines 214 lib/ source-location.js
JavaScript · 214 lines · 11 KB
// A key containing '.', '[', ']', '@', Unicode or the empty key is always quoted, so the
14 // literal key "part.name" can never collide with the nested path part.name.
15
16 const ENCODING = 'replatform-location/1' ;
17 const IDENT = / ^ [A-Za-z_$][\w$-] * / ;
18 const IDENT_FULL = / ^ [A-Za-z_$][\w$-] *$ / ;
19
20 function fail ( message ) {
21 throw Object. assign ( new Error (message), { code: 'source_location_invalid' });
22 }
23 function readJsonString ( text , i ) {
24 if (text[i] !== '"' ) return null ;
25 let j = i + 1 ;
26 while (j < text. length ) {
27 if (text[j] === ' \\ ' ) j += 2 ;
28 else if (text[j] === '"' ) break ;
29 else j += 1 ;
30 }
31 if (j >= text. length ) fail ( 'unterminated quoted key' );
32 let value;
33 try { value = JSON . parse (text. slice (i, j + 1 )); } catch { fail ( 'invalid quoted key' ); }
34 return { value, end: j + 1 };
35 }
36 function readScalar ( text , i ) {
37 const quoted = readJsonString (text, i);
38 if (quoted) return quoted;
39 const m = / ^ - ? (?:0 | [1-9]\d * )(?: \. \d + ) ? (?: [eE][+-] ? \d + ) ? / . exec (text. slice (i));
40 if ( ! m) fail ( 'selector value must be a JSON string or number' );
41 return { value: Number (m[ 0 ]), end: i + m[ 0 ]. length };
42 }
43 // Parse a key-only relative path used inside a selector: ident or ["json"] then .ident / ["json"].
44 function readRelative ( text , i ) {
45 const path = [];
46 for (;;) {
47 if (path. length && text[i] === '.' ) {
48 const m = IDENT . exec (text. slice (i + 1 ));
49 if ( ! m) fail ( 'expected identifier after "."' );
50 path. push ({ key: m[ 0 ] }); i += 1 + m[ 0 ]. length ; continue ;
51 }
52 if ( ! path. length ) {
53 const m = IDENT . exec (text. slice (i));
54 if (m) { path. push ({ key: m[ 0 ] }); i += m[ 0 ]. length ; continue ; }
55 }
56 if (text[i] === '[' && text[i + 1 ] === '"' ) {
57 const q = readJsonString (text, i + 1 );
58 if (text[q.end] !== ']' ) fail ( 'expected "]" after quoted key' );
59 path. push ({ key: q.value }); i = q.end + 1 ; continue ;
60 }
61 break ;
62 }
63 if ( ! path. length ) fail ( 'selector needs a relative key path' );
64 return { path, end: i };
65 }
66 function parse ( text ) {
67 if ( typeof text !== 'string' || ! text. length ) fail ( 'location must be a non-empty string' );
68 const segments = [];
69 let i = 0 ;
70 const first = IDENT . exec (text);
71 if (first) { segments. push ({ key: first[ 0 ] }); i = first[ 0 ]. length ; }
72 while (i < text. length ) {
73 const c = text[i];
74 if (c === '.' ) {
75 if ( ! segments. length ) fail ( 'location cannot start with "."' );
76 const m = IDENT . exec (text. slice (i + 1 ));
77 if ( ! m) fail ( `expected identifier after "." at ${ i }` );
78 segments. push ({ key: m[ 0 ] }); i += 1 + m[ 0 ]. length ;
79 } else if (c === '[' ) {
80 if (text[i + 1 ] === ']' ) { segments. push ({ each: true }); i += 2 ; continue ; }
81 if (text[i + 1 ] === '"' ) {
82 const q = readJsonString (text, i + 1 );
83 if (text[q.end] !== ']' ) fail ( 'expected "]" after quoted key' );
84 segments. push ({ key: q.value }); i = q.end + 1 ; continue ;
85 }
86 const digits = / ^ \d + \] / . exec (text. slice (i + 1 ));
87 if (digits) { segments. push ({ each: true }); i += 1 + digits[ 0 ]. length ; continue ; }
88 const rel = readRelative (text, i + 1 );
89 if (text[rel.end] !== '=' ) fail ( 'selector needs "="' );
90 const val = readScalar (text, rel.end + 1 );
91 if (text[val.end] !== ']' ) fail ( 'selector needs closing "]"' );
92 segments. push ({ each: true , where: { path: rel.path, equals: val.value } });
93 i = val.end + 1 ;
94 } else fail ( `unexpected character at ${ i }` );
95 }
96 return segments;
97 }
98 function formatKey ( key , first ) {
99 if ( typeof key !== 'string' ) fail ( 'key must be a string' );
100 return IDENT_FULL . test (key) ? (first ? key : `.${ key }` ) : `[${ JSON . stringify ( key ) }]` ;
101 }
102 function formatScalar ( v ) {
103 if ( typeof v === 'string' ) return JSON . stringify (v);
104 if ( typeof v === 'number' && Number. isFinite (v)) return JSON . stringify (v);
105 fail ( 'selector value must be a string or finite number' );
106 }
107 function format ( segments ) {
108 if ( ! Array. isArray (segments) || ! segments. length ) fail ( 'location needs segments' );
109 return segments. map (( s , i ) => {
110 if (s && Object. hasOwn (s, 'key' )) return formatKey (s.key, i === 0 );
111 if (s && s.each === true && ! s.where) return '[]' ;
112 if (s && s.each === true && s.where) return `[${ s . where . path . map (( p , j ) => formatKey ( p . key , j === 0 )). join ( '' ) }=${ formatScalar ( s . where . equals ) }]` ;
113 return fail ( 'malformed segment' );
114 }). join ( '' );
115 }
116 // Parsing is pure and the same locations repeat for every record, so the canonical text and the
117 // (read-only) segments used by valuesAt are computed once per distinct string. Errors are not cached.
118 const canonicalCache = new Map (), segmentCache = new Map ();
119 function remember ( cache , key , value ) { if (cache.size >= 50000 ) cache. clear (); cache. set (key, value); return value; }
120 const canonicalize = text => ( typeof text === 'string' && canonicalCache. get (text)) || remember (canonicalCache, text, format ( parse (text)));
121 const segmentsOf = text => segmentCache. get (text) || remember (segmentCache, text, parse (text));
122 function isCanonical ( text ) {
123 try { return canonicalize (text) === text; } catch { return false ; }
124 }
125 // Strict exact location: canonical and without legacy numeric indexes.
126 const isExact = text => isCanonical (text);
127 const segEqual = ( a , b ) => format ([a]) === format ([b]) && Boolean (a.key !== undefined ) === Boolean (b.key !== undefined );
128 function isPrefix ( prefix , location , { strict = false } = {}) {
129 const p = typeof prefix === 'string' ? parse (prefix) : prefix, l = typeof location === 'string' ? parse (location) : location;
130 if (p. length > l. length || (strict && p. length === l. length )) return false ;
131 return p. every (( s , i ) => segEqual (s, l[i]));
132 }
133 // isPrefix for untrusted input: an unparseable location is never within another.
134 function within ( prefix , location , { strict = false } = {}) {
135 try { return isPrefix (prefix, location, { strict }); } catch { return false ; }
136 }
137 function join ( ... parts ) {
138 return format (parts. flatMap ( p => typeof p === 'string' ? parse (p) : p));
139 }
140 // The unselected form: every selector segment becomes [].
141 const base = location => format ( parse (location). map ( s => s.where ? { each: true } : s));
142 function ownGet ( v , key ) {
143 return v && typeof v === 'object' && ! Array. isArray (v) && Object. hasOwn (v, key) ? v[key] : undefined ;
144 }
145 function relativeValue ( element , path ) {
146 let v = element;
147 for ( const s of path) v = ownGet (v, s.key);
148 return v;
149 }
150 const scalar = v => typeof v === 'string' || ( typeof v === 'number' && Number. isFinite (v));
151 function valuesAt ( value , location ) {
152 const segments = typeof location === 'string' ? segmentsOf (location) : location;
153 let values = [value];
154 for ( const s of segments) {
155 if (s.key !== undefined ) values = values. map ( v => ownGet (v, s.key));
156 else values = values. flatMap ( v => Array. isArray (v) ? v. filter ( e => ! s.where || relativeValue (e, s.where.path) === s.where.equals) : []);
157 }
158 return values;
159 }
160 const populated = v => v !== undefined && v !== null && v !== '' && typeof v !== 'function' &&
161 ! ( typeof v === 'object' && Object. keys (v). length === 0 );
162 // A selector template from a dynamic coverage entry, e.g.
163 // {path:'meta_data[].value', selector:{keyPath:'meta_data[].key'}}
164 // array = meta_data[], key = key, value = value. Anything else fails closed.
165 function template ( entry ) {
166 const path = parse (entry?.path), keyPath = parse (entry?.selector?.keyPath);
167 let last = - 1 ;
168 for ( let i = 0 ; i < Math. min (path. length , keyPath. length ); i += 1 ) {
169 if ( ! segEqual (path[i], keyPath[i])) break ;
170 if (path[i].each && ! path[i].where) last = i;
171 }
172 if (last < 0 ) fail ( 'selector keyPath must share the dynamic array with its path' );
173 const value = path. slice (last + 1 ), key = keyPath. slice (last + 1 );
174 if (value. length !== 1 || value[ 0 ].key === undefined || ! key. length || key. some ( s => s.key === undefined )) fail ( 'unsupported selector template' );
175 if (path. slice ( 0 , last + 1 ). some ( s => s.where)) fail ( 'unsupported nested selector template' );
176 return { array: format (path. slice ( 0 , last + 1 )), arraySegments: path. slice ( 0 , last + 1 ), key, value: value[ 0 ].key, path: format (path), keyPath: format (keyPath) };
177 }
178 function selected ( t , keyValue , rest = []) {
179 return format ([ ... t.arraySegments. slice ( 0 , - 1 ), { each: true , where: { path: t.key, equals: keyValue } }, { key: t.value }, ... rest]);
180 }
181 // Populated leaf locations of one record. Elements of a templated array whose discriminator is a
182 // scalar have their value subtree qualified by that key; other elements stay at the base path.
183 function leafLocations ( record , { templates = [] } = {}, out = new Set ()) {
184 const byArray = new Map (templates. map ( t => [t.array, t]));
185 function walk ( v , segs ) {
186 if ( ! populated (v)) return ;
187 if (Array. isArray (v)) {
188 const each = [ ... segs, { each: true }], t = byArray. get ( format (each));
189 for ( const item of v) {
190 const k = t ? relativeValue (item, t.key) : undefined ;
191 if (t && scalar (k) && item && typeof item === 'object' && ! Array. isArray (item)) {
192 for ( const [ name , child ] of Object. entries (item)) {
193 walk (child, name === t.value ? [ ... segs, { each: true , where: { path: t.key, equals: k } }, { key: name }] : [ ... each, { key: name }]);
194 }
195 } else walk (item, each);
196 }
197 } else if ( typeof v === 'object' ) {
198 for ( const [ k , child ] of Object. entries (v)) walk (child, [ ... segs, { key: k }]);
199 } else if (segs. length ) out. add ( format (segs));
200 }
201 walk (record, []);
202 return out;
203 }
204 // Human-readable form for reviews: the canonical string is already plain text; selectors are
205 // spelled out so a reviewer sees which metadata key a row belongs to.
206 function readable ( location ) {
207 return parse (location). map (( s , i ) => {
208 if (s.key !== undefined ) return formatKey (s.key, i === 0 );
209 if ( ! s.where) return '[]' ;
210 return `[where ${ s . where . path . map (( p , j ) => formatKey ( p . key , j === 0 )). join ( '' ) } = ${ formatScalar ( s . where . equals ) }]` ;
211 }). join ( '' );
212 }
213 module . exports = { ENCODING, parse, format, canonicalize, isCanonical, isExact, isPrefix, within, join, base, valuesAt, populated,
214 template, selected, leafLocations, readable, scalar };