@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/scalars.d.ts
CHANGED
|
@@ -7,40 +7,47 @@
|
|
|
7
7
|
import { type LimaValue } from './value.js';
|
|
8
8
|
import { type ParseContext } from './normalize.js';
|
|
9
9
|
import type { ValueBuilder } from './builder.js';
|
|
10
|
-
import { type ReferenceToken2 } from './reference-tokens2.js';
|
|
10
|
+
import { type ReferenceSource, type ReferenceToken2 } from './reference-tokens2.js';
|
|
11
11
|
/**
|
|
12
12
|
* `insertedAt` is never set by Core — it's a References-only annotation
|
|
13
13
|
* (see references.ts's `resolveTree`), stamped on the root of a value
|
|
14
14
|
* copied in by a successful pure-reference resolution, with the source
|
|
15
|
-
* token and
|
|
16
|
-
* global-error attribution (R-112): when a final-result
|
|
17
|
-
* depth, total node count) is violated, the lowest
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* token, line, and character offset that caused the insertion. It powers
|
|
16
|
+
* References §5's global-error attribution (R-112): when a final-result
|
|
17
|
+
* limit (nesting depth, total node count) is violated, the lowest source
|
|
18
|
+
* position (`line`, then `offset`) among the participating nodes identifies
|
|
19
|
+
* which reference token to blame — the spec requires the error message to
|
|
20
|
+
* include both the token and the line. `priorInsertions` retains older
|
|
21
|
+
* root provenance when a resolved pure-reference result is copied again.
|
|
20
22
|
*/
|
|
21
23
|
export type InsertedAt = {
|
|
22
24
|
line: number;
|
|
23
25
|
token: string;
|
|
26
|
+
offset?: number;
|
|
24
27
|
};
|
|
25
28
|
export type PositionedValue = {
|
|
26
29
|
kind: 'null';
|
|
27
30
|
line: number;
|
|
28
31
|
insertedAt?: InsertedAt;
|
|
32
|
+
priorInsertions?: InsertedAt[];
|
|
29
33
|
} | {
|
|
30
34
|
kind: 'bool';
|
|
31
35
|
value: boolean;
|
|
32
36
|
line: number;
|
|
33
37
|
insertedAt?: InsertedAt;
|
|
38
|
+
priorInsertions?: InsertedAt[];
|
|
34
39
|
} | {
|
|
35
40
|
kind: 'int';
|
|
36
41
|
value: number;
|
|
37
42
|
line: number;
|
|
38
43
|
insertedAt?: InsertedAt;
|
|
44
|
+
priorInsertions?: InsertedAt[];
|
|
39
45
|
} | {
|
|
40
46
|
kind: 'float';
|
|
41
47
|
value: number;
|
|
42
48
|
line: number;
|
|
43
49
|
insertedAt?: InsertedAt;
|
|
50
|
+
priorInsertions?: InsertedAt[];
|
|
44
51
|
} | {
|
|
45
52
|
kind: 'string';
|
|
46
53
|
value: string;
|
|
@@ -48,23 +55,27 @@ export type PositionedValue = {
|
|
|
48
55
|
quoted: boolean;
|
|
49
56
|
references2?: ReferenceToken2[];
|
|
50
57
|
insertedAt?: InsertedAt;
|
|
58
|
+
priorInsertions?: InsertedAt[];
|
|
51
59
|
} | {
|
|
52
60
|
kind: 'instant';
|
|
53
61
|
value: Date;
|
|
54
62
|
line: number;
|
|
55
63
|
insertedAt?: InsertedAt;
|
|
64
|
+
priorInsertions?: InsertedAt[];
|
|
56
65
|
} | {
|
|
57
66
|
kind: 'array';
|
|
58
67
|
items: PositionedValue[];
|
|
59
68
|
line: number;
|
|
60
69
|
references2Active?: boolean;
|
|
61
70
|
insertedAt?: InsertedAt;
|
|
71
|
+
priorInsertions?: InsertedAt[];
|
|
62
72
|
} | {
|
|
63
73
|
kind: 'mapping';
|
|
64
74
|
entries: Map<string, PositionedValue>;
|
|
65
75
|
line: number;
|
|
66
76
|
references2Active?: boolean;
|
|
67
77
|
insertedAt?: InsertedAt;
|
|
78
|
+
priorInsertions?: InsertedAt[];
|
|
68
79
|
};
|
|
69
80
|
export declare const hasActiveReferences2: (value: PositionedValue) => boolean;
|
|
70
81
|
/** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
|
|
@@ -78,17 +89,76 @@ export declare const NO_SPAN_VALUE: unique symbol;
|
|
|
78
89
|
* handled by the full string-based scalar/flow grammar.
|
|
79
90
|
*/
|
|
80
91
|
export declare const parseSimpleScalarSpan: <V, M>(source: string, start: number, end: number, line: number, strict: boolean, builder: ValueBuilder<V, M>) => V | typeof NO_SPAN_VALUE;
|
|
92
|
+
/**
|
|
93
|
+
* Index of the quote that closes a quoted scalar opening at `s[0]`, or -1
|
|
94
|
+
* if it is never closed. Escape-aware, so `"a\""` closes at its last
|
|
95
|
+
* character rather than the escaped inner `"`:
|
|
96
|
+
* - double quotes: a backslash escapes the next character (§6.1.2);
|
|
97
|
+
* - single-quoted *values* (§6.1.3 "Backslash pairing"): scanning left
|
|
98
|
+
* to right, `\\` (two literal backslashes) and `\'` (an escaped quote)
|
|
99
|
+
* are each consumed as a two-character unit, so a lone `\` is literal
|
|
100
|
+
* and a closing `'` after an even backslash run closes the string.
|
|
101
|
+
* Pass `singleQuoteEscape = false` for single-quoted *keys*, which are
|
|
102
|
+
* fully literal (§5.2), where the first `'` closes.
|
|
103
|
+
* `s[0]` is assumed to be `'` or `"`.
|
|
104
|
+
*/
|
|
105
|
+
export declare const closingQuoteIndex: (s: string, singleQuoteEscape?: boolean) => number;
|
|
106
|
+
/**
|
|
107
|
+
* Whether `raw` (a key candidate as written, before quote stripping) is a
|
|
108
|
+
* usable Lima key:
|
|
109
|
+
* - a quoted string that is properly closed at its final character, or
|
|
110
|
+
* - an unquoted token matching the Core §5.1 grammar.
|
|
111
|
+
*
|
|
112
|
+
* §5.1 is a closed pattern: a key with a space, a dot, a slash, a paren, or
|
|
113
|
+
* any other character outside `[a-zA-Z0-9_:\-]` (a surviving NBSP included)
|
|
114
|
+
* is not an unquoted key. Per §4, such a line/item is unrecognised and
|
|
115
|
+
* skipped in *both* modes — §10's strict error list stays closed. The three
|
|
116
|
+
* mapping contexts (top level, block-nested, flow) now share this one
|
|
117
|
+
* check; `docs/decisions/structural-indentation-unicode-whitespace.md`
|
|
118
|
+
* governs only whether Unicode whitespace counts as *indentation*, not
|
|
119
|
+
* whether it may appear in a key.
|
|
120
|
+
*/
|
|
121
|
+
export declare const isValidKey: (raw: string) => boolean;
|
|
81
122
|
export declare const unescapeDQ: (s: string, strict?: boolean, line?: number) => string;
|
|
123
|
+
/**
|
|
124
|
+
* The value text with a trailing `#` comment removed, but with `\#`
|
|
125
|
+
* escapes left as written. This is the *physical* form a References 2.0
|
|
126
|
+
* token's column is measured against (§2.4): the comment is not part of
|
|
127
|
+
* the value (so its `${…}`-shaped text must not be scanned as a token),
|
|
128
|
+
* but a `\#` before a token still occupies its two source columns.
|
|
129
|
+
*
|
|
130
|
+
* `trimEnd()` is exactly the project whitespace set (ECMAScript WhiteSpace
|
|
131
|
+
* + LineTerminator = `isTrimWhitespace`) — a U+00A0 / FEFF between the
|
|
132
|
+
* value and the `#` is not part of the value. The Rust and Go ports must
|
|
133
|
+
* use `is_trim_whitespace` here, not a host `trim_end` or `" \t"`.
|
|
134
|
+
*/
|
|
135
|
+
export declare const stripCommentKeepEscapes: (val: string) => string;
|
|
136
|
+
/**
|
|
137
|
+
* The value text with a trailing `#` comment removed and each `\#` *outside*
|
|
138
|
+
* a quoted string collapsed to `#` (Core §6.1.4 — the escaped-hash rule is
|
|
139
|
+
* defined for unquoted values only). Inside `"..."` / `'...'` the backslash
|
|
140
|
+
* is left intact: `\#` there is an unknown double-quoted escape (§6.1.2:
|
|
141
|
+
* strict throws, non-strict keeps it) or a literal backslash in a
|
|
142
|
+
* single-quoted string (§6.1.3). Finds the same comment boundary as
|
|
143
|
+
* `stripCommentKeepEscapes` — the two must agree, they annotate the same
|
|
144
|
+
* value.
|
|
145
|
+
*/
|
|
82
146
|
export declare const stripComment: (val: string) => string;
|
|
83
|
-
/**
|
|
84
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Strips a key's surrounding quotes — unescaping a double-quoted key
|
|
149
|
+
* (§6.1.2 escapes, strict-checked), taking a single-quoted key literally
|
|
150
|
+
* (§5.2). Returns `s` unchanged when it is not a properly-closed quoted
|
|
151
|
+
* string. Callers gate on `isValidKey` first, so an unterminated or
|
|
152
|
+
* trailing-content key never reaches here as a real key.
|
|
153
|
+
*/
|
|
154
|
+
export declare const stripKeyQuotes: (s: string, strict?: boolean, line?: number) => string;
|
|
85
155
|
/**
|
|
86
156
|
* Quoted-or-typed scalar, shared by every value position (top-level inline
|
|
87
157
|
* values, flow-sequence/flow-mapping items, block-array scalar items).
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
158
|
+
* §10.1's two quoted-string strict checks — "unterminated quoted string"
|
|
159
|
+
* and "non-whitespace content after closing quote in an inline value" —
|
|
160
|
+
* apply in every one of those positions, so they are enforced here rather
|
|
161
|
+
* than gated to the top level.
|
|
92
162
|
*/
|
|
93
|
-
export declare const parseQuotedOrTyped: <V, M>(raw: string, ctx: ParseContext, line: number,
|
|
94
|
-
export declare const parseScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M
|
|
163
|
+
export declare const parseQuotedOrTyped: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V;
|
|
164
|
+
export declare const parseScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V;
|
package/dist/scalars.js
CHANGED
|
@@ -13,13 +13,12 @@ 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
|
-
tracksStringSourcePositions: true,
|
|
17
16
|
null: (line) => ({ kind: 'null', line }),
|
|
18
17
|
bool: (value, line) => ({ kind: 'bool', value, line }),
|
|
19
18
|
int: (value, line) => ({ kind: 'int', value, line }),
|
|
20
19
|
float: (value, line) => ({ kind: 'float', value, line }),
|
|
21
|
-
string: (value, line, quoted,
|
|
22
|
-
const references2 = quoted ? undefined : scanReferenceTokens2(value,
|
|
20
|
+
string: (value, line, quoted, source) => {
|
|
21
|
+
const references2 = quoted ? undefined : scanReferenceTokens2(value, source);
|
|
23
22
|
return {
|
|
24
23
|
kind: 'string', value, line, quoted,
|
|
25
24
|
...(references2 && references2.length > 0 ? { references2 } : {}),
|
|
@@ -239,7 +238,7 @@ export const parseSimpleScalarSpan = (source, start, end, line, strict, builder)
|
|
|
239
238
|
* an intermediate tagged `LimaValue` keeps both builders on one grammar while
|
|
240
239
|
* eliminating an allocate-then-switch conversion for every scalar.
|
|
241
240
|
*/
|
|
242
|
-
const buildTyped = (str, strict, line, builder) => {
|
|
241
|
+
const buildTyped = (str, strict, line, builder, source) => {
|
|
243
242
|
if (str === '' || str === 'null' || str === '~')
|
|
244
243
|
return builder.null(line);
|
|
245
244
|
if (str === 'true')
|
|
@@ -253,7 +252,7 @@ const buildTyped = (str, strict, line, builder) => {
|
|
|
253
252
|
// the email/date prechecks for ordinary words, URLs and identifiers.
|
|
254
253
|
if (!((first >= 48 && first <= 57) || first === 45 || first === 46)) {
|
|
255
254
|
checkStringLimit(str, line);
|
|
256
|
-
return builder.string(str, line, false);
|
|
255
|
+
return builder.string(str, line, false, source);
|
|
257
256
|
}
|
|
258
257
|
// Hex (0x/0X), octal (0o/0O), binary (0b/0B) — kept as strings (YAML 1.2 compatible).
|
|
259
258
|
if (str.length > 2 && str.charCodeAt(0) === 48 &&
|
|
@@ -261,7 +260,7 @@ const buildTyped = (str, strict, line, builder) => {
|
|
|
261
260
|
str.charCodeAt(1) === 111 || str.charCodeAt(1) === 79 ||
|
|
262
261
|
str.charCodeAt(1) === 98 || str.charCodeAt(1) === 66)) {
|
|
263
262
|
checkStringLimit(str, line);
|
|
264
|
-
return builder.string(str, line, false);
|
|
263
|
+
return builder.string(str, line, false, source);
|
|
265
264
|
}
|
|
266
265
|
const exactIsoShape = (str.length === 10 && str.charCodeAt(4) === 45 && str.charCodeAt(7) === 45) ||
|
|
267
266
|
(str.length === 20 && str.charCodeAt(4) === 45 && str.charCodeAt(7) === 45 &&
|
|
@@ -272,7 +271,7 @@ const buildTyped = (str, strict, line, builder) => {
|
|
|
272
271
|
if (date !== null)
|
|
273
272
|
return builder.instant(date, line);
|
|
274
273
|
checkStringLimit(str, line);
|
|
275
|
-
return builder.string(str, line, false);
|
|
274
|
+
return builder.string(str, line, false, source);
|
|
276
275
|
}
|
|
277
276
|
if (NUMBER_RE.test(str)) {
|
|
278
277
|
const n = Number(str);
|
|
@@ -307,10 +306,82 @@ const buildTyped = (str, strict, line, builder) => {
|
|
|
307
306
|
return builder.instant(date, line);
|
|
308
307
|
}
|
|
309
308
|
checkStringLimit(str, line);
|
|
310
|
-
return builder.string(str, line, false);
|
|
309
|
+
return builder.string(str, line, false, source);
|
|
311
310
|
};
|
|
312
311
|
// ─── Scalar / quoting ──────────────────────────────────────────────────────
|
|
313
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Index of the quote that closes a quoted scalar opening at `s[0]`, or -1
|
|
314
|
+
* if it is never closed. Escape-aware, so `"a\""` closes at its last
|
|
315
|
+
* character rather than the escaped inner `"`:
|
|
316
|
+
* - double quotes: a backslash escapes the next character (§6.1.2);
|
|
317
|
+
* - single-quoted *values* (§6.1.3 "Backslash pairing"): scanning left
|
|
318
|
+
* to right, `\\` (two literal backslashes) and `\'` (an escaped quote)
|
|
319
|
+
* are each consumed as a two-character unit, so a lone `\` is literal
|
|
320
|
+
* and a closing `'` after an even backslash run closes the string.
|
|
321
|
+
* Pass `singleQuoteEscape = false` for single-quoted *keys*, which are
|
|
322
|
+
* fully literal (§5.2), where the first `'` closes.
|
|
323
|
+
* `s[0]` is assumed to be `'` or `"`.
|
|
324
|
+
*/
|
|
325
|
+
export const closingQuoteIndex = (s, singleQuoteEscape = true) => {
|
|
326
|
+
const q = s.charCodeAt(0);
|
|
327
|
+
for (let i = 1; i < s.length; i++) {
|
|
328
|
+
const c = s.charCodeAt(i);
|
|
329
|
+
if (q === 34) {
|
|
330
|
+
if (c === 92) {
|
|
331
|
+
i++;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (c === 34)
|
|
335
|
+
return i;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
if (singleQuoteEscape && c === 92) {
|
|
339
|
+
const n = s.charCodeAt(i + 1);
|
|
340
|
+
if (n === 92 || n === 39) {
|
|
341
|
+
i++;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (c === 39)
|
|
346
|
+
return i;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return -1;
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* The unquoted-key grammar of Core §5.1: `[a-zA-Z0-9_][a-zA-Z0-9_:\-]*`.
|
|
353
|
+
* Kept byte-for-byte identical to `scanner.ts`'s `isKeyStartChar` /
|
|
354
|
+
* `isKeyContinueChar`, which enforce the same grammar at the top level.
|
|
355
|
+
*/
|
|
356
|
+
const isUnquotedKeyStart = (c) => (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 95;
|
|
357
|
+
const isUnquotedKeyContinue = (c) => isUnquotedKeyStart(c) || c === 58 || c === 45;
|
|
358
|
+
/**
|
|
359
|
+
* Whether `raw` (a key candidate as written, before quote stripping) is a
|
|
360
|
+
* usable Lima key:
|
|
361
|
+
* - a quoted string that is properly closed at its final character, or
|
|
362
|
+
* - an unquoted token matching the Core §5.1 grammar.
|
|
363
|
+
*
|
|
364
|
+
* §5.1 is a closed pattern: a key with a space, a dot, a slash, a paren, or
|
|
365
|
+
* any other character outside `[a-zA-Z0-9_:\-]` (a surviving NBSP included)
|
|
366
|
+
* is not an unquoted key. Per §4, such a line/item is unrecognised and
|
|
367
|
+
* skipped in *both* modes — §10's strict error list stays closed. The three
|
|
368
|
+
* mapping contexts (top level, block-nested, flow) now share this one
|
|
369
|
+
* check; `docs/decisions/structural-indentation-unicode-whitespace.md`
|
|
370
|
+
* governs only whether Unicode whitespace counts as *indentation*, not
|
|
371
|
+
* whether it may appear in a key.
|
|
372
|
+
*/
|
|
373
|
+
export const isValidKey = (raw) => {
|
|
374
|
+
const f = raw.charCodeAt(0);
|
|
375
|
+
if (f === 34 || f === 39)
|
|
376
|
+
return closingQuoteIndex(raw, false) === raw.length - 1;
|
|
377
|
+
if (!isUnquotedKeyStart(f))
|
|
378
|
+
return false;
|
|
379
|
+
for (let i = 1; i < raw.length; i++) {
|
|
380
|
+
if (!isUnquotedKeyContinue(raw.charCodeAt(i)))
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
};
|
|
314
385
|
const ANY_ESCAPE_RE = /\\(u[0-9a-fA-F]{0,4}|U[0-9a-fA-F]{0,8}|x[0-9a-fA-F]{0,2}|.)/gs;
|
|
315
386
|
const SINGLE_CHAR_ESCAPES = '"\\/bfnrt'; // deliberately excludes '0' — Core Appendix A: \0 is unknown, not a null shorthand.
|
|
316
387
|
const U_ESCAPE_RE = /^u([0-9a-fA-F]{4})$/;
|
|
@@ -359,8 +430,54 @@ export const unescapeDQ = (s, strict = false, line = 0) => {
|
|
|
359
430
|
}
|
|
360
431
|
});
|
|
361
432
|
};
|
|
433
|
+
/**
|
|
434
|
+
* The value text with a trailing `#` comment removed, but with `\#`
|
|
435
|
+
* escapes left as written. This is the *physical* form a References 2.0
|
|
436
|
+
* token's column is measured against (§2.4): the comment is not part of
|
|
437
|
+
* the value (so its `${…}`-shaped text must not be scanned as a token),
|
|
438
|
+
* but a `\#` before a token still occupies its two source columns.
|
|
439
|
+
*
|
|
440
|
+
* `trimEnd()` is exactly the project whitespace set (ECMAScript WhiteSpace
|
|
441
|
+
* + LineTerminator = `isTrimWhitespace`) — a U+00A0 / FEFF between the
|
|
442
|
+
* value and the `#` is not part of the value. The Rust and Go ports must
|
|
443
|
+
* use `is_trim_whitespace` here, not a host `trim_end` or `" \t"`.
|
|
444
|
+
*/
|
|
445
|
+
export const stripCommentKeepEscapes = (val) => {
|
|
446
|
+
let quote = 0;
|
|
447
|
+
for (let i = 0; i < val.length; i++) {
|
|
448
|
+
const cc = val.charCodeAt(i);
|
|
449
|
+
if (quote) {
|
|
450
|
+
if (cc === 92)
|
|
451
|
+
i++;
|
|
452
|
+
else if (cc === quote)
|
|
453
|
+
quote = 0;
|
|
454
|
+
}
|
|
455
|
+
else if (cc === 34 || cc === 39) {
|
|
456
|
+
quote = cc;
|
|
457
|
+
}
|
|
458
|
+
else if (cc === 92 && val.charCodeAt(i + 1) === 35) {
|
|
459
|
+
i++;
|
|
460
|
+
}
|
|
461
|
+
else if (cc === 35) {
|
|
462
|
+
return val.slice(0, i).trimEnd();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return val;
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* The value text with a trailing `#` comment removed and each `\#` *outside*
|
|
469
|
+
* a quoted string collapsed to `#` (Core §6.1.4 — the escaped-hash rule is
|
|
470
|
+
* defined for unquoted values only). Inside `"..."` / `'...'` the backslash
|
|
471
|
+
* is left intact: `\#` there is an unknown double-quoted escape (§6.1.2:
|
|
472
|
+
* strict throws, non-strict keeps it) or a literal backslash in a
|
|
473
|
+
* single-quoted string (§6.1.3). Finds the same comment boundary as
|
|
474
|
+
* `stripCommentKeepEscapes` — the two must agree, they annotate the same
|
|
475
|
+
* value.
|
|
476
|
+
*/
|
|
362
477
|
export const stripComment = (val) => {
|
|
363
478
|
let quote = 0;
|
|
479
|
+
let out = '';
|
|
480
|
+
let seg = 0;
|
|
364
481
|
for (let i = 0; i < val.length; i++) {
|
|
365
482
|
const cc = val.charCodeAt(i);
|
|
366
483
|
if (quote) {
|
|
@@ -373,52 +490,64 @@ export const stripComment = (val) => {
|
|
|
373
490
|
quote = cc;
|
|
374
491
|
}
|
|
375
492
|
else if (cc === 92 && val.charCodeAt(i + 1) === 35) {
|
|
493
|
+
out += val.slice(seg, i) + '#';
|
|
376
494
|
i++;
|
|
495
|
+
seg = i + 1;
|
|
377
496
|
}
|
|
378
497
|
else if (cc === 35) {
|
|
379
|
-
return val.slice(
|
|
498
|
+
return (out + val.slice(seg, i)).trimEnd();
|
|
380
499
|
}
|
|
381
500
|
}
|
|
382
|
-
return val.
|
|
501
|
+
return out + val.slice(seg);
|
|
383
502
|
};
|
|
384
|
-
/**
|
|
385
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Strips a key's surrounding quotes — unescaping a double-quoted key
|
|
505
|
+
* (§6.1.2 escapes, strict-checked), taking a single-quoted key literally
|
|
506
|
+
* (§5.2). Returns `s` unchanged when it is not a properly-closed quoted
|
|
507
|
+
* string. Callers gate on `isValidKey` first, so an unterminated or
|
|
508
|
+
* trailing-content key never reaches here as a real key.
|
|
509
|
+
*/
|
|
510
|
+
export const stripKeyQuotes = (s, strict = false, line = 0) => {
|
|
386
511
|
const f = s.charCodeAt(0);
|
|
387
|
-
if (f === 39 &&
|
|
388
|
-
return s.slice(1, -1);
|
|
389
|
-
|
|
390
|
-
return unescapeDQ(s.slice(1, -1));
|
|
512
|
+
if ((f === 34 || f === 39) && closingQuoteIndex(s, false) === s.length - 1) {
|
|
513
|
+
return f === 34 ? unescapeDQ(s.slice(1, -1), strict, line) : s.slice(1, -1);
|
|
514
|
+
}
|
|
391
515
|
return s;
|
|
392
516
|
};
|
|
393
517
|
/**
|
|
394
518
|
* Quoted-or-typed scalar, shared by every value position (top-level inline
|
|
395
519
|
* values, flow-sequence/flow-mapping items, block-array scalar items).
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
520
|
+
* §10.1's two quoted-string strict checks — "unterminated quoted string"
|
|
521
|
+
* and "non-whitespace content after closing quote in an inline value" —
|
|
522
|
+
* apply in every one of those positions, so they are enforced here rather
|
|
523
|
+
* than gated to the top level.
|
|
400
524
|
*/
|
|
401
|
-
export const parseQuotedOrTyped = (raw, ctx, line,
|
|
525
|
+
export const parseQuotedOrTyped = (raw, ctx, line, builder, source) => {
|
|
402
526
|
const first = raw.charCodeAt(0);
|
|
403
527
|
if (first === 34 || first === 39) {
|
|
404
|
-
|
|
528
|
+
const close = closingQuoteIndex(raw);
|
|
529
|
+
if (close === raw.length - 1) {
|
|
405
530
|
const unquoted = raw.slice(1, -1);
|
|
406
531
|
const value = first === 34 ? unescapeDQ(unquoted, ctx.strict, line) : unquoted.replace(/\\'/g, "'");
|
|
407
532
|
checkStringLimit(value, line);
|
|
408
533
|
return builder.string(value, line, true);
|
|
409
534
|
}
|
|
410
|
-
if (
|
|
411
|
-
throw
|
|
535
|
+
if (ctx.strict) {
|
|
536
|
+
throw close === -1
|
|
537
|
+
? new LimaError({ code: 'INVALID_QUOTE', line, message: `Lima: unterminated quoted string at line ${line}` })
|
|
538
|
+
: new LimaError({ code: 'INVALID_QUOTE', line, message: `Lima: non-whitespace content after closing quote at line ${line}` });
|
|
412
539
|
}
|
|
540
|
+
// Non-strict: §6.1.2/§6.1.3 + §10.1 — the entire value falls back to
|
|
541
|
+
// a literal string (opening quote included), handled below.
|
|
413
542
|
}
|
|
414
543
|
if (raw !== '' && raw !== 'null' && raw !== '~' && raw !== 'true' && raw !== 'false' &&
|
|
415
544
|
!((first >= 48 && first <= 57) || first === 45 || first === 46)) {
|
|
416
545
|
checkStringLimit(raw, line);
|
|
417
|
-
return builder.string(raw, line, false);
|
|
546
|
+
return builder.string(raw, line, false, source);
|
|
418
547
|
}
|
|
419
|
-
return buildTyped(raw, ctx.strict, line, builder);
|
|
548
|
+
return buildTyped(raw, ctx.strict, line, builder, source);
|
|
420
549
|
};
|
|
421
|
-
export const parseScalarValue = (raw, ctx, line, builder) => {
|
|
550
|
+
export const parseScalarValue = (raw, ctx, line, builder, source) => {
|
|
422
551
|
const first = raw.charCodeAt(0);
|
|
423
552
|
if (ctx.strict && (first === 91 || first === 123)) {
|
|
424
553
|
throw new LimaError({
|
|
@@ -426,5 +555,5 @@ export const parseScalarValue = (raw, ctx, line, builder) => {
|
|
|
426
555
|
message: `Lima: unclosed flow ${first === 91 ? 'sequence' : 'mapping'} at line ${line}`,
|
|
427
556
|
});
|
|
428
557
|
}
|
|
429
|
-
return parseQuotedOrTyped(raw, ctx, line,
|
|
558
|
+
return parseQuotedOrTyped(raw, ctx, line, builder, source);
|
|
430
559
|
};
|
package/dist/scanner.d.ts
CHANGED
|
@@ -9,15 +9,23 @@
|
|
|
9
9
|
* standard `regex` crate is itself RE2-derived and cannot express
|
|
10
10
|
* backtracking-dependent patterns at all.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* regex it replaced — over 100,000 fuzzed and structured cases, the full
|
|
14
|
-
* conformance corpus, and hand-built adversarial edge cases, zero
|
|
15
|
-
* divergences) the exact matching behavior of:
|
|
12
|
+
* Equivalent to this regex:
|
|
16
13
|
*
|
|
17
|
-
* /^(?:([a-zA-Z\d_][a-zA-Z\d_:-]*)|'([^']*)'|"((?:[^"
|
|
14
|
+
* /^(?:([a-zA-Z\d_][a-zA-Z\d_:-]*)|'([^'\n]*)'|"((?:[^"\\\n]|\\[^\n])*)"):( *\n| )/gm
|
|
18
15
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* The scanner replaced an actual regex of this shape; the differential
|
|
17
|
+
* testing that verified the port — over 100,000 fuzzed and structured
|
|
18
|
+
* cases, the full conformance corpus, hand-built adversarial edge cases,
|
|
19
|
+
* zero divergences — was against that original. The quoted-key character
|
|
20
|
+
* classes have since been tightened to exclude only U+000A (Core errata
|
|
21
|
+
* 1.0.8 / 1.0.9), reflected above: `[^'\n]`, `[^"\\\n]`, and `\\[^\n]` (a
|
|
22
|
+
* backslash escapes any character except U+000A; the resulting sequence is
|
|
23
|
+
* then validated as an escape at decode time — an unknown escape like
|
|
24
|
+
* `\` + U+2028 throws in strict, is kept literally in non-strict, exactly
|
|
25
|
+
* like `\q`).
|
|
26
|
+
*
|
|
27
|
+
* Subtle backtracking-dependent cases that are easy to get wrong without
|
|
28
|
+
* empirical verification against the real parser:
|
|
21
29
|
* - `a:b: value` → key "a:b" (colon is a legal mid-key character; the
|
|
22
30
|
* mandatory separator-introducing `:` is whichever colon within the
|
|
23
31
|
* greedily-matched run is the RIGHTMOST one for which a valid
|
|
@@ -30,12 +38,17 @@
|
|
|
30
38
|
* at all (the separator alternative `( *\n| )` requires an actual
|
|
31
39
|
* trailing newline or a literal space; end-of-string alone satisfies
|
|
32
40
|
* neither).
|
|
33
|
-
* -
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
41
|
+
* - A raw U+000A in a quoted key aborts the match — and only U+000A:
|
|
42
|
+
* Core §15.6's single- and double-quoted-key character classes exclude
|
|
43
|
+
* U+000A alone, so U+2028 / U+2029 (and, on already-§3-normalised
|
|
44
|
+
* input, U+000D) are ordinary quoted-key characters here, matching the
|
|
45
|
+
* nested and flow key paths and the Rust/Go scanners. The `\n` escape
|
|
46
|
+
* is unaffected — it decodes to a key containing U+000A.
|
|
47
|
+
* - A backslash directly followed by U+000A inside a double-quoted key
|
|
48
|
+
* aborts the match (`\\[^\n]` — the backslash needs a non-U+000A
|
|
49
|
+
* character after it). A backslash followed by any other character,
|
|
50
|
+
* U+2028 / U+2029 included, is consumed as an escape pair here and
|
|
51
|
+
* resolved at decode time.
|
|
39
52
|
*/
|
|
40
53
|
export interface KeyMatch {
|
|
41
54
|
/** One-based source line of the key. */
|
package/dist/scanner.js
CHANGED
|
@@ -9,15 +9,23 @@
|
|
|
9
9
|
* standard `regex` crate is itself RE2-derived and cannot express
|
|
10
10
|
* backtracking-dependent patterns at all.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* regex it replaced — over 100,000 fuzzed and structured cases, the full
|
|
14
|
-
* conformance corpus, and hand-built adversarial edge cases, zero
|
|
15
|
-
* divergences) the exact matching behavior of:
|
|
12
|
+
* Equivalent to this regex:
|
|
16
13
|
*
|
|
17
|
-
* /^(?:([a-zA-Z\d_][a-zA-Z\d_:-]*)|'([^']*)'|"((?:[^"
|
|
14
|
+
* /^(?:([a-zA-Z\d_][a-zA-Z\d_:-]*)|'([^'\n]*)'|"((?:[^"\\\n]|\\[^\n])*)"):( *\n| )/gm
|
|
18
15
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* The scanner replaced an actual regex of this shape; the differential
|
|
17
|
+
* testing that verified the port — over 100,000 fuzzed and structured
|
|
18
|
+
* cases, the full conformance corpus, hand-built adversarial edge cases,
|
|
19
|
+
* zero divergences — was against that original. The quoted-key character
|
|
20
|
+
* classes have since been tightened to exclude only U+000A (Core errata
|
|
21
|
+
* 1.0.8 / 1.0.9), reflected above: `[^'\n]`, `[^"\\\n]`, and `\\[^\n]` (a
|
|
22
|
+
* backslash escapes any character except U+000A; the resulting sequence is
|
|
23
|
+
* then validated as an escape at decode time — an unknown escape like
|
|
24
|
+
* `\` + U+2028 throws in strict, is kept literally in non-strict, exactly
|
|
25
|
+
* like `\q`).
|
|
26
|
+
*
|
|
27
|
+
* Subtle backtracking-dependent cases that are easy to get wrong without
|
|
28
|
+
* empirical verification against the real parser:
|
|
21
29
|
* - `a:b: value` → key "a:b" (colon is a legal mid-key character; the
|
|
22
30
|
* mandatory separator-introducing `:` is whichever colon within the
|
|
23
31
|
* greedily-matched run is the RIGHTMOST one for which a valid
|
|
@@ -30,12 +38,17 @@
|
|
|
30
38
|
* at all (the separator alternative `( *\n| )` requires an actual
|
|
31
39
|
* trailing newline or a literal space; end-of-string alone satisfies
|
|
32
40
|
* neither).
|
|
33
|
-
* -
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
41
|
+
* - A raw U+000A in a quoted key aborts the match — and only U+000A:
|
|
42
|
+
* Core §15.6's single- and double-quoted-key character classes exclude
|
|
43
|
+
* U+000A alone, so U+2028 / U+2029 (and, on already-§3-normalised
|
|
44
|
+
* input, U+000D) are ordinary quoted-key characters here, matching the
|
|
45
|
+
* nested and flow key paths and the Rust/Go scanners. The `\n` escape
|
|
46
|
+
* is unaffected — it decodes to a key containing U+000A.
|
|
47
|
+
* - A backslash directly followed by U+000A inside a double-quoted key
|
|
48
|
+
* aborts the match (`\\[^\n]` — the backslash needs a non-U+000A
|
|
49
|
+
* character after it). A backslash followed by any other character,
|
|
50
|
+
* U+2028 / U+2029 included, is consumed as an escape pair here and
|
|
51
|
+
* resolved at decode time.
|
|
39
52
|
*/
|
|
40
53
|
const isKeyStartChar = (c) => (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 95; // a-z A-Z 0-9 _
|
|
41
54
|
const isKeyContinueChar = (c) => isKeyStartChar(c) || c === 58 || c === 45; // + : -
|
|
@@ -62,6 +75,10 @@ const matchAt = (s, pos, line, out) => {
|
|
|
62
75
|
const end = s.indexOf("'", pos + 1);
|
|
63
76
|
if (end === -1 || s.charCodeAt(end + 1) !== 58)
|
|
64
77
|
return false;
|
|
78
|
+
for (let i = pos + 1; i < end; i++) {
|
|
79
|
+
if (s.charCodeAt(i) === 10)
|
|
80
|
+
return false; // §15.6 excludes U+000A only
|
|
81
|
+
}
|
|
65
82
|
sepStart = end + 2;
|
|
66
83
|
separator = matchSeparator(s, sepStart);
|
|
67
84
|
if (separator === 0)
|
|
@@ -77,8 +94,7 @@ const matchAt = (s, pos, line, out) => {
|
|
|
77
94
|
const cc = s.charCodeAt(i);
|
|
78
95
|
if (cc === 92) {
|
|
79
96
|
// `\\.` in the source regex — see the module doc comment.
|
|
80
|
-
|
|
81
|
-
if (i + 1 >= s.length || next === 10 || next === 13 || next === 0x2028 || next === 0x2029)
|
|
97
|
+
if (i + 1 >= s.length || s.charCodeAt(i + 1) === 10)
|
|
82
98
|
return false;
|
|
83
99
|
i += 2;
|
|
84
100
|
continue;
|
|
@@ -87,6 +103,8 @@ const matchAt = (s, pos, line, out) => {
|
|
|
87
103
|
closed = true;
|
|
88
104
|
break;
|
|
89
105
|
}
|
|
106
|
+
if (cc === 10)
|
|
107
|
+
return false; // §15.6 excludes U+000A only
|
|
90
108
|
i++;
|
|
91
109
|
}
|
|
92
110
|
if (!closed)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@limaformat/lima",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -20,27 +20,25 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"files": ["dist", "README.md", "LICENSE", "CHANGELOG.md"],
|
|
23
|
-
"engines": {
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"typescript-classic": "npm:typescript@^5.9.3"
|
|
45
|
-
}
|
|
23
|
+
"engines": { "node": ">=18" },
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
|
+
"pretest": "bun run build",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "bun run typecheck && bun test",
|
|
30
|
+
"bench": "bun run bench/index.ts",
|
|
31
|
+
"bench:vs-yaml": "bun run bench/vs-yaml.ts",
|
|
32
|
+
"bench:vs-yaml:node": "node bench/vs-yaml-node.mjs",
|
|
33
|
+
"bench:cross-language": "bun run bench/cross-language.ts",
|
|
34
|
+
"bench:differential": "bun run bench/differential.ts",
|
|
35
|
+
"wordcount": "bun run scripts/wordcount.ts"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/bun": "^1.3.14",
|
|
39
|
+
"@types/js-yaml": "^4.0.9",
|
|
40
|
+
"js-yaml": "^5.2.3",
|
|
41
|
+
"typescript": "^7.0.2",
|
|
42
|
+
"typescript-classic": "npm:typescript@^5.9.3"
|
|
43
|
+
}
|
|
46
44
|
}
|