@limaformat/lima 0.5.1 → 0.7.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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Lima Core 1.0 parser — reference-unaware by construction (Appendix B:
2
+ * Lima Core 1.1 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
@@ -19,6 +19,7 @@
19
19
  * and mappings) — this file is the orchestrator that ties them together.
20
20
  */
21
21
  import { checkKeyLength, byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, } from './normalize.js';
22
+ import { validateDocument } from './document.js';
22
23
  import { unescapeDQ, stripComment, stripCommentKeepEscapes, positionedBuilder, NO_SPAN_VALUE, parseSimpleScalarSpan, } from './scalars.js';
23
24
  import { isTrimWhitespace } from './chars.js';
24
25
  import { parseFlowOrScalarValue } from './flow.js';
@@ -26,6 +27,7 @@ import { parseBlockRange } from './block.js';
26
27
  import { buildBlockScalar } from './block-scalar.js';
27
28
  import { LimaError } from './errors.js';
28
29
  import { KeyCursor, scanKeys } from './scanner.js';
30
+ import { checkRangeLines, isBlockScalarMarker } from './structure.js';
29
31
  export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
30
32
  export { toPlainValue } from './scalars.js';
31
33
  /** Every Lima mapping result must be a prototype-free object (Core §11.1). */
@@ -77,7 +79,7 @@ const lineAt = (s, pos) => {
77
79
  return n;
78
80
  };
