@malloydata/malloy-tag 0.0.335 → 0.0.337
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/CONTEXT.md +83 -9
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/peggy/dist/peg-tag-parser.d.ts +11 -0
- package/dist/peggy/dist/peg-tag-parser.js +3130 -0
- package/dist/peggy/dist/peg-tag-parser.js.map +1 -0
- package/dist/peggy/index.d.ts +13 -0
- package/dist/peggy/index.js +117 -0
- package/dist/peggy/index.js.map +1 -0
- package/dist/peggy/interpreter.d.ts +32 -0
- package/dist/peggy/interpreter.js +208 -0
- package/dist/peggy/interpreter.js.map +1 -0
- package/dist/peggy/statements.d.ts +51 -0
- package/dist/peggy/statements.js +7 -0
- package/dist/peggy/statements.js.map +1 -0
- package/dist/schema.d.ts +41 -0
- package/dist/schema.js +573 -0
- package/dist/schema.js.map +1 -0
- package/dist/schema.spec.d.ts +1 -0
- package/dist/schema.spec.js +980 -0
- package/dist/schema.spec.js.map +1 -0
- package/dist/tags.d.ts +144 -37
- package/dist/tags.js +535 -344
- package/dist/tags.js.map +1 -1
- package/dist/tags.spec.js +524 -45
- package/dist/tags.spec.js.map +1 -1
- package/package.json +6 -8
- package/src/index.ts +3 -0
- package/src/motly-schema.motly +52 -0
- package/src/peggy/dist/peg-tag-parser.js +2790 -0
- package/src/peggy/index.ts +89 -0
- package/src/peggy/interpreter.ts +265 -0
- package/src/peggy/malloy-tag.peggy +224 -0
- package/src/peggy/statements.ts +49 -0
- package/src/schema.spec.ts +1280 -0
- package/src/schema.ts +852 -0
- package/src/tags.spec.ts +591 -46
- package/src/tags.ts +597 -398
- package/tsconfig.json +3 -2
- package/dist/lib/Malloy/MalloyTagLexer.d.ts +0 -42
- package/dist/lib/Malloy/MalloyTagLexer.js +0 -395
- package/dist/lib/Malloy/MalloyTagLexer.js.map +0 -1
- package/dist/lib/Malloy/MalloyTagParser.d.ts +0 -180
- package/dist/lib/Malloy/MalloyTagParser.js +0 -1077
- package/dist/lib/Malloy/MalloyTagParser.js.map +0 -1
- package/dist/lib/Malloy/MalloyTagVisitor.d.ts +0 -120
- package/dist/lib/Malloy/MalloyTagVisitor.js +0 -4
- package/dist/lib/Malloy/MalloyTagVisitor.js.map +0 -1
- package/scripts/build_parser.js +0 -98
- package/src/MalloyTag.g4 +0 -104
- package/src/lib/Malloy/MalloyTag.interp +0 -61
- package/src/lib/Malloy/MalloyTag.tokens +0 -32
- package/src/lib/Malloy/MalloyTagLexer.interp +0 -85
- package/src/lib/Malloy/MalloyTagLexer.tokens +0 -32
- package/src/lib/Malloy/MalloyTagLexer.ts +0 -386
- package/src/lib/Malloy/MalloyTagParser.ts +0 -1065
- package/src/lib/Malloy/MalloyTagVisitor.ts +0 -141
- package/src/lib/Malloy/_BUILD_DIGEST_ +0 -1
package/src/tags.spec.ts
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import type {TagDict} from './tags';
|
|
25
|
-
import {Tag} from './tags';
|
|
25
|
+
import {Tag, RefTag, interfaceFromDict} from './tags';
|
|
26
|
+
import {parseTag} from './peggy';
|
|
26
27
|
|
|
27
28
|
declare global {
|
|
28
29
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
@@ -36,7 +37,7 @@ declare global {
|
|
|
36
37
|
expect.extend({
|
|
37
38
|
tagsAre(src: string | Tag, result: Tag) {
|
|
38
39
|
if (typeof src === 'string') {
|
|
39
|
-
const {tag, log} =
|
|
40
|
+
const {tag, log} = parseTag(src);
|
|
40
41
|
const errs = log.map(e => e.message);
|
|
41
42
|
if (log.length > 0) {
|
|
42
43
|
return {
|
|
@@ -46,7 +47,7 @@ expect.extend({
|
|
|
46
47
|
}
|
|
47
48
|
src = tag;
|
|
48
49
|
}
|
|
49
|
-
const got = src.properties;
|
|
50
|
+
const got = src.properties ? interfaceFromDict(src.properties) : undefined;
|
|
50
51
|
if (this.equals(got, result)) {
|
|
51
52
|
return {
|
|
52
53
|
pass: true,
|
|
@@ -111,26 +112,19 @@ describe('tagParse to Tag', () => {
|
|
|
111
112
|
|
|
112
113
|
['x={y z} -x.y', {x: {properties: {z: {}, y: {deleted: true}}}}],
|
|
113
114
|
['x={y z} x {-y}', {x: {properties: {z: {}, y: {deleted: true}}}}],
|
|
114
|
-
['x=1 x {xx=11}', {x: {eq:
|
|
115
|
-
['x.y=xx x=1 {...}', {x: {eq:
|
|
116
|
-
['a {b c} a=1', {a: {eq:
|
|
117
|
-
['a=1 a=...{b}', {a: {eq:
|
|
118
|
-
[
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
],
|
|
126
|
-
['x=.01', {x: {eq: '.01'}}],
|
|
127
|
-
['x=-7', {x: {eq: '-7'}}],
|
|
128
|
-
['x=7', {x: {eq: '7'}}],
|
|
129
|
-
['x=7.0', {x: {eq: '7.0'}}],
|
|
130
|
-
['x=.7', {x: {eq: '.7'}}],
|
|
131
|
-
['x=.7e2', {x: {eq: '.7e2'}}],
|
|
132
|
-
['x=7E2', {x: {eq: '7E2'}}],
|
|
115
|
+
['x=1 x {xx=11}', {x: {eq: 1, properties: {xx: {eq: 11}}}}],
|
|
116
|
+
['x.y=xx x=1 {...}', {x: {eq: 1, properties: {y: {eq: 'xx'}}}}],
|
|
117
|
+
['a {b c} a=1', {a: {eq: 1}}],
|
|
118
|
+
['a=1 a=...{b}', {a: {eq: 1, properties: {b: {}}}}],
|
|
119
|
+
['x=.01', {x: {eq: 0.01}}],
|
|
120
|
+
['x=-7', {x: {eq: -7}}],
|
|
121
|
+
['x=7', {x: {eq: 7}}],
|
|
122
|
+
['x=7.0', {x: {eq: 7.0}}],
|
|
123
|
+
['x=.7', {x: {eq: 0.7}}],
|
|
124
|
+
['x=.7e2', {x: {eq: 70}}],
|
|
125
|
+
['x=7E2', {x: {eq: 700}}],
|
|
133
126
|
['`spacey name`=Zaphod', {'spacey name': {eq: 'Zaphod'}}],
|
|
127
|
+
["name='single quoted'", {name: {eq: 'single quoted'}}],
|
|
134
128
|
[
|
|
135
129
|
'image { alt=hello { field=department } }',
|
|
136
130
|
{
|
|
@@ -152,6 +146,57 @@ describe('tagParse to Tag', () => {
|
|
|
152
146
|
},
|
|
153
147
|
],
|
|
154
148
|
['can remove.properties -...', {}],
|
|
149
|
+
// Colon syntax REPLACES properties (deletes old props)
|
|
150
|
+
['name: { prop }', {name: {properties: {prop: {}}}}],
|
|
151
|
+
['name: { a=1 b=2 }', {name: {properties: {a: {eq: 1}, b: {eq: 2}}}}],
|
|
152
|
+
['name { old } name: { new }', {name: {properties: {new: {}}}}],
|
|
153
|
+
// Space syntax MERGES properties (keeps old props)
|
|
154
|
+
['name { old } name { new }', {name: {properties: {old: {}, new: {}}}}],
|
|
155
|
+
// Colon and space syntax with dotted paths
|
|
156
|
+
['a.b: { c }', {a: {properties: {b: {properties: {c: {}}}}}}],
|
|
157
|
+
[
|
|
158
|
+
'a.b { c } a.b { d }',
|
|
159
|
+
{a: {properties: {b: {properties: {c: {}, d: {}}}}}},
|
|
160
|
+
],
|
|
161
|
+
['a.b { c } a.b: { d }', {a: {properties: {b: {properties: {d: {}}}}}}],
|
|
162
|
+
// Colon syntax deletes existing value
|
|
163
|
+
['name=val name: { prop }', {name: {properties: {prop: {}}}}],
|
|
164
|
+
// Space syntax preserves existing value
|
|
165
|
+
['name=val name { prop }', {name: {eq: 'val', properties: {prop: {}}}}],
|
|
166
|
+
// Multi-line input
|
|
167
|
+
[
|
|
168
|
+
'person {\n name="ted"\n age=42\n}',
|
|
169
|
+
{person: {properties: {name: {eq: 'ted'}, age: {eq: 42}}}},
|
|
170
|
+
],
|
|
171
|
+
// Triple-quoted strings (multi-line values)
|
|
172
|
+
['desc="""hello"""', {desc: {eq: 'hello'}}],
|
|
173
|
+
['desc="""line one\nline two"""', {desc: {eq: 'line one\nline two'}}],
|
|
174
|
+
['desc="""has " quote"""', {desc: {eq: 'has " quote'}}],
|
|
175
|
+
['desc="""has "" two quotes"""', {desc: {eq: 'has "" two quotes'}}],
|
|
176
|
+
// Boolean values
|
|
177
|
+
['enabled=@true', {enabled: {eq: true}}],
|
|
178
|
+
['disabled=@false', {disabled: {eq: false}}],
|
|
179
|
+
// Date values
|
|
180
|
+
['created=@2024-01-15', {created: {eq: new Date('2024-01-15')}}],
|
|
181
|
+
[
|
|
182
|
+
'updated=@2024-01-15T10:30:00Z',
|
|
183
|
+
{updated: {eq: new Date('2024-01-15T10:30:00Z')}},
|
|
184
|
+
],
|
|
185
|
+
// Mixed types
|
|
186
|
+
[
|
|
187
|
+
'config { enabled=@true count=42 name=test }',
|
|
188
|
+
{
|
|
189
|
+
config: {
|
|
190
|
+
properties: {
|
|
191
|
+
enabled: {eq: true},
|
|
192
|
+
count: {eq: 42},
|
|
193
|
+
name: {eq: 'test'},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
// Arrays with typed values
|
|
199
|
+
['flags=[@true, @false]', {flags: {eq: [{eq: true}, {eq: false}]}}],
|
|
155
200
|
];
|
|
156
201
|
test.each(tagTests)('tag %s', (expression: string, expected: TagDict) => {
|
|
157
202
|
expect(expression).tagsAre(expected);
|
|
@@ -165,7 +210,7 @@ describe('tagParse to Tag', () => {
|
|
|
165
210
|
describe('Tag access', () => {
|
|
166
211
|
test('just text', () => {
|
|
167
212
|
const strToParse = 'a=b';
|
|
168
|
-
const getTags =
|
|
213
|
+
const getTags = parseTag(strToParse);
|
|
169
214
|
expect(getTags.log).toEqual([]);
|
|
170
215
|
const a = getTags.tag.tag('a');
|
|
171
216
|
expect(a).toBeDefined();
|
|
@@ -173,7 +218,7 @@ describe('Tag access', () => {
|
|
|
173
218
|
});
|
|
174
219
|
test('tag path', () => {
|
|
175
220
|
const strToParse = 'a.b.c.d.e=f';
|
|
176
|
-
const tagParse =
|
|
221
|
+
const tagParse = parseTag(strToParse);
|
|
177
222
|
expect(tagParse.log).toEqual([]);
|
|
178
223
|
const abcde = tagParse.tag.tag('a', 'b', 'c', 'd', 'e');
|
|
179
224
|
expect(abcde).toBeDefined();
|
|
@@ -181,7 +226,7 @@ describe('Tag access', () => {
|
|
|
181
226
|
});
|
|
182
227
|
test('just array', () => {
|
|
183
228
|
const strToParse = 'a=[b]';
|
|
184
|
-
const getTags =
|
|
229
|
+
const getTags = parseTag(strToParse);
|
|
185
230
|
expect(getTags.log).toEqual([]);
|
|
186
231
|
const a = getTags.tag.tag('a');
|
|
187
232
|
const aval = a?.array();
|
|
@@ -193,7 +238,7 @@ describe('Tag access', () => {
|
|
|
193
238
|
});
|
|
194
239
|
test('tag path into array', () => {
|
|
195
240
|
const strToParse = 'a.b.c = [{d=e}]';
|
|
196
|
-
const tagParse =
|
|
241
|
+
const tagParse = parseTag(strToParse);
|
|
197
242
|
expect(tagParse.log).toEqual([]);
|
|
198
243
|
const abcde = tagParse.tag.tag('a', 'b', 'c', 0, 'd');
|
|
199
244
|
expect(abcde).toBeDefined();
|
|
@@ -201,7 +246,7 @@ describe('Tag access', () => {
|
|
|
201
246
|
});
|
|
202
247
|
test('array as text', () => {
|
|
203
248
|
const strToParse = 'a=[b]';
|
|
204
|
-
const getTags =
|
|
249
|
+
const getTags = parseTag(strToParse);
|
|
205
250
|
expect(getTags.log).toEqual([]);
|
|
206
251
|
const a = getTags.tag.tag('a');
|
|
207
252
|
expect(a).toBeDefined();
|
|
@@ -209,7 +254,7 @@ describe('Tag access', () => {
|
|
|
209
254
|
});
|
|
210
255
|
test('text as array', () => {
|
|
211
256
|
const strToParse = 'a=b';
|
|
212
|
-
const getTags =
|
|
257
|
+
const getTags = parseTag(strToParse);
|
|
213
258
|
expect(getTags.log).toEqual([]);
|
|
214
259
|
const a = getTags.tag.tag('a');
|
|
215
260
|
expect(a).toBeDefined();
|
|
@@ -217,7 +262,7 @@ describe('Tag access', () => {
|
|
|
217
262
|
});
|
|
218
263
|
test('just numeric', () => {
|
|
219
264
|
const strToParse = 'a=7';
|
|
220
|
-
const getTags =
|
|
265
|
+
const getTags = parseTag(strToParse);
|
|
221
266
|
expect(getTags.log).toEqual([]);
|
|
222
267
|
const a = getTags.tag.tag('a');
|
|
223
268
|
expect(a).toBeDefined();
|
|
@@ -227,7 +272,7 @@ describe('Tag access', () => {
|
|
|
227
272
|
});
|
|
228
273
|
test('text as numeric', () => {
|
|
229
274
|
const strToParse = 'a=seven';
|
|
230
|
-
const getTags =
|
|
275
|
+
const getTags = parseTag(strToParse);
|
|
231
276
|
expect(getTags.log).toEqual([]);
|
|
232
277
|
const a = getTags.tag.tag('a');
|
|
233
278
|
expect(a).toBeDefined();
|
|
@@ -236,7 +281,7 @@ describe('Tag access', () => {
|
|
|
236
281
|
});
|
|
237
282
|
test('array as numeric', () => {
|
|
238
283
|
const strToParse = 'a=[seven]';
|
|
239
|
-
const getTags =
|
|
284
|
+
const getTags = parseTag(strToParse);
|
|
240
285
|
expect(getTags.log).toEqual([]);
|
|
241
286
|
const a = getTags.tag.tag('a');
|
|
242
287
|
expect(a).toBeDefined();
|
|
@@ -245,7 +290,7 @@ describe('Tag access', () => {
|
|
|
245
290
|
});
|
|
246
291
|
test('full text array', () => {
|
|
247
292
|
const strToParse = 'a=[b,c]';
|
|
248
|
-
const getTags =
|
|
293
|
+
const getTags = parseTag(strToParse);
|
|
249
294
|
expect(getTags.log).toEqual([]);
|
|
250
295
|
const a = getTags.tag.tag('a');
|
|
251
296
|
expect(a).toBeDefined();
|
|
@@ -254,7 +299,7 @@ describe('Tag access', () => {
|
|
|
254
299
|
});
|
|
255
300
|
test('filtered text array', () => {
|
|
256
301
|
const strToParse = 'a=[b,c,{d}]';
|
|
257
|
-
const getTags =
|
|
302
|
+
const getTags = parseTag(strToParse);
|
|
258
303
|
expect(getTags.log).toEqual([]);
|
|
259
304
|
const a = getTags.tag.tag('a');
|
|
260
305
|
expect(a).toBeDefined();
|
|
@@ -263,7 +308,7 @@ describe('Tag access', () => {
|
|
|
263
308
|
});
|
|
264
309
|
test('full numeric array', () => {
|
|
265
310
|
const strToParse = 'a=[1,2]';
|
|
266
|
-
const getTags =
|
|
311
|
+
const getTags = parseTag(strToParse);
|
|
267
312
|
expect(getTags.log).toEqual([]);
|
|
268
313
|
const a = getTags.tag.tag('a');
|
|
269
314
|
expect(a).toBeDefined();
|
|
@@ -272,7 +317,7 @@ describe('Tag access', () => {
|
|
|
272
317
|
});
|
|
273
318
|
test('filtered numeric array', () => {
|
|
274
319
|
const strToParse = 'a=[1,2,three]';
|
|
275
|
-
const getTags =
|
|
320
|
+
const getTags = parseTag(strToParse);
|
|
276
321
|
expect(getTags.log).toEqual([]);
|
|
277
322
|
const a = getTags.tag.tag('a');
|
|
278
323
|
expect(a).toBeDefined();
|
|
@@ -281,15 +326,51 @@ describe('Tag access', () => {
|
|
|
281
326
|
});
|
|
282
327
|
test('has', () => {
|
|
283
328
|
const strToParse = 'a b.d';
|
|
284
|
-
const getTags =
|
|
329
|
+
const getTags = parseTag(strToParse);
|
|
285
330
|
expect(getTags.log).toEqual([]);
|
|
286
331
|
expect(getTags.tag.has('a')).toBeTruthy();
|
|
287
332
|
expect(getTags.tag.has('b', 'd')).toBeTruthy();
|
|
288
333
|
expect(getTags.tag.has('c')).toBeFalsy();
|
|
289
334
|
});
|
|
335
|
+
test('boolean accessor', () => {
|
|
336
|
+
const {tag} = parseTag('enabled=@true disabled=@false');
|
|
337
|
+
expect(tag.boolean('enabled')).toBe(true);
|
|
338
|
+
expect(tag.boolean('disabled')).toBe(false);
|
|
339
|
+
expect(tag.boolean('missing')).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
test('isTrue and isFalse', () => {
|
|
342
|
+
const {tag} = parseTag('enabled=@true disabled=@false name=test');
|
|
343
|
+
expect(tag.isTrue('enabled')).toBe(true);
|
|
344
|
+
expect(tag.isFalse('enabled')).toBe(false);
|
|
345
|
+
expect(tag.isTrue('disabled')).toBe(false);
|
|
346
|
+
expect(tag.isFalse('disabled')).toBe(true);
|
|
347
|
+
// Non-boolean values
|
|
348
|
+
expect(tag.isTrue('name')).toBe(false);
|
|
349
|
+
expect(tag.isFalse('name')).toBe(false);
|
|
350
|
+
// Missing values
|
|
351
|
+
expect(tag.isTrue('missing')).toBe(false);
|
|
352
|
+
expect(tag.isFalse('missing')).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
test('date accessor', () => {
|
|
355
|
+
const {tag} = parseTag('created=@2024-01-15 updated=@2024-01-15T10:30:00Z');
|
|
356
|
+
const created = tag.date('created');
|
|
357
|
+
expect(created).toBeInstanceOf(Date);
|
|
358
|
+
expect(created?.toISOString()).toBe('2024-01-15T00:00:00.000Z');
|
|
359
|
+
const updated = tag.date('updated');
|
|
360
|
+
expect(updated).toBeInstanceOf(Date);
|
|
361
|
+
expect(updated?.toISOString()).toBe('2024-01-15T10:30:00.000Z');
|
|
362
|
+
expect(tag.date('missing')).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
test('text returns string for all scalar types', () => {
|
|
365
|
+
const {tag} = parseTag('n=42 b=@true d=@2024-01-15 s=hello');
|
|
366
|
+
expect(tag.text('n')).toBe('42');
|
|
367
|
+
expect(tag.text('b')).toBe('true');
|
|
368
|
+
expect(tag.text('d')).toBe('2024-01-15T00:00:00.000Z');
|
|
369
|
+
expect(tag.text('s')).toBe('hello');
|
|
370
|
+
});
|
|
290
371
|
test('property access on existing tag (which does not yet have properties)', () => {
|
|
291
|
-
const parsePlot =
|
|
292
|
-
const parsed =
|
|
372
|
+
const parsePlot = parseTag('# plot');
|
|
373
|
+
const parsed = parseTag('# plot.x=2', parsePlot.tag);
|
|
293
374
|
const allTags = parsed.tag;
|
|
294
375
|
const plotTag = allTags.tag('plot');
|
|
295
376
|
const xTag = plotTag!.tag('x');
|
|
@@ -302,7 +383,7 @@ describe('Tag access', () => {
|
|
|
302
383
|
const base = Tag.withPrefix('# ');
|
|
303
384
|
const ext = base.set(['a', 'b', 0], 3).set(['a', 'b', 1], 4);
|
|
304
385
|
expect(ext).tagsAre({
|
|
305
|
-
a: {properties: {b: {eq: [{eq:
|
|
386
|
+
a: {properties: {b: {eq: [{eq: 3}, {eq: 4}]}}},
|
|
306
387
|
});
|
|
307
388
|
expect(ext.toString()).toBe('# a.b = [3, 4]\n');
|
|
308
389
|
});
|
|
@@ -313,13 +394,13 @@ describe('Tag access', () => {
|
|
|
313
394
|
.set(['c', 0], 'foo')
|
|
314
395
|
.set(['c', 0, 'a'], 4);
|
|
315
396
|
expect(ext).tagsAre({
|
|
316
|
-
a: {properties: {b: {eq: [{properties: {a: {eq:
|
|
317
|
-
c: {eq: [{eq: 'foo', properties: {a: {eq:
|
|
397
|
+
a: {properties: {b: {eq: [{properties: {a: {eq: 3}}}]}}},
|
|
398
|
+
c: {eq: [{eq: 'foo', properties: {a: {eq: 4}}}]},
|
|
318
399
|
});
|
|
319
400
|
expect(ext.toString()).toBe('# a.b = [{ a = 3 }] c = [foo { a = 4 }]\n');
|
|
320
401
|
});
|
|
321
402
|
test('soft remove', () => {
|
|
322
|
-
const base =
|
|
403
|
+
const base = parseTag('# a.b.c = [{ d = 1 }]').tag;
|
|
323
404
|
const ext = base.delete('a', 'b', 'c', 0, 'd').delete('a', 'b', 'c', 0);
|
|
324
405
|
expect(ext).tagsAre({
|
|
325
406
|
a: {properties: {b: {properties: {c: {eq: []}}}}},
|
|
@@ -327,7 +408,7 @@ describe('Tag access', () => {
|
|
|
327
408
|
expect(ext.toString()).toBe('# a.b.c = []\n');
|
|
328
409
|
});
|
|
329
410
|
test('hard remove', () => {
|
|
330
|
-
const base =
|
|
411
|
+
const base = parseTag('# hello').tag;
|
|
331
412
|
const ext = base.unset('goodbye').unset('a', 'dieu');
|
|
332
413
|
expect(ext).tagsAre({
|
|
333
414
|
hello: {},
|
|
@@ -384,7 +465,7 @@ describe('Tag access', () => {
|
|
|
384
465
|
const base = Tag.withPrefix('#(malloy) ');
|
|
385
466
|
const ext = base.set(['value'], '\n');
|
|
386
467
|
expect(ext.toString()).toBe('#(malloy) value = "\\n"\n');
|
|
387
|
-
expect(
|
|
468
|
+
expect(ext.text('value')).toBe('\n');
|
|
388
469
|
idempotent(ext);
|
|
389
470
|
});
|
|
390
471
|
test('value has a double quote', () => {
|
|
@@ -393,7 +474,7 @@ describe('Tag access', () => {
|
|
|
393
474
|
expect(ext.toString()).toBe('#(malloy) value = "\\""\n');
|
|
394
475
|
idempotent(ext);
|
|
395
476
|
});
|
|
396
|
-
test
|
|
477
|
+
test('value is empty string', () => {
|
|
397
478
|
const base = Tag.withPrefix('#(malloy) ');
|
|
398
479
|
const ext = base.set(['value'], '');
|
|
399
480
|
expect(ext.toString()).toBe('#(malloy) value = ""\n');
|
|
@@ -412,11 +493,475 @@ describe('Tag access', () => {
|
|
|
412
493
|
idempotent(ext);
|
|
413
494
|
});
|
|
414
495
|
});
|
|
496
|
+
describe('parsing escape sequences in strings', () => {
|
|
497
|
+
test('\\n becomes newline', () => {
|
|
498
|
+
const {tag} = parseTag('x="hello\\nworld"');
|
|
499
|
+
expect(tag.text('x')).toBe('hello\nworld');
|
|
500
|
+
});
|
|
501
|
+
test('\\t becomes tab', () => {
|
|
502
|
+
const {tag} = parseTag('x="hello\\tworld"');
|
|
503
|
+
expect(tag.text('x')).toBe('hello\tworld');
|
|
504
|
+
});
|
|
505
|
+
test('\\r becomes carriage return', () => {
|
|
506
|
+
const {tag} = parseTag('x="hello\\rworld"');
|
|
507
|
+
expect(tag.text('x')).toBe('hello\rworld');
|
|
508
|
+
});
|
|
509
|
+
test('\\b becomes backspace', () => {
|
|
510
|
+
const {tag} = parseTag('x="hello\\bworld"');
|
|
511
|
+
expect(tag.text('x')).toBe('hello\bworld');
|
|
512
|
+
});
|
|
513
|
+
test('\\f becomes form feed', () => {
|
|
514
|
+
const {tag} = parseTag('x="hello\\fworld"');
|
|
515
|
+
expect(tag.text('x')).toBe('hello\fworld');
|
|
516
|
+
});
|
|
517
|
+
test('\\uXXXX becomes unicode character', () => {
|
|
518
|
+
const {tag} = parseTag('x="hello\\u0026world"');
|
|
519
|
+
expect(tag.text('x')).toBe('hello&world');
|
|
520
|
+
});
|
|
521
|
+
test('\\uXXXX with uppercase hex', () => {
|
|
522
|
+
const {tag} = parseTag('x="\\u003F"');
|
|
523
|
+
expect(tag.text('x')).toBe('?');
|
|
524
|
+
});
|
|
525
|
+
test('\\\\ becomes backslash', () => {
|
|
526
|
+
const {tag} = parseTag('x="hello\\\\world"');
|
|
527
|
+
expect(tag.text('x')).toBe('hello\\world');
|
|
528
|
+
});
|
|
529
|
+
test('\\" becomes double quote', () => {
|
|
530
|
+
const {tag} = parseTag('x="hello\\"world"');
|
|
531
|
+
expect(tag.text('x')).toBe('hello"world');
|
|
532
|
+
});
|
|
533
|
+
test("\\' in single quoted string", () => {
|
|
534
|
+
const {tag} = parseTag("x='hello\\'world'");
|
|
535
|
+
expect(tag.text('x')).toBe("hello'world");
|
|
536
|
+
});
|
|
537
|
+
test('\\` in backtick identifier', () => {
|
|
538
|
+
const {tag} = parseTag('`hello\\`world`=value');
|
|
539
|
+
expect(tag.text('hello`world')).toBe('value');
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe('Tag prefix handling', () => {
|
|
545
|
+
test('# prefix skips to first space', () => {
|
|
546
|
+
const {tag, log} = parseTag('# name=value');
|
|
547
|
+
expect(log).toEqual([]);
|
|
548
|
+
expect(tag.text('name')).toEqual('value');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('#(docs) prefix skips to first space', () => {
|
|
552
|
+
const {tag, log} = parseTag('#(docs) name=value');
|
|
553
|
+
expect(log).toEqual([]);
|
|
554
|
+
expect(tag.text('name')).toEqual('value');
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test('# with no space returns empty tag', () => {
|
|
558
|
+
const {tag, log} = parseTag('#noSpace');
|
|
559
|
+
expect(log).toEqual([]);
|
|
560
|
+
expect(tag.properties).toBeUndefined();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('everything after # on same line is ignored (comment behavior)', () => {
|
|
564
|
+
// When parsing a single tag line, # at start means "skip prefix"
|
|
565
|
+
// The rest of the line after the space is the tag content
|
|
566
|
+
const {tag, log} = parseTag('# name=value # this is not a comment');
|
|
567
|
+
expect(log).toEqual([]);
|
|
568
|
+
// The "# this is not a comment" is parsed as tag content, not ignored
|
|
569
|
+
// because single-line parsing doesn't have comment support
|
|
570
|
+
expect(tag.has('name')).toBe(true);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe('Empty and whitespace input', () => {
|
|
575
|
+
test('empty string produces empty tag', () => {
|
|
576
|
+
const {tag, log} = parseTag('');
|
|
577
|
+
expect(log).toEqual([]);
|
|
578
|
+
expect(tag.properties).toBeUndefined();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('whitespace only produces empty tag', () => {
|
|
582
|
+
const {tag, log} = parseTag(' ');
|
|
583
|
+
expect(log).toEqual([]);
|
|
584
|
+
expect(tag.properties).toBeUndefined();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('whitespace with comment produces empty tag', () => {
|
|
588
|
+
const {tag, log} = parseTag(' # this is a comment');
|
|
589
|
+
expect(log).toEqual([]);
|
|
590
|
+
expect(tag.properties).toBeUndefined();
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe('Error handling', () => {
|
|
595
|
+
test('syntax error has 0-based line and offset', () => {
|
|
596
|
+
const {log} = parseTag('a = [');
|
|
597
|
+
expect(log.length).toBe(1);
|
|
598
|
+
expect(log[0].code).toBe('tag-parse-syntax-error');
|
|
599
|
+
expect(log[0].line).toBe(0);
|
|
600
|
+
// Error at position 5 (after "a = [")
|
|
601
|
+
expect(log[0].offset).toBeGreaterThan(0);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test('error offset accounts for input position', () => {
|
|
605
|
+
const {log} = parseTag('valid another_valid oops=');
|
|
606
|
+
expect(log.length).toBe(1);
|
|
607
|
+
expect(log[0].line).toBe(0);
|
|
608
|
+
// Error should be near end of line
|
|
609
|
+
expect(log[0].offset).toBeGreaterThan(20);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('error offset after prefix stripping', () => {
|
|
613
|
+
// "# " is stripped, so input becomes " a = ["
|
|
614
|
+
const {log} = parseTag('# a = [');
|
|
615
|
+
expect(log.length).toBe(1);
|
|
616
|
+
expect(log[0].line).toBe(0);
|
|
617
|
+
// Offset is relative to stripped input (after "#")
|
|
618
|
+
expect(log[0].offset).toBeGreaterThan(0);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test('longer prefix is stripped correctly', () => {
|
|
622
|
+
// "#(docs) " is stripped
|
|
623
|
+
const {log} = parseTag('#(docs) a = [');
|
|
624
|
+
expect(log.length).toBe(1);
|
|
625
|
+
expect(log[0].line).toBe(0);
|
|
626
|
+
// Offset is relative to stripped input (after "#(docs)")
|
|
627
|
+
expect(log[0].offset).toBeGreaterThan(0);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test('error on second line reports correct line number', () => {
|
|
631
|
+
// Error is on line 1 (0-based), the unclosed bracket
|
|
632
|
+
const {log} = parseTag('valid=1\ninvalid=[');
|
|
633
|
+
expect(log.length).toBe(1);
|
|
634
|
+
expect(log[0].line).toBe(1);
|
|
635
|
+
expect(log[0].offset).toBeGreaterThan(0);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('unclosed string with newline produces error', () => {
|
|
639
|
+
// Regular strings cannot contain raw newlines - must close on same line
|
|
640
|
+
const {log} = parseTag('desc="forgot to close\n');
|
|
641
|
+
expect(log.length).toBe(1);
|
|
642
|
+
expect(log[0].line).toBe(0);
|
|
643
|
+
});
|
|
415
644
|
});
|
|
416
645
|
|
|
417
646
|
function idempotent(tag: Tag) {
|
|
418
647
|
const str = tag.toString();
|
|
419
|
-
const clone =
|
|
648
|
+
const clone = parseTag(str).tag;
|
|
420
649
|
clone.prefix = tag.prefix;
|
|
421
650
|
expect(clone.toString()).toBe(str);
|
|
422
651
|
}
|
|
652
|
+
|
|
653
|
+
describe('toObject', () => {
|
|
654
|
+
test('bare tag becomes true', () => {
|
|
655
|
+
const {tag} = parseTag('hidden');
|
|
656
|
+
expect(tag.toObject()).toEqual({hidden: true});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('string value becomes string', () => {
|
|
660
|
+
const {tag} = parseTag('color=blue');
|
|
661
|
+
expect(tag.toObject()).toEqual({color: 'blue'});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test('numeric value becomes number', () => {
|
|
665
|
+
const {tag} = parseTag('size=10');
|
|
666
|
+
expect(tag.toObject()).toEqual({size: 10});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test('float value becomes number', () => {
|
|
670
|
+
const {tag} = parseTag('ratio=3.14');
|
|
671
|
+
expect(tag.toObject()).toEqual({ratio: 3.14});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test('properties only becomes nested object', () => {
|
|
675
|
+
const {tag} = parseTag('box { width=100 height=200 }');
|
|
676
|
+
expect(tag.toObject()).toEqual({box: {width: 100, height: 200}});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test('value and properties uses = key', () => {
|
|
680
|
+
const {tag} = parseTag('link="http://example.com" { target=_blank }');
|
|
681
|
+
expect(tag.toObject()).toEqual({
|
|
682
|
+
link: {'=': 'http://example.com', 'target': '_blank'},
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test('array of simple values', () => {
|
|
687
|
+
const {tag} = parseTag('items=[a, b, c]');
|
|
688
|
+
expect(tag.toObject()).toEqual({items: ['a', 'b', 'c']});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test('array of numeric values', () => {
|
|
692
|
+
const {tag} = parseTag('nums=[1, 2, 3]');
|
|
693
|
+
expect(tag.toObject()).toEqual({nums: [1, 2, 3]});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('array with properties on elements', () => {
|
|
697
|
+
const {tag} = parseTag('items=[a { x=1 }, b { y=2 }]');
|
|
698
|
+
expect(tag.toObject()).toEqual({
|
|
699
|
+
items: [
|
|
700
|
+
{'=': 'a', 'x': 1},
|
|
701
|
+
{'=': 'b', 'y': 2},
|
|
702
|
+
],
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test('complex nested structure', () => {
|
|
707
|
+
const {tag} = parseTag('# hidden color=blue size=10 box { width=100 }');
|
|
708
|
+
expect(tag.toObject()).toEqual({
|
|
709
|
+
hidden: true,
|
|
710
|
+
color: 'blue',
|
|
711
|
+
size: 10,
|
|
712
|
+
box: {width: 100},
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test('deleted properties are excluded', () => {
|
|
717
|
+
const {tag} = parseTag('a b -a');
|
|
718
|
+
expect(tag.toObject()).toEqual({b: true});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('empty tag returns empty object', () => {
|
|
722
|
+
const {tag} = parseTag('');
|
|
723
|
+
expect(tag.toObject()).toEqual({});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test('deeply nested properties', () => {
|
|
727
|
+
const {tag} = parseTag('a.b.c=1');
|
|
728
|
+
expect(tag.toObject()).toEqual({a: {b: {c: 1}}});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test('array of objects (dictionaries)', () => {
|
|
732
|
+
const {tag} = parseTag('items=[{name=alice age=30}, {name=bob age=25}]');
|
|
733
|
+
expect(tag.toObject()).toEqual({
|
|
734
|
+
items: [
|
|
735
|
+
{name: 'alice', age: 30},
|
|
736
|
+
{name: 'bob', age: 25},
|
|
737
|
+
],
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
describe('Tag parent tracking', () => {
|
|
743
|
+
test('root tag has no parent', () => {
|
|
744
|
+
const {tag} = parseTag('a=1');
|
|
745
|
+
expect(tag.parent).toBeUndefined();
|
|
746
|
+
expect(tag.root).toBe(tag);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test('child tag has parent set', () => {
|
|
750
|
+
const {tag} = parseTag('a { b=1 }');
|
|
751
|
+
const a = tag.tag('a');
|
|
752
|
+
expect(a?.parent).toBe(tag);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test('nested child has correct parent chain', () => {
|
|
756
|
+
const {tag} = parseTag('a { b { c=1 } }');
|
|
757
|
+
const a = tag.tag('a');
|
|
758
|
+
const b = a?.tag('b');
|
|
759
|
+
const c = b?.tag('c');
|
|
760
|
+
|
|
761
|
+
expect(a?.parent).toBe(tag);
|
|
762
|
+
expect(b?.parent).toBe(a);
|
|
763
|
+
expect(c?.parent).toBe(b);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test('root traverses to top of tree', () => {
|
|
767
|
+
const {tag} = parseTag('a { b { c=1 } }');
|
|
768
|
+
const c = tag.tag('a', 'b', 'c');
|
|
769
|
+
|
|
770
|
+
expect(c?.root).toBe(tag);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test('array elements have parent set to containing tag', () => {
|
|
774
|
+
const {tag} = parseTag('items=[a, b, c]');
|
|
775
|
+
const items = tag.tag('items');
|
|
776
|
+
const arr = items?.array();
|
|
777
|
+
|
|
778
|
+
expect(arr?.[0].parent).toBe(items);
|
|
779
|
+
expect(arr?.[1].parent).toBe(items);
|
|
780
|
+
expect(arr?.[2].parent).toBe(items);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('nested array elements have correct parents', () => {
|
|
784
|
+
const {tag} = parseTag('items=[{name=alice}, {name=bob}]');
|
|
785
|
+
const items = tag.tag('items');
|
|
786
|
+
const arr = items?.array();
|
|
787
|
+
const alice = arr?.[0];
|
|
788
|
+
const name = alice?.tag('name');
|
|
789
|
+
|
|
790
|
+
expect(alice?.parent).toBe(items);
|
|
791
|
+
expect(name?.parent).toBe(alice);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test('dict accessor returns tags with correct parent', () => {
|
|
795
|
+
const {tag} = parseTag('a=1 b=2 c=3');
|
|
796
|
+
const dict = tag.dict;
|
|
797
|
+
|
|
798
|
+
expect(dict['a'].parent).toBe(tag);
|
|
799
|
+
expect(dict['b'].parent).toBe(tag);
|
|
800
|
+
expect(dict['c'].parent).toBe(tag);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test('entries iterator returns tags with correct parent', () => {
|
|
804
|
+
const {tag} = parseTag('a=1 b=2');
|
|
805
|
+
for (const [, child] of tag.entries()) {
|
|
806
|
+
expect(child.parent).toBe(tag);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
describe('References (RefTag)', () => {
|
|
812
|
+
test('absolute reference resolves to root property', () => {
|
|
813
|
+
const {tag} = parseTag('source=hello target=$source');
|
|
814
|
+
expect(tag.text('target')).toBe('hello');
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test('absolute reference with path resolves correctly', () => {
|
|
818
|
+
const {tag} = parseTag(
|
|
819
|
+
'config { db { host=localhost } } target=$config.db.host'
|
|
820
|
+
);
|
|
821
|
+
expect(tag.text('target')).toBe('localhost');
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test('relative reference up one level', () => {
|
|
825
|
+
const {tag} = parseTag('outer { value=42 inner { ref=$^value } }');
|
|
826
|
+
expect(tag.numeric('outer', 'inner', 'ref')).toBe(42);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test('relative reference up two levels', () => {
|
|
830
|
+
const {tag} = parseTag('root=hello outer { inner { ref=$^^root } }');
|
|
831
|
+
expect(tag.text('outer', 'inner', 'ref')).toBe('hello');
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test('reference with array index', () => {
|
|
835
|
+
const {tag} = parseTag('items=[first, second, third] target=$items[1]');
|
|
836
|
+
expect(tag.text('target')).toBe('second');
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test('reference in array', () => {
|
|
840
|
+
const {tag} = parseTag('source=value refs=[$source, $source]');
|
|
841
|
+
const arr = tag.textArray('refs');
|
|
842
|
+
expect(arr).toEqual(['value', 'value']);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test('unresolved reference returns undefined', () => {
|
|
846
|
+
const {tag} = parseTag('ref=$nonexistent');
|
|
847
|
+
expect(tag.text('ref')).toBeUndefined();
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test('RefTag.toRefString() returns source representation', () => {
|
|
851
|
+
const {tag} = parseTag('ref=$path.to.thing');
|
|
852
|
+
const ref = tag.tag('ref');
|
|
853
|
+
expect(ref).toBeInstanceOf(RefTag);
|
|
854
|
+
expect((ref as RefTag).toRefString()).toBe('$path.to.thing');
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
test('RefTag.toRefString() with ups', () => {
|
|
858
|
+
const {tag} = parseTag('a { ref=$^^root.path }');
|
|
859
|
+
const ref = tag.tag('a', 'ref');
|
|
860
|
+
expect(ref).toBeInstanceOf(RefTag);
|
|
861
|
+
expect((ref as RefTag).toRefString()).toBe('$^^root.path');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test('RefTag.toRefString() with array index', () => {
|
|
865
|
+
const {tag} = parseTag('ref=$items[0].name');
|
|
866
|
+
const ref = tag.tag('ref');
|
|
867
|
+
expect(ref).toBeInstanceOf(RefTag);
|
|
868
|
+
expect((ref as RefTag).toRefString()).toBe('$items[0].name');
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
test('chained reference access', () => {
|
|
872
|
+
const {tag} = parseTag('data { name=alice age=30 } ref=$data');
|
|
873
|
+
expect(tag.text('ref', 'name')).toBe('alice');
|
|
874
|
+
expect(tag.numeric('ref', 'age')).toBe(30);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
test('reference has correct parent', () => {
|
|
878
|
+
const {tag} = parseTag('outer { ref=$something }');
|
|
879
|
+
const outer = tag.tag('outer');
|
|
880
|
+
const ref = outer?.tag('ref');
|
|
881
|
+
expect(ref?.parent).toBe(outer);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
describe('validateReferences', () => {
|
|
885
|
+
test('no errors for valid references', () => {
|
|
886
|
+
const {tag} = parseTag('source=hello target=$source');
|
|
887
|
+
expect(tag.validateReferences()).toEqual([]);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test('error for unresolved reference', () => {
|
|
891
|
+
const {tag} = parseTag('ref=$nonexistent');
|
|
892
|
+
const errors = tag.validateReferences();
|
|
893
|
+
expect(errors).toHaveLength(1);
|
|
894
|
+
expect(errors[0]).toContain('Unresolved reference');
|
|
895
|
+
expect(errors[0]).toContain('$nonexistent');
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test('error for unresolved nested reference', () => {
|
|
899
|
+
const {tag} = parseTag('outer { inner { ref=$missing } }');
|
|
900
|
+
const errors = tag.validateReferences();
|
|
901
|
+
expect(errors).toHaveLength(1);
|
|
902
|
+
expect(errors[0]).toContain('outer.inner.ref');
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test('error for reference that goes up too far', () => {
|
|
906
|
+
const {tag} = parseTag('ref=$^^^^^way.too.far');
|
|
907
|
+
const errors = tag.validateReferences();
|
|
908
|
+
expect(errors).toHaveLength(1);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test('multiple unresolved references', () => {
|
|
912
|
+
const {tag} = parseTag('a=$missing1 b=$missing2');
|
|
913
|
+
const errors = tag.validateReferences();
|
|
914
|
+
expect(errors).toHaveLength(2);
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
describe('toJSON for references', () => {
|
|
919
|
+
test('RefTag serializes to linkTo marker', () => {
|
|
920
|
+
const {tag} = parseTag('ref=$path.to.thing');
|
|
921
|
+
const ref = tag.tag('ref');
|
|
922
|
+
expect(ref?.toJSON()).toEqual({linkTo: '$path.to.thing'});
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test('RefTag with ups serializes correctly', () => {
|
|
926
|
+
const {tag} = parseTag('a { ref=$^^root }');
|
|
927
|
+
const ref = tag.tag('a', 'ref');
|
|
928
|
+
expect(ref?.toJSON()).toEqual({linkTo: '$^^root'});
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
describe('toObject with references', () => {
|
|
933
|
+
test('reference resolves to actual value', () => {
|
|
934
|
+
const {tag} = parseTag('source=hello target=$source');
|
|
935
|
+
const obj = tag.toObject();
|
|
936
|
+
expect(obj['target']).toBe('hello');
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
test('reference to object resolves correctly', () => {
|
|
940
|
+
const {tag} = parseTag('data { name=alice } ref=$data');
|
|
941
|
+
const obj = tag.toObject();
|
|
942
|
+
expect(obj['ref']).toEqual({name: 'alice'});
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test('unresolved reference becomes undefined', () => {
|
|
946
|
+
const {tag} = parseTag('ref=$nonexistent');
|
|
947
|
+
const obj = tag.toObject();
|
|
948
|
+
expect(obj['ref']).toBeUndefined();
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
describe('cloning with references', () => {
|
|
953
|
+
test('reference survives when extending tag is cloned', () => {
|
|
954
|
+
// Parse two lines - first creates reference, second extends it
|
|
955
|
+
const {tag} = parseTag(['source=hello target=$source', 'extra=data']);
|
|
956
|
+
// The reference should still work after the second parse cloned the first result
|
|
957
|
+
expect(tag.text('target')).toBe('hello');
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test('clone preserves RefTag', () => {
|
|
961
|
+
const {tag} = parseTag('source=hello target=$source');
|
|
962
|
+
const cloned = tag.clone();
|
|
963
|
+
// After cloning, the reference should still resolve
|
|
964
|
+
expect(cloned.text('target')).toBe('hello');
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
});
|