@limaformat/lima 0.7.0 → 0.7.1
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 +21 -0
- package/dist/block.js +6 -8
- package/dist/core.js +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/references2.js +44 -9
- package/dist/scalars.js +1 -1
- package/dist/structure.js +2 -2
- package/dist/value.d.ts +6 -0
- package/dist/value.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,27 @@ carry their own.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.7.1] — 2026-10-31
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
- A few KB of references could copy millions of nodes before the limit on the result stopped them, and interpolation
|
|
20
|
+
kept growing a value past its limit. Resolution now counts what it copies and stops (References §4.1), with the
|
|
21
|
+
error of an oversized result.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- A value or sequence item followed by whitespace other than spaces kept it at the top level: `draft: false` with a no-break space
|
|
26
|
+
after it is now the boolean, as in a block (Core §4).
|
|
27
|
+
- `.0` and `-.0` are zero, not an underflow.
|
|
28
|
+
- A line with an empty key in a block is an unrecognised line.
|
|
29
|
+
- Spaces before a flow value in a sequence item threw an error without a Lima code.
|
|
30
|
+
- Messages no longer quote control or direction characters of the source, and shorten a long line.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `LimaError` is exported.
|
|
35
|
+
|
|
15
36
|
## [0.7.0] — 2026-10-30
|
|
16
37
|
|
|
17
38
|
Lima Core 1.2 and References 2.1: an unquoted key may be written in any
|
package/dist/block.js
CHANGED
|
@@ -13,7 +13,7 @@ import { stripKeyQuotes, stripComment, stripCommentKeepEscapes, parseQuotedOrTyp
|
|
|
13
13
|
import { KEY_GRAMMAR_HINT, specificKeyHint } from './key-hints.js';
|
|
14
14
|
import { parseFlowOrScalarValue, parseFlowSequence } from './flow.js';
|
|
15
15
|
import { buildBlockScalar } from './block-scalar.js';
|
|
16
|
-
import { LimaError } from './errors.js';
|
|
16
|
+
import { LimaError, printableLine } from './errors.js';
|
|
17
17
|
import { isTrimWhitespace } from './chars.js';
|
|
18
18
|
import { BlockCursor } from './block-cursor.js';
|
|
19
19
|
const DASH_PREFIX_RE = /^-\s+/;
|
|
@@ -167,7 +167,7 @@ const cursorAfterDash = (cursor) => {
|
|
|
167
167
|
/** Core 1.1 §10.1: a line in a block whose key the key grammar (§5.1) rejects, in strict mode. */
|
|
168
168
|
const unrecognizedKeyLine = (line, trimmed) => new LimaError({
|
|
169
169
|
code: 'UNRECOGNIZED_LINE', line,
|
|
170
|
-
message: `Lima: unrecognised line at line ${line}: "${trimmed}"${specificKeyHint(trimmed) ?? KEY_GRAMMAR_HINT}`,
|
|
170
|
+
message: `Lima: unrecognised line at line ${line}: "${printableLine(trimmed)}"${specificKeyHint(trimmed) ?? KEY_GRAMMAR_HINT}`,
|
|
171
171
|
});
|
|
172
172
|
/** Shared block grammar consuming one mutable physical-line cursor. */
|
|
173
173
|
const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
|
|
@@ -208,7 +208,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact =
|
|
|
208
208
|
const keyRaw = colonPos !== -1
|
|
209
209
|
? trimEndSlice(trimmed, 0, colonPos)
|
|
210
210
|
: bareKey ? trimEndSlice(trimmed, 0, trimmed.length - 1) : '';
|
|
211
|
-
if (keyRaw !== '' && !isValidKey(keyRaw)) {
|
|
211
|
+
if ((keyRaw !== '' || colonPos !== -1) && !isValidKey(keyRaw)) {
|
|
212
212
|
// §5.1: not a usable key — an unrecognised line, skipped in non-strict mode and an error in strict
|
|
213
213
|
// mode, as at the top level (Core 1.1 §10.1)
|
|
214
214
|
if (ctx.strict)
|
|
@@ -268,9 +268,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact =
|
|
|
268
268
|
if (items === null)
|
|
269
269
|
items = [];
|
|
270
270
|
const afterDashRaw = cursorAfterDash(cursor);
|
|
271
|
-
|
|
272
|
-
if (afterDash.includes('#'))
|
|
273
|
-
afterDash = stripComment(afterDash);
|
|
271
|
+
const afterDash = afterDashRaw.includes('#') ? stripComment(afterDashRaw) : afterDashRaw.trimEnd();
|
|
274
272
|
const afterDashCol = wantSource ? cursor.indent + dashPrefixLen(cursor) : 0;
|
|
275
273
|
const afterDashTab = wantSource && ctx.tabAdjust ? [ctx.tabAdjust[line - 1] ?? 0] : undefined;
|
|
276
274
|
const dashSource = wantSource ? ({
|
|
@@ -331,7 +329,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact =
|
|
|
331
329
|
// The key sits after `- `, two columns past the dash.
|
|
332
330
|
const valueCol = wantSource ? afterDashCol + valueStart + leadingWsLen(afterDash.slice(valueStart)) : 0;
|
|
333
331
|
pendingColumn = cursor.indent + dashPrefixLen(cursor);
|
|
334
|
-
pendingItem = builder.createMappingWith(key, inlineOrBlockScalar(raw, trimSlice(afterDashRaw, valueStart, afterDashRaw.length) === '|', pendingColumn, line, valueCol, wantSource ? afterDashRaw.
|
|
332
|
+
pendingItem = builder.createMappingWith(key, inlineOrBlockScalar(raw, trimSlice(afterDashRaw, valueStart, afterDashRaw.length) === '|', pendingColumn, line, valueCol, wantSource ? trimSlice(afterDashRaw, valueStart, afterDashRaw.length) : raw, cursor, ctx, builder));
|
|
335
333
|
// the item's later keys at its key column; any other line is the block loop's (§7.2)
|
|
336
334
|
while (cursor.valid && cursor.indent === pendingColumn) {
|
|
337
335
|
const continuationLine = baseLine + cursor.lineIndex;
|
|
@@ -399,7 +397,7 @@ const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact =
|
|
|
399
397
|
const keyRaw = colonPos !== -1
|
|
400
398
|
? trimEndSlice(trimmed, 0, colonPos)
|
|
401
399
|
: bareKey ? trimEndSlice(trimmed, 0, trimmed.length - 1) : '';
|
|
402
|
-
if (keyRaw !== '' && !isValidKey(keyRaw)) {
|
|
400
|
+
if ((keyRaw !== '' || colonPos !== -1) && !isValidKey(keyRaw)) {
|
|
403
401
|
// §5.1: not a usable key — an unrecognised line, skipped in non-strict mode and an error in strict
|
|
404
402
|
// mode, as at the top level (Core 1.1 §10.1)
|
|
405
403
|
if (ctx.strict)
|
package/dist/core.js
CHANGED
|
@@ -221,7 +221,7 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
221
221
|
}
|
|
222
222
|
const val = frontMatter.slice(rawStart, valueEnd);
|
|
223
223
|
const hasHash = val.includes('#');
|
|
224
|
-
const uncommented = hasHash ? stripComment(val) : val;
|
|
224
|
+
const uncommented = hasHash ? stripComment(val) : val.trimEnd();
|
|
225
225
|
// The physical anchor keeps `\#` but drops a real comment, so a
|
|
226
226
|
// `${…}`-shaped token inside a trailing comment is not scanned (§2.4).
|
|
227
227
|
const source = wantSource ? {
|
package/dist/errors.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface LimaDiagnostic {
|
|
|
19
19
|
partial?: string;
|
|
20
20
|
path?: string;
|
|
21
21
|
}
|
|
22
|
+
/** A line of the source for a message: shortened, and no control or direction character reaches a terminal. */
|
|
23
|
+
export declare const printableLine: (text: string) => string;
|
|
22
24
|
export declare class LimaError extends Error {
|
|
23
25
|
readonly code: LimaDiagnosticCode;
|
|
24
26
|
readonly line?: number;
|
package/dist/errors.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* module directly rather than duplicating it) inspect *why* a parse failed
|
|
9
9
|
* without re-parsing the message string.
|
|
10
10
|
*/
|
|
11
|
+
/** A line of the source for a message: shortened, and no control or direction character reaches a terminal. */
|
|
12
|
+
export const printableLine = (text) => (text.length > 120 ? `${text.slice(0, 120)}…` : text).replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
11
13
|
export class LimaError extends Error {
|
|
12
14
|
code;
|
|
13
15
|
line;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,5 +12,6 @@
|
|
|
12
12
|
* retained as its deprecated compatibility alias.
|
|
13
13
|
*/
|
|
14
14
|
export { parseCore, type CoreOptions, type Diagnostic } from './core.js';
|
|
15
|
+
export { LimaError, type LimaDiagnostic, type LimaDiagnosticCode } from './errors.js';
|
|
15
16
|
export { parseReferences, parse, type ParseMode, type ParseOptions } from './references2.js';
|
|
16
17
|
export type ReferencesOptions = import('./references2.js').ParseOptions;
|
package/dist/index.js
CHANGED
package/dist/references2.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Lima References 2.1 resolver. References 1.0 remains isolated in references.ts. */
|
|
2
|
-
import { canonicalString, codepointLength, countNodes, ingestPartialValue, PARTIAL_COUNT_LIMIT, PARTIAL_NAME_LENGTH_LIMIT, PARTIAL_NODE_LIMIT, RESULT_NODE_LIMIT, SCALAR_LENGTH_LIMIT, } from './value.js';
|
|
2
|
+
import { canonicalString, codepointLength, countNodes, ingestPartialValue, PARTIAL_COUNT_LIMIT, PARTIAL_NAME_LENGTH_LIMIT, PARTIAL_NODE_LIMIT, RESOLUTION_COPY_LIMIT, RESULT_NODE_LIMIT, SCALAR_LENGTH_LIMIT, } from './value.js';
|
|
3
3
|
import { isValidUnquotedKey } from './key-grammar.js';
|
|
4
4
|
import { validateDocument } from './document.js';
|
|
5
5
|
import { NESTING_DEPTH_LIMIT, parseCoreValidated, parseCoreWithPositions, toPlainValue, } from './core.js';
|
|
@@ -13,13 +13,44 @@ const TARGET_EDGE_BUDGET = MAX_EDGES - 1;
|
|
|
13
13
|
const referenceResourceStatuses = new WeakMap();
|
|
14
14
|
/** Copies a pure-reference target and stamps the new insertion without
|
|
15
15
|
* discarding provenance already attached to the copied root (R-112). */
|
|
16
|
-
const copyWithInsertion = (target, insertedAt) => {
|
|
16
|
+
const copyWithInsertion = (target, insertedAt, ctx) => {
|
|
17
|
+
const earliest = ctx.earliestInsertion;
|
|
18
|
+
ctx.earliestInsertion = earliest === null ? insertedAt : earliestParticipant([earliest, insertedAt]);
|
|
19
|
+
if (!ctx.overflowed) {
|
|
20
|
+
ctx.copied += countPositioned(target);
|
|
21
|
+
ctx.overflowed = ctx.copied > RESOLUTION_COPY_LIMIT;
|
|
22
|
+
}
|
|
23
|
+
// the result is lost by now: resolution only goes on to see the earliest insertion
|
|
24
|
+
if (ctx.overflowed)
|
|
25
|
+
return { kind: 'null', line: 0 };
|
|
17
26
|
const copy = deepCopyPositioned(target);
|
|
18
27
|
const priorInsertions = copy.insertedAt === undefined
|
|
19
28
|
? copy.priorInsertions
|
|
20
29
|
: [...(copy.priorInsertions ?? []), copy.insertedAt];
|
|
21
30
|
return { ...copy, insertedAt, priorInsertions };
|
|
22
31
|
};
|
|
32
|
+
const countPositioned = (v) => {
|
|
33
|
+
let count = 1;
|
|
34
|
+
if (v.kind === 'array')
|
|
35
|
+
for (const item of v.items)
|
|
36
|
+
count += countPositioned(item);
|
|
37
|
+
if (v.kind === 'mapping')
|
|
38
|
+
for (const child of v.entries.values())
|
|
39
|
+
count += countPositioned(child);
|
|
40
|
+
return count;
|
|
41
|
+
};
|
|
42
|
+
const newContext = (cache) => ({
|
|
43
|
+
diagnostics: [], copied: 0, overflowed: false, earliestInsertion: null, ...(cache ? { cache } : {}),
|
|
44
|
+
});
|
|
45
|
+
/** The error of a document whose references would copy past RESOLUTION_COPY_LIMIT: that of an oversized result. */
|
|
46
|
+
const overflowError = (ctx) => {
|
|
47
|
+
const winner = ctx.earliestInsertion;
|
|
48
|
+
return new LimaError({
|
|
49
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
50
|
+
...(winner?.offset !== undefined ? { column: winner.offset + 1 } : {}),
|
|
51
|
+
message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
23
54
|
const addDiagnostic = (ctx, diagnostic, offset = 0) => {
|
|
24
55
|
ctx.diagnostics.push({ ...diagnostic, offset });
|
|
25
56
|
};
|
|
@@ -58,10 +89,10 @@ export const resolveDocumentReferenceTarget = (parsed, path) => {
|
|
|
58
89
|
return { status: resourceStatus };
|
|
59
90
|
if (resourceStatus === 'invalid')
|
|
60
91
|
return { status: 'unresolved' };
|
|
61
|
-
const ctx =
|
|
92
|
+
const ctx = newContext(new WeakMap());
|
|
62
93
|
const stack = new Set([target.root]);
|
|
63
94
|
const resolved = resolveNode(target.root, document, new Map(), ctx, TARGET_EDGE_BUDGET, stack);
|
|
64
|
-
if (!resolved.complete || ctx.diagnostics.length > 0)
|
|
95
|
+
if (!resolved.complete || ctx.overflowed || ctx.diagnostics.length > 0)
|
|
65
96
|
return { status: 'unresolved' };
|
|
66
97
|
const selected = lookupPath(resolved.value, target.tail);
|
|
67
98
|
return selected === undefined
|
|
@@ -116,9 +147,9 @@ const documentReferenceResourceStatus = (parsed) => {
|
|
|
116
147
|
const cached = referenceResourceStatuses.get(document);
|
|
117
148
|
if (cached !== undefined)
|
|
118
149
|
return cached;
|
|
119
|
-
const ctx =
|
|
150
|
+
const ctx = newContext(new WeakMap());
|
|
120
151
|
const resolved = resolveDocument(document, new Map(), ctx);
|
|
121
|
-
let valid = !ctx.diagnostics.some((diagnostic) => diagnostic.code === 'RESOURCE_LIMIT');
|
|
152
|
+
let valid = !ctx.overflowed && !ctx.diagnostics.some((diagnostic) => diagnostic.code === 'RESOURCE_LIMIT');
|
|
122
153
|
if (valid) {
|
|
123
154
|
try {
|
|
124
155
|
assertResolvedDocumentResourceLimits(resolved);
|
|
@@ -220,7 +251,7 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
220
251
|
// `partialToPositioned` never annotates partial targets: partials are
|
|
221
252
|
// inert and have no source token before this document insertion.
|
|
222
253
|
return {
|
|
223
|
-
value: copyWithInsertion(target, { line: pure.line, offset: pure.offset, token: pure.token }),
|
|
254
|
+
value: copyWithInsertion(target, { line: pure.line, offset: pure.offset, token: pure.token }, ctx),
|
|
224
255
|
complete: true,
|
|
225
256
|
};
|
|
226
257
|
}
|
|
@@ -235,7 +266,7 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
235
266
|
return {
|
|
236
267
|
// `selected` may itself be the root inserted by an earlier reference;
|
|
237
268
|
// both it and this outer copy participate in final-structure errors.
|
|
238
|
-
value: copyWithInsertion(selected, { line: pure.line, offset: pure.offset, token: pure.token }),
|
|
269
|
+
value: copyWithInsertion(selected, { line: pure.line, offset: pure.offset, token: pure.token }, ctx),
|
|
239
270
|
complete: true,
|
|
240
271
|
};
|
|
241
272
|
}
|
|
@@ -280,6 +311,8 @@ const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stac
|
|
|
280
311
|
code: 'RESOURCE_LIMIT', line: first.line, token: first.token,
|
|
281
312
|
message: `Lima: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${first.line}`,
|
|
282
313
|
}, first.offset);
|
|
314
|
+
// the document has failed; a value that holds this one still exceeds the limit, and none grows further
|
|
315
|
+
output = output.slice(0, 2 * (SCALAR_LENGTH_LIMIT + 1));
|
|
283
316
|
}
|
|
284
317
|
return { value: { ...node, value: output, references2: unresolvedTokens }, complete };
|
|
285
318
|
};
|
|
@@ -373,8 +406,10 @@ const parseInternal = (frontMatter, options, useResolveCache) => {
|
|
|
373
406
|
return parseCoreValidated(frontMatter, options);
|
|
374
407
|
}
|
|
375
408
|
const document = parseCoreWithPositions(frontMatter, { strict: options?.strict ?? false, onWarning: options?.onWarning });
|
|
376
|
-
const ctx =
|
|
409
|
+
const ctx = newContext(useResolveCache ? new WeakMap() : undefined);
|
|
377
410
|
const resolved = resolveDocument(document, partials, ctx);
|
|
411
|
+
if (ctx.overflowed)
|
|
412
|
+
throw overflowError(ctx);
|
|
378
413
|
if (options?.strict)
|
|
379
414
|
for (const value of resolved.values())
|
|
380
415
|
scanUnresolved(value, ctx);
|
package/dist/scalars.js
CHANGED
|
@@ -187,7 +187,7 @@ const parseDateUTC = (str, strict = false, line = 0) => {
|
|
|
187
187
|
const NUMBER_RE = /^-?(?:(?:0|[1-9][0-9]*)(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
|
|
188
188
|
const DATE_PRE_RE = /\d[\d\-:.\/a-zA-Z]{4,}/;
|
|
189
189
|
const isFloatForm = (str) => str.includes('.') || str.includes('e') || str.includes('E');
|
|
190
|
-
const isZeroLiteral = (str) => /^0+(
|
|
190
|
+
const isZeroLiteral = (str) => /^(?:0+(?:\.0+)?|\.0+)$/.test(str.replace(/^-/, '').split(/[eE]/)[0]);
|
|
191
191
|
export const NO_SPAN_VALUE = Symbol('NO_SPAN_VALUE');
|
|
192
192
|
/**
|
|
193
193
|
* Parses allocation-free scalar forms whose complete grammar can be proven
|
package/dist/structure.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* these lines, as Core 1.0 did in both modes.
|
|
16
16
|
*/
|
|
17
17
|
import { isTrimWhitespace } from './chars.js';
|
|
18
|
-
import { LimaError } from './errors.js';
|
|
18
|
+
import { LimaError, printableLine } from './errors.js';
|
|
19
19
|
import { isValidUnquotedKey } from './key-grammar.js';
|
|
20
20
|
import { KEY_GRAMMAR_HINT, specificKeyHint } from './key-hints.js';
|
|
21
21
|
/**
|
|
@@ -59,7 +59,7 @@ export function checkRangeLines(frontMatter, start, end, line, owner, valueStart
|
|
|
59
59
|
const text = frontMatter.slice(pos, stop);
|
|
60
60
|
throw new LimaError({
|
|
61
61
|
code: 'UNRECOGNIZED_LINE', line: current,
|
|
62
|
-
message: `Lima: unrecognised line at line ${current}: "${text}"${hint(text)}`,
|
|
62
|
+
message: `Lima: unrecognised line at line ${current}: "${printableLine(text)}"${hint(text)}`,
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
if (owner === 'mapping' && base === -1)
|
package/dist/value.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ export declare const PARTIAL_COUNT_LIMIT = 128;
|
|
|
60
60
|
export declare const PARTIAL_NAME_LENGTH_LIMIT = 128;
|
|
61
61
|
export declare const PARTIAL_NODE_LIMIT = 4096;
|
|
62
62
|
export declare const RESULT_NODE_LIMIT = 65536;
|
|
63
|
+
/**
|
|
64
|
+
* Nodes a resolution may copy (References §4.1: limits are validated during expansion). Each value is resolved
|
|
65
|
+
* for at most four edge budgets, so a document within RESULT_NODE_LIMIT stays far below this. Past it the result
|
|
66
|
+
* cannot fit, and copying on would only fill the memory: a few KB of references can reach millions of nodes.
|
|
67
|
+
*/
|
|
68
|
+
export declare const RESOLUTION_COPY_LIMIT: number;
|
|
63
69
|
export declare const SCALAR_LENGTH_LIMIT = 16384;
|
|
64
70
|
/**
|
|
65
71
|
* Counts Unicode code points (what every length-limit check in this
|
package/dist/value.js
CHANGED
|
@@ -63,6 +63,12 @@ export const PARTIAL_COUNT_LIMIT = 128;
|
|
|
63
63
|
export const PARTIAL_NAME_LENGTH_LIMIT = 128;
|
|
64
64
|
export const PARTIAL_NODE_LIMIT = 4096;
|
|
65
65
|
export const RESULT_NODE_LIMIT = 65536;
|
|
66
|
+
/**
|
|
67
|
+
* Nodes a resolution may copy (References §4.1: limits are validated during expansion). Each value is resolved
|
|
68
|
+
* for at most four edge budgets, so a document within RESULT_NODE_LIMIT stays far below this. Past it the result
|
|
69
|
+
* cannot fit, and copying on would only fill the memory: a few KB of references can reach millions of nodes.
|
|
70
|
+
*/
|
|
71
|
+
export const RESOLUTION_COPY_LIMIT = 16 * RESULT_NODE_LIMIT;
|
|
66
72
|
export const SCALAR_LENGTH_LIMIT = 16384;
|
|
67
73
|
/**
|
|
68
74
|
* Counts Unicode code points (what every length-limit check in this
|
package/package.json
CHANGED