@limaformat/lima 0.7.1 → 0.8.1

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/block.js CHANGED
@@ -11,20 +11,12 @@
11
11
  import { checkKeyLength, checkDuplicateKey, NESTING_DEPTH_LIMIT } from './normalize.js';
12
12
  import { stripKeyQuotes, stripComment, stripCommentKeepEscapes, parseQuotedOrTyped, closingQuoteIndex, isValidKey, } from './scalars.js';
13
13
  import { KEY_GRAMMAR_HINT, specificKeyHint } from './key-hints.js';
14
- import { parseFlowOrScalarValue, parseFlowSequence } from './flow.js';
14
+ import { parseFlowOrScalarValue, parseFlowSequence, flowValueReader } from './flow.js';
15
15
  import { buildBlockScalar } from './block-scalar.js';
16
16
  import { LimaError, printableLine } from './errors.js';
17
17
  import { isTrimWhitespace } from './chars.js';
18
- import { BlockCursor } from './block-cursor.js';
18
+ import { BlockCursor, SourceBlockCursor } from './block-cursor.js';
19
19
  const DASH_PREFIX_RE = /^-\s+/;
20
- /**
21
- * A key's inline value text. If it is exactly `|`, consume the following
22
- * physical lines belonging to the Core §6.1.5 block scalar introduced by a
23
- * key at `keyIndent` and return the scalar; the cursor is left on the first
24
- * line past the scalar. Otherwise parse `raw` as an ordinary inline value
25
- * and advance one line. §6.1.5 places no top-level restriction on block
26
- * scalars, so this is the same primitive `core.ts` uses at the top level.
27
- */
28
20
  /** Length of the `- ` (dash + whitespace) prefix on the cursor's current line. */
29
21
  const dashPrefixLen = (cursor) => {
30
22
  const start = cursor.contentStart, end = cursor.lineEnd;
@@ -49,24 +41,18 @@ const inlineSource = (wantSource, ctx, keyLine, valueCol, rawUndecoded) => wantS
49
41
  raw: physicalRaw(rawUndecoded), line: keyLine, col: valueCol,
50
42
  tabAdjust: ctx.tabAdjust ? [ctx.tabAdjust[keyLine - 1] ?? 0] : undefined,
51
43
  }) : undefined;
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) => {
44
+ /**
45
+ * An inline value, or a block scalar when `marker`: `|` alone (§6.1.5;
46
+ * `| # c` is the string `|`). Both leave the cursor on the next unread line.
47
+ */
48
+ export const parseInlineValue = (raw, marker, keyIndent, keyLine, valueCol, rawUndecoded, cursor, ctx, builder, readValue) => {
54
49
  const wantSource = builder.positions === true;
55
50
  if (!marker) {
56
- const value = parseFlowOrScalarValue(raw, ctx, keyLine, builder, inlineSource(wantSource, ctx, keyLine, valueCol, rawUndecoded));
51
+ const value = readValue(raw, ctx, keyLine, builder, inlineSource(wantSource, ctx, keyLine, valueCol, rawUndecoded), cursor.lineStart + valueCol);
57
52
  cursor.next();
58
53
  return value;
59
54
  }
60
- cursor.next(); // past the `key: |` line
61
- const bodyLines = [];
62
- while (cursor.valid) {
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)
66
- break;
67
- bodyLines.push(cursor.source.slice(cursor.lineStart, cursor.lineEnd));
68
- cursor.next();
69
- }
55
+ const bodyLines = cursor.scalarLines(keyIndent);
70
56
  return buildBlockScalar(bodyLines, keyIndent, keyLine, ctx, builder).value;
71
57
  };
72
58
  export const findKeySep = (s) => {
@@ -82,11 +68,6 @@ export const findKeySep = (s) => {
82
68
  }
83
69
  return s.indexOf(': ');
84
70
  };
