@limaformat/lima 0.1.0
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/CHANGELOG.md +29 -0
- package/LICENSE +15 -0
- package/README.md +70 -0
- package/dist/block-cursor.d.ts +24 -0
- package/dist/block-cursor.js +71 -0
- package/dist/block.d.ts +18 -0
- package/dist/block.js +305 -0
- package/dist/builder.d.ts +41 -0
- package/dist/builder.js +1 -0
- package/dist/chars.d.ts +2 -0
- package/dist/chars.js +4 -0
- package/dist/core.d.ts +73 -0
- package/dist/core.js +439 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +24 -0
- package/dist/flow.d.ts +7 -0
- package/dist/flow.js +162 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/normalize.d.ts +41 -0
- package/dist/normalize.js +67 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.js +522 -0
- package/dist/scalars.d.ts +89 -0
- package/dist/scalars.js +407 -0
- package/dist/scanner.d.ts +82 -0
- package/dist/scanner.js +204 -0
- package/dist/value.d.ts +95 -0
- package/dist/value.js +185 -0
- package/package.json +41 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LIMA References 1.0 — layered strictly on top of `core.ts` (Appendix B:
|
|
3
|
+
* reference resolution is exclusively this extension's concern; Core never
|
|
4
|
+
* sees it). Reads the annotated `PositionedValue` tree Core produces and
|
|
5
|
+
* performs the two-phase resolution the spec describes (§4), keeping every
|
|
6
|
+
* intermediate result in the SAME annotated representation instead of a
|
|
7
|
+
* separate raw-value tree with a bolted-on marker for "inert":
|
|
8
|
+
*
|
|
9
|
+
* - A string's `quoted` flag (set once, by Core, from actual quote syntax)
|
|
10
|
+
* is the single source of truth for "never treat this as a reference
|
|
11
|
+
* site" — §2.3 (quoted tokens) and §3.8 (partial content) are the same
|
|
12
|
+
* rule applied to two different origins, so partial values are wrapped
|
|
13
|
+
* into this same representation with every string leaf pre-marked
|
|
14
|
+
* `quoted: true` before insertion (`partialToPositioned`), rather than
|
|
15
|
+
* needing a second, unrelated mechanism.
|
|
16
|
+
* - Every node keeps the source `line` it was parsed with, even after
|
|
17
|
+
* being deep-copied into a new position — diagnostics read it directly
|
|
18
|
+
* off the node instead of falling back to a coarser key-level lookup.
|
|
19
|
+
*
|
|
20
|
+
* There is no module-level mutable state: diagnostics are collected in a
|
|
21
|
+
* `ResolutionContext` created fresh per call and threaded explicitly.
|
|
22
|
+
*/
|
|
23
|
+
import { countNodes, canonicalString, codepointLength, ingestPartialValue, PARTIAL_COUNT_LIMIT, PARTIAL_NAME_LENGTH_LIMIT, PARTIAL_NODE_LIMIT, RESULT_NODE_LIMIT, SCALAR_LENGTH_LIMIT, } from './value.js';
|
|
24
|
+
import { parseCoreWithPositions, toPlainValue, toNativeFromPositioned, NESTING_DEPTH_LIMIT, } from './core.js';
|
|
25
|
+
import { LimaError } from './errors.js';
|
|
26
|
+
const emptyMapping = () => Object.create(null);
|
|
27
|
+
// References §2.1/§2.2/Appendix B grammar, per sigil.
|
|
28
|
+
const DOC_SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
|
|
29
|
+
const DOC_PATH = `${DOC_SEGMENT}(?:\\.${DOC_SEGMENT})*`;
|
|
30
|
+
const PARTIAL_KEY = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
31
|
+
const PURE_REF_RE = new RegExp(`^\\((?:\\$(${DOC_PATH})|%(${PARTIAL_KEY}))\\)$`);
|
|
32
|
+
const INTERP_RE = new RegExp(`\\((?:\\$(${DOC_PATH})|%(${PARTIAL_KEY}))\\)`, 'g');
|
|
33
|
+
/**
|
|
34
|
+
* §5: of every reference-resolution error encountered during a single
|
|
35
|
+
* resolveTree pass, only the one at the lowest source line is ultimately
|
|
36
|
+
* thrown. Tracking a running minimum instead of collecting every diagnostic
|
|
37
|
+
* into an array and sorting it at the end avoids an O(e log e) sort (and
|
|
38
|
+
* its allocations) for documents with many reference errors — `<` (not
|
|
39
|
+
* `<=`) preserves the original stable-sort-then-take-first tie-break: the
|
|
40
|
+
* first diagnostic reported at any given minimum line keeps winning over a
|
|
41
|
+
* later one at the same line, exactly as it did when every one of them was
|
|
42
|
+
* pushed into an array and stably sorted by line.
|
|
43
|
+
*/
|
|
44
|
+
const reportDiagnostic = (ctx, d) => {
|
|
45
|
+
if (ctx.best === null || d.line < ctx.best.line)
|
|
46
|
+
ctx.best = d;
|
|
47
|
+
};
|
|
48
|
+
// ─── PositionedValue-level helpers ─────────────────────────────────────────
|
|
49
|
+
const getNestedValueP = (root, path) => {
|
|
50
|
+
if (!path.includes('.'))
|
|
51
|
+
return root.get(path);
|
|
52
|
+
const parts = path.split('.');
|
|
53
|
+
let cur = root.get(parts[0]);
|
|
54
|
+
for (let i = 1; i < parts.length; i++) {
|
|
55
|
+
if (cur === undefined || cur.kind !== 'mapping')
|
|
56
|
+
return undefined;
|
|
57
|
+
cur = cur.entries.get(parts[i]);
|
|
58
|
+
}
|
|
59
|
+
return cur;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* `resolveTree` re-checks reference-freedom repeatedly on the SAME target
|
|
63
|
+
* subtree — once per reference pointing at it (the fast-path guard at the
|
|
64
|
+
* top of `resolveTree`, once more before copying a pure-reference target,
|
|
65
|
+
* once more per interpolation match) — so a document with many references
|
|
66
|
+
* into one large shared mapping would otherwise re-traverse that whole
|
|
67
|
+
* subtree for every single reference. `PositionedValue` nodes are only ever
|
|
68
|
+
* produced fresh (by Core, or by `deepCopyPositioned` below) and never
|
|
69
|
+
* mutated in place, so object identity is a stable cache key for the
|
|
70
|
+
* lifetime of a single `parseReferences` call — a module-level `WeakMap`
|
|
71
|
+
* is safe (and self-cleaning: entries vanish once a parse's tree is
|
|
72
|
+
* garbage-collected) rather than needing a per-call cache threaded through
|
|
73
|
+
* every function.
|
|
74
|
+
*/
|
|
75
|
+
const referenceFreeCache = new WeakMap();
|
|
76
|
+
/**
|
|
77
|
+
* A reference is only resolved from a target that is itself reference-free
|
|
78
|
+
* (§4). Quoted strings are always free (§2.3); partial-derived values are
|
|
79
|
+
* always free too, but never reach this function — see `partialToPositioned`.
|
|
80
|
+
*/
|
|
81
|
+
const isReferenceFreeP = (v) => {
|
|
82
|
+
if (v.kind !== 'string' && v.kind !== 'array' && v.kind !== 'mapping')
|
|
83
|
+
return true;
|
|
84
|
+
const cached = referenceFreeCache.get(v);
|
|
85
|
+
if (cached !== undefined)
|
|
86
|
+
return cached;
|
|
87
|
+
let result;
|
|
88
|
+
if (v.kind === 'string') {
|
|
89
|
+
result = v.quoted || (!v.value.includes('($') && !v.value.includes('(%'));
|
|
90
|
+
}
|
|
91
|
+
else if (v.kind === 'array') {
|
|
92
|
+
result = v.items.every(isReferenceFreeP);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
result = true;
|
|
96
|
+
for (const c of v.entries.values())
|
|
97
|
+
if (!isReferenceFreeP(c)) {
|
|
98
|
+
result = false;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
referenceFreeCache.set(v, result);
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Structural deep copy for document-derived targets — preserves each leaf's
|
|
107
|
+
* own line/quoted (§4.2/R-104: still eligible for further resolution) and
|
|
108
|
+
* any existing `insertedAt` provenance from an earlier, nested resolution
|
|
109
|
+
* (R-112: a reference copied wholesale into a new position can itself
|
|
110
|
+
* already contain the result of an inner reference that resolved first —
|
|
111
|
+
* both insertion sites remain identifiable participants after the copy).
|
|
112
|
+
*/
|
|
113
|
+
const deepCopyPositioned = (v) => {
|
|
114
|
+
switch (v.kind) {
|
|
115
|
+
case 'array': return { kind: 'array', items: v.items.map(deepCopyPositioned), line: v.line, insertedAt: v.insertedAt };
|
|
116
|
+
case 'mapping': {
|
|
117
|
+
const entries = new Map();
|
|
118
|
+
for (const [k, c] of v.entries)
|
|
119
|
+
entries.set(k, deepCopyPositioned(c));
|
|
120
|
+
return { kind: 'mapping', entries, line: v.line, insertedAt: v.insertedAt };
|
|
121
|
+
}
|
|
122
|
+
case 'instant': return { kind: 'instant', value: new Date(v.value.getTime()), line: v.line, insertedAt: v.insertedAt };
|
|
123
|
+
default: return v;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Wraps a partial's ingested value into the annotated representation, with
|
|
128
|
+
* every string leaf marked permanently inert (§3.8: "no traversal into
|
|
129
|
+
* partial values" — the resolution phases must never rediscover a
|
|
130
|
+
* reference-like substring inside partial content). Called once per partial
|
|
131
|
+
* name at ingestion (see the call site), producing the one canonical tree
|
|
132
|
+
* every pure reference to that partial retrieves — deep-copying it on every
|
|
133
|
+
* such reference (§3.1) is the resolveTree call site's job, not this one.
|
|
134
|
+
*/
|
|
135
|
+
const partialToPositioned = (v, line) => {
|
|
136
|
+
switch (v.kind) {
|
|
137
|
+
case 'null': return { kind: 'null', line };
|
|
138
|
+
case 'bool': return { kind: 'bool', value: v.value, line };
|
|
139
|
+
case 'int': return { kind: 'int', value: v.value, line };
|
|
140
|
+
case 'float': return { kind: 'float', value: v.value, line };
|
|
141
|
+
case 'string': return { kind: 'string', value: v.value, line, quoted: true };
|
|
142
|
+
case 'instant': return { kind: 'instant', value: v.value, line };
|
|
143
|
+
case 'array': return { kind: 'array', items: v.items.map((i) => partialToPositioned(i, line)), line };
|
|
144
|
+
case 'mapping': {
|
|
145
|
+
const entries = new Map();
|
|
146
|
+
for (const [k, c] of v.entries)
|
|
147
|
+
entries.set(k, partialToPositioned(c, line));
|
|
148
|
+
return { kind: 'mapping', entries, line };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Recursively resolves reference-shaped string leaves in `node` against
|
|
154
|
+
* `lookup` — the single function both resolution phases share (§4.1's live
|
|
155
|
+
* backward-reference pass and §4.2's snapshot-based forward pass differ
|
|
156
|
+
* only in which `lookup` map and which source tree they're applied to, not
|
|
157
|
+
* in the resolution logic itself). Errors are collected into `ctx`, never
|
|
158
|
+
* thrown directly — see the module doc comment on error ordering.
|
|
159
|
+
*/
|
|
160
|
+
const resolveTree = (node, lookup, partials, ctx) => {
|
|
161
|
+
// Fast path: a subtree with no active reference-shaped string anywhere
|
|
162
|
+
// inside it has nothing this function could change — skip rebuilding
|
|
163
|
+
// it. Matters most for phase 2's redundant re-walk (see the module doc
|
|
164
|
+
// comment) of a large already-resolved value, e.g. a big partial copied
|
|
165
|
+
// in by several separate references: without this, every element gets
|
|
166
|
+
// a fresh resolveTree call and every array/mapping level gets
|
|
167
|
+
// reconstructed via .map()/a new Map on a pass that can only ever
|
|
168
|
+
// return its input unchanged.
|
|
169
|
+
if (node.kind !== 'string' && isReferenceFreeP(node))
|
|
170
|
+
return node;
|
|
171
|
+
if (node.kind === 'string') {
|
|
172
|
+
if (node.quoted)
|
|
173
|
+
return node;
|
|
174
|
+
const val = node.value;
|
|
175
|
+
// Pure reference: entire value is exactly one ($path) or (%key).
|
|
176
|
+
if (val.charCodeAt(0) === 40 && val.charCodeAt(val.length - 1) === 41) {
|
|
177
|
+
const m = val.match(PURE_REF_RE);
|
|
178
|
+
if (m) {
|
|
179
|
+
const isPartial = m[2] !== undefined;
|
|
180
|
+
const key = (isPartial ? m[2] : m[1]);
|
|
181
|
+
// R-112: stamped on the copy's root only — descendants keep
|
|
182
|
+
// whatever insertedAt they already carried from an earlier,
|
|
183
|
+
// more deeply nested resolution (see deepCopyPositioned).
|
|
184
|
+
const insertedAt = { line: node.line, token: val };
|
|
185
|
+
if (isPartial) {
|
|
186
|
+
const target = partials.get(key);
|
|
187
|
+
// §3.1 deep-copy requirement: `target` is the SAME cached tree
|
|
188
|
+
// built once per partial name at ingestion (see
|
|
189
|
+
// `partialToPositioned`'s call site) — every pure reference to
|
|
190
|
+
// this partial anywhere in the document retrieves that one
|
|
191
|
+
// tree. A shallow spread of its root would leave descendants
|
|
192
|
+
// (including a mutable `Date` in an `instant` node) aliased
|
|
193
|
+
// across every such reference; deep-copying here, exactly like
|
|
194
|
+
// the document-reference branch below, is what actually
|
|
195
|
+
// satisfies the guarantee.
|
|
196
|
+
if (target !== undefined)
|
|
197
|
+
return { ...deepCopyPositioned(target), insertedAt };
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const target = getNestedValueP(lookup, key);
|
|
201
|
+
if (target !== undefined && isReferenceFreeP(target)) {
|
|
202
|
+
return { ...deepCopyPositioned(target), insertedAt };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Unresolved (or target not yet reference-free) — leave unchanged.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// String interpolation: replace all ($path) / (%key) occurrences.
|
|
209
|
+
if (val.includes('($') || val.includes('(%')) {
|
|
210
|
+
const replaced = val.replace(INTERP_RE, (match, docPath, partialKey) => {
|
|
211
|
+
const isPartial = partialKey !== undefined;
|
|
212
|
+
const key = isPartial ? partialKey : docPath;
|
|
213
|
+
const target = isPartial ? partials.get(key) : getNestedValueP(lookup, key);
|
|
214
|
+
if (target === undefined || (!isPartial && !isReferenceFreeP(target)))
|
|
215
|
+
return match;
|
|
216
|
+
if (target.kind === 'mapping') {
|
|
217
|
+
reportDiagnostic(ctx, {
|
|
218
|
+
code: 'INVALID_INTERPOLATION', line: node.line, token: match,
|
|
219
|
+
message: `LIMA: invalid interpolation of "${match}" at line ${node.line}: mapping cannot be interpolated into a string`,
|
|
220
|
+
});
|
|
221
|
+
return match;
|
|
222
|
+
}
|
|
223
|
+
if (target.kind === 'array') {
|
|
224
|
+
if (target.items.some((item) => item.kind === 'array' || item.kind === 'mapping')) {
|
|
225
|
+
reportDiagnostic(ctx, {
|
|
226
|
+
code: 'INVALID_INTERPOLATION', line: node.line, token: match,
|
|
227
|
+
message: `LIMA: invalid interpolation of "${match}" at line ${node.line}: array contains a nested array or mapping`,
|
|
228
|
+
});
|
|
229
|
+
return match;
|
|
230
|
+
}
|
|
231
|
+
return target.items.map((item) => canonicalString(toPlainValue(item))).join(', ');
|
|
232
|
+
}
|
|
233
|
+
return canonicalString(toPlainValue(target));
|
|
234
|
+
});
|
|
235
|
+
// §6.2 final scalar-length limit: interpolation can grow a string
|
|
236
|
+
// past the limit even when neither the raw document text nor the
|
|
237
|
+
// interpolated target individually violated it — a hard error in
|
|
238
|
+
// both modes, thrown immediately like the other resource limits
|
|
239
|
+
// (never part of the ordered diagnostics set below).
|
|
240
|
+
if (codepointLength(replaced) > SCALAR_LENGTH_LIMIT) {
|
|
241
|
+
throw new LimaError({
|
|
242
|
+
code: 'RESOURCE_LIMIT', line: node.line,
|
|
243
|
+
message: `LIMA: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${node.line}`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return { kind: 'string', value: replaced, line: node.line, quoted: false };
|
|
247
|
+
}
|
|
248
|
+
return node;
|
|
249
|
+
}
|
|
250
|
+
if (node.kind === 'array') {
|
|
251
|
+
return {
|
|
252
|
+
kind: 'array',
|
|
253
|
+
line: node.line,
|
|
254
|
+
// Phase 2 redundantly re-walks values phase 1 already resolved
|
|
255
|
+
// (harmless — see the module doc comment) — including, sometimes,
|
|
256
|
+
// a node that phase 1 already stamped with insertedAt. Must be
|
|
257
|
+
// carried over here, not just at the point of insertion, or a
|
|
258
|
+
// reference's provenance silently disappears on the second pass.
|
|
259
|
+
insertedAt: node.insertedAt,
|
|
260
|
+
items: node.items.map((item) => {
|
|
261
|
+
const resolved = resolveTree(item, lookup, partials, ctx);
|
|
262
|
+
if (item.kind === 'string' && resolved.kind === 'array') {
|
|
263
|
+
// References Appendix: array spreading was removed, and a
|
|
264
|
+
// nested array produced by reference insertion violates
|
|
265
|
+
// Core §7.2 (sequences contain scalars or mappings only) —
|
|
266
|
+
// throws in BOTH modes (R-036/R-143).
|
|
267
|
+
reportDiagnostic(ctx, {
|
|
268
|
+
code: 'INVALID_REFERENCE_SHAPE', line: item.line, token: item.value,
|
|
269
|
+
message: `LIMA: reference "${item.value}" resolves to an array, which cannot be inserted as a sequence item at line ${item.line}`,
|
|
270
|
+
});
|
|
271
|
+
return item;
|
|
272
|
+
}
|
|
273
|
+
return resolved;
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (node.kind === 'mapping') {
|
|
278
|
+
const entries = new Map();
|
|
279
|
+
for (const [k, c] of node.entries)
|
|
280
|
+
entries.set(k, resolveTree(c, lookup, partials, ctx));
|
|
281
|
+
return { kind: 'mapping', entries, line: node.line, insertedAt: node.insertedAt };
|
|
282
|
+
}
|
|
283
|
+
return node; // null/bool/int/float/instant — nothing to resolve
|
|
284
|
+
};
|
|
285
|
+
const finalizePositioned = (v) => {
|
|
286
|
+
const own = v.insertedAt ? [v.insertedAt] : [];
|
|
287
|
+
if (v.kind === 'array') {
|
|
288
|
+
if (v.items.length === 0)
|
|
289
|
+
return { native: [], nodeCount: 1, depth: 1, deepestParticipants: own };
|
|
290
|
+
const native = new Array(v.items.length);
|
|
291
|
+
let nodeCount = 1;
|
|
292
|
+
let maxDepth = -1;
|
|
293
|
+
let childParticipants = [];
|
|
294
|
+
for (let i = 0; i < v.items.length; i++) {
|
|
295
|
+
const r = finalizePositioned(v.items[i]);
|
|
296
|
+
native[i] = r.native;
|
|
297
|
+
nodeCount += r.nodeCount;
|
|
298
|
+
if (r.depth > maxDepth) {
|
|
299
|
+
maxDepth = r.depth;
|
|
300
|
+
childParticipants = r.deepestParticipants.slice();
|
|
301
|
+
}
|
|
302
|
+
else if (r.depth === maxDepth) {
|
|
303
|
+
for (const p of r.deepestParticipants)
|
|
304
|
+
childParticipants.push(p);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { native, nodeCount, depth: 1 + maxDepth, deepestParticipants: own.length ? own.concat(childParticipants) : childParticipants };
|
|
308
|
+
}
|
|
309
|
+
if (v.kind === 'mapping') {
|
|
310
|
+
if (v.entries.size === 0)
|
|
311
|
+
return { native: emptyMapping(), nodeCount: 1, depth: 1, deepestParticipants: own };
|
|
312
|
+
const native = emptyMapping();
|
|
313
|
+
let nodeCount = 1;
|
|
314
|
+
let maxDepth = -1;
|
|
315
|
+
let childParticipants = [];
|
|
316
|
+
for (const [k, c] of v.entries) {
|
|
317
|
+
const r = finalizePositioned(c);
|
|
318
|
+
native[k] = r.native;
|
|
319
|
+
nodeCount += r.nodeCount;
|
|
320
|
+
if (r.depth > maxDepth) {
|
|
321
|
+
maxDepth = r.depth;
|
|
322
|
+
childParticipants = r.deepestParticipants.slice();
|
|
323
|
+
}
|
|
324
|
+
else if (r.depth === maxDepth) {
|
|
325
|
+
for (const p of r.deepestParticipants)
|
|
326
|
+
childParticipants.push(p);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { native: native, nodeCount, depth: 1 + maxDepth, deepestParticipants: own.length ? own.concat(childParticipants) : childParticipants };
|
|
330
|
+
}
|
|
331
|
+
// Scalar leaf: null/bool/int/float/string/instant. No further recursion,
|
|
332
|
+
// so plain toNativeFromPositioned is exactly the single-node conversion
|
|
333
|
+
// needed here — reused rather than duplicated.
|
|
334
|
+
return { native: toNativeFromPositioned(v), nodeCount: 1, depth: 0, deepestParticipants: own };
|
|
335
|
+
};
|
|
336
|
+
/** Earliest (lowest-line) participant, or null when none exist — R-113's "line 1" fallback applies then. */
|
|
337
|
+
const earliestParticipant = (participants) => participants.length === 0 ? null : participants.reduce((a, b) => (b.line < a.line ? b : a));
|
|
338
|
+
/** Node-count attribution for the RESOURCE_LIMIT error path: every reference insertion anywhere in the tree contributes to the total. */
|
|
339
|
+
const collectAllParticipants = (v, acc) => {
|
|
340
|
+
if (v.insertedAt)
|
|
341
|
+
acc.push(v.insertedAt);
|
|
342
|
+
if (v.kind === 'array')
|
|
343
|
+
for (const item of v.items)
|
|
344
|
+
collectAllParticipants(item, acc);
|
|
345
|
+
if (v.kind === 'mapping')
|
|
346
|
+
for (const c of v.entries.values())
|
|
347
|
+
collectAllParticipants(c, acc);
|
|
348
|
+
};
|
|
349
|
+
export const parseReferences = (frontMatter, options) => {
|
|
350
|
+
const strict = options?.strict ?? false;
|
|
351
|
+
const rawPartials = options?.partials ?? {};
|
|
352
|
+
// §6.2 / R-114: partials are validated and deep-copied before document
|
|
353
|
+
// parsing begins — this must run even for an empty document. Partial
|
|
354
|
+
// errors carry a value path, never a document line (parsing hasn't
|
|
355
|
+
// started), so they throw directly rather than joining the ordered
|
|
356
|
+
// diagnostics collected below.
|
|
357
|
+
const partialNames = Object.keys(rawPartials);
|
|
358
|
+
if (partialNames.length > PARTIAL_COUNT_LIMIT) {
|
|
359
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', message: `LIMA: too many partials (max ${PARTIAL_COUNT_LIMIT})` });
|
|
360
|
+
}
|
|
361
|
+
for (const name of partialNames) {
|
|
362
|
+
if (codepointLength(name) > PARTIAL_NAME_LENGTH_LIMIT) {
|
|
363
|
+
throw new LimaError({
|
|
364
|
+
code: 'INVALID_PARTIAL', partial: name, path: name,
|
|
365
|
+
message: `LIMA: invalid partial "${name}" at path "${name}": name exceeds maximum length of ${PARTIAL_NAME_LENGTH_LIMIT} code points`,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const partials = new Map();
|
|
370
|
+
for (const [name, value] of Object.entries(rawPartials)) {
|
|
371
|
+
partials.set(name, ingestPartialValue(value, name, name));
|
|
372
|
+
}
|
|
373
|
+
let totalPartialNodes = 0;
|
|
374
|
+
for (const v of partials.values())
|
|
375
|
+
totalPartialNodes += countNodes(v);
|
|
376
|
+
if (totalPartialNodes > PARTIAL_NODE_LIMIT) {
|
|
377
|
+
throw new LimaError({
|
|
378
|
+
code: 'INVALID_PARTIAL',
|
|
379
|
+
message: `LIMA: partials exceed the combined maximum of ${PARTIAL_NODE_LIMIT} value nodes`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// Converted to the annotated representation once per partial, not once
|
|
383
|
+
// per reference: nothing downstream ever mutates a PositionedValue tree
|
|
384
|
+
// in place (resolveTree's fast path returns inert subtrees unchanged;
|
|
385
|
+
// toNativeFromPositioned always allocates fresh native containers for
|
|
386
|
+
// the public result), so every reference to the same partial can safely
|
|
387
|
+
// reuse the same body and only needs its own `insertedAt` stamped onto
|
|
388
|
+
// a shallow copy of the root — no per-reference deep copy required. The
|
|
389
|
+
// internal `line` on partial-derived nodes is never read downstream
|
|
390
|
+
// (every diagnostic that could involve one uses the referencing site's
|
|
391
|
+
// own line instead), so the placeholder `0` here is fine.
|
|
392
|
+
const partialsPositioned = new Map();
|
|
393
|
+
for (const [name, value] of partials)
|
|
394
|
+
partialsPositioned.set(name, partialToPositioned(value, 0));
|
|
395
|
+
const root = parseCoreWithPositions(frontMatter, { strict, onWarning: options?.onWarning });
|
|
396
|
+
const ctx = { best: null };
|
|
397
|
+
const hasRefs = frontMatter.includes('($') || frontMatter.includes('(%');
|
|
398
|
+
let finalMap;
|
|
399
|
+
if (!hasRefs) {
|
|
400
|
+
finalMap = root;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
// Phase 1 (§4.1): resolve each top-level key's value in document
|
|
404
|
+
// order against a live, growing snapshot of already-processed keys
|
|
405
|
+
// — reproduces backward-reference resolution without needing a
|
|
406
|
+
// second pass for the common case.
|
|
407
|
+
const phase1Live = new Map();
|
|
408
|
+
// §3.7/§4 one-hop limit: a top-level key whose OWN inline value was
|
|
409
|
+
// itself a pure reference token must never be usable as another
|
|
410
|
+
// key's hop target via its (possibly already-resolved) live value —
|
|
411
|
+
// only its original token text is a valid phase-2 lookup target, so
|
|
412
|
+
// a chain a→b→c can never fully resolve regardless of where c
|
|
413
|
+
// happens to be written relative to b (§4: independent of mapping
|
|
414
|
+
// enumeration order).
|
|
415
|
+
const originalPureRefText = new Map();
|
|
416
|
+
for (const [key, node] of root) {
|
|
417
|
+
if (node.kind === 'string' && !node.quoted && PURE_REF_RE.test(node.value)) {
|
|
418
|
+
originalPureRefText.set(key, node.value);
|
|
419
|
+
}
|
|
420
|
+
phase1Live.set(key, resolveTree(node, phase1Live, partialsPositioned, ctx));
|
|
421
|
+
}
|
|
422
|
+
// Phase 2 (§4.2): re-resolve every key's phase-1 value — this is
|
|
423
|
+
// what catches forward references — against a snapshot that is
|
|
424
|
+
// immutable for the whole phase (a plain reference to a Map built
|
|
425
|
+
// once above, no cloning needed: nothing here mutates it).
|
|
426
|
+
const phase1Snapshot = new Map();
|
|
427
|
+
for (const [key, value] of phase1Live) {
|
|
428
|
+
phase1Snapshot.set(key, originalPureRefText.has(key)
|
|
429
|
+
? { kind: 'string', value: originalPureRefText.get(key), line: root.get(key).line, quoted: false }
|
|
430
|
+
: value);
|
|
431
|
+
}
|
|
432
|
+
finalMap = new Map();
|
|
433
|
+
for (const [key] of root) {
|
|
434
|
+
finalMap.set(key, resolveTree(phase1Live.get(key), phase1Snapshot, partialsPositioned, ctx));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Strict mode: collect every reference still unresolved after both
|
|
438
|
+
// phases. Each leaf reports its OWN line — every node in this tree
|
|
439
|
+
// carries one from the moment Core parsed it, so no separate
|
|
440
|
+
// provenance-tracking pass is needed to attribute this precisely.
|
|
441
|
+
if (strict) {
|
|
442
|
+
const scanUnresolved = (v) => {
|
|
443
|
+
if (v.kind === 'string') {
|
|
444
|
+
if (!v.quoted && (v.value.includes('($') || v.value.includes('(%'))) {
|
|
445
|
+
const m = v.value.match(/\(([%$])([^)]+)\)/);
|
|
446
|
+
if (m) {
|
|
447
|
+
reportDiagnostic(ctx, {
|
|
448
|
+
code: 'UNRESOLVED_REFERENCE', line: v.line, token: `(${m[1]}${m[2]})`,
|
|
449
|
+
message: `LIMA: unresolved reference "(${m[1]}${m[2]})" at line ${v.line}`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (v.kind === 'array') {
|
|
456
|
+
for (const item of v.items)
|
|
457
|
+
scanUnresolved(item);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (v.kind === 'mapping') {
|
|
461
|
+
for (const c of v.entries.values())
|
|
462
|
+
scanUnresolved(c);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
for (const v of finalMap.values())
|
|
467
|
+
scanUnresolved(v);
|
|
468
|
+
}
|
|
469
|
+
// §5: of every reference-resolution error collected above, the one at
|
|
470
|
+
// the lowest source line is thrown; the rest are discarded.
|
|
471
|
+
if (ctx.best !== null) {
|
|
472
|
+
throw new LimaError(ctx.best);
|
|
473
|
+
}
|
|
474
|
+
// Depth (Core §9, re-checked on the final POST-substitution tree —
|
|
475
|
+
// inserted values can add depth Core's own pre-resolution check inside
|
|
476
|
+
// parseCoreWithPositions could not see yet), node count (§6.2), and the
|
|
477
|
+
// native result itself all come out of one pass over `finalMap` — see
|
|
478
|
+
// finalizePositioned's doc comment for why this replaces three
|
|
479
|
+
// independent full-tree walks.
|
|
480
|
+
const finalResults = [];
|
|
481
|
+
for (const [k, v] of finalMap)
|
|
482
|
+
finalResults.push([k, finalizePositioned(v)]);
|
|
483
|
+
// References §5/R-112: attributed to the lowest-line reference token
|
|
484
|
+
// among the insertions on the actual deepest path, or line 1 (R-113)
|
|
485
|
+
// when the excess depth came entirely from literal, non-reference
|
|
486
|
+
// content.
|
|
487
|
+
const depth = finalResults.length === 0 ? 0 : Math.max(...finalResults.map(([, r]) => r.depth));
|
|
488
|
+
if (depth > NESTING_DEPTH_LIMIT) {
|
|
489
|
+
const participants = finalResults.filter(([, r]) => r.depth === depth).flatMap(([, r]) => r.deepestParticipants);
|
|
490
|
+
const winner = earliestParticipant(participants);
|
|
491
|
+
throw new LimaError({
|
|
492
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
493
|
+
message: winner
|
|
494
|
+
? `LIMA: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner.line}: "${winner.token}"`
|
|
495
|
+
: `LIMA: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line 1`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
// §6.2: total node count of the final result tree, both modes. Same
|
|
499
|
+
// R-112 attribution — every reference insertion anywhere in the tree
|
|
500
|
+
// contributes to the total, so the lowest-line one is reported.
|
|
501
|
+
let totalResultNodes = 1; // the root mapping itself counts as one node
|
|
502
|
+
for (const [, r] of finalResults)
|
|
503
|
+
totalResultNodes += r.nodeCount;
|
|
504
|
+
if (totalResultNodes > RESULT_NODE_LIMIT) {
|
|
505
|
+
const participants = [];
|
|
506
|
+
for (const v of finalMap.values())
|
|
507
|
+
collectAllParticipants(v, participants);
|
|
508
|
+
const winner = earliestParticipant(participants);
|
|
509
|
+
throw new LimaError({
|
|
510
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
511
|
+
message: winner
|
|
512
|
+
? `LIMA: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner.line}: "${winner.token}"`
|
|
513
|
+
: `LIMA: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line 1`,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const out = emptyMapping();
|
|
517
|
+
for (const [k, r] of finalResults)
|
|
518
|
+
out[k] = r.native;
|
|
519
|
+
return out;
|
|
520
|
+
};
|
|
521
|
+
/** Backward-compatible primary entry point — References layered on Core. */
|
|
522
|
+
export const parse = parseReferences;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core scalar grammar: the annotated `PositionedValue` tree type, dates,
|
|
3
|
+
* numbers/type coercion, quoting/escaping, and the shared quoted-or-typed
|
|
4
|
+
* scalar parser every value position (inline values, flow items, block
|
|
5
|
+
* array/map items) builds on.
|
|
6
|
+
*/
|
|
7
|
+
import { type LimaValue } from './value.js';
|
|
8
|
+
import { type ParseContext } from './normalize.js';
|
|
9
|
+
import type { ValueBuilder } from './builder.js';
|
|
10
|
+
/**
|
|
11
|
+
* `insertedAt` is never set by Core — it's a References-only annotation
|
|
12
|
+
* (see references.ts's `resolveTree`), stamped on the root of a value
|
|
13
|
+
* copied in by a successful pure-reference resolution, with the source
|
|
14
|
+
* token and line that caused the insertion. It powers References §5's
|
|
15
|
+
* global-error attribution (R-112): when a final-result limit (nesting
|
|
16
|
+
* depth, total node count) is violated, the lowest-line `insertedAt` among
|
|
17
|
+
* the participating nodes identifies which reference token to blame — the
|
|
18
|
+
* spec requires the error message to include both the token and the line.
|
|
19
|
+
*/
|
|
20
|
+
export type InsertedAt = {
|
|
21
|
+
line: number;
|
|
22
|
+
token: string;
|
|
23
|
+
};
|
|
24
|
+
export type PositionedValue = {
|
|
25
|
+
kind: 'null';
|
|
26
|
+
line: number;
|
|
27
|
+
insertedAt?: InsertedAt;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'bool';
|
|
30
|
+
value: boolean;
|
|
31
|
+
line: number;
|
|
32
|
+
insertedAt?: InsertedAt;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'int';
|
|
35
|
+
value: number;
|
|
36
|
+
line: number;
|
|
37
|
+
insertedAt?: InsertedAt;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'float';
|
|
40
|
+
value: number;
|
|
41
|
+
line: number;
|
|
42
|
+
insertedAt?: InsertedAt;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'string';
|
|
45
|
+
value: string;
|
|
46
|
+
line: number;
|
|
47
|
+
quoted: boolean;
|
|
48
|
+
insertedAt?: InsertedAt;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'instant';
|
|
51
|
+
value: Date;
|
|
52
|
+
line: number;
|
|
53
|
+
insertedAt?: InsertedAt;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'array';
|
|
56
|
+
items: PositionedValue[];
|
|
57
|
+
line: number;
|
|
58
|
+
insertedAt?: InsertedAt;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'mapping';
|
|
61
|
+
entries: Map<string, PositionedValue>;
|
|
62
|
+
line: number;
|
|
63
|
+
insertedAt?: InsertedAt;
|
|
64
|
+
};
|
|
65
|
+
/** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
|
|
66
|
+
export declare const positionedBuilder: ValueBuilder<PositionedValue>;
|
|
67
|
+
/** Strips position/quoted-origin annotations, recursively — the public parseCore() projection. */
|
|
68
|
+
export declare const toPlainValue: (v: PositionedValue) => LimaValue;
|
|
69
|
+
export declare const NO_SPAN_VALUE: unique symbol;
|
|
70
|
+
/**
|
|
71
|
+
* Parses allocation-free scalar forms whose complete grammar can be proven
|
|
72
|
+
* directly from one source span. Everything else returns a sentinel and is
|
|
73
|
+
* handled by the full string-based scalar/flow grammar.
|
|
74
|
+
*/
|
|
75
|
+
export declare const parseSimpleScalarSpan: <V, M>(source: string, start: number, end: number, line: number, strict: boolean, builder: ValueBuilder<V, M>) => V | typeof NO_SPAN_VALUE;
|
|
76
|
+
export declare const unescapeDQ: (s: string, strict?: boolean, line?: number) => string;
|
|
77
|
+
export declare const stripComment: (val: string) => string;
|
|
78
|
+
/** Strips a key's surrounding quotes (unescaping double-quoted keys), or returns it unchanged. */
|
|
79
|
+
export declare const stripKeyQuotes: (s: string) => string;
|
|
80
|
+
/**
|
|
81
|
+
* Quoted-or-typed scalar, shared by every value position (top-level inline
|
|
82
|
+
* values, flow-sequence/flow-mapping items, block-array scalar items).
|
|
83
|
+
* `topLevel` gates two checks that only apply at the outermost resolveValue
|
|
84
|
+
* call site in the legacy parser and are deliberately not extended to flow
|
|
85
|
+
* items here, to keep this a faithful behavioral port: the "unclosed flow
|
|
86
|
+
* bracket" throw and the "non-whitespace after closing quote" strict throw.
|
|
87
|
+
*/
|
|
88
|
+
export declare const parseQuotedOrTyped: <V, M>(raw: string, ctx: ParseContext, line: number, topLevel: boolean, builder: ValueBuilder<V, M>) => V;
|
|
89
|
+
export declare const parseScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>) => V;
|