@limaformat/lima 0.7.0 → 0.8.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 +29 -0
- package/README.md +25 -1
- package/dist/block.d.ts +3 -0
- package/dist/block.js +7 -9
- package/dist/core.js +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/flow.d.ts +18 -0
- package/dist/flow.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/outline.d.ts +28 -0
- package/dist/outline.js +339 -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,35 @@ carry their own.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.8.0] — 2026-11-05
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `outline(input)` returns decoded key paths, key and separator spans, comments,
|
|
20
|
+
and parser diagnostics for editors. Positions use UTF-16 code units in the
|
|
21
|
+
BOM-free physical input, with tolerant recovery for unfinished documents.
|
|
22
|
+
|
|
23
|
+
## [0.7.1] — 2026-10-31
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
- A few KB of references could copy millions of nodes before the limit on the result stopped them, and interpolation
|
|
28
|
+
kept growing a value past its limit. Resolution now counts what it copies and stops (References §4.1), with the
|
|
29
|
+
error of an oversized result.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- A value or sequence item followed by whitespace other than spaces kept it at the top level: `draft: false` with a no-break space
|
|
34
|
+
after it is now the boolean, as in a block (Core §4).
|
|
35
|
+
- `.0` and `-.0` are zero, not an underflow.
|
|
36
|
+
- A line with an empty key in a block is an unrecognised line.
|
|
37
|
+
- Spaces before a flow value in a sequence item threw an error without a Lima code.
|
|
38
|
+
- Messages no longer quote control or direction characters of the source, and shorten a long line.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `LimaError` is exported.
|
|
43
|
+
|
|
15
44
|
## [0.7.0] — 2026-10-30
|
|
16
45
|
|
|
17
46
|
Lima Core 1.2 and References 2.1: an unquoted key may be written in any
|
package/README.md
CHANGED
|
@@ -35,8 +35,9 @@ tags:
|
|
|
35
35
|
`parse` accepts the raw content **between** the frontmatter delimiters
|
|
36
36
|
(`---`) — stripping them is the caller's job.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Four functions are exported:
|
|
39
39
|
|
|
40
|
+
- `outline(input)` — source positions for editor keys, comments and diagnostics.
|
|
40
41
|
- `parseCore(input, options?)` — the base format only.
|
|
41
42
|
- `parse(input, options?)` — the primary References <!-- facts:language.references_version -->2.1<!-- /facts --> parser; adds
|
|
42
43
|
`${key}` document references and `$(key)` external partials.
|
|
@@ -49,6 +50,29 @@ parse(frontmatter, { partials: { author: 'Alice' } })
|
|
|
49
50
|
parse(frontmatter, { mode: 'core' }) // same reference-unaware path as parseCore
|
|
50
51
|
```
|
|
51
52
|
|
|
53
|
+
## Editor outlines
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { outline } from '@limaformat/lima'
|
|
57
|
+
|
|
58
|
+
const { keys, comments, diagnostics } = outline('author: {name: Ada} # byline')
|
|
59
|
+
// keys: paths ['author'] and ['author', 'name'], with key and separator spans
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`outline` tolerates unfinished content and returns keys in source order, including
|
|
63
|
+
all duplicate occurrences. Sequence paths include string indices, such as
|
|
64
|
+
`['authors', '0', 'name']`. Quoted keys are decoded; their spans include the quotes.
|
|
65
|
+
Comments run from `#` through the physical line's end, excluding its terminator.
|
|
66
|
+
Block scalar content contributes no keys or comments.
|
|
67
|
+
|
|
68
|
+
Spans are half-open. Lines and columns start at 1; columns and offsets count
|
|
69
|
+
UTF-16 code units (`offsetUnit: 'utf16-code-unit'`). Positions refer to the raw
|
|
70
|
+
input with a leading BOM removed; CRLF and tabs keep their original widths.
|
|
71
|
+
Diagnostics contain strict `parse`'s first error, followed by non-strict duplicate
|
|
72
|
+
warnings. A resource limit produces diagnostics and no spans. Structure remains
|
|
73
|
+
reference-inert: keys copied by reference substitution have no source occurrence
|
|
74
|
+
and are omitted; only the diagnostic pass evaluates references, without partials.
|
|
75
|
+
|
|
52
76
|
The **[guide](https://github.com/limaformat/lima/blob/main/docs/guide.md)**
|
|
53
77
|
covers Core <!-- facts:language.core_version -->1.2<!-- /facts --> and References <!-- facts:language.references_version -->2.1<!-- /facts --> syntax, resource limits, strict mode, and the
|
|
54
78
|
public API. The normative rules are defined in the
|
package/dist/block.d.ts
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { type ParseContext } from './normalize.js';
|
|
12
12
|
import type { ValueBuilder } from './builder.js';
|
|
13
|
+
import { BlockCursor } from './block-cursor.js';
|
|
13
14
|
export declare const findKeySep: (s: string) => number;
|
|
15
|
+
/** Shared block grammar consuming one mutable physical-line cursor. */
|
|
16
|
+
export declare const parseCursorBlock: <V, M>(cursor: BlockCursor, baseIndent: number, ctx: ParseContext, baseLine: number, builder: ValueBuilder<V, M>, compact?: boolean) => V | null;
|
|
14
17
|
/** Complete block grammar over one UTF-16 source range. */
|
|
15
18
|
export type BlockDepthRisk = {
|
|
16
19
|
mayExceed: boolean;
|
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,10 +167,10 @@ 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
|
-
const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
|
|
173
|
+
export const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder, compact = false) => {
|
|
174
174
|
const wantSource = builder.positions === true;
|
|
175
175
|
let items = null;
|
|
176
176
|
let entries = null;
|
|
@@ -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/flow.d.ts
CHANGED
|
@@ -2,7 +2,25 @@
|
|
|
2
2
|
import { type ParseContext } from './normalize.js';
|
|
3
3
|
import type { ValueBuilder } from './builder.js';
|
|
4
4
|
import type { ReferenceSource } from './reference-tokens2.js';
|
|
5
|
+
/** Stateful comma-item cursor over a flow container's original string. */
|
|
6
|
+
export declare class FlowCursor {
|
|
7
|
+
private readonly source;
|
|
8
|
+
private readonly end;
|
|
9
|
+
itemStart: number;
|
|
10
|
+
itemEnd: number;
|
|
11
|
+
private nextStart;
|
|
12
|
+
private done;
|
|
13
|
+
constructor(source: string, start: number, end: number);
|
|
14
|
+
next(): boolean;
|
|
15
|
+
get isLast(): boolean;
|
|
16
|
+
}
|
|
5
17
|
export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V[] | null;
|
|
18
|
+
/**
|
|
19
|
+
* Index of the key/value `: ` separator within a flow-mapping item — the
|
|
20
|
+
* first `: ` that is not inside a quoted key (§5.1). Returns -1 when the
|
|
21
|
+
* item has no separator.
|
|
22
|
+
*/
|
|
23
|
+
export declare const flowItemSeparator: (val: string, start: number, end: number) => number;
|
|
6
24
|
export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V | null;
|
|
7
25
|
/** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
|
|
8
26
|
export declare const parseFlowOrScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V;
|
package/dist/flow.js
CHANGED
|
@@ -71,7 +71,7 @@ const trimEnd = (source, start, end) => {
|
|
|
71
71
|
return end;
|
|
72
72
|
};
|
|
73
73
|
/** Stateful comma-item cursor over a flow container's original string. */
|
|
74
|
-
class FlowCursor {
|
|
74
|
+
export class FlowCursor {
|
|
75
75
|
source;
|
|
76
76
|
end;
|
|
77
77
|
itemStart = 0;
|
|
@@ -162,7 +162,7 @@ export const parseFlowSequence = (val, ctx, line, builder, source) => {
|
|
|
162
162
|
* first `: ` that is not inside a quoted key (§5.1). Returns -1 when the
|
|
163
163
|
* item has no separator.
|
|
164
164
|
*/
|
|
165
|
-
const flowItemSeparator = (val, start, end) => {
|
|
165
|
+
export const flowItemSeparator = (val, start, end) => {
|
|
166
166
|
let scan = start;
|
|
167
167
|
if (val.charCodeAt(start) === 34 || val.charCodeAt(start) === 39) {
|
|
168
168
|
// §5.2: a single-quoted key is literal, so its first `'` closes.
|
package/dist/index.d.ts
CHANGED
|
@@ -12,5 +12,7 @@
|
|
|
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;
|
|
18
|
+
export { outline, type LimaPoint, type LimaSpan, type LimaOutlineKey, type LimaOutline } from './outline.js';
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type LimaDiagnostic } from './errors.js';
|
|
2
|
+
export interface LimaPoint {
|
|
3
|
+
/** One-based source line. */
|
|
4
|
+
readonly line: number;
|
|
5
|
+
/** One-based column in UTF-16 code units. */
|
|
6
|
+
readonly column: number;
|
|
7
|
+
/** UTF-16 code units from the start of the BOM-free input. */
|
|
8
|
+
readonly offset: number;
|
|
9
|
+
}
|
|
10
|
+
export interface LimaSpan {
|
|
11
|
+
readonly start: LimaPoint;
|
|
12
|
+
/** Exclusive end. */
|
|
13
|
+
readonly end: LimaPoint;
|
|
14
|
+
}
|
|
15
|
+
export interface LimaOutlineKey {
|
|
16
|
+
readonly key: string;
|
|
17
|
+
readonly path: readonly string[];
|
|
18
|
+
readonly span: LimaSpan;
|
|
19
|
+
readonly separator: LimaSpan;
|
|
20
|
+
}
|
|
21
|
+
export interface LimaOutline {
|
|
22
|
+
readonly offsetUnit: 'utf16-code-unit';
|
|
23
|
+
readonly keys: readonly LimaOutlineKey[];
|
|
24
|
+
readonly comments: readonly LimaSpan[];
|
|
25
|
+
readonly diagnostics: readonly LimaDiagnostic[];
|
|
26
|
+
}
|
|
27
|
+
/** Source structure for editors; references are only evaluated by the diagnostic oracle. */
|
|
28
|
+
export declare function outline(frontMatter: string): LimaOutline;
|
package/dist/outline.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { BlockCursor } from './block-cursor.js';
|
|
2
|
+
import { findKeySep, parseCursorBlock } from './block.js';
|
|
3
|
+
import { buildBlockScalar } from './block-scalar.js';
|
|
4
|
+
import { parseCore } from './core.js';
|
|
5
|
+
import { LimaError } from './errors.js';
|
|
6
|
+
import { FlowCursor, flowItemSeparator, parseFlowOrScalarValue } from './flow.js';
|
|
7
|
+
import { byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, checkKeyLength, checkDuplicateKey, } from './normalize.js';
|
|
8
|
+
import { parse } from './references2.js';
|
|
9
|
+
import { scanKeys } from './scanner.js';
|
|
10
|
+
import { isValidKey, stripComment, stripCommentKeepEscapes, stripKeyQuotes } from './scalars.js';
|
|
11
|
+
/** Core §3 normalization, with a boundary map back to the BOM-free physical input. */
|
|
12
|
+
function normalize(input) {
|
|
13
|
+
let source = '';
|
|
14
|
+
const offsets = [];
|
|
15
|
+
let leading = true;
|
|
16
|
+
for (let i = 0; i < input.length; i++) {
|
|
17
|
+
const c = input[i];
|
|
18
|
+
if (c === '\r' || c === '\n') {
|
|
19
|
+
while (source.endsWith(' ')) {
|
|
20
|
+
source = source.slice(0, -1);
|
|
21
|
+
offsets.pop();
|
|
22
|
+
}
|
|
23
|
+
offsets.push(i);
|
|
24
|
+
source += '\n';
|
|
25
|
+
if (c === '\r' && input[i + 1] === '\n')
|
|
26
|
+
i++;
|
|
27
|
+
leading = true;
|
|
28
|
+
}
|
|
29
|
+
else if (leading && c === '\t') {
|
|
30
|
+
source += ' ';
|
|
31
|
+
offsets.push(i, i);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
source += c;
|
|
35
|
+
offsets.push(i);
|
|
36
|
+
if (c !== ' ')
|
|
37
|
+
leading = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
while (source.endsWith(' ')) {
|
|
41
|
+
source = source.slice(0, -1);
|
|
42
|
+
offsets.pop();
|
|
43
|
+
}
|
|
44
|
+
offsets.push(input.length);
|
|
45
|
+
return { source, offsets };
|
|
46
|
+
}
|
|
47
|
+
/** Editor positions count UTF-16 units, including CRLF's two units. */
|
|
48
|
+
function spanReader(input, offsets) {
|
|
49
|
+
const lines = [0];
|
|
50
|
+
for (let i = 0; i < input.length; i++) {
|
|
51
|
+
if (input[i] === '\r') {
|
|
52
|
+
if (input[i + 1] === '\n')
|
|
53
|
+
i++;
|
|
54
|
+
lines.push(i + 1);
|
|
55
|
+
}
|
|
56
|
+
else if (input[i] === '\n')
|
|
57
|
+
lines.push(i + 1);
|
|
58
|
+
}
|
|
59
|
+
const point = (position) => {
|
|
60
|
+
const offset = position;
|
|
61
|
+
let low = 0, high = lines.length;
|
|
62
|
+
while (low + 1 < high) {
|
|
63
|
+
const middle = (low + high) >>> 1;
|
|
64
|
+
if (lines[middle] <= offset)
|
|
65
|
+
low = middle;
|
|
66
|
+
else
|
|
67
|
+
high = middle;
|
|
68
|
+
}
|
|
69
|
+
return { line: low + 1, column: offset - lines[low] + 1, offset };
|
|
70
|
+
};
|
|
71
|
+
return (start, end) => ({ start: point(offsets[start]),
|
|
72
|
+
end: point(end > start ? offsets[end - 1] + 1 : offsets[end]) });
|
|
73
|
+
}
|
|
74
|
+
/** Physical flow sites use the parser's item and separator readers. */
|
|
75
|
+
function flowSites(source, start, end, line, sites) {
|
|
76
|
+
const value = source.slice(start, end).trimEnd();
|
|
77
|
+
const first = value[0], last = value.at(-1);
|
|
78
|
+
if (!((first === '{' && last === '}') || (first === '[' && last === ']')))
|
|
79
|
+
return;
|
|
80
|
+
const cursor = new FlowCursor(value, 1, value.length - 1);
|
|
81
|
+
while (cursor.next()) {
|
|
82
|
+
const a = cursor.itemStart, b = cursor.itemEnd;
|
|
83
|
+
if (first === '[') {
|
|
84
|
+
if (value[a] === '{')
|
|
85
|
+
flowSites(source, start + a, start + b, line, sites);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const colon = flowItemSeparator(value, a, b);
|
|
89
|
+
if (colon === -1)
|
|
90
|
+
continue;
|
|
91
|
+
const raw = value.slice(a, colon).trimEnd();
|
|
92
|
+
if (isValidKey(raw))
|
|
93
|
+
sites.push({ key: stripKeyQuotes(raw), start: start + a,
|
|
94
|
+
end: start + a + raw.length, colon: start + colon, line, sequence: false });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The real block/flow parsers build an occurrence tree. With warnings enabled,
|
|
99
|
+
* hasMappingKey records a key's line before its value is read. Sequence-item
|
|
100
|
+
* entries without duplicate checks are anchored by their value's first line.
|
|
101
|
+
* Physical sites are assigned in source order when walking the finished tree,
|
|
102
|
+
* so a flow key named like its enclosing key cannot steal the parent's site.
|
|
103
|
+
*/
|
|
104
|
+
function outlineBuilder(currentLine) {
|
|
105
|
+
const leaf = (line) => ({ line });
|
|
106
|
+
return {
|
|
107
|
+
null: leaf, bool: (_, line) => leaf(line), int: (_, line) => leaf(line),
|
|
108
|
+
float: (_, line) => leaf(line), string: (_, line) => leaf(line), instant: (_, line) => leaf(line),
|
|
109
|
+
array: (items, line) => ({ line, items }), createMapping: () => [],
|
|
110
|
+
createMappingWith: (key, value) => [{ key, line: value.line, exact: false, sequence: true, value }],
|
|
111
|
+
hasMappingKey: (entries, key) => {
|
|
112
|
+
entries.push({ key, line: currentLine(), exact: true, sequence: false });
|
|
113
|
+
return false; // diagnostics come from separate public-parser passes
|
|
114
|
+
},
|
|
115
|
+
setMapping: (entries, key, value) => {
|
|
116
|
+
const entry = entries.findLast((e) => e.key === key && e.value === undefined);
|
|
117
|
+
if (entry)
|
|
118
|
+
entry.value = value;
|
|
119
|
+
else
|
|
120
|
+
entries.push({ key, line: value.line, exact: false, sequence: false, value });
|
|
121
|
+
},
|
|
122
|
+
mappingMaxDepth: () => 0, mapping: (entries, line) => ({ line, entries }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Source structure for editors; references are only evaluated by the diagnostic oracle. */
|
|
126
|
+
export function outline(frontMatter) {
|
|
127
|
+
const diagnostics = [];
|
|
128
|
+
const keys = [];
|
|
129
|
+
const comments = [];
|
|
130
|
+
const result = { offsetUnit: 'utf16-code-unit', keys, comments, diagnostics };
|
|
131
|
+
const addWarning = (warning) => {
|
|
132
|
+
if (!diagnostics.some((d) => d.code === warning.code && d.line === warning.line && d.key === warning.key))
|
|
133
|
+
diagnostics.push(warning);
|
|
134
|
+
};
|
|
135
|
+
let limited = false;
|
|
136
|
+
try {
|
|
137
|
+
parse(frontMatter, { strict: true });
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!(error instanceof LimaError))
|
|
141
|
+
throw error;
|
|
142
|
+
diagnostics.push({ ...error, message: error.message });
|
|
143
|
+
limited = error.code === 'RESOURCE_LIMIT';
|
|
144
|
+
}
|
|
145
|
+
// Also check limits beyond strict mode's first error, and collect duplicate
|
|
146
|
+
// warnings without resolving references in the tolerant structural pass.
|
|
147
|
+
try {
|
|
148
|
+
parseCore(frontMatter, { onWarning: (warning) => addWarning(warning) });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (!(error instanceof LimaError))
|
|
152
|
+
throw error;
|
|
153
|
+
if (error.code === 'RESOURCE_LIMIT') {
|
|
154
|
+
limited = true;
|
|
155
|
+
if (!diagnostics.some((d) => d.code === error.code && d.line === error.line))
|
|
156
|
+
diagnostics.push({ ...error, message: error.message });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (limited)
|
|
160
|
+
return result;
|
|
161
|
+
try {
|
|
162
|
+
if (byteLength(frontMatter) > DOCUMENT_SIZE_LIMIT)
|
|
163
|
+
throw new LimaError({ code: 'RESOURCE_LIMIT', line: 1,
|
|
164
|
+
message: `Lima: document exceeds maximum size of ${DOCUMENT_SIZE_LIMIT} bytes at line 1` });
|
|
165
|
+
const input = frontMatter.startsWith('\uFEFF') ? frontMatter.slice(1) : frontMatter;
|
|
166
|
+
const { source, offsets } = normalize(input);
|
|
167
|
+
const span = spanReader(input, offsets);
|
|
168
|
+
const matches = scanKeys(source);
|
|
169
|
+
if (matches.length > TOP_LEVEL_KEY_LIMIT)
|
|
170
|
+
throw new LimaError({ code: 'RESOURCE_LIMIT', line: 1,
|
|
171
|
+
message: `Lima: too many top-level key entries (max ${TOP_LEVEL_KEY_LIMIT}) at line 1` });
|
|
172
|
+
const scalarLines = new Set();
|
|
173
|
+
const ctx = { strict: false, onWarning: (warning) => addWarning(warning) };
|
|
174
|
+
const topKeys = new Set();
|
|
175
|
+
const emit = (site, path) => {
|
|
176
|
+
checkKeyLength(site.key, site.line);
|
|
177
|
+
const colon = span(site.colon, site.colon + 1);
|
|
178
|
+
const separator = input[colon.end.offset] === ' '
|
|
179
|
+
? { start: colon.start, end: { ...colon.end, column: colon.end.column + 1, offset: colon.end.offset + 1 } }
|
|
180
|
+
: colon;
|
|
181
|
+
keys.push({ key: site.key, path: [...path, site.key], span: span(site.start, site.end), separator });
|
|
182
|
+
const lineEnd = source.indexOf('\n', site.colon);
|
|
183
|
+
const end = lineEnd === -1 ? source.length : lineEnd;
|
|
184
|
+
if (source.slice(site.colon + 1, end).trim() === '|') {
|
|
185
|
+
const body = source.slice(end + 1).split('\n');
|
|
186
|
+
if (body.at(-1) === '')
|
|
187
|
+
body.pop();
|
|
188
|
+
const column = site.start - (source.lastIndexOf('\n', site.start - 1) + 1);
|
|
189
|
+
const consumed = buildBlockScalar(body, column, site.line, ctx, outlineBuilder(() => site.line)).consumed;
|
|
190
|
+
for (let j = 1; j <= consumed; j++)
|
|
191
|
+
scalarLines.add(site.line + j);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const walk = (value, path, sites, position, depth = 0) => {
|
|
195
|
+
if (value.entries || value.items)
|
|
196
|
+
depth++;
|
|
197
|
+
if (depth > NESTING_DEPTH_LIMIT)
|
|
198
|
+
throw new LimaError({ code: 'RESOURCE_LIMIT', line: 1,
|
|
199
|
+
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line 1` });
|
|
200
|
+
let keyColumn;
|
|
201
|
+
const seen = new Set();
|
|
202
|
+
for (const entry of value.entries ?? []) {
|
|
203
|
+
const site = sites.find((s) => s.key === entry.key && s.start > position.after &&
|
|
204
|
+
!scalarLines.has(s.line) && s.sequence === entry.sequence &&
|
|
205
|
+
(entry.exact ? s.line === entry.line : s.line <= entry.line) &&
|
|
206
|
+
(entry.exact || entry.sequence || keyColumn === undefined ||
|
|
207
|
+
s.start - (source.lastIndexOf('\n', s.start - 1) + 1) === keyColumn));
|
|
208
|
+
// Incomplete content can produce a tree entry without a readable
|
|
209
|
+
// physical key. Omit that entry and its children, then keep walking.
|
|
210
|
+
if (!site)
|
|
211
|
+
continue;
|
|
212
|
+
keyColumn ??= site.start - (source.lastIndexOf('\n', site.start - 1) + 1);
|
|
213
|
+
if (entry.exact)
|
|
214
|
+
checkDuplicateKey(seen.has(entry.key), entry.key, site.line, ctx);
|
|
215
|
+
seen.add(entry.key);
|
|
216
|
+
emit(site, path);
|
|
217
|
+
position.after = site.start;
|
|
218
|
+
if (entry.value)
|
|
219
|
+
walk(entry.value, [...path, entry.key], sites, position, depth);
|
|
220
|
+
}
|
|
221
|
+
value.items?.forEach((item, index) => walk(item, [...path, String(index)], sites, position, depth));
|
|
222
|
+
};
|
|
223
|
+
for (let i = 0; i < matches.length; i++) {
|
|
224
|
+
const match = matches[i];
|
|
225
|
+
const end = matches[i + 1]?.matchStart ?? source.length;
|
|
226
|
+
const key = match.unquoted ?? match.singleQuoted ?? stripKeyQuotes(source.slice(match.matchStart, match.sepStart - 1));
|
|
227
|
+
const site = { key, start: match.matchStart, end: match.sepStart - 1,
|
|
228
|
+
colon: match.sepStart - 1, line: match.line, sequence: false };
|
|
229
|
+
checkDuplicateKey(topKeys.has(key), key, site.line, ctx);
|
|
230
|
+
topKeys.add(key);
|
|
231
|
+
emit(site, []);
|
|
232
|
+
const sites = [];
|
|
233
|
+
const cursor = new BlockCursor(source, match.isBlock ? match.rawStart : match.matchStart, end);
|
|
234
|
+
while (cursor.next()) {
|
|
235
|
+
const line = (match.isBlock ? match.line + 1 : match.line) + cursor.lineIndex;
|
|
236
|
+
if (scalarLines.has(line) || cursor.empty || cursor.firstCode === 35)
|
|
237
|
+
continue;
|
|
238
|
+
let start = cursor.contentStart;
|
|
239
|
+
let sequence = false;
|
|
240
|
+
if (source[start] === '-' && (start + 1 === cursor.lineEnd || source[start + 1].trim() === '')) {
|
|
241
|
+
sequence = true;
|
|
242
|
+
start++;
|
|
243
|
+
while (start < cursor.lineEnd && source[start].trim() === '')
|
|
244
|
+
start++;
|
|
245
|
+
}
|
|
246
|
+
const content = source.slice(start, cursor.lineEnd).trimEnd();
|
|
247
|
+
let colon = findKeySep(content);
|
|
248
|
+
if (colon === -1 && content.endsWith(':'))
|
|
249
|
+
colon = content.length - 1;
|
|
250
|
+
const rawKey = colon === -1 ? '' : content.slice(0, colon).trimEnd();
|
|
251
|
+
let valueStart = start;
|
|
252
|
+
if (colon !== -1 && isValidKey(rawKey)) {
|
|
253
|
+
const candidate = { key: stripKeyQuotes(rawKey), start, end: start + rawKey.length,
|
|
254
|
+
colon: start + colon, line, sequence };
|
|
255
|
+
if (start !== match.matchStart)
|
|
256
|
+
sites.push(candidate);
|
|
257
|
+
valueStart = start + colon + 1;
|
|
258
|
+
while (valueStart < cursor.lineEnd && source[valueStart].trim() === '')
|
|
259
|
+
valueStart++;
|
|
260
|
+
}
|
|
261
|
+
const raw = stripCommentKeepEscapes(source.slice(valueStart, cursor.lineEnd));
|
|
262
|
+
flowSites(source, valueStart, valueStart + raw.length, line, sites);
|
|
263
|
+
}
|
|
264
|
+
if (match.isBlock) {
|
|
265
|
+
// Some malformed flows throw even in non-strict mode. Replace
|
|
266
|
+
// their value with a scalar (or skip an unreadable item) and retry.
|
|
267
|
+
let tolerant = source;
|
|
268
|
+
for (;;) {
|
|
269
|
+
const block = new BlockCursor(tolerant, match.rawStart, end);
|
|
270
|
+
while (block.next() && (block.empty || block.firstCode === 35)) { }
|
|
271
|
+
if (!block.valid || (block.asciiIndent === 0 && block.firstCode !== 45))
|
|
272
|
+
break;
|
|
273
|
+
const builder = outlineBuilder(() => match.line + 1 + block.lineIndex);
|
|
274
|
+
try {
|
|
275
|
+
const value = parseCursorBlock(block, block.asciiIndent, ctx, match.line + 1, builder, block.asciiIndent === 0);
|
|
276
|
+
if (value)
|
|
277
|
+
walk(value, [key], sites, { after: site.start });
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (!(error instanceof LimaError))
|
|
282
|
+
throw error;
|
|
283
|
+
if (error.code === 'RESOURCE_LIMIT')
|
|
284
|
+
throw error;
|
|
285
|
+
const line = match.line + 1 + block.lineIndex;
|
|
286
|
+
const keySite = sites.find((s) => s.line === line && (s.start === block.contentStart || s.sequence));
|
|
287
|
+
const a = keySite ? keySite.colon + 1 : block.lineStart, b = block.lineEnd;
|
|
288
|
+
const replacement = keySite ? ' 0'.padEnd(b - a, ' ') : ' '.repeat(b - a);
|
|
289
|
+
tolerant = tolerant.slice(0, a) + replacement + tolerant.slice(b);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const raw = stripComment(source.slice(match.rawStart, match.inlineEnd ?? end)).trim();
|
|
295
|
+
if (raw !== '|') {
|
|
296
|
+
try {
|
|
297
|
+
walk(parseFlowOrScalarValue(raw, ctx, match.line, outlineBuilder(() => match.line)), [key], sites, { after: site.start });
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
if (!(error instanceof LimaError) || error.code === 'RESOURCE_LIMIT')
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const lines = new BlockCursor(source, 0, source.length);
|
|
307
|
+
while (lines.next()) {
|
|
308
|
+
if (scalarLines.has(lines.lineIndex + 1))
|
|
309
|
+
continue;
|
|
310
|
+
let start = lines.contentStart;
|
|
311
|
+
// Quoted-key contents do not participate in value comment stripping.
|
|
312
|
+
const key = keys.find((k) => k.span.start.line === lines.lineIndex + 1);
|
|
313
|
+
if (key)
|
|
314
|
+
while (start < lines.lineEnd && offsets[start] < key.separator.end.offset)
|
|
315
|
+
start++;
|
|
316
|
+
const raw = source.slice(start, lines.lineEnd);
|
|
317
|
+
const kept = stripCommentKeepEscapes(raw);
|
|
318
|
+
if (kept.length !== raw.length) {
|
|
319
|
+
const hash = source.indexOf('#', start + kept.length);
|
|
320
|
+
const comment = span(hash, lines.lineEnd);
|
|
321
|
+
let end = comment.end.offset;
|
|
322
|
+
while (end < input.length && input[end] !== '\r' && input[end] !== '\n')
|
|
323
|
+
end++;
|
|
324
|
+
comments.push({ start: comment.start, end: { ...comment.end,
|
|
325
|
+
column: comment.end.column + end - comment.end.offset, offset: end } });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
keys.sort((a, b) => a.span.start.offset - b.span.start.offset);
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!(error instanceof LimaError))
|
|
332
|
+
throw error;
|
|
333
|
+
keys.length = 0;
|
|
334
|
+
comments.length = 0;
|
|
335
|
+
if (!diagnostics.some((d) => d.code === error.code && d.line === error.line))
|
|
336
|
+
diagnostics.push({ ...error, message: error.message });
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
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