@limaformat/lima 0.3.1 → 0.5.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 -0
- package/dist/block-scalar.d.ts +33 -0
- package/dist/block-scalar.js +109 -0
- package/dist/block.js +167 -80
- package/dist/builder.d.ts +8 -9
- package/dist/core.js +43 -89
- package/dist/flow.d.ts +4 -3
- package/dist/flow.js +97 -16
- package/dist/normalize.d.ts +8 -9
- package/dist/normalize.js +2 -11
- package/dist/{references.d.ts → positioned-tree.d.ts} +9 -37
- package/dist/positioned-tree.js +123 -0
- package/dist/reference-tokens2.d.ts +34 -3
- package/dist/reference-tokens2.js +56 -23
- package/dist/references2.d.ts +2 -0
- package/dist/references2.js +47 -16
- package/dist/scalars.d.ts +84 -14
- package/dist/scalars.js +158 -29
- package/dist/scanner.d.ts +26 -13
- package/dist/scanner.js +33 -15
- package/package.json +24 -24
- package/dist/references.js +0 -524
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,97 @@ are both frozen regardless of what this file shows.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.5.0] — 2026-09-09
|
|
16
|
+
|
|
17
|
+
Scope cleanup. The frozen **References 1.0** resolver is no longer bundled in
|
|
18
|
+
the published package — it was never a public entry point (`parseReferences`
|
|
19
|
+
and `parse` are References 2.0) and only existed to run the frozen 1.0
|
|
20
|
+
conformance corpus, which now does so against the repository source. The
|
|
21
|
+
1.0 specification and its 101-case corpus stay in the repository as a
|
|
22
|
+
frozen regression target.
|
|
23
|
+
|
|
24
|
+
No changes to Core 1.0 or References 2.0 syntax, semantics, or public API.
|
|
25
|
+
The version advances to `0.5.0` in step with the Rust crate and Go module.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- The published tarball drops from ~59.3 KB to ~53.4 KB — the internal
|
|
30
|
+
References 1.0 resolver (`references.ts`) is excluded from the build.
|
|
31
|
+
- Documentation de-emphasises References 1.0 throughout: it is presented as
|
|
32
|
+
a briefly-published, immediately-superseded early version, not a current
|
|
33
|
+
target. The "Migrating References 1.0 to 2.0" guide is removed (see git
|
|
34
|
+
history if you need it).
|
|
35
|
+
|
|
36
|
+
## [0.4.0] — 2026-09-08
|
|
37
|
+
|
|
38
|
+
First independent whole-repo review of Lima, then a second review of the
|
|
39
|
+
consolidated result. Nine rounds of Core 1.0 errata (`1.0.1`–`1.0.9`) and
|
|
40
|
+
a cross-implementation rework of References 2.0 error positions, all
|
|
41
|
+
fixing behaviour the specifications already required. The npm package
|
|
42
|
+
version advances to `0.4.0` to align with the Rust crate and Go module.
|
|
43
|
+
The Lima Core 1.0 and References 2.0 specification texts gain only
|
|
44
|
+
non-normative clarifications (§5, §5.2, §6.1.3, error-api); no rule
|
|
45
|
+
changed.
|
|
46
|
+
|
|
47
|
+
The shared conformance corpus grows to 211 Core and 136 References 2.0
|
|
48
|
+
cases (from 149 / 119). The frozen 1.0.0 baseline of 149 Core cases is
|
|
49
|
+
byte-identical; every added case carries a `since` revision marker.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- An unquoted key must now match the Core §5.1 grammar
|
|
54
|
+
(`[A-Za-z0-9_][A-Za-z0-9_:-]*`) in **every** mapping context — top level,
|
|
55
|
+
nested, and flow. Keys such as `a.b`, `($x)`, `${x}`, or a key with a
|
|
56
|
+
leading non-ASCII space are no longer recognised; the line or item is
|
|
57
|
+
skipped in both strict and non-strict mode. Previously some of these
|
|
58
|
+
were accepted as literal keys in nested or flow mappings. (errata 1.0.5)
|
|
59
|
+
- `parseCore` no longer builds reference-position metadata — that work is
|
|
60
|
+
now confined to the `parse` / positioned path — restoring the Core hot
|
|
61
|
+
path to its pre-errata baseline.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- `|` literal block scalars: recognised under a nested key, not only at the
|
|
66
|
+
top level; the scalar now ends at the first line indented to the key's
|
|
67
|
+
column or less, so a dedented comment or freetext is no longer absorbed;
|
|
68
|
+
indentation trimming removes exactly the smallest common indent (no
|
|
69
|
+
`key.length + 2` cap, a one-space indent is handled). (errata 1.0.1)
|
|
70
|
+
- Quoted-string lexing is escape-aware in every value position: `"a\""` and
|
|
71
|
+
`'a\''` are unterminated in flow and block contexts too; `"a: b": v` and
|
|
72
|
+
`"say \"hi\"": v` parse as a single key; an unquoted key with interior
|
|
73
|
+
whitespace makes its line or item unrecognised in nested and flow
|
|
74
|
+
mappings, matching the top level. (errata 1.0.2)
|
|
75
|
+
- A comment line between a bare key and its nested block no longer drops
|
|
76
|
+
the block. (errata 1.0.3)
|
|
77
|
+
- A block-sequence item's first bare key now aligns following sibling keys
|
|
78
|
+
at the key's own column, not the dash column. (errata 1.0.6)
|
|
79
|
+
- A raw U+000A inside a quoted key is rejected (Core §15.6), the `\n`
|
|
80
|
+
escape is unaffected, and — corrected in 1.0.9 — U+2028 / U+2029 are
|
|
81
|
+
ordinary quoted-key characters (§15.6 excludes only U+000A), matching
|
|
82
|
+
the Rust and Go implementations. (errata 1.0.8 / 1.0.9)
|
|
83
|
+
- A block-scalar line whose only content is a non-ASCII space (U+00A0,
|
|
84
|
+
U+2028, …) is no longer treated as blank in the Go implementation; it
|
|
85
|
+
ends the scalar, matching TypeScript and Rust. (errata 1.0.9)
|
|
86
|
+
- `\#` is collapsed to `#` only in unquoted values (Core §6.1.4). Inside
|
|
87
|
+
`"..."` it is now an unknown escape — non-strict keeps the backslash,
|
|
88
|
+
strict throws `INVALID_ESCAPE` — and inside `'...'` a literal backslash.
|
|
89
|
+
(errata 1.0.9)
|
|
90
|
+
- The whitespace between a value and a trailing `#` comment is stripped
|
|
91
|
+
with the full project whitespace set in all three implementations, so a
|
|
92
|
+
U+00A0 or U+FEFF before a comment no longer stays attached to the value
|
|
93
|
+
(Go and Rust previously used narrower sets). U+0085 is not in the set
|
|
94
|
+
and remains literal content. (errata 1.0.9)
|
|
95
|
+
- References 2.0 error positions: `line` and `column` are the physical
|
|
96
|
+
position in the original source text, counted in Unicode code points and
|
|
97
|
+
identical across the TypeScript, Rust, and Go implementations.
|
|
98
|
+
Leading-tab expansion and `\#` escapes no longer shift a later token's
|
|
99
|
+
reported column — including inside a flow `[...]` / `{...}`.
|
|
100
|
+
`LimaError.column` is now populated on reference errors.
|
|
101
|
+
- References 2.0 diagnostic ordering: an unresolved-reference or structural
|
|
102
|
+
error is attributed to the earliest participating source token,
|
|
103
|
+
including across flow-collection elements on one line and across a second
|
|
104
|
+
pure-reference copy.
|
|
105
|
+
|
|
15
106
|
## [0.3.1] — 2026-08-12
|
|
16
107
|
|
|
17
108
|
### Fixed
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core §6.1.5 literal block scalars (`|`). Shared by the top-level key path
|
|
3
|
+
* (`core.ts`) and the nested block path (`block.ts`): §6.1.5 defines block
|
|
4
|
+
* scalar extent generically ("indentation strictly greater than the
|
|
5
|
+
* indentation of the key that introduced the scalar"), with no top-level
|
|
6
|
+
* restriction, so both call sites must agree byte for byte.
|
|
7
|
+
*
|
|
8
|
+
* Generic over the output representation via `ValueBuilder` (see
|
|
9
|
+
* `builder.ts`), like the rest of the Core grammar.
|
|
10
|
+
*/
|
|
11
|
+
import { type ParseContext } from './normalize.js';
|
|
12
|
+
import type { ValueBuilder } from './builder.js';
|
|
13
|
+
/** Leading U+0020 count. "Indentation" in §6.1.5 is spaces, measured from column 0. */
|
|
14
|
+
export declare const leadingSpaces: (line: string) => number;
|
|
15
|
+
/** Index right after the last non-space character — the `trimEnd()` boundary, without allocating. */
|
|
16
|
+
export declare const trailingSpaceEnd: (line: string) => number;
|
|
17
|
+
/**
|
|
18
|
+
* Builds a `|` block scalar value.
|
|
19
|
+
*
|
|
20
|
+
* `bodyLines` are the physical source lines *after* the `|` line, each in
|
|
21
|
+
* its original column-0 form (trailing whitespace is stripped here).
|
|
22
|
+
* `keyIndent` is the indentation of the key that introduced the scalar
|
|
23
|
+
* (0 at the top level). `pipeLine` is the 1-based line number of the `|`
|
|
24
|
+
* line, so `bodyLines[i]` is at `pipeLine + 1 + i`.
|
|
25
|
+
*
|
|
26
|
+
* Returns the joined value plus how many of `bodyLines` the scalar
|
|
27
|
+
* consumed — a caller iterating physical lines resumes past that; a caller
|
|
28
|
+
* that already sliced the range to the scalar can ignore it.
|
|
29
|
+
*/
|
|
30
|
+
export declare const buildBlockScalar: <V, M>(bodyLines: string[], keyIndent: number, pipeLine: number, ctx: ParseContext, builder: ValueBuilder<V, M>) => {
|
|
31
|
+
value: V;
|
|
32
|
+
consumed: number;
|
|
33
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core §6.1.5 literal block scalars (`|`). Shared by the top-level key path
|
|
3
|
+
* (`core.ts`) and the nested block path (`block.ts`): §6.1.5 defines block
|
|
4
|
+
* scalar extent generically ("indentation strictly greater than the
|
|
5
|
+
* indentation of the key that introduced the scalar"), with no top-level
|
|
6
|
+
* restriction, so both call sites must agree byte for byte.
|
|
7
|
+
*
|
|
8
|
+
* Generic over the output representation via `ValueBuilder` (see
|
|
9
|
+
* `builder.ts`), like the rest of the Core grammar.
|
|
10
|
+
*/
|
|
11
|
+
import { LString } from './value.js';
|
|
12
|
+
import { checkScalarLimit } from './normalize.js';
|
|
13
|
+
/** Leading U+0020 count. "Indentation" in §6.1.5 is spaces, measured from column 0. */
|
|
14
|
+
export const leadingSpaces = (line) => {
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < line.length && line.charCodeAt(i) === 32)
|
|
17
|
+
i++;
|
|
18
|
+
return i;
|
|
19
|
+
};
|
|
20
|
+
/** Index right after the last non-space character — the `trimEnd()` boundary, without allocating. */
|
|
21
|
+
export const trailingSpaceEnd = (line) => {
|
|
22
|
+
let i = line.length;
|
|
23
|
+
while (i > 0 && line.charCodeAt(i - 1) === 32)
|
|
24
|
+
i--;
|
|
25
|
+
return i;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Builds a `|` block scalar value.
|
|
29
|
+
*
|
|
30
|
+
* `bodyLines` are the physical source lines *after* the `|` line, each in
|
|
31
|
+
* its original column-0 form (trailing whitespace is stripped here).
|
|
32
|
+
* `keyIndent` is the indentation of the key that introduced the scalar
|
|
33
|
+
* (0 at the top level). `pipeLine` is the 1-based line number of the `|`
|
|
34
|
+
* line, so `bodyLines[i]` is at `pipeLine + 1 + i`.
|
|
35
|
+
*
|
|
36
|
+
* Returns the joined value plus how many of `bodyLines` the scalar
|
|
37
|
+
* consumed — a caller iterating physical lines resumes past that; a caller
|
|
38
|
+
* that already sliced the range to the scalar can ignore it.
|
|
39
|
+
*/
|
|
40
|
+
export const buildBlockScalar = (bodyLines, keyIndent, pipeLine, ctx, builder) => {
|
|
41
|
+
// Extent (§6.1.5): a line belongs to the scalar iff its indentation is
|
|
42
|
+
// strictly greater than the introducing key's. Empty lines between
|
|
43
|
+
// content lines belong regardless of indentation; the first non-empty
|
|
44
|
+
// line dedented to the key's column or less ends the scalar, `#` lines
|
|
45
|
+
// included.
|
|
46
|
+
let consumed = bodyLines.length;
|
|
47
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
48
|
+
const bl = bodyLines[i];
|
|
49
|
+
const ind = leadingSpaces(bl);
|
|
50
|
+
if (ind === bl.length)
|
|
51
|
+
continue; // empty (blank or whitespace-only)
|
|
52
|
+
if (ind <= keyIndent) {
|
|
53
|
+
consumed = i;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const lines = consumed === bodyLines.length ? bodyLines : bodyLines.slice(0, consumed);
|
|
58
|
+
// Content indentation (§6.1.5): the smallest number of leading spaces
|
|
59
|
+
// among all non-empty content lines, measured from column 0, removed
|
|
60
|
+
// uniformly from every line.
|
|
61
|
+
let minIndent = Infinity;
|
|
62
|
+
for (const l of lines) {
|
|
63
|
+
const ind = leadingSpaces(l);
|
|
64
|
+
if (ind === l.length)
|
|
65
|
+
continue; // empty lines do not participate
|
|
66
|
+
if (ind < minIndent)
|
|
67
|
+
minIndent = ind;
|
|
68
|
+
}
|
|
69
|
+
const trimAmt = isFinite(minIndent) ? minIndent : 0;
|
|
70
|
+
// dedent + `^^` continuation strip + trailing-space trim collapse into a
|
|
71
|
+
// single slice() per line. leadingSpaces/trailingSpaceEnd avoid the
|
|
72
|
+
// temporary strings trim()/trimStart()/trimEnd() would allocate.
|
|
73
|
+
const merged = [];
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const l = lines[i];
|
|
76
|
+
const lineLen = l.length;
|
|
77
|
+
let start = trimAmt < lineLen ? trimAmt : lineLen;
|
|
78
|
+
const isContinuation = l.charCodeAt(start) === 94 && l.charCodeAt(start + 1) === 94; // ^^
|
|
79
|
+
if (isContinuation)
|
|
80
|
+
start += 2;
|
|
81
|
+
let end = trailingSpaceEnd(l);
|
|
82
|
+
if (end < start)
|
|
83
|
+
end = start;
|
|
84
|
+
const content = l.slice(start, end);
|
|
85
|
+
if (isContinuation && merged.length > 0) {
|
|
86
|
+
if (content)
|
|
87
|
+
merged[merged.length - 1] += ' ' + content;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
merged.push(content);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Trailing content (§6.1.5): all trailing empty lines are stripped.
|
|
94
|
+
while (merged.length > 0 && merged[merged.length - 1] === '')
|
|
95
|
+
merged.pop();
|
|
96
|
+
const joined = merged.join('\n');
|
|
97
|
+
checkScalarLimit(LString(joined), pipeLine);
|
|
98
|
+
// A token's physical `(line, offset)` is read from the original body
|
|
99
|
+
// lines (column-0 form), not reconstructed from the dedented/`^^`-merged
|
|
100
|
+
// `joined` string — References 2.0 §2.4. `lines` are at physical lines
|
|
101
|
+
// `pipeLine + 1 .. pipeLine + consumed` (0-based indices `pipeLine ..`).
|
|
102
|
+
return {
|
|
103
|
+
value: builder.string(joined, pipeLine + 1, false, {
|
|
104
|
+
raw: lines.join('\n'), line: pipeLine + 1, col: 0,
|
|
105
|
+
tabAdjust: ctx.tabAdjust?.slice(pipeLine, pipeLine + consumed),
|
|
106
|
+
}),
|
|
107
|
+
consumed,
|
|
108
|
+
};
|
|
109
|
+
};
|
package/dist/block.js
CHANGED
|
@@ -8,22 +8,72 @@
|
|
|
8
8
|
* reference-shape check — that error class cannot occur here since Core
|
|
9
9
|
* never resolves a reference in the first place).
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { stripKeyQuotes, unescapeDQ, stripComment, parseQuotedOrTyped, } from './scalars.js';
|
|
11
|
+
import { checkKeyLength, checkDuplicateKey, NESTING_DEPTH_LIMIT } from './normalize.js';
|
|
12
|
+
import { stripKeyQuotes, stripComment, stripCommentKeepEscapes, parseQuotedOrTyped, closingQuoteIndex, isValidKey, positionedBuilder, } from './scalars.js';
|
|
14
13
|
import { parseFlowMapping, parseFlowOrScalarValue } from './flow.js';
|
|
14
|
+
import { buildBlockScalar } from './block-scalar.js';
|
|
15
15
|
import { LimaError } from './errors.js';
|
|
16
16
|
import { isTrimWhitespace } from './chars.js';
|
|
17
17
|
import { BlockCursor } from './block-cursor.js';
|
|
18
18
|
const DASH_PREFIX_RE = /^-\s+/;
|
|
19
|
+
/**
|
|
20
|
+
* A key's inline value text. If it is exactly `|`, consume the following
|
|
21
|
+
* physical lines belonging to the Core §6.1.5 block scalar introduced by a
|
|
22
|
+
* key at `keyIndent` and return the scalar; the cursor is left on the first
|
|
23
|
+
* line past the scalar. Otherwise parse `raw` as an ordinary inline value
|
|
24
|
+
* and advance one line. §6.1.5 places no top-level restriction on block
|
|
25
|
+
* scalars, so this is the same primitive `core.ts` uses at the top level.
|
|
26
|
+
*/
|
|
27
|
+
/** Length of the `- ` (dash + whitespace) prefix on the cursor's current line. */
|
|
28
|
+
const dashPrefixLen = (cursor) => {
|
|
29
|
+
const start = cursor.contentStart, end = cursor.lineEnd;
|
|
30
|
+
if (start + 1 === end)
|
|
31
|
+
return 1;
|
|
32
|
+
let content = start + 1;
|
|
33
|
+
while (content < end && isTrimWhitespace(cursor.source.charCodeAt(content)))
|
|
34
|
+
content++;
|
|
35
|
+
return content - start;
|
|
36
|
+
};
|
|
37
|
+
/** Codepoints of leading whitespace on `s` — for a value's column within its line. */
|
|
38
|
+
const leadingWsLen = (s) => s.length - s.trimStart().length;
|
|
39
|
+
/** A value's physical `raw` for References 2.0 token positions: a trailing
|
|
40
|
+
* `#` comment removed (its `${…}`-shaped text is not part of the value, so
|
|
41
|
+
* must not be scanned), but `\#` kept as written (§2.4 — a `\#` before a
|
|
42
|
+
* token still occupies two source columns). */
|
|
43
|
+
const physicalRaw = (s) => (s.includes('#') ? stripCommentKeepEscapes(s) : s);
|
|
44
|
+
/** The `ReferenceSource` for an inline value on line `keyLine` at column
|
|
45
|
+
* `valueCol`; `rawUndecoded` is the value text before comment stripping /
|
|
46
|
+
* `\#` collapse (so a `\#` before a token doesn't shift its column). */
|
|
47
|
+
const inlineSource = (wantSource, ctx, keyLine, valueCol, rawUndecoded) => wantSource ? ({
|
|
48
|
+
raw: physicalRaw(rawUndecoded), line: keyLine, col: valueCol,
|
|
49
|
+
tabAdjust: ctx.tabAdjust ? [ctx.tabAdjust[keyLine - 1] ?? 0] : undefined,
|
|
50
|
+
}) : undefined;
|
|
51
|
+
const inlineOrBlockScalar = (raw, keyIndent, keyLine, valueCol, rawUndecoded, cursor, ctx, builder) => {
|
|
52
|
+
const wantSource = builder === positionedBuilder;
|
|
53
|
+
if (raw !== '|') {
|
|
54
|
+
const value = parseFlowOrScalarValue(raw, ctx, keyLine, builder, inlineSource(wantSource, ctx, keyLine, valueCol, rawUndecoded));
|
|
55
|
+
cursor.next();
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
cursor.next(); // past the `key: |` line
|
|
59
|
+
const bodyLines = [];
|
|
60
|
+
while (cursor.valid) {
|
|
61
|
+
if (!cursor.empty && cursor.asciiIndent <= keyIndent)
|
|
62
|
+
break;
|
|
63
|
+
bodyLines.push(cursor.source.slice(cursor.lineStart, cursor.lineEnd));
|
|
64
|
+
cursor.next();
|
|
65
|
+
}
|
|
66
|
+
return buildBlockScalar(bodyLines, keyIndent, keyLine, ctx, builder).value;
|
|
67
|
+
};
|
|
19
68
|
export const findKeySep = (s) => {
|
|
20
69
|
const first = s.charCodeAt(0);
|
|
21
70
|
if (first === 39 || first === 34) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
71
|
+
// §5.1: the `: ` separator must sit outside the quoted key, so scan
|
|
72
|
+
// past its closing quote first. Double-quoted keys honour `\"`; a
|
|
73
|
+
// single-quoted key is literal (§5.2), so its first `'` closes.
|
|
74
|
+
const close = closingQuoteIndex(s, false);
|
|
75
|
+
if (close !== -1 && s.charCodeAt(close + 1) === 58 && s.charCodeAt(close + 2) === 32)
|
|
76
|
+
return close + 1;
|
|
27
77
|
return -1;
|
|
28
78
|
}
|
|
29
79
|
return s.indexOf(': ');
|
|
@@ -52,6 +102,19 @@ const trimSlice = (source, start, end) => {
|
|
|
52
102
|
return source.slice(start, end);
|
|
53
103
|
};
|
|
54
104
|
const cursorContent = (cursor) => cursor.source.slice(cursor.contentStart, cursor.lineEnd);
|
|
105
|
+
/**
|
|
106
|
+
* Core §4 rule 7 / §6.1.3: comment lines (first non-whitespace character
|
|
107
|
+
* `#`) do not affect base indentation and are skipped — including in the
|
|
108
|
+
* lookahead that decides whether a bare key (`key:` with no inline value)
|
|
109
|
+
* has any nested block at all. Advancing past a comment here, not only a
|
|
110
|
+
* blank line, keeps that pre-check consistent with the same skip already
|
|
111
|
+
* applied once inside an established block (`parseCursorBlock`'s own
|
|
112
|
+
* `cursor.empty || cursor.firstCode === 35` check at the top of its loop).
|
|
113
|
+
*/
|
|
114
|
+
const skipEmptyAndCommentLines = (cursor) => {
|
|
115
|
+
while (cursor.valid && (cursor.empty || cursor.firstCode === 35))
|
|
116
|
+
cursor.next();
|
|
117
|
+
};
|
|
55
118
|
const cursorAfterDash = (cursor) => {
|
|
56
119
|
const start = cursor.contentStart;
|
|
57
120
|
const end = cursor.lineEnd;
|
|
@@ -66,6 +129,7 @@ const cursorAfterDash = (cursor) => {
|
|
|
66
129
|
};
|
|
67
130
|
/** Shared block grammar consuming one mutable physical-line cursor. */
|
|
68
131
|
const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
132
|
+
const wantSource = builder === positionedBuilder;
|
|
69
133
|
let items = null;
|
|
70
134
|
let entries = null;
|
|
71
135
|
let pendingItem = null;
|
|
@@ -82,22 +146,30 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
82
146
|
if (indent > baseIndent) {
|
|
83
147
|
const trimmed = cursorContent(cursor);
|
|
84
148
|
if (items !== null && pendingItem !== null) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
builder.setMapping(pendingItem, key, parseFlowOrScalarValue(raw, ctx, line, builder));
|
|
149
|
+
const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
|
|
150
|
+
const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
|
|
151
|
+
const keyRaw = colonPos !== -1
|
|
152
|
+
? trimSlice(trimmed, 0, colonPos)
|
|
153
|
+
: bareKey ? trimSlice(trimmed, 0, trimmed.length - 1) : '';
|
|
154
|
+
if (keyRaw !== '' && !isValidKey(keyRaw)) {
|
|
155
|
+
// §5.1: not a usable key — skipped in both modes, as at the top level.
|
|
93
156
|
cursor.next();
|
|
94
157
|
}
|
|
95
|
-
else if (
|
|
96
|
-
const key = stripKeyQuotes(
|
|
97
|
-
checkKeyLength(key,
|
|
158
|
+
else if (colonPos !== -1) {
|
|
159
|
+
const key = stripKeyQuotes(keyRaw, ctx.strict, line);
|
|
160
|
+
checkKeyLength(key, line);
|
|
161
|
+
const rawUndecoded = trimSlice(trimmed, colonPos + 2, trimmed.length);
|
|
162
|
+
let raw = rawUndecoded;
|
|
163
|
+
if (raw !== '|' && raw.includes('#'))
|
|
164
|
+
raw = stripComment(raw);
|
|
165
|
+
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));
|
|
167
|
+
}
|
|
168
|
+
else if (bareKey) {
|
|
169
|
+
const key = stripKeyQuotes(keyRaw, ctx.strict, line);
|
|
170
|
+
checkKeyLength(key, line);
|
|
98
171
|
cursor.next();
|
|
99
|
-
|
|
100
|
-
cursor.next();
|
|
172
|
+
skipEmptyAndCommentLines(cursor);
|
|
101
173
|
if (cursor.valid && cursor.indent > indent) {
|
|
102
174
|
const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
|
|
103
175
|
builder.setMapping(pendingItem, key, nested ?? builder.null(line));
|
|
@@ -134,17 +206,23 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
134
206
|
cursor.next();
|
|
135
207
|
continue;
|
|
136
208
|
}
|
|
137
|
-
|
|
209
|
+
const afterDashRaw = cursorAfterDash(cursor);
|
|
210
|
+
let afterDash = afterDashRaw;
|
|
138
211
|
if (afterDash.includes('#'))
|
|
139
212
|
afterDash = stripComment(afterDash);
|
|
213
|
+
const afterDashCol = wantSource ? cursor.indent + dashPrefixLen(cursor) : 0;
|
|
214
|
+
const afterDashTab = wantSource && ctx.tabAdjust ? [ctx.tabAdjust[line - 1] ?? 0] : undefined;
|
|
215
|
+
const dashSource = wantSource ? ({
|
|
216
|
+
raw: physicalRaw(afterDashRaw), line, col: afterDashCol, tabAdjust: afterDashTab,
|
|
217
|
+
}) : undefined;
|
|
140
218
|
const first = afterDash.charCodeAt(0);
|
|
141
219
|
if (first !== 34 && first !== 39 && first !== 45 && first !== 123 &&
|
|
142
220
|
afterDash.indexOf(': ') === -1 && !afterDash.endsWith(':')) {
|
|
143
|
-
items.push(parseQuotedOrTyped(afterDash, ctx, line,
|
|
221
|
+
items.push(parseQuotedOrTyped(afterDash, ctx, line, builder, dashSource));
|
|
144
222
|
cursor.next();
|
|
145
223
|
continue;
|
|
146
224
|
}
|
|
147
|
-
const flowMap = parseFlowMapping(afterDash, ctx, line, builder);
|
|
225
|
+
const flowMap = parseFlowMapping(afterDash, ctx, line, builder, dashSource);
|
|
148
226
|
const colonPos = findKeySep(afterDash);
|
|
149
227
|
if (flowMap !== null) {
|
|
150
228
|
items.push(flowMap);
|
|
@@ -166,53 +244,60 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
166
244
|
cursor.next();
|
|
167
245
|
}
|
|
168
246
|
}
|
|
169
|
-
else if (colonPos !== -1) {
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
? afterDash.slice(0, colonPos) : trimSlice(afterDash, 0, colonPos);
|
|
174
|
-
const key = keyFirst === 34 || keyFirst === 39 ? stripKeyQuotes(keyRaw) : keyRaw;
|
|
175
|
-
checkKeyLength(key, () => line);
|
|
247
|
+
else if (colonPos !== -1 && isValidKey(trimSlice(afterDash, 0, colonPos))) {
|
|
248
|
+
const keyRaw = trimSlice(afterDash, 0, colonPos);
|
|
249
|
+
const key = stripKeyQuotes(keyRaw, ctx.strict, line);
|
|
250
|
+
checkKeyLength(key, line);
|
|
176
251
|
const valueStart = colonPos + 2;
|
|
177
252
|
const valueFirst = afterDash.charCodeAt(valueStart);
|
|
178
253
|
const valueLast = afterDash.charCodeAt(afterDash.length - 1);
|
|
179
254
|
const raw = valueFirst > 0x20 && valueFirst < 0x7f && valueLast > 0x20 && valueLast < 0x7f
|
|
180
255
|
? afterDash.slice(valueStart) : trimSlice(afterDash, valueStart, afterDash.length);
|
|
181
|
-
|
|
182
|
-
|
|
256
|
+
// The key sits after `- `, two columns past the dash.
|
|
257
|
+
const valueCol = wantSource ? afterDashCol + valueStart + leadingWsLen(afterDash.slice(valueStart)) : 0;
|
|
258
|
+
pendingItem = builder.createMappingWith(key, inlineOrBlockScalar(raw, baseIndent + 2, line, valueCol, wantSource ? afterDashRaw.slice(valueStart) : raw, cursor, ctx, builder));
|
|
183
259
|
while (cursor.valid && cursor.indent > baseIndent) {
|
|
184
260
|
const continuationLine = baseLine + cursor.lineIndex;
|
|
185
|
-
const
|
|
186
|
-
const cfirst = cursor.source.charCodeAt(
|
|
187
|
-
if (cfirst ===
|
|
261
|
+
const ckeyIndent = cursor.asciiIndent;
|
|
262
|
+
const cfirst = cursor.source.charCodeAt(cursor.contentStart);
|
|
263
|
+
if (cfirst === 35)
|
|
264
|
+
break;
|
|
265
|
+
const contLine = trimSlice(cursor.source, cursor.contentStart, cursor.lineEnd);
|
|
266
|
+
const csep = findKeySep(contLine);
|
|
267
|
+
if (csep === -1)
|
|
188
268
|
break;
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
269
|
+
const ckeyRaw = trimSlice(contLine, 0, csep);
|
|
270
|
+
if (!isValidKey(ckeyRaw))
|
|
191
271
|
break;
|
|
192
|
-
const
|
|
193
|
-
const ckey = cfirst > 0x20 && cfirst < 0x7f && keyLast > 0x20 && keyLast < 0x7f
|
|
194
|
-
? cursor.source.slice(start, sep) : trimSlice(cursor.source, start, sep);
|
|
272
|
+
const ckey = stripKeyQuotes(ckeyRaw, ctx.strict, continuationLine);
|
|
195
273
|
if (!ckey)
|
|
196
274
|
break;
|
|
197
|
-
checkKeyLength(ckey,
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let value = valueFirst > 0x20 && valueFirst < 0x7f && valueLast > 0x20 && valueLast < 0x7f
|
|
202
|
-
? cursor.source.slice(valueStart, end) : trimSlice(cursor.source, valueStart, end);
|
|
203
|
-
if (value.includes('#'))
|
|
275
|
+
checkKeyLength(ckey, continuationLine);
|
|
276
|
+
const valueUndecoded = trimSlice(contLine, csep + 2, contLine.length);
|
|
277
|
+
let value = valueUndecoded;
|
|
278
|
+
if (value !== '|' && value.includes('#'))
|
|
204
279
|
value = stripComment(value);
|
|
205
|
-
|
|
206
|
-
|
|
280
|
+
// contLine starts exactly at contentStart (no leading trim), so
|
|
281
|
+
// the value's column is the indent plus its offset in contLine.
|
|
282
|
+
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));
|
|
207
284
|
}
|
|
208
285
|
}
|
|
209
|
-
else if (afterDash.endsWith(':')) {
|
|
210
|
-
const key = stripKeyQuotes(trimSlice(afterDash, 0, afterDash.length - 1));
|
|
211
|
-
checkKeyLength(key,
|
|
286
|
+
else if (afterDash.endsWith(':') && isValidKey(trimSlice(afterDash, 0, afterDash.length - 1))) {
|
|
287
|
+
const key = stripKeyQuotes(trimSlice(afterDash, 0, afterDash.length - 1), ctx.strict, line);
|
|
288
|
+
checkKeyLength(key, line);
|
|
289
|
+
// §7.1 rule 3 / §7.2: sibling keys of an object item align at
|
|
290
|
+
// the column of the first key *after* `- `; a nested block
|
|
291
|
+
// must be indented deeper than that column, not merely deeper
|
|
292
|
+
// than the item's dash. A line at the key's own column is the
|
|
293
|
+
// next sibling key, so the bare key is `null`.
|
|
294
|
+
let keyColumn = cursor.contentStart + 1;
|
|
295
|
+
while (keyColumn < cursor.lineEnd && isTrimWhitespace(cursor.source.charCodeAt(keyColumn)))
|
|
296
|
+
keyColumn++;
|
|
297
|
+
keyColumn -= cursor.lineStart;
|
|
212
298
|
cursor.next();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (cursor.valid && cursor.indent > baseIndent) {
|
|
299
|
+
skipEmptyAndCommentLines(cursor);
|
|
300
|
+
if (cursor.valid && cursor.indent > keyColumn) {
|
|
216
301
|
const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
|
|
217
302
|
pendingItem = builder.createMappingWith(key, nested ?? builder.null(line));
|
|
218
303
|
}
|
|
@@ -220,15 +305,9 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
220
305
|
pendingItem = builder.createMappingWith(key, builder.null(line));
|
|
221
306
|
}
|
|
222
307
|
else {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const value = q === 34 ? unescapeDQ(inner, ctx.strict, line) : inner.replace(/\\'/g, "'");
|
|
227
|
-
checkScalarLimit(LString(value), line);
|
|
228
|
-
items.push(builder.string(value, line, true));
|
|
229
|
-
}
|
|
230
|
-
else
|
|
231
|
-
items.push(parseQuotedOrTyped(afterDash, ctx, line, false, builder));
|
|
308
|
+
// A quoted-or-typed scalar item — parseQuotedOrTyped enforces
|
|
309
|
+
// §10.1's unterminated / trailing-content strict checks.
|
|
310
|
+
items.push(parseQuotedOrTyped(afterDash, ctx, line, builder, dashSource));
|
|
232
311
|
cursor.next();
|
|
233
312
|
}
|
|
234
313
|
}
|
|
@@ -241,30 +320,39 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
|
241
320
|
cursor.next();
|
|
242
321
|
continue;
|
|
243
322
|
}
|
|
244
|
-
const
|
|
245
|
-
|
|
323
|
+
const bareKey = trimmed.charCodeAt(trimmed.length - 1) === 58;
|
|
324
|
+
const colonPos = bareKey && trimmed.indexOf(' ') === -1 ? -1 : findKeySep(trimmed);
|
|
325
|
+
const keyRaw = colonPos !== -1
|
|
326
|
+
? trimSlice(trimmed, 0, colonPos)
|
|
327
|
+
: bareKey ? trimSlice(trimmed, 0, trimmed.length - 1) : '';
|
|
328
|
+
if (keyRaw !== '' && !isValidKey(keyRaw)) {
|
|
329
|
+
// §5.1: not a usable key — unrecognised line, skipped in both
|
|
330
|
+
// modes (§10's strict list is closed and does not cover this).
|
|
331
|
+
cursor.next();
|
|
332
|
+
}
|
|
333
|
+
else if (colonPos !== -1) {
|
|
246
334
|
if (entries === null)
|
|
247
335
|
entries = builder.createMapping();
|
|
248
|
-
const key = stripKeyQuotes(
|
|
249
|
-
checkKeyLength(key,
|
|
336
|
+
const key = stripKeyQuotes(keyRaw, ctx.strict, line);
|
|
337
|
+
checkKeyLength(key, line);
|
|
250
338
|
if (ctx.strict || ctx.onWarning !== undefined)
|
|
251
339
|
checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
|
|
252
|
-
|
|
253
|
-
|
|
340
|
+
const rawUndecoded = trimSlice(trimmed, colonPos + 2, trimmed.length);
|
|
341
|
+
let raw = rawUndecoded;
|
|
342
|
+
if (raw !== '|' && raw.includes('#'))
|
|
254
343
|
raw = stripComment(raw);
|
|
255
|
-
|
|
256
|
-
|
|
344
|
+
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));
|
|
257
346
|
}
|
|
258
|
-
else if (
|
|
347
|
+
else if (bareKey) {
|
|
259
348
|
if (entries === null)
|
|
260
349
|
entries = builder.createMapping();
|
|
261
|
-
const key = stripKeyQuotes(
|
|
262
|
-
checkKeyLength(key,
|
|
350
|
+
const key = stripKeyQuotes(keyRaw, ctx.strict, line);
|
|
351
|
+
checkKeyLength(key, line);
|
|
263
352
|
if (ctx.strict || ctx.onWarning !== undefined)
|
|
264
353
|
checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
|
|
265
354
|
cursor.next();
|
|
266
|
-
|
|
267
|
-
cursor.next();
|
|
355
|
+
skipEmptyAndCommentLines(cursor);
|
|
268
356
|
if (cursor.valid && cursor.indent > baseIndent) {
|
|
269
357
|
const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
|
|
270
358
|
builder.setMapping(entries, key, nested ?? builder.null(line));
|
|
@@ -289,8 +377,7 @@ export const parseBlockRange = (source, start, end, ctx, baseLine, builder, dept
|
|
|
289
377
|
const cursor = new BlockCursor(source, start, end);
|
|
290
378
|
if (!cursor.next())
|
|
291
379
|
return null;
|
|
292
|
-
|
|
293
|
-
cursor.next();
|
|
380
|
+
skipEmptyAndCommentLines(cursor);
|
|
294
381
|
if (!cursor.valid)
|
|
295
382
|
return null;
|
|
296
383
|
const baseIndent = cursor.asciiIndent;
|
package/dist/builder.d.ts
CHANGED
|
@@ -24,14 +24,19 @@
|
|
|
24
24
|
* is compile-time-only: generics are erased, so the emitted JS — and
|
|
25
25
|
* therefore performance — is identical either way, verified by benchmark.
|
|
26
26
|
*/
|
|
27
|
+
import type { ReferenceSource } from './reference-tokens2.js';
|
|
27
28
|
export interface ValueBuilder<V, M = Map<string, V>> {
|
|
28
|
-
/** Opt-in for References' annotated builder; native Core avoids source-map work. */
|
|
29
|
-
readonly tracksStringSourcePositions?: boolean;
|
|
30
29
|
null(line: number): V;
|
|
31
30
|
bool(value: boolean, line: number): V;
|
|
32
31
|
int(value: number, line: number): V;
|
|
33
32
|
float(value: number, line: number): V;
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* `source` (when given, for unquoted strings that may hold References 2.0
|
|
35
|
+
* tokens) carries the physical position needed to attribute those tokens
|
|
36
|
+
* to a real `(line, offset)` in the source text (§2.4). `nativeBuilder`
|
|
37
|
+
* ignores it.
|
|
38
|
+
*/
|
|
39
|
+
string(value: string, line: number, quoted: boolean, source?: ReferenceSource): V;
|
|
35
40
|
instant(value: Date, line: number): V;
|
|
36
41
|
array(items: V[], line: number): V;
|
|
37
42
|
createMapping(): M;
|
|
@@ -41,9 +46,3 @@ export interface ValueBuilder<V, M = Map<string, V>> {
|
|
|
41
46
|
mappingMaxDepth(entries: M, depthOf: (value: V) => number): number;
|
|
42
47
|
mapping(entries: M, line: number): V;
|
|
43
48
|
}
|
|
44
|
-
/** Maps a span in a decoded scalar back to its physical source line/column. */
|
|
45
|
-
export type StringSourceSpan = {
|
|
46
|
-
start: number;
|
|
47
|
-
line: number;
|
|
48
|
-
sourceOffset: number;
|
|
49
|
-
};
|