@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 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
- ## [0.1.0] — Unreleased
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
+ [![npm version](https://img.shields.io/npm/v/%40limaformat%2Flima.svg)](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
- Two functions are exported:
38
+ Three functions are exported:
37
39
 
38
40
  - `parseCore(input, options?)` — the base format only.
39
- - `parse(input, options?)` (alias for `parseReferences`) — adds
40
- `($key)` document references and `(%key)` external partials.
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
- Full syntax reference, the References extension, resource limits, strict
48
- mode, and the complete API:
49
- **[docs/guide.md](https://github.com/limaformat/lima/blob/main/docs/guide.md)**.
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 1.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-1.0-spec.md)
56
- specifications are the normative source of truth — this package implements
57
- them exactly, verified against a
58
- [250-case conformance corpus](https://github.com/limaformat/lima/tree/main/corpus)
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: `LIMA: unexpected syntax in array item continuation at line ${line}: "${trimmed}"` });
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: `LIMA: unexpected indentation at line ${line}: "${trimmed}"` });
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: `LIMA: mixed array and map entries for the same key at line ${line}` });
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: `LIMA: nested block sequence at line ${line}: "${cursorContent(cursor)}"` });
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: `LIMA: mixed map and array entries for the same key at line ${line}` });
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: `LIMA: indented freetext without a block scalar marker at line ${line}: "${trimmed}"` });
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
- * LIMA Core 1.0 parser — reference-unaware by construction (Appendix B:
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 LIMA Core 1.0 syntax into the internal annotated value tree —
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
- * LIMA Core 1.0 parser — reference-unaware by construction (Appendix B:
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 LIMA Core 1.0 syntax, generic over the output representation
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: `LIMA: document exceeds maximum size of ${DOCUMENT_SIZE_LIMIT} bytes at line 1`,
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: `LIMA: space between closing quote and colon at line ${lineAt(frontMatter, searchFrom)}`,
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: `LIMA: duplicate key "${key}" at line ${line} — last value wins`,
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
- for (const bodyLine of bodyLines) {
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: `LIMA: too many top-level key entries (max ${TOP_LEVEL_KEY_LIMIT}) at line 1`,
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: `LIMA: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line 1`,
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 LIMA Core 1.0 syntax into the internal annotated value tree —
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: `LIMA: empty element in flow sequence at line ${line}` });
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: `LIMA: nested flow sequence not permitted at line ${line}: "${item}"`,
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: `LIMA: empty element in flow mapping at line ${line}` });
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: `LIMA: invalid flow mapping item (missing ": ") at line ${line}: "${item}"`,
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: `LIMA: invalid flow nesting at line ${line}: "${rawVal}"` });
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
- * LIMA Parser — LIMA Is Metadata Annotation
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` — LIMA Core 1.0, reference-unaware by construction.
7
- * - `references.ts` — the optional References 1.0 extension, layered on
8
- * top of Core's internal annotated value tree.
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` (the historical default export) resolves references — it is
12
- * `parseReferences` under a name kept for backward compatibility.
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 ReferencesOptions, type ParseOptions } from './references.js';
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
- * LIMA Parser — LIMA Is Metadata Annotation
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` — LIMA Core 1.0, reference-unaware by construction.
7
- * - `references.ts` — the optional References 1.0 extension, layered on
8
- * top of Core's internal annotated value tree.
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` (the historical default export) resolves references — it is
12
- * `parseReferences` under a name kept for backward compatibility.
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 './references.js';
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: `LIMA: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${line}`,
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: `LIMA: key "${key}" exceeds maximum length of ${KEY_LENGTH_LIMIT} code points at line ${l}`,
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: `LIMA: duplicate key "${key}" at line ${line} — last value wins`,
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
+ };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * LIMA References 1.0 — layered strictly on top of `core.ts` (Appendix B:
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 Diagnostic } from './core.js';
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 {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * LIMA References 1.0 — layered strictly on top of `core.ts` (Appendix B:
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: `LIMA: invalid interpolation of "${match}" at line ${node.line}: mapping cannot be interpolated into a string`,
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: `LIMA: invalid interpolation of "${match}" at line ${node.line}: array contains a nested array or mapping`,
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: `LIMA: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${node.line}`,
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: `LIMA: reference "${item.value}" resolves to an array, which cannot be inserted as a sequence item at line ${item.line}`,
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: `LIMA: too many partials (max ${PARTIAL_COUNT_LIMIT})` });
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: `LIMA: invalid partial "${name}" at path "${name}": name exceeds maximum length of ${PARTIAL_NAME_LENGTH_LIMIT} code points`,
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: `LIMA: partials exceed the combined maximum of ${PARTIAL_NODE_LIMIT} value nodes`,
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: `LIMA: unresolved reference "(${m[1]}${m[2]})" at line ${v.line}`,
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
- ? `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`,
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
- ? `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`,
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) => ({ kind: '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) => ({ kind: '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) => ({ kind: '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: `LIMA: invalid date "${raw}" at line ${line}` });
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: `LIMA: invalid date "${str}" at line ${line}` });
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: `LIMA: float value overflows to a non-finite value at line ${line}: "${str}"`,
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: `LIMA: non-zero float value underflows to zero at line ${line}: "${str}"`,
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: `LIMA: unknown escape sequence "${m[0]}" at line ${line}` });
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: `LIMA: non-whitespace content after closing quote at line ${line}` });
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: `LIMA: unclosed flow ${first === 91 ? 'sequence' : 'mapping'} at line ${line}`,
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: `LIMA: invalid partial "${partialName}" at path "${path}": ${reason}`,
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: `LIMA: invalid partial "${partialName}" at path "${path}[${i}]": nested arrays are not supported`,
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: `LIMA: invalid partial "${partialName}" at path "${path}.${key}": accessor properties are not supported`,
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: `LIMA: invalid partial "${partialName}" at path "${path}.${key}": key exceeds maximum length of ${PARTIAL_KEY_LENGTH_LIMIT} code points`,
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.1.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:differential": "bun run bench/differential.ts"
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
  }