@limaformat/lima 0.5.1 → 0.6.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,13 +4,78 @@ 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.1](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.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)
11
+ carry their own.
12
12
 
13
- ## [Unreleased]
13
+ ## [0.6.0] — 2026-10-28
14
+
15
+ Lima Core 1.1: strict mode rejects a content line that belongs to no
16
+ structure, which Core 1.0 skipped in both modes. Core 1.1 also settles
17
+ cases Core 1.0 left open, where the three implementations differed; some
18
+ non-strict results change with them (below).
19
+
20
+ ### Changed
21
+
22
+ - Strict mode throws `INVALID_INDENTATION` for a line indented after a
23
+ top-level key with an inline value — the lines after a YAML `>`, `|-` or
24
+ `|+`, a key indented by a tab, freetext after a value — as it already did
25
+ one level down; the message names the YAML marker and what Lima writes
26
+ instead.
27
+ - Strict mode throws the new `UNRECOGNIZED_LINE` for an unindented line that
28
+ matches no key pattern: a key without its `: `, a dotted or otherwise
29
+ invalid key, a YAML `---` / `...` marker, a quoted key broken by a newline.
30
+ - A line of a block mapping whose key the key grammar rejects (`a.b: value`,
31
+ `bad key: value`), skipped in strict mode until now, throws `UNRECOGNIZED_LINE`
32
+ too; an indented line below a block mapping's base indentation, or after a
33
+ `|` scalar a column-0 comment ended, throws `INVALID_INDENTATION`.
34
+ - `LimaDiagnosticCode` gains `'UNRECOGNIZED_LINE'`.
35
+ - A bare key's sequence may start at the key's own column, as YAML writes
36
+ it (`tags:` / `- a` / `- b`), at any level (Core 1.1 §7.2). At the top
37
+ level this was read so by accident; one level down the sequence and the
38
+ keys after it were lost.
39
+ - An item whose value is a flow sequence (`- [a, b]`, `- []`) is a nested
40
+ sequence (Core 1.1 §7.2): `null` in non-strict mode, `INVALID_FLOW_SYNTAX`
41
+ in strict mode.
42
+ - Indentation is spaces only (Core 1.1 §7.1): a line whose spaces are followed
43
+ by other whitespace, such as U+00A0, before its content is unrecognised —
44
+ skipped in non-strict mode, an error in strict mode. Whitespace is defined
45
+ once (§1), as the set every check already used.
46
+ TypeScript counted such whitespace as indentation.
47
+ - An object item's later keys stand at its first key's column (§7.2): a line
48
+ of the item deeper than that is skipped in non-strict mode, and one less
49
+ indented ends the item; strict mode throws `INVALID_INDENTATION` for
50
+ either. Any deeper line was taken as a key of the item.
51
+
52
+ ### Fixed
53
+
54
+ - A `|` scalar as an object item's first key measured its body from the dash
55
+ plus two columns, so after wider whitespace (`- a: |`) it took the item's
56
+ next key as text; it measures from the key's column (§6.1.5, §7.2).
57
+ - In strict mode, a line deeper than a nested `|` scalar's key, after a line
58
+ of other whitespace that ended the scalar, was dropped without an error; it
59
+ throws `INVALID_INDENTATION`.
60
+ - A top-level `key:` at the very end of the input, without a line
61
+ terminator, was no key at all and vanished; it is `null`, as at any nested
62
+ level and in the Rust and Go implementations, and as Core 1.1 §4 now says.
63
+ This changes a non-strict result too.
64
+ - A `|` with no content lines (`a: |` at the end of a block, or of the
65
+ input) was the string `"|"`; it is `""` (Core 1.1 §6.1.5).
66
+ - A block of mapping entries with a sequence item in it, or the reverse, was
67
+ an empty sequence in non-strict mode, all its entries lost; the first kind
68
+ wins and a line of the other kind is skipped (Core 1.1 §7.1).
69
+ - An item value that begins with `[` but is no flow sequence (`- [x] done`),
70
+ or with `{` and does not close (`- {a: 1`), throws `INVALID_FLOW_SYNTAX`
71
+ in strict mode, as an inline value does.
72
+ - `key: | # c` below the top level, in a block mapping or an object item,
73
+ was a block scalar; it is the string `"|"`, as at the top level — the
74
+ marker stands alone (§6.1.5).
75
+
76
+ The shared conformance corpus is at 336 Core cases: the 218 Core 1.0 cases
77
+ without the 8 that Core 1.1 supersedes, plus 126 Core 1.1 cases
78
+ (`corpus/core-1.1/`).
14
79
 
