@limaformat/lima 0.3.1 → 0.5.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/dist/core.js CHANGED
@@ -18,11 +18,11 @@
18
18
  * `flow.ts` (`[...]`/`{...}`), and `block.ts` (indentation-based sequences
19
19
  * and mappings) — this file is the orchestrator that ties them together.
20
20
  */
21
- import { LString } from './value.js';
22
- import { checkKeyLength, checkScalarLimit, byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, } from './normalize.js';
23
- import { unescapeDQ, stripComment, positionedBuilder, NO_SPAN_VALUE, parseSimpleScalarSpan, } from './scalars.js';
21
+ import { checkKeyLength, byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, } from './normalize.js';
22
+ import { unescapeDQ, stripComment, stripCommentKeepEscapes, positionedBuilder, NO_SPAN_VALUE, parseSimpleScalarSpan, } from './scalars.js';
24
23
  import { parseFlowOrScalarValue } from './flow.js';
25
24
  import { parseBlockRange } from './block.js';
25
+ import { buildBlockScalar } from './block-scalar.js';
26
26
  import { LimaError } from './errors.js';
27
27
  import { KeyCursor, scanKeys } from './scanner.js';
28
28
  export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
@@ -68,19 +68,6 @@ const depthOfNative = (v) => {
68
68
  return 0;
69
69
  };
70
70
  const SPACE_BEFORE_COLON_RE = /^(?:'[^']*'|"(?:[^"\\]|\\.)*")[ \t]+:/;
71
- const leadingSpaces = (line) => {
72
- let i = 0;
73
- while (i < line.length && line.charCodeAt(i) === 32)
74
- i++;
75
- return i;
76
- };
77
- /** Index right after the last non-space character — the `trimEnd()` boundary, without allocating. */
78
- const trailingSpaceEnd = (line) => {
79
- let i = line.length;
80
- while (i > 0 && line.charCodeAt(i - 1) === 32)
81
- i--;
82
- return i;
83
- };
84
71
  const lineAt = (s, pos) => {
85
72
  let n = 1;
86
73
  for (let i = 0; i < pos; i++)
@@ -136,7 +123,14 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
136
123
  if (frontMatter.includes('\r'))
137
124
  frontMatter = frontMatter.replace(/\r\n|\r/g, '\n');
138
125
  if (frontMatter.includes('\t')) {
139
- frontMatter = frontMatter.replace(/^([ \t]*)/gm, (leading) => (leading.includes('\t') ? leading.replace(/\t/g, ' ') : leading));
126
+ // Record how many columns each line's leading whitespace grew, so a
127
+ // References 2.0 token can be reported at its *original* column (§2.4).
128
+ ctx.tabAdjust = [];
129
+ frontMatter = frontMatter.replace(/^([ \t]*)/gm, (leading) => {
130
+ const expanded = leading.includes('\t') ? leading.replace(/\t/g, ' ') : leading;
131
+ ctx.tabAdjust.push(expanded.length - leading.length);
132
+ return expanded;
133
+ });
140
134
  }
141
135
  if (frontMatter.includes(' \n') || frontMatter.endsWith(' ')) {
142
136
  // Capture-and-reinsert instead of a lookahead: `( +)(\n|$)` replaced
@@ -163,7 +157,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
163
157
  }
164
158
  }
