@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/CHANGELOG.md CHANGED
@@ -4,14 +4,109 @@ All notable changes to `@limaformat/lima` are documented in this file.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
  This package's version number tracks its own release history and is
7
- independent of the Lima specification version — [Lima Core
8
- 1.0](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
7
+ independent of the Lima specification versions — [Lima Core
8
+ 1.2](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
9
9
  and [Lima References
10
- 1.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-1.0-spec.md)
11
- are both frozen regardless of what this file shows.
10
+ 2.1](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)
11
+ carry their own.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.7.0] — 2026-10-30
16
+
17
+ Lima Core 1.2 and References 2.1: an unquoted key may be written in any
18
+ script. It starts with a letter or number of Unicode 18.0 or `_`, and after
19
+ its first character also holds marks, `*` and `·`, so keys can be written
20
+ in any language and gendered (`größe`, `題名`, `autor*in`, `autor·in`);
21
+ references reach every such key. Core 1.2 also states in one table what
22
+ each kind of line does where it stands (§7.1), without changing an outcome.
23
+
24
+ ### Added
25
+
26
+ - Unquoted keys and reference names take Unicode letters, marks and numbers
27
+ (Unicode 18.0, fixed by the specification) and `*` and `·` after their
28
+ first character. The categories come from a generated table, not the
29
+ host's Unicode support, and no dependency; ASCII keys are read as fast as
30
+ before.
31
+ - Hints: whitespace other than spaces before a key, a FULLWIDTH COLON
32
+ (U+FF1A) in place of `: `, and the key-grammar hint for a key such as
33
+ `x y: 2` at every level.
34
+
35
+ ### Changed
36
+
37
+ - A line whose key Core 1.1 rejected for its characters, and Core 1.2
38
+ admits, is an entry now; non-strict mode skipped it, strict mode threw.
39
+ - A document with a lone surrogate is rejected with `INVALID_DOCUMENT` and
40
+ the line it is on (Core §1), in both modes.
41
+ - Node.js 22 or later (`String.prototype.isWellFormed`).
42
+
43
+ ## [0.6.0] — 2026-10-28
44
+
45
+ Lima Core 1.1: strict mode rejects a content line that belongs to no
46
+ structure, which Core 1.0 skipped in both modes. Core 1.1 also settles
47
+ cases Core 1.0 left open, where the three implementations differed; some
48
+ non-strict results change with them (below).
49
+
50
+ ### Changed
51
+
52
+ - Strict mode throws `INVALID_INDENTATION` for a line indented after a
53
+ top-level key with an inline value — the lines after a YAML `>`, `|-` or
54
+ `|+`, a key indented by a tab, freetext after a value — as it already did
55
+ one level down; the message names the YAML marker and what Lima writes
56
+ instead.
57
+ - Strict mode throws the new `UNRECOGNIZED_LINE` for an unindented line that
58
+ matches no key pattern: a key without its `: `, a dotted or otherwise
59
+ invalid key, a YAML `---` / `...` marker, a quoted key broken by a newline.
60
+ - A line of a block mapping whose key the key grammar rejects (`a.b: value`,
61
+ `bad key: value`), skipped in strict mode until now, throws `UNRECOGNIZED_LINE`
62
+ too; an indented line below a block mapping's base indentation, or after a
63
+ `|` scalar a column-0 comment ended, throws `INVALID_INDENTATION`.
64
+ - `LimaDiagnosticCode` gains `'UNRECOGNIZED_LINE'`.
65
+ - A bare key's sequence may start at the key's own column, as YAML writes
66
+ it (`tags:` / `- a` / `- b`), at any level (Core 1.1 §7.2). At the top
67
+ level this was read so by accident; one level down the sequence and the
68
+ keys after it were lost.
69
+ - An item whose value is a flow sequence (`- [a, b]`, `- []`) is a nested
70
+ sequence (Core 1.1 §7.2): `null` in non-strict mode, `INVALID_FLOW_SYNTAX`
71
+ in strict mode.
72
+ - Indentation is spaces only (Core 1.1 §7.1): a line whose spaces are followed
73
+ by other whitespace, such as U+00A0, before its content is unrecognised —
74
+ skipped in non-strict mode, an error in strict mode. Whitespace is defined
75
+ once (§1), as the set every check already used.
76
+ TypeScript counted such whitespace as indentation.
77
+ - An object item's later keys stand at its first key's column (§7.2): a line
78
+ of the item deeper than that is skipped in non-strict mode, and one less
79
+ indented ends the item; strict mode throws `INVALID_INDENTATION` for
80
+ either. Any deeper line was taken as a key of the item.
81
+
82
+ ### Fixed
83
+
84
+ - A `|` scalar as an object item's first key measured its body from the dash
85
+ plus two columns, so after wider whitespace (`- a: |`) it took the item's
86
+ next key as text; it measures from the key's column (§6.1.5, §7.2).
87
+ - In strict mode, a line deeper than a nested `|` scalar's key, after a line
88
+ of other whitespace that ended the scalar, was dropped without an error; it
89
+ throws `INVALID_INDENTATION`.
90
+ - A top-level `key:` at the very end of the input, without a line
91
+ terminator, was no key at all and vanished; it is `null`, as at any nested
92
+ level and in the Rust and Go implementations, and as Core 1.1 §4 now says.
93
+ This changes a non-strict result too.
94
+ - A `|` with no content lines (`a: |` at the end of a block, or of the
95
+ input) was the string `"|"`; it is `""` (Core 1.1 §6.1.5).
96
+ - A block of mapping entries with a sequence item in it, or the reverse, was
97
+ an empty sequence in non-strict mode, all its entries lost; the first kind
98
+ wins and a line of the other kind is skipped (Core 1.1 §7.1).
99
+ - An item value that begins with `[` but is no flow sequence (`- [x] done`),
100
+ or with `{` and does not close (`- {a: 1`), throws `INVALID_FLOW_SYNTAX`
101
+ in strict mode, as an inline value does.
102
+ - `key: | # c` below the top level, in a block mapping or an object item,
103
+ was a block scalar; it is the string `"|"`, as at the top level — the
104
+ marker stands alone (§6.1.5).
105
+
106
+ The shared conformance corpus is at 336 Core cases: the 218 Core 1.0 cases
107
+ without the 8 that Core 1.1 supersedes, plus 126 Core 1.1 cases
108
+ (`corpus/core-1.1/`).
109
+
15
110
  ## [0.5.1] — 2026-10-28
