@limaformat/lima 0.1.0 → 0.2.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 +25 -1
- package/README.md +16 -11
- package/dist/block.js +6 -6
- package/dist/builder.d.ts +9 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js +33 -12
- package/dist/flow.js +5 -5
- package/dist/index.d.ts +8 -7
- package/dist/index.js +7 -7
- package/dist/normalize.js +3 -3
- package/dist/reference-tokens2.d.ts +12 -0
- package/dist/reference-tokens2.js +37 -0
- package/dist/references.d.ts +67 -2
- package/dist/references.js +22 -20
- package/dist/references2.d.ts +13 -0
- package/dist/references2.js +295 -0
- package/dist/scalars.d.ts +5 -0
- package/dist/scalars.js +33 -10
- package/dist/value.js +4 -4
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,7 +10,31 @@ and [Lima References
|
|
|
10
10
|
1.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-1.0-spec.md)
|
|
11
11
|
are both frozen regardless of what this file shows.
|
|
12
12
|
|
|
13
|
-
## [
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
## [0.2.0] — 2026-08-11
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Breaking:** `parse(input, options?)` now implements Lima References 2.0.
|
|
20
|
+
Document references use `$(key)` instead of `($key)`, and partial
|
|
21
|
+
references use `$(:key)` instead of `(%key)`. References 1.0 tokens are
|
|
22
|
+
literal text through the public 2.0 API.
|
|
23
|
+
- `parseReferences(input, options?)` is now a deprecated compatibility alias
|
|
24
|
+
for `parse` with identical References 2.0 results and diagnostics. It is not
|
|
25
|
+
a References 1.0 mode.
|
|
26
|
+
- `ParseOptions.mode` selects `"references"` (the default) or `"core"`.
|
|
27
|
+
Core mode uses the same reference-unaware path as `parseCore`; supplying
|
|
28
|
+
`partials` in Core mode is invalid. `parseCore` itself is unchanged.
|
|
29
|
+
- References 2.0 adds bounded transitive resolution (at most three reference
|
|
30
|
+
edges), partial mapping traversal, and `$(:namespace/name.path)` partials.
|
|
31
|
+
- Error message prefix `LIMA:` → `Lima:` throughout (e.g. `LIMA: duplicate
|
|
32
|
+
key "a" at line 3` → `Lima: duplicate key "a" at line 3`), plus internal
|
|
33
|
+
code comments — naming-consistency fix, no functional change. `.code`
|
|
34
|
+
(the stable, documented field for programmatic error handling) is
|
|
35
|
+
unaffected; message text was never a documented stable API surface.
|
|
36
|
+
|
|
37
|
+
## [0.1.0] — 2026-08-05
|
|
14
38
|
|
|
15
39
|
Initial release.
|
|
16
40
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @limaformat/lima
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@limaformat/lima)
|
|
4
|
+
|
|
3
5
|
**LIMA Is Metadata Annotation** — a small, predictable frontmatter format.
|
|
4
6
|
A deliberate, focused subset of YAML: the part frontmatter actually needs,
|
|
5
7
|
with well-defined types, no surprises, and zero runtime dependencies.
|
|
@@ -33,30 +35,33 @@ tags:
|
|
|
33
35
|
`parse` accepts the raw content **between** the frontmatter delimiters
|
|
34
36
|
(`---`) — stripping them is the caller's job.
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
Three functions are exported:
|
|
37
39
|
|
|
38
40
|
- `parseCore(input, options?)` — the base format only.
|
|
39
|
-
- `parse(input, options?)`
|
|
40
|
-
|
|
41
|
+
- `parse(input, options?)` — the primary References 2.0 parser; adds
|
|
42
|
+
`$(key)` document references and `$(:key)` external partials.
|
|
43
|
+
- `parseReferences(input, options?)` — deprecated compatibility alias for
|
|
44
|
+
`parse` with the same References 2.0 semantics.
|
|
41
45
|
|
|
42
46
|
```ts
|
|
43
47
|
parse(frontmatter, { strict: true })
|
|
44
48
|
parse(frontmatter, { partials: { author: 'Alice' } })
|
|
49
|
+
parse(frontmatter, { mode: 'core' }) // same reference-unaware path as parseCore
|
|
45
50
|
```
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
mode, and the
|
|
49
|
-
|
|
52
|
+
The **[guide](https://github.com/limaformat/lima/blob/main/docs/guide.md)**
|
|
53
|
+
covers Core and References 2.0 syntax, resource limits, strict mode, and the
|
|
54
|
+
public API. The normative rules are defined in the
|
|
55
|
+
**[References 2.0 specification](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)**.
|
|
50
56
|
|
|
51
57
|
Why Lima exists, the case against YAML, and security rationale:
|
|
52
58
|
**[repository README](https://github.com/limaformat/lima#readme)**.
|
|
53
59
|
|
|
54
60
|
The [Lima Core 1.0](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
|
|
55
|
-
and [Lima References
|
|
56
|
-
specifications are the
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
(count pinned by a test) shared with any other Lima implementation.
|
|
61
|
+
and [Lima References 2.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)
|
|
62
|
+
specifications are the source of truth for these public entry points. The
|
|
63
|
+
frozen References 1.0 implementation remains available internally to run its
|
|
64
|
+
independent conformance suite.
|
|
60
65
|
|
|
61
66
|
Migrating existing YAML frontmatter:
|
|
62
67
|
[docs/migrating-from-yaml.md](https://github.com/limaformat/lima/blob/main/docs/migrating-from-yaml.md).
|
package/dist/block.js
CHANGED
|
@@ -108,14 +108,14 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
108
108
|
else {
|
|
109
109
|
if (ctx.strict)
|
|
110
110
|
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
111
|
-
message: `
|
|
111
|
+
message: `Lima: unexpected syntax in array item continuation at line ${line}: "${trimmed}"` });
|
|
112
112
|
cursor.next();
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
else {
|
|
116
116
|
if (ctx.strict)
|
|
117
117
|
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
118
|
-
message: `
|
|
118
|
+
message: `Lima: unexpected indentation at line ${line}: "${trimmed}"` });
|
|
119
119
|
cursor.next();
|
|
120
120
|
}
|
|
121
121
|
continue;
|
|
@@ -130,7 +130,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
130
130
|
if (entries !== null) {
|
|
131
131
|
if (ctx.strict)
|
|
132
132
|
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
133
|
-
message: `
|
|
133
|
+
message: `Lima: mixed array and map entries for the same key at line ${line}` });
|
|
134
134
|
cursor.next();
|
|
135
135
|
continue;
|
|
136
136
|
}
|
|
@@ -153,7 +153,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
153
153
|
else if (afterDash === '-' || DASH_PREFIX_RE.test(afterDash)) {
|
|
154
154
|
if (ctx.strict)
|
|
155
155
|
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
156
|
-
message: `
|
|
156
|
+
message: `Lima: nested block sequence at line ${line}: "${cursorContent(cursor)}"` });
|
|
157
157
|
items.push(builder.null(line));
|
|
158
158
|
cursor.next();
|
|
159
159
|
while (cursor.valid) {
|
|
@@ -237,7 +237,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
237
237
|
if (items !== null) {
|
|
238
238
|
if (ctx.strict)
|
|
239
239
|
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
240
|
-
message: `
|
|
240
|
+
message: `Lima: mixed map and array entries for the same key at line ${line}` });
|
|
241
241
|
cursor.next();
|
|
242
242
|
continue;
|
|
243
243
|
}
|
|
@@ -275,7 +275,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
275
275
|
else {
|
|
276
276
|
if (ctx.strict)
|
|
277
277
|
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
278
|
-
message: `
|
|
278
|
+
message: `Lima: indented freetext without a block scalar marker at line ${line}: "${trimmed}"` });
|
|
279
279
|
cursor.next();
|
|
280
280
|
}
|
|
281
281
|
}
|
package/dist/builder.d.ts
CHANGED
|
@@ -25,11 +25,13 @@
|
|
|
25
25
|
* therefore performance — is identical either way, verified by benchmark.
|
|
26
26
|
*/
|
|
27
27
|
export interface ValueBuilder<V, M = Map<string, V>> {
|
|
28
|
+
/** Opt-in for References' annotated builder; native Core avoids source-map work. */
|
|
29
|
+
readonly tracksStringSourcePositions?: boolean;
|
|
28
30
|
null(line: number): V;
|
|
29
31
|
bool(value: boolean, line: number): V;
|
|
30
32
|
int(value: number, line: number): V;
|
|
31
33
|
float(value: number, line: number): V;
|
|
32
|
-
string(value: string, line: number, quoted: boolean): V;
|
|
34
|
+
string(value: string, line: number, quoted: boolean, sourceSpans?: StringSourceSpan[]): V;
|
|
33
35
|
instant(value: Date, line: number): V;
|
|
34
36
|
array(items: V[], line: number): V;
|
|
35
37
|
createMapping(): M;
|
|
@@ -39,3 +41,9 @@ export interface ValueBuilder<V, M = Map<string, V>> {
|
|
|
39
41
|
mappingMaxDepth(entries: M, depthOf: (value: V) => number): number;
|
|
40
42
|
mapping(entries: M, line: number): V;
|
|
41
43
|
}
|
|
44
|
+
/** Maps a span in a decoded scalar back to its physical source line/column. */
|
|
45
|
+
export type StringSourceSpan = {
|
|
46
|
+
start: number;
|
|
47
|
+
line: number;
|
|
48
|
+
sourceOffset: number;
|
|
49
|
+
};
|
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lima Core 1.0 parser — reference-unaware by construction (Appendix B:
|
|
3
3
|
* reference resolution is exclusively the concern of the optional
|
|
4
4
|
* References extension). `($key)`/`(%key)`-shaped text is never
|
|
5
5
|
* recognised or coerced here; it simply falls through as an ordinary
|
|
@@ -33,7 +33,7 @@ export type CoreOptions = {
|
|
|
33
33
|
onWarning?: (diagnostic: Diagnostic) => void;
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
36
|
-
* Parses
|
|
36
|
+
* Parses Lima Core 1.0 syntax into the internal annotated value tree —
|
|
37
37
|
* every node carrying its source line, string leaves additionally carrying
|
|
38
38
|
* whether they came from quoted syntax. `($key)`/`(%key)` text is left
|
|
39
39
|
* exactly as written; nothing here ever inspects or resolves it. The
|
package/dist/core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lima Core 1.0 parser — reference-unaware by construction (Appendix B:
|
|
3
3
|
* reference resolution is exclusively the concern of the optional
|
|
4
4
|
* References extension). `($key)`/`(%key)`-shaped text is never
|
|
5
5
|
* recognised or coerced here; it simply falls through as an ordinary
|
|
@@ -89,7 +89,7 @@ const lineAt = (s, pos) => {
|
|
|
89
89
|
return n;
|
|
90
90
|
};
|
|
91
91
|
/**
|
|
92
|
-
* Parses
|
|
92
|
+
* Parses Lima Core 1.0 syntax, generic over the output representation
|
|
93
93
|
* (`builder` — see `builder.ts`): References needs the annotated
|
|
94
94
|
* `PositionedValue` tree (`parseCoreWithPositions` below fixes `V` to
|
|
95
95
|
* that), while `parseCore` fixes `V` to the public native shape directly,
|
|
@@ -111,7 +111,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
111
111
|
byteLength(frontMatter) > DOCUMENT_SIZE_LIMIT) {
|
|
112
112
|
throw new LimaError({
|
|
113
113
|
code: 'RESOURCE_LIMIT', line: 1,
|
|
114
|
-
message: `
|
|
114
|
+
message: `Lima: document exceeds maximum size of ${DOCUMENT_SIZE_LIMIT} bytes at line 1`,
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
117
|
const root = builder.createMapping();
|
|
@@ -154,7 +154,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
154
154
|
if (SPACE_BEFORE_COLON_RE.test(line)) {
|
|
155
155
|
throw new LimaError({
|
|
156
156
|
code: 'INVALID_QUOTE', line: lineAt(frontMatter, searchFrom),
|
|
157
|
-
message: `
|
|
157
|
+
message: `Lima: space between closing quote and colon at line ${lineAt(frontMatter, searchFrom)}`,
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
if (lineEnd === -1)
|
|
@@ -167,7 +167,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
167
167
|
if ((strict || ctx.onWarning !== undefined) && builder.hasMappingKey(root, key)) {
|
|
168
168
|
const diagnostic = {
|
|
169
169
|
code: 'DUPLICATE_KEY', line, key,
|
|
170
|
-
message: `
|
|
170
|
+
message: `Lima: duplicate key "${key}" at line ${line} — last value wins`,
|
|
171
171
|
};
|
|
172
172
|
if (strict)
|
|
173
173
|
throw new LimaError(diagnostic);
|
|
@@ -239,7 +239,9 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
239
239
|
minIndent = Math.min(minIndent, key.length + 2);
|
|
240
240
|
const trimAmt = minIndent > 1 && isFinite(minIndent) ? minIndent : 0;
|
|
241
241
|
const mergedLines = [];
|
|
242
|
-
|
|
242
|
+
const mergedLineSpans = builder.tracksStringSourcePositions ? [] : undefined;
|
|
243
|
+
for (let bodyIndex = 0; bodyIndex < bodyLines.length; bodyIndex++) {
|
|
244
|
+
const bodyLine = bodyLines[bodyIndex];
|
|
243
245
|
const lineLen = bodyLine.length;
|
|
244
246
|
let start = trimAmt < lineLen ? trimAmt : lineLen;
|
|
245
247
|
const isContinuation = bodyLine.charCodeAt(start) === 94 && bodyLine.charCodeAt(start + 1) === 94; // ^^
|
|
@@ -251,22 +253,41 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
251
253
|
const content = bodyLine.slice(start, end);
|
|
252
254
|
if (isContinuation) {
|
|
253
255
|
if (mergedLines.length > 0) {
|
|
254
|
-
if (content)
|
|
256
|
+
if (content) {
|
|
257
|
+
const outputStart = mergedLines[mergedLines.length - 1].length + 1;
|
|
255
258
|
mergedLines[mergedLines.length - 1] += ' ' + content;
|
|
259
|
+
mergedLineSpans?.[mergedLineSpans.length - 1].push({
|
|
260
|
+
start: outputStart, line: line + 1 + bodyIndex, sourceOffset: start,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
256
263
|
}
|
|
257
264
|
else {
|
|
258
265
|
mergedLines.push(content);
|
|
266
|
+
mergedLineSpans?.push(content ? [{ start: 0, line: line + 1 + bodyIndex, sourceOffset: start }] : []);
|
|
259
267
|
}
|
|
260
268
|
}
|
|
261
269
|
else {
|
|
262
270
|
mergedLines.push(content);
|
|
271
|
+
mergedLineSpans?.push(content ? [{ start: 0, line: line + 1 + bodyIndex, sourceOffset: start }] : []);
|
|
263
272
|
}
|
|
264
273
|
}
|
|
265
|
-
while (mergedLines.length > 0 && mergedLines[mergedLines.length - 1] === '')
|
|
274
|
+
while (mergedLines.length > 0 && mergedLines[mergedLines.length - 1] === '') {
|
|
266
275
|
mergedLines.pop();
|
|
276
|
+
mergedLineSpans?.pop();
|
|
277
|
+
}
|
|
267
278
|
const joined = mergedLines.join('\n');
|
|
279
|
+
let sourceSpans;
|
|
280
|
+
if (mergedLineSpans) {
|
|
281
|
+
sourceSpans = [];
|
|
282
|
+
let outputStart = 0;
|
|
283
|
+
for (let i = 0; i < mergedLines.length; i++) {
|
|
284
|
+
for (const span of mergedLineSpans[i])
|
|
285
|
+
sourceSpans.push({ ...span, start: outputStart + span.start });
|
|
286
|
+
outputStart += mergedLines[i].length + 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
268
289
|
checkScalarLimit(LString(joined), line);
|
|
269
|
-
builder.setMapping(root, key, builder.string(joined, line, false));
|
|
290
|
+
builder.setMapping(root, key, builder.string(joined, line + 1, false, sourceSpans));
|
|
270
291
|
}
|
|
271
292
|
};
|
|
272
293
|
// A recognised top-level entry needs at least one UTF-16 unit of key text,
|
|
@@ -309,7 +330,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
309
330
|
if (keyCount > TOP_LEVEL_KEY_LIMIT) {
|
|
310
331
|
throw new LimaError({
|
|
311
332
|
code: 'RESOURCE_LIMIT', line: 1,
|
|
312
|
-
message: `
|
|
333
|
+
message: `Lima: too many top-level key entries (max ${TOP_LEVEL_KEY_LIMIT}) at line 1`,
|
|
313
334
|
});
|
|
314
335
|
}
|
|
315
336
|
for (let i = 0; i < keyCount; i++) {
|
|
@@ -327,13 +348,13 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
327
348
|
if (blockDepthRisk.mayExceed && builder.mappingMaxDepth(root, computeDepth) > NESTING_DEPTH_LIMIT) {
|
|
328
349
|
throw new LimaError({
|
|
329
350
|
code: 'RESOURCE_LIMIT', line: 1,
|
|
330
|
-
message: `
|
|
351
|
+
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line 1`,
|
|
331
352
|
});
|
|
332
353
|
}
|
|
333
354
|
return root;
|
|
334
355
|
};
|
|
335
356
|
/**
|
|
336
|
-
* Parses
|
|
357
|
+
* Parses Lima Core 1.0 syntax into the internal annotated value tree —
|
|
337
358
|
* every node carrying its source line, string leaves additionally carrying
|
|
338
359
|
* whether they came from quoted syntax. `($key)`/`(%key)` text is left
|
|
339
360
|
* exactly as written; nothing here ever inspects or resolves it. The
|
package/dist/flow.js
CHANGED
|
@@ -79,14 +79,14 @@ export const parseFlowSequence = (val, ctx, line, builder) => {
|
|
|
79
79
|
const item = val.slice(start, end);
|
|
80
80
|
if (!item) {
|
|
81
81
|
if (ctx.strict)
|
|
82
|
-
throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `
|
|
82
|
+
throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `Lima: empty element in flow sequence at line ${line}` });
|
|
83
83
|
items.push(builder.null(line));
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
86
|
if (item.charCodeAt(0) === 91 && item.charCodeAt(item.length - 1) === 93) {
|
|
87
87
|
throw new LimaError({
|
|
88
88
|
code: 'INVALID_FLOW_SYNTAX', line,
|
|
89
|
-
message: `
|
|
89
|
+
message: `Lima: nested flow sequence not permitted at line ${line}: "${item}"`,
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
if (item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125) {
|
|
@@ -114,7 +114,7 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
|
|
|
114
114
|
const item = val.slice(itemStart, itemEnd);
|
|
115
115
|
if (!item) {
|
|
116
116
|
if (ctx.strict)
|
|
117
|
-
throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `
|
|
117
|
+
throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `Lima: empty element in flow mapping at line ${line}` });
|
|
118
118
|
continue;
|
|
119
119
|
}
|
|
120
120
|
const colonPos = val.indexOf(': ', itemStart);
|
|
@@ -122,7 +122,7 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
|
|
|
122
122
|
if (ctx.strict)
|
|
123
123
|
throw new LimaError({
|
|
124
124
|
code: 'INVALID_FLOW_SYNTAX', line,
|
|
125
|
-
message: `
|
|
125
|
+
message: `Lima: invalid flow mapping item (missing ": ") at line ${line}: "${item}"`,
|
|
126
126
|
});
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
@@ -137,7 +137,7 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
|
|
|
137
137
|
const valueEnd = trimEnd(val, valueStart, itemEnd);
|
|
138
138
|
const rawVal = val.slice(valueStart, valueEnd);
|
|
139
139
|
if (isNestedFlowConstruct(rawVal)) {
|
|
140
|
-
throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `
|
|
140
|
+
throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `Lima: invalid flow nesting at line ${line}: "${rawVal}"` });
|
|
141
141
|
}
|
|
142
142
|
builder.setMapping(entries, key, parseQuotedOrTyped(rawVal, ctx, line, false, builder));
|
|
143
143
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lima Parser — LIMA Is Metadata Annotation
|
|
3
3
|
*
|
|
4
4
|
* Public entry point. The implementation is split per Appendix B's own
|
|
5
5
|
* layering:
|
|
6
|
-
* - `core.ts` —
|
|
7
|
-
* - `
|
|
8
|
-
*
|
|
6
|
+
* - `core.ts` — Lima Core 1.0, reference-unaware by construction.
|
|
7
|
+
* - `references2.ts` — the References 2.0 extension layered on Core.
|
|
8
|
+
* - `references.ts` — the frozen internal References 1.0 implementation.
|
|
9
9
|
* - `value.ts` — the shared Lima Value Model both build on.
|
|
10
10
|
*
|
|
11
|
-
* `parse`
|
|
12
|
-
*
|
|
11
|
+
* `parse` is the primary References 2.0 entry point. `parseReferences` is
|
|
12
|
+
* retained as its deprecated compatibility alias.
|
|
13
13
|
*/
|
|
14
14
|
export { parseCore, type CoreOptions, type Diagnostic } from './core.js';
|
|
15
|
-
export { parseReferences, parse, type
|
|
15
|
+
export { parseReferences, parse, type ParseMode, type ParseOptions } from './references2.js';
|
|
16
|
+
export type ReferencesOptions = import('./references2.js').ParseOptions;
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lima Parser — LIMA Is Metadata Annotation
|
|
3
3
|
*
|
|
4
4
|
* Public entry point. The implementation is split per Appendix B's own
|
|
5
5
|
* layering:
|
|
6
|
-
* - `core.ts` —
|
|
7
|
-
* - `
|
|
8
|
-
*
|
|
6
|
+
* - `core.ts` — Lima Core 1.0, reference-unaware by construction.
|
|
7
|
+
* - `references2.ts` — the References 2.0 extension layered on Core.
|
|
8
|
+
* - `references.ts` — the frozen internal References 1.0 implementation.
|
|
9
9
|
* - `value.ts` — the shared Lima Value Model both build on.
|
|
10
10
|
*
|
|
11
|
-
* `parse`
|
|
12
|
-
*
|
|
11
|
+
* `parse` is the primary References 2.0 entry point. `parseReferences` is
|
|
12
|
+
* retained as its deprecated compatibility alias.
|
|
13
13
|
*/
|
|
14
14
|
export { parseCore } from './core.js';
|
|
15
|
-
export { parseReferences, parse } from './
|
|
15
|
+
export { parseReferences, parse } from './references2.js';
|
package/dist/normalize.js
CHANGED
|
@@ -20,7 +20,7 @@ export const checkStringLimit = (value, line) => {
|
|
|
20
20
|
if (value.length > SCALAR_LENGTH_LIMIT && codepointLength(value) > SCALAR_LENGTH_LIMIT) {
|
|
21
21
|
throw new LimaError({
|
|
22
22
|
code: 'RESOURCE_LIMIT', line,
|
|
23
|
-
message: `
|
|
23
|
+
message: `Lima: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${line}`,
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
};
|
|
@@ -41,7 +41,7 @@ export const checkKeyLength = (key, line) => {
|
|
|
41
41
|
const l = line();
|
|
42
42
|
throw new LimaError({
|
|
43
43
|
code: 'RESOURCE_LIMIT', line: l,
|
|
44
|
-
message: `
|
|
44
|
+
message: `Lima: key "${key}" exceeds maximum length of ${KEY_LENGTH_LIMIT} code points at line ${l}`,
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
};
|
|
@@ -50,7 +50,7 @@ export const checkDuplicateKey = (exists, key, line, ctx) => {
|
|
|
50
50
|
return;
|
|
51
51
|
const diagnostic = {
|
|
52
52
|
code: 'DUPLICATE_KEY', line, key,
|
|
53
|
-
message: `
|
|
53
|
+
message: `Lima: duplicate key "${key}" at line ${line} — last value wins`,
|
|
54
54
|
};
|
|
55
55
|
if (ctx.strict)
|
|
56
56
|
throw new LimaError(diagnostic);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Reference tokens recorded while the annotated Core tree is being built. */
|
|
2
|
+
import type { StringSourceSpan } from './builder.js';
|
|
3
|
+
export type ReferenceToken2 = {
|
|
4
|
+
token: string;
|
|
5
|
+
index: number;
|
|
6
|
+
offset: number;
|
|
7
|
+
line: number;
|
|
8
|
+
documentPath?: string;
|
|
9
|
+
partialPath?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const PURE_REFERENCE_2: RegExp;
|
|
12
|
+
export declare const scanReferenceTokens2: (value: string, firstLine: number, sourceSpans?: StringSourceSpan[]) => ReferenceToken2[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
|
|
2
|
+
const DOC_PATH = `${SEGMENT}(?:\\.${SEGMENT})*`;
|
|
3
|
+
const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
4
|
+
const PARTIAL_PATH = `${PARTIAL_NAME}(?:\\.${SEGMENT})*`;
|
|
5
|
+
export const PURE_REFERENCE_2 = new RegExp(`^\\$\\((?:(${DOC_PATH})|:(${PARTIAL_PATH}))\\)$`);
|
|
6
|
+
const REFERENCE_2 = new RegExp(`\\$\\((?:(${DOC_PATH})|:(${PARTIAL_PATH}))\\)`, 'g');
|
|
7
|
+
const NO_TOKENS = [];
|
|
8
|
+
export const scanReferenceTokens2 = (value, firstLine, sourceSpans) => {
|
|
9
|
+
if (!value.includes('$('))
|
|
10
|
+
return NO_TOKENS;
|
|
11
|
+
REFERENCE_2.lastIndex = 0;
|
|
12
|
+
const result = [];
|
|
13
|
+
let line = firstLine;
|
|
14
|
+
let lineStart = 0;
|
|
15
|
+
let scanned = 0;
|
|
16
|
+
let spanIndex = 0;
|
|
17
|
+
for (const match of value.matchAll(REFERENCE_2)) {
|
|
18
|
+
const index = match.index ?? 0;
|
|
19
|
+
while (scanned < index) {
|
|
20
|
+
if (value.charCodeAt(scanned) === 10) {
|
|
21
|
+
line++;
|
|
22
|
+
lineStart = scanned + 1;
|
|
23
|
+
}
|
|
24
|
+
scanned++;
|
|
25
|
+
}
|
|
26
|
+
while (sourceSpans && spanIndex + 1 < sourceSpans.length && sourceSpans[spanIndex + 1].start <= index)
|
|
27
|
+
spanIndex++;
|
|
28
|
+
const source = sourceSpans?.[spanIndex];
|
|
29
|
+
result.push({
|
|
30
|
+
token: match[0], index,
|
|
31
|
+
line: source?.line ?? line,
|
|
32
|
+
offset: source ? source.sourceOffset + index - source.start : index - lineStart,
|
|
33
|
+
...(match[1] !== undefined ? { documentPath: match[1] } : { partialPath: match[2] }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
};
|
package/dist/references.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lima References 1.0 — layered strictly on top of `core.ts` (Appendix B:
|
|
3
3
|
* reference resolution is exclusively this extension's concern; Core never
|
|
4
4
|
* sees it). Reads the annotated `PositionedValue` tree Core produces and
|
|
5
5
|
* performs the two-phase resolution the spec describes (§4), keeping every
|
|
@@ -20,8 +20,71 @@
|
|
|
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
|
|
23
|
+
import { type LimaValue } from './value.js';
|
|
24
|
+
import { type PositionedValue, type Diagnostic, type InsertedAt, type NativeValue } from './core.js';
|
|
24
25
|
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;
|
|
25
88
|
export type ReferencesOptions = {
|
|
26
89
|
/** Named values available via `(%key)` references. */
|
|
27
90
|
partials?: Meta;
|
|
@@ -33,4 +96,6 @@ export declare const parseReferences: <T extends Record<string, unknown> = Meta>
|
|
|
33
96
|
/** Backward-compatible primary entry point — References layered on Core. */
|
|
34
97
|
export declare const parse: typeof parseReferences;
|
|
35
98
|
export type ParseOptions = ReferencesOptions;
|
|
99
|
+
/** Internal conformance entry point for the frozen References 1.0 suite. */
|
|
100
|
+
export declare const parseReferencesV1: typeof parseReferences;
|
|
36
101
|
export {};
|
package/dist/references.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lima References 1.0 — layered strictly on top of `core.ts` (Appendix B:
|
|
3
3
|
* reference resolution is exclusively this extension's concern; Core never
|
|
4
4
|
* sees it). Reads the annotated `PositionedValue` tree Core produces and
|
|
5
5
|
* performs the two-phase resolution the spec describes (§4), keeping every
|
|
@@ -110,14 +110,14 @@ const isReferenceFreeP = (v) => {
|
|
|
110
110
|
* already contain the result of an inner reference that resolved first —
|
|
111
111
|
* both insertion sites remain identifiable participants after the copy).
|
|
112
112
|
*/
|
|
113
|
-
const deepCopyPositioned = (v) => {
|
|
113
|
+
export const deepCopyPositioned = (v) => {
|
|
114
114
|
switch (v.kind) {
|
|
115
|
-
case 'array': return { kind: 'array', items: v.items.map(deepCopyPositioned), line: v.line, insertedAt: v.insertedAt };
|
|
115
|
+
case 'array': return { kind: 'array', items: v.items.map(deepCopyPositioned), line: v.line, references2Active: v.references2Active, insertedAt: v.insertedAt };
|
|
116
116
|
case 'mapping': {
|
|
117
117
|
const entries = new Map();
|
|
118
118
|
for (const [k, c] of v.entries)
|
|
119
119
|
entries.set(k, deepCopyPositioned(c));
|
|
120
|
-
return { kind: 'mapping', entries, line: v.line, insertedAt: v.insertedAt };
|
|
120
|
+
return { kind: 'mapping', entries, line: v.line, references2Active: v.references2Active, insertedAt: v.insertedAt };
|
|
121
121
|
}
|
|
122
122
|
case 'instant': return { kind: 'instant', value: new Date(v.value.getTime()), line: v.line, insertedAt: v.insertedAt };
|
|
123
123
|
default: return v;
|
|
@@ -132,7 +132,7 @@ const deepCopyPositioned = (v) => {
|
|
|
132
132
|
* every pure reference to that partial retrieves — deep-copying it on every
|
|
133
133
|
* such reference (§3.1) is the resolveTree call site's job, not this one.
|
|
134
134
|
*/
|
|
135
|
-
const partialToPositioned = (v, line) => {
|
|
135
|
+
export const partialToPositioned = (v, line) => {
|
|
136
136
|
switch (v.kind) {
|
|
137
137
|
case 'null': return { kind: 'null', line };
|
|
138
138
|
case 'bool': return { kind: 'bool', value: v.value, line };
|
|
@@ -216,7 +216,7 @@ const resolveTree = (node, lookup, partials, ctx) => {
|
|
|
216
216
|
if (target.kind === 'mapping') {
|
|
217
217
|
reportDiagnostic(ctx, {
|
|
218
218
|
code: 'INVALID_INTERPOLATION', line: node.line, token: match,
|
|
219
|
-
message: `
|
|
219
|
+
message: `Lima: invalid interpolation of "${match}" at line ${node.line}: mapping cannot be interpolated into a string`,
|
|
220
220
|
});
|
|
221
221
|
return match;
|
|
222
222
|
}
|
|
@@ -224,7 +224,7 @@ const resolveTree = (node, lookup, partials, ctx) => {
|
|
|
224
224
|
if (target.items.some((item) => item.kind === 'array' || item.kind === 'mapping')) {
|
|
225
225
|
reportDiagnostic(ctx, {
|
|
226
226
|
code: 'INVALID_INTERPOLATION', line: node.line, token: match,
|
|
227
|
-
message: `
|
|
227
|
+
message: `Lima: invalid interpolation of "${match}" at line ${node.line}: array contains a nested array or mapping`,
|
|
228
228
|
});
|
|
229
229
|
return match;
|
|
230
230
|
}
|
|
@@ -240,7 +240,7 @@ const resolveTree = (node, lookup, partials, ctx) => {
|
|
|
240
240
|
if (codepointLength(replaced) > SCALAR_LENGTH_LIMIT) {
|
|
241
241
|
throw new LimaError({
|
|
242
242
|
code: 'RESOURCE_LIMIT', line: node.line,
|
|
243
|
-
message: `
|
|
243
|
+
message: `Lima: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${node.line}`,
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
246
|
return { kind: 'string', value: replaced, line: node.line, quoted: false };
|
|
@@ -266,7 +266,7 @@ const resolveTree = (node, lookup, partials, ctx) => {
|
|
|
266
266
|
// throws in BOTH modes (R-036/R-143).
|
|
267
267
|
reportDiagnostic(ctx, {
|
|
268
268
|
code: 'INVALID_REFERENCE_SHAPE', line: item.line, token: item.value,
|
|
269
|
-
message: `
|
|
269
|
+
message: `Lima: reference "${item.value}" resolves to an array, which cannot be inserted as a sequence item at line ${item.line}`,
|
|
270
270
|
});
|
|
271
271
|
return item;
|
|
272
272
|
}
|
|
@@ -282,7 +282,7 @@ const resolveTree = (node, lookup, partials, ctx) => {
|
|
|
282
282
|
}
|
|
283
283
|
return node; // null/bool/int/float/instant — nothing to resolve
|
|
284
284
|
};
|
|
285
|
-
const finalizePositioned = (v) => {
|
|
285
|
+
export const finalizePositioned = (v) => {
|
|
286
286
|
const own = v.insertedAt ? [v.insertedAt] : [];
|
|
287
287
|
if (v.kind === 'array') {
|
|
288
288
|
if (v.items.length === 0)
|
|
@@ -334,9 +334,9 @@ const finalizePositioned = (v) => {
|
|
|
334
334
|
return { native: toNativeFromPositioned(v), nodeCount: 1, depth: 0, deepestParticipants: own };
|
|
335
335
|
};
|
|
336
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));
|
|
337
|
+
export const earliestParticipant = (participants) => participants.length === 0 ? null : participants.reduce((a, b) => (b.line < a.line ? b : a));
|
|
338
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) => {
|
|
339
|
+
export const collectAllParticipants = (v, acc) => {
|
|
340
340
|
if (v.insertedAt)
|
|
341
341
|
acc.push(v.insertedAt);
|
|
342
342
|
if (v.kind === 'array')
|
|
@@ -356,13 +356,13 @@ export const parseReferences = (frontMatter, options) => {
|
|
|
356
356
|
// diagnostics collected below.
|
|
357
357
|
const partialNames = Object.keys(rawPartials);
|
|
358
358
|
if (partialNames.length > PARTIAL_COUNT_LIMIT) {
|
|
359
|
-
throw new LimaError({ code: 'INVALID_PARTIAL', message: `
|
|
359
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', message: `Lima: too many partials (max ${PARTIAL_COUNT_LIMIT})` });
|
|
360
360
|
}
|
|
361
361
|
for (const name of partialNames) {
|
|
362
362
|
if (codepointLength(name) > PARTIAL_NAME_LENGTH_LIMIT) {
|
|
363
363
|
throw new LimaError({
|
|
364
364
|
code: 'INVALID_PARTIAL', partial: name, path: name,
|
|
365
|
-
message: `
|
|
365
|
+
message: `Lima: invalid partial "${name}" at path "${name}": name exceeds maximum length of ${PARTIAL_NAME_LENGTH_LIMIT} code points`,
|
|
366
366
|
});
|
|
367
367
|
}
|
|
368
368
|
}
|
|
@@ -376,7 +376,7 @@ export const parseReferences = (frontMatter, options) => {
|
|
|
376
376
|
if (totalPartialNodes > PARTIAL_NODE_LIMIT) {
|
|
377
377
|
throw new LimaError({
|
|
378
378
|
code: 'INVALID_PARTIAL',
|
|
379
|
-
message: `
|
|
379
|
+
message: `Lima: partials exceed the combined maximum of ${PARTIAL_NODE_LIMIT} value nodes`,
|
|
380
380
|
});
|
|
381
381
|
}
|
|
382
382
|
// Converted to the annotated representation once per partial, not once
|
|
@@ -446,7 +446,7 @@ export const parseReferences = (frontMatter, options) => {
|
|
|
446
446
|
if (m) {
|
|
447
447
|
reportDiagnostic(ctx, {
|
|
448
448
|
code: 'UNRESOLVED_REFERENCE', line: v.line, token: `(${m[1]}${m[2]})`,
|
|
449
|
-
message: `
|
|
449
|
+
message: `Lima: unresolved reference "(${m[1]}${m[2]})" at line ${v.line}`,
|
|
450
450
|
});
|
|
451
451
|
}
|
|
452
452
|
}
|
|
@@ -491,8 +491,8 @@ export const parseReferences = (frontMatter, options) => {
|
|
|
491
491
|
throw new LimaError({
|
|
492
492
|
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
493
493
|
message: winner
|
|
494
|
-
? `
|
|
495
|
-
: `
|
|
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
496
|
});
|
|
497
497
|
}
|
|
498
498
|
// §6.2: total node count of the final result tree, both modes. Same
|
|
@@ -509,8 +509,8 @@ export const parseReferences = (frontMatter, options) => {
|
|
|
509
509
|
throw new LimaError({
|
|
510
510
|
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
511
511
|
message: winner
|
|
512
|
-
? `
|
|
513
|
-
: `
|
|
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
514
|
});
|
|
515
515
|
}
|
|
516
516
|
const out = emptyMapping();
|
|
@@ -520,3 +520,5 @@ export const parseReferences = (frontMatter, options) => {
|
|
|
520
520
|
};
|
|
521
521
|
/** Backward-compatible primary entry point — References layered on Core. */
|
|
522
522
|
export const parse = parseReferences;
|
|
523
|
+
/** Internal conformance entry point for the frozen References 1.0 suite. */
|
|
524
|
+
export const parseReferencesV1 = parseReferences;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Diagnostic } from './core.js';
|
|
2
|
+
type Meta = Record<string, unknown>;
|
|
3
|
+
export type ParseMode = 'references' | 'core';
|
|
4
|
+
export type ParseOptions = {
|
|
5
|
+
mode?: ParseMode;
|
|
6
|
+
partials?: Meta;
|
|
7
|
+
strict?: boolean;
|
|
8
|
+
onWarning?: (diagnostic: Diagnostic) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare const parse: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: ParseOptions) => T;
|
|
11
|
+
/** @deprecated Use parse(). */
|
|
12
|
+
export declare const parseReferences: typeof parse;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/** Lima References 2.0 resolver. References 1.0 remains isolated in references.ts. */
|
|
2
|
+
import { canonicalString, codepointLength, countNodes, ingestPartialValue, PARTIAL_COUNT_LIMIT, PARTIAL_NAME_LENGTH_LIMIT, PARTIAL_NODE_LIMIT, RESULT_NODE_LIMIT, SCALAR_LENGTH_LIMIT, } from './value.js';
|
|
3
|
+
import { NESTING_DEPTH_LIMIT, parseCore, parseCoreWithPositions, toPlainValue, } from './core.js';
|
|
4
|
+
import { hasActiveReferences2 } from './scalars.js';
|
|
5
|
+
import { LimaError } from './errors.js';
|
|
6
|
+
import { collectAllParticipants, deepCopyPositioned, earliestParticipant, finalizePositioned, partialToPositioned, } from './references.js';
|
|
7
|
+
const emptyMapping = () => Object.create(null);
|
|
8
|
+
const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
9
|
+
const PARTIAL_NAME_RE = new RegExp(`^${PARTIAL_NAME}$`);
|
|
10
|
+
const MAX_EDGES = 3;
|
|
11
|
+
const addDiagnostic = (ctx, diagnostic, offset = 0) => {
|
|
12
|
+
ctx.diagnostics.push({ ...diagnostic, offset });
|
|
13
|
+
};
|
|
14
|
+
const tokenMatches = (node) => node.references2 ?? [];
|
|
15
|
+
const lookupPath = (root, segments) => {
|
|
16
|
+
let current = root;
|
|
17
|
+
for (const segment of segments) {
|
|
18
|
+
if (current?.kind !== 'mapping')
|
|
19
|
+
return undefined;
|
|
20
|
+
current = current.entries.get(segment);
|
|
21
|
+
}
|
|
22
|
+
return current;
|
|
23
|
+
};
|
|
24
|
+
const documentTarget = (root, path) => {
|
|
25
|
+
const segments = path.split('.');
|
|
26
|
+
const value = root.get(segments[0]);
|
|
27
|
+
return value === undefined ? undefined : { root: value, tail: segments.slice(1) };
|
|
28
|
+
};
|
|
29
|
+
const lookupPartial = (partials, path) => {
|
|
30
|
+
const dot = path.indexOf('.');
|
|
31
|
+
if (dot < 0)
|
|
32
|
+
return partials.get(path);
|
|
33
|
+
return lookupPath(partials.get(path.slice(0, dot)), path.slice(dot + 1).split('.'));
|
|
34
|
+
};
|
|
35
|
+
const isActiveString = (node) => node.kind === 'string' && !node.quoted;
|
|
36
|
+
const scalarText = (value, token, line, offset, ctx) => {
|
|
37
|
+
if (value.kind === 'mapping') {
|
|
38
|
+
addDiagnostic(ctx, {
|
|
39
|
+
code: 'INVALID_INTERPOLATION', line, token,
|
|
40
|
+
message: `Lima: invalid interpolation of "${token}" at line ${line}: mapping cannot be interpolated into a string`,
|
|
41
|
+
}, offset);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (value.kind === 'array') {
|
|
45
|
+
if (value.items.some((item) => item.kind === 'array' || item.kind === 'mapping')) {
|
|
46
|
+
addDiagnostic(ctx, {
|
|
47
|
+
code: 'INVALID_INTERPOLATION', line, token,
|
|
48
|
+
message: `Lima: invalid interpolation of "${token}" at line ${line}: array contains a nested array or mapping`,
|
|
49
|
+
}, offset);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return value.items.map((item) => canonicalString(toPlainValue(item))).join(', ');
|
|
53
|
+
}
|
|
54
|
+
return canonicalString(toPlainValue(value));
|
|
55
|
+
};
|
|
56
|
+
const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stack) => {
|
|
57
|
+
if (node.kind === 'array') {
|
|
58
|
+
if (!hasActiveReferences2(node))
|
|
59
|
+
return { value: node, complete: true };
|
|
60
|
+
let complete = true;
|
|
61
|
+
const items = node.items.map((item) => {
|
|
62
|
+
const result = resolveNode(item, document, partials, ctx, remainingEdges, stack);
|
|
63
|
+
complete &&= result.complete;
|
|
64
|
+
if (isActiveString(item) && result.value.kind === 'array') {
|
|
65
|
+
const match = tokenMatches(item)[0];
|
|
66
|
+
const token = match?.token ?? item.value;
|
|
67
|
+
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
|
+
});
|
|
71
|
+
return item;
|
|
72
|
+
}
|
|
73
|
+
return result.value;
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
value: { ...node, items, references2Active: items.some(hasActiveReferences2) || undefined },
|
|
77
|
+
complete,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (node.kind === 'mapping') {
|
|
81
|
+
if (!hasActiveReferences2(node))
|
|
82
|
+
return { value: node, complete: true };
|
|
83
|
+
let complete = true;
|
|
84
|
+
const entries = new Map();
|
|
85
|
+
for (const [key, child] of node.entries) {
|
|
86
|
+
const result = resolveNode(child, document, partials, ctx, remainingEdges, stack);
|
|
87
|
+
complete &&= result.complete;
|
|
88
|
+
entries.set(key, result.value);
|
|
89
|
+
}
|
|
90
|
+
let references2Active = false;
|
|
91
|
+
for (const value of entries.values()) {
|
|
92
|
+
if (hasActiveReferences2(value)) {
|
|
93
|
+
references2Active = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { value: { ...node, entries, references2Active: references2Active || undefined }, complete };
|
|
98
|
+
}
|
|
99
|
+
if (!isActiveString(node))
|
|
100
|
+
return { value: node, complete: true };
|
|
101
|
+
const recorded = tokenMatches(node);
|
|
102
|
+
const pure = recorded.length === 1 && recorded[0].index === 0 && recorded[0].token.length === node.value.length
|
|
103
|
+
? recorded[0]
|
|
104
|
+
: undefined;
|
|
105
|
+
if (pure) {
|
|
106
|
+
if (remainingEdges === 0)
|
|
107
|
+
return { value: node, complete: false };
|
|
108
|
+
const documentLookup = pure.documentPath !== undefined ? documentTarget(document, pure.documentPath) : undefined;
|
|
109
|
+
const target = pure.documentPath !== undefined ? documentLookup?.root : lookupPartial(partials, pure.partialPath);
|
|
110
|
+
if (target === undefined || stack.has(target))
|
|
111
|
+
return { value: node, complete: false };
|
|
112
|
+
if (pure.partialPath !== undefined) {
|
|
113
|
+
return {
|
|
114
|
+
value: { ...deepCopyPositioned(target), insertedAt: { line: pure.line, token: pure.token } },
|
|
115
|
+
complete: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
stack.add(target);
|
|
119
|
+
const result = resolveNode(target, document, partials, ctx, remainingEdges - 1, stack);
|
|
120
|
+
stack.delete(target);
|
|
121
|
+
if (!result.complete)
|
|
122
|
+
return { value: node, complete: false };
|
|
123
|
+
const selected = lookupPath(result.value, documentLookup?.tail ?? []);
|
|
124
|
+
if (selected === undefined)
|
|
125
|
+
return { value: node, complete: false };
|
|
126
|
+
return {
|
|
127
|
+
value: { ...deepCopyPositioned(selected), insertedAt: { line: pure.line, token: pure.token } },
|
|
128
|
+
complete: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const matches = recorded;
|
|
132
|
+
if (matches.length === 0)
|
|
133
|
+
return { value: node, complete: true };
|
|
134
|
+
let complete = true;
|
|
135
|
+
let cursor = 0;
|
|
136
|
+
let output = '';
|
|
137
|
+
const unresolvedTokens = [];
|
|
138
|
+
for (const match of matches) {
|
|
139
|
+
const offset = match.index;
|
|
140
|
+
output += node.value.slice(cursor, offset);
|
|
141
|
+
const documentLookup = match.documentPath !== undefined ? documentTarget(document, match.documentPath) : undefined;
|
|
142
|
+
const target = match.documentPath !== undefined ? documentLookup?.root : lookupPartial(partials, match.partialPath);
|
|
143
|
+
let replacement = null;
|
|
144
|
+
if (remainingEdges > 0 && target !== undefined && !stack.has(target)) {
|
|
145
|
+
if (match.partialPath !== undefined)
|
|
146
|
+
replacement = scalarText(target, match.token, match.line, match.offset, ctx);
|
|
147
|
+
else {
|
|
148
|
+
stack.add(target);
|
|
149
|
+
const result = resolveNode(target, document, partials, ctx, remainingEdges - 1, stack);
|
|
150
|
+
stack.delete(target);
|
|
151
|
+
const selected = result.complete ? lookupPath(result.value, documentLookup?.tail ?? []) : undefined;
|
|
152
|
+
if (selected !== undefined)
|
|
153
|
+
replacement = scalarText(selected, match.token, match.line, match.offset, ctx);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (replacement === null) {
|
|
157
|
+
complete = false;
|
|
158
|
+
unresolvedTokens.push({ ...match, index: output.length });
|
|
159
|
+
output += match.token;
|
|
160
|
+
}
|
|
161
|
+
else
|
|
162
|
+
output += replacement;
|
|
163
|
+
cursor = offset + match.token.length;
|
|
164
|
+
}
|
|
165
|
+
output += node.value.slice(cursor);
|
|
166
|
+
if (codepointLength(output) > SCALAR_LENGTH_LIMIT) {
|
|
167
|
+
const first = matches[0];
|
|
168
|
+
addDiagnostic(ctx, {
|
|
169
|
+
code: 'RESOURCE_LIMIT', line: first.line, token: first.token,
|
|
170
|
+
message: `Lima: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${first.line}`,
|
|
171
|
+
}, first.offset);
|
|
172
|
+
}
|
|
173
|
+
return { value: { ...node, value: output, references2: unresolvedTokens }, complete };
|
|
174
|
+
};
|
|
175
|
+
/** Successful and failed dependency results are immutable and budget-specific. */
|
|
176
|
+
const resolveNode = (node, document, partials, ctx, remainingEdges, stack) => {
|
|
177
|
+
const cacheable = stack.size > 1 && (node.kind === 'array' || node.kind === 'mapping' ||
|
|
178
|
+
(isActiveString(node) && tokenMatches(node).length > 0));
|
|
179
|
+
if (!cacheable)
|
|
180
|
+
return resolveNodeUncached(node, document, partials, ctx, remainingEdges, stack);
|
|
181
|
+
const byBudget = ctx.cache.get(node);
|
|
182
|
+
const cached = byBudget?.get(remainingEdges);
|
|
183
|
+
if (cached !== undefined)
|
|
184
|
+
return cached;
|
|
185
|
+
const result = resolveNodeUncached(node, document, partials, ctx, remainingEdges, stack);
|
|
186
|
+
if (byBudget !== undefined)
|
|
187
|
+
byBudget.set(remainingEdges, result);
|
|
188
|
+
else
|
|
189
|
+
ctx.cache.set(node, new Map([[remainingEdges, result]]));
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
192
|
+
const validatePartials = (raw) => {
|
|
193
|
+
const names = Object.keys(raw);
|
|
194
|
+
if (names.length > PARTIAL_COUNT_LIMIT) {
|
|
195
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', message: `Lima: too many partials (max ${PARTIAL_COUNT_LIMIT})` });
|
|
196
|
+
}
|
|
197
|
+
for (const name of names) {
|
|
198
|
+
if (!PARTIAL_NAME_RE.test(name)) {
|
|
199
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', partial: name, message: `Lima: invalid partial name "${name}"` });
|
|
200
|
+
}
|
|
201
|
+
if (codepointLength(name) > PARTIAL_NAME_LENGTH_LIMIT) {
|
|
202
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', partial: name, message: `Lima: invalid partial name "${name}": exceeds maximum length of ${PARTIAL_NAME_LENGTH_LIMIT} code points` });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const ingested = new Map();
|
|
206
|
+
for (const name of names)
|
|
207
|
+
ingested.set(name, ingestPartialValue(raw[name], name, name));
|
|
208
|
+
let nodes = 0;
|
|
209
|
+
for (const value of ingested.values())
|
|
210
|
+
nodes += countNodes(value);
|
|
211
|
+
if (nodes > PARTIAL_NODE_LIMIT) {
|
|
212
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', message: `Lima: partials exceed the combined maximum of ${PARTIAL_NODE_LIMIT} value nodes` });
|
|
213
|
+
}
|
|
214
|
+
const result = new Map();
|
|
215
|
+
for (const [name, value] of ingested)
|
|
216
|
+
result.set(name, partialToPositioned(value, 0));
|
|
217
|
+
return result;
|
|
218
|
+
};
|
|
219
|
+
const scanUnresolved = (node, ctx) => {
|
|
220
|
+
if (isActiveString(node)) {
|
|
221
|
+
for (const match of tokenMatches(node)) {
|
|
222
|
+
addDiagnostic(ctx, {
|
|
223
|
+
code: 'UNRESOLVED_REFERENCE', line: match.line, token: match.token,
|
|
224
|
+
message: `Lima: unresolved reference "${match.token}" at line ${match.line}`,
|
|
225
|
+
}, match.offset);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (node.kind === 'array')
|
|
230
|
+
for (const item of node.items)
|
|
231
|
+
scanUnresolved(item, ctx);
|
|
232
|
+
if (node.kind === 'mapping')
|
|
233
|
+
for (const child of node.entries.values())
|
|
234
|
+
scanUnresolved(child, ctx);
|
|
235
|
+
};
|
|
236
|
+
export const parse = (frontMatter, options) => {
|
|
237
|
+
if (options?.mode !== undefined && options.mode !== 'core' && options.mode !== 'references') {
|
|
238
|
+
throw new TypeError(`Lima: invalid parse mode "${String(options.mode)}"`);
|
|
239
|
+
}
|
|
240
|
+
if (options?.mode === 'core') {
|
|
241
|
+
if (Object.prototype.hasOwnProperty.call(options, 'partials')) {
|
|
242
|
+
throw new TypeError('Lima: partials cannot be supplied in core mode');
|
|
243
|
+
}
|
|
244
|
+
// Core ignores the additional `mode` property. Passing the same object
|
|
245
|
+
// avoids allocating a projected options object on this hot dispatch path.
|
|
246
|
+
return parseCore(frontMatter, options);
|
|
247
|
+
}
|
|
248
|
+
const partials = validatePartials(options?.partials ?? {});
|
|
249
|
+
if (!frontMatter.includes('$(')) {
|
|
250
|
+
return parseCore(frontMatter, options);
|
|
251
|
+
}
|
|
252
|
+
const document = parseCoreWithPositions(frontMatter, { strict: options?.strict ?? false, onWarning: options?.onWarning });
|
|
253
|
+
const ctx = { diagnostics: [], cache: new WeakMap() };
|
|
254
|
+
const resolved = new Map();
|
|
255
|
+
for (const [key, value] of document) {
|
|
256
|
+
const stack = new Set([value]);
|
|
257
|
+
resolved.set(key, resolveNode(value, document, partials, ctx, MAX_EDGES, stack).value);
|
|
258
|
+
}
|
|
259
|
+
if (options?.strict)
|
|
260
|
+
for (const value of resolved.values())
|
|
261
|
+
scanUnresolved(value, ctx);
|
|
262
|
+
if (ctx.diagnostics.length > 0) {
|
|
263
|
+
ctx.diagnostics.sort((a, b) => a.line - b.line || a.offset - b.offset);
|
|
264
|
+
throw new LimaError(ctx.diagnostics[0]);
|
|
265
|
+
}
|
|
266
|
+
const finalized = [...resolved].map(([key, value]) => [key, finalizePositioned(value)]);
|
|
267
|
+
const depth = finalized.length === 0 ? 0 : Math.max(...finalized.map(([, value]) => value.depth));
|
|
268
|
+
if (depth > NESTING_DEPTH_LIMIT) {
|
|
269
|
+
const participants = finalized.filter(([, value]) => value.depth === depth).flatMap(([, value]) => value.deepestParticipants);
|
|
270
|
+
const winner = earliestParticipant(participants);
|
|
271
|
+
throw new LimaError({
|
|
272
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
273
|
+
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner?.line ?? 1}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
let nodeCount = 1;
|
|
277
|
+
for (const [, value] of finalized)
|
|
278
|
+
nodeCount += value.nodeCount;
|
|
279
|
+
if (nodeCount > RESULT_NODE_LIMIT) {
|
|
280
|
+
const participants = [];
|
|
281
|
+
for (const value of resolved.values())
|
|
282
|
+
collectAllParticipants(value, participants);
|
|
283
|
+
const winner = earliestParticipant(participants);
|
|
284
|
+
throw new LimaError({
|
|
285
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
286
|
+
message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const out = emptyMapping();
|
|
290
|
+
for (const [key, value] of finalized)
|
|
291
|
+
out[key] = value.native;
|
|
292
|
+
return out;
|
|
293
|
+
};
|
|
294
|
+
/** @deprecated Use parse(). */
|
|
295
|
+
export const parseReferences = parse;
|
package/dist/scalars.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { type LimaValue } from './value.js';
|
|
8
8
|
import { type ParseContext } from './normalize.js';
|
|
9
9
|
import type { ValueBuilder } from './builder.js';
|
|
10
|
+
import { type ReferenceToken2 } from './reference-tokens2.js';
|
|
10
11
|
/**
|
|
11
12
|
* `insertedAt` is never set by Core — it's a References-only annotation
|
|
12
13
|
* (see references.ts's `resolveTree`), stamped on the root of a value
|
|
@@ -45,6 +46,7 @@ export type PositionedValue = {
|
|
|
45
46
|
value: string;
|
|
46
47
|
line: number;
|
|
47
48
|
quoted: boolean;
|
|
49
|
+
references2?: ReferenceToken2[];
|
|
48
50
|
insertedAt?: InsertedAt;
|
|
49
51
|
} | {
|
|
50
52
|
kind: 'instant';
|
|
@@ -55,13 +57,16 @@ export type PositionedValue = {
|
|
|
55
57
|
kind: 'array';
|
|
56
58
|
items: PositionedValue[];
|
|
57
59
|
line: number;
|
|
60
|
+
references2Active?: boolean;
|
|
58
61
|
insertedAt?: InsertedAt;
|
|
59
62
|
} | {
|
|
60
63
|
kind: 'mapping';
|
|
61
64
|
entries: Map<string, PositionedValue>;
|
|
62
65
|
line: number;
|
|
66
|
+
references2Active?: boolean;
|
|
63
67
|
insertedAt?: InsertedAt;
|
|
64
68
|
};
|
|
69
|
+
export declare const hasActiveReferences2: (value: PositionedValue) => boolean;
|
|
65
70
|
/** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
|
|
66
71
|
export declare const positionedBuilder: ValueBuilder<PositionedValue>;
|
|
67
72
|
/** Strips position/quoted-origin annotations, recursively — the public parseCore() projection. */
|
package/dist/scalars.js
CHANGED
|
@@ -7,15 +7,29 @@
|
|
|
7
7
|
import { LNull, LBool, LFloat, LInt, LString, LInstant } from './value.js';
|
|
8
8
|
import { checkStringLimit } from './normalize.js';
|
|
9
9
|
import { LimaError } from './errors.js';
|
|
10
|
+
import { scanReferenceTokens2 } from './reference-tokens2.js';
|
|
11
|
+
export const hasActiveReferences2 = (value) => value.kind === 'string' ? (value.references2?.length ?? 0) > 0
|
|
12
|
+
: value.kind === 'array' || value.kind === 'mapping' ? value.references2Active === true
|
|
13
|
+
: false;
|
|
10
14
|
/** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
|
|
11
15
|
export const positionedBuilder = {
|
|
16
|
+
tracksStringSourcePositions: true,
|
|
12
17
|
null: (line) => ({ kind: 'null', line }),
|
|
13
18
|
bool: (value, line) => ({ kind: 'bool', value, line }),
|
|
14
19
|
int: (value, line) => ({ kind: 'int', value, line }),
|
|
15
20
|
float: (value, line) => ({ kind: 'float', value, line }),
|
|
16
|
-
string: (value, line, quoted) =>
|
|
21
|
+
string: (value, line, quoted, sourceSpans) => {
|
|
22
|
+
const references2 = quoted ? undefined : scanReferenceTokens2(value, line, sourceSpans);
|
|
23
|
+
return {
|
|
24
|
+
kind: 'string', value, line, quoted,
|
|
25
|
+
...(references2 && references2.length > 0 ? { references2 } : {}),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
17
28
|
instant: (value, line) => ({ kind: 'instant', value, line }),
|
|
18
|
-
array: (items, line) => ({
|
|
29
|
+
array: (items, line) => ({
|
|
30
|
+
kind: 'array', items, line,
|
|
31
|
+
...(items.some(hasActiveReferences2) ? { references2Active: true } : {}),
|
|
32
|
+
}),
|
|
19
33
|
createMapping: () => new Map(),
|
|
20
34
|
createMappingWith: (key, value) => new Map([[key, value]]),
|
|
21
35
|
hasMappingKey: (entries, key) => entries.has(key),
|
|
@@ -28,7 +42,16 @@ export const positionedBuilder = {
|
|
|
28
42
|
}
|
|
29
43
|
return max;
|
|
30
44
|
},
|
|
31
|
-
mapping: (entries, line) =>
|
|
45
|
+
mapping: (entries, line) => {
|
|
46
|
+
let references2Active = false;
|
|
47
|
+
for (const value of entries.values()) {
|
|
48
|
+
if (hasActiveReferences2(value)) {
|
|
49
|
+
references2Active = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { kind: 'mapping', entries, line, ...(references2Active ? { references2Active: true } : {}) };
|
|
54
|
+
},
|
|
32
55
|
};
|
|
33
56
|
/** Strips position/quoted-origin annotations, recursively — the public parseCore() projection. */
|
|
34
57
|
export const toPlainValue = (v) => {
|
|
@@ -87,7 +110,7 @@ const parseExactIsoSpan = (source, start, end, strict, line) => {
|
|
|
87
110
|
hour > 23 || minute > 59 || second > 59) {
|
|
88
111
|
if (strict) {
|
|
89
112
|
const raw = source.slice(start, end);
|
|
90
|
-
throw new LimaError({ code: 'INVALID_DATE', line, message: `
|
|
113
|
+
throw new LimaError({ code: 'INVALID_DATE', line, message: `Lima: invalid date "${raw}" at line ${line}` });
|
|
91
114
|
}
|
|
92
115
|
return null;
|
|
93
116
|
}
|
|
@@ -102,7 +125,7 @@ const parseExactIsoSpan = (source, start, end, strict, line) => {
|
|
|
102
125
|
const parseDateUTC = (str, strict = false, line = 0) => {
|
|
103
126
|
const invalid = () => {
|
|
104
127
|
if (strict)
|
|
105
|
-
throw new LimaError({ code: 'INVALID_DATE', line, message: `
|
|
128
|
+
throw new LimaError({ code: 'INVALID_DATE', line, message: `Lima: invalid date "${str}" at line ${line}` });
|
|
106
129
|
return null;
|
|
107
130
|
};
|
|
108
131
|
const exact = parseExactIsoSpan(str, 0, str.length, strict, line);
|
|
@@ -258,14 +281,14 @@ const buildTyped = (str, strict, line, builder) => {
|
|
|
258
281
|
if (strict)
|
|
259
282
|
throw new LimaError({
|
|
260
283
|
code: 'INVALID_NUMBER', line,
|
|
261
|
-
message: `
|
|
284
|
+
message: `Lima: float value overflows to a non-finite value at line ${line}: "${str}"`,
|
|
262
285
|
});
|
|
263
286
|
}
|
|
264
287
|
else if (n === 0 && !isZeroLiteral(str)) {
|
|
265
288
|
if (strict)
|
|
266
289
|
throw new LimaError({
|
|
267
290
|
code: 'INVALID_NUMBER', line,
|
|
268
|
-
message: `
|
|
291
|
+
message: `Lima: non-zero float value underflows to zero at line ${line}: "${str}"`,
|
|
269
292
|
});
|
|
270
293
|
}
|
|
271
294
|
else {
|
|
@@ -312,7 +335,7 @@ export const unescapeDQ = (s, strict = false, line = 0) => {
|
|
|
312
335
|
if (strict) {
|
|
313
336
|
for (const m of s.matchAll(ANY_ESCAPE_RE)) {
|
|
314
337
|
if (!isValidEscape(m[0].slice(1))) {
|
|
315
|
-
throw new LimaError({ code: 'INVALID_ESCAPE', line, message: `
|
|
338
|
+
throw new LimaError({ code: 'INVALID_ESCAPE', line, message: `Lima: unknown escape sequence "${m[0]}" at line ${line}` });
|
|
316
339
|
}
|
|
317
340
|
}
|
|
318
341
|
}
|
|
@@ -385,7 +408,7 @@ export const parseQuotedOrTyped = (raw, ctx, line, topLevel, builder) => {
|
|
|
385
408
|
return builder.string(value, line, true);
|
|
386
409
|
}
|
|
387
410
|
if (topLevel && ctx.strict) {
|
|
388
|
-
throw new LimaError({ code: 'INVALID_QUOTE', line, message: `
|
|
411
|
+
throw new LimaError({ code: 'INVALID_QUOTE', line, message: `Lima: non-whitespace content after closing quote at line ${line}` });
|
|
389
412
|
}
|
|
390
413
|
}
|
|
391
414
|
if (raw !== '' && raw !== 'null' && raw !== '~' && raw !== 'true' && raw !== 'false' &&
|
|
@@ -400,7 +423,7 @@ export const parseScalarValue = (raw, ctx, line, builder) => {
|
|
|
400
423
|
if (ctx.strict && (first === 91 || first === 123)) {
|
|
401
424
|
throw new LimaError({
|
|
402
425
|
code: 'INVALID_FLOW_SYNTAX', line,
|
|
403
|
-
message: `
|
|
426
|
+
message: `Lima: unclosed flow ${first === 91 ? 'sequence' : 'mapping'} at line ${line}`,
|
|
404
427
|
});
|
|
405
428
|
}
|
|
406
429
|
return parseQuotedOrTyped(raw, ctx, line, true, builder);
|
package/dist/value.js
CHANGED
|
@@ -106,7 +106,7 @@ export const ingestPartialValue = (value, partialName, path, depth = 0, seen = n
|
|
|
106
106
|
const invalid = (reason) => {
|
|
107
107
|
throw new LimaError({
|
|
108
108
|
code: 'INVALID_PARTIAL', partial: partialName, path,
|
|
109
|
-
message: `
|
|
109
|
+
message: `Lima: invalid partial "${partialName}" at path "${path}": ${reason}`,
|
|
110
110
|
});
|
|
111
111
|
};
|
|
112
112
|
if (value === null)
|
|
@@ -147,7 +147,7 @@ export const ingestPartialValue = (value, partialName, path, depth = 0, seen = n
|
|
|
147
147
|
if (Array.isArray(item)) {
|
|
148
148
|
throw new LimaError({
|
|
149
149
|
code: 'INVALID_PARTIAL', partial: partialName, path: `${path}[${i}]`,
|
|
150
|
-
message: `
|
|
150
|
+
message: `Lima: invalid partial "${partialName}" at path "${path}[${i}]": nested arrays are not supported`,
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
return ingestPartialValue(item, partialName, `${path}[${i}]`, depth + 1, seen);
|
|
@@ -167,13 +167,13 @@ export const ingestPartialValue = (value, partialName, path, depth = 0, seen = n
|
|
|
167
167
|
if (!descriptor || !('value' in descriptor)) {
|
|
168
168
|
throw new LimaError({
|
|
169
169
|
code: 'INVALID_PARTIAL', partial: partialName, path: `${path}.${key}`,
|
|
170
|
-
message: `
|
|
170
|
+
message: `Lima: invalid partial "${partialName}" at path "${path}.${key}": accessor properties are not supported`,
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
if (codepointLength(key) > PARTIAL_KEY_LENGTH_LIMIT) {
|
|
174
174
|
throw new LimaError({
|
|
175
175
|
code: 'INVALID_PARTIAL', partial: partialName, path: `${path}.${key}`,
|
|
176
|
-
message: `
|
|
176
|
+
message: `Lima: invalid partial "${partialName}" at path "${path}.${key}": key exceeds maximum length of ${PARTIAL_KEY_LENGTH_LIMIT} code points`,
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
179
|
entries.set(key, ingestPartialValue(value[key], partialName, `${path}.${key}`, depth + 1, seen));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@limaformat/lima",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lima frontmatter parser — LIMA Is Metadata Annotation",
|
|
5
5
|
"keywords": ["frontmatter", "parser", "yaml", "markdown", "metadata"],
|
|
6
6
|
"homepage": "https://limaformat.dev",
|
|
@@ -30,12 +30,15 @@
|
|
|
30
30
|
"bench": "bun run bench/index.ts",
|
|
31
31
|
"bench:vs-yaml": "bun run bench/vs-yaml.ts",
|
|
32
32
|
"bench:vs-yaml:node": "node bench/vs-yaml-node.mjs",
|
|
33
|
-
"bench:
|
|
33
|
+
"bench:cross-language": "bun run bench/cross-language.ts",
|
|
34
|
+
"bench:differential": "bun run bench/differential.ts",
|
|
35
|
+
"wordcount": "bun run scripts/wordcount.ts"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
38
|
"@types/bun": "^1.3.14",
|
|
37
39
|
"@types/js-yaml": "^4.0.9",
|
|
38
40
|
"js-yaml": "^5.2.3",
|
|
39
|
-
"typescript": "^7.0.2"
|
|
41
|
+
"typescript": "^7.0.2",
|
|
42
|
+
"typescript-classic": "npm:typescript@^5.9.3"
|
|
40
43
|
}
|
|
41
44
|
}
|