15
80
  ## [0.5.1] — 2026-10-28
16
81
 
package/README.md CHANGED
@@ -57,7 +57,7 @@ public API. The normative rules are defined in the
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)
60
+ The [Lima Core 1.1](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
61
61
  and [Lima References 2.0](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
@@ -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,7 @@
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 { parseFlowOrScalarValue, parseFlowSequence } from './flow.js';
14
14
  import { buildBlockScalar } from './block-scalar.js';
15
15
  import { LimaError } from './errors.js';
16
16
  import { isTrimWhitespace } from './chars.js';
@@ -48,9 +48,10 @@ const inlineSource = (wantSource, ctx, keyLine, valueCol, rawUndecoded) => wantS
48
48
  raw: physicalRaw(rawUndecoded), line: keyLine, col: valueCol,
49
49
  tabAdjust: ctx.tabAdjust ? [ctx.tabAdjust[keyLine - 1] ?? 0] : undefined,
50
50
  }) : undefined;
51
- const inlineOrBlockScalar = (raw, keyIndent, keyLine, valueCol, rawUndecoded, cursor, ctx, builder) => {
51
+ /** An inline value, or a `|` block scalar when `marker`: the value is `|` alone (§6.1.5; `| # c` is the string `|`). */
52
+ const inlineOrBlockScalar = (raw, marker, keyIndent, keyLine, valueCol, rawUndecoded, cursor, ctx, builder) => {
52
53
  const wantSource = builder.positions === true;
53
- if (raw !== '|') {
54
+ if (!marker) {
54
55
  const value = parseFlowOrScalarValue(raw, ctx, keyLine, builder, inlineSource(wantSource, ctx, keyLine, valueCol, rawUndecoded));
55
56
  cursor.next();
56
57
  return value;
@@ -58,7 +59,9 @@ const inlineOrBlockScalar = (raw, keyIndent, keyLine, valueCol, rawUndecoded, cu
58
59
  cursor.next(); // past the `key: |` line
59
60
  const bodyLines = [];
60
61
  while (cursor.valid) {
61
- if (!cursor.empty && cursor.asciiIndent <= keyIndent)
62
+ // the scalar's own extent (§6.1.5, `buildBlockScalar`): a line of spaces only is empty, any other ends it at the
63
+ // key's column or less — a line of other whitespace too, which is content here
64
+ if (cursor.lineEnd - cursor.lineStart > cursor.asciiIndent && cursor.asciiIndent <= keyIndent)
62
65
  break;
63
66
  bodyLines.push(cursor.source.slice(cursor.lineStart, cursor.lineEnd));
64
67
  cursor.next();
@@ -83,6 +86,15 @@ export const findKeySep = (s) => {
83
86
  * the original source. Strings are materialized only when grammar or scalar
84
87
  * parsing needs their content.
85
88
  */
89
+ /**
90
+ * `source.slice(start, end).trimEnd()`: a key, or a line's content, keeps the whitespace it starts with, which makes it
91
+ * no key (Core 1.1 §7.1: other whitespace before content is no indentation).
92
+ */
93
+ const trimEndSlice = (source, start, end) => {
94
+ while (end > start && isTrimWhitespace(source.charCodeAt(end - 1)))
95
+ end--;
96
+ return source.slice(start, end);
97
+ };
86
98
  /** `source.slice(start, end).trim()` with no temporary untrimmed substring. */
87
99
  const trimSlice = (source, start, end) => {
88
100
  if (start < end) {
@@ -115,6 +127,30 @@ const skipEmptyAndCommentLines = (cursor) => {
115
127
  while (cursor.valid && (cursor.empty || cursor.firstCode === 35))
116
128
  cursor.next();
117
129
  };
130
+ /**
131
+ * §7.2: a sequence item is a line of `-` alone or `-` followed by whitespace; `---` or `-x` is none (Core 1.1). The
132
+ * cursor is on a line with content.
133
+ */
134
+ const isSequenceItem = (cursor) => {
135
+ if (cursor.firstCode !== 45)
136
+ return false;
137
+ const next = cursor.contentStart + 1;
138
+ return next === cursor.lineEnd || isTrimWhitespace(cursor.source.charCodeAt(next));
139
+ };
140
+ /**
141
+ * 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
142
+ * starts at the key's own column, which `compact` parsing ends at that column's first line that is no item. Null
143
+ * without either. The cursor is on the first content line after the key.
144
+ */
145
+ const blockOfBareKey = (cursor, keyColumn, ctx, baseLine, builder) => {
146
+ if (!cursor.valid)
147
+ return null;
148
+ if (cursor.indent > keyColumn)
149
+ return parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
150
+ if (cursor.indent === keyColumn && isSequenceItem(cursor))
151
+ return parseCursorBlock(cursor, keyColumn, ctx, baseLine, builder, true);
152
+ return null;
153
+ };
118
154
  const cursorAfterDash = (cursor) => {
119
155
  const start = cursor.contentStart;
120
156
  const end = cursor.lineEnd;
@@ -127,12 +163,19 @@ const cursorAfterDash = (cursor) => {
127
163
  content++;
128
164
  return cursor.source.slice(content, end);
129
165
  };
166
+ /** Core 1.1 §10.1: a line in a block whose key the key grammar (§5.1) rejects, in strict mode. */
167
+ const unrecognizedKeyLine = (line, trimmed) => new LimaError({
168
+ code: 'UNRECOGNIZED_LINE', line,
169
+ message: `Lima: unrecognised line at line ${line}: "${trimmed}" (an unquoted key has letters, digits, "_", "-" and ":" only; quote any other)`,
170
+ });
130
171
  /** Shared block grammar consuming one mutable physical-line cursor. */
131
- const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
172
+ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
132
173
  const wantSource = builder.positions === true;
133
174
  let items = null;
134
175
  let entries = null;
135
176
  let pendingItem = null;
177
+ // the column of the pending object item's keys: its first key's, after `- ` (§7.2)
178
+ let pendingColumn = 0;
136
179
  const startLine = cursor.lineIndex;
137
180
  while (cursor.valid) {
138
181
  const line = baseLine + cursor.lineIndex;
@@ -145,14 +188,30 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
145
188
  break;
146
189
  if (indent > baseIndent) {
147
190
  const trimmed = cursorContent(cursor);
191
+ // §7.2: an object item's keys stand at its first key's column. A line less indented ends the item (§7.1
192
+ // rule 4); a deeper one, which no nested block took, is inconsistently indented (rule 5) and the item goes on
193
+ if (pendingItem !== null && indent < pendingColumn) {
194
+ items.push(builder.mapping(pendingItem, line));
195
+ pendingItem = null;
196
+ }
197
+ if (pendingItem !== null && indent > pendingColumn) {
198
+ if (ctx.strict)
199
+ throw new LimaError({ code: 'INVALID_INDENTATION', line,
200
+ message: `Lima: unexpected indentation at line ${line}: "${trimmed}"` });
201
+ cursor.next();
202
+ continue;
203
+ }
148
204
  if (items !== null && pendingItem !== null) {
149
205
  const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
150
206
  const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
151
207
  const keyRaw = colonPos !== -1
152
- ? trimSlice(trimmed, 0, colonPos)
153
- : bareKey ? trimSlice(trimmed, 0, trimmed.length - 1) : '';
208
+ ? trimEndSlice(trimmed, 0, colonPos)
209
+ : bareKey ? trimEndSlice(trimmed, 0, trimmed.length - 1) : '';
154
210
  if (keyRaw !== '' && !isValidKey(keyRaw)) {
155
- // §5.1: not a usable key — skipped in both modes, as at the top level.
211
+ // §5.1: not a usable key — an unrecognised line, skipped in non-strict mode and an error in strict
212
+ // mode, as at the top level (Core 1.1 §10.1)
213
+ if (ctx.strict)
214
+ throw unrecognizedKeyLine(line, trimmed);
156
215
  cursor.next();
157
216
  }
158
217
  else if (colonPos !== -1) {
@@ -163,19 +222,15 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
163
222
  if (raw !== '|' && raw.includes('#'))
164
223
  raw = stripComment(raw);
165
224
  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));
225
+ builder.setMapping(pendingItem, key, inlineOrBlockScalar(raw, rawUndecoded === '|', indent, line, valueCol, rawUndecoded, cursor, ctx, builder));
167
226
  }
168
227
  else if (bareKey) {
169
228
  const key = stripKeyQuotes(keyRaw, ctx.strict, line);
170
229
  checkKeyLength(key, line);
171
230
  cursor.next();
172
231
  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));
232
+ const nested = blockOfBareKey(cursor, indent, ctx, baseLine, builder);
233
+ builder.setMapping(pendingItem, key, nested ?? builder.null(line));
179
234
  }
180
235
  else {
181
236
  if (ctx.strict)
@@ -192,13 +247,16 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
192
247
  }
193
248
  continue;
194
249
  }
195
- if (cursor.firstCode === 45) {
250
+ // a sequence at its key's column ends at that column's first line that is no item (Core 1.1 §7.2)
251
+ if (compact && !isSequenceItem(cursor))
252
+ break;
253
+ if (isSequenceItem(cursor)) {
196
254
  if (pendingItem !== null) {
197
255
  items.push(builder.mapping(pendingItem, line));
198
256
  pendingItem = null;
199
257
  }
200
- if (items === null)
201
- items = [];
258
+ // a block is a mapping or a sequence, as its first line decides; in non-strict mode a line of the other kind
259
+ // is skipped, and what the block holds stays (Core 1.1 §7.1)
202
260
  if (entries !== null) {
203
261
  if (ctx.strict)
204
262
  throw new LimaError({ code: 'INVALID_INDENTATION', line,
@@ -206,6 +264,8 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
206
264
  cursor.next();
207
265
  continue;
208
266
  }
267
+ if (items === null)
268
+ items = [];
209
269
  const afterDashRaw = cursorAfterDash(cursor);
210
270
  let afterDash = afterDashRaw;
211
271
  if (afterDash.includes('#'))
@@ -216,19 +276,33 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
216
276
  raw: physicalRaw(afterDashRaw), line, col: afterDashCol, tabAdjust: afterDashTab,
217
277
  }) : undefined;
218
278
  const first = afterDash.charCodeAt(0);
219
- if (first !== 34 && first !== 39 && first !== 45 && first !== 123 &&
279
+ // an item's value is an inline value (§15.3): a flow mapping, or what an unclosed one falls back to (§7.5); a
280
+ // flow sequence there is a sequence in a sequence (§7.2)
281
+ if (first === 123) {
282
+ items.push(parseFlowOrScalarValue(afterDash, ctx, line, builder, dashSource));
283
+ cursor.next();
284
+ continue;
285
+ }
286
+ if (first === 91) {
287
+ const sequence = parseFlowSequence(afterDash, ctx, line, builder, dashSource);
288
+ if (sequence === null)
289
+ items.push(parseFlowOrScalarValue(afterDash, ctx, line, builder, dashSource));
290
+ else if (ctx.strict)
291
+ throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line,
292
+ message: `Lima: nested sequence at line ${line}: "${cursorContent(cursor)}"` });
293
+ else
294
+ items.push(builder.null(line));
295
+ cursor.next();
296
+ continue;
297
+ }
298
+ if (first !== 34 && first !== 39 && first !== 45 &&
220
299
  afterDash.indexOf(': ') === -1 && !afterDash.endsWith(':')) {
221
300
  items.push(parseQuotedOrTyped(afterDash, ctx, line, builder, dashSource));
222
301
  cursor.next();
223
302
  continue;
224
303
  }
225
- const flowMap = parseFlowMapping(afterDash, ctx, line, builder, dashSource);
226
304
  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)) {
305
+ if (afterDash === '-' || DASH_PREFIX_RE.test(afterDash)) {
232
306
  if (ctx.strict)
233
307
  throw new LimaError({ code: 'INVALID_INDENTATION', line,
234
308
  message: `Lima: nested block sequence at line ${line}: "${cursorContent(cursor)}"` });
@@ -255,18 +329,20 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
255
329
  ? afterDash.slice(valueStart) : trimSlice(afterDash, valueStart, afterDash.length);
256
330
  // The key sits after `- `, two columns past the dash.
257
331
  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) {
332
+ pendingColumn = cursor.indent + dashPrefixLen(cursor);
333
+ pendingItem = builder.createMappingWith(key, inlineOrBlockScalar(raw, trimSlice(afterDashRaw, valueStart, afterDashRaw.length) === '|', pendingColumn, line, valueCol, wantSource ? afterDashRaw.slice(valueStart) : raw, cursor, ctx, builder));
334
+ // the item's later keys at its key column; any other line is the block loop's (§7.2)
335
+ while (cursor.valid && cursor.indent === pendingColumn) {
260
336
  const continuationLine = baseLine + cursor.lineIndex;
261
337
  const ckeyIndent = cursor.asciiIndent;
262
338
  const cfirst = cursor.source.charCodeAt(cursor.contentStart);
263
339
  if (cfirst === 35)
264
340
  break;
265
- const contLine = trimSlice(cursor.source, cursor.contentStart, cursor.lineEnd);
341
+ const contLine = trimEndSlice(cursor.source, cursor.contentStart, cursor.lineEnd);
266
342
  const csep = findKeySep(contLine);
267
343
  if (csep === -1)
268
344
  break;
269
- const ckeyRaw = trimSlice(contLine, 0, csep);
345
+ const ckeyRaw = trimEndSlice(contLine, 0, csep);
270
346
  if (!isValidKey(ckeyRaw))
271
347
  break;
272
348
  const ckey = stripKeyQuotes(ckeyRaw, ctx.strict, continuationLine);
@@ -280,7 +356,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
280
356
  // contLine starts exactly at contentStart (no leading trim), so
281
357
  // the value's column is the indent plus its offset in contLine.
282
358
  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));
359
+ builder.setMapping(pendingItem, ckey, inlineOrBlockScalar(value, valueUndecoded === '|', ckeyIndent, continuationLine, cValueCol, valueUndecoded, cursor, ctx, builder));
284
360
  }
285
361
  }
286
362
  else if (afterDash.endsWith(':') && isValidKey(trimSlice(afterDash, 0, afterDash.length - 1))) {
@@ -297,12 +373,9 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
297
373
  keyColumn -= cursor.lineStart;
298
374
  cursor.next();
299
375
  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));
376
+ const nested = blockOfBareKey(cursor, keyColumn, ctx, baseLine, builder);
377
+ pendingColumn = keyColumn;
378
+ pendingItem = builder.createMappingWith(key, nested ?? builder.null(line));
306
379
  }
307
380
  else {
308
381
  // A quoted-or-typed scalar item — parseQuotedOrTyped enforces
@@ -323,11 +396,13 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
323
396
  const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
324
397
  const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
325
398
  const keyRaw = colonPos !== -1
326
- ? trimSlice(trimmed, 0, colonPos)
327
- : bareKey ? trimSlice(trimmed, 0, trimmed.length - 1) : '';
399
+ ? trimEndSlice(trimmed, 0, colonPos)
400
+ : bareKey ? trimEndSlice(trimmed, 0, trimmed.length - 1) : '';
328
401
  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).
402
+ // §5.1: not a usable key — an unrecognised line, skipped in non-strict mode and an error in strict
403
+ // mode, as at the top level (Core 1.1 §10.1)
404
+ if (ctx.strict)
405
+ throw unrecognizedKeyLine(line, trimmed);
331
406
  cursor.next();
332
407
  }
333
408
  else if (colonPos !== -1) {
@@ -342,7 +417,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
342
417
  if (raw !== '|' && raw.includes('#'))
343
418
  raw = stripComment(raw);
344
419
  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));
420
+ builder.setMapping(entries, key, inlineOrBlockScalar(raw, rawUndecoded === '|', baseIndent, line, valueCol, rawUndecoded, cursor, ctx, builder));
346
421
  }
347
422
  else if (bareKey) {
348
423
  if (entries === null)
@@ -353,12 +428,8 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
353
428
  checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
354
429
  cursor.next();
355
430
  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));