165
159
  const parseEntry = (key, line, rawStart, isBlock, inlineEnd, nextStart) => {
166
- checkKeyLength(key, () => line);
160
+ checkKeyLength(key, line);
167
161
  if ((strict || ctx.onWarning !== undefined) && builder.hasMappingKey(root, key)) {
168
162
  const diagnostic = {
169
163
  code: 'DUPLICATE_KEY', line, key,
@@ -178,6 +172,14 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
178
172
  }
179
173
  const firstNewline = inlineEnd ?? frontMatter.indexOf('\n', rawStart);
180
174
  const newlineInRange = firstNewline < nextStart;
175
+ // Physical codepoint column of the value's first character in its
176
+ // source line — the anchor for References 2.0 token positions (§2.4).
177
+ // References 2.0 token positions (§2.4): only build the source anchor
178
+ // for the annotated builder; `parseCore` (native) discards it.
179
+ const wantSource = builder === positionedBuilder;
180
+ const valueLineStart = wantSource ? frontMatter.lastIndexOf('\n', rawStart - 1) + 1 : 0;
181
+ const valueCol = wantSource ? [...frontMatter.slice(valueLineStart, rawStart)].length : 0;
182
+ const lineTabAdjust = wantSource && ctx.tabAdjust ? [ctx.tabAdjust[line - 1] ?? 0] : undefined;
181
183
  // Typical frontmatter uses one inline scalar per key. Avoid building
182
184
  // and filling a temporary lines array for that overwhelmingly common
183
185
  // case; the scanner-delimited raw slice contains at most its terminal
@@ -190,8 +192,15 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
190
192
  return;
191
193
  }
192
194
  const val = frontMatter.slice(rawStart, valueEnd);
193
- const uncommented = val.includes('#') ? stripComment(val) : val;
194
- builder.setMapping(root, key, parseFlowOrScalarValue(uncommented, ctx, line, builder));
195
+ const hasHash = val.includes('#');
196
+ const uncommented = hasHash ? stripComment(val) : val;
197
+ // The physical anchor keeps `\#` but drops a real comment, so a
198
+ // `${…}`-shaped token inside a trailing comment is not scanned (§2.4).
199
+ const source = wantSource ? {
200
+ raw: hasHash ? stripCommentKeepEscapes(val) : val,
201
+ line, col: valueCol, tabAdjust: lineTabAdjust,
202
+ } : undefined;
203
+ builder.setMapping(root, key, parseFlowOrScalarValue(uncommented, ctx, line, builder, source));
195
204
  return;
196
205
  }
197
206
  if (isBlock) {
@@ -217,77 +226,22 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
217
226
  const line0Trimmed = lines[0].trim();
218
227
  if (lines.length === 1 || line0Trimmed !== '|') {
219
228
  const line0 = lines[0];
220
- const val = line0.includes('#') ? stripComment(line0) : line0;
221
- builder.setMapping(root, key, parseFlowOrScalarValue(val, ctx, line, builder));
229
+ const hasHash = line0.includes('#');
230
+ const val = hasHash ? stripComment(line0) : line0;
231
+ const source = wantSource ? {
232
+ raw: hasHash ? stripCommentKeepEscapes(line0) : line0,
233
+ line, col: valueCol, tabAdjust: lineTabAdjust,
234
+ } : undefined;
235
+ builder.setMapping(root, key, parseFlowOrScalarValue(val, ctx, line, builder, source));
222
236
  return;
223
237
  }
224
- // Multi-line string (`|` literal block scalar).
238
+ // Multi-line string (`|` literal block scalar). The introducing key
239
+ // is at column 0 here; `buildBlockScalar` finds the scalar's own
240
+ // extent within the range (which runs to the next top-level key),
241
+ // so a dedented comment or dedented freetext before that key ends
242
+ // the scalar rather than being absorbed into it.
225
243
  const bodyLines = raw.slice(raw.indexOf('\n') + 1).split('\n');
226
- // Avoids trim()/trimStart()/trimEnd() — each allocates a whole new
227
- // string just to measure or strip whitespace. leadingSpaces/
228
- // trailingSpaceEnd compute the same boundaries by scanning char
229
- // codes, and dedent+continuation-strip+trailing-trim collapse
230
- // into a single final slice() per line instead of up to three.
231
- let minIndent = Infinity;
232
- for (const bodyLine of bodyLines) {
233
- const indent = leadingSpaces(bodyLine);
234
- if (indent === bodyLine.length)
235
- continue; // blank (all spaces, or empty)
236
- if (indent < minIndent)
237
- minIndent = indent;
238
- }
239
- minIndent = Math.min(minIndent, key.length + 2);
240
- const trimAmt = minIndent > 1 && isFinite(minIndent) ? minIndent : 0;
241
- const mergedLines = [];
242
- const mergedLineSpans = builder.tracksStringSourcePositions ? [] : undefined;
243
- for (let bodyIndex = 0; bodyIndex < bodyLines.length; bodyIndex++) {
244
- const bodyLine = bodyLines[bodyIndex];
245
- const lineLen = bodyLine.length;
246
- let start = trimAmt < lineLen ? trimAmt : lineLen;
247
- const isContinuation = bodyLine.charCodeAt(start) === 94 && bodyLine.charCodeAt(start + 1) === 94; // ^^
248
- if (isContinuation)
249
- start += 2;
250
- let end = trailingSpaceEnd(bodyLine);
251
- if (end < start)
252
- end = start;
253
- const content = bodyLine.slice(start, end);
254
- if (isContinuation) {
255
- if (mergedLines.length > 0) {
256
- if (content) {
257
- const outputStart = mergedLines[mergedLines.length - 1].length + 1;
258
- mergedLines[mergedLines.length - 1] += ' ' + content;
259
- mergedLineSpans?.[mergedLineSpans.length - 1].push({
260
- start: outputStart, line: line + 1 + bodyIndex, sourceOffset: start,
261
- });
262
- }
263
- }
264
- else {
265
- mergedLines.push(content);
266
- mergedLineSpans?.push(content ? [{ start: 0, line: line + 1 + bodyIndex, sourceOffset: start }] : []);
267
- }
268
- }
269
- else {
270
- mergedLines.push(content);
271
- mergedLineSpans?.push(content ? [{ start: 0, line: line + 1 + bodyIndex, sourceOffset: start }] : []);
272
- }
273
- }
274
- while (mergedLines.length > 0 && mergedLines[mergedLines.length - 1] === '') {
275
- mergedLines.pop();
276
- mergedLineSpans?.pop();
277
- }
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
- }
289
- checkScalarLimit(LString(joined), line);
290
- builder.setMapping(root, key, builder.string(joined, line + 1, false, sourceSpans));
244
+ builder.setMapping(root, key, buildBlockScalar(bodyLines, 0, line, ctx, builder).value);
291
245
  }