79
81
  /**
80
- * Parses Lima Core 1.0 syntax, generic over the output representation
82
+ * Parses Lima Core 1.1 syntax, generic over the output representation
81
83
  * (`builder` — see `builder.ts`): References needs the annotated
82
84
  * `PositionedValue` tree (`parseCoreWithPositions` below fixes `V` to
83
85
  * that), while `parseCore` fixes `V` to the public native shape directly,
@@ -186,6 +188,18 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
186
188
  }
187
189
  const firstNewline = inlineEnd ?? frontMatter.indexOf('\n', rawStart);
188
190
  const newlineInRange = firstNewline < nextStart;
191
+ // §6.1.5: `|` alone is a literal block scalar marker, with a body or without one (then its value is empty).
192
+ // The introducing key is at column 0; `buildBlockScalar` finds the scalar's own extent within the range (which
193
+ // runs to the next top-level key), so a dedented comment or dedented freetext before that key ends the scalar
194
+ // rather than being absorbed into it.
195
+ if (!isBlock && frontMatter.charCodeAt(rawStart) === 124) {
196
+ const markerEnd = newlineInRange && firstNewline >= 0 ? firstNewline : nextStart;
197
+ if (frontMatter.slice(rawStart, markerEnd).trim() === '|') {
198
+ const bodyLines = markerEnd < nextStart ? frontMatter.slice(markerEnd + 1, nextStart).split('\n') : [];
199
+ builder.setMapping(root, key, buildBlockScalar(bodyLines, 0, line, ctx, builder).value);
200
+ return;
201
+ }
202
+ }
189
203
  // Physical codepoint column of the value's first character in its
190
204
  // source line — the anchor for References 2.0 token positions (§2.4).
191
205
  // References 2.0 token positions (§2.4): only build the source anchor
@@ -232,31 +246,18 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
232
246
  lineStart = j + 1;
233
247
  }
234
248
  }
235
- {
236
- if (lines.length === 0) {
237
- builder.setMapping(root, key, builder.null(line));
238
- return;
239
- }
240
- const line0Trimmed = lines[0].trim();
241
- if (lines.length === 1 || line0Trimmed !== '|') {
242
- const line0 = lines[0];
243
- const hasHash = line0.includes('#');
244
- const val = hasHash ? stripComment(line0) : line0;
245
- const source = wantSource ? {
246
- raw: hasHash ? stripCommentKeepEscapes(line0) : line0,
247
- line, col: valueCol, tabAdjust: lineTabAdjust,
248
- } : undefined;
249
- builder.setMapping(root, key, parseFlowOrScalarValue(val, ctx, line, builder, source));
250
- return;
251
- }
252
- // Multi-line string (`|` literal block scalar). The introducing key
253
- // is at column 0 here; `buildBlockScalar` finds the scalar's own
254
- // extent within the range (which runs to the next top-level key),
255
- // so a dedented comment or dedented freetext before that key ends
256
- // the scalar rather than being absorbed into it.
257
- const bodyLines = raw.slice(raw.indexOf('\n') + 1).split('\n');
258
- builder.setMapping(root, key, buildBlockScalar(bodyLines, 0, line, ctx, builder).value);
249
+ if (lines.length === 0) {
250
+ builder.setMapping(root, key, builder.null(line));
251
+ return;
259
252
  }
253
+ const line0 = lines[0];
254
+ const hasHash = line0.includes('#');
255
+ const val = hasHash ? stripComment(line0) : line0;
256
+ const source = wantSource ? {
257
+ raw: hasHash ? stripCommentKeepEscapes(line0) : line0,
258
+ line, col: valueCol, tabAdjust: lineTabAdjust,
259
+ } : undefined;
260
+ builder.setMapping(root, key, parseFlowOrScalarValue(val, ctx, line, builder, source));
260
261
  };
261
262
  // A recognised top-level entry needs at least one UTF-16 unit of key text,
262
263
  // `:`, and its terminating newline: three units. Consequently 129 entries
@@ -293,6 +294,9 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
293
294
  else {
294
295
  const matches = scanKeys(frontMatter);
295
296
  const keyCount = matches.length;
297
+ // Core 1.1: before the first key no line belongs to any structure (structure.ts)
298
+ if (strict)
299
+ checkRangeLines(frontMatter, 0, keyCount === 0 ? frontMatter.length : matches[0].matchStart, 1, 'none');
296
300
  if (keyCount === 0)
297
301
  return root;
298
302
  if (keyCount > TOP_LEVEL_KEY_LIMIT) {
@@ -305,8 +309,19 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
305
309
  const m = matches[i];
306
310
  const rawDQ = m.doubleQuotedRaw;
307
311
  const key = m.unquoted ?? m.singleQuoted ?? (rawDQ !== undefined ? unescapeDQ(rawDQ, strict, m.line) : undefined);
312
+ const nextStart = i + 1 < keyCount ? matches[i + 1].matchStart : frontMatter.length;
308
313
  if (key !== undefined)
309
- parseEntry(key, m.line, m.rawStart, m.isBlock, m.inlineEnd, i + 1 < keyCount ? matches[i + 1].matchStart : frontMatter.length);
314
+ parseEntry(key, m.line, m.rawStart, m.isBlock, m.inlineEnd, nextStart);
315
+ // Core 1.1: the lines after the key's own line, up to the next key, belong to its block or to nothing
316
+ if (strict) {
317
+ const lineEnd = m.isBlock ? m.rawStart - 1 : (m.inlineEnd ?? frontMatter.indexOf('\n', m.rawStart));
318
+ // no line after the key's own one: nothing to check, the usual case
319
+ if (lineEnd !== -1 && lineEnd + 1 < nextStart) {
320
+ const owner = m.isBlock ? 'mapping'
321
+ : isBlockScalarMarker(frontMatter, m.rawStart, lineEnd) ? 'scalar' : 'inline';
322
+ checkRangeLines(frontMatter, lineEnd + 1, nextStart, m.line + 1, owner, m.rawStart, lineEnd);
323
+ }
324
+ }
310
325
  }
311
326
  }
312
327
  // Core §9 nesting depth, over Core's own reference-inert tree. When
@@ -322,7 +337,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
322
337
  return root;
323
338
  };
324
339
  /**
325
- * Parses Lima Core 1.0 syntax into the internal annotated value tree —
340
+ * Parses Lima Core 1.1 syntax into the internal annotated value tree —
326
341
  * every node carrying its source line, string leaves additionally carrying
327
342
  * whether they came from quoted syntax. `($key)`/`(%key)` text is left
328
343
  * exactly as written; nothing here ever inspects or resolves it. The
@@ -438,13 +453,17 @@ export const toNativeFromPositioned = (v) => {
438
453
  }
439
454
  };
440
455
  /**
441
- * Public Core 1.0 entry point. Never resolves `($key)`/`(%key)` text — see
456
+ * Public Core 1.1 entry point. Never resolves `($key)`/`(%key)` text — see
442
457
  * the module doc comment. Equivalent in observable behavior to calling
443
458
  * `parseReferences()` with no partials on a document that happens to
444
459
  * contain no references, except that here reference-shaped text is
445
460
  * guaranteed to always pass through unresolved, even in strict mode.
446
461
  */