85
- /**
86
- * Recursively parses a block value from a mutable physical-line cursor over
87
- * the original source. Strings are materialized only when grammar or scalar
88
- * parsing needs their content.
89
- */
90
71
  /**
91
72
  * `source.slice(start, end).trimEnd()`: a key, or a line's content, keeps the whitespace it starts with, which makes it
92
73
  * no key (Core 1.1 §7.1: other whitespace before content is no indentation).
@@ -138,20 +119,6 @@ const isSequenceItem = (cursor) => {
138
119
  const next = cursor.contentStart + 1;
139
120
  return next === cursor.lineEnd || isTrimWhitespace(cursor.source.charCodeAt(next));
140
121
  };
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
- };
155
122
  const cursorAfterDash = (cursor) => {
156
123
  const start = cursor.contentStart;
157
124
  const end = cursor.lineEnd;
@@ -169,40 +136,263 @@ const unrecognizedKeyLine = (line, trimmed) => new LimaError({
169
136
  code: 'UNRECOGNIZED_LINE', line,
170
137
  message: `Lima: unrecognised line at line ${line}: "${printableLine(trimmed)}"${specificKeyHint(trimmed) ?? KEY_GRAMMAR_HINT}`,
171
138
  });
172
- /** Shared block grammar consuming one mutable physical-line cursor. */
173
- const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
174
- const wantSource = builder.positions === true;
175
- let items = null;
176
- let entries = null;
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;
180
- const startLine = cursor.lineIndex;
181
- while (cursor.valid) {
182
- const line = baseLine + cursor.lineIndex;
183
- if (cursor.empty || cursor.firstCode === 35) {
184
- cursor.next();
185
- continue;
186
- }
187
- const indent = cursor.indent;
188
- if (indent < baseIndent)
189
- break;
190
- if (indent > baseIndent) {
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}"` });
139
+ const sourceKeyReader = (cursor, positions) => {
140
+ if (!(cursor instanceof SourceBlockCursor))
141
+ throw new Error('Lima: source positions require a source block cursor');
142
+ return (raw, strict, line, colon, prefix = 0, requireNonempty = false) => {
143
+ const key = stripKeyQuotes(raw, strict, line);
144
+ if (requireNonempty && !key)
145
+ return key;
146
+ const start = cursor.contentStart + prefix;
147
+ const separator = start + colon;
148
+ // Core §4 / §15.4: the physical colon must precede a space or end the
149
+ // line/input. Comment stripping can make `- name:# unfinished` look
150
+ // like a bare key, so its parsed colon alone is insufficient.
151
+ const readable = separator === cursor.lineEnd - 1 || cursor.source.charCodeAt(separator + 1) === 32;
152
+ positions.key(key, start, readable ? start + raw.length : start, separator, line);
153
+ cursor.excludeCommentsBefore(separator + 1);
154
+ return key;
155
+ };
156
+ };
157
+ const sourceSequenceReader = (positions) => (raw, ctx, line, builder, _source, offset) => positions.recover(() => parseFlowSequence(raw, ctx, line, builder, undefined, offset), null);
158
+ /** Bind reporting outside the grammar, once for the entire recursive block. */
159
+ const blockParser = (readKey, uncomment, readValue, readSequence) => {
160
+ /**
161
+ * 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
162
+ * starts at the key's own column, which `compact` parsing ends at that column's first line that is no item. Null
163
+ * without either. The cursor is on the first content line after the key.
164
+ */
165
+ const blockOfBareKey = (cursor, keyColumn, ctx, baseLine, builder) => {
166
+ if (!cursor.valid)
167
+ return null;
168
+ if (cursor.indent > keyColumn)
169
+ return parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
170
+ if (cursor.indent === keyColumn && isSequenceItem(cursor))
171
+ return parseCursorBlock(cursor, keyColumn, ctx, baseLine, builder, true);
172
+ return null;
173
+ };
174
+ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
175
+ const wantSource = builder.positions === true;
176
+ let items = null;
177
+ let entries = null;
178
+ let pendingItem = null;
179
+ // the column of the pending object item's keys: its first key's, after `- ` (§7.2)
180
+ let pendingColumn = 0;
181
+ const startLine = cursor.lineIndex;
182
+ while (cursor.valid) {
183
+ const line = baseLine + cursor.lineIndex;
184
+ if (cursor.empty || cursor.firstCode === 35) {
202
185
  cursor.next();
203
186
  continue;
204
187
  }
205
- if (items !== null && pendingItem !== null) {
188
+ const indent = cursor.indent;
189
+ if (indent < baseIndent)
190
+ break;
191
+ if (indent > baseIndent) {
192
+ const trimmed = cursorContent(cursor);
193
+ // §7.2: an object item's keys stand at its first key's column. A line less indented ends the item (§7.1
194
+ // rule 4); a deeper one, which no nested block took, is inconsistently indented (rule 5) and the item goes on
195
+ if (pendingItem !== null && indent < pendingColumn) {
196
+ items.push(builder.mapping(pendingItem, line));
197
+ pendingItem = null;
198
+ }
199
+ if (pendingItem !== null && indent > pendingColumn) {
200
+ if (ctx.strict)
201
+ throw new LimaError({ code: 'INVALID_INDENTATION', line,
202
+ message: `Lima: unexpected indentation at line ${line}: "${trimmed}"` });
203
+ cursor.next();
204
+ continue;
205
+ }
206
+ if (items !== null && pendingItem !== null) {
207
+ const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
208
+ const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
209
+ const keyRaw = colonPos !== -1
210
+ ? trimEndSlice(trimmed, 0, colonPos)
211
+ : bareKey ? trimEndSlice(trimmed, 0, trimmed.length - 1) : '';
212
+ if ((keyRaw !== '' || colonPos !== -1) && !isValidKey(keyRaw)) {
213
+ // §5.1: not a usable key — an unrecognised line, skipped in non-strict mode and an error in strict
214
+ // mode, as at the top level (Core 1.1 §10.1)
215
+ if (ctx.strict)
216
+ throw unrecognizedKeyLine(line, trimmed);
217
+ cursor.next();
218
+ }
219
+ else if (colonPos !== -1) {
220
+ const key = readKey(keyRaw, ctx.strict, line, colonPos);
221
+ checkKeyLength(key, line);
222
+ const rawUndecoded = trimSlice(trimmed, colonPos + 2, trimmed.length);
223
+ let raw = rawUndecoded;
224
+ if (raw !== '|' && raw.includes('#'))
225
+ raw = uncomment(raw);
226
+ const valueCol = wantSource ? cursor.indent + colonPos + 2 + leadingWsLen(trimmed.slice(colonPos + 2)) : 0;
227
+ builder.setMapping(pendingItem, key, parseInlineValue(raw, rawUndecoded === '|', indent, line, valueCol, rawUndecoded, cursor, ctx, builder, readValue));
228
+ }
229
+ else if (bareKey) {
230
+ const key = readKey(keyRaw, ctx.strict, line, trimmed.length - 1);
231
+ checkKeyLength(key, line);
232
+ cursor.next();
233
+ skipEmptyAndCommentLines(cursor);
234
+ const nested = blockOfBareKey(cursor, indent, ctx, baseLine, builder);
235
+ builder.setMapping(pendingItem, key, nested ?? builder.null(line));
236
+ }
237
+ else {
238
+ if (ctx.strict)
239
+ throw new LimaError({ code: 'INVALID_INDENTATION', line,
240
+ message: `Lima: unexpected syntax in array item continuation at line ${line}: "${trimmed}"` });
241
+ cursor.next();
242
+ }
243
+ }
244
+ else {
245
+ if (ctx.strict)
246
+ throw new LimaError({ code: 'INVALID_INDENTATION', line,
247
+ message: `Lima: unexpected indentation at line ${line}: "${trimmed}"` });
248
+ cursor.next();
249
+ }
250
+ continue;
251
+ }
252
+ // a sequence at its key's column ends at that column's first line that is no item (Core 1.1 §7.2)
253
+ if (compact && !isSequenceItem(cursor))
254
+ break;
255
+ if (isSequenceItem(cursor)) {
256
+ if (pendingItem !== null) {
257
+ items.push(builder.mapping(pendingItem, line));
258
+ pendingItem = null;
259
+ }
260
+ // a block is a mapping or a sequence, as its first line decides; in non-strict mode a line of the other kind
261
+ // is skipped, and what the block holds stays (Core 1.1 §7.1)
262
+ if (entries !== null) {
263
+ if (ctx.strict)
264
+ throw new LimaError({ code: 'INVALID_INDENTATION', line,
265
+ message: `Lima: mixed array and map entries for the same key at line ${line}` });
266
+ cursor.next();
267
+ continue;
268
+ }
269
+ if (items === null)
270
+ items = [];
271
+ const afterDashRaw = cursorAfterDash(cursor);
272
+ const afterDash = afterDashRaw.includes('#') ? uncomment(afterDashRaw) : afterDashRaw.trimEnd();
273
+ const afterDashCol = wantSource ? cursor.indent + dashPrefixLen(cursor) : 0;
274
+ const afterDashTab = wantSource && ctx.tabAdjust ? [ctx.tabAdjust[line - 1] ?? 0] : undefined;
275
+ const dashSource = wantSource ? ({
276
+ raw: physicalRaw(afterDashRaw), line, col: afterDashCol, tabAdjust: afterDashTab,
277
+ }) : undefined;
278
+ const first = afterDash.charCodeAt(0);
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(readValue(afterDash, ctx, line, builder, dashSource, cursor.contentStart + dashPrefixLen(cursor)));
283
+ cursor.next();
284
+ continue;
285
+ }
286
+ if (first === 91) {
287
+ const sequence = readSequence(afterDash, ctx, line, builder, dashSource, cursor.contentStart + dashPrefixLen(cursor));
288
+ if (sequence === null)
289
+ items.push(readValue(afterDash, ctx, line, builder, dashSource, cursor.contentStart + dashPrefixLen(cursor)));
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 &&
299
+ afterDash.indexOf(': ') === -1 && !afterDash.endsWith(':')) {
300
+ items.push(parseQuotedOrTyped(afterDash, ctx, line, builder, dashSource));
301
+ cursor.next();
302
+ continue;
303
+ }
304
+ const colonPos = findKeySep(afterDash);
305
+ if (afterDash === '-' || DASH_PREFIX_RE.test(afterDash)) {
306
+ if (ctx.strict)
307
+ throw new LimaError({ code: 'INVALID_INDENTATION', line,
308
+ message: `Lima: nested block sequence at line ${line}: "${cursorContent(cursor)}"` });
309
+ items.push(builder.null(line));
310
+ cursor.next();
311
+ while (cursor.valid) {
312
+ if (cursor.empty || cursor.source.charCodeAt(cursor.contentStart) === 35) {
313
+ cursor.next();
314
+ continue;
315
+ }
316
+ if (cursor.indent <= baseIndent)
317
+ break;
318
+ cursor.next();
319
+ }
320
+ }
321
+ else if (colonPos !== -1 && isValidKey(trimSlice(afterDash, 0, colonPos))) {
322
+ const keyRaw = trimSlice(afterDash, 0, colonPos);
323
+ const key = readKey(keyRaw, ctx.strict, line, colonPos, dashPrefixLen(cursor));
324
+ checkKeyLength(key, line);
325
+ const valueStart = colonPos + 2;
326
+ const valueFirst = afterDash.charCodeAt(valueStart);
327
+ const valueLast = afterDash.charCodeAt(afterDash.length - 1);
328
+ const raw = valueFirst > 0x20 && valueFirst < 0x7f && valueLast > 0x20 && valueLast < 0x7f
329
+ ? afterDash.slice(valueStart) : trimSlice(afterDash, valueStart, afterDash.length);
330
+ // The key sits after `- `, two columns past the dash.
331
+ const valueCol = wantSource ? afterDashCol + valueStart + leadingWsLen(afterDash.slice(valueStart)) : 0;
332
+ pendingColumn = cursor.indent + dashPrefixLen(cursor);
333
+ pendingItem = builder.createMappingWith(key, parseInlineValue(raw, trimSlice(afterDashRaw, valueStart, afterDashRaw.length) === '|', pendingColumn, line, valueCol, wantSource ? trimSlice(afterDashRaw, valueStart, afterDashRaw.length) : raw, cursor, ctx, builder, readValue));
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) {
336
+ const continuationLine = baseLine + cursor.lineIndex;
337
+ const ckeyIndent = cursor.asciiIndent;
338
+ const cfirst = cursor.source.charCodeAt(cursor.contentStart);
339
+ if (cfirst === 35)
340
+ break;
341
+ const contLine = trimEndSlice(cursor.source, cursor.contentStart, cursor.lineEnd);
342
+ const csep = findKeySep(contLine);
343
+ if (csep === -1)
344
+ break;
345
+ const ckeyRaw = trimEndSlice(contLine, 0, csep);
346
+ if (!isValidKey(ckeyRaw))
347
+ break;
348
+ const ckey = readKey(ckeyRaw, ctx.strict, continuationLine, csep, 0, true);
349
+ if (!ckey)
350
+ break;
351
+ checkKeyLength(ckey, continuationLine);
352
+ const valueUndecoded = trimSlice(contLine, csep + 2, contLine.length);
353
+ let value = valueUndecoded;
354
+ if (value !== '|' && value.includes('#'))
355
+ value = uncomment(value);
356
+ // contLine starts exactly at contentStart (no leading trim), so
357
+ // the value's column is the indent plus its offset in contLine.
358
+ const cValueCol = wantSource ? cursor.indent + csep + 2 + leadingWsLen(contLine.slice(csep + 2)) : 0;
359
+ builder.setMapping(pendingItem, ckey, parseInlineValue(value, valueUndecoded === '|', ckeyIndent, continuationLine, cValueCol, valueUndecoded, cursor, ctx, builder, readValue));
360
+ }
361
+ }
362
+ else if (afterDash.endsWith(':') && isValidKey(trimSlice(afterDash, 0, afterDash.length - 1))) {
363
+ const key = readKey(trimSlice(afterDash, 0, afterDash.length - 1), ctx.strict, line, afterDash.length - 1, dashPrefixLen(cursor));
364
+ checkKeyLength(key, line);
365
+ // §7.1 rule 3 / §7.2: sibling keys of an object item align at
366
+ // the column of the first key *after* `- `; a nested block
367
+ // must be indented deeper than that column, not merely deeper
368
+ // than the item's dash. A line at the key's own column is the
369
+ // next sibling key, so the bare key is `null`.
370
+ let keyColumn = cursor.contentStart + 1;
371
+ while (keyColumn < cursor.lineEnd && isTrimWhitespace(cursor.source.charCodeAt(keyColumn)))
372
+ keyColumn++;
373
+ keyColumn -= cursor.lineStart;
374
+ cursor.next();
375
+ skipEmptyAndCommentLines(cursor);
376
+ const nested = blockOfBareKey(cursor, keyColumn, ctx, baseLine, builder);
377
+ pendingColumn = keyColumn;
378
+ pendingItem = builder.createMappingWith(key, nested ?? builder.null(line));
379
+ }
380
+ else {
381
+ // A quoted-or-typed scalar item — parseQuotedOrTyped enforces
382
+ // §10.1's unterminated / trailing-content strict checks.
383
+ items.push(parseQuotedOrTyped(afterDash, ctx, line, builder, dashSource));
384
+ cursor.next();
385
+ }
386
+ }
387
+ else {
388
+ const trimmed = cursorContent(cursor);
389
+ if (items !== null) {
390
+ if (ctx.strict)
391
+ throw new LimaError({ code: 'INVALID_INDENTATION', line,
392
+ message: `Lima: mixed map and array entries for the same key at line ${line}` });
393
+ cursor.next();
394
+ continue;
395
+ }
206
396
  const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
207
397
  const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
208
398
  const keyRaw = colonPos !== -1
@@ -216,235 +406,56 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact =
216
406
  cursor.next();
217
407
  }
218
408
  else if (colonPos !== -1) {
219
- const key = stripKeyQuotes(keyRaw, ctx.strict, line);
409
+ if (entries === null)
410
+ entries = builder.createMapping();
411
+ const key = readKey(keyRaw, ctx.strict, line, colonPos);
220
412
  checkKeyLength(key, line);
413
+ if (ctx.strict || ctx.onWarning !== undefined)
414
+ checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
221
415
  const rawUndecoded = trimSlice(trimmed, colonPos + 2, trimmed.length);
222
416
  let raw = rawUndecoded;
223
417
  if (raw !== '|' && raw.includes('#'))
224
- raw = stripComment(raw);
418
+ raw = uncomment(raw);
225
419
  const valueCol = wantSource ? cursor.indent + colonPos + 2 + leadingWsLen(trimmed.slice(colonPos + 2)) : 0;
226
- builder.setMapping(pendingItem, key, inlineOrBlockScalar(raw, rawUndecoded === '|', indent, line, valueCol, rawUndecoded, cursor, ctx, builder));
420
+ builder.setMapping(entries, key, parseInlineValue(raw, rawUndecoded === '|', baseIndent, line, valueCol, rawUndecoded, cursor, ctx, builder, readValue));
227
421
  }
228
422
  else if (bareKey) {
229
- const key = stripKeyQuotes(keyRaw, ctx.strict, line);
423
+ if (entries === null)
424
+ entries = builder.createMapping();
425
+ const key = readKey(keyRaw, ctx.strict, line, trimmed.length - 1);
230
426
  checkKeyLength(key, line);
427
+ if (ctx.strict || ctx.onWarning !== undefined)
428
+ checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
231
429
  cursor.next();
232
430
  skipEmptyAndCommentLines(cursor);
233
- const nested = blockOfBareKey(cursor, indent, ctx, baseLine, builder);
234
- builder.setMapping(pendingItem, key, nested ?? builder.null(line));
431
+ const nested = blockOfBareKey(cursor, baseIndent, ctx, baseLine, builder);
432
+ builder.setMapping(entries, key, nested ?? builder.null(line));
235
433
  }
236
434
  else {
237
435
  if (ctx.strict)
238
436
  throw new LimaError({ code: 'INVALID_INDENTATION', line,
239
- message: `Lima: unexpected syntax in array item continuation at line ${line}: "${trimmed}"` });
437
+ message: `Lima: indented freetext without a block scalar marker at line ${line}: "${trimmed}"` });
240
438
  cursor.next();
241
439
  }
