@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 +70 -5
- package/README.md +1 -1
- package/dist/block-cursor.d.ts +4 -3
- package/dist/block-cursor.js +7 -3
- package/dist/block.js +122 -48
- package/dist/core.d.ts +3 -3
- package/dist/core.js +43 -29
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/scanner.js +4 -0
- package/dist/structure.d.ts +13 -0
- package/dist/structure.js +110 -0
- package/package.json +1 -1
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
|
|
8
|
-
1.
|
|
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
|
-
|
|
11
|
-
|
|
10
|
+
2.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)
|
|
11
|
+
carry their own.
|
|
12
12
|
|
|
13
|
-
## [
|
|
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.
|
|
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
|
package/dist/block-cursor.d.ts
CHANGED
|
@@ -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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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;
|
package/dist/block-cursor.js
CHANGED
|
@@ -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.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
?
|
|
153
|
-
: bareKey ?
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
259
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
?
|
|
327
|
-
: bareKey ?
|
|
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
|
|
330
|
-
//
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
237
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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