447
- export const parseCore = (frontMatter, options) => {
462
+ export const parseCoreValidated = (frontMatter, options) => {
448
463
  const ctx = { strict: options?.strict ?? false, onWarning: options?.onWarning };
449
464
  return parseCoreGeneric(frontMatter, ctx, nativeBuilder, depthOfNative);
450
465
  };
466
+ export const parseCore = (frontMatter, options) => {
467
+ validateDocument(frontMatter);
468
+ return parseCoreValidated(frontMatter, options);
469
+ };
@@ -0,0 +1,2 @@
1
+ /** Core §1: a document is a sequence of Unicode scalar values. */
2
+ export declare const validateDocument: (document: string) => void;
@@ -0,0 +1,29 @@
1
+ import { LimaError } from './errors.js';
2
+ /** Core §1: a document is a sequence of Unicode scalar values. */
3
+ export const validateDocument = (document) => {
4
+ // Node 22's native scalar-value check keeps the common valid path out of
5
+ // JavaScript. Only malformed input pays for the line-attribution walk.
6
+ if (document.isWellFormed())
7
+ return;
8
+ let line = 1;
9
+ for (let i = 0; i < document.length; i++) {
10
+ const c = document.charCodeAt(i);
11
+ if (c === 10) {
12
+ line++;
13
+ continue;
14
+ }
15
+ if (c >= 0xd800 && c <= 0xdbff && i + 1 < document.length) {
16
+ const low = document.charCodeAt(i + 1);
17
+ if (low >= 0xdc00 && low <= 0xdfff) {
18
+ i++;
19
+ continue;
20
+ }
21
+ }
22
+ if (c >= 0xd800 && c <= 0xdfff) {
23
+ throw new LimaError({
24
+ code: 'INVALID_DOCUMENT', line,
25
+ message: `Lima: document contains an invalid Unicode scalar value at line ${line}`,
26
+ });
27
+ }
28
+ }
29
+ };
package/dist/errors.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * module directly rather than duplicating it) inspect *why* a parse failed
9
9
  * without re-parsing the message string.
10
10
  */