292
246
  };
293
247
  // A recognised top-level entry needs at least one UTF-16 unit of key text,
@@ -336,7 +290,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
336
290
  for (let i = 0; i < keyCount; i++) {
337
291
  const m = matches[i];
338
292
  const rawDQ = m.doubleQuotedRaw;
339
- const key = m.unquoted ?? m.singleQuoted ?? (rawDQ !== undefined ? unescapeDQ(rawDQ) : undefined);
293
+ const key = m.unquoted ?? m.singleQuoted ?? (rawDQ !== undefined ? unescapeDQ(rawDQ, strict, m.line) : undefined);
340
294
  if (key !== undefined)
341
295
  parseEntry(key, m.line, m.rawStart, m.isBlock, m.inlineEnd, i + 1 < keyCount ? matches[i + 1].matchStart : frontMatter.length);
342
296
  }
package/dist/flow.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /** Core §15.8 flow collections: `[...]` sequences and `{...}` mappings. */
2
2
  import { type ParseContext } from './normalize.js';
3
3
  import type { ValueBuilder } from './builder.js';
4
- export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>) => V[] | null;
5
- export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>) => V | null;
4
+ import type { ReferenceSource } from './reference-tokens2.js';
5
+ export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V[] | null;
6
+ export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V | null;
6
7
  /** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
7
- export declare const parseFlowOrScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>) => V;
8
+ export declare const parseFlowOrScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V;
package/dist/flow.js CHANGED
@@ -1,8 +1,65 @@
1
1
  /** Core §15.8 flow collections: `[...]` sequences and `{...}` mappings. */
2
2
  import { checkKeyLength, checkDuplicateKey } from './normalize.js';
3
- import { parseQuotedOrTyped, parseScalarValue, stripKeyQuotes } from './scalars.js';
3
+ import { parseQuotedOrTyped, parseScalarValue, stripKeyQuotes, closingQuoteIndex, isValidKey } from './scalars.js';
4
4
  import { LimaError } from './errors.js';
5
5
  import { isTrimWhitespace } from './chars.js';
