@limaformat/lima 0.3.0 → 0.4.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 +79 -0
- package/dist/block-scalar.d.ts +33 -0
- package/dist/block-scalar.js +109 -0
- package/dist/block.js +167 -80
- package/dist/builder.d.ts +8 -9
- package/dist/core.js +43 -89
- package/dist/flow.d.ts +4 -3
- package/dist/flow.js +97 -16
- package/dist/normalize.d.ts +8 -9
- package/dist/normalize.js +2 -11
- package/dist/positioned-tree.d.ts +73 -0
- package/dist/positioned-tree.js +123 -0
- package/dist/reference-tokens2.d.ts +34 -3
- package/dist/reference-tokens2.js +56 -23
- package/dist/references.d.ts +1 -64
- package/dist/references.js +2 -113
- package/dist/references2.d.ts +2 -0
- package/dist/references2.js +59 -18
- package/dist/scalars.d.ts +84 -14
- package/dist/scalars.js +158 -29
- package/dist/scanner.d.ts +26 -13
- package/dist/scanner.js +33 -15
- package/package.json +1 -1
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
/** Reference tokens recorded while the annotated Core tree is being built. */
|
|
2
|
-
import type { StringSourceSpan } from './builder.js';
|
|
3
2
|
export type ReferenceToken2 = {
|
|
4
3
|
token: string;
|
|
4
|
+
/**
|
|
5
|
+
* Index of the token within the decoded scalar value — the splice point
|
|
6
|
+
* for interpolation. A UTF-16 offset (JS `String.slice` units); stays
|
|
7
|
+
* implementation-local, unlike `line`/`offset`.
|
|
8
|
+
*/
|
|
5
9
|
index: number;
|
|
6
|
-
|
|
10
|
+
/** Physical 1-based source line of the token (References 2.0 §2.4). */
|
|
7
11
|
line: number;
|
|
12
|
+
/**
|
|
13
|
+
* 0-based **codepoint** offset of the token within its source line — the
|
|
14
|
+
* `column - 1` a diagnostic reports, and the tie-break key for §5 error
|
|
15
|
+
* ordering. Identical across implementations.
|
|
16
|
+
*/
|
|
17
|
+
offset: number;
|
|
8
18
|
documentPath?: string;
|
|
9
19
|
partialPath?: string;
|
|
10
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Where a scalar begins in the source: physical 1-based `line` and 0-based
|
|
23
|
+
* codepoint column `col` of its first character. A token's position is this
|
|
24
|
+
* anchor plus its codepoint offset within the value.
|
|
25
|
+
*
|
|
26
|
+
* `raw` is the value text a token's position is *read from*, before comment
|
|
27
|
+
* stripping and `\#` collapse (so `x: a\#b ${t}` reports `${t}` at its real
|
|
28
|
+
* column) — for a block scalar it is the original body lines joined with
|
|
29
|
+
* `\n` (§2.4 forbids reconstructing a token's line from the `^^`-merged
|
|
30
|
+
* string). Omitted only when it equals the decoded value.
|
|
31
|
+
*
|
|
32
|
+
* `tabAdjust[i]` is the column count Core §3 added to physical line
|
|
33
|
+
* `line + i` by leading-tab expansion; it is subtracted so the reported
|
|
34
|
+
* column is the *original*-source column (§2.4).
|
|
35
|
+
*/
|
|
36
|
+
export type ReferenceSource = {
|
|
37
|
+
raw?: string;
|
|
38
|
+
line: number;
|
|
39
|
+
col: number;
|
|
40
|
+
tabAdjust?: number[];
|
|
41
|
+
};
|
|
11
42
|
export declare const PURE_REFERENCE_2: RegExp;
|
|
12
|
-
export declare const scanReferenceTokens2: (value: string,
|
|
43
|
+
export declare const scanReferenceTokens2: (value: string, source: ReferenceSource | undefined) => ReferenceToken2[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Reference tokens recorded while the annotated Core tree is being built. */
|
|
1
2
|
const SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
|
|
2
3
|
const DOC_PATH = `${SEGMENT}(?:\\.${SEGMENT})*`;
|
|
3
4
|
const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
@@ -5,33 +6,65 @@ const PARTIAL_PATH = `${PARTIAL_NAME}(?:\\.${SEGMENT})*`;
|
|
|
5
6
|
export const PURE_REFERENCE_2 = new RegExp(`^(?:\\$\\{(${DOC_PATH})\\}|\\$\\((${PARTIAL_PATH})\\))$`);
|
|
6
7
|
const REFERENCE_2 = new RegExp(`\\$\\{(${DOC_PATH})\\}|\\$\\((${PARTIAL_PATH})\\)`, 'g');
|
|
7
8
|
const NO_TOKENS = [];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/** Codepoint length of the UTF-16 slice `[a, b)` of `s`. */
|
|
10
|
+
const codepointLen = (s, a, b) => {
|
|
11
|
+
let n = 0;
|
|
12
|
+
for (let i = a; i < b;) {
|
|
13
|
+
i += s.codePointAt(i) > 0xffff ? 2 : 1;
|
|
14
|
+
n++;
|
|
15
|
+
}
|
|
16
|
+
return n;
|
|
17
|
+
};
|
|
18
|
+
/** Splice points (decoded value order): token text, UTF-16 index, path. */
|
|
19
|
+
const scanDecoded = (value) => {
|
|
11
20
|
REFERENCE_2.lastIndex = 0;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const m of value.matchAll(REFERENCE_2)) {
|
|
23
|
+
out.push({
|
|
24
|
+
token: m[0], index: m.index ?? 0,
|
|
25
|
+
...(m[1] !== undefined ? { documentPath: m[1] } : { partialPath: m[2] }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
};
|
|
30
|
+
/** Physical `(line, offset)` of each token, in source order, from the raw text. */
|
|
31
|
+
const scanPhysical = (raw, firstLine, firstCol, tabAdjust) => {
|
|
32
|
+
REFERENCE_2.lastIndex = 0;
|
|
33
|
+
const out = [];
|
|
15
34
|
let scanned = 0;
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
let line = firstLine;
|
|
36
|
+
let col = firstCol;
|
|
37
|
+
let lineIndex = 0;
|
|
38
|
+
for (const m of raw.matchAll(REFERENCE_2)) {
|
|
39
|
+
const idx = m.index ?? 0;
|
|
40
|
+
while (scanned < idx) {
|
|
41
|
+
const cp = raw.codePointAt(scanned);
|
|
42
|
+
if (cp === 10) {
|
|
21
43
|
line++;
|
|
22
|
-
|
|
44
|
+
col = 0;
|
|
45
|
+
lineIndex++;
|
|
23
46
|
}
|
|
24
|
-
|
|
47
|
+
else
|
|
48
|
+
col++;
|
|
49
|
+
scanned += cp > 0xffff ? 2 : 1;
|
|
25
50
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
out.push({ line, offset: col - (tabAdjust?.[lineIndex] ?? 0) });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
};
|
|
55
|
+
export const scanReferenceTokens2 = (value, source) => {
|
|
56
|
+
if (!value.includes('${') && !value.includes('$('))
|
|
57
|
+
return NO_TOKENS;
|
|
58
|
+
const decoded = scanDecoded(value);
|
|
59
|
+
if (decoded.length === 0)
|
|
60
|
+
return NO_TOKENS;
|
|
61
|
+
// The raw and decoded scans see the same tokens in the same order —
|
|
62
|
+
// `\#` collapse and `^^` merge never add or remove a `${…}` / `$(…)`.
|
|
63
|
+
const physical = source === undefined
|
|
64
|
+
? scanPhysical(value, 1, 0)
|
|
65
|
+
: scanPhysical(source.raw ?? value, source.line, source.col, source.tabAdjust);
|
|
66
|
+
if (physical.length !== decoded.length) {
|
|
67
|
+
throw new Error(`Lima internal: reference token count mismatch (raw ${physical.length}, decoded ${decoded.length})`);
|
|
35
68
|
}
|
|
36
|
-
return
|
|
69
|
+
return decoded.map((d, i) => ({ ...d, line: physical[i].line, offset: physical[i].offset }));
|
|
37
70
|
};
|
package/dist/references.d.ts
CHANGED
|
@@ -20,71 +20,8 @@
|
|
|
20
20
|
* There is no module-level mutable state: diagnostics are collected in a
|
|
21
21
|
* `ResolutionContext` created fresh per call and threaded explicitly.
|
|
22
22
|
*/
|
|
23
|
-
import { type
|
|
24
|
-
import { type PositionedValue, type Diagnostic, type InsertedAt, type NativeValue } from './core.js';
|
|
23
|
+
import { type Diagnostic } from './core.js';
|
|
25
24
|
type Meta = Record<string, unknown>;
|
|
26
|
-
/**
|
|
27
|
-
* Structural deep copy for document-derived targets — preserves each leaf's
|
|
28
|
-
* own line/quoted (§4.2/R-104: still eligible for further resolution) and
|
|
29
|
-
* any existing `insertedAt` provenance from an earlier, nested resolution
|
|
30
|
-
* (R-112: a reference copied wholesale into a new position can itself
|
|
31
|
-
* already contain the result of an inner reference that resolved first —
|
|
32
|
-
* both insertion sites remain identifiable participants after the copy).
|
|
33
|
-
*/
|
|
34
|
-
export declare const deepCopyPositioned: (v: PositionedValue) => PositionedValue;
|
|
35
|
-
/**
|
|
36
|
-
* Wraps a partial's ingested value into the annotated representation, with
|
|
37
|
-
* every string leaf marked permanently inert (§3.8: "no traversal into
|
|
38
|
-
* partial values" — the resolution phases must never rediscover a
|
|
39
|
-
* reference-like substring inside partial content). Called once per partial
|
|
40
|
-
* name at ingestion (see the call site), producing the one canonical tree
|
|
41
|
-
* every pure reference to that partial retrieves — deep-copying it on every
|
|
42
|
-
* such reference (§3.1) is the resolveTree call site's job, not this one.
|
|
43
|
-
*/
|
|
44
|
-
export declare const partialToPositioned: (v: LimaValue, line: number) => PositionedValue;
|
|
45
|
-
/**
|
|
46
|
-
* References §5/R-112 (nesting depth), §6.2 (node count), and the public
|
|
47
|
-
* result shape (native conversion) computed together in one recursive pass
|
|
48
|
-
* over the final, post-substitution tree — instead of three independent
|
|
49
|
-
* full-tree walks (a depth-only pass, a count-only pass, then
|
|
50
|
-
* `toNativeFromPositioned`), each of which would otherwise revisit every
|
|
51
|
-
* node of what can be a large, reference-expanded result. Depth attribution
|
|
52
|
-
* needs more than a depth number: it needs to know which reference
|
|
53
|
-
* insertions lie on the actual deepest path, so the earliest of those (by
|
|
54
|
-
* line) can be blamed if the limit is exceeded — ties for "deepest child"
|
|
55
|
-
* all count, since a violation can be reached via more than one
|
|
56
|
-
* maximal-depth branch. `native` is still built unconditionally even
|
|
57
|
-
* though a depth/node-count violation elsewhere in the document means it
|
|
58
|
-
* will be discarded and never returned — cheaper than adding a second
|
|
59
|
-
* conditional branch to every call site, and the discarded case is the
|
|
60
|
-
* rare (error) path, not the one this pass exists to speed up.
|
|
61
|
-
*
|
|
62
|
-
* Each array/mapping branch tracks a running max depth and its participant
|
|
63
|
-
* list in a single loop over its children, instead of first materializing a
|
|
64
|
-
* full array of child results and then doing separate `.map()`/`Math.max`/
|
|
65
|
-
* `.filter().flatMap()` passes over it — for a wide sibling group (many
|
|
66
|
-
* children at the same depth, the common shape for flat documents with many
|
|
67
|
-
* top-level references) that would mean an extra full pass per level on top
|
|
68
|
-
* of the traversal itself. A running accumulator only ever copies-on-write:
|
|
69
|
-
* `.slice()` when a strictly greater depth replaces the running set, plain
|
|
70
|
-
* `.push()` onto that fresh copy for a tie at the current max — never a
|
|
71
|
-
* mutation of a child's own returned array. Reusing a child's array in
|
|
72
|
-
* place would currently be safe (each node is visited exactly once, no
|
|
73
|
-
* aliasing left after the pure-reference deep-copy fix), but relying on
|
|
74
|
-
* that invariant here has no upside worth the risk in a file that has
|
|
75
|
-
* already had one real aliasing bug.
|
|
76
|
-
*/
|
|
77
|
-
export type FinalizedValue = {
|
|
78
|
-
native: NativeValue;
|
|
79
|
-
nodeCount: number;
|
|
80
|
-
depth: number;
|
|
81
|
-
deepestParticipants: InsertedAt[];
|
|
82
|
-
};
|
|
83
|
-
export declare const finalizePositioned: (v: PositionedValue) => FinalizedValue;
|
|
84
|
-
/** Earliest (lowest-line) participant, or null when none exist — R-113's "line 1" fallback applies then. */
|
|
85
|
-
export declare const earliestParticipant: (participants: InsertedAt[]) => InsertedAt | null;
|
|
86
|
-
/** Node-count attribution for the RESOURCE_LIMIT error path: every reference insertion anywhere in the tree contributes to the total. */
|
|
87
|
-
export declare const collectAllParticipants: (v: PositionedValue, acc: InsertedAt[]) => void;
|
|
88
25
|
export type ReferencesOptions = {
|
|
89
26
|
/** Named values available via `(%key)` references. */
|
|
90
27
|
partials?: Meta;
|
package/dist/references.js
CHANGED
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* `ResolutionContext` created fresh per call and threaded explicitly.
|
|
22
22
|
*/
|
|
23
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,
|
|
24
|
+
import { parseCoreWithPositions, toPlainValue, NESTING_DEPTH_LIMIT, } from './core.js';
|
|
25
25
|
import { LimaError } from './errors.js';
|
|
26
|
-
|
|
26
|
+
import { collectAllParticipants, deepCopyPositioned, earliestParticipant, emptyMapping, finalizePositioned, partialToPositioned, } from './positioned-tree.js';
|
|
27
27
|
// References §2.1/§2.2/Appendix B grammar, per sigil.
|
|
28
28
|
const DOC_SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
|
|
29
29
|
const DOC_PATH = `${DOC_SEGMENT}(?:\\.${DOC_SEGMENT})*`;
|
|
@@ -102,53 +102,6 @@ const isReferenceFreeP = (v) => {
|
|
|
102
102
|
referenceFreeCache.set(v, result);
|
|
103
103
|
return result;
|
|
104
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
|
-
export const deepCopyPositioned = (v) => {
|
|
114
|
-
switch (v.kind) {
|
|
115
|
-
case 'array': return { kind: 'array', items: v.items.map(deepCopyPositioned), line: v.line, references2Active: v.references2Active, 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, references2Active: v.references2Active, 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
|
-
export 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
105
|
/**
|
|
153
106
|
* Recursively resolves reference-shaped string leaves in `node` against
|
|
154
107
|
* `lookup` — the single function both resolution phases share (§4.1's live
|
|
@@ -282,70 +235,6 @@ const resolveTree = (node, lookup, partials, ctx) => {
|
|
|
282
235
|
}
|
|
283
236
|
return node; // null/bool/int/float/instant — nothing to resolve
|
|
284
237
|
};
|
|
285
|
-
export 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
|
-
export 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
|
-
export 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
238
|
export const parseReferences = (frontMatter, options) => {
|
|
350
239
|
const strict = options?.strict ?? false;
|
|
351
240
|
const rawPartials = options?.partials ?? {};
|
package/dist/references2.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type ParseOptions = {
|
|
|
8
8
|
onWarning?: (diagnostic: Diagnostic) => void;
|
|
9
9
|
};
|
|
10
10
|
export declare const parse: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: ParseOptions) => T;
|
|
11
|
+
/** Test oracle: exercises the resolver without its dependency-result cache. */
|
|
12
|
+
export declare const __parseWithoutResolveCacheForTest: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: ParseOptions) => T;
|
|
11
13
|
/** @deprecated Use parse(). */
|
|
12
14
|
export declare const parseReferences: typeof parse;
|
|
13
15
|
export {};
|
package/dist/references2.js
CHANGED
|
@@ -3,11 +3,19 @@ import { canonicalString, codepointLength, countNodes, ingestPartialValue, PARTI
|
|
|
3
3
|
import { NESTING_DEPTH_LIMIT, parseCore, parseCoreWithPositions, toPlainValue, } from './core.js';
|
|
4
4
|
import { hasActiveReferences2 } from './scalars.js';
|
|
5
5
|
import { LimaError } from './errors.js';
|
|
6
|
-
import { collectAllParticipants, deepCopyPositioned, earliestParticipant, finalizePositioned, partialToPositioned, } from './
|
|
7
|
-
const emptyMapping = () => Object.create(null);
|
|
6
|
+
import { collectAllParticipants, deepCopyPositioned, earliestParticipant, emptyMapping, finalizePositioned, partialToPositioned, } from './positioned-tree.js';
|
|
8
7
|
const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
9
8
|
const PARTIAL_NAME_RE = new RegExp(`^${PARTIAL_NAME}$`);
|
|
10
9
|
const MAX_EDGES = 3;
|
|
10
|
+
/** Copies a pure-reference target and stamps the new insertion without
|
|
11
|
+
* discarding provenance already attached to the copied root (R-112). */
|
|
12
|
+
const copyWithInsertion = (target, insertedAt) => {
|
|
13
|
+
const copy = deepCopyPositioned(target);
|
|
14
|
+
const priorInsertions = copy.insertedAt === undefined
|
|
15
|
+
? copy.priorInsertions
|
|
16
|
+
: [...(copy.priorInsertions ?? []), copy.insertedAt];
|
|
17
|
+
return { ...copy, insertedAt, priorInsertions };
|
|
18
|
+
};
|
|
11
19
|
const addDiagnostic = (ctx, diagnostic, offset = 0) => {
|
|
12
20
|
ctx.diagnostics.push({ ...diagnostic, offset });
|
|
13
21
|
};
|
|
@@ -23,8 +31,18 @@ const lookupPath = (root, segments) => {
|
|
|
23
31
|
};
|
|
24
32
|
const documentTarget = (root, path) => {
|
|
25
33
|
const segments = path.split('.');
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
let value = root.get(segments[0]);
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
let index = 1;
|
|
38
|
+
while (index < segments.length && value.kind === 'mapping') {
|
|
39
|
+
const child = value.entries.get(segments[index]);
|
|
40
|
+
if (child === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
value = child;
|
|
43
|
+
index++;
|
|
44
|
+
}
|
|
45
|
+
return { root: value, tail: segments.slice(index) };
|
|
28
46
|
};
|
|
29
47
|
const lookupPartial = (partials, path) => {
|
|
30
48
|
const dot = path.indexOf('.');
|
|
@@ -65,9 +83,9 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
65
83
|
const match = tokenMatches(item)[0];
|
|
66
84
|
const token = match?.token ?? item.value;
|
|
67
85
|
addDiagnostic(ctx, {
|
|
68
|
-
code: 'INVALID_REFERENCE_SHAPE', line: item.line, token,
|
|
69
|
-
message: `Lima: reference "${token}" resolves to an array, which cannot be inserted as a sequence item at line ${item.line}`,
|
|
70
|
-
});
|
|
86
|
+
code: 'INVALID_REFERENCE_SHAPE', line: match?.line ?? item.line, token,
|
|
87
|
+
message: `Lima: reference "${token}" resolves to an array, which cannot be inserted as a sequence item at line ${match?.line ?? item.line}`,
|
|
88
|
+
}, match?.offset ?? 0);
|
|
71
89
|
return item;
|
|
72
90
|
}
|
|
73
91
|
return result.value;
|
|
@@ -110,8 +128,10 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
110
128
|
if (target === undefined || stack.has(target))
|
|
111
129
|
return { value: node, complete: false };
|
|
112
130
|
if (pure.partialPath !== undefined) {
|
|
131
|
+
// `partialToPositioned` never annotates partial targets: partials are
|
|
132
|
+
// inert and have no source token before this document insertion.
|
|
113
133
|
return {
|
|
114
|
-
value:
|
|
134
|
+
value: copyWithInsertion(target, { line: pure.line, offset: pure.offset, token: pure.token }),
|
|
115
135
|
complete: true,
|
|
116
136
|
};
|
|
117
137
|
}
|
|
@@ -124,7 +144,9 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
124
144
|
if (selected === undefined)
|
|
125
145
|
return { value: node, complete: false };
|
|
126
146
|
return {
|
|
127
|
-
|
|
147
|
+
// `selected` may itself be the root inserted by an earlier reference;
|
|
148
|
+
// both it and this outer copy participate in final-structure errors.
|
|
149
|
+
value: copyWithInsertion(selected, { line: pure.line, offset: pure.offset, token: pure.token }),
|
|
128
150
|
complete: true,
|
|
129
151
|
};
|
|
130
152
|
}
|
|
@@ -172,13 +194,24 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
172
194
|
}
|
|
173
195
|
return { value: { ...node, value: output, references2: unresolvedTokens }, complete };
|
|
174
196
|
};
|
|
175
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* The cache key is complete without `stack`: for a fixed node and edge
|
|
199
|
+
* budget, its outgoing targets and traversal order are fixed by the parsed
|
|
200
|
+
* document. If one of those targets is already on the current stack, that
|
|
201
|
+
* target is on a dependency cycle reachable from this node; entering from a
|
|
202
|
+
* different ancestor cannot make the cycle complete, and the monotonically
|
|
203
|
+
* decreasing edge budget fixes where an overlong path becomes unresolved.
|
|
204
|
+
* Thus `stack` only detects an already-inevitable incomplete dependency; it
|
|
205
|
+
* does not select a different successful value. Top-level calls are excluded
|
|
206
|
+
* (`stack.size > 1`) and cached Resolution trees are immutable, so sharing a
|
|
207
|
+
* successful or incomplete result at the same remaining budget is safe.
|
|
208
|
+
*/
|
|
176
209
|
const resolveNode = (node, document, partials, ctx, remainingEdges, stack) => {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
const cache = ctx.cache;
|
|
211
|
+
if (cache === undefined || stack.size <= 1 || !(node.kind === 'array' || node.kind === 'mapping' ||
|
|
212
|
+
(isActiveString(node) && tokenMatches(node).length > 0)))
|
|
180
213
|
return resolveNodeUncached(node, document, partials, ctx, remainingEdges, stack);
|
|
181
|
-
const byBudget =
|
|
214
|
+
const byBudget = cache.get(node);
|
|
182
215
|
const cached = byBudget?.get(remainingEdges);
|
|
183
216
|
if (cached !== undefined)
|
|
184
217
|
return cached;
|
|
@@ -186,7 +219,7 @@ const resolveNode = (node, document, partials, ctx, remainingEdges, stack) => {
|
|
|
186
219
|
if (byBudget !== undefined)
|
|
187
220
|
byBudget.set(remainingEdges, result);
|
|
188
221
|
else
|
|
189
|
-
|
|
222
|
+
cache.set(node, new Map([[remainingEdges, result]]));
|
|
190
223
|
return result;
|
|
191
224
|
};
|
|
192
225
|
const validatePartials = (raw) => {
|
|
@@ -233,7 +266,7 @@ const scanUnresolved = (node, ctx) => {
|
|
|
233
266
|
for (const child of node.entries.values())
|
|
234
267
|
scanUnresolved(child, ctx);
|
|
235
268
|
};
|
|
236
|
-
|
|
269
|
+
const parseInternal = (frontMatter, options, useResolveCache) => {
|
|
237
270
|
if (options?.mode !== undefined && options.mode !== 'core' && options.mode !== 'references') {
|
|
238
271
|
throw new TypeError(`Lima: invalid parse mode "${String(options.mode)}"`);
|
|
239
272
|
}
|
|
@@ -250,7 +283,7 @@ export const parse = (frontMatter, options) => {
|
|
|
250
283
|
return parseCore(frontMatter, options);
|
|
251
284
|
}
|
|
252
285
|
const document = parseCoreWithPositions(frontMatter, { strict: options?.strict ?? false, onWarning: options?.onWarning });
|
|
253
|
-
const ctx = { diagnostics: [], cache: new WeakMap() };
|
|
286
|
+
const ctx = { diagnostics: [], ...(useResolveCache ? { cache: new WeakMap() } : {}) };
|
|
254
287
|
const resolved = new Map();
|
|
255
288
|
for (const [key, value] of document) {
|
|
256
289
|
const stack = new Set([value]);
|
|
@@ -261,7 +294,10 @@ export const parse = (frontMatter, options) => {
|
|
|
261
294
|
scanUnresolved(value, ctx);
|
|
262
295
|
if (ctx.diagnostics.length > 0) {
|
|
263
296
|
ctx.diagnostics.sort((a, b) => a.line - b.line || a.offset - b.offset);
|
|
264
|
-
|
|
297
|
+
const winner = ctx.diagnostics[0];
|
|
298
|
+
// §2.4: expose the token's physical position (1-based) — Rust and Go
|
|
299
|
+
// already do; `offset` was internal-only here.
|
|
300
|
+
throw new LimaError({ ...winner, column: winner.offset + 1 });
|
|
265
301
|
}
|
|
266
302
|
const finalized = [...resolved].map(([key, value]) => [key, finalizePositioned(value)]);
|
|
267
303
|
const depth = finalized.length === 0 ? 0 : Math.max(...finalized.map(([, value]) => value.depth));
|
|
@@ -270,6 +306,7 @@ export const parse = (frontMatter, options) => {
|
|
|
270
306
|
const winner = earliestParticipant(participants);
|
|
271
307
|
throw new LimaError({
|
|
272
308
|
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
309
|
+
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
273
310
|
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner?.line ?? 1}`,
|
|
274
311
|
});
|
|
275
312
|
}
|
|
@@ -283,6 +320,7 @@ export const parse = (frontMatter, options) => {
|
|
|
283
320
|
const winner = earliestParticipant(participants);
|
|
284
321
|
throw new LimaError({
|
|
285
322
|
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
323
|
+
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
286
324
|
message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
|
|
287
325
|
});
|
|
288
326
|
}
|
|
@@ -291,5 +329,8 @@ export const parse = (frontMatter, options) => {
|
|
|
291
329
|
out[key] = value.native;
|
|
292
330
|
return out;
|
|
293
331
|
};
|
|
332
|
+
export const parse = (frontMatter, options) => parseInternal(frontMatter, options, true);
|
|
333
|
+
/** Test oracle: exercises the resolver without its dependency-result cache. */
|
|
334
|
+
export const __parseWithoutResolveCacheForTest = (frontMatter, options) => parseInternal(frontMatter, options, false);
|
|
294
335
|
/** @deprecated Use parse(). */
|
|
295
336
|
export const parseReferences = parse;
|