@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.
- 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.ts
CHANGED
|
@@ -21,29 +21,6 @@
|
|
|
21
21
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {AbstractParseTreeVisitor} from 'antlr4ts/tree';
|
|
25
|
-
import {MalloyTagLexer} from './lib/Malloy/MalloyTagLexer';
|
|
26
|
-
import type {
|
|
27
|
-
ArrayElementContext,
|
|
28
|
-
ArrayValueContext,
|
|
29
|
-
PropNameContext,
|
|
30
|
-
PropertiesContext,
|
|
31
|
-
ReferenceContext,
|
|
32
|
-
StringContext,
|
|
33
|
-
TagDefContext,
|
|
34
|
-
TagEmptyContext,
|
|
35
|
-
TagEqContext,
|
|
36
|
-
TagLineContext,
|
|
37
|
-
TagReplacePropertiesContext,
|
|
38
|
-
TagSpecContext,
|
|
39
|
-
TagUpdatePropertiesContext,
|
|
40
|
-
} from './lib/Malloy/MalloyTagParser';
|
|
41
|
-
import {MalloyTagParser} from './lib/Malloy/MalloyTagParser';
|
|
42
|
-
import type {MalloyTagVisitor} from './lib/Malloy/MalloyTagVisitor';
|
|
43
|
-
import type {ANTLRErrorListener, ParserRuleContext, Token} from 'antlr4ts';
|
|
44
|
-
import {CharStreams, CommonTokenStream} from 'antlr4ts';
|
|
45
|
-
import {parseString} from './util';
|
|
46
|
-
|
|
47
24
|
export interface TagError {
|
|
48
25
|
message: string;
|
|
49
26
|
line: number;
|
|
@@ -51,20 +28,37 @@ export interface TagError {
|
|
|
51
28
|
code: string;
|
|
52
29
|
}
|
|
53
30
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// is why only TagDict interface is exported.
|
|
31
|
+
// TagInterface exists for tests and serialization only.
|
|
32
|
+
// Internally, Tag uses Tag instances for properties and array elements.
|
|
57
33
|
export type TagDict = Record<string, TagInterface>;
|
|
58
34
|
|
|
59
|
-
type
|
|
35
|
+
export type TagScalar = string | number | boolean | Date;
|
|
36
|
+
type TagValue = TagScalar | Tag[];
|
|
60
37
|
|
|
38
|
+
// Input format - for tests and constructors (no links)
|
|
61
39
|
export interface TagInterface {
|
|
62
|
-
eq?:
|
|
40
|
+
eq?: TagScalar | TagInterface[];
|
|
63
41
|
properties?: TagDict;
|
|
64
42
|
deleted?: boolean;
|
|
65
43
|
prefix?: string;
|
|
66
44
|
}
|
|
67
45
|
|
|
46
|
+
// Output format - for toJSON (can contain links)
|
|
47
|
+
// This format is round-trippable: a Tag.fromJSON(TagJSON) could reconstruct
|
|
48
|
+
// the original Tag tree, including RefTags from TagLink.linkTo strings.
|
|
49
|
+
export interface TagLink {
|
|
50
|
+
linkTo: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TagJSONInterface {
|
|
54
|
+
eq?: TagScalar | TagJSON[];
|
|
55
|
+
properties?: Record<string, TagJSON>;
|
|
56
|
+
deleted?: boolean;
|
|
57
|
+
prefix?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type TagJSON = TagJSONInterface | TagLink;
|
|
61
|
+
|
|
68
62
|
export interface TagParse {
|
|
69
63
|
tag: Tag;
|
|
70
64
|
log: TagError[];
|
|
@@ -73,7 +67,15 @@ export interface TagParse {
|
|
|
73
67
|
export type PathSegment = string | number;
|
|
74
68
|
export type Path = PathSegment[];
|
|
75
69
|
|
|
76
|
-
export type TagSetValue =
|
|
70
|
+
export type TagSetValue =
|
|
71
|
+
| string
|
|
72
|
+
| number
|
|
73
|
+
| boolean
|
|
74
|
+
| Date
|
|
75
|
+
| string[]
|
|
76
|
+
| number[]
|
|
77
|
+
| Tag
|
|
78
|
+
| null;
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
81
|
* Class for interacting with the parsed output of an annotation
|
|
@@ -81,27 +83,46 @@ export type TagSetValue = string | number | string[] | number[] | null;
|
|
|
81
83
|
* generate parsed data, and as an API to that data.
|
|
82
84
|
* ```
|
|
83
85
|
* tag.text(p?) => string value of tag.p or undefined
|
|
84
|
-
* tag.array(p?) => Tag[] value of tag.p or undefined
|
|
85
86
|
* tag.numeric(p?) => numeric value of tag.p or undefined
|
|
86
|
-
* tag.
|
|
87
|
-
* tag.
|
|
88
|
-
* tag.
|
|
89
|
-
* tag.
|
|
90
|
-
* tag.
|
|
87
|
+
* tag.boolean(p?) => boolean value of tag.p or undefined
|
|
88
|
+
* tag.isTrue(p?) => true if tag.p is boolean true
|
|
89
|
+
* tag.isFalse(p?) => true if tag.p is boolean false
|
|
90
|
+
* tag.date(p?) => Date value of tag.p or undefined
|
|
91
|
+
* tag.array(p?) => Tag[] value of tag.p or undefined
|
|
92
|
+
* tag.textArray(p?) => string[] value of elements in tag.p or undefined
|
|
93
|
+
* tag.numericArray(p?) => number[] value of elements in tag.p or undefined
|
|
94
|
+
* tag.tag(p?) => Tag value of tag.p
|
|
95
|
+
* tag.has(p?) => boolean "tag contains tag.p"
|
|
96
|
+
* tag.bare(p?) => tag.p exists and has no properties
|
|
91
97
|
* tag.dict => Record<string,Tag> of tag properties
|
|
98
|
+
* tag.toObject() => Plain JS object representation
|
|
92
99
|
* ```
|
|
93
100
|
*/
|
|
94
|
-
export class Tag
|
|
101
|
+
export class Tag {
|
|
95
102
|
eq?: TagValue;
|
|
96
|
-
properties?:
|
|
103
|
+
properties?: Record<string, Tag>;
|
|
97
104
|
prefix?: string;
|
|
98
105
|
deleted?: boolean;
|
|
106
|
+
private _parent?: Tag;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the parent tag, if this tag is part of a tree.
|
|
110
|
+
*/
|
|
111
|
+
get parent(): Tag | undefined {
|
|
112
|
+
return this._parent;
|
|
113
|
+
}
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Get the root tag by traversing up the parent chain.
|
|
117
|
+
* Returns this tag if it has no parent.
|
|
118
|
+
*/
|
|
119
|
+
get root(): Tag {
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
121
|
+
let current: Tag = this;
|
|
122
|
+
while (current._parent !== undefined) {
|
|
123
|
+
current = current._parent;
|
|
103
124
|
}
|
|
104
|
-
return
|
|
125
|
+
return current;
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
// --- Just for debugging ---
|
|
@@ -127,70 +148,45 @@ export class Tag implements TagInterface {
|
|
|
127
148
|
return str + `=${this.eq}`;
|
|
128
149
|
}
|
|
129
150
|
str += ' {';
|
|
130
|
-
if (this.eq) {
|
|
131
|
-
if (
|
|
132
|
-
str += `\n${spaces} =: ${this.eq}`;
|
|
133
|
-
} else {
|
|
151
|
+
if (this.eq !== undefined) {
|
|
152
|
+
if (Array.isArray(this.eq)) {
|
|
134
153
|
str += `\n${spaces} =: [\n${spaces} ${this.eq
|
|
135
|
-
.map(el =>
|
|
154
|
+
.map(el => el.peek(indent + 4))
|
|
136
155
|
.join(`\n${spaces} `)}\n${spaces} ]`;
|
|
156
|
+
} else {
|
|
157
|
+
str += `\n${spaces} =: ${this.eq}`;
|
|
137
158
|
}
|
|
138
159
|
}
|
|
139
160
|
|
|
140
161
|
if (this.properties) {
|
|
141
162
|
for (const k in this.properties) {
|
|
142
|
-
|
|
143
|
-
str += `\n${spaces} ${k}: ${val.peek(indent + 2)}`;
|
|
163
|
+
str += `\n${spaces} ${k}: ${this.properties[k].peek(indent + 2)}`;
|
|
144
164
|
}
|
|
145
165
|
}
|
|
146
166
|
str += `\n${spaces}}`;
|
|
147
167
|
return str;
|
|
148
168
|
}
|
|
149
169
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
* all characters up to the first space.
|
|
154
|
-
* @param lineNumber -- A line number to be associated with the parse errors.
|
|
155
|
-
* @param extending A tag which this line will be extending
|
|
156
|
-
* @param importing Outer "scopes" for $() references
|
|
157
|
-
* @returns Something shaped like { tag: Tag, log: ParseErrors[] }
|
|
158
|
-
*/
|
|
159
|
-
static fromTagLine(
|
|
160
|
-
source: string,
|
|
161
|
-
lineNumber = 0,
|
|
162
|
-
extending?: Tag,
|
|
163
|
-
...importing: Tag[]
|
|
164
|
-
): TagParse {
|
|
165
|
-
return parseTagLine(source, extending, importing, lineNumber);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Parse multiple lines of Malloy tag language, merging them into a single Tag
|
|
170
|
-
* @param lines -- The source line to be parsed. If the string starts with #, then it skips
|
|
171
|
-
* all characters up to the first space.
|
|
172
|
-
* @param extending A tag which this line will be extending
|
|
173
|
-
* @param importing Outer "scopes" for $() references
|
|
174
|
-
* @returns Something shaped like { tag: Tag, log: ParseErrors[] }
|
|
175
|
-
*/
|
|
176
|
-
static fromTagLines(lines: string[], extending?: Tag, ...importing: Tag[]) {
|
|
177
|
-
const allErrs: TagError[] = [];
|
|
178
|
-
let current: Tag | undefined = extending;
|
|
179
|
-
for (let i = 0; i < lines.length; i++) {
|
|
180
|
-
const text = lines[i];
|
|
181
|
-
const noteParse = parseTagLine(text, current, importing, i);
|
|
182
|
-
current = noteParse.tag;
|
|
183
|
-
allErrs.push(...noteParse.log);
|
|
170
|
+
constructor(from: TagInterface = {}, parent?: Tag) {
|
|
171
|
+
if (parent !== undefined) {
|
|
172
|
+
this._parent = parent;
|
|
184
173
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
174
|
+
if (from.eq !== undefined) {
|
|
175
|
+
if (Array.isArray(from.eq)) {
|
|
176
|
+
// Convert array elements to Tags
|
|
177
|
+
this.eq = from.eq.map(el =>
|
|
178
|
+
el instanceof Tag ? el : new Tag(el, this)
|
|
179
|
+
);
|
|
180
|
+
} else {
|
|
181
|
+
this.eq = from.eq;
|
|
182
|
+
}
|
|
191
183
|
}
|
|
192
184
|
if (from.properties) {
|
|
193
|
-
|
|
185
|
+
// Convert property values to Tags
|
|
186
|
+
this.properties = {};
|
|
187
|
+
for (const [key, val] of Object.entries(from.properties)) {
|
|
188
|
+
this.properties[key] = val instanceof Tag ? val : new Tag(val, this);
|
|
189
|
+
}
|
|
194
190
|
}
|
|
195
191
|
if (from.deleted) {
|
|
196
192
|
this.deleted = from.deleted;
|
|
@@ -209,20 +205,52 @@ export class Tag implements TagInterface {
|
|
|
209
205
|
}
|
|
210
206
|
|
|
211
207
|
text(...at: Path): string | undefined {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
return
|
|
208
|
+
const val = this.find(at)?.getEq();
|
|
209
|
+
if (val === undefined || Array.isArray(val)) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (val instanceof Date) {
|
|
213
|
+
return val.toISOString();
|
|
215
214
|
}
|
|
215
|
+
return String(val);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
numeric(...at: Path): number | undefined {
|
|
219
|
-
const
|
|
220
|
-
if (typeof
|
|
221
|
-
|
|
219
|
+
const val = this.find(at)?.getEq();
|
|
220
|
+
if (typeof val === 'number') {
|
|
221
|
+
return val;
|
|
222
|
+
}
|
|
223
|
+
if (typeof val === 'string') {
|
|
224
|
+
const num = Number.parseFloat(val);
|
|
222
225
|
if (!Number.isNaN(num)) {
|
|
223
226
|
return num;
|
|
224
227
|
}
|
|
225
228
|
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
boolean(...at: Path): boolean | undefined {
|
|
233
|
+
const val = this.find(at)?.getEq();
|
|
234
|
+
if (typeof val === 'boolean') {
|
|
235
|
+
return val;
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
isTrue(...at: Path): boolean {
|
|
241
|
+
return this.find(at)?.getEq() === true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
isFalse(...at: Path): boolean {
|
|
245
|
+
return this.find(at)?.getEq() === false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
date(...at: Path): Date | undefined {
|
|
249
|
+
const val = this.find(at)?.getEq();
|
|
250
|
+
if (val instanceof Date) {
|
|
251
|
+
return val;
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
226
254
|
}
|
|
227
255
|
|
|
228
256
|
bare(...at: Path): boolean | undefined {
|
|
@@ -230,51 +258,107 @@ export class Tag implements TagInterface {
|
|
|
230
258
|
if (p === undefined) {
|
|
231
259
|
return;
|
|
232
260
|
}
|
|
233
|
-
return (
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
return !p.hasProperties();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Virtual accessor for eq - overridden in RefTag to resolve */
|
|
265
|
+
getEq(): TagValue | undefined {
|
|
266
|
+
return this.eq;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Virtual accessor for a property - overridden in RefTag to resolve */
|
|
270
|
+
getProperty(name: string): Tag | undefined {
|
|
271
|
+
return this.properties?.[name];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Virtual accessor for an array element - overridden in RefTag to resolve */
|
|
275
|
+
getArrayElement(index: number): Tag | undefined {
|
|
276
|
+
if (Array.isArray(this.eq) && index < this.eq.length) {
|
|
277
|
+
return this.eq[index];
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
236
280
|
}
|
|
237
281
|
|
|
238
282
|
get dict(): Record<string, Tag> {
|
|
239
|
-
|
|
283
|
+
return this.properties ?? {};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Iterate over [name, Tag] pairs for each property */
|
|
287
|
+
*entries(): Generator<[string, Tag]> {
|
|
240
288
|
if (this.properties) {
|
|
241
289
|
for (const key in this.properties) {
|
|
242
|
-
|
|
290
|
+
yield [key, this.properties[key]];
|
|
243
291
|
}
|
|
244
292
|
}
|
|
245
|
-
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Iterate over property names */
|
|
296
|
+
*keys(): Generator<string> {
|
|
297
|
+
if (this.properties) {
|
|
298
|
+
for (const key in this.properties) {
|
|
299
|
+
yield key;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Check if this tag has any properties */
|
|
305
|
+
hasProperties(): boolean {
|
|
306
|
+
return (
|
|
307
|
+
this.properties !== undefined && Object.keys(this.properties).length > 0
|
|
308
|
+
);
|
|
246
309
|
}
|
|
247
310
|
|
|
248
311
|
array(...at: Path): Tag[] | undefined {
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
312
|
+
const found = this.find(at);
|
|
313
|
+
if (found === undefined) {
|
|
251
314
|
return undefined;
|
|
252
315
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
316
|
+
const arr = found.getEq();
|
|
317
|
+
if (!Array.isArray(arr)) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
return arr.map((_, i) => found.getArrayElement(i)!);
|
|
256
321
|
}
|
|
257
322
|
|
|
258
323
|
textArray(...at: Path): string[] | undefined {
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
324
|
+
const found = this.find(at);
|
|
325
|
+
if (found === undefined) {
|
|
261
326
|
return undefined;
|
|
262
327
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
)
|
|
328
|
+
const arr = found.getEq();
|
|
329
|
+
if (!Array.isArray(arr)) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
return arr.reduce<string[]>((allStrs, _, i) => {
|
|
333
|
+
const el = found.getArrayElement(i);
|
|
334
|
+
const val = el?.getEq();
|
|
335
|
+
if (val === undefined || Array.isArray(val)) {
|
|
336
|
+
return allStrs;
|
|
337
|
+
}
|
|
338
|
+
if (val instanceof Date) {
|
|
339
|
+
return allStrs.concat(val.toISOString());
|
|
340
|
+
}
|
|
341
|
+
return allStrs.concat(String(val));
|
|
342
|
+
}, []);
|
|
268
343
|
}
|
|
269
344
|
|
|
270
345
|
numericArray(...at: Path): number[] | undefined {
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
346
|
+
const found = this.find(at);
|
|
347
|
+
if (found === undefined) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
const arr = found.getEq();
|
|
351
|
+
if (!Array.isArray(arr)) {
|
|
273
352
|
return undefined;
|
|
274
353
|
}
|
|
275
|
-
return
|
|
276
|
-
|
|
277
|
-
|
|
354
|
+
return arr.reduce<number[]>((allNums, _, i) => {
|
|
355
|
+
const el = found.getArrayElement(i);
|
|
356
|
+
const val = el?.getEq();
|
|
357
|
+
if (typeof val === 'number') {
|
|
358
|
+
return allNums.concat(val);
|
|
359
|
+
}
|
|
360
|
+
if (typeof val === 'string') {
|
|
361
|
+
const num = Number.parseFloat(val);
|
|
278
362
|
if (!Number.isNaN(num)) {
|
|
279
363
|
return allNums.concat(num);
|
|
280
364
|
}
|
|
@@ -283,16 +367,197 @@ export class Tag implements TagInterface {
|
|
|
283
367
|
}, []);
|
|
284
368
|
}
|
|
285
369
|
|
|
286
|
-
// Has the sometimes
|
|
287
|
-
getProperties():
|
|
370
|
+
// Has the sometimes desirable side effect of initializing properties
|
|
371
|
+
getProperties(): Record<string, Tag> {
|
|
288
372
|
if (this.properties === undefined) {
|
|
289
373
|
this.properties = {};
|
|
290
374
|
}
|
|
291
375
|
return this.properties;
|
|
292
376
|
}
|
|
293
377
|
|
|
294
|
-
clone(): Tag {
|
|
295
|
-
|
|
378
|
+
clone(newParent?: Tag): Tag {
|
|
379
|
+
const cloned = new Tag({}, newParent);
|
|
380
|
+
cloned.prefix = this.prefix;
|
|
381
|
+
cloned.deleted = this.deleted;
|
|
382
|
+
|
|
383
|
+
if (this.eq !== undefined) {
|
|
384
|
+
if (Array.isArray(this.eq)) {
|
|
385
|
+
cloned.eq = this.eq.map(el => el.clone(cloned));
|
|
386
|
+
} else {
|
|
387
|
+
// Scalar value - copy directly (Date needs special handling)
|
|
388
|
+
cloned.eq = this.eq instanceof Date ? new Date(this.eq) : this.eq;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.properties) {
|
|
393
|
+
cloned.properties = {};
|
|
394
|
+
for (const [key, val] of Object.entries(this.properties)) {
|
|
395
|
+
cloned.properties[key] = val.clone(cloned);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return cloned;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private static scalarToObject(
|
|
403
|
+
val: TagScalar
|
|
404
|
+
): string | number | boolean | Date {
|
|
405
|
+
return val;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private static tagToObject(tag: TagInterface): unknown {
|
|
409
|
+
const hasProps =
|
|
410
|
+
tag.properties !== undefined && Object.keys(tag.properties).length > 0;
|
|
411
|
+
const hasValue = tag.eq !== undefined;
|
|
412
|
+
|
|
413
|
+
// Bare tag (no value, no properties)
|
|
414
|
+
if (!hasValue && !hasProps) {
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Properties only
|
|
419
|
+
if (!hasValue && hasProps) {
|
|
420
|
+
const result: Record<string, unknown> = {};
|
|
421
|
+
for (const [key, val] of Object.entries(tag.properties!)) {
|
|
422
|
+
if (!val.deleted) {
|
|
423
|
+
result[key] = Tag.tagToObject(val);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Value only
|
|
430
|
+
if (hasValue && !hasProps) {
|
|
431
|
+
if (Array.isArray(tag.eq)) {
|
|
432
|
+
return tag.eq.map(el => Tag.tagToObject(el));
|
|
433
|
+
}
|
|
434
|
+
return Tag.scalarToObject(tag.eq!);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Both value and properties
|
|
438
|
+
const result: Record<string, unknown> = {};
|
|
439
|
+
if (Array.isArray(tag.eq)) {
|
|
440
|
+
result['='] = tag.eq.map(el => Tag.tagToObject(el));
|
|
441
|
+
} else {
|
|
442
|
+
result['='] = Tag.scalarToObject(tag.eq!);
|
|
443
|
+
}
|
|
444
|
+
for (const [key, val] of Object.entries(tag.properties!)) {
|
|
445
|
+
if (!val.deleted) {
|
|
446
|
+
result[key] = Tag.tagToObject(val);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Convert to a plain JS object. References are resolved to actual
|
|
454
|
+
* object pointers (which may be circular in JS - that's fine).
|
|
455
|
+
* @param resolving - RefTags currently being resolved (for cycle detection)
|
|
456
|
+
*/
|
|
457
|
+
toObject(resolving: Set<RefTag> = new Set()): Record<string, unknown> {
|
|
458
|
+
const result: Record<string, unknown> = {};
|
|
459
|
+
if (this.properties) {
|
|
460
|
+
for (const [key, prop] of Object.entries(this.properties)) {
|
|
461
|
+
if (!prop.deleted) {
|
|
462
|
+
result[key] = prop.toObjectValue(resolving);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Convert this tag's value to a plain JS object.
|
|
471
|
+
* Override in RefTag to resolve references.
|
|
472
|
+
* @param resolving - RefTags currently being resolved (for cycle detection)
|
|
473
|
+
*/
|
|
474
|
+
toObjectValue(resolving: Set<RefTag>): unknown {
|
|
475
|
+
const hasProps =
|
|
476
|
+
this.properties !== undefined && Object.keys(this.properties).length > 0;
|
|
477
|
+
const hasValue = this.eq !== undefined;
|
|
478
|
+
|
|
479
|
+
// Bare tag (no value, no properties)
|
|
480
|
+
if (!hasValue && !hasProps) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Properties only
|
|
485
|
+
if (!hasValue && hasProps) {
|
|
486
|
+
return this.toObject(resolving);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Value only
|
|
490
|
+
if (hasValue && !hasProps) {
|
|
491
|
+
if (Array.isArray(this.eq)) {
|
|
492
|
+
return this.eq.map(el => el.toObjectValue(resolving));
|
|
493
|
+
}
|
|
494
|
+
return this.eq;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Both value and properties
|
|
498
|
+
const result: Record<string, unknown> = this.toObject(resolving);
|
|
499
|
+
if (Array.isArray(this.eq)) {
|
|
500
|
+
result['='] = this.eq.map(el => el.toObjectValue(resolving));
|
|
501
|
+
} else {
|
|
502
|
+
result['='] = this.eq;
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Custom JSON serialization that excludes _parent to avoid circular references.
|
|
509
|
+
* This is called automatically by JSON.stringify().
|
|
510
|
+
*/
|
|
511
|
+
toJSON(): TagJSON {
|
|
512
|
+
const result: TagJSONInterface = {};
|
|
513
|
+
if (this.eq !== undefined) {
|
|
514
|
+
if (Array.isArray(this.eq)) {
|
|
515
|
+
result.eq = this.eq.map(el => el.toJSON());
|
|
516
|
+
} else {
|
|
517
|
+
result.eq = this.eq;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (this.properties !== undefined) {
|
|
521
|
+
result.properties = {};
|
|
522
|
+
for (const [key, val] of Object.entries(this.properties)) {
|
|
523
|
+
result.properties[key] = val.toJSON();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (this.deleted) {
|
|
527
|
+
result.deleted = true;
|
|
528
|
+
}
|
|
529
|
+
if (this.prefix) {
|
|
530
|
+
result.prefix = this.prefix;
|
|
531
|
+
}
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Validate all references in this tag tree.
|
|
537
|
+
* Returns an array of error messages for unresolved references.
|
|
538
|
+
*/
|
|
539
|
+
validateReferences(): string[] {
|
|
540
|
+
const errors: string[] = [];
|
|
541
|
+
this.collectReferenceErrors(errors, []);
|
|
542
|
+
return errors;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Recursively collect reference errors.
|
|
547
|
+
*/
|
|
548
|
+
collectReferenceErrors(errors: string[], path: string[]): void {
|
|
549
|
+
if (this.properties) {
|
|
550
|
+
for (const [key, prop] of Object.entries(this.properties)) {
|
|
551
|
+
if (!prop.deleted) {
|
|
552
|
+
prop.collectReferenceErrors(errors, [...path, key]);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (Array.isArray(this.eq)) {
|
|
557
|
+
this.eq.forEach((el, i) => {
|
|
558
|
+
el.collectReferenceErrors(errors, [...path, `[${i}]`]);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
296
561
|
}
|
|
297
562
|
|
|
298
563
|
private static escapeString(str: string) {
|
|
@@ -313,6 +578,19 @@ export class Tag implements TagInterface {
|
|
|
313
578
|
return `"${Tag.escapeString(str)}"`;
|
|
314
579
|
}
|
|
315
580
|
|
|
581
|
+
private static serializeScalar(val: TagScalar): string {
|
|
582
|
+
if (typeof val === 'boolean') {
|
|
583
|
+
return val ? '@true' : '@false';
|
|
584
|
+
}
|
|
585
|
+
if (val instanceof Date) {
|
|
586
|
+
return `@${val.toISOString()}`;
|
|
587
|
+
}
|
|
588
|
+
if (typeof val === 'number') {
|
|
589
|
+
return String(val);
|
|
590
|
+
}
|
|
591
|
+
return Tag.quoteAndEscape(val);
|
|
592
|
+
}
|
|
593
|
+
|
|
316
594
|
toString(): string {
|
|
317
595
|
let annotation = this.prefix ?? '# ';
|
|
318
596
|
function addChildren(tag: TagInterface) {
|
|
@@ -335,7 +613,7 @@ export class Tag implements TagInterface {
|
|
|
335
613
|
}
|
|
336
614
|
annotation += ']';
|
|
337
615
|
} else {
|
|
338
|
-
annotation += Tag.
|
|
616
|
+
annotation += Tag.serializeScalar(child.eq);
|
|
339
617
|
}
|
|
340
618
|
}
|
|
341
619
|
if (child.properties) {
|
|
@@ -370,25 +648,19 @@ export class Tag implements TagInterface {
|
|
|
370
648
|
}
|
|
371
649
|
|
|
372
650
|
find(path: Path): Tag | undefined {
|
|
373
|
-
|
|
651
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
652
|
+
let currentTag: Tag = this;
|
|
374
653
|
for (const segment of path) {
|
|
654
|
+
let next: Tag | undefined;
|
|
375
655
|
if (typeof segment === 'number') {
|
|
376
|
-
|
|
377
|
-
currentTag.eq === undefined ||
|
|
378
|
-
!Array.isArray(currentTag.eq) ||
|
|
379
|
-
currentTag.eq.length <= segment
|
|
380
|
-
) {
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
currentTag = Tag.tagFrom(currentTag.eq[segment]);
|
|
656
|
+
next = currentTag.getArrayElement(segment);
|
|
384
657
|
} else {
|
|
385
|
-
|
|
386
|
-
if (segment in properties) {
|
|
387
|
-
currentTag = Tag.tagFrom(properties[segment]);
|
|
388
|
-
} else {
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
658
|
+
next = currentTag.getProperty(segment);
|
|
391
659
|
}
|
|
660
|
+
if (next === undefined) {
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
currentTag = next;
|
|
392
664
|
}
|
|
393
665
|
return currentTag.deleted ? undefined : currentTag;
|
|
394
666
|
}
|
|
@@ -398,16 +670,23 @@ export class Tag implements TagInterface {
|
|
|
398
670
|
}
|
|
399
671
|
|
|
400
672
|
set(path: Path, value: TagSetValue = null): Tag {
|
|
401
|
-
|
|
402
|
-
let currentTag:
|
|
673
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
674
|
+
let currentTag: Tag = this;
|
|
675
|
+
let parentTag: Tag | undefined;
|
|
676
|
+
let lastSegment: PathSegment | undefined;
|
|
677
|
+
|
|
403
678
|
for (const segment of path) {
|
|
679
|
+
parentTag = currentTag;
|
|
680
|
+
lastSegment = segment;
|
|
404
681
|
if (typeof segment === 'number') {
|
|
405
682
|
if (currentTag.eq === undefined || !Array.isArray(currentTag.eq)) {
|
|
406
|
-
currentTag.eq = Array.from({length: segment + 1}).map(
|
|
683
|
+
currentTag.eq = Array.from({length: segment + 1}).map(
|
|
684
|
+
() => new Tag({}, currentTag)
|
|
685
|
+
);
|
|
407
686
|
} else if (currentTag.eq.length <= segment) {
|
|
408
687
|
const values = currentTag.eq;
|
|
409
688
|
const newVal = Array.from({length: segment + 1}).map((_, i) =>
|
|
410
|
-
i < values.length ? values[i] : {}
|
|
689
|
+
i < values.length ? values[i] : new Tag({}, currentTag)
|
|
411
690
|
);
|
|
412
691
|
currentTag.eq = newVal;
|
|
413
692
|
}
|
|
@@ -415,7 +694,7 @@ export class Tag implements TagInterface {
|
|
|
415
694
|
} else {
|
|
416
695
|
const properties = currentTag.properties;
|
|
417
696
|
if (properties === undefined) {
|
|
418
|
-
currentTag.properties = {[segment]: {}};
|
|
697
|
+
currentTag.properties = {[segment]: new Tag({}, currentTag)};
|
|
419
698
|
currentTag = currentTag.properties[segment];
|
|
420
699
|
} else if (segment in properties) {
|
|
421
700
|
currentTag = properties[segment];
|
|
@@ -423,23 +702,37 @@ export class Tag implements TagInterface {
|
|
|
423
702
|
currentTag.deleted = false;
|
|
424
703
|
}
|
|
425
704
|
} else {
|
|
426
|
-
properties[segment] = {};
|
|
705
|
+
properties[segment] = new Tag({}, currentTag);
|
|
427
706
|
currentTag = properties[segment];
|
|
428
707
|
}
|
|
429
708
|
}
|
|
430
709
|
}
|
|
710
|
+
|
|
431
711
|
if (value === null) {
|
|
432
712
|
currentTag.eq = undefined;
|
|
433
|
-
} else if (
|
|
713
|
+
} else if (value instanceof Tag) {
|
|
714
|
+
// Clone the tag with correct parent and replace in parent's slot
|
|
715
|
+
const cloned = value.clone(parentTag);
|
|
716
|
+
if (parentTag && lastSegment !== undefined) {
|
|
717
|
+
if (typeof lastSegment === 'number' && Array.isArray(parentTag.eq)) {
|
|
718
|
+
parentTag.eq[lastSegment] = cloned;
|
|
719
|
+
} else if (typeof lastSegment === 'string' && parentTag.properties) {
|
|
720
|
+
parentTag.properties[lastSegment] = cloned;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
} else if (
|
|
724
|
+
typeof value === 'string' ||
|
|
725
|
+
typeof value === 'number' ||
|
|
726
|
+
typeof value === 'boolean' ||
|
|
727
|
+
value instanceof Date
|
|
728
|
+
) {
|
|
434
729
|
currentTag.eq = value;
|
|
435
|
-
} else if (typeof value === 'number') {
|
|
436
|
-
currentTag.eq = value.toString(); // TODO big numbers?
|
|
437
730
|
} else if (Array.isArray(value)) {
|
|
438
731
|
currentTag.eq = value.map((v: string | number) => {
|
|
439
|
-
return {eq:
|
|
732
|
+
return new Tag({eq: v}, currentTag);
|
|
440
733
|
});
|
|
441
734
|
}
|
|
442
|
-
return
|
|
735
|
+
return this;
|
|
443
736
|
}
|
|
444
737
|
|
|
445
738
|
delete(...path: Path): Tag {
|
|
@@ -451,18 +744,20 @@ export class Tag implements TagInterface {
|
|
|
451
744
|
}
|
|
452
745
|
|
|
453
746
|
private remove(path: Path, hard = false): Tag {
|
|
454
|
-
|
|
455
|
-
let currentTag:
|
|
747
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
748
|
+
let currentTag: Tag = this;
|
|
456
749
|
for (const segment of path.slice(0, path.length - 1)) {
|
|
457
750
|
if (typeof segment === 'number') {
|
|
458
751
|
if (currentTag.eq === undefined || !Array.isArray(currentTag.eq)) {
|
|
459
|
-
if (!hard) return
|
|
460
|
-
currentTag.eq = Array.from({length: segment}).map(
|
|
752
|
+
if (!hard) return this;
|
|
753
|
+
currentTag.eq = Array.from({length: segment}).map(
|
|
754
|
+
() => new Tag({}, currentTag)
|
|
755
|
+
);
|
|
461
756
|
} else if (currentTag.eq.length <= segment) {
|
|
462
|
-
if (!hard) return
|
|
757
|
+
if (!hard) return this;
|
|
463
758
|
const values = currentTag.eq;
|
|
464
759
|
const newVal = Array.from({length: segment}).map((_, i) =>
|
|
465
|
-
i < values.length ? values[i] : {}
|
|
760
|
+
i < values.length ? values[i] : new Tag({}, currentTag)
|
|
466
761
|
);
|
|
467
762
|
currentTag.eq = newVal;
|
|
468
763
|
}
|
|
@@ -470,14 +765,14 @@ export class Tag implements TagInterface {
|
|
|
470
765
|
} else {
|
|
471
766
|
const properties = currentTag.properties;
|
|
472
767
|
if (properties === undefined) {
|
|
473
|
-
if (!hard) return
|
|
474
|
-
currentTag.properties = {[segment]: {}};
|
|
768
|
+
if (!hard) return this;
|
|
769
|
+
currentTag.properties = {[segment]: new Tag({}, currentTag)};
|
|
475
770
|
currentTag = currentTag.properties[segment];
|
|
476
771
|
} else if (segment in properties) {
|
|
477
772
|
currentTag = properties[segment];
|
|
478
773
|
} else {
|
|
479
|
-
if (!hard) return
|
|
480
|
-
properties[segment] = {};
|
|
774
|
+
if (!hard) return this;
|
|
775
|
+
properties[segment] = new Tag({}, currentTag);
|
|
481
776
|
currentTag = properties[segment];
|
|
482
777
|
}
|
|
483
778
|
}
|
|
@@ -488,290 +783,194 @@ export class Tag implements TagInterface {
|
|
|
488
783
|
delete currentTag.properties[segment];
|
|
489
784
|
} else if (hard) {
|
|
490
785
|
currentTag.properties ??= {};
|
|
491
|
-
currentTag.properties[segment] = {deleted: true};
|
|
786
|
+
currentTag.properties[segment] = new Tag({deleted: true}, currentTag);
|
|
492
787
|
}
|
|
493
788
|
} else {
|
|
494
789
|
if (Array.isArray(currentTag.eq)) {
|
|
495
790
|
currentTag.eq.splice(segment, 1);
|
|
496
791
|
}
|
|
497
792
|
}
|
|
498
|
-
return
|
|
793
|
+
return this;
|
|
499
794
|
}
|
|
500
795
|
}
|
|
501
796
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
code: 'tag-parse-syntax-error',
|
|
516
|
-
message: msg,
|
|
517
|
-
line: this.line,
|
|
518
|
-
offset: charPositionInLine,
|
|
519
|
-
};
|
|
520
|
-
this.log.push(logMsg);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
semanticError(cx: ParserRuleContext, code: string, msg: string): void {
|
|
524
|
-
const logMsg: TagError = {
|
|
525
|
-
code,
|
|
526
|
-
message: msg,
|
|
527
|
-
line: this.line,
|
|
528
|
-
offset: cx.start.charPositionInLine,
|
|
529
|
-
};
|
|
530
|
-
this.log.push(logMsg);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
797
|
+
/**
|
|
798
|
+
* A tag that references another location in the tag tree.
|
|
799
|
+
* When accessed, it dereferences to the target tag.
|
|
800
|
+
*
|
|
801
|
+
* Reference syntax:
|
|
802
|
+
* $path.to.thing - absolute from root
|
|
803
|
+
* $^thing - up one level, then 'thing'
|
|
804
|
+
* $^^thing - up two levels, then 'thing'
|
|
805
|
+
* $items[0].name - with array indexing
|
|
806
|
+
*/
|
|
807
|
+
export class RefTag extends Tag {
|
|
808
|
+
readonly ups: number;
|
|
809
|
+
readonly refPath: Path;
|
|
533
810
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
811
|
+
constructor(ups: number, refPath: Path, parent?: Tag) {
|
|
812
|
+
super({}, parent);
|
|
813
|
+
this.ups = ups;
|
|
814
|
+
this.refPath = refPath;
|
|
538
815
|
}
|
|
539
|
-
return new Tag();
|
|
540
|
-
}
|
|
541
816
|
|
|
542
|
-
/**
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
let next: Tag;
|
|
553
|
-
if (parentPropertyObject[p] === undefined) {
|
|
554
|
-
next = new Tag({});
|
|
555
|
-
parentPropertyObject[p] = next;
|
|
817
|
+
/**
|
|
818
|
+
* Resolve this reference to the target tag.
|
|
819
|
+
* Returns undefined if the reference cannot be resolved.
|
|
820
|
+
*/
|
|
821
|
+
resolve(): Tag | undefined {
|
|
822
|
+
// Start from the appropriate point based on ups
|
|
823
|
+
let current: Tag | undefined;
|
|
824
|
+
if (this.ups === 0) {
|
|
825
|
+
// Absolute reference from root
|
|
826
|
+
current = this.root;
|
|
556
827
|
} else {
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
828
|
+
// Relative reference - go up 'ups' levels from parent
|
|
829
|
+
// $^ means go up 1 level from the containing scope
|
|
830
|
+
current = this.parent;
|
|
831
|
+
for (let i = 0; i < this.ups && current !== undefined; i++) {
|
|
832
|
+
current = current.parent;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (current === undefined) {
|
|
837
|
+
return undefined;
|
|
562
838
|
}
|
|
563
|
-
|
|
839
|
+
|
|
840
|
+
// Follow the path
|
|
841
|
+
return current.find(this.refPath);
|
|
564
842
|
}
|
|
565
|
-
return [path[path.length - 1], parentPropertyObject];
|
|
566
|
-
}
|
|
567
843
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
844
|
+
/**
|
|
845
|
+
* Quote an identifier if it contains special characters.
|
|
846
|
+
*/
|
|
847
|
+
private quoteSegment(seg: string): string {
|
|
848
|
+
// Match bareString from peggy grammar: [0-9A-Za-z_\u00C0-\u024F\u1E00-\u1EFF]+
|
|
849
|
+
if (/^[0-9A-Za-z_\u00C0-\u024F\u1E00-\u1EFF]+$/.test(seg)) {
|
|
850
|
+
return seg;
|
|
851
|
+
}
|
|
852
|
+
// Otherwise, backquote it (escaping backslashes and backquotes)
|
|
853
|
+
return '`' + seg.replace(/\\/g, '\\\\').replace(/`/g, '\\`') + '`';
|
|
571
854
|
}
|
|
572
|
-
return ctx.text;
|
|
573
|
-
}
|
|
574
855
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const tokenStream = new CommonTokenStream(lexer);
|
|
592
|
-
const pLog = new TagErrorListener(onLine);
|
|
593
|
-
const taglineParser = new MalloyTagParser(tokenStream);
|
|
594
|
-
taglineParser.removeErrorListeners();
|
|
595
|
-
taglineParser.addErrorListener(pLog);
|
|
596
|
-
const tagTree = taglineParser.tagLine();
|
|
597
|
-
const treeWalker = new TagLineParser(outerScope, pLog);
|
|
598
|
-
const tag = treeWalker.tagLineToTag(tagTree, extending);
|
|
599
|
-
return {tag, log: pLog.log};
|
|
600
|
-
}
|
|
856
|
+
/**
|
|
857
|
+
* Convert this reference to its string representation.
|
|
858
|
+
*/
|
|
859
|
+
toRefString(): string {
|
|
860
|
+
const prefix = '$' + '^'.repeat(this.ups);
|
|
861
|
+
const pathStr = this.refPath
|
|
862
|
+
.map((seg, i) => {
|
|
863
|
+
if (typeof seg === 'number') {
|
|
864
|
+
return `[${seg}]`;
|
|
865
|
+
}
|
|
866
|
+
const quoted = this.quoteSegment(seg);
|
|
867
|
+
return i === 0 ? quoted : `.${quoted}`;
|
|
868
|
+
})
|
|
869
|
+
.join('');
|
|
870
|
+
return prefix + pathStr;
|
|
871
|
+
}
|
|
601
872
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
{
|
|
606
|
-
scopes: Tag[] = [];
|
|
607
|
-
msgLog: TagErrorListener;
|
|
608
|
-
constructor(outerScopes: Tag[] = [], msgLog: TagErrorListener) {
|
|
609
|
-
super();
|
|
610
|
-
this.msgLog = msgLog;
|
|
611
|
-
this.scopes.unshift(...outerScopes);
|
|
873
|
+
// Override virtual accessors to resolve the reference
|
|
874
|
+
override getEq(): TagValue | undefined {
|
|
875
|
+
return this.resolve()?.getEq();
|
|
612
876
|
}
|
|
613
877
|
|
|
614
|
-
|
|
615
|
-
return
|
|
878
|
+
override getProperty(name: string): Tag | undefined {
|
|
879
|
+
return this.resolve()?.getProperty(name);
|
|
616
880
|
}
|
|
617
881
|
|
|
618
|
-
|
|
619
|
-
return
|
|
882
|
+
override getArrayElement(index: number): Tag | undefined {
|
|
883
|
+
return this.resolve()?.getArrayElement(index);
|
|
620
884
|
}
|
|
621
885
|
|
|
622
|
-
|
|
623
|
-
return
|
|
624
|
-
.identifier()
|
|
625
|
-
.map(cx =>
|
|
626
|
-
cx.BARE_STRING() ? cx.text : parseString(cx.text, cx.text[0])
|
|
627
|
-
);
|
|
886
|
+
override hasProperties(): boolean {
|
|
887
|
+
return this.resolve()?.hasProperties() ?? false;
|
|
628
888
|
}
|
|
629
889
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
890
|
+
/**
|
|
891
|
+
* For toObject, resolve the reference and return the target's object value.
|
|
892
|
+
* This creates actual object pointers (circular references are allowed).
|
|
893
|
+
* Detects cycles in the reference chain to prevent infinite recursion.
|
|
894
|
+
*/
|
|
895
|
+
override toObjectValue(resolving: Set<RefTag>): unknown {
|
|
896
|
+
// Check for cycle in reference chain
|
|
897
|
+
if (resolving.has(this)) {
|
|
898
|
+
// We're in a cycle - return undefined to break it
|
|
899
|
+
// (The cycle will be completed when the outer resolution finishes)
|
|
900
|
+
return undefined;
|
|
636
901
|
}
|
|
637
|
-
return tagLine;
|
|
638
|
-
}
|
|
639
902
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return extending;
|
|
645
|
-
}
|
|
903
|
+
const resolved = this.resolve();
|
|
904
|
+
if (resolved === undefined) {
|
|
905
|
+
return undefined;
|
|
906
|
+
}
|
|
646
907
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
908
|
+
// Track that we're resolving this RefTag
|
|
909
|
+
resolving.add(this);
|
|
910
|
+
const result = resolved.toObjectValue(resolving);
|
|
911
|
+
resolving.delete(this);
|
|
651
912
|
|
|
652
|
-
|
|
653
|
-
return this.getTags(ctx.tagSpec(), getBuildOn(ctx));
|
|
913
|
+
return result;
|
|
654
914
|
}
|
|
655
915
|
|
|
656
|
-
|
|
657
|
-
|
|
916
|
+
/**
|
|
917
|
+
* For JSON serialization, return a marker object instead of resolving.
|
|
918
|
+
*/
|
|
919
|
+
override toJSON(): TagJSON {
|
|
920
|
+
return {linkTo: this.toRefString()};
|
|
658
921
|
}
|
|
659
922
|
|
|
660
|
-
|
|
661
|
-
|
|
923
|
+
/**
|
|
924
|
+
* Check if this reference resolves, add error if not.
|
|
925
|
+
*/
|
|
926
|
+
override collectReferenceErrors(errors: string[], path: string[]): void {
|
|
927
|
+
if (this.resolve() === undefined) {
|
|
928
|
+
const location = path.length > 0 ? path.join('.') : 'root';
|
|
929
|
+
errors.push(`Unresolved reference at ${location}: ${this.toRefString()}`);
|
|
930
|
+
}
|
|
662
931
|
}
|
|
663
932
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (arrayCx) {
|
|
672
|
-
value = this.getArray(arrayCx);
|
|
673
|
-
}
|
|
933
|
+
/**
|
|
934
|
+
* Clone this RefTag, preserving the reference information.
|
|
935
|
+
*/
|
|
936
|
+
override clone(newParent?: Tag): RefTag {
|
|
937
|
+
return new RefTag(this.ups, [...this.refPath], newParent);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
674
940
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
941
|
+
/**
|
|
942
|
+
* Convert a Tag to a plain TagInterface without internal fields like _parent.
|
|
943
|
+
* Useful for test comparisons.
|
|
944
|
+
*/
|
|
945
|
+
export function interfaceFromTag(tag: TagInterface): TagInterface {
|
|
946
|
+
const result: TagInterface = {};
|
|
681
947
|
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
948
|
+
if (tag.eq !== undefined) {
|
|
949
|
+
if (Array.isArray(tag.eq)) {
|
|
950
|
+
result.eq = tag.eq.map(el => interfaceFromTag(el));
|
|
951
|
+
} else {
|
|
952
|
+
result.eq = tag.eq;
|
|
685
953
|
}
|
|
686
|
-
return new Tag({eq: value});
|
|
687
954
|
}
|
|
688
955
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
for (const scope of this.scopes) {
|
|
692
|
-
// first scope which has the first component gets to resolve the whole path
|
|
693
|
-
if (scope.has(path[0])) {
|
|
694
|
-
const refTo = scope.tag(...path);
|
|
695
|
-
if (refTo) {
|
|
696
|
-
return refTo.clone();
|
|
697
|
-
}
|
|
698
|
-
break;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
this.msgLog.semanticError(
|
|
702
|
-
ctx,
|
|
703
|
-
'tag-property-not-found',
|
|
704
|
-
`Reference to undefined property ${path.join('.')}`
|
|
705
|
-
);
|
|
706
|
-
return this.defaultResult();
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
visitTagEq(ctx: TagEqContext): Tag {
|
|
710
|
-
const buildOn = getBuildOn(ctx);
|
|
711
|
-
const name = this.getPropName(ctx.propName());
|
|
712
|
-
const [writeKey, writeInto] = buildAccessPath(buildOn, name);
|
|
713
|
-
const eq = this.visit(ctx.eqValue());
|
|
714
|
-
const propCx = ctx.properties();
|
|
715
|
-
if (propCx) {
|
|
716
|
-
// a.b.c { -y } means i want to do -y on
|
|
717
|
-
if (propCx.DOTTY() === undefined) {
|
|
718
|
-
const properties = this.visitProperties(propCx).dict;
|
|
719
|
-
// Add new properties old value
|
|
720
|
-
writeInto[writeKey] = {...eq, properties};
|
|
721
|
-
} else {
|
|
722
|
-
// preserve old properties, add new value
|
|
723
|
-
writeInto[writeKey] = {...writeInto[writeKey], ...eq};
|
|
724
|
-
}
|
|
725
|
-
} else {
|
|
726
|
-
writeInto[writeKey] = eq;
|
|
727
|
-
}
|
|
728
|
-
return buildOn;
|
|
956
|
+
if (tag.properties !== undefined) {
|
|
957
|
+
result.properties = interfaceFromDict(tag.properties);
|
|
729
958
|
}
|
|
730
959
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const name = this.getPropName(ctx.propName());
|
|
734
|
-
const [writeKey, writeInto] = buildAccessPath(buildOn, name);
|
|
735
|
-
const propCx = ctx.properties();
|
|
736
|
-
const props = this.visitProperties(propCx);
|
|
737
|
-
if (ctx.DOTTY() === undefined) {
|
|
738
|
-
// No dots, thropw away the value
|
|
739
|
-
writeInto[writeKey] = {properties: props.dict};
|
|
740
|
-
} else {
|
|
741
|
-
/// DOTS, just update the properties
|
|
742
|
-
writeInto[writeKey].properties = props.dict;
|
|
743
|
-
}
|
|
744
|
-
return buildOn;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
visitTagUpdateProperties(ctx: TagUpdatePropertiesContext): Tag {
|
|
748
|
-
const buildOn = getBuildOn(ctx);
|
|
749
|
-
const name = this.getPropName(ctx.propName());
|
|
750
|
-
const [writeKey, writeInto] = buildAccessPath(buildOn, name);
|
|
751
|
-
const propCx = ctx.properties();
|
|
752
|
-
propCx['buildOn'] = Tag.tagFrom(writeInto[writeKey]);
|
|
753
|
-
const props = this.visitProperties(propCx);
|
|
754
|
-
const thisObj = writeInto[writeKey] || new Tag({});
|
|
755
|
-
const properties = {...thisObj.properties, ...props.dict};
|
|
756
|
-
writeInto[writeKey] = {...thisObj, properties};
|
|
757
|
-
return buildOn;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
visitTagDef(ctx: TagDefContext): Tag {
|
|
761
|
-
const buildOn = getBuildOn(ctx);
|
|
762
|
-
const path = this.getPropName(ctx.propName());
|
|
763
|
-
const [writeKey, writeInto] = buildAccessPath(buildOn, path);
|
|
764
|
-
if (ctx.MINUS()) {
|
|
765
|
-
writeInto[writeKey] = {deleted: true};
|
|
766
|
-
} else {
|
|
767
|
-
writeInto[writeKey] = new Tag({});
|
|
768
|
-
}
|
|
769
|
-
return buildOn;
|
|
960
|
+
if (tag.deleted) {
|
|
961
|
+
result.deleted = true;
|
|
770
962
|
}
|
|
771
963
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Convert a TagDict to a plain TagDict without internal fields.
|
|
969
|
+
*/
|
|
970
|
+
export function interfaceFromDict(dict: TagDict): TagDict {
|
|
971
|
+
const result: TagDict = {};
|
|
972
|
+
for (const [key, val] of Object.entries(dict)) {
|
|
973
|
+
result[key] = interfaceFromTag(val);
|
|
776
974
|
}
|
|
975
|
+
return result;
|
|
777
976
|
}
|