@limaformat/lima 0.8.0 → 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/CHANGELOG.md +9 -0
- package/dist/block-cursor.d.ts +15 -0
- package/dist/block-cursor.js +46 -0
- package/dist/block.d.ts +10 -2
- package/dist/block.js +294 -283
- package/dist/builder.d.ts +18 -0
- package/dist/flow.d.ts +6 -4
- package/dist/flow.js +19 -7
- package/dist/outline.js +138 -229
- package/dist/scalars.d.ts +2 -0
- package/dist/scalars.js +20 -15
- package/package.json +1 -1
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
|
-
/**
|
|
53
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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 @@ export const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, com
|
|
|
216
406
|
cursor.next();
|
|
217
407
|
}
|
|
218
408
|
else if (colonPos !== -1) {
|
|
219
|
-
|
|
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 =
|
|
418
|
+
raw = uncomment(raw);
|
|
225
419
|
const valueCol = wantSource ? cursor.indent + colonPos + 2 + leadingWsLen(trimmed.slice(colonPos + 2)) : 0;
|
|
226
|
-
builder.setMapping(
|
|
420
|
+
builder.setMapping(entries, key, parseInlineValue(raw, rawUndecoded === '|', baseIndent, line, valueCol, rawUndecoded, cursor, ctx, builder, readValue));
|
|
227
421
|
}
|
|
228
422
|
else if (bareKey) {
|
|
229
|
-
|
|
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,
|
|
234
|
-
builder.setMapping(
|
|
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:
|
|
240
|
-
cursor.next();
|
|
241
|
-
}
|
|
242
|
-
}
|
|
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
|
-
}
|
|
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;
|
|
437
|
+
message: `Lima: indented freetext without a block scalar marker at line ${line}: "${trimmed}"` });
|
|
317
438
|
cursor.next();
|
|
318
439
|
}
|
|
319
440
|
}
|
|
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
441
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
items
|
|
443
|
-
|
|
444
|
-
|
|
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);
|