6
+ /**
7
+ * Maps a UTF-16 offset in the decoded container `val` to the corresponding
8
+ * offset in the physical container `raw`. The two differ only by `\#` → `#`
9
+ * collapses outside quoted strings (FR-5 keeps `\#` inside quotes), so every
10
+ * `#` in `val` outside a quote is one such collapse — `raw` has `\#` there.
11
+ * Brackets, commas, quotes and quoted content are identical in both.
12
+ */
13
+ const rawOffsetOf = (raw, val, valOffset) => {
14
+ let ri = 0;
15
+ let vi = 0;
16
+ let quote = 0;
17
+ while (vi < valOffset) {
18
+ const vc = val.charCodeAt(vi);
19
+ if (quote) {
20
+ if (vc === 92) {
21
+ vi += 2;
22
+ ri += 2;
23
+ continue;
24
+ } // escaped pair, identical in both
25
+ if (vc === quote)
26
+ quote = 0;
27
+ }
28
+ else if (vc === 34 || vc === 39) {
29
+ quote = vc;
30
+ }
31
+ else if (vc === 35) {
32
+ vi += 1;
33
+ ri += 2;
34
+ continue; // '#' in the decoded container ⇐ '\#' in raw
35
+ }
36
+ vi += 1;
37
+ ri += 1;
38
+ }
39
+ return ri;
40
+ };
41
+ /**
42
+ * The `ReferenceSource` for a flow element spanning UTF-16 `[start, end)` in
43
+ * the decoded container `val`. The element's physical column is the
44
+ * container's column plus the *codepoint* distance to the element's start in
45
+ * the physical text, and its `raw` is the physical element slice — so a
46
+ * token's reported position (References §2.4) and §5 left-to-right ordering
47
+ * are correct even when an earlier `\#` widened the source. `source.raw` is
48
+ * the physical container (equal to `val` when the value carried no `\#`).
49
+ */
50
+ const elementSource = (source, val, start, end) => {
51
+ if (source === undefined)
52
+ return undefined;
53
+ const raw = source.raw ?? val;
54
+ const rs = rawOffsetOf(raw, val, start);
55
+ const re = rawOffsetOf(raw, val, end);
56
+ return {
57
+ line: source.line,
58
+ col: source.col + [...raw.slice(0, rs)].length,
59
+ raw: raw.slice(rs, re),
60
+ tabAdjust: source.tabAdjust,
61
+ };
62
+ };
6
63
  const trimStart = (source, start, end) => {
7
64
  while (start < end && isTrimWhitespace(source.charCodeAt(start)))
8
65
  start++;
@@ -61,7 +118,7 @@ class FlowCursor {
61
118
  }
62
119
  const isNestedFlowConstruct = (item) => (item.charCodeAt(0) === 91 && item.charCodeAt(item.length - 1) === 93) ||
63
120
  (item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125);
64
- export const parseFlowSequence = (val, ctx, line, builder) => {
121
+ export const parseFlowSequence = (val, ctx, line, builder, source) => {
65
122
  if (val.charCodeAt(0) !== 91 || val.charCodeAt(val.length - 1) !== 93)
66
123
  return null;
67
124
  const innerStart = trimStart(val, 1, val.length - 1);
@@ -90,17 +147,34 @@ export const parseFlowSequence = (val, ctx, line, builder) => {
90
147
  });
91
148
  }
92
149
  if (item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125) {
93
- const nested = parseFlowMapping(item, ctx, line, builder);
150
+ const nested = parseFlowMapping(item, ctx, line, builder, elementSource(source, val, start, end));
94
151
  if (nested !== null) {
95
152
  items.push(nested);
96
153
  continue;
97
154
  }
98
155
  }
99
- items.push(parseQuotedOrTyped(item, ctx, line, false, builder));
156
+ items.push(parseQuotedOrTyped(item, ctx, line, builder, elementSource(source, val, start, end)));
100
157
  }
101
158
  return items;
102
159
  };
103
- export const parseFlowMapping = (val, ctx, line, builder) => {
160
+ /**
161
+ * Index of the key/value `: ` separator within a flow-mapping item — the
162
+ * first `: ` that is not inside a quoted key (§5.1). Returns -1 when the
163
+ * item has no separator.
164
+ */
165
+ const flowItemSeparator = (val, start, end) => {
166
+ let scan = start;
167
+ if (val.charCodeAt(start) === 34 || val.charCodeAt(start) === 39) {
168
+ // §5.2: a single-quoted key is literal, so its first `'` closes.
169
+ const close = closingQuoteIndex(val.slice(start, end), false);
170
+ if (close === -1)
171
+ return -1;
172
+ scan = start + close + 1;
173
+ }
174
+ const sep = val.indexOf(': ', scan);
175
+ return sep === -1 || sep >= end ? -1 : sep;
176
+ };
177
+ export const parseFlowMapping = (val, ctx, line, builder, source) => {
104
178
  if (val.charCodeAt(0) !== 123 || val.charCodeAt(val.length - 1) !== 125)
105
179
  return null;
106
180
  const innerStart = trimStart(val, 1, val.length - 1);
@@ -117,8 +191,8 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
117
191
  throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `Lima: empty element in flow mapping at line ${line}` });
