@malloydata/malloy-tag 0.0.335 → 0.0.336

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