@limaformat/lima 0.7.1 → 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 +8 -0
- package/README.md +25 -1
- package/dist/block.d.ts +3 -0
- package/dist/block.js +1 -1
- package/dist/flow.d.ts +18 -0
- package/dist/flow.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/outline.d.ts +28 -0
- package/dist/outline.js +339 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,14 @@ 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
|
+
|
|
15
23
|
## [0.7.1] — 2026-10-31
|
|
16
24
|
|
|
17
25
|
### Security
|
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
|
@@ -170,7 +170,7 @@ const unrecognizedKeyLine = (line, trimmed) => new LimaError({
|
|
|
170
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;
|
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
|
@@ -15,3 +15,4 @@ export { parseCore, type CoreOptions, type Diagnostic } from './core.js';
|
|
|
15
15
|
export { LimaError, type LimaDiagnostic, type LimaDiagnosticCode } from './errors.js';
|
|
16
16
|
export { parseReferences, parse, type ParseMode, type ParseOptions } from './references2.js';
|
|
17
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/package.json
CHANGED