118
192
  continue;
119
193
  }
120
- const colonPos = val.indexOf(': ', itemStart);
121
- if (colonPos === -1 || colonPos >= itemEnd) {
194
+ const colonPos = flowItemSeparator(val, itemStart, itemEnd);
195
+ if (colonPos === -1) {
122
196
  if (ctx.strict)
123
197
  throw new LimaError({
124
198
  code: 'INVALID_FLOW_SYNTAX', line,
@@ -128,8 +202,15 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
128
202
  }
129
203
  const keyStart = trimStart(val, itemStart, colonPos);
130
204
  const keyEnd = trimEnd(val, keyStart, colonPos);
131
- const key = stripKeyQuotes(val.slice(keyStart, keyEnd));
132
- checkKeyLength(key, () => line);
205
+ const keyRaw = val.slice(keyStart, keyEnd);
206
+ if (!isValidKey(keyRaw)) {
207
+ // §5.1: an unquoted key with a space/dot, or a malformed quoted
208
+ // key, is not a key. Skip the item in both modes (§10's strict
209
+ // list is closed and does not cover this — same as the top level).
210
+ continue;
211
+ }
212
+ const key = stripKeyQuotes(keyRaw, ctx.strict, line);
213
+ checkKeyLength(key, line);
133
214
  if (ctx.strict || ctx.onWarning !== undefined) {
134
215
  checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
135
216
  }
@@ -139,24 +220,24 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
139
220
  if (isNestedFlowConstruct(rawVal)) {
140
221
  throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `Lima: invalid flow nesting at line ${line}: "${rawVal}"` });
141
222
  }
142
- builder.setMapping(entries, key, parseQuotedOrTyped(rawVal, ctx, line, false, builder));
223
+ builder.setMapping(entries, key, parseQuotedOrTyped(rawVal, ctx, line, builder, elementSource(source, val, valueStart, valueEnd)));
143
224
  }
144
225
  return builder.mapping(entries, line);
145
226
  };
146
227
  /** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
147
- export const parseFlowOrScalarValue = (raw, ctx, line, builder) => {
228
+ export const parseFlowOrScalarValue = (raw, ctx, line, builder, source) => {
148
229
  const first = raw.charCodeAt(0);
149
230
  if (first === 91) {
150
- const sequence = parseFlowSequence(raw, ctx, line, builder);
231
+ const sequence = parseFlowSequence(raw, ctx, line, builder, source);
151
232
  if (sequence !== null)
152
233
  return builder.array(sequence, line);
153
- return parseScalarValue(raw, ctx, line, builder);
234
+ return parseScalarValue(raw, ctx, line, builder, source);
154
235
  }
155
236
  else if (first === 123) {
156
- const mapping = parseFlowMapping(raw, ctx, line, builder);
237
+ const mapping = parseFlowMapping(raw, ctx, line, builder, source);
157
238
  if (mapping !== null)
158
239
  return mapping;
159
- return parseScalarValue(raw, ctx, line, builder);
240
+ return parseScalarValue(raw, ctx, line, builder, source);
160
241
  }
161
- return parseQuotedOrTyped(raw, ctx, line, true, builder);
242
+ return parseQuotedOrTyped(raw, ctx, line, builder, source);
162
243
  };
@@ -20,6 +20,13 @@ export type Diagnostic = {
20
20
  export type ParseContext = {
21
21
  strict: boolean;
22
22
  onWarning?: (diagnostic: Diagnostic) => void;
23
+ /**
24
+ * Per-line (0-based) count of columns added by Core §3 leading-tab
25
+ * expansion — subtracted from a References 2.0 token's column so it
26
+ * reports its position in the *original* source (§2.4), not the
27
+ * tab-expanded one. Absent when the document had no tabs.
28
+ */
29
+ tabAdjust?: number[];
23
30
  };
24
31
  export declare const DOCUMENT_SIZE_LIMIT = 65536;
25
32
  export declare const KEY_LENGTH_LIMIT = 128;
