@limaformat/lima 0.3.1 → 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 +70 -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 +47 -16
- 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 +22 -24
|
@@ -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
|
};
|
|
@@ -75,9 +83,9 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
75
83
|
const match = tokenMatches(item)[0];
|
|
76
84
|
const token = match?.token ?? item.value;
|
|
77
85
|
addDiagnostic(ctx, {
|
|
78
|
-
code: 'INVALID_REFERENCE_SHAPE', line: item.line, token,
|
|
79
|
-
message: `Lima: reference "${token}" resolves to an array, which cannot be inserted as a sequence item at line ${item.line}`,
|
|
80
|
-
});
|
|
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);
|
|
81
89
|
return item;
|
|
82
90
|
}
|
|
83
91
|
return result.value;
|
|
@@ -120,8 +128,10 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
120
128
|
if (target === undefined || stack.has(target))
|
|
121
129
|
return { value: node, complete: false };
|
|
122
130
|
if (pure.partialPath !== undefined) {
|
|
131
|
+
// `partialToPositioned` never annotates partial targets: partials are
|
|
132
|
+
// inert and have no source token before this document insertion.
|
|
123
133
|
return {
|
|
124
|
-
value:
|
|
134
|
+
value: copyWithInsertion(target, { line: pure.line, offset: pure.offset, token: pure.token }),
|
|
125
135
|
complete: true,
|
|
126
136
|
};
|
|
127
137
|
}
|
|
@@ -134,7 +144,9 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
134
144
|
if (selected === undefined)
|
|
135
145
|
return { value: node, complete: false };
|
|
136
146
|
return {
|
|
137
|
-
|
|
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 }),
|
|
138
150
|
complete: true,
|
|
139
151
|
};
|
|
140
152
|
}
|
|
@@ -182,13 +194,24 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
182
194
|
}
|
|
183
195
|
return { value: { ...node, value: output, references2: unresolvedTokens }, complete };
|
|
184
196
|
};
|
|
185
|
-
/**
|
|
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
|
+
*/
|
|
186
209
|
const resolveNode = (node, document, partials, ctx, remainingEdges, stack) => {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
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)))
|
|
190
213
|
return resolveNodeUncached(node, document, partials, ctx, remainingEdges, stack);
|
|
191
|
-
const byBudget =
|
|
214
|
+
const byBudget = cache.get(node);
|
|
192
215
|
const cached = byBudget?.get(remainingEdges);
|
|
193
216
|
if (cached !== undefined)
|
|
194
217
|
return cached;
|
|
@@ -196,7 +219,7 @@ const resolveNode = (node, document, partials, ctx, remainingEdges, stack) => {
|
|
|
196
219
|
if (byBudget !== undefined)
|
|
197
220
|
byBudget.set(remainingEdges, result);
|
|
198
221
|
else
|
|
199
|
-
|
|
222
|
+
cache.set(node, new Map([[remainingEdges, result]]));
|
|
200
223
|
return result;
|
|
201
224
|
};
|
|
202
225
|
const validatePartials = (raw) => {
|
|
@@ -243,7 +266,7 @@ const scanUnresolved = (node, ctx) => {
|
|
|
243
266
|
for (const child of node.entries.values())
|
|
244
267
|
scanUnresolved(child, ctx);
|
|
245
268
|
};
|
|
246
|
-
|
|
269
|
+
const parseInternal = (frontMatter, options, useResolveCache) => {
|
|
247
270
|
if (options?.mode !== undefined && options.mode !== 'core' && options.mode !== 'references') {
|
|
248
271
|
throw new TypeError(`Lima: invalid parse mode "${String(options.mode)}"`);
|
|
249
272
|
}
|
|
@@ -260,7 +283,7 @@ export const parse = (frontMatter, options) => {
|
|
|
260
283
|
return parseCore(frontMatter, options);
|
|
261
284
|
}
|
|
262
285
|
const document = parseCoreWithPositions(frontMatter, { strict: options?.strict ?? false, onWarning: options?.onWarning });
|
|
263
|
-
const ctx = { diagnostics: [], cache: new WeakMap() };
|
|
286
|
+
const ctx = { diagnostics: [], ...(useResolveCache ? { cache: new WeakMap() } : {}) };
|
|
264
287
|
const resolved = new Map();
|
|
265
288
|
for (const [key, value] of document) {
|
|
266
289
|
const stack = new Set([value]);
|
|
@@ -271,7 +294,10 @@ export const parse = (frontMatter, options) => {
|
|
|
271
294
|
scanUnresolved(value, ctx);
|
|
272
295
|
if (ctx.diagnostics.length > 0) {
|
|
273
296
|
ctx.diagnostics.sort((a, b) => a.line - b.line || a.offset - b.offset);
|
|
274
|
-
|
|
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 });
|
|
275
301
|
}
|
|
276
302
|
const finalized = [...resolved].map(([key, value]) => [key, finalizePositioned(value)]);
|
|
277
303
|
const depth = finalized.length === 0 ? 0 : Math.max(...finalized.map(([, value]) => value.depth));
|
|
@@ -280,6 +306,7 @@ export const parse = (frontMatter, options) => {
|
|
|
280
306
|
const winner = earliestParticipant(participants);
|
|
281
307
|
throw new LimaError({
|
|
282
308
|
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
309
|
+
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
283
310
|
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner?.line ?? 1}`,
|
|
284
311
|
});
|
|
285
312
|
}
|
|
@@ -293,6 +320,7 @@ export const parse = (frontMatter, options) => {
|
|
|
293
320
|
const winner = earliestParticipant(participants);
|
|
294
321
|
throw new LimaError({
|
|
295
322
|
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
323
|
+
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
296
324
|
message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
|
|
297
325
|
});
|
|
298
326
|
}
|
|
@@ -301,5 +329,8 @@ export const parse = (frontMatter, options) => {
|
|
|
301
329
|
out[key] = value.native;
|
|
302
330
|
return out;
|
|
303
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);
|
|
304
335
|
/** @deprecated Use parse(). */
|
|
305
336
|
export const parseReferences = parse;
|