431
+ const nested = blockOfBareKey(cursor, baseIndent, ctx, baseLine, builder);
432
+ builder.setMapping(entries, key, nested ?? builder.null(line));
362
433
  }
363
434
  else {
364
435
  if (ctx.strict)
@@ -381,7 +452,10 @@ export const parseBlockRange = (source, start, end, ctx, baseLine, builder, dept
381
452
  if (!cursor.valid)
382
453
  return null;
383
454
  const baseIndent = cursor.asciiIndent;
384
- const value = parseCursorBlock(cursor, baseIndent, ctx, baseLine, builder);
455
+ // 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
456
+ if (baseIndent === 0 && !isSequenceItem(cursor))
457
+ return null;
458
+ const value = parseCursorBlock(cursor, baseIndent, ctx, baseLine, builder, baseIndent === 0);
385
459
  // Recursive block containers require strictly increasing integer
386
460
  // indentation. At one indentation level a sequence-item mapping can add
387
461
  // 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,7 +75,7 @@ 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
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
@@ -26,6 +26,7 @@ import { parseBlockRange } from './block.js';
26
26
  import { buildBlockScalar } from './block-scalar.js';
27
27
  import { LimaError } from './errors.js';
28
28
  import { KeyCursor, scanKeys } from './scanner.js';
29
+ import { checkRangeLines, isBlockScalarMarker } from './structure.js';
29
30
  export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
30
31
  export { toPlainValue } from './scalars.js';
31
32
  /** Every Lima mapping result must be a prototype-free object (Core §11.1). */
@@ -77,7 +78,7 @@ const lineAt = (s, pos) => {
77
78
  return n;
78
79
  };
79
80
  /**
80
- * Parses Lima Core 1.0 syntax, generic over the output representation
81
+ * Parses Lima Core 1.1 syntax, generic over the output representation
81
82
  * (`builder` — see `builder.ts`): References needs the annotated
82
83
  * `PositionedValue` tree (`parseCoreWithPositions` below fixes `V` to
83
84
  * that), while `parseCore` fixes `V` to the public native shape directly,
@@ -186,6 +187,18 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
186
187
  }
187
188
  const firstNewline = inlineEnd ?? frontMatter.indexOf('\n', rawStart);
188
189
  const newlineInRange = firstNewline < nextStart;
190
+ // §6.1.5: `|` alone is a literal block scalar marker, with a body or without one (then its value is empty).
191
+ // The introducing key is at column 0; `buildBlockScalar` finds the scalar's own extent within the range (which
192
+ // runs to the next top-level key), so a dedented comment or dedented freetext before that key ends the scalar
193
+ // rather than being absorbed into it.
194
+ if (!isBlock && frontMatter.charCodeAt(rawStart) === 124) {
195
+ const markerEnd = newlineInRange && firstNewline >= 0 ? firstNewline : nextStart;
196
+ if (frontMatter.slice(rawStart, markerEnd).trim() === '|') {
197
+ const bodyLines = markerEnd < nextStart ? frontMatter.slice(markerEnd + 1, nextStart).split('\n') : [];
198
+ builder.setMapping(root, key, buildBlockScalar(bodyLines, 0, line, ctx, builder).value);
199
+ return;
200
+ }
201
+ }
189
202
  // Physical codepoint column of the value's first character in its
190
203
  // source line — the anchor for References 2.0 token positions (§2.4).
191
204
  // References 2.0 token positions (§2.4): only build the source anchor
@@ -232,31 +245,18 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
232
245
  lineStart = j + 1;
233
246
  }
234
247
  }
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);
248
+ if (lines.length === 0) {
249
+ builder.setMapping(root, key, builder.null(line));
250
+ return;
259
251
  }
252
+ const line0 = lines[0];
253
+ const hasHash = line0.includes('#');
254
+ const val = hasHash ? stripComment(line0) : line0;
255
+ const source = wantSource ? {
256
+ raw: hasHash ? stripCommentKeepEscapes(line0) : line0,
257
+ line, col: valueCol, tabAdjust: lineTabAdjust,
258
+ } : undefined;
259
+ builder.setMapping(root, key, parseFlowOrScalarValue(val, ctx, line, builder, source));
260
260
  };
261
261
  // A recognised top-level entry needs at least one UTF-16 unit of key text,
262
262
  // `:`, and its terminating newline: three units. Consequently 129 entries
@@ -293,6 +293,9 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
293
293
  else {
294
294
  const matches = scanKeys(frontMatter);
295
295
  const keyCount = matches.length;
296
+ // Core 1.1: before the first key no line belongs to any structure (structure.ts)
297
+ if (strict)
298
+ checkRangeLines(frontMatter, 0, keyCount === 0 ? frontMatter.length : matches[0].matchStart, 1, 'none');
296
299
  if (keyCount === 0)
297
300
  return root;
298
301
  if (keyCount > TOP_LEVEL_KEY_LIMIT) {
@@ -305,8 +308,19 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
305
308
  const m = matches[i];
306
309
  const rawDQ = m.doubleQuotedRaw;
307
310
  const key = m.unquoted ?? m.singleQuoted ?? (rawDQ !== undefined ? unescapeDQ(rawDQ, strict, m.line) : undefined);
311
+ const nextStart = i + 1 < keyCount ? matches[i + 1].matchStart : frontMatter.length;
308
312
  if (key !== undefined)
309
- parseEntry(key, m.line, m.rawStart, m.isBlock, m.inlineEnd, i + 1 < keyCount ? matches[i + 1].matchStart : frontMatter.length);
313
+ parseEntry(key, m.line, m.rawStart, m.isBlock, m.inlineEnd, nextStart);
314
+ // Core 1.1: the lines after the key's own line, up to the next key, belong to its block or to nothing
315
+ if (strict) {
316
+ const lineEnd = m.isBlock ? m.rawStart - 1 : (m.inlineEnd ?? frontMatter.indexOf('\n', m.rawStart));
317
+ // no line after the key's own one: nothing to check, the usual case
318
+ if (lineEnd !== -1 && lineEnd + 1 < nextStart) {
319
+ const owner = m.isBlock ? 'mapping'
320
+ : isBlockScalarMarker(frontMatter, m.rawStart, lineEnd) ? 'scalar' : 'inline';
321
+ checkRangeLines(frontMatter, lineEnd + 1, nextStart, m.line + 1, owner, m.rawStart, lineEnd);
322
+ }
323
+ }
310
324
  }
311
325
  }
312
326
  // Core §9 nesting depth, over Core's own reference-inert tree. When
@@ -322,7 +336,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
322
336
  return root;
323
337
  };
324
338
  /**
325
- * Parses Lima Core 1.0 syntax into the internal annotated value tree —
339
+ * Parses Lima Core 1.1 syntax into the internal annotated value tree —
326
340
  * every node carrying its source line, string leaves additionally carrying
327
341
  * whether they came from quoted syntax. `($key)`/`(%key)` text is left
328
342
  * exactly as written; nothing here ever inspects or resolves it. The
@@ -438,7 +452,7 @@ export const toNativeFromPositioned = (v) => {
438
452
  }
439
453
  };
440
454
  /**
441
- * Public Core 1.0 entry point. Never resolves `($key)`/`(%key)` text — see
455
+ * Public Core 1.1 entry point. Never resolves `($key)`/`(%key)` text — see
442
456
  * the module doc comment. Equivalent in observable behavior to calling
443
457
  * `parseReferences()` with no partials on a document that happens to
444
458
  * contain no references, except that here reference-shaped text is
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_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,7 +3,7 @@
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.
6
+ * - `core.ts` — Lima Core 1.1, reference-unaware by construction.
7
7
  * - `references2.ts` — the References 2.0 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.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
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.
6
+ * - `core.ts` — Lima Core 1.1, reference-unaware by construction.
7
7
  * - `references2.ts` — the References 2.0 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.
package/dist/scanner.js CHANGED
@@ -59,6 +59,10 @@ const matchSeparator = (s, pos) => {
59
59
  i++; // consume spaces
60
60
  if (i < s.length && s.charCodeAt(i) === 10)
61
61
  return i + 1;
62
+ // 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
63
+ // is at any nested level (it was no key at all at the top level before)
64
+ if (i === s.length)
65
+ return i;
62
66
  // Backtracking a pure-space run can never reveal a `\n` the maximal
63
67
  // scan didn't already see (every intermediate position is followed by
64
68
  // either another space or the same non-`\n` stop character) — so
@@ -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;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Core 1.1 §4, §7.1 rule 5 and §10.1: in strict mode every content line belongs to the structure. Between two
3
+ * top-level keys (and before the first), a line that is neither blank nor a comment is
4
+ *
5
+ * - unrecognised (`UNRECOGNIZED_LINE`) when it starts at column 0: it matches no key pattern, or the scanner would
6
+ * have ended the range at it;
7
+ * A sequence that starts at column 0 under a bare key is that key's (Core 1.1 §7.2), up to the first line at
8
+ * column 0 that is no item.
9
+ * - inconsistently indented (`INVALID_INDENTATION`) when it is indented and no block owns it: before the first key;
10
+ * after a key with an inline value other than `|`, which opens no block; under a block mapping, but less indented
11
+ * than its first line (its base indentation, §7.1 rule 1), where the block parser stops; or after a `|` scalar
12
+ * that a line at column 0 ended (§6.1.5).
13
+ *
14
+ * The lines a block mapping or a `|` scalar owns are checked where they are parsed. Non-strict mode skips all of
15
+ * these lines, as Core 1.0 did in both modes.
16
+ */
17
+ import { isTrimWhitespace } from './chars.js';
18
+ import { LimaError } from './errors.js';
19
+ /**
20
+ * Checks the lines of `frontMatter` from `start` (a line start) to `end` that follow a key owned as `owner`; `line`
21
+ * is the 1-based line at `start`; `valueStart` to `valueEnd` is the inline value of that key, read only for the hint a
22
+ * YAML marker earns in the error. Throws for the first line that belongs to no structure.
23
+ */
24
+ export function checkRangeLines(frontMatter, start, end, line, owner, valueStart = 0, valueEnd = 0) {
25
+ let pos = start;
26
+ let current = line;
27
+ let base = -1; // a block mapping's base indentation, once its first content line set it
28
+ let scalarOpen = owner === 'scalar';
29
+ while (pos < end) {
30
+ const lineEnd = frontMatter.indexOf('\n', pos);
31
+ const stop = lineEnd === -1 || lineEnd > end ? end : lineEnd;
32
+ // positions, not substrings: a line is read out of `frontMatter` only for an error message
33
+ let spaces = pos;
34
+ while (spaces < stop && frontMatter.charCodeAt(spaces) === 32)
35
+ spaces++;
36
+ const indent = spaces - pos;
37
+ // §6.1.5: a `|` scalar holds every line up to the first non-empty one at its key's column (0), `#` included
38
+ if (scalarOpen && (spaces === stop || indent > 0)) {
39
+ pos = lineEnd + 1;
40
+ current++;
41
+ if (lineEnd === -1 || lineEnd >= end)
42
+ break;
43
+ continue;
44
+ }
45
+ scalarOpen = false;
46
+ // blank and comment lines by Lima's own whitespace; indentation by ASCII space only (§3 expanded the tabs)
47
+ let first = pos;
48
+ while (first < stop && isTrimWhitespace(frontMatter.charCodeAt(first)))
49
+ first++;
50
+ if (first < stop && frontMatter.charCodeAt(first) !== 35) {
51
+ // §7.2: a bare key's sequence may start at its own column, 0 here; it ends at the first line there that is no
52
+ // item
53
+ const item = indent === 0 && isItemAt(frontMatter, pos, stop);
54
+ if (owner === 'mapping' && base === -1 && item)
55
+ base = 0;
56
+ if (indent === 0 && !(base === 0 && item)) {
57
+ const text = frontMatter.slice(pos, stop);
58
+ throw new LimaError({
59
+ code: 'UNRECOGNIZED_LINE', line: current,
60
+ message: `Lima: unrecognised line at line ${current}: "${text}"${hint(text)}`,
61
+ });
62
+ }
63
+ if (owner === 'mapping' && base === -1)
64
+ base = indent;
65
+ if (indent > 0 && !(owner === 'mapping' && indent >= base)) {
66
+ throw new LimaError({
67
+ code: 'INVALID_INDENTATION', line: current,
68
+ message: `Lima: unexpected indentation at line ${current}: "${frontMatter.slice(first, stop)}"${owner === 'inline' ? yamlHint(frontMatter.slice(valueStart, valueEnd).trim()) : ''}`,
69
+ });
70
+ }
71
+ }
72
+ if (lineEnd === -1 || lineEnd >= end)
73
+ break;
74
+ pos = lineEnd + 1;
75
+ current++;
76
+ }
77
+ }
78
+ /** Whether the inline value that starts at `rawStart` and ends its line at `lineEnd` is a `|` block scalar marker. */
79
+ export const isBlockScalarMarker = (frontMatter, rawStart, lineEnd) => {
80
+ let i = rawStart, j = lineEnd;
81
+ while (i < j && isTrimWhitespace(frontMatter.charCodeAt(i)))
82
+ i++;
83
+ while (j > i && isTrimWhitespace(frontMatter.charCodeAt(j - 1)))
84
+ j--;
85
+ return j - i === 1 && frontMatter.charCodeAt(i) === 124;
86
+ };
87
+ /** What a reader likely meant by an unrecognised line, as a hint after the message; none for anything else. */
88
+ const hint = (text) => {
89
+ if (text === '---' || text === '...')
90
+ return ' (a YAML document marker; Lima has one document)';
91
+ if (isItem(text))
92
+ return ' (a sequence item belongs to a key with no value of its own)';
93
+ if (text.startsWith('"') || text.startsWith("'"))
94
+ return ' (a quoted key closes on its own line)';
95
+ // `author Odile`, `key:value`: key characters, but no ": " after them
96
+ if (!text.includes(':') || /^[a-zA-Z0-9_][a-zA-Z0-9_:-]*$/.test(text.split(' ')[0]))
97
+ return ' (a key needs ": " after it)';
98
+ return ' (an unquoted key has letters, digits, "_", "-" and ":" only; quote any other)';
99
+ };
100
+ /** The hint a YAML block scalar marker in place of a value earns; none for any other value. */
101
+ const yamlHint = (value) => {
102
+ if (value === '>' || value === '>-' || value === '>+')
103
+ return ` ("${value}" is YAML's folded scalar; Lima writes "|" and continues a line with "^^")`;
104
+ if (value === '|-' || value === '|+')
105
+ return ` ("${value}" is a YAML chomping indicator; Lima writes "|")`;
106
+ return '';
107
+ };
108
+ /** §7.2: `-` alone or `-` followed by whitespace begins a sequence item (Core 1.1); `---` or `-x` does not. */
109
+ const isItemAt = (s, start, end) => s.charCodeAt(start) === 45 && (end - start === 1 || isTrimWhitespace(s.charCodeAt(start + 1)));
110
+ const isItem = (text) => isItemAt(text, 0, text.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limaformat/lima",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Lima frontmatter parser — LIMA Is Metadata Annotation",
5
5
  "keywords": ["frontmatter", "parser", "yaml", "markdown", "metadata"],
6
6
  "homepage": "https://limaformat.dev",