@@ -28,14 +35,6 @@ export declare const NESTING_DEPTH_LIMIT = 16;
28
35
  export declare const byteLength: (s: string) => number;
29
36
  export declare const checkStringLimit: (value: string, line: number) => void;
30
37
  export declare const checkScalarLimit: (v: LimaValue, line: number) => void;
31
- /**
32
- * `line` is a thunk, not a plain number: computing a top-level key's line
33
- * can trigger an O(document length) scan (see `keyLine` in core.ts) the very
34
- * first time it's called, and this check runs for every key in the
35
- * document. Evaluating it eagerly would pay that cost on every parse, even
36
- * though the overwhelming majority of keys never violate the limit — the
37
- * thunk defers it to the one branch that actually needs a line number.
38
- */
39
- export declare const checkKeyLength: (key: string, line: () => number) => void;
38
+ export declare const checkKeyLength: (key: string, line: number) => void;
40
39
  export declare const checkDuplicateKey: (exists: boolean, key: string, line: number, ctx: ParseContext) => void;
41
40
  export declare const checkDuplicateKeyMap: (entries: Map<string, unknown>, key: string, line: number, ctx: ParseContext) => void;
package/dist/normalize.js CHANGED
@@ -28,20 +28,11 @@ export const checkScalarLimit = (v, line) => {
28
28
  if (v.kind === 'string')
29
29
  checkStringLimit(v.value, line);
30
30
  };
31
- /**
32
- * `line` is a thunk, not a plain number: computing a top-level key's line
33
- * can trigger an O(document length) scan (see `keyLine` in core.ts) the very
34
- * first time it's called, and this check runs for every key in the
35
- * document. Evaluating it eagerly would pay that cost on every parse, even
36
- * though the overwhelming majority of keys never violate the limit — the
37
- * thunk defers it to the one branch that actually needs a line number.
38
- */
39
31
  export const checkKeyLength = (key, line) => {
40
32
  if (key.length > KEY_LENGTH_LIMIT && codepointLength(key) > KEY_LENGTH_LIMIT) {
41
- const l = line();
42
33
  throw new LimaError({
43
- code: 'RESOURCE_LIMIT', line: l,
44
- message: `Lima: key "${key}" exceeds maximum length of ${KEY_LENGTH_LIMIT} code points at line ${l}`,
34
+ code: 'RESOURCE_LIMIT', line,
35
+ message: `Lima: key "${key}" exceeds maximum length of ${KEY_LENGTH_LIMIT} code points at line ${line}`,
45
36
  });
46
37
  }
47
38
  };
@@ -1,28 +1,9 @@
1
- /**
2
- * Lima References 1.0 — layered strictly on top of `core.ts` (Appendix B:
3
- * reference resolution is exclusively this extension's concern; Core never
4
- * sees it). Reads the annotated `PositionedValue` tree Core produces and
5
- * performs the two-phase resolution the spec describes (§4), keeping every
6
- * intermediate result in the SAME annotated representation instead of a
7
- * separate raw-value tree with a bolted-on marker for "inert":
8
- *
9
- * - A string's `quoted` flag (set once, by Core, from actual quote syntax)
10
- * is the single source of truth for "never treat this as a reference
11
- * site" — §2.3 (quoted tokens) and §3.8 (partial content) are the same
12
- * rule applied to two different origins, so partial values are wrapped
13
- * into this same representation with every string leaf pre-marked
14
- * `quoted: true` before insertion (`partialToPositioned`), rather than
15
- * needing a second, unrelated mechanism.
16
- * - Every node keeps the source `line` it was parsed with, even after
17
- * being deep-copied into a new position — diagnostics read it directly
18
- * off the node instead of falling back to a coarser key-level lookup.
19
- *
20
- * There is no module-level mutable state: diagnostics are collected in a
21
- * `ResolutionContext` created fresh per call and threaded explicitly.
22
- */
1
+ /** Position-annotated tree helpers shared by frozen References 1.0 and active References 2.0; contains no reference grammar or resolution logic. */
2
+ import { type InsertedAt, type NativeValue, type PositionedValue } from './core.js';
23
3
  import { type LimaValue } from './value.js';
24
- import { type PositionedValue, type Diagnostic, type InsertedAt, type NativeValue } from './core.js';
25
4
  type Meta = Record<string, unknown>;