242
440
  }
243
- else {
244
- if (ctx.strict)
245
- throw new LimaError({ code: 'INVALID_INDENTATION', line,
246
- message: `Lima: unexpected indentation at line ${line}: "${trimmed}"` });
247
- cursor.next();
248
- }
249
- continue;
250
441
  }
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)) {
255
- if (pendingItem !== null) {
256
- items.push(builder.mapping(pendingItem, line));
257
- pendingItem = null;
258
- }
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)
261
- if (entries !== null) {
262
- if (ctx.strict)
263
- throw new LimaError({ code: 'INVALID_INDENTATION', line,
264
- message: `Lima: mixed array and map entries for the same key at line ${line}` });
265
- cursor.next();
266
- continue;
267
- }
268
- if (items === null)
269
- items = [];
270
- const afterDashRaw = cursorAfterDash(cursor);
271
- const afterDash = afterDashRaw.includes('#') ? stripComment(afterDashRaw) : afterDashRaw.trimEnd();
272
- const afterDashCol = wantSource ? cursor.indent + dashPrefixLen(cursor) : 0;
273
- const afterDashTab = wantSource && ctx.tabAdjust ? [ctx.tabAdjust[line - 1] ?? 0] : undefined;
274
- const dashSource = wantSource ? ({
275
- raw: physicalRaw(afterDashRaw), line, col: afterDashCol, tabAdjust: afterDashTab,
276
- }) : undefined;
277
- const first = afterDash.charCodeAt(0);
278
- // an item's value is an inline value (§15.3): a flow mapping, or what an unclosed one falls back to (§7.5); a
279
- // flow sequence there is a sequence in a sequence (§7.2)
280
- if (first === 123) {
281
- items.push(parseFlowOrScalarValue(afterDash, ctx, line, builder, dashSource));
282
- cursor.next();
283
- continue;
284
- }
285
- if (first === 91) {
286
- const sequence = parseFlowSequence(afterDash, ctx, line, builder, dashSource);
287
- if (sequence === null)
288
- items.push(parseFlowOrScalarValue(afterDash, ctx, line, builder, dashSource));
289
- else if (ctx.strict)
290
- throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line,
291
- message: `Lima: nested sequence at line ${line}: "${cursorContent(cursor)}"` });
292
- else
293
- items.push(builder.null(line));
294
- cursor.next();
295
- continue;
296
- }
297
- if (first !== 34 && first !== 39 && first !== 45 &&
298
- afterDash.indexOf(': ') === -1 && !afterDash.endsWith(':')) {
299
- items.push(parseQuotedOrTyped(afterDash, ctx, line, builder, dashSource));
300
- cursor.next();
301
- continue;
302
- }
303
- const colonPos = findKeySep(afterDash);
304
- if (afterDash === '-' || DASH_PREFIX_RE.test(afterDash)) {
305
- if (ctx.strict)
306
- throw new LimaError({ code: 'INVALID_INDENTATION', line,
307
- message: `Lima: nested block sequence at line ${line}: "${cursorContent(cursor)}"` });
308
- items.push(builder.null(line));
309
- cursor.next();
310
- while (cursor.valid) {
311
- if (cursor.empty || cursor.source.charCodeAt(cursor.contentStart) === 35) {
312
- cursor.next();
313
- continue;
314
- }
315
- if (cursor.indent <= baseIndent)
316
- break;
317
- cursor.next();
318
- }
319
- }
320
- else if (colonPos !== -1 && isValidKey(trimSlice(afterDash, 0, colonPos))) {
321
- const keyRaw = trimSlice(afterDash, 0, colonPos);
322
- const key = stripKeyQuotes(keyRaw, ctx.strict, line);
323
- checkKeyLength(key, line);
324
- const valueStart = colonPos + 2;
325
- const valueFirst = afterDash.charCodeAt(valueStart);
326
- const valueLast = afterDash.charCodeAt(afterDash.length - 1);
327
- const raw = valueFirst > 0x20 && valueFirst < 0x7f && valueLast > 0x20 && valueLast < 0x7f
328
- ? afterDash.slice(valueStart) : trimSlice(afterDash, valueStart, afterDash.length);
329
- // The key sits after `- `, two columns past the dash.
330
- const valueCol = wantSource ? afterDashCol + valueStart + leadingWsLen(afterDash.slice(valueStart)) : 0;
331
- pendingColumn = cursor.indent + dashPrefixLen(cursor);
332
- pendingItem = builder.createMappingWith(key, inlineOrBlockScalar(raw, trimSlice(afterDashRaw, valueStart, afterDashRaw.length) === '|', pendingColumn, line, valueCol, wantSource ? trimSlice(afterDashRaw, valueStart, afterDashRaw.length) : raw, cursor, ctx, builder));
333
- // the item's later keys at its key column; any other line is the block loop's (§7.2)
334
- while (cursor.valid && cursor.indent === pendingColumn) {
335
- const continuationLine = baseLine + cursor.lineIndex;
336
- const ckeyIndent = cursor.asciiIndent;
337
- const cfirst = cursor.source.charCodeAt(cursor.contentStart);
338
- if (cfirst === 35)
339
- break;
340
- const contLine = trimEndSlice(cursor.source, cursor.contentStart, cursor.lineEnd);
341
- const csep = findKeySep(contLine);
342
- if (csep === -1)
343
- break;
344
- const ckeyRaw = trimEndSlice(contLine, 0, csep);
345
- if (!isValidKey(ckeyRaw))
346
- break;
347
- const ckey = stripKeyQuotes(ckeyRaw, ctx.strict, continuationLine);
348
- if (!ckey)
349
- break;
350
- checkKeyLength(ckey, continuationLine);
351
- const valueUndecoded = trimSlice(contLine, csep + 2, contLine.length);
352
- let value = valueUndecoded;
353
- if (value !== '|' && value.includes('#'))
354
- value = stripComment(value);
355
- // contLine starts exactly at contentStart (no leading trim), so
356
- // the value's column is the indent plus its offset in contLine.
357
- const cValueCol = wantSource ? cursor.indent + csep + 2 + leadingWsLen(contLine.slice(csep + 2)) : 0;
358
- builder.setMapping(pendingItem, ckey, inlineOrBlockScalar(value, valueUndecoded === '|', ckeyIndent, continuationLine, cValueCol, valueUndecoded, cursor, ctx, builder));
359
- }
360
- }
361
- else if (afterDash.endsWith(':') && isValidKey(trimSlice(afterDash, 0, afterDash.length - 1))) {
362
- const key = stripKeyQuotes(trimSlice(afterDash, 0, afterDash.length - 1), ctx.strict, line);
363
- checkKeyLength(key, line);
364
- // §7.1 rule 3 / §7.2: sibling keys of an object item align at
365
- // the column of the first key *after* `- `; a nested block
366
- // must be indented deeper than that column, not merely deeper
367
- // than the item's dash. A line at the key's own column is the
368
- // next sibling key, so the bare key is `null`.
369
- let keyColumn = cursor.contentStart + 1;
370
- while (keyColumn < cursor.lineEnd && isTrimWhitespace(cursor.source.charCodeAt(keyColumn)))
371
- keyColumn++;
372
- keyColumn -= cursor.lineStart;
373
- cursor.next();
374
- skipEmptyAndCommentLines(cursor);
375
- const nested = blockOfBareKey(cursor, keyColumn, ctx, baseLine, builder);
376
- pendingColumn = keyColumn;
377
- pendingItem = builder.createMappingWith(key, nested ?? builder.null(line));
378
- }
379
- else {
380
- // A quoted-or-typed scalar item — parseQuotedOrTyped enforces
381
- // §10.1's unterminated / trailing-content strict checks.
382
- items.push(parseQuotedOrTyped(afterDash, ctx, line, builder, dashSource));
383
- cursor.next();
384
- }
385
- }
386
- else {
387
- const trimmed = cursorContent(cursor);
388
- if (items !== null) {
389
- if (ctx.strict)
390
- throw new LimaError({ code: 'INVALID_INDENTATION', line,
391
- message: `Lima: mixed map and array entries for the same key at line ${line}` });
392
- cursor.next();
393
- continue;
394
- }
395
- const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
396
- const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
397
- const keyRaw = colonPos !== -1
398
- ? trimEndSlice(trimmed, 0, colonPos)
399
- : bareKey ? trimEndSlice(trimmed, 0, trimmed.length - 1) : '';
400
- if ((keyRaw !== '' || colonPos !== -1) && !isValidKey(keyRaw)) {
401
- // §5.1: not a usable key — an unrecognised line, skipped in non-strict mode and an error in strict
402
- // mode, as at the top level (Core 1.1 §10.1)
403
- if (ctx.strict)
404
- throw unrecognizedKeyLine(line, trimmed);
405
- cursor.next();
406
- }
407
- else if (colonPos !== -1) {
408
- if (entries === null)
409
- entries = builder.createMapping();
410
- const key = stripKeyQuotes(keyRaw, ctx.strict, line);
411
- checkKeyLength(key, line);
412
- if (ctx.strict || ctx.onWarning !== undefined)
413
- checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
414
- const rawUndecoded = trimSlice(trimmed, colonPos + 2, trimmed.length);
415
- let raw = rawUndecoded;
416
- if (raw !== '|' && raw.includes('#'))
417
- raw = stripComment(raw);
418
- const valueCol = wantSource ? cursor.indent + colonPos + 2 + leadingWsLen(trimmed.slice(colonPos + 2)) : 0;
419
- builder.setMapping(entries, key, inlineOrBlockScalar(raw, rawUndecoded === '|', baseIndent, line, valueCol, rawUndecoded, cursor, ctx, builder));
420
- }
421
- else if (bareKey) {
422
- if (entries === null)
423
- entries = builder.createMapping();
424
- const key = stripKeyQuotes(keyRaw, ctx.strict, line);
425
- checkKeyLength(key, line);
426
- if (ctx.strict || ctx.onWarning !== undefined)
427
- checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
428
- cursor.next();
429
- skipEmptyAndCommentLines(cursor);
430
- const nested = blockOfBareKey(cursor, baseIndent, ctx, baseLine, builder);
431
- builder.setMapping(entries, key, nested ?? builder.null(line));
432
- }
433
- else {
434
- if (ctx.strict)
435
- throw new LimaError({ code: 'INVALID_INDENTATION', line,
436
- message: `Lima: indented freetext without a block scalar marker at line ${line}: "${trimmed}"` });
437
- cursor.next();
438
- }
439
- }
440
- }
441
- if (pendingItem !== null)
442
- items.push(builder.mapping(pendingItem, baseLine + startLine));
443
- return items !== null ? builder.array(items, baseLine + startLine) :
444
- entries !== null ? builder.mapping(entries, baseLine + startLine) : null;
442
+ if (pendingItem !== null)
443
+ items.push(builder.mapping(pendingItem, baseLine + startLine));
444
+ return items !== null ? builder.array(items, baseLine + startLine) :
445
+ entries !== null ? builder.mapping(entries, baseLine + startLine) : null;
446
+ };
447
+ return parseCursorBlock;
448
+ };
449
+ const nativeBlockParser = blockParser(stripKeyQuotes, stripComment, parseFlowOrScalarValue, parseFlowSequence);
450
+ /** Shared block grammar consuming one mutable physical-line cursor. */
451
+ export const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
452
+ const positions = builder.sourcePositions;
453
+ const read = positions
454
+ ? blockParser(sourceKeyReader(cursor, positions), stripCommentKeepEscapes, flowValueReader(positions), sourceSequenceReader(positions))
455
+ : nativeBlockParser;
456
+ return read(cursor, baseIndent, ctx, baseLine, builder, compact);
445
457
  };
446
- export const parseBlockRange = (source, start, end, ctx, baseLine, builder, depthRisk) => {
447
- const cursor = new BlockCursor(source, start, end);
458
+ export const parseBlockRange = (source, start, end, ctx, baseLine, builder, depthRisk, cursor = new BlockCursor(source, start, end)) => {
448
459
  if (!cursor.next())
449
460
  return null;
450
461
  skipEmptyAndCommentLines(cursor);