@malloydata/malloy-tag 0.0.339 → 0.0.340

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 (45) hide show
  1. package/dist/index.d.ts +1 -3
  2. package/dist/index.js +4 -5
  3. package/dist/index.js.map +1 -1
  4. package/dist/{peggy/index.d.ts → parser.d.ts} +13 -4
  5. package/dist/parser.js +181 -0
  6. package/dist/parser.js.map +1 -0
  7. package/package.json +13 -6
  8. package/src/index.ts +1 -3
  9. package/src/parser.ts +203 -0
  10. package/CONTEXT.md +0 -173
  11. package/README.md +0 -0
  12. package/dist/peggy/dist/peg-tag-parser.d.ts +0 -11
  13. package/dist/peggy/dist/peg-tag-parser.js +0 -3130
  14. package/dist/peggy/dist/peg-tag-parser.js.map +0 -1
  15. package/dist/peggy/index.js +0 -117
  16. package/dist/peggy/index.js.map +0 -1
  17. package/dist/peggy/interpreter.d.ts +0 -32
  18. package/dist/peggy/interpreter.js +0 -208
  19. package/dist/peggy/interpreter.js.map +0 -1
  20. package/dist/peggy/statements.d.ts +0 -51
  21. package/dist/peggy/statements.js +0 -7
  22. package/dist/peggy/statements.js.map +0 -1
  23. package/dist/schema.d.ts +0 -41
  24. package/dist/schema.js +0 -573
  25. package/dist/schema.js.map +0 -1
  26. package/dist/schema.spec.d.ts +0 -1
  27. package/dist/schema.spec.js +0 -980
  28. package/dist/schema.spec.js.map +0 -1
  29. package/dist/tags.spec.d.ts +0 -8
  30. package/dist/tags.spec.js +0 -884
  31. package/dist/tags.spec.js.map +0 -1
  32. package/dist/util.spec.d.ts +0 -1
  33. package/dist/util.spec.js +0 -43
  34. package/dist/util.spec.js.map +0 -1
  35. package/src/motly-schema.motly +0 -52
  36. package/src/peggy/dist/peg-tag-parser.js +0 -2790
  37. package/src/peggy/index.ts +0 -89
  38. package/src/peggy/interpreter.ts +0 -265
  39. package/src/peggy/malloy-tag.peggy +0 -224
  40. package/src/peggy/statements.ts +0 -49
  41. package/src/schema.spec.ts +0 -1280
  42. package/src/schema.ts +0 -852
  43. package/src/tags.spec.ts +0 -967
  44. package/src/util.spec.ts +0 -43
  45. package/tsconfig.json +0 -12