16
111
 
17
112
  Core errata `1.0.10`: aligned top-level values are trimmed, as Core 1.0
package/README.md CHANGED
@@ -38,10 +38,10 @@ tags:
38
38
  Three functions are exported:
39
39
 
40
40
  - `parseCore(input, options?)` — the base format only.
41
- - `parse(input, options?)` — the primary References 2.0 parser; adds
41
+ - `parse(input, options?)` — the primary References <!-- facts:language.references_version -->2.1<!-- /facts --> parser; adds
42
42
  `${key}` document references and `$(key)` external partials.
43
43
  - `parseReferences(input, options?)` — deprecated compatibility alias for
44
- `parse` with the same References 2.0 semantics.
44
+ `parse` with the same References <!-- facts:language.references_version -->2.1<!-- /facts --> semantics.
45
45
 
46
46
  ```ts
47
47
  parse(frontmatter, { strict: true })
@@ -50,15 +50,15 @@ parse(frontmatter, { mode: 'core' }) // same reference-unaware path as parseCore
50
50
  ```
51
51
 
52
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
53
+ covers Core <!-- facts:language.core_version -->1.2<!-- /facts --> and References <!-- facts:language.references_version -->2.1<!-- /facts --> syntax, resource limits, strict mode, and the
54
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)**.
55
+ **[References <!-- facts:language.references_version -->2.1<!-- /facts --> specification](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)**.
56
56
 
57
57
  Why Lima exists, the case against YAML, and security rationale:
58
58
  **[repository README](https://github.com/limaformat/lima#readme)**.
59
59
 
60
- The [Lima Core 1.0](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
61
- and [Lima References 2.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)
60
+ The [Lima Core <!-- facts:language.core_version -->1.2<!-- /facts -->](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
61
+ and [Lima References <!-- facts:language.references_version -->2.1<!-- /facts -->](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)
62
62
  specifications are the source of truth for these public entry points. The
63
63
  frozen References 1.0 implementation remains available internally to run its
64
64
  independent conformance suite.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Allocation-free physical-line cursor over one top-level block extent.
3
- * Positions are UTF-16 code-unit offsets. Structural boundaries are ASCII;
4
- * indentation additionally follows the complete whitespace predicate used by
5
- * `trimStart()`, matching the existing block grammar.
3
+ * Positions are UTF-16 code-unit offsets. Indentation is the run of spaces
4
+ * (Core 1.1 §7.1); other whitespace after it, such as U+00A0, starts the
5
+ * content of a line that has any, and only a blank or comment line looks
6
+ * past it (the complete whitespace predicate used by `trimStart()`).
6
7
  */
7
8
  export declare class BlockCursor {
8
9
  readonly source: string;
@@ -1,9 +1,10 @@
1
1
  import { isTrimWhitespace } from './chars.js';
2
2
  /**
3
3
  * Allocation-free physical-line cursor over one top-level block extent.
4
- * Positions are UTF-16 code-unit offsets. Structural boundaries are ASCII;
5
- * indentation additionally follows the complete whitespace predicate used by
6
- * `trimStart()`, matching the existing block grammar.
4
+ * Positions are UTF-16 code-unit offsets. Indentation is the run of spaces
5
+ * (Core 1.1 §7.1); other whitespace after it, such as U+00A0, starts the
6
+ * content of a line that has any, and only a blank or comment line looks
7
+ * past it (the complete whitespace predicate used by `trimStart()`).
7
8
  */
8
9
  export class BlockCursor {
9
10
  source;
@@ -50,6 +51,9 @@ export class BlockCursor {
50
51
  } while (content < end);
51
52
  }
52
53
  }
54
+ // other whitespace before content is no indentation: the content starts at it (Core 1.1 §7.1)
55
+ if (content > asciiEnd && content < end && this.source.charCodeAt(content) !== 35)
56
+ content = asciiEnd;
53
57
  this.lineStart = start;
54
58
  this.lineEnd = end;
55
59
  this.contentStart = content;
package/dist/block.js CHANGED
@@ -10,7 +10,8 @@
10
10
  */
11
11
  import { checkKeyLength, checkDuplicateKey, NESTING_DEPTH_LIMIT } from './normalize.js';
12
12
  import { stripKeyQuotes, stripComment, stripCommentKeepEscapes, parseQuotedOrTyped, closingQuoteIndex, isValidKey, } from './scalars.js';
13
- import { parseFlowMapping, parseFlowOrScalarValue } from './flow.js';
13
+ import { KEY_GRAMMAR_HINT, specificKeyHint } from './key-hints.js';
14
+ import { parseFlowOrScalarValue, parseFlowSequence } from './flow.js';
14
15
  import { buildBlockScalar } from './block-scalar.js';
15
16
  import { LimaError } from './errors.js';
16
17
  import { isTrimWhitespace } from './chars.js';
@@ -48,9 +49,10 @@ const inlineSource = (wantSource, ctx, keyLine, valueCol, rawUndecoded) => wantS
48
49
  raw: physicalRaw(rawUndecoded), line: keyLine, col: valueCol,
49
50
  tabAdjust: ctx.tabAdjust ? [ctx.tabAdjust[keyLine - 1] ?? 0] : undefined,
50
51
  }) : undefined;
51
- const inlineOrBlockScalar = (raw, keyIndent, keyLine, valueCol, rawUndecoded, cursor, ctx, builder) => {
52
+ /** An inline value, or a `|` block scalar when `marker`: the value is `|` alone (§6.1.5; `| # c` is the string `|`). */
53
+ const inlineOrBlockScalar = (raw, marker, keyIndent, keyLine, valueCol, rawUndecoded, cursor, ctx, builder) => {
52
54
  const wantSource = builder.positions === true;
53
- if (raw !== '|') {
55
+ if (!marker) {
54
56
  const value = parseFlowOrScalarValue(raw, ctx, keyLine, builder, inlineSource(wantSource, ctx, keyLine, valueCol, rawUndecoded));
55
57
  cursor.next();
56
58
  return value;
@@ -58,7 +60,9 @@ const inlineOrBlockScalar = (raw, keyIndent, keyLine, valueCol, rawUndecoded, cu
58
60
  cursor.next(); // past the `key: |` line
59
61
  const bodyLines = [];
60
62
  while (cursor.valid) {
61
- if (!cursor.empty && cursor.asciiIndent <= keyIndent)
63
+ // the scalar's own extent (§6.1.5, `buildBlockScalar`): a line of spaces only is empty, any other ends it at the
64
+ // key's column or less — a line of other whitespace too, which is content here
65
+ if (cursor.lineEnd - cursor.lineStart > cursor.asciiIndent && cursor.asciiIndent <= keyIndent)
62
66
  break;
63
67
  bodyLines.push(cursor.source.slice(cursor.lineStart, cursor.lineEnd));
64
68
  cursor.next();
@@ -83,6 +87,15 @@ export const findKeySep = (s) => {
83
87
  * the original source. Strings are materialized only when grammar or scalar
84
88
  * parsing needs their content.
85
89
  */
90
+ /**
91
+ * `source.slice(start, end).trimEnd()`: a key, or a line's content, keeps the whitespace it starts with, which makes it
92
+ * no key (Core 1.1 §7.1: other whitespace before content is no indentation).
93
+ */
94
+ const trimEndSlice = (source, start, end) => {
95
+ while (end > start && isTrimWhitespace(source.charCodeAt(end - 1)))
96
+ end--;
97
+ return source.slice(start, end);
98
+ };
86
99
  /** `source.slice(start, end).trim()` with no temporary untrimmed substring. */
87
100
  const trimSlice = (source, start, end) => {
88
101
  if (start < end) {
@@ -115,6 +128,30 @@ const skipEmptyAndCommentLines = (cursor) => {
115
128
  while (cursor.valid && (cursor.empty || cursor.firstCode === 35))
116
129
  cursor.next();
117
130
  };
131
+ /**
132
+ * §7.2: a sequence item is a line of `-` alone or `-` followed by whitespace; `---` or `-x` is none (Core 1.1). The
133
+ * cursor is on a line with content.
134
+ */
135
+ const isSequenceItem = (cursor) => {
136
+ if (cursor.firstCode !== 45)
137
+ return false;
138
+ const next = cursor.contentStart + 1;
139
+ return next === cursor.lineEnd || isTrimWhitespace(cursor.source.charCodeAt(next));
140
+ };
141
+ /**
142
+ * Core 1.1 §7.2: the block of a bare key at `keyColumn` — deeper than the key (§7.1 rule 3), or a sequence that
143
+ * starts at the key's own column, which `compact` parsing ends at that column's first line that is no item. Null
144
+ * without either. The cursor is on the first content line after the key.
145
+ */
146
+ const blockOfBareKey = (cursor, keyColumn, ctx, baseLine, builder) => {
147
+ if (!cursor.valid)
148
+ return null;
149
+ if (cursor.indent > keyColumn)
150
+ return parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
151
+ if (cursor.indent === keyColumn && isSequenceItem(cursor))
152
+ return parseCursorBlock(cursor, keyColumn, ctx, baseLine, builder, true);
153
+ return null;
154
+ };
118
155
  const cursorAfterDash = (cursor) => {
119
156
  const start = cursor.contentStart;
120
157
  const end = cursor.lineEnd;
@@ -127,12 +164,19 @@ const cursorAfterDash = (cursor) => {
127
164
  content++;
128
165
  return cursor.source.slice(content, end);
129
166
  };
167
+ /** Core 1.1 §10.1: a line in a block whose key the key grammar (§5.1) rejects, in strict mode. */
168
+ const unrecognizedKeyLine = (line, trimmed) => new LimaError({
169
+ code: 'UNRECOGNIZED_LINE', line,
170
+ message: `Lima: unrecognised line at line ${line}: "${trimmed}"${specificKeyHint(trimmed) ?? KEY_GRAMMAR_HINT}`,
171
+ });
130
172
  /** Shared block grammar consuming one mutable physical-line cursor. */
131
- const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
173
+ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
132
174
  const wantSource = builder.positions === true;
133
175
  let items = null;
134
176
  let entries = null;
135
177
  let pendingItem = null;
178
+ // the column of the pending object item's keys: its first key's, after `- ` (§7.2)
179
+ let pendingColumn = 0;
136
180
  const startLine = cursor.lineIndex;
137
181
  while (cursor.valid) {
138
182
  const line = baseLine + cursor.lineIndex;
@@ -145,14 +189,30 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
145
189
  break;
146
190
  if (indent > baseIndent) {
147
191
  const trimmed = cursorContent(cursor);
192
+ // §7.2: an object item's keys stand at its first key's column. A line less indented ends the item (§7.1
193
+ // rule 4); a deeper one, which no nested block took, is inconsistently indented (rule 5) and the item goes on
194
+ if (pendingItem !== null && indent < pendingColumn) {
195
+ items.push(builder.mapping(pendingItem, line));
196
+ pendingItem = null;
197
+ }
198
+ if (pendingItem !== null && indent > pendingColumn) {
199
+ if (ctx.strict)
200
+ throw new LimaError({ code: 'INVALID_INDENTATION', line,
201
+ message: `Lima: unexpected indentation at line ${line}: "${trimmed}"` });
202
+ cursor.next();
203
+ continue;
204
+ }
148
205
  if (items !== null && pendingItem !== null) {
149
206
  const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
150
207
  const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
151
208
  const keyRaw = colonPos !== -1
152
- ? trimSlice(trimmed, 0, colonPos)
153
- : bareKey ? trimSlice(trimmed, 0, trimmed.length - 1) : '';
209
+ ? trimEndSlice(trimmed, 0, colonPos)
210
+ : bareKey ? trimEndSlice(trimmed, 0, trimmed.length - 1) : '';
154
211
  if (keyRaw !== '' && !isValidKey(keyRaw)) {
155
- // §5.1: not a usable key — skipped in both modes, as at the top level.
212
+ // §5.1: not a usable key — an unrecognised line, skipped in non-strict mode and an error in strict
213
+ // mode, as at the top level (Core 1.1 §10.1)
214
+ if (ctx.strict)
215
+ throw unrecognizedKeyLine(line, trimmed);
156
216
  cursor.next();
157
217
  }
158
218
  else if (colonPos !== -1) {
@@ -163,19 +223,15 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
163
223
  if (raw !== '|' && raw.includes('#'))
164
224
  raw = stripComment(raw);
165
225
  const valueCol = wantSource ? cursor.indent + colonPos + 2 + leadingWsLen(trimmed.slice(colonPos + 2)) : 0;
166
- builder.setMapping(pendingItem, key, inlineOrBlockScalar(raw, indent, line, valueCol, rawUndecoded, cursor, ctx, builder));
226
+ builder.setMapping(pendingItem, key, inlineOrBlockScalar(raw, rawUndecoded === '|', indent, line, valueCol, rawUndecoded, cursor, ctx, builder));
167
227
  }
168
228
  else if (bareKey) {
169
229
  const key = stripKeyQuotes(keyRaw, ctx.strict, line);
170
230
  checkKeyLength(key, line);
171
231
  cursor.next();
172
232
  skipEmptyAndCommentLines(cursor);
173
- if (cursor.valid && cursor.indent > indent) {
174
- const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
175
- builder.setMapping(pendingItem, key, nested ?? builder.null(line));
176
- }
177
- else
178
- builder.setMapping(pendingItem, key, builder.null(line));
233
+ const nested = blockOfBareKey(cursor, indent, ctx, baseLine, builder);
234
+ builder.setMapping(pendingItem, key, nested ?? builder.null(line));
179
235
  }
180
236
  else {
181
237
  if (ctx.strict)
@@ -192,13 +248,16 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
192
248
  }
193
249
  continue;
194
250
  }
195
- if (cursor.firstCode === 45) {
251
+ // a sequence at its key's column ends at that column's first line that is no item (Core 1.1 §7.2)
252
+ if (compact && !isSequenceItem(cursor))
253
+ break;
254
+ if (isSequenceItem(cursor)) {
196
255
  if (pendingItem !== null) {
197
256
  items.push(builder.mapping(pendingItem, line));
198
257
  pendingItem = null;
199
258
  }
200
- if (items === null)
201
- items = [];
259
+ // a block is a mapping or a sequence, as its first line decides; in non-strict mode a line of the other kind
260
+ // is skipped, and what the block holds stays (Core 1.1 §7.1)
202
261
  if (entries !== null) {
203
262
  if (ctx.strict)
204
263
  throw new LimaError({ code: 'INVALID_INDENTATION', line,
@@ -206,6 +265,8 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
206
265
  cursor.next();
207
266
  continue;
208
267
  }
268
+ if (items === null)
269
+ items = [];
209
270
  const afterDashRaw = cursorAfterDash(cursor);
210
271
  let afterDash = afterDashRaw;
211
272
  if (afterDash.includes('#'))
@@ -216,19 +277,33 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
216
277
  raw: physicalRaw(afterDashRaw), line, col: afterDashCol, tabAdjust: afterDashTab,
217
278
  }) : undefined;
218
279
  const first = afterDash.charCodeAt(0);
219
- if (first !== 34 && first !== 39 && first !== 45 && first !== 123 &&
280
+ // an item's value is an inline value (§15.3): a flow mapping, or what an unclosed one falls back to (§7.5); a
281
+ // flow sequence there is a sequence in a sequence (§7.2)
282
+ if (first === 123) {
283
+ items.push(parseFlowOrScalarValue(afterDash, ctx, line, builder, dashSource));
284
+ cursor.next();
285
+ continue;
286
+ }
287
+ if (first === 91) {
288
+ const sequence = parseFlowSequence(afterDash, ctx, line, builder, dashSource);
289
+ if (sequence === null)
290
+ items.push(parseFlowOrScalarValue(afterDash, ctx, line, builder, dashSource));
291
+ else if (ctx.strict)
292
+ throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line,
293
+ message: `Lima: nested sequence at line ${line}: "${cursorContent(cursor)}"` });
294
+ else
295
+ items.push(builder.null(line));
296
+ cursor.next();
297
+ continue;
298
+ }
299
+ if (first !== 34 && first !== 39 && first !== 45 &&
220
300
  afterDash.indexOf(': ') === -1 && !afterDash.endsWith(':')) {
221
301
  items.push(parseQuotedOrTyped(afterDash, ctx, line, builder, dashSource));
222
302
  cursor.next();
223
303
  continue;
224
304
  }
225
- const flowMap = parseFlowMapping(afterDash, ctx, line, builder, dashSource);
226
305
  const colonPos = findKeySep(afterDash);
227
- if (flowMap !== null) {
228
- items.push(flowMap);
229
- cursor.next();
230
- }
231
- else if (afterDash === '-' || DASH_PREFIX_RE.test(afterDash)) {
306
+ if (afterDash === '-' || DASH_PREFIX_RE.test(afterDash)) {
232
307
  if (ctx.strict)
233
308
  throw new LimaError({ code: 'INVALID_INDENTATION', line,
234
309
  message: `Lima: nested block sequence at line ${line}: "${cursorContent(cursor)}"` });
@@ -255,18 +330,20 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
255
330
  ? afterDash.slice(valueStart) : trimSlice(afterDash, valueStart, afterDash.length);
256
331
  // The key sits after `- `, two columns past the dash.
257
332
  const valueCol = wantSource ? afterDashCol + valueStart + leadingWsLen(afterDash.slice(valueStart)) : 0;
258
- pendingItem = builder.createMappingWith(key, inlineOrBlockScalar(raw, baseIndent + 2, line, valueCol, wantSource ? afterDashRaw.slice(valueStart) : raw, cursor, ctx, builder));
259
- while (cursor.valid && cursor.indent > baseIndent) {
333
+ pendingColumn = cursor.indent + dashPrefixLen(cursor);
334
+ pendingItem = builder.createMappingWith(key, inlineOrBlockScalar(raw, trimSlice(afterDashRaw, valueStart, afterDashRaw.length) === '|', pendingColumn, line, valueCol, wantSource ? afterDashRaw.slice(valueStart) : raw, cursor, ctx, builder));
335
+ // the item's later keys at its key column; any other line is the block loop's (§7.2)
336
+ while (cursor.valid && cursor.indent === pendingColumn) {
260
337
  const continuationLine = baseLine + cursor.lineIndex;
261
338
  const ckeyIndent = cursor.asciiIndent;
262
339
  const cfirst = cursor.source.charCodeAt(cursor.contentStart);
263
340
  if (cfirst === 35)
264
341
  break;
265
- const contLine = trimSlice(cursor.source, cursor.contentStart, cursor.lineEnd);
342
+ const contLine = trimEndSlice(cursor.source, cursor.contentStart, cursor.lineEnd);
266
343
  const csep = findKeySep(contLine);
267
344
  if (csep === -1)
268
345
  break;
269
- const ckeyRaw = trimSlice(contLine, 0, csep);
346
+ const ckeyRaw = trimEndSlice(contLine, 0, csep);
270
347
  if (!isValidKey(ckeyRaw))
271
348
  break;
272
349
  const ckey = stripKeyQuotes(ckeyRaw, ctx.strict, continuationLine);
@@ -280,7 +357,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
280
357
  // contLine starts exactly at contentStart (no leading trim), so
281
358
  // the value's column is the indent plus its offset in contLine.
282
359
  const cValueCol = wantSource ? cursor.indent + csep + 2 + leadingWsLen(contLine.slice(csep + 2)) : 0;
283
- builder.setMapping(pendingItem, ckey, inlineOrBlockScalar(value, ckeyIndent, continuationLine, cValueCol, valueUndecoded, cursor, ctx, builder));
360
+ builder.setMapping(pendingItem, ckey, inlineOrBlockScalar(value, valueUndecoded === '|', ckeyIndent, continuationLine, cValueCol, valueUndecoded, cursor, ctx, builder));
284
361
  }
285
362
  }
286
363
  else if (afterDash.endsWith(':') && isValidKey(trimSlice(afterDash, 0, afterDash.length - 1))) {
@@ -297,12 +374,9 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
297
374
  keyColumn -= cursor.lineStart;
298
375
  cursor.next();
299
376
  skipEmptyAndCommentLines(cursor);
300
- if (cursor.valid && cursor.indent > keyColumn) {
301
- const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
302
- pendingItem = builder.createMappingWith(key, nested ?? builder.null(line));
303
- }
304
- else
305
- pendingItem = builder.createMappingWith(key, builder.null(line));
377
+ const nested = blockOfBareKey(cursor, keyColumn, ctx, baseLine, builder);
378
+ pendingColumn = keyColumn;
379
+ pendingItem = builder.createMappingWith(key, nested ?? builder.null(line));
306
380
  }
307
381
  else {
308
382
  // A quoted-or-typed scalar item — parseQuotedOrTyped enforces
@@ -323,11 +397,13 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
323
397
  const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
324
398
  const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
325
399
  const keyRaw = colonPos !== -1
326
- ? trimSlice(trimmed, 0, colonPos)
327
- : bareKey ? trimSlice(trimmed, 0, trimmed.length - 1) : '';
400
+ ? trimEndSlice(trimmed, 0, colonPos)
401
+ : bareKey ? trimEndSlice(trimmed, 0, trimmed.length - 1) : '';
328
402
  if (keyRaw !== '' && !isValidKey(keyRaw)) {
329
- // §5.1: not a usable key — unrecognised line, skipped in both
330
- // modes (§10's strict list is closed and does not cover this).
403
+ // §5.1: not a usable key — an unrecognised line, skipped in non-strict mode and an error in strict
404
+ // mode, as at the top level (Core 1.1 §10.1)
405
+ if (ctx.strict)
406
+ throw unrecognizedKeyLine(line, trimmed);
331
407
  cursor.next();
332
408
  }
333
409
  else if (colonPos !== -1) {
@@ -342,7 +418,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
342
418
  if (raw !== '|' && raw.includes('#'))
343
419
  raw = stripComment(raw);
344
420
  const valueCol = wantSource ? cursor.indent + colonPos + 2 + leadingWsLen(trimmed.slice(colonPos + 2)) : 0;
345
- builder.setMapping(entries, key, inlineOrBlockScalar(raw, baseIndent, line, valueCol, rawUndecoded, cursor, ctx, builder));
421
+ builder.setMapping(entries, key, inlineOrBlockScalar(raw, rawUndecoded === '|', baseIndent, line, valueCol, rawUndecoded, cursor, ctx, builder));
346
422
  }
347
423
  else if (bareKey) {
348
424
  if (entries === null)
@@ -353,12 +429,8 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
353
429
  checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
354
430
  cursor.next();
355
431
  skipEmptyAndCommentLines(cursor);
356
- if (cursor.valid && cursor.indent > baseIndent) {
357
- const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
358
- builder.setMapping(entries, key, nested ?? builder.null(line));
359
- }
360
- else
361
- builder.setMapping(entries, key, builder.null(line));
432
+ const nested = blockOfBareKey(cursor, baseIndent, ctx, baseLine, builder);
433
+ builder.setMapping(entries, key, nested ?? builder.null(line));
362
434
  }
363
435
  else {
364
436
  if (ctx.strict)
@@ -381,7 +453,10 @@ export const parseBlockRange = (source, start, end, ctx, baseLine, builder, dept
381
453
  if (!cursor.valid)
382
454
  return null;
383
455
  const baseIndent = cursor.asciiIndent;
384
- const value = parseCursorBlock(cursor, baseIndent, ctx, baseLine, builder);
456
+ // Core 1.1 §7.2: at the top level's own column 0 only a sequence is the key's block; any other first line is not
457
+ if (baseIndent === 0 && !isSequenceItem(cursor))
458
+ return null;
459
+ const value = parseCursorBlock(cursor, baseIndent, ctx, baseLine, builder, baseIndent === 0);
385
460
  // Recursive block containers require strictly increasing integer
386
461
  // indentation. At one indentation level a sequence-item mapping can add
387
462
  // one container and Core flow syntax at most two more, so delta + 4 is a
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.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
@@ -34,7 +34,7 @@ export type CoreOptions = {
34
34
  onWarning?: (diagnostic: Diagnostic) => void;
35
35
  };
36
36
  /**
37
- * Parses Lima Core 1.0 syntax into the internal annotated value tree —
37
+ * Parses Lima Core 1.1 syntax into the internal annotated value tree —
38
38
  * every node carrying its source line, string leaves additionally carrying
39
39
  * whether they came from quoted syntax. `($key)`/`(%key)` text is left
40
40
  * exactly as written; nothing here ever inspects or resolves it. The
@@ -75,10 +75,11 @@ export declare const toNative: (v: LimaValue) => NativeValue;
75
75
  */
76
76
  export declare const toNativeFromPositioned: (v: PositionedValue) => NativeValue;
77
77
  /**
78
- * Public Core 1.0 entry point. Never resolves `($key)`/`(%key)` text — see
78
+ * Public Core 1.1 entry point. Never resolves `($key)`/`(%key)` text — see
79
79
  * the module doc comment. Equivalent in observable behavior to calling
80
80
  * `parseReferences()` with no partials on a document that happens to
81
81
  * contain no references, except that here reference-shaped text is
82
82
  * guaranteed to always pass through unresolved, even in strict mode.
83
83
  */
84
+ export declare const parseCoreValidated: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: CoreOptions) => T;
84
85
  export declare const parseCore: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: CoreOptions) => T;