@limaformat/lima 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -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/positioned-tree.d.ts +73 -0
- package/dist/positioned-tree.js +123 -0
- package/dist/reference-tokens2.d.ts +34 -3
- package/dist/reference-tokens2.js +56 -23
- package/dist/references.d.ts +1 -64
- package/dist/references.js +2 -113
- 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 +22 -24
package/dist/core.js
CHANGED
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* `flow.ts` (`[...]`/`{...}`), and `block.ts` (indentation-based sequences
|
|
19
19
|
* and mappings) — this file is the orchestrator that ties them together.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { unescapeDQ, stripComment, positionedBuilder, NO_SPAN_VALUE, parseSimpleScalarSpan, } from './scalars.js';
|
|
21
|
+
import { checkKeyLength, byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, } from './normalize.js';
|
|
22
|
+
import { unescapeDQ, stripComment, stripCommentKeepEscapes, positionedBuilder, NO_SPAN_VALUE, parseSimpleScalarSpan, } from './scalars.js';
|
|
24
23
|
import { parseFlowOrScalarValue } from './flow.js';
|
|
25
24
|
import { parseBlockRange } from './block.js';
|
|
25
|
+
import { buildBlockScalar } from './block-scalar.js';
|
|
26
26
|
import { LimaError } from './errors.js';
|
|
27
27
|
import { KeyCursor, scanKeys } from './scanner.js';
|
|
28
28
|
export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
|
|
@@ -68,19 +68,6 @@ const depthOfNative = (v) => {
|
|
|
68
68
|
return 0;
|
|
69
69
|
};
|
|
70
70
|
const SPACE_BEFORE_COLON_RE = /^(?:'[^']*'|"(?:[^"\\]|\\.)*")[ \t]+:/;
|
|
71
|
-
const leadingSpaces = (line) => {
|
|
72
|
-
let i = 0;
|
|
73
|
-
while (i < line.length && line.charCodeAt(i) === 32)
|
|
74
|
-
i++;
|
|
75
|
-
return i;
|
|
76
|
-
};
|
|
77
|
-
/** Index right after the last non-space character — the `trimEnd()` boundary, without allocating. */
|
|
78
|
-
const trailingSpaceEnd = (line) => {
|
|
79
|
-
let i = line.length;
|
|
80
|
-
while (i > 0 && line.charCodeAt(i - 1) === 32)
|
|
81
|
-
i--;
|
|
82
|
-
return i;
|
|
83
|
-
};
|
|
84
71
|
const lineAt = (s, pos) => {
|
|
85
72
|
let n = 1;
|
|
86
73
|
for (let i = 0; i < pos; i++)
|
|
@@ -136,7 +123,14 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
136
123
|
if (frontMatter.includes('\r'))
|
|
137
124
|
frontMatter = frontMatter.replace(/\r\n|\r/g, '\n');
|
|
138
125
|
if (frontMatter.includes('\t')) {
|
|
139
|
-
|
|
126
|
+
// Record how many columns each line's leading whitespace grew, so a
|
|
127
|
+
// References 2.0 token can be reported at its *original* column (§2.4).
|
|
128
|
+
ctx.tabAdjust = [];
|
|
129
|
+
frontMatter = frontMatter.replace(/^([ \t]*)/gm, (leading) => {
|
|
130
|
+
const expanded = leading.includes('\t') ? leading.replace(/\t/g, ' ') : leading;
|
|
131
|
+
ctx.tabAdjust.push(expanded.length - leading.length);
|
|
132
|
+
return expanded;
|
|
133
|
+
});
|
|
140
134
|
}
|
|
141
135
|
if (frontMatter.includes(' \n') || frontMatter.endsWith(' ')) {
|
|
142
136
|
// Capture-and-reinsert instead of a lookahead: `( +)(\n|$)` replaced
|
|
@@ -163,7 +157,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
163
157
|
}
|
|
164
158
|
}
|
|
165
159
|
const parseEntry = (key, line, rawStart, isBlock, inlineEnd, nextStart) => {
|
|
166
|
-
checkKeyLength(key,
|
|
160
|
+
checkKeyLength(key, line);
|
|
167
161
|
if ((strict || ctx.onWarning !== undefined) && builder.hasMappingKey(root, key)) {
|
|
168
162
|
const diagnostic = {
|
|
169
163
|
code: 'DUPLICATE_KEY', line, key,
|
|
@@ -178,6 +172,14 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
178
172
|
}
|
|
179
173
|
const firstNewline = inlineEnd ?? frontMatter.indexOf('\n', rawStart);
|
|
180
174
|
const newlineInRange = firstNewline < nextStart;
|
|
175
|
+
// Physical codepoint column of the value's first character in its
|
|
176
|
+
// source line — the anchor for References 2.0 token positions (§2.4).
|
|
177
|
+
// References 2.0 token positions (§2.4): only build the source anchor
|
|
178
|
+
// for the annotated builder; `parseCore` (native) discards it.
|
|
179
|
+
const wantSource = builder === positionedBuilder;
|
|
180
|
+
const valueLineStart = wantSource ? frontMatter.lastIndexOf('\n', rawStart - 1) + 1 : 0;
|
|
181
|
+
const valueCol = wantSource ? [...frontMatter.slice(valueLineStart, rawStart)].length : 0;
|
|
182
|
+
const lineTabAdjust = wantSource && ctx.tabAdjust ? [ctx.tabAdjust[line - 1] ?? 0] : undefined;
|
|
181
183
|
// Typical frontmatter uses one inline scalar per key. Avoid building
|
|
182
184
|
// and filling a temporary lines array for that overwhelmingly common
|
|
183
185
|
// case; the scanner-delimited raw slice contains at most its terminal
|
|
@@ -190,8 +192,15 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
190
192
|
return;
|
|
191
193
|
}
|
|
192
194
|
const val = frontMatter.slice(rawStart, valueEnd);
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
+
const hasHash = val.includes('#');
|
|
196
|
+
const uncommented = hasHash ? stripComment(val) : val;
|
|
197
|
+
// The physical anchor keeps `\#` but drops a real comment, so a
|
|
198
|
+
// `${…}`-shaped token inside a trailing comment is not scanned (§2.4).
|
|
199
|
+
const source = wantSource ? {
|
|
200
|
+
raw: hasHash ? stripCommentKeepEscapes(val) : val,
|
|
201
|
+
line, col: valueCol, tabAdjust: lineTabAdjust,
|
|
202
|
+
} : undefined;
|
|
203
|
+
builder.setMapping(root, key, parseFlowOrScalarValue(uncommented, ctx, line, builder, source));
|
|
195
204
|
return;
|
|
196
205
|
}
|
|
197
206
|
if (isBlock) {
|
|
@@ -217,77 +226,22 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
217
226
|
const line0Trimmed = lines[0].trim();
|
|
218
227
|
if (lines.length === 1 || line0Trimmed !== '|') {
|
|
219
228
|
const line0 = lines[0];
|
|
220
|
-
const
|
|
221
|
-
|
|
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));
|
|
222
236
|
return;
|
|
223
237
|
}
|
|
224
|
-
// Multi-line string (`|` literal block scalar).
|
|
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.
|
|
225
243
|
const bodyLines = raw.slice(raw.indexOf('\n') + 1).split('\n');
|
|
226
|
-
|
|
227
|
-
// string just to measure or strip whitespace. leadingSpaces/
|
|
228
|
-
// trailingSpaceEnd compute the same boundaries by scanning char
|
|
229
|
-
// codes, and dedent+continuation-strip+trailing-trim collapse
|
|
230
|
-
// into a single final slice() per line instead of up to three.
|
|
231
|
-
let minIndent = Infinity;
|
|
232
|
-
for (const bodyLine of bodyLines) {
|
|
233
|
-
const indent = leadingSpaces(bodyLine);
|
|
234
|
-
if (indent === bodyLine.length)
|
|
235
|
-
continue; // blank (all spaces, or empty)
|
|
236
|
-
if (indent < minIndent)
|
|
237
|
-
minIndent = indent;
|
|
238
|
-
}
|
|
239
|
-
minIndent = Math.min(minIndent, key.length + 2);
|
|
240
|
-
const trimAmt = minIndent > 1 && isFinite(minIndent) ? minIndent : 0;
|
|
241
|
-
const mergedLines = [];
|
|
242
|
-
const mergedLineSpans = builder.tracksStringSourcePositions ? [] : undefined;
|
|
243
|
-
for (let bodyIndex = 0; bodyIndex < bodyLines.length; bodyIndex++) {
|
|
244
|
-
const bodyLine = bodyLines[bodyIndex];
|
|
245
|
-
const lineLen = bodyLine.length;
|
|
246
|
-
let start = trimAmt < lineLen ? trimAmt : lineLen;
|
|
247
|
-
const isContinuation = bodyLine.charCodeAt(start) === 94 && bodyLine.charCodeAt(start + 1) === 94; // ^^
|
|
248
|
-
if (isContinuation)
|
|
249
|
-
start += 2;
|
|
250
|
-
let end = trailingSpaceEnd(bodyLine);
|
|
251
|
-
if (end < start)
|
|
252
|
-
end = start;
|
|
253
|
-
const content = bodyLine.slice(start, end);
|
|
254
|
-
if (isContinuation) {
|
|
255
|
-
if (mergedLines.length > 0) {
|
|
256
|
-
if (content) {
|
|
257
|
-
const outputStart = mergedLines[mergedLines.length - 1].length + 1;
|
|
258
|
-
mergedLines[mergedLines.length - 1] += ' ' + content;
|
|
259
|
-
mergedLineSpans?.[mergedLineSpans.length - 1].push({
|
|
260
|
-
start: outputStart, line: line + 1 + bodyIndex, sourceOffset: start,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
mergedLines.push(content);
|
|
266
|
-
mergedLineSpans?.push(content ? [{ start: 0, line: line + 1 + bodyIndex, sourceOffset: start }] : []);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
mergedLines.push(content);
|
|
271
|
-
mergedLineSpans?.push(content ? [{ start: 0, line: line + 1 + bodyIndex, sourceOffset: start }] : []);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
while (mergedLines.length > 0 && mergedLines[mergedLines.length - 1] === '') {
|
|
275
|
-
mergedLines.pop();
|
|
276
|
-
mergedLineSpans?.pop();
|
|
277
|
-
}
|
|
278
|
-
const joined = mergedLines.join('\n');
|
|
279
|
-
let sourceSpans;
|
|
280
|
-
if (mergedLineSpans) {
|
|
281
|
-
sourceSpans = [];
|
|
282
|
-
let outputStart = 0;
|
|
283
|
-
for (let i = 0; i < mergedLines.length; i++) {
|
|
284
|
-
for (const span of mergedLineSpans[i])
|
|
285
|
-
sourceSpans.push({ ...span, start: outputStart + span.start });
|
|
286
|
-
outputStart += mergedLines[i].length + 1;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
checkScalarLimit(LString(joined), line);
|
|
290
|
-
builder.setMapping(root, key, builder.string(joined, line + 1, false, sourceSpans));
|
|
244
|
+
builder.setMapping(root, key, buildBlockScalar(bodyLines, 0, line, ctx, builder).value);
|
|
291
245
|
}
|
|
292
246
|
};
|
|
293
247
|
// A recognised top-level entry needs at least one UTF-16 unit of key text,
|
|
@@ -336,7 +290,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
336
290
|
for (let i = 0; i < keyCount; i++) {
|
|
337
291
|
const m = matches[i];
|
|
338
292
|
const rawDQ = m.doubleQuotedRaw;
|
|
339
|
-
const key = m.unquoted ?? m.singleQuoted ?? (rawDQ !== undefined ? unescapeDQ(rawDQ) : undefined);
|
|
293
|
+
const key = m.unquoted ?? m.singleQuoted ?? (rawDQ !== undefined ? unescapeDQ(rawDQ, strict, m.line) : undefined);
|
|
340
294
|
if (key !== undefined)
|
|
341
295
|
parseEntry(key, m.line, m.rawStart, m.isBlock, m.inlineEnd, i + 1 < keyCount ? matches[i + 1].matchStart : frontMatter.length);
|
|
342
296
|
}
|
package/dist/flow.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Core §15.8 flow collections: `[...]` sequences and `{...}` mappings. */
|
|
2
2
|
import { type ParseContext } from './normalize.js';
|
|
3
3
|
import type { ValueBuilder } from './builder.js';
|
|
4
|
-
|
|
5
|
-
export declare const
|
|
4
|
+
import type { ReferenceSource } from './reference-tokens2.js';
|
|
5
|
+
export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V[] | null;
|
|
6
|
+
export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V | null;
|
|
6
7
|
/** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
|
|
7
|
-
export declare const parseFlowOrScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M
|
|
8
|
+
export declare const parseFlowOrScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V;
|
package/dist/flow.js
CHANGED
|
@@ -1,8 +1,65 @@
|
|
|
1
1
|
/** Core §15.8 flow collections: `[...]` sequences and `{...}` mappings. */
|
|
2
2
|
import { checkKeyLength, checkDuplicateKey } from './normalize.js';
|
|
3
|
-
import { parseQuotedOrTyped, parseScalarValue, stripKeyQuotes } from './scalars.js';
|
|
3
|
+
import { parseQuotedOrTyped, parseScalarValue, stripKeyQuotes, closingQuoteIndex, isValidKey } from './scalars.js';
|
|
4
4
|
import { LimaError } from './errors.js';
|
|
5
5
|
import { isTrimWhitespace } from './chars.js';
|
|
6
|
+
/**
|
|
7
|
+
* Maps a UTF-16 offset in the decoded container `val` to the corresponding
|
|
8
|
+
* offset in the physical container `raw`. The two differ only by `\#` → `#`
|
|
9
|
+
* collapses outside quoted strings (FR-5 keeps `\#` inside quotes), so every
|
|
10
|
+
* `#` in `val` outside a quote is one such collapse — `raw` has `\#` there.
|
|
11
|
+
* Brackets, commas, quotes and quoted content are identical in both.
|
|
12
|
+
*/
|
|
13
|
+
const rawOffsetOf = (raw, val, valOffset) => {
|
|
14
|
+
let ri = 0;
|
|
15
|
+
let vi = 0;
|
|
16
|
+
let quote = 0;
|
|
17
|
+
while (vi < valOffset) {
|
|
18
|
+
const vc = val.charCodeAt(vi);
|
|
19
|
+
if (quote) {
|
|
20
|
+
if (vc === 92) {
|
|
21
|
+
vi += 2;
|
|
22
|
+
ri += 2;
|
|
23
|
+
continue;
|
|
24
|
+
} // escaped pair, identical in both
|
|
25
|
+
if (vc === quote)
|
|
26
|
+
quote = 0;
|
|
27
|
+
}
|
|
28
|
+
else if (vc === 34 || vc === 39) {
|
|
29
|
+
quote = vc;
|
|
30
|
+
}
|
|
31
|
+
else if (vc === 35) {
|
|
32
|
+
vi += 1;
|
|
33
|
+
ri += 2;
|
|
34
|
+
continue; // '#' in the decoded container ⇐ '\#' in raw
|
|
35
|
+
}
|
|
36
|
+
vi += 1;
|
|
37
|
+
ri += 1;
|
|
38
|
+
}
|
|
39
|
+
return ri;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The `ReferenceSource` for a flow element spanning UTF-16 `[start, end)` in
|
|
43
|
+
* the decoded container `val`. The element's physical column is the
|
|
44
|
+
* container's column plus the *codepoint* distance to the element's start in
|
|
45
|
+
* the physical text, and its `raw` is the physical element slice — so a
|
|
46
|
+
* token's reported position (References §2.4) and §5 left-to-right ordering
|
|
47
|
+
* are correct even when an earlier `\#` widened the source. `source.raw` is
|
|
48
|
+
* the physical container (equal to `val` when the value carried no `\#`).
|
|
49
|
+
*/
|
|
50
|
+
const elementSource = (source, val, start, end) => {
|
|
51
|
+
if (source === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
const raw = source.raw ?? val;
|
|
54
|
+
const rs = rawOffsetOf(raw, val, start);
|
|
55
|
+
const re = rawOffsetOf(raw, val, end);
|
|
56
|
+
return {
|
|
57
|
+
line: source.line,
|
|
58
|
+
col: source.col + [...raw.slice(0, rs)].length,
|
|
59
|
+
raw: raw.slice(rs, re),
|
|
60
|
+
tabAdjust: source.tabAdjust,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
6
63
|
const trimStart = (source, start, end) => {
|
|
7
64
|
while (start < end && isTrimWhitespace(source.charCodeAt(start)))
|
|
8
65
|
start++;
|
|
@@ -61,7 +118,7 @@ class FlowCursor {
|
|
|
61
118
|
}
|
|
62
119
|
const isNestedFlowConstruct = (item) => (item.charCodeAt(0) === 91 && item.charCodeAt(item.length - 1) === 93) ||
|
|
63
120
|
(item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125);
|
|
64
|
-
export const parseFlowSequence = (val, ctx, line, builder) => {
|
|
121
|
+
export const parseFlowSequence = (val, ctx, line, builder, source) => {
|
|
65
122
|
if (val.charCodeAt(0) !== 91 || val.charCodeAt(val.length - 1) !== 93)
|
|
66
123
|
return null;
|
|
67
124
|
const innerStart = trimStart(val, 1, val.length - 1);
|
|
@@ -90,17 +147,34 @@ export const parseFlowSequence = (val, ctx, line, builder) => {
|
|
|
90
147
|
});
|
|
91
148
|
}
|
|
92
149
|
if (item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125) {
|
|
93
|
-
const nested = parseFlowMapping(item, ctx, line, builder);
|
|
150
|
+
const nested = parseFlowMapping(item, ctx, line, builder, elementSource(source, val, start, end));
|
|
94
151
|
if (nested !== null) {
|
|
95
152
|
items.push(nested);
|
|
96
153
|
continue;
|
|
97
154
|
}
|
|
98
155
|
}
|
|
99
|
-
items.push(parseQuotedOrTyped(item, ctx, line,
|
|
156
|
+
items.push(parseQuotedOrTyped(item, ctx, line, builder, elementSource(source, val, start, end)));
|
|
100
157
|
}
|
|
101
158
|
return items;
|
|
102
159
|
};
|
|
103
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Index of the key/value `: ` separator within a flow-mapping item — the
|
|
162
|
+
* first `: ` that is not inside a quoted key (§5.1). Returns -1 when the
|
|
163
|
+
* item has no separator.
|
|
164
|
+
*/
|
|
165
|
+
const flowItemSeparator = (val, start, end) => {
|
|
166
|
+
let scan = start;
|
|
167
|
+
if (val.charCodeAt(start) === 34 || val.charCodeAt(start) === 39) {
|
|
168
|
+
// §5.2: a single-quoted key is literal, so its first `'` closes.
|
|
169
|
+
const close = closingQuoteIndex(val.slice(start, end), false);
|
|
170
|
+
if (close === -1)
|
|
171
|
+
return -1;
|
|
172
|
+
scan = start + close + 1;
|
|
173
|
+
}
|
|
174
|
+
const sep = val.indexOf(': ', scan);
|
|
175
|
+
return sep === -1 || sep >= end ? -1 : sep;
|
|
176
|
+
};
|
|
177
|
+
export const parseFlowMapping = (val, ctx, line, builder, source) => {
|
|
104
178
|
if (val.charCodeAt(0) !== 123 || val.charCodeAt(val.length - 1) !== 125)
|
|
105
179
|
return null;
|
|
106
180
|
const innerStart = trimStart(val, 1, val.length - 1);
|
|
@@ -117,8 +191,8 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
|
|
|
117
191
|
throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `Lima: empty element in flow mapping at line ${line}` });
|
|
118
192
|
continue;
|
|
119
193
|
}
|
|
120
|
-
const colonPos = val
|
|
121
|
-
if (colonPos === -1
|
|
194
|
+
const colonPos = flowItemSeparator(val, itemStart, itemEnd);
|
|
195
|
+
if (colonPos === -1) {
|
|
122
196
|
if (ctx.strict)
|
|
123
197
|
throw new LimaError({
|
|
124
198
|
code: 'INVALID_FLOW_SYNTAX', line,
|
|
@@ -128,8 +202,15 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
|
|
|
128
202
|
}
|
|
129
203
|
const keyStart = trimStart(val, itemStart, colonPos);
|
|
130
204
|
const keyEnd = trimEnd(val, keyStart, colonPos);
|
|
131
|
-
const
|
|
132
|
-
|
|
205
|
+
const keyRaw = val.slice(keyStart, keyEnd);
|
|
206
|
+
if (!isValidKey(keyRaw)) {
|
|
207
|
+
// §5.1: an unquoted key with a space/dot, or a malformed quoted
|
|
208
|
+
// key, is not a key. Skip the item in both modes (§10's strict
|
|
209
|
+
// list is closed and does not cover this — same as the top level).
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const key = stripKeyQuotes(keyRaw, ctx.strict, line);
|
|
213
|
+
checkKeyLength(key, line);
|
|
133
214
|
if (ctx.strict || ctx.onWarning !== undefined) {
|
|
134
215
|
checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
|
|
135
216
|
}
|
|
@@ -139,24 +220,24 @@ export const parseFlowMapping = (val, ctx, line, builder) => {
|
|
|
139
220
|
if (isNestedFlowConstruct(rawVal)) {
|
|
140
221
|
throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `Lima: invalid flow nesting at line ${line}: "${rawVal}"` });
|
|
141
222
|
}
|
|
142
|
-
builder.setMapping(entries, key, parseQuotedOrTyped(rawVal, ctx, line,
|
|
223
|
+
builder.setMapping(entries, key, parseQuotedOrTyped(rawVal, ctx, line, builder, elementSource(source, val, valueStart, valueEnd)));
|
|
143
224
|
}
|
|
144
225
|
return builder.mapping(entries, line);
|
|
145
226
|
};
|
|
146
227
|
/** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
|
|
147
|
-
export const parseFlowOrScalarValue = (raw, ctx, line, builder) => {
|
|
228
|
+
export const parseFlowOrScalarValue = (raw, ctx, line, builder, source) => {
|
|
148
229
|
const first = raw.charCodeAt(0);
|
|
149
230
|
if (first === 91) {
|
|
150
|
-
const sequence = parseFlowSequence(raw, ctx, line, builder);
|
|
231
|
+
const sequence = parseFlowSequence(raw, ctx, line, builder, source);
|
|
151
232
|
if (sequence !== null)
|
|
152
233
|
return builder.array(sequence, line);
|
|
153
|
-
return parseScalarValue(raw, ctx, line, builder);
|
|
234
|
+
return parseScalarValue(raw, ctx, line, builder, source);
|
|
154
235
|
}
|
|
155
236
|
else if (first === 123) {
|
|
156
|
-
const mapping = parseFlowMapping(raw, ctx, line, builder);
|
|
237
|
+
const mapping = parseFlowMapping(raw, ctx, line, builder, source);
|
|
157
238
|
if (mapping !== null)
|
|
158
239
|
return mapping;
|
|
159
|
-
return parseScalarValue(raw, ctx, line, builder);
|
|
240
|
+
return parseScalarValue(raw, ctx, line, builder, source);
|
|
160
241
|
}
|
|
161
|
-
return parseQuotedOrTyped(raw, ctx, line,
|
|
242
|
+
return parseQuotedOrTyped(raw, ctx, line, builder, source);
|
|
162
243
|
};
|
package/dist/normalize.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export type Diagnostic = {
|
|
|
20
20
|
export type ParseContext = {
|
|
21
21
|
strict: boolean;
|
|
22
22
|
onWarning?: (diagnostic: Diagnostic) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Per-line (0-based) count of columns added by Core §3 leading-tab
|
|
25
|
+
* expansion — subtracted from a References 2.0 token's column so it
|
|
26
|
+
* reports its position in the *original* source (§2.4), not the
|
|
27
|
+
* tab-expanded one. Absent when the document had no tabs.
|
|
28
|
+
*/
|
|
29
|
+
tabAdjust?: number[];
|
|
23
30
|
};
|
|
24
31
|
export declare const DOCUMENT_SIZE_LIMIT = 65536;
|
|
25
32
|
export declare const KEY_LENGTH_LIMIT = 128;
|
|
@@ -28,14 +35,6 @@ export declare const NESTING_DEPTH_LIMIT = 16;
|
|
|
28
35
|
export declare const byteLength: (s: string) => number;
|
|
29
36
|
export declare const checkStringLimit: (value: string, line: number) => void;
|
|
30
37
|
export declare const checkScalarLimit: (v: LimaValue, line: number) => void;
|
|
31
|
-
|
|
32
|
-
* `line` is a thunk, not a plain number: computing a top-level key's line
|
|
33
|
-
* can trigger an O(document length) scan (see `keyLine` in core.ts) the very
|
|
34
|
-
* first time it's called, and this check runs for every key in the
|
|
35
|
-
* document. Evaluating it eagerly would pay that cost on every parse, even
|
|
36
|
-
* though the overwhelming majority of keys never violate the limit — the
|
|
37
|
-
* thunk defers it to the one branch that actually needs a line number.
|
|
38
|
-
*/
|
|
39
|
-
export declare const checkKeyLength: (key: string, line: () => number) => void;
|
|
38
|
+
export declare const checkKeyLength: (key: string, line: number) => void;
|
|
40
39
|
export declare const checkDuplicateKey: (exists: boolean, key: string, line: number, ctx: ParseContext) => void;
|
|
41
40
|
export declare const checkDuplicateKeyMap: (entries: Map<string, unknown>, key: string, line: number, ctx: ParseContext) => void;
|
package/dist/normalize.js
CHANGED
|
@@ -28,20 +28,11 @@ export const checkScalarLimit = (v, line) => {
|
|
|
28
28
|
if (v.kind === 'string')
|
|
29
29
|
checkStringLimit(v.value, line);
|
|
30
30
|
};
|
|
31
|
-
/**
|
|
32
|
-
* `line` is a thunk, not a plain number: computing a top-level key's line
|
|
33
|
-
* can trigger an O(document length) scan (see `keyLine` in core.ts) the very
|
|
34
|
-
* first time it's called, and this check runs for every key in the
|
|
35
|
-
* document. Evaluating it eagerly would pay that cost on every parse, even
|
|
36
|
-
* though the overwhelming majority of keys never violate the limit — the
|
|
37
|
-
* thunk defers it to the one branch that actually needs a line number.
|
|
38
|
-
*/
|
|
39
31
|
export const checkKeyLength = (key, line) => {
|
|
40
32
|
if (key.length > KEY_LENGTH_LIMIT && codepointLength(key) > KEY_LENGTH_LIMIT) {
|
|
41
|
-
const l = line();
|
|
42
33
|
throw new LimaError({
|
|
43
|
-
code: 'RESOURCE_LIMIT', line
|
|
44
|
-
message: `Lima: key "${key}" exceeds maximum length of ${KEY_LENGTH_LIMIT} code points at line ${
|
|
34
|
+
code: 'RESOURCE_LIMIT', line,
|
|
35
|
+
message: `Lima: key "${key}" exceeds maximum length of ${KEY_LENGTH_LIMIT} code points at line ${line}`,
|
|
45
36
|
});
|
|
46
37
|
}
|
|
47
38
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Position-annotated tree helpers shared by frozen References 1.0 and active References 2.0; contains no reference grammar or resolution logic. */
|
|
2
|
+
import { type InsertedAt, type NativeValue, type PositionedValue } from './core.js';
|
|
3
|
+
import { type LimaValue } from './value.js';
|
|
4
|
+
type Meta = Record<string, unknown>;
|
|
5
|
+
/** Every Lima mapping result must be a prototype-free object. */
|
|
6
|
+
export declare const emptyMapping: () => Meta;
|
|
7
|
+
/**
|
|
8
|
+
* Structural deep copy for document-derived targets — preserves each leaf's
|
|
9
|
+
* own line/quoted (§4.2/R-104: still eligible for further resolution) and
|
|
10
|
+
* any existing `insertedAt` provenance from an earlier, nested resolution
|
|
11
|
+
* (R-112: a reference copied wholesale into a new position can itself
|
|
12
|
+
* already contain the result of an inner reference that resolved first —
|
|
13
|
+
* both insertion sites remain identifiable participants after the copy).
|
|
14
|
+
*/
|
|
15
|
+
export declare const deepCopyPositioned: (v: PositionedValue) => PositionedValue;
|
|
16
|
+
/**
|
|
17
|
+
* Wraps a partial's ingested value into the annotated representation, with
|
|
18
|
+
* every string leaf marked permanently inert (§3.8: "no traversal into
|
|
19
|
+
* partial values" — the resolution phases must never rediscover a
|
|
20
|
+
* reference-like substring inside partial content). Called once per partial
|
|
21
|
+
* name at ingestion (see the call site), producing the one canonical tree
|
|
22
|
+
* every pure reference to that partial retrieves — deep-copying it on every
|
|
23
|
+
* such reference (§3.1) is the resolveTree call site's job, not this one.
|
|
24
|
+
*/
|
|
25
|
+
export declare const partialToPositioned: (v: LimaValue, line: number) => PositionedValue;
|
|
26
|
+
/**
|
|
27
|
+
* References §5/R-112 (nesting depth), §6.2 (node count), and the public
|
|
28
|
+
* result shape (native conversion) computed together in one recursive pass
|
|
29
|
+
* over the final, post-substitution tree — instead of three independent
|
|
30
|
+
* full-tree walks (a depth-only pass, a count-only pass, then
|
|
31
|
+
* `toNativeFromPositioned`), each of which would otherwise revisit every
|
|
32
|
+
* node of what can be a large, reference-expanded result. Depth attribution
|
|
33
|
+
* needs more than a depth number: it needs to know which reference
|
|
34
|
+
* insertions lie on the actual deepest path, so the earliest of those (by
|
|
35
|
+
* line) can be blamed if the limit is exceeded — ties for "deepest child"
|
|
36
|
+
* all count, since a violation can be reached via more than one
|
|
37
|
+
* maximal-depth branch. `native` is still built unconditionally even
|
|
38
|
+
* though a depth/node-count violation elsewhere in the document means it
|
|
39
|
+
* will be discarded and never returned — cheaper than adding a second
|
|
40
|
+
* conditional branch to every call site, and the discarded case is the
|
|
41
|
+
* rare (error) path, not the one this pass exists to speed up.
|
|
42
|
+
*
|
|
43
|
+
* Each array/mapping branch tracks a running max depth and its participant
|
|
44
|
+
* list in a single loop over its children, instead of first materializing a
|
|
45
|
+
* full array of child results and then doing separate `.map()`/`Math.max`/
|
|
46
|
+
* `.filter().flatMap()` passes over it — for a wide sibling group (many
|
|
47
|
+
* children at the same depth, the common shape for flat documents with many
|
|
48
|
+
* top-level references) that would mean an extra full pass per level on top
|
|
49
|
+
* of the traversal itself. A running accumulator only ever copies-on-write:
|
|
50
|
+
* `.slice()` when a strictly greater depth replaces the running set, plain
|
|
51
|
+
* `.push()` onto that fresh copy for a tie at the current max — never a
|
|
52
|
+
* mutation of a child's own returned array. Reusing a child's array in
|
|
53
|
+
* place would currently be safe (each node is visited exactly once, no
|
|
54
|
+
* aliasing left after the pure-reference deep-copy fix), but relying on
|
|
55
|
+
* that invariant here has no upside worth the risk in a file that has
|
|
56
|
+
* already had one real aliasing bug.
|
|
57
|
+
*/
|
|
58
|
+
export type FinalizedValue = {
|
|
59
|
+
native: NativeValue;
|
|
60
|
+
nodeCount: number;
|
|
61
|
+
depth: number;
|
|
62
|
+
deepestParticipants: InsertedAt[];
|
|
63
|
+
};
|
|
64
|
+
export declare const finalizePositioned: (v: PositionedValue) => FinalizedValue;
|
|
65
|
+
/**
|
|
66
|
+
* Earliest participant by source position — lowest line, then lowest
|
|
67
|
+
* character offset (References §5) — or null when none exist (R-113's
|
|
68
|
+
* "line 1" fallback applies then).
|
|
69
|
+
*/
|
|
70
|
+
export declare const earliestParticipant: (participants: InsertedAt[]) => InsertedAt | null;
|
|
71
|
+
/** Node-count attribution for the RESOURCE_LIMIT error path: every reference insertion anywhere in the tree contributes to the total. */
|
|
72
|
+
export declare const collectAllParticipants: (v: PositionedValue, acc: InsertedAt[]) => void;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Position-annotated tree helpers shared by frozen References 1.0 and active References 2.0; contains no reference grammar or resolution logic. */
|
|
2
|
+
import { toNativeFromPositioned, } from './core.js';
|
|
3
|
+
/** Every Lima mapping result must be a prototype-free object. */
|
|
4
|
+
export const emptyMapping = () => Object.create(null);
|
|
5
|
+
/**
|
|
6
|
+
* Structural deep copy for document-derived targets — preserves each leaf's
|
|
7
|
+
* own line/quoted (§4.2/R-104: still eligible for further resolution) and
|
|
8
|
+
* any existing `insertedAt` provenance from an earlier, nested resolution
|
|
9
|
+
* (R-112: a reference copied wholesale into a new position can itself
|
|
10
|
+
* already contain the result of an inner reference that resolved first —
|
|
11
|
+
* both insertion sites remain identifiable participants after the copy).
|
|
12
|
+
*/
|
|
13
|
+
export const deepCopyPositioned = (v) => {
|
|
14
|
+
switch (v.kind) {
|
|
15
|
+
case 'array': return { kind: 'array', items: v.items.map(deepCopyPositioned), line: v.line, references2Active: v.references2Active, insertedAt: v.insertedAt, priorInsertions: v.priorInsertions };
|
|
16
|
+
case 'mapping': {
|
|
17
|
+
const entries = new Map();
|
|
18
|
+
for (const [k, c] of v.entries)
|
|
19
|
+
entries.set(k, deepCopyPositioned(c));
|
|
20
|
+
return { kind: 'mapping', entries, line: v.line, references2Active: v.references2Active, insertedAt: v.insertedAt, priorInsertions: v.priorInsertions };
|
|
21
|
+
}
|
|
22
|
+
case 'instant': return { kind: 'instant', value: new Date(v.value.getTime()), line: v.line, insertedAt: v.insertedAt, priorInsertions: v.priorInsertions };
|
|
23
|
+
default: return v;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Wraps a partial's ingested value into the annotated representation, with
|
|
28
|
+
* every string leaf marked permanently inert (§3.8: "no traversal into
|
|
29
|
+
* partial values" — the resolution phases must never rediscover a
|
|
30
|
+
* reference-like substring inside partial content). Called once per partial
|
|
31
|
+
* name at ingestion (see the call site), producing the one canonical tree
|
|
32
|
+
* every pure reference to that partial retrieves — deep-copying it on every
|
|
33
|
+
* such reference (§3.1) is the resolveTree call site's job, not this one.
|
|
34
|
+
*/
|
|
35
|
+
export const partialToPositioned = (v, line) => {
|
|
36
|
+
switch (v.kind) {
|
|
37
|
+
case 'null': return { kind: 'null', line };
|
|
38
|
+
case 'bool': return { kind: 'bool', value: v.value, line };
|
|
39
|
+
case 'int': return { kind: 'int', value: v.value, line };
|
|
40
|
+
case 'float': return { kind: 'float', value: v.value, line };
|
|
41
|
+
case 'string': return { kind: 'string', value: v.value, line, quoted: true };
|
|
42
|
+
case 'instant': return { kind: 'instant', value: v.value, line };
|
|
43
|
+
case 'array': return { kind: 'array', items: v.items.map((i) => partialToPositioned(i, line)), line };
|
|
44
|
+
case 'mapping': {
|
|
45
|
+
const entries = new Map();
|
|
46
|
+
for (const [k, c] of v.entries)
|
|
47
|
+
entries.set(k, partialToPositioned(c, line));
|
|
48
|
+
return { kind: 'mapping', entries, line };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
export const finalizePositioned = (v) => {
|
|
53
|
+
const own = v.insertedAt ? [...(v.priorInsertions ?? []), v.insertedAt] : (v.priorInsertions ?? []);
|
|
54
|
+
if (v.kind === 'array') {
|
|
55
|
+
if (v.items.length === 0)
|
|
56
|
+
return { native: [], nodeCount: 1, depth: 1, deepestParticipants: own };
|
|
57
|
+
const native = new Array(v.items.length);
|
|
58
|
+
let nodeCount = 1;
|
|
59
|
+
let maxDepth = -1;
|
|
60
|
+
let childParticipants = [];
|
|
61
|
+
for (let i = 0; i < v.items.length; i++) {
|
|
62
|
+
const r = finalizePositioned(v.items[i]);
|
|
63
|
+
native[i] = r.native;
|
|
64
|
+
nodeCount += r.nodeCount;
|
|
65
|
+
if (r.depth > maxDepth) {
|
|
66
|
+
maxDepth = r.depth;
|
|
67
|
+
childParticipants = r.deepestParticipants.slice();
|
|
68
|
+
}
|
|
69
|
+
else if (r.depth === maxDepth) {
|
|
70
|
+
for (const p of r.deepestParticipants)
|
|
71
|
+
childParticipants.push(p);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { native, nodeCount, depth: 1 + maxDepth, deepestParticipants: own.length ? own.concat(childParticipants) : childParticipants };
|
|
75
|
+
}
|
|
76
|
+
if (v.kind === 'mapping') {
|
|
77
|
+
if (v.entries.size === 0)
|
|
78
|
+
return { native: emptyMapping(), nodeCount: 1, depth: 1, deepestParticipants: own };
|
|
79
|
+
const native = emptyMapping();
|
|
80
|
+
let nodeCount = 1;
|
|
81
|
+
let maxDepth = -1;
|
|
82
|
+
let childParticipants = [];
|
|
83
|
+
for (const [k, c] of v.entries) {
|
|
84
|
+
const r = finalizePositioned(c);
|
|
85
|
+
native[k] = r.native;
|
|
86
|
+
nodeCount += r.nodeCount;
|
|
87
|
+
if (r.depth > maxDepth) {
|
|
88
|
+
maxDepth = r.depth;
|
|
89
|
+
childParticipants = r.deepestParticipants.slice();
|
|
90
|
+
}
|
|
91
|
+
else if (r.depth === maxDepth) {
|
|
92
|
+
for (const p of r.deepestParticipants)
|
|
93
|
+
childParticipants.push(p);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { native: native, nodeCount, depth: 1 + maxDepth, deepestParticipants: own.length ? own.concat(childParticipants) : childParticipants };
|
|
97
|
+
}
|
|
98
|
+
// Scalar leaf: null/bool/int/float/string/instant. No further recursion,
|
|
99
|
+
// so plain toNativeFromPositioned is exactly the single-node conversion
|
|
100
|
+
// needed here — reused rather than duplicated.
|
|
101
|
+
return { native: toNativeFromPositioned(v), nodeCount: 1, depth: 0, deepestParticipants: own };
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Earliest participant by source position — lowest line, then lowest
|
|
105
|
+
* character offset (References §5) — or null when none exist (R-113's
|
|
106
|
+
* "line 1" fallback applies then).
|
|
107
|
+
*/
|
|
108
|
+
export const earliestParticipant = (participants) => participants.length === 0
|
|
109
|
+
? null
|
|
110
|
+
: participants.reduce((a, b) => b.line < a.line || (b.line === a.line && (b.offset ?? 0) < (a.offset ?? 0)) ? b : a);
|
|
111
|
+
/** Node-count attribution for the RESOURCE_LIMIT error path: every reference insertion anywhere in the tree contributes to the total. */
|
|
112
|
+
export const collectAllParticipants = (v, acc) => {
|
|
113
|
+
if (v.priorInsertions)
|
|
114
|
+
acc.push(...v.priorInsertions);
|
|
115
|
+
if (v.insertedAt)
|
|
116
|
+
acc.push(v.insertedAt);
|
|
117
|
+
if (v.kind === 'array')
|
|
118
|
+
for (const item of v.items)
|
|
119
|
+
collectAllParticipants(item, acc);
|
|
120
|
+
if (v.kind === 'mapping')
|
|
121
|
+
for (const c of v.entries.values())
|
|
122
|
+
collectAllParticipants(c, acc);
|
|
123
|
+
};
|