5
+ /** Every Lima mapping result must be a prototype-free object. */
6
+ export declare const emptyMapping: () => Meta;
26
7
  /**
27
8
  * Structural deep copy for document-derived targets — preserves each leaf's
28
9
  * own line/quoted (§4.2/R-104: still eligible for further resolution) and
@@ -81,21 +62,12 @@ export type FinalizedValue = {
81
62
  deepestParticipants: InsertedAt[];
82
63
  };
83
64
  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. */
65
+ /**
66
+ * Earliest participant by source position — lowest line, then lowest
67
+ * character offset (References §5) — or null when none exist (R-113's
68
+ * "line 1" fallback applies then).
69
+ */
85
70
  export declare const earliestParticipant: (participants: InsertedAt[]) => InsertedAt | null;
86
71
  /** Node-count attribution for the RESOURCE_LIMIT error path: every reference insertion anywhere in the tree contributes to the total. */
87
72
  export declare const collectAllParticipants: (v: PositionedValue, acc: InsertedAt[]) => void;
88
- export type ReferencesOptions = {
89
- /** Named values available via `(%key)` references. */
90
- partials?: Meta;
91
- strict?: boolean;
92
- /** Core §11.2 (inherited): callback for non-strict warnings (e.g. duplicate keys). Discarded if omitted. */
93
- onWarning?: (diagnostic: Diagnostic) => void;
94
- };
95
- export declare const parseReferences: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: ReferencesOptions) => T;
96
- /** Backward-compatible primary entry point — References layered on Core. */
97
- export declare const parse: typeof parseReferences;
98
- export type ParseOptions = ReferencesOptions;
99
- /** Internal conformance entry point for the frozen References 1.0 suite. */
100
- export declare const parseReferencesV1: typeof parseReferences;
101
73
  export {};
