@limaformat/lima 0.5.0 → 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 +91 -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 +125 -51
- package/dist/builder.d.ts +2 -0
- package/dist/core.d.ts +14 -3
- package/dist/core.js +81 -31
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/reference-tokens2.d.ts +8 -0
- package/dist/reference-tokens2.js +27 -5
- package/dist/references2.d.ts +15 -1
- package/dist/references2.js +91 -30
- package/dist/scalars.js +1 -0
- package/dist/scanner.js +4 -0
- package/dist/structure.d.ts +13 -0
- package/dist/structure.js +110 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,99 @@ 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/`).
|
|
79
|
+
|
|
80
|
+
## [0.5.1] — 2026-10-28
|
|
81
|
+
|
|
82
|
+
Core errata `1.0.10`: aligned top-level values are trimmed, as Core 1.0
|
|
83
|
+
always required.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- A top-level inline value kept every space after `: ` but the first:
|
|
88
|
+
`title: Hello` read `' Hello'`, and `n: 42` the string `' 42'`
|
|
89
|
+
instead of the integer 42 — against §4 step 3 and the §4 and §5.1
|
|
90
|
+
examples. Nested keys were not affected. The top-level entry now skips the
|
|
91
|
+
whitespace after the separator before reading the value, as the block
|
|
92
|
+
mapping does; the References 2.0 columns it reports were already physical
|
|
93
|
+
and are unchanged.
|
|
94
|
+
|
|
95
|
+
The shared conformance corpus grows to 218 Core cases (7 new, `since:
|
|
96
|
+
"1.0.10"`); TypeScript, Rust, and Go pass all of them.
|
|
97
|
+
|
|
98
|
+
A unit test in `src/block.test.ts` had asserted the defect (`key: value`
|
|
99
|
+
→ `' value'`); it now asserts the specified `'value'`.
|
|
14
100
|
|
|
15
101
|
## [0.5.0] — 2026-09-09
|
|
16
102
|
|
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
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* never resolves a reference in the first place).
|
|
10
10
|
*/
|
|
11
11
|
import { checkKeyLength, checkDuplicateKey, NESTING_DEPTH_LIMIT } from './normalize.js';
|
|
12
|
-
import { stripKeyQuotes, stripComment, stripCommentKeepEscapes, parseQuotedOrTyped, closingQuoteIndex, isValidKey,
|
|
13
|
-
import {
|
|
12
|
+
import { stripKeyQuotes, stripComment, stripCommentKeepEscapes, parseQuotedOrTyped, closingQuoteIndex, isValidKey, } from './scalars.js';
|
|
13
|
+
import { parseFlowOrScalarValue, parseFlowSequence } from './flow.js';
|
|
14
14
|
import { buildBlockScalar } from './block-scalar.js';
|
|
15
15
|
import { LimaError } from './errors.js';
|
|
16
16
|
import { isTrimWhitespace } from './chars.js';
|
|
@@ -48,9 +48,10 @@ const inlineSource = (wantSource, ctx, keyLine, valueCol, rawUndecoded) => wantS
|
|
|
48
48
|
raw: physicalRaw(rawUndecoded), line: keyLine, col: valueCol,
|
|
49
49
|
tabAdjust: ctx.tabAdjust ? [ctx.tabAdjust[keyLine - 1] ?? 0] : undefined,
|
|
50
50
|
}) : undefined;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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) => {
|
|
53
|
+
const wantSource = builder.positions === true;
|
|
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) => {
|
|
132
|
-
const wantSource = builder ===
|
|
172
|
+
const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
|
|
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/builder.d.ts
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { ReferenceSource } from './reference-tokens2.js';
|
|
28
28
|
export interface ValueBuilder<V, M = Map<string, V>> {
|
|
29
|
+
/** Whether scalar builders need physical source anchors for editor positions. */
|
|
30
|
+
readonly positions?: boolean;
|
|
29
31
|
null(line: number): V;
|
|
30
32
|
bool(value: boolean, line: number): V;
|
|
31
33
|
int(value: number, line: number): V;
|
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
|
|
@@ -22,6 +22,7 @@ import { type LimaValue } from './value.js';
|
|
|
22
22
|
import { type ParseContext, type Diagnostic } from './normalize.js';
|
|
23
23
|
import { type PositionedValue } from './scalars.js';
|
|
24
24
|
import type { ValueBuilder } from './builder.js';
|
|
25
|
+
import type { ReferenceToken2 } from './reference-tokens2.js';
|
|
25
26
|
export type { Diagnostic, ParseContext } from './normalize.js';
|
|
26
27
|
export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
|
|
27
28
|
export type { PositionedValue, InsertedAt } from './scalars.js';
|
|
@@ -33,13 +34,23 @@ export type CoreOptions = {
|
|
|
33
34
|
onWarning?: (diagnostic: Diagnostic) => void;
|
|
34
35
|
};
|
|
35
36
|
/**
|
|
36
|
-
* Parses Lima Core 1.
|
|
37
|
+
* Parses Lima Core 1.1 syntax into the internal annotated value tree —
|
|
37
38
|
* every node carrying its source line, string leaves additionally carrying
|
|
38
39
|
* whether they came from quoted syntax. `($key)`/`(%key)` text is left
|
|
39
40
|
* exactly as written; nothing here ever inspects or resolves it. The
|
|
40
41
|
* primitive the References layer (`references.ts`) builds on.
|
|
41
42
|
*/
|
|
42
43
|
export declare const parseCoreWithPositions: (frontMatter: string, ctx: ParseContext) => Map<string, PositionedValue>;
|
|
44
|
+
export type PositionedReferenceParse = {
|
|
45
|
+
document: Map<string, PositionedValue>;
|
|
46
|
+
references: ReferenceToken2[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Position-aware Core parse plus every active References 2.0 token seen while
|
|
50
|
+
* values are built. Recording at construction time preserves tokens from
|
|
51
|
+
* earlier duplicate-key values even though the result map is last-value-wins.
|
|
52
|
+
*/
|
|
53
|
+
export declare const parseCoreWithPositionedReferences: (frontMatter: string, ctx: ParseContext) => PositionedReferenceParse;
|
|
43
54
|
/** The public result shape (Core §11.1): every value `toNative*` can produce. */
|
|
44
55
|
export type NativeValue = null | boolean | number | string | Date | NativeValue[] | {
|
|
45
56
|
[key: string]: NativeValue;
|
|
@@ -64,7 +75,7 @@ export declare const toNative: (v: LimaValue) => NativeValue;
|
|
|
64
75
|
*/
|
|
65
76
|
export declare const toNativeFromPositioned: (v: PositionedValue) => NativeValue;
|
|
66
77
|
/**
|
|
67
|
-
* Public Core 1.
|
|
78
|
+
* Public Core 1.1 entry point. Never resolves `($key)`/`(%key)` text — see
|
|
68
79
|
* the module doc comment. Equivalent in observable behavior to calling
|
|
69
80
|
* `parseReferences()` with no partials on a document that happens to
|
|
70
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
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { checkKeyLength, byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, } from './normalize.js';
|
|
22
22
|
import { unescapeDQ, stripComment, stripCommentKeepEscapes, positionedBuilder, NO_SPAN_VALUE, parseSimpleScalarSpan, } from './scalars.js';
|
|
23
|
+
import { isTrimWhitespace } from './chars.js';
|
|
23
24
|
import { parseFlowOrScalarValue } from './flow.js';
|
|
24
25
|
import { parseBlockRange } from './block.js';
|
|
25
26
|
import { buildBlockScalar } from './block-scalar.js';
|
|
26
27
|
import { LimaError } from './errors.js';
|
|
27
28
|
import { KeyCursor, scanKeys } from './scanner.js';
|
|
29
|
+
import { checkRangeLines, isBlockScalarMarker } from './structure.js';
|
|
28
30
|
export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
|
|
29
31
|
export { toPlainValue } from './scalars.js';
|
|
30
32
|
/** Every Lima mapping result must be a prototype-free object (Core §11.1). */
|
|
@@ -76,7 +78,7 @@ const lineAt = (s, pos) => {
|
|
|
76
78
|
return n;
|
|
77
79
|
};
|
|
78
80
|
/**
|
|
79
|
-
* Parses Lima Core 1.
|
|
81
|
+
* Parses Lima Core 1.1 syntax, generic over the output representation
|
|
80
82
|
* (`builder` — see `builder.ts`): References needs the annotated
|
|
81
83
|
* `PositionedValue` tree (`parseCoreWithPositions` below fixes `V` to
|
|
82
84
|
* that), while `parseCore` fixes `V` to the public native shape directly,
|
|
@@ -156,8 +158,21 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
156
158
|
searchFrom = lineEnd + 1;
|
|
157
159
|
}
|
|
158
160
|
}
|
|
159
|
-
const parseEntry = (key, line,
|
|
161
|
+
const parseEntry = (key, line, separatorEnd, isBlock, inlineEnd, nextStart) => {
|
|
160
162
|
checkKeyLength(key, line);
|
|
163
|
+
// §4 step 3: an inline value begins after the spaces that follow `: `, however many there are — as in a
|
|
164
|
+
// block mapping (block.ts trimSlice), so that the scalar paths below and the source column see the value
|
|
165
|
+
// itself. The newline that ends the line is not skipped.
|
|
166
|
+
let rawStart = separatorEnd;
|
|
167
|
+
if (!isBlock) {
|
|
168
|
+
while (rawStart < nextStart) {
|
|
169
|
+
const c = frontMatter.charCodeAt(rawStart);
|
|
170
|
+
// printable ASCII other than SPACE is never whitespace: the common case ends here at once
|
|
171
|
+
if ((c > 0x20 && c < 0x7f) || c === 10 || !isTrimWhitespace(c))
|
|
172
|
+
break;
|
|
173
|
+
rawStart++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
161
176
|
if ((strict || ctx.onWarning !== undefined) && builder.hasMappingKey(root, key)) {
|
|
162
177
|
const diagnostic = {
|
|
163
178
|
code: 'DUPLICATE_KEY', line, key,
|
|
@@ -172,11 +187,23 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
172
187
|
}
|
|
173
188
|
const firstNewline = inlineEnd ?? frontMatter.indexOf('\n', rawStart);
|
|
174
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
|
+
}
|
|
175
202
|
// Physical codepoint column of the value's first character in its
|
|
176
203
|
// source line — the anchor for References 2.0 token positions (§2.4).
|
|
177
204
|
// References 2.0 token positions (§2.4): only build the source anchor
|
|
178
205
|
// for the annotated builder; `parseCore` (native) discards it.
|
|
179
|
-
const wantSource = builder ===
|
|
206
|
+
const wantSource = builder.positions === true;
|
|
180
207
|
const valueLineStart = wantSource ? frontMatter.lastIndexOf('\n', rawStart - 1) + 1 : 0;
|
|
181
208
|
const valueCol = wantSource ? [...frontMatter.slice(valueLineStart, rawStart)].length : 0;
|
|
182
209
|
const lineTabAdjust = wantSource && ctx.tabAdjust ? [ctx.tabAdjust[line - 1] ?? 0] : undefined;
|
|
@@ -218,31 +245,18 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
218
245
|
lineStart = j + 1;
|
|
219
246
|
}
|
|
220
247
|
}
|
|
221
|
-
{
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
const line0Trimmed = lines[0].trim();
|
|
227
|
-
if (lines.length === 1 || line0Trimmed !== '|') {
|
|
228
|
-
const line0 = lines[0];
|
|
229
|
-
const hasHash = line0.includes('#');
|
|
230
|
-
const val = hasHash ? stripComment(line0) : line0;
|
|
231
|
-
const source = wantSource ? {
|
|
232
|
-
raw: hasHash ? stripCommentKeepEscapes(line0) : line0,
|
|
233
|
-
line, col: valueCol, tabAdjust: lineTabAdjust,
|
|
234
|
-
} : undefined;
|
|
235
|
-
builder.setMapping(root, key, parseFlowOrScalarValue(val, ctx, line, builder, source));
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
// Multi-line string (`|` literal block scalar). The introducing key
|
|
239
|
-
// is at column 0 here; `buildBlockScalar` finds the scalar's own
|
|
240
|
-
// extent within the range (which runs to the next top-level key),
|
|
241
|
-
// so a dedented comment or dedented freetext before that key ends
|
|
242
|
-
// the scalar rather than being absorbed into it.
|
|
243
|
-
const bodyLines = raw.slice(raw.indexOf('\n') + 1).split('\n');
|
|
244
|
-
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;
|
|
245
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));
|
|
246
260
|
};
|
|
247
261
|
// A recognised top-level entry needs at least one UTF-16 unit of key text,
|
|
248
262
|
// `:`, and its terminating newline: three units. Consequently 129 entries
|
|
@@ -279,6 +293,9 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
279
293
|
else {
|
|
280
294
|
const matches = scanKeys(frontMatter);
|
|
281
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');
|
|
282
299
|
if (keyCount === 0)
|
|
283
300
|
return root;
|
|
284
301
|
if (keyCount > TOP_LEVEL_KEY_LIMIT) {
|
|
@@ -291,8 +308,19 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
291
308
|
const m = matches[i];
|
|
292
309
|
const rawDQ = m.doubleQuotedRaw;
|
|
293
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;
|
|
294
312
|
if (key !== undefined)
|
|
295
|
-
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
|
+
}
|
|
296
324
|
}
|
|
297
325
|
}
|
|
298
326
|
// Core §9 nesting depth, over Core's own reference-inert tree. When
|
|
@@ -308,13 +336,35 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
308
336
|
return root;
|
|
309
337
|
};
|
|
310
338
|
/**
|
|
311
|
-
* Parses Lima Core 1.
|
|
339
|
+
* Parses Lima Core 1.1 syntax into the internal annotated value tree —
|
|
312
340
|
* every node carrying its source line, string leaves additionally carrying
|
|
313
341
|
* whether they came from quoted syntax. `($key)`/`(%key)` text is left
|
|
314
342
|
* exactly as written; nothing here ever inspects or resolves it. The
|
|
315
343
|
* primitive the References layer (`references.ts`) builds on.
|
|
316
344
|
*/
|
|
317
345
|
export const parseCoreWithPositions = (frontMatter, ctx) => parseCoreGeneric(frontMatter, ctx, positionedBuilder, depthOfPositioned);
|
|
346
|
+
/**
|
|
347
|
+
* Position-aware Core parse plus every active References 2.0 token seen while
|
|
348
|
+
* values are built. Recording at construction time preserves tokens from
|
|
349
|
+
* earlier duplicate-key values even though the result map is last-value-wins.
|
|
350
|
+
*/
|
|
351
|
+
export const parseCoreWithPositionedReferences = (frontMatter, ctx) => {
|
|
352
|
+
const references = [];
|
|
353
|
+
const recordingBuilder = {
|
|
354
|
+
...positionedBuilder,
|
|
355
|
+
string: (value, line, quoted, source) => {
|
|
356
|
+
const positioned = positionedBuilder.string(value, line, quoted, source);
|
|
357
|
+
if (positioned.kind === 'string' && positioned.references2) {
|
|
358
|
+
references.push(...positioned.references2);
|
|
359
|
+
}
|
|
360
|
+
return positioned;
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
return {
|
|
364
|
+
document: parseCoreGeneric(frontMatter, ctx, recordingBuilder, depthOfPositioned),
|
|
365
|
+
references,
|
|
366
|
+
};
|
|
367
|
+
};
|
|
318
368
|
/**
|
|
319
369
|
* The `ValueBuilder<NativeValue>` — `parseCore`'s fast path. Every scalar
|
|
320
370
|
* builder is the identity function: unlike `positionedBuilder`, there is no
|
|
@@ -402,7 +452,7 @@ export const toNativeFromPositioned = (v) => {
|
|
|
402
452
|
}
|
|
403
453
|
};
|
|
404
454
|
/**
|
|
405
|
-
* Public Core 1.
|
|
455
|
+
* Public Core 1.1 entry point. Never resolves `($key)`/`(%key)` text — see
|
|
406
456
|
* the module doc comment. Equivalent in observable behavior to calling
|
|
407
457
|
* `parseReferences()` with no partials on a document that happens to
|
|
408
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.
|
|
@@ -39,5 +39,13 @@ export type ReferenceSource = {
|
|
|
39
39
|
col: number;
|
|
40
40
|
tabAdjust?: number[];
|
|
41
41
|
};
|
|
42
|
+
export declare const SEGMENT = "[a-zA-Z0-9_][a-zA-Z0-9_:-]*";
|
|
43
|
+
export declare const DOC_PATH = "[a-zA-Z0-9_][a-zA-Z0-9_:-]*(?:\\.[a-zA-Z0-9_][a-zA-Z0-9_:-]*)*";
|
|
44
|
+
export declare const PARTIAL_NAME = "[a-zA-Z0-9_][a-zA-Z0-9_:/-]*";
|
|
45
|
+
export declare const PARTIAL_PATH = "[a-zA-Z0-9_][a-zA-Z0-9_:/-]*(?:\\.[a-zA-Z0-9_][a-zA-Z0-9_:-]*)*";
|
|
42
46
|
export declare const PURE_REFERENCE_2: RegExp;
|
|
47
|
+
/** Converts a 0-based codepoint offset to JavaScript/LSP UTF-16 units. */
|
|
48
|
+
export declare const codepointOffsetToUtf16: (text: string, codepointOffset: number) => number;
|
|
49
|
+
/** Converts a 0-based JavaScript/LSP UTF-16 offset to a codepoint offset. */
|
|
50
|
+
export declare const utf16OffsetToCodepoint: (text: string, utf16Offset: number) => number;
|
|
43
51
|
export declare const scanReferenceTokens2: (value: string, source: ReferenceSource | undefined) => ReferenceToken2[];
|
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
/** Reference tokens recorded while the annotated Core tree is being built. */
|
|
2
|
-
const SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
|
|
3
|
-
const DOC_PATH = `${SEGMENT}(?:\\.${SEGMENT})*`;
|
|
4
|
-
const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
5
|
-
const PARTIAL_PATH = `${PARTIAL_NAME}(?:\\.${SEGMENT})*`;
|
|
2
|
+
export const SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
|
|
3
|
+
export const DOC_PATH = `${SEGMENT}(?:\\.${SEGMENT})*`;
|
|
4
|
+
export const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
5
|
+
export const PARTIAL_PATH = `${PARTIAL_NAME}(?:\\.${SEGMENT})*`;
|
|
6
6
|
export const PURE_REFERENCE_2 = new RegExp(`^(?:\\$\\{(${DOC_PATH})\\}|\\$\\((${PARTIAL_PATH})\\))$`);
|
|
7
7
|
const REFERENCE_2 = new RegExp(`\\$\\{(${DOC_PATH})\\}|\\$\\((${PARTIAL_PATH})\\)`, 'g');
|
|
8
8
|
const NO_TOKENS = [];
|
|
9
|
+
const utf16WidthAt = (text, offset) => text.codePointAt(offset) > 0xffff ? 2 : 1;
|
|
9
10
|
/** Codepoint length of the UTF-16 slice `[a, b)` of `s`. */
|
|
10
11
|
const codepointLen = (s, a, b) => {
|
|
11
12
|
let n = 0;
|
|
12
13
|
for (let i = a; i < b;) {
|
|
13
|
-
i += s
|
|
14
|
+
i += utf16WidthAt(s, i);
|
|
14
15
|
n++;
|
|
15
16
|
}
|
|
16
17
|
return n;
|
|
17
18
|
};
|
|
19
|
+
/** Converts a 0-based codepoint offset to JavaScript/LSP UTF-16 units. */
|
|
20
|
+
export const codepointOffsetToUtf16 = (text, codepointOffset) => {
|
|
21
|
+
let utf16Offset = 0;
|
|
22
|
+
let codepoints = 0;
|
|
23
|
+
while (utf16Offset < text.length && codepoints < codepointOffset) {
|
|
24
|
+
utf16Offset += utf16WidthAt(text, utf16Offset);
|
|
25
|
+
codepoints++;
|
|
26
|
+
}
|
|
27
|
+
return utf16Offset;
|
|
28
|
+
};
|
|
29
|
+
/** Converts a 0-based JavaScript/LSP UTF-16 offset to a codepoint offset. */
|
|
30
|
+
export const utf16OffsetToCodepoint = (text, utf16Offset) => {
|
|
31
|
+
let currentUtf16 = 0;
|
|
32
|
+
let codepoints = 0;
|
|
33
|
+
const limit = Math.min(Math.max(utf16Offset, 0), text.length);
|
|
34
|
+
while (currentUtf16 < limit) {
|
|
35
|
+
currentUtf16 += utf16WidthAt(text, currentUtf16);
|
|
36
|
+
codepoints++;
|
|
37
|
+
}
|
|
38
|
+
return codepoints;
|
|
39
|
+
};
|
|
18
40
|
/** Splice points (decoded value order): token text, UTF-16 index, path. */
|
|
19
41
|
const scanDecoded = (value) => {
|
|
20
42
|
REFERENCE_2.lastIndex = 0;
|
package/dist/references2.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import { type Diagnostic } from './core.js';
|
|
1
|
+
import { type Diagnostic, type PositionedReferenceParse, type PositionedValue } from './core.js';
|
|
2
2
|
type Meta = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve one document-reference path with the same traversal, cycle checks,
|
|
5
|
+
* and edge budget used by the public References 2.0 parser. The returned
|
|
6
|
+
* positioned value retains the definition site's source line.
|
|
7
|
+
*/
|
|
8
|
+
export type DocumentReferenceTargetResolution = {
|
|
9
|
+
status: 'resolved';
|
|
10
|
+
target: PositionedValue;
|
|
11
|
+
} | {
|
|
12
|
+
status: 'unverifiable-partials';
|
|
13
|
+
} | {
|
|
14
|
+
status: 'unresolved';
|
|
15
|
+
};
|
|
16
|
+
export declare const resolveDocumentReferenceTarget: (parsed: PositionedReferenceParse, path: string) => DocumentReferenceTargetResolution;
|
|
3
17
|
export type ParseMode = 'references' | 'core';
|
|
4
18
|
export type ParseOptions = {
|
|
5
19
|
mode?: ParseMode;
|
package/dist/references2.js
CHANGED
|
@@ -7,6 +7,10 @@ import { collectAllParticipants, deepCopyPositioned, earliestParticipant, emptyM
|
|
|
7
7
|
const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
8
8
|
const PARTIAL_NAME_RE = new RegExp(`^${PARTIAL_NAME}$`);
|
|
9
9
|
const MAX_EDGES = 3;
|
|
10
|
+
// Looking up the root path is itself the first reference edge; recursion into
|
|
11
|
+
// that root therefore gets the public resolver's remaining two-edge budget.
|
|
12
|
+
const TARGET_EDGE_BUDGET = MAX_EDGES - 1;
|
|
13
|
+
const referenceResourceStatuses = new WeakMap();
|
|
10
14
|
/** Copies a pure-reference target and stamps the new insertion without
|
|
11
15
|
* discarding provenance already attached to the copied root (R-112). */
|
|
12
16
|
const copyWithInsertion = (target, insertedAt) => {
|
|
@@ -44,6 +48,91 @@ const documentTarget = (root, path) => {
|
|
|
44
48
|
}
|
|
45
49
|
return { root: value, tail: segments.slice(index) };
|
|
46
50
|
};
|
|
51
|
+
export const resolveDocumentReferenceTarget = (parsed, path) => {
|
|
52
|
+
const { document } = parsed;
|
|
53
|
+
const target = documentTarget(document, path);
|
|
54
|
+
if (target === undefined)
|
|
55
|
+
return { status: 'unresolved' };
|
|
56
|
+
const resourceStatus = documentReferenceResourceStatus(parsed);
|
|
57
|
+
if (resourceStatus === 'unverifiable-partials')
|
|
58
|
+
return { status: resourceStatus };
|
|
59
|
+
if (resourceStatus === 'invalid')
|
|
60
|
+
return { status: 'unresolved' };
|
|
61
|
+
const ctx = { diagnostics: [], cache: new WeakMap() };
|
|
62
|
+
const stack = new Set([target.root]);
|
|
63
|
+
const resolved = resolveNode(target.root, document, new Map(), ctx, TARGET_EDGE_BUDGET, stack);
|
|
64
|
+
if (!resolved.complete || ctx.diagnostics.length > 0)
|
|
65
|
+
return { status: 'unresolved' };
|
|
66
|
+
const selected = lookupPath(resolved.value, target.tail);
|
|
67
|
+
return selected === undefined
|
|
68
|
+
? { status: 'unresolved' }
|
|
69
|
+
: { status: 'resolved', target: selected };
|
|
70
|
+
};
|
|
71
|
+
const resolveDocument = (document, partials, ctx) => {
|
|
72
|
+
const resolved = new Map();
|
|
73
|
+
for (const [key, value] of document) {
|
|
74
|
+
const stack = new Set([value]);
|
|
75
|
+
resolved.set(key, resolveNode(value, document, partials, ctx, MAX_EDGES, stack).value);
|
|
76
|
+
}
|
|
77
|
+
return resolved;
|
|
78
|
+
};
|
|
79
|
+
const assertResolvedDocumentResourceLimits = (resolved) => {
|
|
80
|
+
const finalized = [...resolved].map(([key, value]) => [key, finalizePositioned(value)]);
|
|
81
|
+
const depth = finalized.length === 0 ? 0 : Math.max(...finalized.map(([, value]) => value.depth));
|
|
82
|
+
if (depth > NESTING_DEPTH_LIMIT) {
|
|
83
|
+
const participants = finalized
|
|
84
|
+
.filter(([, value]) => value.depth === depth)
|
|
85
|
+
.flatMap(([, value]) => value.deepestParticipants);
|
|
86
|
+
const winner = earliestParticipant(participants);
|
|
87
|
+
throw new LimaError({
|
|
88
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
89
|
+
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
90
|
+
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner?.line ?? 1}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
let nodeCount = 1;
|
|
94
|
+
for (const [, value] of finalized)
|
|
95
|
+
nodeCount += value.nodeCount;
|
|
96
|
+
if (nodeCount > RESULT_NODE_LIMIT) {
|
|
97
|
+
const participants = [];
|
|
98
|
+
for (const value of resolved.values())
|
|
99
|
+
collectAllParticipants(value, participants);
|
|
100
|
+
const winner = earliestParticipant(participants);
|
|
101
|
+
throw new LimaError({
|
|
102
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
103
|
+
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
104
|
+
message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return finalized;
|
|
108
|
+
};
|
|
109
|
+
const documentReferenceResourceStatus = (parsed) => {
|
|
110
|
+
// Partial values are supplied by caller code and can change whole-document
|
|
111
|
+
// depth/node limits. With no file convention for them, editor tooling cannot
|
|
112
|
+
// prove that even an unrelated document reference is safe to resolve.
|
|
113
|
+
if (parsed.references.some((reference) => reference.partialPath !== undefined))
|
|
114
|
+
return 'unverifiable-partials';
|
|
115
|
+
const { document } = parsed;
|
|
116
|
+
const cached = referenceResourceStatuses.get(document);
|
|
117
|
+
if (cached !== undefined)
|
|
118
|
+
return cached;
|
|
119
|
+
const ctx = { diagnostics: [], cache: new WeakMap() };
|
|
120
|
+
const resolved = resolveDocument(document, new Map(), ctx);
|
|
121
|
+
let valid = !ctx.diagnostics.some((diagnostic) => diagnostic.code === 'RESOURCE_LIMIT');
|
|
122
|
+
if (valid) {
|
|
123
|
+
try {
|
|
124
|
+
assertResolvedDocumentResourceLimits(resolved);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (!(error instanceof LimaError) || error.code !== 'RESOURCE_LIMIT')
|
|
128
|
+
throw error;
|
|
129
|
+
valid = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const status = valid ? 'valid' : 'invalid';
|
|
133
|
+
referenceResourceStatuses.set(document, status);
|
|
134
|
+
return status;
|
|
135
|
+
};
|
|
47
136
|
const lookupPartial = (partials, path) => {
|
|
48
137
|
const dot = path.indexOf('.');
|
|
49
138
|
if (dot < 0)
|
|
@@ -284,11 +373,7 @@ const parseInternal = (frontMatter, options, useResolveCache) => {
|
|
|
284
373
|
}
|
|
285
374
|
const document = parseCoreWithPositions(frontMatter, { strict: options?.strict ?? false, onWarning: options?.onWarning });
|
|
286
375
|
const ctx = { diagnostics: [], ...(useResolveCache ? { cache: new WeakMap() } : {}) };
|
|
287
|
-
const resolved =
|
|
288
|
-
for (const [key, value] of document) {
|
|
289
|
-
const stack = new Set([value]);
|
|
290
|
-
resolved.set(key, resolveNode(value, document, partials, ctx, MAX_EDGES, stack).value);
|
|
291
|
-
}
|
|
376
|
+
const resolved = resolveDocument(document, partials, ctx);
|
|
292
377
|
if (options?.strict)
|
|
293
378
|
for (const value of resolved.values())
|
|
294
379
|
scanUnresolved(value, ctx);
|
|
@@ -299,31 +384,7 @@ const parseInternal = (frontMatter, options, useResolveCache) => {
|
|
|
299
384
|
// already do; `offset` was internal-only here.
|
|
300
385
|
throw new LimaError({ ...winner, column: winner.offset + 1 });
|
|
301
386
|
}
|
|
302
|
-
const finalized =
|
|
303
|
-
const depth = finalized.length === 0 ? 0 : Math.max(...finalized.map(([, value]) => value.depth));
|
|
304
|
-
if (depth > NESTING_DEPTH_LIMIT) {
|
|
305
|
-
const participants = finalized.filter(([, value]) => value.depth === depth).flatMap(([, value]) => value.deepestParticipants);
|
|
306
|
-
const winner = earliestParticipant(participants);
|
|
307
|
-
throw new LimaError({
|
|
308
|
-
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
309
|
-
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
310
|
-
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner?.line ?? 1}`,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
let nodeCount = 1;
|
|
314
|
-
for (const [, value] of finalized)
|
|
315
|
-
nodeCount += value.nodeCount;
|
|
316
|
-
if (nodeCount > RESULT_NODE_LIMIT) {
|
|
317
|
-
const participants = [];
|
|
318
|
-
for (const value of resolved.values())
|
|
319
|
-
collectAllParticipants(value, participants);
|
|
320
|
-
const winner = earliestParticipant(participants);
|
|
321
|
-
throw new LimaError({
|
|
322
|
-
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
323
|
-
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
324
|
-
message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
387
|
+
const finalized = assertResolvedDocumentResourceLimits(resolved);
|
|
327
388
|
const out = emptyMapping();
|
|
328
389
|
for (const [key, value] of finalized)
|
|
329
390
|
out[key] = value.native;
|
package/dist/scalars.js
CHANGED
|
@@ -13,6 +13,7 @@ export const hasActiveReferences2 = (value) => value.kind === 'string' ? (value.
|
|
|
13
13
|
: false;
|
|
14
14
|
/** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
|
|
15
15
|
export const positionedBuilder = {
|
|
16
|
+
positions: true,
|
|
16
17
|
null: (line) => ({ kind: 'null', line }),
|
|
17
18
|
bool: (value, line) => ({ kind: 'bool', value, line }),
|
|
18
19
|
int: (value, line) => ({ kind: 'int', value, line }),
|
package/dist/scanner.js
CHANGED
|
@@ -59,6 +59,10 @@ const matchSeparator = (s, pos) => {
|
|
|
59
59
|
i++; // consume spaces
|
|
60
60
|
if (i < s.length && s.charCodeAt(i) === 10)
|
|
61
61
|
return i + 1;
|
|
62
|
+
// Core 1.1: the end of the input ends the key's line as a newline does, so a last `key:` is `key: null`, as it
|
|
63
|
+
// is at any nested level (it was no key at all at the top level before)
|
|
64
|
+
if (i === s.length)
|
|
65
|
+
return i;
|
|
62
66
|
// Backtracking a pure-space run can never reveal a `\n` the maximal
|
|
63
67
|
// scan didn't already see (every intermediate position is followed by
|
|
64
68
|
// either another space or the same non-`\n` stop character) — so
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lines after a key belong to: nothing (`none`, before the first key; `inline`, after an inline value), a
|
|
3
|
+
* block mapping or sequence (`mapping`, after a bare key) or a `|` scalar (`scalar`).
|
|
4
|
+
*/
|
|
5
|
+
export type RangeOwner = 'none' | 'inline' | 'mapping' | 'scalar';
|
|
6
|
+
/**
|
|
7
|
+
* Checks the lines of `frontMatter` from `start` (a line start) to `end` that follow a key owned as `owner`; `line`
|
|
8
|
+
* is the 1-based line at `start`; `valueStart` to `valueEnd` is the inline value of that key, read only for the hint a
|
|
9
|
+
* YAML marker earns in the error. Throws for the first line that belongs to no structure.
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkRangeLines(frontMatter: string, start: number, end: number, line: number, owner: RangeOwner, valueStart?: number, valueEnd?: number): void;
|
|
12
|
+
/** Whether the inline value that starts at `rawStart` and ends its line at `lineEnd` is a `|` block scalar marker. */
|
|
13
|
+
export declare const isBlockScalarMarker: (frontMatter: string, rawStart: number, lineEnd: number) => boolean;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core 1.1 §4, §7.1 rule 5 and §10.1: in strict mode every content line belongs to the structure. Between two
|
|
3
|
+
* top-level keys (and before the first), a line that is neither blank nor a comment is
|
|
4
|
+
*
|
|
5
|
+
* - unrecognised (`UNRECOGNIZED_LINE`) when it starts at column 0: it matches no key pattern, or the scanner would
|
|
6
|
+
* have ended the range at it;
|
|
7
|
+
* A sequence that starts at column 0 under a bare key is that key's (Core 1.1 §7.2), up to the first line at
|
|
8
|
+
* column 0 that is no item.
|
|
9
|
+
* - inconsistently indented (`INVALID_INDENTATION`) when it is indented and no block owns it: before the first key;
|
|
10
|
+
* after a key with an inline value other than `|`, which opens no block; under a block mapping, but less indented
|
|
11
|
+
* than its first line (its base indentation, §7.1 rule 1), where the block parser stops; or after a `|` scalar
|
|
12
|
+
* that a line at column 0 ended (§6.1.5).
|
|
13
|
+
*
|
|
14
|
+
* The lines a block mapping or a `|` scalar owns are checked where they are parsed. Non-strict mode skips all of
|
|
15
|
+
* these lines, as Core 1.0 did in both modes.
|
|
16
|
+
*/
|
|
17
|
+
import { isTrimWhitespace } from './chars.js';
|
|
18
|
+
import { LimaError } from './errors.js';
|
|
19
|
+
/**
|
|
20
|
+
* Checks the lines of `frontMatter` from `start` (a line start) to `end` that follow a key owned as `owner`; `line`
|
|
21
|
+
* is the 1-based line at `start`; `valueStart` to `valueEnd` is the inline value of that key, read only for the hint a
|
|
22
|
+
* YAML marker earns in the error. Throws for the first line that belongs to no structure.
|
|
23
|
+
*/
|
|
24
|
+
export function checkRangeLines(frontMatter, start, end, line, owner, valueStart = 0, valueEnd = 0) {
|
|
25
|
+
let pos = start;
|
|
26
|
+
let current = line;
|
|
27
|
+
let base = -1; // a block mapping's base indentation, once its first content line set it
|
|
28
|
+
let scalarOpen = owner === 'scalar';
|
|
29
|
+
while (pos < end) {
|
|
30
|
+
const lineEnd = frontMatter.indexOf('\n', pos);
|
|
31
|
+
const stop = lineEnd === -1 || lineEnd > end ? end : lineEnd;
|
|
32
|
+
// positions, not substrings: a line is read out of `frontMatter` only for an error message
|
|
33
|
+
let spaces = pos;
|
|
34
|
+
while (spaces < stop && frontMatter.charCodeAt(spaces) === 32)
|
|
35
|
+
spaces++;
|
|
36
|
+
const indent = spaces - pos;
|
|
37
|
+
// §6.1.5: a `|` scalar holds every line up to the first non-empty one at its key's column (0), `#` included
|
|
38
|
+
if (scalarOpen && (spaces === stop || indent > 0)) {
|
|
39
|
+
pos = lineEnd + 1;
|
|
40
|
+
current++;
|
|
41
|
+
if (lineEnd === -1 || lineEnd >= end)
|
|
42
|
+
break;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
scalarOpen = false;
|
|
46
|
+
// blank and comment lines by Lima's own whitespace; indentation by ASCII space only (§3 expanded the tabs)
|
|
47
|
+
let first = pos;
|
|
48
|
+
while (first < stop && isTrimWhitespace(frontMatter.charCodeAt(first)))
|
|
49
|
+
first++;
|
|
50
|
+
if (first < stop && frontMatter.charCodeAt(first) !== 35) {
|
|
51
|
+
// §7.2: a bare key's sequence may start at its own column, 0 here; it ends at the first line there that is no
|
|
52
|
+
// item
|
|
53
|
+
const item = indent === 0 && isItemAt(frontMatter, pos, stop);
|
|
54
|
+
if (owner === 'mapping' && base === -1 && item)
|
|
55
|
+
base = 0;
|
|
56
|
+
if (indent === 0 && !(base === 0 && item)) {
|
|
57
|
+
const text = frontMatter.slice(pos, stop);
|
|
58
|
+
throw new LimaError({
|
|
59
|
+
code: 'UNRECOGNIZED_LINE', line: current,
|
|
60
|
+
message: `Lima: unrecognised line at line ${current}: "${text}"${hint(text)}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (owner === 'mapping' && base === -1)
|
|
64
|
+
base = indent;
|
|
65
|
+
if (indent > 0 && !(owner === 'mapping' && indent >= base)) {
|
|
66
|
+
throw new LimaError({
|
|
67
|
+
code: 'INVALID_INDENTATION', line: current,
|
|
68
|
+
message: `Lima: unexpected indentation at line ${current}: "${frontMatter.slice(first, stop)}"${owner === 'inline' ? yamlHint(frontMatter.slice(valueStart, valueEnd).trim()) : ''}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (lineEnd === -1 || lineEnd >= end)
|
|
73
|
+
break;
|
|
74
|
+
pos = lineEnd + 1;
|
|
75
|
+
current++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Whether the inline value that starts at `rawStart` and ends its line at `lineEnd` is a `|` block scalar marker. */
|
|
79
|
+
export const isBlockScalarMarker = (frontMatter, rawStart, lineEnd) => {
|
|
80
|
+
let i = rawStart, j = lineEnd;
|
|
81
|
+
while (i < j && isTrimWhitespace(frontMatter.charCodeAt(i)))
|
|
82
|
+
i++;
|
|
83
|
+
while (j > i && isTrimWhitespace(frontMatter.charCodeAt(j - 1)))
|
|
84
|
+
j--;
|
|
85
|
+
return j - i === 1 && frontMatter.charCodeAt(i) === 124;
|
|
86
|
+
};
|
|
87
|
+
/** What a reader likely meant by an unrecognised line, as a hint after the message; none for anything else. */
|
|
88
|
+
const hint = (text) => {
|
|
89
|
+
if (text === '---' || text === '...')
|
|
90
|
+
return ' (a YAML document marker; Lima has one document)';
|
|
91
|
+
if (isItem(text))
|
|
92
|
+
return ' (a sequence item belongs to a key with no value of its own)';
|
|
93
|
+
if (text.startsWith('"') || text.startsWith("'"))
|
|
94
|
+
return ' (a quoted key closes on its own line)';
|
|
95
|
+
// `author Odile`, `key:value`: key characters, but no ": " after them
|
|
96
|
+
if (!text.includes(':') || /^[a-zA-Z0-9_][a-zA-Z0-9_:-]*$/.test(text.split(' ')[0]))
|
|
97
|
+
return ' (a key needs ": " after it)';
|
|
98
|
+
return ' (an unquoted key has letters, digits, "_", "-" and ":" only; quote any other)';
|
|
99
|
+
};
|
|
100
|
+
/** The hint a YAML block scalar marker in place of a value earns; none for any other value. */
|
|
101
|
+
const yamlHint = (value) => {
|
|
102
|
+
if (value === '>' || value === '>-' || value === '>+')
|
|
103
|
+
return ` ("${value}" is YAML's folded scalar; Lima writes "|" and continues a line with "^^")`;
|
|
104
|
+
if (value === '|-' || value === '|+')
|
|
105
|
+
return ` ("${value}" is a YAML chomping indicator; Lima writes "|")`;
|
|
106
|
+
return '';
|
|
107
|
+
};
|
|
108
|
+
/** §7.2: `-` alone or `-` followed by whitespace begins a sequence item (Core 1.1); `---` or `-x` does not. */
|
|
109
|
+
const isItemAt = (s, start, end) => s.charCodeAt(start) === 45 && (end - start === 1 || isTrimWhitespace(s.charCodeAt(start + 1)));
|
|
110
|
+
const isItem = (text) => isItemAt(text, 0, text.length);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@limaformat/lima",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Lima frontmatter parser — LIMA Is Metadata Annotation",
|
|
5
5
|
"keywords": ["frontmatter", "parser", "yaml", "markdown", "metadata"],
|
|
6
6
|
"homepage": "https://limaformat.dev",
|
|
7
7
|
"author": "Michael Kortstiege",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/limaformat/lima",
|
|
10
|
+
"url": "git+https://github.com/limaformat/lima.git",
|
|
11
11
|
"directory": "js"
|
|
12
12
|
},
|
|
13
13
|
"bugs": "https://github.com/limaformat/lima/issues",
|