package/dist/tags.spec.js DELETED
@@ -1,884 +0,0 @@
1
- "use strict";
2
- /*
3
- * Copyright 2023 Google LLC
4
- *
5
- * Permission is hereby granted, free of charge, to any person obtaining
6
- * a copy of this software and associated documentation files
7
- * (the "Software"), to deal in the Software without restriction,
8
- * including without limitation the rights to use, copy, modify, merge,
9
- * publish, distribute, sublicense, and/or sell copies of the Software,
10
- * and to permit persons to whom the Software is furnished to do so,
11
- * subject to the following conditions:
12
- *
13
- * The above copyright notice and this permission notice shall be
14
- * included in all copies or substantial portions of the Software.
15
- *
16
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
- * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
- * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
- */
24
- Object.defineProperty(exports, "__esModule", { value: true });
25
- const tags_1 = require("./tags");
26
- const peggy_1 = require("./peggy");
27
- expect.extend({
28
- tagsAre(src, result) {
29
- if (typeof src === 'string') {
30
- const { tag, log } = (0, peggy_1.parseTag)(src);
31
- const errs = log.map(e => e.message);
32
- if (log.length > 0) {
33
- return {
34
- pass: false,
35
- message: () => `${src}: Tag Parsing Error(s)\n${errs.join('\n')}`,
36
- };
37
- }
38
- src = tag;
39
- }
40
- const got = src.properties ? (0, tags_1.interfaceFromDict)(src.properties) : undefined;
41
- if (this.equals(got, result)) {
42
- return {
43
- pass: true,
44
- message: () => 'Parse returned expected object',
45
- };
46
- }
47
- return {
48
- pass: false,
49
- message: () => { var _a; return (_a = this.utils.diff(result, got)) !== null && _a !== void 0 ? _a : 'Not different'; },
50
- };
51
- },
52
- });
53
- describe('tagParse to Tag', () => {
54
- const tagTests = [
55
- ['just_name', { just_name: {} }],
56
- ['name=bare_string', { name: { eq: 'bare_string' } }],
57
- ['name="quoted_string"', { name: { eq: 'quoted_string' } }],
58
- ['name {prop1}', { name: { properties: { prop1: {} } } }],
59
- [
60
- 'name {prop1 prop2=value}',
61
- {
62
- name: {
63
- properties: {
64
- prop1: {},
65
- prop2: { eq: 'value' },
66
- },
67
- },
68
- },
69
- ],
70
- ['name.prop', { name: { properties: { prop: {} } } }],
71
- ['name.prop=value', { name: { properties: { prop: { eq: 'value' } } } }],
72
- [
73
- 'name.prop.sub=value',
74
- { name: { properties: { prop: { properties: { sub: { eq: 'value' } } } } } },
75
- ],
76
- [
77
- 'name{first3=[a, b, c]}',
78
- { name: { properties: { first3: { eq: [{ eq: 'a' }, { eq: 'b' }, { eq: 'c' }] } } } },
79
- ],
80
- ['name{first1=[a,]}', { name: { properties: { first1: { eq: [{ eq: 'a' }] } } } }],
81
- [
82
- 'name{first=[a {A}]}',
83
- { name: { properties: { first: { eq: [{ eq: 'a', properties: { A: {} } }] } } } },
84
- ],
85
- [
86
- 'name{first=[{A}]}',
87
- { name: { properties: { first: { eq: [{ properties: { A: {} } }] } } } },
88
- ],
89
- ['name=value {prop}', { name: { eq: 'value', properties: { prop: {} } } }],
90
- [
91
- 'name.prop={prop2}',
92
- { name: { properties: { prop: { properties: { prop2: {} } } } } },
93
- ],
94
- ['no yes -no', { yes: {}, no: { deleted: true } }],
95
- // TODO interesting behavior that removing a non-existant element, or the last element,
96
- // does not remove the `properties`.
97
- ['x -x.y', { x: { properties: { y: { deleted: true } } } }],
98
- ['x={y} -x.y', { x: { properties: { y: { deleted: true } } } }],
99
- ['x={y z} -x.y', { x: { properties: { z: {}, y: { deleted: true } } } }],
100
- ['x={y z} x {-y}', { x: { properties: { z: {}, y: { deleted: true } } } }],
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 } }],
112
- ['`spacey name`=Zaphod', { 'spacey name': { eq: 'Zaphod' } }],
113
- ["name='single quoted'", { name: { eq: 'single quoted' } }],
114
- [
115
- 'image { alt=hello { field=department } }',
116
- {
117
- image: {
118
- properties: {
119
- alt: { eq: 'hello', properties: { field: { eq: 'department' } } },
120
- },
121
- },
122
- },
123
- ],
124
- [
125
- 'image image.alt=hello image.alt.field=department',
126
- {
127
- image: {
128
- properties: {
129
- alt: { eq: 'hello', properties: { field: { eq: 'department' } } },
130
- },
131
- },
132
- },
133
- ],
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 }] } }],
186
- ];
187
- test.each(tagTests)('tag %s', (expression, expected) => {
188
- expect(expression).tagsAre(expected);
189
- });
190
- test.skip('unskip to debug just one of the expressions', () => {
191
- const x = ['word -...', {}];
192
- expect(x[0]).tagsAre(x[1]);
193
- });
194
- });
195
- describe('Tag access', () => {
196
- test('just text', () => {
197
- const strToParse = 'a=b';
198
- const getTags = (0, peggy_1.parseTag)(strToParse);
199
- expect(getTags.log).toEqual([]);
200
- const a = getTags.tag.tag('a');
201
- expect(a).toBeDefined();
202
- expect(a === null || a === void 0 ? void 0 : a.text()).toEqual('b');
203
- });
204
- test('tag path', () => {
205
- const strToParse = 'a.b.c.d.e=f';
206
- const tagParse = (0, peggy_1.parseTag)(strToParse);
207
- expect(tagParse.log).toEqual([]);
208
- const abcde = tagParse.tag.tag('a', 'b', 'c', 'd', 'e');
209
- expect(abcde).toBeDefined();
210
- expect(abcde === null || abcde === void 0 ? void 0 : abcde.text()).toEqual('f');
211
- });
212
- test('just array', () => {
213
- const strToParse = 'a=[b]';
214
- const getTags = (0, peggy_1.parseTag)(strToParse);
215
- expect(getTags.log).toEqual([]);
216
- const a = getTags.tag.tag('a');
217
- const aval = a === null || a === void 0 ? void 0 : a.array();
218
- expect(aval).toBeDefined();
219
- if (aval) {
220
- expect(aval.length).toEqual(1);
221
- expect(aval[0].text()).toEqual('b');
222
- }
223
- });
224
- test('tag path into array', () => {
225
- const strToParse = 'a.b.c = [{d=e}]';
226
- const tagParse = (0, peggy_1.parseTag)(strToParse);
227
- expect(tagParse.log).toEqual([]);
228
- const abcde = tagParse.tag.tag('a', 'b', 'c', 0, 'd');
229
- expect(abcde).toBeDefined();
230
- expect(abcde === null || abcde === void 0 ? void 0 : abcde.text()).toEqual('e');
231
- });
232
- test('array as text', () => {
233
- const strToParse = 'a=[b]';
234
- const getTags = (0, peggy_1.parseTag)(strToParse);
235
- expect(getTags.log).toEqual([]);
236
- const a = getTags.tag.tag('a');
237
- expect(a).toBeDefined();
238
- expect(a === null || a === void 0 ? void 0 : a.text()).toBeUndefined();
239
- });
240
- test('text as array', () => {
241
- const strToParse = 'a=b';
242
- const getTags = (0, peggy_1.parseTag)(strToParse);
243
- expect(getTags.log).toEqual([]);
244
- const a = getTags.tag.tag('a');
245
- expect(a).toBeDefined();
246
- expect(a === null || a === void 0 ? void 0 : a.array()).toBeUndefined();
247
- });
248
- test('just numeric', () => {
249
- const strToParse = 'a=7';
250
- const getTags = (0, peggy_1.parseTag)(strToParse);
251
- expect(getTags.log).toEqual([]);
252
- const a = getTags.tag.tag('a');
253
- expect(a).toBeDefined();
254
- const n = a === null || a === void 0 ? void 0 : a.numeric();
255
- expect(typeof n).toBe('number');
256
- expect(n).toEqual(7);
257
- });
258
- test('text as numeric', () => {
259
- const strToParse = 'a=seven';
260
- const getTags = (0, peggy_1.parseTag)(strToParse);
261
- expect(getTags.log).toEqual([]);
262
- const a = getTags.tag.tag('a');
263
- expect(a).toBeDefined();
264
- const n = a === null || a === void 0 ? void 0 : a.numeric();
265
- expect(n).toBeUndefined();
266
- });
267
- test('array as numeric', () => {
268
- const strToParse = 'a=[seven]';
269
- const getTags = (0, peggy_1.parseTag)(strToParse);
270
- expect(getTags.log).toEqual([]);
271
- const a = getTags.tag.tag('a');
272
- expect(a).toBeDefined();
273
- const n = a === null || a === void 0 ? void 0 : a.numeric();
274
- expect(n).toBeUndefined();
275
- });
276
- test('full text array', () => {
277
- const strToParse = 'a=[b,c]';
278
- const getTags = (0, peggy_1.parseTag)(strToParse);
279
- expect(getTags.log).toEqual([]);
280
- const a = getTags.tag.tag('a');
281
- expect(a).toBeDefined();
282
- const ais = a === null || a === void 0 ? void 0 : a.textArray();
283
- expect(ais).toEqual(['b', 'c']);
284
- });
285
- test('filtered text array', () => {
286
- const strToParse = 'a=[b,c,{d}]';
287
- const getTags = (0, peggy_1.parseTag)(strToParse);
288
- expect(getTags.log).toEqual([]);
289
- const a = getTags.tag.tag('a');
290
- expect(a).toBeDefined();
291
- const ais = a === null || a === void 0 ? void 0 : a.textArray();
292
- expect(ais).toEqual(['b', 'c']);
293
- });
294
- test('full numeric array', () => {
295
- const strToParse = 'a=[1,2]';
296
- const getTags = (0, peggy_1.parseTag)(strToParse);
297
- expect(getTags.log).toEqual([]);
298
- const a = getTags.tag.tag('a');
299
- expect(a).toBeDefined();
300
- const ais = a === null || a === void 0 ? void 0 : a.numericArray();
301
- expect(ais).toEqual([1, 2]);
302
- });
303
- test('filtered numeric array', () => {
304
- const strToParse = 'a=[1,2,three]';
305
- const getTags = (0, peggy_1.parseTag)(strToParse);
306
- expect(getTags.log).toEqual([]);
307
- const a = getTags.tag.tag('a');
308
- expect(a).toBeDefined();
309
- const ais = a === null || a === void 0 ? void 0 : a.numericArray();
310
- expect(ais).toEqual([1, 2]);
311
- });
312
- test('has', () => {
313
- const strToParse = 'a b.d';
314
- const getTags = (0, peggy_1.parseTag)(strToParse);
315
- expect(getTags.log).toEqual([]);
316
- expect(getTags.tag.has('a')).toBeTruthy();
317
- expect(getTags.tag.has('b', 'd')).toBeTruthy();
318
- expect(getTags.tag.has('c')).toBeFalsy();
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
- });
356
- test('property access on existing tag (which does not yet have properties)', () => {
357
- const parsePlot = (0, peggy_1.parseTag)('# plot');
358
- const parsed = (0, peggy_1.parseTag)('# plot.x=2', parsePlot.tag);
359
- const allTags = parsed.tag;
360
- const plotTag = allTags.tag('plot');
361
- const xTag = plotTag.tag('x');
362
- const x = xTag.numeric();
363
- expect(parsed.tag.numeric('plot', 'x')).toEqual(2);
364
- expect(plotTag.numeric('x')).toEqual(2);
365
- expect(x).toEqual(2);
366
- });
367
- test('set tag', () => {
368
- const base = tags_1.Tag.withPrefix('# ');
369
- const ext = base.set(['a', 'b', 0], 3).set(['a', 'b', 1], 4);
370
- expect(ext).tagsAre({
371
- a: { properties: { b: { eq: [{ eq: 3 }, { eq: 4 }] } } },
372
- });
373
- expect(ext.toString()).toBe('# a.b = [3, 4]\n');
374
- });
375
- test('set tag array element with properties', () => {
376
- const base = tags_1.Tag.withPrefix('# ');
377
- const ext = base
378
- .set(['a', 'b', 0, 'a'], 3)
379
- .set(['c', 0], 'foo')
380
- .set(['c', 0, 'a'], 4);
381
- expect(ext).tagsAre({
382
- a: { properties: { b: { eq: [{ properties: { a: { eq: 3 } } }] } } },
383
- c: { eq: [{ eq: 'foo', properties: { a: { eq: 4 } } }] },
384
- });
385
- expect(ext.toString()).toBe('# a.b = [{ a = 3 }] c = [foo { a = 4 }]\n');
386
- });
387
- test('soft remove', () => {
388
- const base = (0, peggy_1.parseTag)('# a.b.c = [{ d = 1 }]').tag;
389
- const ext = base.delete('a', 'b', 'c', 0, 'd').delete('a', 'b', 'c', 0);
390
- expect(ext).tagsAre({
391
- a: { properties: { b: { properties: { c: { eq: [] } } } } },
392
- });
393
- expect(ext.toString()).toBe('# a.b.c = []\n');
394
- });
395
- test('hard remove', () => {
396
- const base = (0, peggy_1.parseTag)('# hello').tag;
397
- const ext = base.unset('goodbye').unset('a', 'dieu');
398
- expect(ext).tagsAre({
399
- hello: {},
400
- goodbye: { deleted: true },
401
- a: { properties: { dieu: { deleted: true } } },
402
- });
403
- expect(ext.toString()).toBe('# hello -goodbye a { -dieu }\n');
404
- idempotent(ext);
405
- });
406
- test('set with different prefix', () => {
407
- const base = tags_1.Tag.withPrefix('#(docs) ');
408
- const ext = base.set(['a'], 3).set(['a', 'b'], null);
409
- expect(ext.toString()).toBe('#(docs) a = 3 { b }\n');
410
- });
411
- test('empty array', () => {
412
- const base = tags_1.Tag.withPrefix('#(docs) ');
413
- const ext = base.set(['a'], []);
414
- expect(ext.toString()).toBe('#(docs) a = []\n');
415
- idempotent(ext);
416
- });
417
- test('empty array followed by field', () => {
418
- const base = tags_1.Tag.withPrefix('#(docs) ');
419
- const ext = base.set(['a'], []).set(['b'], 'foo');
420
- expect(ext.toString()).toBe('#(docs) a = [] b = foo\n');
421
- idempotent(ext);
422
- });
423
- describe('toString escapes and quotes strings if necessary', () => {
424
- test('in eq value', () => {
425
- const base = tags_1.Tag.withPrefix('#(malloy) ');
426
- const ext = base.set(['drill_expression'], 'joined.two');
427
- expect(ext.toString()).toBe('#(malloy) drill_expression = "joined.two"\n');
428
- idempotent(ext);
429
- });
430
- test('in property name', () => {
431
- const base = tags_1.Tag.withPrefix('#(malloy) ');
432
- const ext = base.set(['foo bar'], '4');
433
- expect(ext.toString()).toBe('#(malloy) `foo bar` = 4\n');
434
- idempotent(ext);
435
- });
436
- test('deleted property name', () => {
437
- const base = tags_1.Tag.withPrefix('#(malloy) ');
438
- const ext = base.unset('two words');
439
- expect(ext.toString()).toBe('#(malloy) -`two words`\n');
440
- });
441
- test('value has a backslash', () => {
442
- const base = tags_1.Tag.withPrefix('#(malloy) ');
443
- const ext = base.set(['value'], '\\');
444
- expect(ext.toString()).toBe('#(malloy) value = "\\\\"\n');
445
- idempotent(ext);
446
- });
447
- test('value has a newline', () => {
448
- const base = tags_1.Tag.withPrefix('#(malloy) ');
449
- const ext = base.set(['value'], '\n');
450
- expect(ext.toString()).toBe('#(malloy) value = "\\n"\n');
451
- expect(ext.text('value')).toBe('\n');
452
- idempotent(ext);
453
- });
454
- test('value has a double quote', () => {
455
- const base = tags_1.Tag.withPrefix('#(malloy) ');
456
- const ext = base.set(['value'], '"');
457
- expect(ext.toString()).toBe('#(malloy) value = "\\""\n');
458
- idempotent(ext);
459
- });
460
- test('value is empty string', () => {
461
- const base = tags_1.Tag.withPrefix('#(malloy) ');
462
- const ext = base.set(['value'], '');
463
- expect(ext.toString()).toBe('#(malloy) value = ""\n');
464
- idempotent(ext);
465
- });
466
- test('prop has a back tick', () => {
467
- const base = tags_1.Tag.withPrefix('#(malloy) ');
468
- const ext = base.set(['a`b'], 1);
469
- expect(ext.toString()).toBe('#(malloy) `a\\`b` = 1\n');
470
- idempotent(ext);
471
- });
472
- test('prop has multiple back ticks', () => {
473
- const base = tags_1.Tag.withPrefix('#(malloy) ');
474
- const ext = base.set(['a`b`c'], 1);
475
- expect(ext.toString()).toBe('#(malloy) `a\\`b\\`c` = 1\n');
476
- idempotent(ext);
477
- });
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
- });
614
- });
615
- function idempotent(tag) {
616
- const str = tag.toString();
617
- const clone = (0, peggy_1.parseTag)(str).tag;
618
- clone.prefix = tag.prefix;
619
- expect(clone.toString()).toBe(str);
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
- });
884
- //# sourceMappingURL=tags.spec.js.map