@@ -0,0 +1,123 @@
1
+ /** Position-annotated tree helpers shared by frozen References 1.0 and active References 2.0; contains no reference grammar or resolution logic. */
2
+ import { toNativeFromPositioned, } from './core.js';
3
+ /** Every Lima mapping result must be a prototype-free object. */
4
+ export const emptyMapping = () => Object.create(null);
5
+ /**
6
+ * Structural deep copy for document-derived targets — preserves each leaf's
7
+ * own line/quoted (§4.2/R-104: still eligible for further resolution) and
8
+ * any existing `insertedAt` provenance from an earlier, nested resolution
9
+ * (R-112: a reference copied wholesale into a new position can itself
10
+ * already contain the result of an inner reference that resolved first —
11
+ * both insertion sites remain identifiable participants after the copy).
12
+ */
13
+ export const deepCopyPositioned = (v) => {
14
+ switch (v.kind) {
15
+ case 'array': return { kind: 'array', items: v.items.map(deepCopyPositioned), line: v.line, references2Active: v.references2Active, insertedAt: v.insertedAt, priorInsertions: v.priorInsertions };
16
+ case 'mapping': {
17
+ const entries = new Map();
18
+ for (const [k, c] of v.entries)
19
+ entries.set(k, deepCopyPositioned(c));
20
+ return { kind: 'mapping', entries, line: v.line, references2Active: v.references2Active, insertedAt: v.insertedAt, priorInsertions: v.priorInsertions };
21
+ }
22
+ case 'instant': return { kind: 'instant', value: new Date(v.value.getTime()), line: v.line, insertedAt: v.insertedAt, priorInsertions: v.priorInsertions };
23
+ default: return v;
24
+ }
25
+ };
26
+ /**
27
+ * Wraps a partial's ingested value into the annotated representation, with
28
+ * every string leaf marked permanently inert (§3.8: "no traversal into
29
+ * partial values" — the resolution phases must never rediscover a
30
+ * reference-like substring inside partial content). Called once per partial
31
+ * name at ingestion (see the call site), producing the one canonical tree
32
+ * every pure reference to that partial retrieves — deep-copying it on every
33
+ * such reference (§3.1) is the resolveTree call site's job, not this one.
34
+ */
35
+ export const partialToPositioned = (v, line) => {
36
+ switch (v.kind) {
37
+ case 'null': return { kind: 'null', line };
38
+ case 'bool': return { kind: 'bool', value: v.value, line };
39
+ case 'int': return { kind: 'int', value: v.value, line };
40
+ case 'float': return { kind: 'float', value: v.value, line };
41
+ case 'string': return { kind: 'string', value: v.value, line, quoted: true };
42
+ case 'instant': return { kind: 'instant', value: v.value, line };
43
+ case 'array': return { kind: 'array', items: v.items.map((i) => partialToPositioned(i, line)), line };
44
+ case 'mapping': {
45
+ const entries = new Map();
46
+ for (const [k, c] of v.entries)
47
+ entries.set(k, partialToPositioned(c, line));
48
+ return { kind: 'mapping', entries, line };
49
+ }
50
+ }
51
+ };
52
+ export const finalizePositioned = (v) => {
53
+ const own = v.insertedAt ? [...(v.priorInsertions ?? []), v.insertedAt] : (v.priorInsertions ?? []);
54
+ if (v.kind === 'array') {
55
+ if (v.items.length === 0)
56
+ return { native: [], nodeCount: 1, depth: 1, deepestParticipants: own };
57
+ const native = new Array(v.items.length);
58
+ let nodeCount = 1;
59
+ let maxDepth = -1;
60
+ let childParticipants = [];
61
+ for (let i = 0; i < v.items.length; i++) {
62
+ const r = finalizePositioned(v.items[i]);
63
+ native[i] = r.native;
64
+ nodeCount += r.nodeCount;
65
+ if (r.depth > maxDepth) {
66
+ maxDepth = r.depth;
67
+ childParticipants = r.deepestParticipants.slice();
68
+ }
69
+ else if (r.depth === maxDepth) {
70
+ for (const p of r.deepestParticipants)
71
+ childParticipants.push(p);
72
+ }
73
+ }
74
+ return { native, nodeCount, depth: 1 + maxDepth, deepestParticipants: own.length ? own.concat(childParticipants) : childParticipants };
75
+ }
76
+ if (v.kind === 'mapping') {
77
+ if (v.entries.size === 0)
78
+ return { native: emptyMapping(), nodeCount: 1, depth: 1, deepestParticipants: own };
79
+ const native = emptyMapping();
80
+ let nodeCount = 1;
81
+ let maxDepth = -1;
82
+ let childParticipants = [];
83
+ for (const [k, c] of v.entries) {
84
+ const r = finalizePositioned(c);
85
+ native[k] = r.native;
86
+ nodeCount += r.nodeCount;
87
+ if (r.depth > maxDepth) {
88
+ maxDepth = r.depth;
89
+ childParticipants = r.deepestParticipants.slice();
90
+ }
91
+ else if (r.depth === maxDepth) {
92
+ for (const p of r.deepestParticipants)
93
+ childParticipants.push(p);
94
+ }
95
+ }
96
+ return { native: native, nodeCount, depth: 1 + maxDepth, deepestParticipants: own.length ? own.concat(childParticipants) : childParticipants };
97
+ }
98
+ // Scalar leaf: null/bool/int/float/string/instant. No further recursion,
99
+ // so plain toNativeFromPositioned is exactly the single-node conversion
100
+ // needed here — reused rather than duplicated.
101
+ return { native: toNativeFromPositioned(v), nodeCount: 1, depth: 0, deepestParticipants: own };
102
+ };
103
+ /**
104
+ * Earliest participant by source position — lowest line, then lowest
105
+ * character offset (References §5) — or null when none exist (R-113's
106
+ * "line 1" fallback applies then).
107
+ */
108
+ export const earliestParticipant = (participants) => participants.length === 0
109
+ ? null
110
+ : participants.reduce((a, b) => b.line < a.line || (b.line === a.line && (b.offset ?? 0) < (a.offset ?? 0)) ? b : a);
111
+ /** Node-count attribution for the RESOURCE_LIMIT error path: every reference insertion anywhere in the tree contributes to the total. */
112
+ export const collectAllParticipants = (v, acc) => {
113
+ if (v.priorInsertions)
114
+ acc.push(...v.priorInsertions);
115
+ if (v.insertedAt)
116
+ acc.push(v.insertedAt);
117
+ if (v.kind === 'array')
118
+ for (const item of v.items)
119
+ collectAllParticipants(item, acc);
120
+ if (v.kind === 'mapping')
121
+ for (const c of v.entries.values())
122
+ collectAllParticipants(c, acc);
123
+ };