11
- export type LimaDiagnosticCode = 'INVALID_ESCAPE' | 'INVALID_QUOTE' | 'INVALID_DATE' | 'INVALID_NUMBER' | 'INVALID_REFERENCE_SHAPE' | 'INVALID_INDENTATION' | 'INVALID_FLOW_SYNTAX' | 'DUPLICATE_KEY' | 'RESOURCE_LIMIT' | 'UNRESOLVED_REFERENCE' | 'INVALID_INTERPOLATION' | 'INVALID_PARTIAL';
11
+ export type LimaDiagnosticCode = 'INVALID_DOCUMENT' | 'INVALID_ESCAPE' | 'INVALID_QUOTE' | 'INVALID_DATE' | 'INVALID_NUMBER' | 'INVALID_REFERENCE_SHAPE' | 'INVALID_INDENTATION' | 'UNRECOGNIZED_LINE' | 'INVALID_FLOW_SYNTAX' | 'DUPLICATE_KEY' | 'RESOURCE_LIMIT' | 'UNRESOLVED_REFERENCE' | 'INVALID_INTERPOLATION' | 'INVALID_PARTIAL';
12
12
  export interface LimaDiagnostic {
13
13
  code: LimaDiagnosticCode;
14
14
  message: string;
package/dist/index.d.ts CHANGED
@@ -3,12 +3,12 @@
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
- * - `references2.ts` — the References 2.0 extension layered on Core.
6
+ * - `core.ts` — Lima Core 1.2, reference-unaware by construction.
7
+ * - `references2.ts` — the References 2.1 extension layered on Core.
8
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` is the primary References 2.0 entry point. `parseReferences` is
11
+ * `parse` is the primary References 2.1 entry point. `parseReferences` is
12
12
  * retained as its deprecated compatibility alias.
13
13
  */
14
14
  export { parseCore, type CoreOptions, type Diagnostic } from './core.js';
package/dist/index.js CHANGED
@@ -3,12 +3,12 @@
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
- * - `references2.ts` — the References 2.0 extension layered on Core.
6
+ * - `core.ts` — Lima Core 1.2, reference-unaware by construction.
7
+ * - `references2.ts` — the References 2.1 extension layered on Core.
8
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` is the primary References 2.0 entry point. `parseReferences` is
11
+ * `parse` is the primary References 2.1 entry point. `parseReferences` is
12
12
  * retained as its deprecated compatibility alias.
13
13
  */
14
14
  export { parseCore } from './core.js';
@@ -0,0 +1,3 @@
1
+ export declare const isKeyInitialCodePoint: (c: number) => boolean;
2
+ export declare const isKeyCharacterCodePoint: (c: number) => boolean;
3
+ export declare const isValidUnquotedKey: (raw: string, allowSlash?: boolean) => boolean;
@@ -0,0 +1,58 @@
1
+ import { UNICODE_KEY_RANGES } from './unicode-key-table.js';
2
+ const unicodeKeyCategory = (codePoint) => {
3
+ let low = 0;
4
+ let high = UNICODE_KEY_RANGES.length / 3;
5
+ while (low < high) {
6
+ const mid = (low + high) >>> 1;
7
+ const offset = mid * 3;
8
+ if (codePoint < UNICODE_KEY_RANGES[offset])
9
+ high = mid;
10
+ else if (codePoint > UNICODE_KEY_RANGES[offset + 1])
11
+ low = mid + 1;
12
+ else
13
+ return UNICODE_KEY_RANGES[offset + 2];
14
+ }
15
+ return 0;
16
+ };
17
+ export const isKeyInitialCodePoint = (c) => {
18
+ if (c <= 0x7f)
19
+ return c === 95 || c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122;
20
+ return (unicodeKeyCategory(c) & 5) !== 0; // L or N
21
+ };
22
+ export const isKeyCharacterCodePoint = (c) => {
23
+ if (c <= 0x7f)
24
+ return isKeyInitialCodePoint(c) || c === 58 || c === 45 || c === 42;
25
+ return c === 0xb7 || unicodeKeyCategory(c) !== 0; // L, M or N
26
+ };
27
+ export const isValidUnquotedKey = (raw, allowSlash = false) => {
28
+ if (raw.length === 0)
29
+ return false;
30
+ let i = 0;
31
+ let cp = raw.charCodeAt(0);
32
+ if (cp <= 0x7f) {
33
+ if (!(cp === 95 || cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122))
34
+ return false;
35
+ i = 1;
36
+ }
37
+ else {
38
+ cp = raw.codePointAt(0);
39
+ if (!isKeyInitialCodePoint(cp))
40
+ return false;
41
+ i = cp > 0xffff ? 2 : 1;
42
+ }
43
+ while (i < raw.length) {
44
+ cp = raw.charCodeAt(i);
45
+ if (cp <= 0x7f) {
46
+ if (!(cp === 95 || cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 ||
47
+ cp === 58 || cp === 45 || cp === 42 || allowSlash && cp === 47))
48
+ return false;
49
+ }
50
+ else {
51
+ cp = raw.codePointAt(i);
52
+ if (!isKeyCharacterCodePoint(cp))
53
+ return false;
54
+ }
55
+ i += cp > 0xffff ? 2 : 1;
56
+ }
57
+ return true;
58
+ };
@@ -0,0 +1,2 @@
1
+ export declare const KEY_GRAMMAR_HINT = " (an unquoted key starts with a Unicode letter or number or \"_\", then has letters, marks, numbers, \"_\", \":\", \"-\", \"*\" and \"\u00B7\"; quote any other)";
2
+ export declare const specificKeyHint: (text: string) => string | undefined;
@@ -0,0 +1,17 @@
1
+ import { isTrimWhitespace } from './chars.js';
2
+ import { isValidUnquotedKey } from './key-grammar.js';
3
+ export const KEY_GRAMMAR_HINT = ' (an unquoted key starts with a Unicode letter or number or "_", then has letters, marks, numbers, "_", ":", "-", "*" and "·"; quote any other)';
4
+ export const specificKeyHint = (text) => {
5
+ let i = 0;
6
+ while (text.charCodeAt(i) === 32)
7
+ i++;
8
+ const cp = text.codePointAt(i);
9
+ if (cp !== undefined && cp !== 32 && isTrimWhitespace(cp)) {
10
+ return ` (U+${cp.toString(16).toUpperCase().padStart(4, '0')} before the key; Lima indents by spaces only)`;
11
+ }
12
+ const colon = text.indexOf('\uFF1A');
13
+ if (colon > 0 && isValidUnquotedKey(text.slice(0, colon))) {
14
+ return ' (the key-value separator is ": " (U+003A and a space), not U+FF1A FULLWIDTH COLON)';
15
+ }
16
+ return undefined;
17
+ };
@@ -39,11 +39,6 @@ export type ReferenceSource = {
39
39
  col: number;
40
40
  tabAdjust?: number[];
41
41
  };
42
- export declare const SEGMENT = "[a-zA-Z0-9_][a-zA-Z0-9_:-]*";
43
- export declare const DOC_PATH = "[a-zA-Z0-9_][a-zA-Z0-9_:-]*(?:\\.[a-zA-Z0-9_][a-zA-Z0-9_:-]*)*";
44
- export declare const PARTIAL_NAME = "[a-zA-Z0-9_][a-zA-Z0-9_:/-]*";
45
- export declare const PARTIAL_PATH = "[a-zA-Z0-9_][a-zA-Z0-9_:/-]*(?:\\.[a-zA-Z0-9_][a-zA-Z0-9_:-]*)*";
46
- export declare const PURE_REFERENCE_2: RegExp;
47
42
  /** Converts a 0-based codepoint offset to JavaScript/LSP UTF-16 units. */
48
43
  export declare const codepointOffsetToUtf16: (text: string, codepointOffset: number) => number;
49
44
  /** Converts a 0-based JavaScript/LSP UTF-16 offset to a codepoint offset. */
@@ -1,11 +1,8 @@
1
1
  /** Reference tokens recorded while the annotated Core tree is being built. */
2
- export const SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
3
- export const DOC_PATH = `${SEGMENT}(?:\\.${SEGMENT})*`;
4
- export const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
5
- export const PARTIAL_PATH = `${PARTIAL_NAME}(?:\\.${SEGMENT})*`;
6
- export const PURE_REFERENCE_2 = new RegExp(`^(?:\\$\\{(${DOC_PATH})\\}|\\$\\((${PARTIAL_PATH})\\))$`);
7
- const REFERENCE_2 = new RegExp(`\\$\\{(${DOC_PATH})\\}|\\$\\((${PARTIAL_PATH})\\)`, 'g');
2
+ import { isKeyCharacterCodePoint, isKeyInitialCodePoint } from './key-grammar.js';
8
3
  const NO_TOKENS = [];
4
+ const isAsciiReferenceInitial = (c) => (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 95;
5
+ const isAsciiReferenceCharacter = (c, allowSlash) => isAsciiReferenceInitial(c) || c === 58 || c === 45 || c === 42 || allowSlash && c === 47;
9
6
  const utf16WidthAt = (text, offset) => text.codePointAt(offset) > 0xffff ? 2 : 1;
10
7
  /** Codepoint length of the UTF-16 slice `[a, b)` of `s`. */
11
8
  const codepointLen = (s, a, b) => {
@@ -38,27 +35,80 @@ export const utf16OffsetToCodepoint = (text, utf16Offset) => {
38
35
  return codepoints;
39
36
  };
40
37
  /** Splice points (decoded value order): token text, UTF-16 index, path. */
38
+ const scanComponent = (value, start, allowSlash) => {
39
+ let i = start;
40
+ if (i >= value.length)
41
+ return -1;
42
+ let cp = value.charCodeAt(i);
43
+ if (cp <= 0x7f) {
44
+ if (!isAsciiReferenceInitial(cp))
45
+ return -1;
46
+ }
47
+ else {
48
+ cp = value.codePointAt(i);
49
+ if (!isKeyInitialCodePoint(cp))
50
+ return -1;
51
+ }
52
+ i += cp > 0xffff ? 2 : 1;
53
+ while (i < value.length && isAsciiReferenceCharacter(value.charCodeAt(i), allowSlash))
54
+ i++;
55
+ while (i < value.length) {
56
+ cp = value.charCodeAt(i);
57
+ if (cp <= 0x7f)
58
+ break;
59
+ cp = value.codePointAt(i);
60
+ if (!isKeyCharacterCodePoint(cp))
61
+ break;
62
+ i += cp > 0xffff ? 2 : 1;
63
+ while (i < value.length && isAsciiReferenceCharacter(value.charCodeAt(i), allowSlash))
64
+ i++;
65
+ }
66
+ return i;
67
+ };
68
+ const scanPath = (value, start, partial) => {
69
+ let i = scanComponent(value, start, partial);
70
+ if (i < 0)
71
+ return -1;
72
+ while (value.charCodeAt(i) === 46) {
73
+ const next = scanComponent(value, i + 1, false);
74
+ if (next < 0)
75
+ return -1;
76
+ i = next;
77
+ }
78
+ return i;
79
+ };
41
80
  const scanDecoded = (value) => {
42
- REFERENCE_2.lastIndex = 0;
43
81
  const out = [];
44
- for (const m of value.matchAll(REFERENCE_2)) {
45
- out.push({
46
- token: m[0], index: m.index ?? 0,
47
- ...(m[1] !== undefined ? { documentPath: m[1] } : { partialPath: m[2] }),
48
- });
82
+ for (let i = 0; i + 2 < value.length;) {
83
+ const partial = value.startsWith('$(', i);
84
+ const document = !partial && value.startsWith('${', i);
85
+ if (!partial && !document) {
86
+ const cp = value.codePointAt(i);
87
+ i += cp > 0xffff ? 2 : 1;
88
+ continue;
89
+ }
90
+ const end = scanPath(value, i + 2, partial);
91
+ const close = partial ? 41 : 125;
92
+ if (end >= 0 && value.charCodeAt(end) === close) {
93
+ const token = value.slice(i, end + 1);
94
+ const path = value.slice(i + 2, end);
95
+ out.push({ token, index: i, ...(partial ? { partialPath: path } : { documentPath: path }) });
96
+ i = end + 1;
97
+ }
98
+ else
99
+ i++;
49
100
  }
50
101
  return out;
51
102
  };
52
103
  /** Physical `(line, offset)` of each token, in source order, from the raw text. */
53
104
  const scanPhysical = (raw, firstLine, firstCol, tabAdjust) => {
54
- REFERENCE_2.lastIndex = 0;
55
105
  const out = [];
56
106
  let scanned = 0;
57
107
  let line = firstLine;
58
108
  let col = firstCol;
59
109
  let lineIndex = 0;
60
- for (const m of raw.matchAll(REFERENCE_2)) {
61
- const idx = m.index ?? 0;
110
+ for (const m of scanDecoded(raw)) {
111
+ const idx = m.index;
62
112
  while (scanned < idx) {
63
113
  const cp = raw.codePointAt(scanned);
64
114
  if (cp === 10) {
@@ -1,11 +1,11 @@
1
- /** Lima References 2.0 resolver. References 1.0 remains isolated in references.ts. */
1
+ /** Lima References 2.1 resolver. References 1.0 remains isolated in references.ts. */
2
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';
3
+ import { isValidUnquotedKey } from './key-grammar.js';
4
+ import { validateDocument } from './document.js';
5
+ import { NESTING_DEPTH_LIMIT, parseCoreValidated, parseCoreWithPositions, toPlainValue, } from './core.js';
4
6
  import { hasActiveReferences2 } from './scalars.js';
5
7
  import { LimaError } from './errors.js';
6
8
  import { collectAllParticipants, deepCopyPositioned, earliestParticipant, emptyMapping, finalizePositioned, partialToPositioned, } from './positioned-tree.js';
7
- const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
8
- const PARTIAL_NAME_RE = new RegExp(`^${PARTIAL_NAME}$`);
9
9
  const MAX_EDGES = 3;
10
10
  // Looking up the root path is itself the first reference edge; recursion into
11
11
  // that root therefore gets the public resolver's remaining two-edge budget.
@@ -317,7 +317,7 @@ const validatePartials = (raw) => {
317
317
  throw new LimaError({ code: 'INVALID_PARTIAL', message: `Lima: too many partials (max ${PARTIAL_COUNT_LIMIT})` });
318
318
  }
319
319
  for (const name of names) {
320
- if (!PARTIAL_NAME_RE.test(name)) {
320
+ if (!isValidUnquotedKey(name, true)) {
321
321
  throw new LimaError({ code: 'INVALID_PARTIAL', partial: name, message: `Lima: invalid partial name "${name}"` });
322
322
  }
323
323
  if (codepointLength(name) > PARTIAL_NAME_LENGTH_LIMIT) {
@@ -356,6 +356,7 @@ const scanUnresolved = (node, ctx) => {
356
356
  scanUnresolved(child, ctx);
357
357
  };
358
358
  const parseInternal = (frontMatter, options, useResolveCache) => {
359
+ validateDocument(frontMatter);
359
360
  if (options?.mode !== undefined && options.mode !== 'core' && options.mode !== 'references') {
360
361
  throw new TypeError(`Lima: invalid parse mode "${String(options.mode)}"`);
361
362
  }
@@ -365,11 +366,11 @@ const parseInternal = (frontMatter, options, useResolveCache) => {
365
366
  }
366
367
  // Core ignores the additional `mode` property. Passing the same object
367
368
  // avoids allocating a projected options object on this hot dispatch path.
368
- return parseCore(frontMatter, options);
369
+ return parseCoreValidated(frontMatter, options);
369
370
  }
370
371
  const partials = validatePartials(options?.partials ?? {});
371
372
  if (!frontMatter.includes('${') && !frontMatter.includes('$(')) {
372
- return parseCore(frontMatter, options);
373
+ return parseCoreValidated(frontMatter, options);
373
374
  }
374
375
  const document = parseCoreWithPositions(frontMatter, { strict: options?.strict ?? false, onWarning: options?.onWarning });
375
376
  const ctx = { diagnostics: [], ...(useResolveCache ? { cache: new WeakMap() } : {}) };
package/dist/scalars.js CHANGED
@@ -354,8 +354,9 @@ export const closingQuoteIndex = (s, singleQuoteEscape = true) => {
354
354
  * Kept byte-for-byte identical to `scanner.ts`'s `isKeyStartChar` /
355
355
  * `isKeyContinueChar`, which enforce the same grammar at the top level.
356
356
  */
357
- const isUnquotedKeyStart = (c) => (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 95;
358
- const isUnquotedKeyContinue = (c) => isUnquotedKeyStart(c) || c === 58 || c === 45;
357
+ import { isValidUnquotedKey } from './key-grammar.js';
358
+ const isAsciiUnquotedKeyStart = (c) => (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 95;
359
+ const isAsciiUnquotedKeyContinue = (c) => isAsciiUnquotedKeyStart(c) || c === 58 || c === 45 || c === 42;
359
360
  /**
360
361
  * Whether `raw` (a key candidate as written, before quote stripping) is a
361
362
  * usable Lima key:
@@ -375,13 +376,17 @@ export const isValidKey = (raw) => {
375
376
  const f = raw.charCodeAt(0);
376
377
  if (f === 34 || f === 39)
377
378
  return closingQuoteIndex(raw, false) === raw.length - 1;
378
- if (!isUnquotedKeyStart(f))
379
- return false;
380
- for (let i = 1; i < raw.length; i++) {
381
- if (!isUnquotedKeyContinue(raw.charCodeAt(i)))
379
+ if (f <= 0x7f) {
380
+ if (!isAsciiUnquotedKeyStart(f))
382
381
  return false;
382
+ for (let i = 1; i < raw.length; i++) {
383
+ const c = raw.charCodeAt(i);
384
+ if (!isAsciiUnquotedKeyContinue(c))
385
+ return c > 0x7f && isValidUnquotedKey(raw);
386
+ }
387
+ return true;
383
388
  }
384
- return true;
389
+ return isValidUnquotedKey(raw);
385
390
  };
386
391
  const ANY_ESCAPE_RE = /\\(u[0-9a-fA-F]{0,4}|U[0-9a-fA-F]{0,8}|x[0-9a-fA-F]{0,2}|.)/gs;
387
392
  const SINGLE_CHAR_ESCAPES = '"\\/bfnrt'; // deliberately excludes '0' — Core Appendix A: \0 is unknown, not a null shorthand.
package/dist/scanner.js CHANGED
@@ -50,8 +50,9 @@
50
50
  * U+2028 / U+2029 included, is consumed as an escape pair here and
51
51
  * resolved at decode time.
52
52
  */
53
- const isKeyStartChar = (c) => (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 95; // a-z A-Z 0-9 _
54
- const isKeyContinueChar = (c) => isKeyStartChar(c) || c === 58 || c === 45; // + : -
53
+ import { isKeyCharacterCodePoint, isKeyInitialCodePoint } from './key-grammar.js';
54
+ const isAsciiKeyStart = (c) => (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 95;
55
+ const isAsciiKeyContinue = (c) => isAsciiKeyStart(c) || c === 58 || c === 45 || c === 42;
55
56
  /** Positive = block end, negative = inline end, zero = no separator. */
56
57
  const matchSeparator = (s, pos) => {
57
58
  let i = pos;
@@ -59,6 +60,10 @@ const matchSeparator = (s, pos) => {
59
60
  i++; // consume spaces
60
61
  if (i < s.length && s.charCodeAt(i) === 10)
61
62
  return i + 1;
63
+ // Core 1.1: the end of the input ends the key's line as a newline does, so a last `key:` is `key: null`, as it
64
+ // is at any nested level (it was no key at all at the top level before)
65
+ if (i === s.length)
66
+ return i;
62
67
  // Backtracking a pure-space run can never reveal a `\n` the maximal
63
68
  // scan didn't already see (every intermediate position is followed by
64
69
  // either another space or the same non-`\n` stop character) — so
@@ -121,11 +126,30 @@ const matchAt = (s, pos, line, out) => {
121
126
  keyKind = 2;
122
127
  }
123
128
  else {
124
- if (!isKeyStartChar(c))
129
+ if (c <= 0x7f && !isAsciiKeyStart(c))
125
130
  return false;
126
131
  let runEnd = pos + 1;
127
- while (runEnd < s.length && isKeyContinueChar(s.charCodeAt(runEnd)))
132
+ if (c > 0x7f) {
133
+ const first = s.codePointAt(pos);
134
+ if (!isKeyInitialCodePoint(first))
135
+ return false;
136
+ runEnd = pos + (first > 0xffff ? 2 : 1);
137
+ }
138
+ // Preserve the old tiny ASCII loop. Unicode decoding is entered only
139
+ // when this key actually reaches a non-ASCII code unit.
140
+ while (runEnd < s.length && isAsciiKeyContinue(s.charCodeAt(runEnd)))
128
141
  runEnd++;
142
+ while (runEnd < s.length) {
143
+ let cp = s.charCodeAt(runEnd);
144
+ if (cp <= 0x7f)
145
+ break;
146
+ cp = s.codePointAt(runEnd);
147
+ if (!isKeyCharacterCodePoint(cp))
148
+ break;
149
+ runEnd += cp > 0xffff ? 2 : 1;
150
+ while (runEnd < s.length && isAsciiKeyContinue(s.charCodeAt(runEnd)))
151
+ runEnd++;
152
+ }
129
153
  for (let k = runEnd - 1; k >= pos + 1; k--) {
130
154
  if (s.charCodeAt(k) !== 58)
131
155
  continue;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * What the lines after a key belong to: nothing (`none`, before the first key; `inline`, after an inline value), a
3
+ * block mapping or sequence (`mapping`, after a bare key) or a `|` scalar (`scalar`).
4
+ */
5
+ export type RangeOwner = 'none' | 'inline' | 'mapping' | 'scalar';
6
+ /**
7
+ * Checks the lines of `frontMatter` from `start` (a line start) to `end` that follow a key owned as `owner`; `line`
8
+ * is the 1-based line at `start`; `valueStart` to `valueEnd` is the inline value of that key, read only for the hint a
9
+ * YAML marker earns in the error. Throws for the first line that belongs to no structure.
10
+ */
11
+ export declare function checkRangeLines(frontMatter: string, start: number, end: number, line: number, owner: RangeOwner, valueStart?: number, valueEnd?: number): void;
12
+ /** Whether the inline value that starts at `rawStart` and ends its line at `lineEnd` is a `|` block scalar marker. */
13
+ export declare const isBlockScalarMarker: (frontMatter: string, rawStart: number, lineEnd: number) => boolean;