@limaformat/lima 0.8.0 → 0.8.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 +9 -0
- package/dist/block-cursor.d.ts +15 -0
- package/dist/block-cursor.js +46 -0
- package/dist/block.d.ts +10 -2
- package/dist/block.js +294 -283
- package/dist/builder.d.ts +18 -0
- package/dist/flow.d.ts +6 -4
- package/dist/flow.js +19 -7
- package/dist/outline.js +138 -229
- package/dist/scalars.d.ts +2 -0
- package/dist/scalars.js +20 -15
- package/package.json +1 -1
package/dist/builder.d.ts
CHANGED
|
@@ -25,7 +25,25 @@
|
|
|
25
25
|
* therefore performance — is identical either way, verified by benchmark.
|
|
26
26
|
*/
|
|
27
27
|
import type { ReferenceSource } from './reference-tokens2.js';
|
|
28
|
+
/**
|
|
29
|
+
* Reader events for structural editor output. All positions are UTF-16 offsets
|
|
30
|
+
* in the normalized source; ends are exclusive, separator is the colon and
|
|
31
|
+
* line is one-based. A zero-width key denotes a tolerant entry without a
|
|
32
|
+
* physical separator; its value's events are still balanced but not emitted.
|
|
33
|
+
* Readers select reporting functions once on entry. Native/References builders
|
|
34
|
+
* never set this hook, so their key loops do no optional-sink checks.
|
|
35
|
+
*/
|
|
36
|
+
export interface SourcePositions {
|
|
37
|
+
key(key: string, start: number, end: number, separator: number, line: number): void;
|
|
38
|
+
comment(start: number, end: number): void;
|
|
39
|
+
/**
|
|
40
|
+
* On failure, discard key events left unfinished by `read`. Return `fallback`
|
|
41
|
+
* for non-resource LimaErrors; rethrow resource-limit and non-Lima errors.
|
|
42
|
+
*/
|
|
43
|
+
recover<T>(read: () => T, fallback: T): T;
|
|
44
|
+
}
|
|
28
45
|
export interface ValueBuilder<V, M = Map<string, V>> {
|
|
46
|
+
readonly sourcePositions?: SourcePositions;
|
|
29
47
|
/** Whether scalar builders need physical source anchors for editor positions. */
|
|
30
48
|
readonly positions?: boolean;
|
|
31
49
|
null(line: number): V;
|
package/dist/flow.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Core §15.8 flow collections: `[...]` sequences and `{...}` mappings. */
|
|
2
2
|
import { type ParseContext } from './normalize.js';
|
|
3
|
-
import type { ValueBuilder } from './builder.js';
|
|
3
|
+
import type { SourcePositions, ValueBuilder } from './builder.js';
|
|
4
4
|
import type { ReferenceSource } from './reference-tokens2.js';
|
|
5
5
|
/** Stateful comma-item cursor over a flow container's original string. */
|
|
6
6
|
export declare class FlowCursor {
|
|
@@ -14,13 +14,15 @@ export declare class FlowCursor {
|
|
|
14
14
|
next(): boolean;
|
|
15
15
|
get isLast(): boolean;
|
|
16
16
|
}
|
|
17
|
-
export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V[] | null;
|
|
17
|
+
export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource, offset?: number) => V[] | null;
|
|
18
18
|
/**
|
|
19
19
|
* Index of the key/value `: ` separator within a flow-mapping item — the
|
|
20
20
|
* first `: ` that is not inside a quoted key (§5.1). Returns -1 when the
|
|
21
21
|
* item has no separator.
|
|
22
22
|
*/
|
|
23
23
|
export declare const flowItemSeparator: (val: string, start: number, end: number) => number;
|
|
24
|
-
export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V | null;
|
|
24
|
+
export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource, offset?: number) => V | null;
|
|
25
25
|
/** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
|
|
26
|
-
export declare const parseFlowOrScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V;
|
|
26
|
+
export declare const parseFlowOrScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource, offset?: number) => V;
|
|
27
|
+
/** Select recovery once, outside the block reader's key loop. */
|
|
28
|
+
export declare const flowValueReader: (positions?: SourcePositions) => typeof parseFlowOrScalarValue;
|
package/dist/flow.js
CHANGED
|
@@ -118,7 +118,7 @@ export class FlowCursor {
|
|
|
118
118
|
}
|
|
119
119
|
const isNestedFlowConstruct = (item) => (item.charCodeAt(0) === 91 && item.charCodeAt(item.length - 1) === 93) ||
|
|
120
120
|
(item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125);
|
|
121
|
-
export const parseFlowSequence = (val, ctx, line, builder, source) => {
|
|
121
|
+
export const parseFlowSequence = (val, ctx, line, builder, source, offset = 0) => {
|
|
122
122
|
if (val.charCodeAt(0) !== 91 || val.charCodeAt(val.length - 1) !== 93)
|
|
123
123
|
return null;
|
|
124
124
|
const innerStart = trimStart(val, 1, val.length - 1);
|
|
@@ -147,7 +147,7 @@ export const parseFlowSequence = (val, ctx, line, builder, source) => {
|
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
if (item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125) {
|
|
150
|
-
const nested = parseFlowMapping(item, ctx, line, builder, elementSource(source, val, start, end));
|
|
150
|
+
const nested = parseFlowMapping(item, ctx, line, builder, elementSource(source, val, start, end), offset + start);
|
|
151
151
|
if (nested !== null) {
|
|
152
152
|
items.push(nested);
|
|
153
153
|
continue;
|
|
@@ -174,11 +174,18 @@ export const flowItemSeparator = (val, start, end) => {
|
|
|
174
174
|
const sep = val.indexOf(': ', scan);
|
|
175
175
|
return sep === -1 || sep >= end ? -1 : sep;
|
|
176
176
|
};
|
|
177
|
-
|
|
177
|
+
const sourceKeyReader = (positions, offset) => (raw, strict, line, start, colon) => {
|
|
178
|
+
const key = stripKeyQuotes(raw, strict, line);
|
|
179
|
+
positions.key(key, offset + start, offset + start + raw.length, offset + colon, line);
|
|
180
|
+
return key;
|
|
181
|
+
};
|
|
182
|
+
export const parseFlowMapping = (val, ctx, line, builder, source, offset = 0) => {
|
|
178
183
|
if (val.charCodeAt(0) !== 123 || val.charCodeAt(val.length - 1) !== 125)
|
|
179
184
|
return null;
|
|
180
185
|
const innerStart = trimStart(val, 1, val.length - 1);
|
|
181
186
|
const innerEnd = trimEnd(val, innerStart, val.length - 1);
|
|
187
|
+
const positions = builder.sourcePositions;
|
|
188
|
+
const readKey = positions ? sourceKeyReader(positions, offset) : stripKeyQuotes;
|
|
182
189
|
const entries = builder.createMapping();
|
|
183
190
|
if (innerStart === innerEnd)
|
|
184
191
|
return builder.mapping(entries, line);
|
|
@@ -209,7 +216,7 @@ export const parseFlowMapping = (val, ctx, line, builder, source) => {
|
|
|
209
216
|
// list is closed and does not cover this — same as the top level).
|
|
210
217
|
continue;
|
|
211
218
|
}
|
|
212
|
-
const key =
|
|
219
|
+
const key = readKey(keyRaw, ctx.strict, line, keyStart, colonPos);
|
|
213
220
|
checkKeyLength(key, line);
|
|
214
221
|
if (ctx.strict || ctx.onWarning !== undefined) {
|
|
215
222
|
checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
|
|
@@ -225,19 +232,24 @@ export const parseFlowMapping = (val, ctx, line, builder, source) => {
|
|
|
225
232
|
return builder.mapping(entries, line);
|
|
226
233
|
};
|
|
227
234
|
/** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
|
|
228
|
-
export const parseFlowOrScalarValue = (raw, ctx, line, builder, source) => {
|
|
235
|
+
export const parseFlowOrScalarValue = (raw, ctx, line, builder, source, offset = 0) => {
|
|
229
236
|
const first = raw.charCodeAt(0);
|
|
230
237
|
if (first === 91) {
|
|
231
|
-
const sequence = parseFlowSequence(raw, ctx, line, builder, source);
|
|
238
|
+
const sequence = parseFlowSequence(raw, ctx, line, builder, source, offset);
|
|
232
239
|
if (sequence !== null)
|
|
233
240
|
return builder.array(sequence, line);
|
|
234
241
|
return parseScalarValue(raw, ctx, line, builder, source);
|
|
235
242
|
}
|
|
236
243
|
else if (first === 123) {
|
|
237
|
-
const mapping = parseFlowMapping(raw, ctx, line, builder, source);
|
|
244
|
+
const mapping = parseFlowMapping(raw, ctx, line, builder, source, offset);
|
|
238
245
|
if (mapping !== null)
|
|
239
246
|
return mapping;
|
|
240
247
|
return parseScalarValue(raw, ctx, line, builder, source);
|
|
241
248
|
}
|
|
242
249
|
return parseQuotedOrTyped(raw, ctx, line, builder, source);
|
|
243
250
|
};
|
|
251
|
+
/** Select recovery once, outside the block reader's key loop. */
|
|
252
|
+
export const flowValueReader = (positions) => positions ? sourceValueReader(positions) : parseFlowOrScalarValue;
|
|
253
|
+
// Structural values are still physical text; reference anchors would rescan
|
|
254
|
+
// every flow prefix for codepoint columns that the outline never uses.
|
|
255
|
+
const sourceValueReader = (positions) => (raw, ctx, line, builder, _source, offset) => positions.recover(() => parseFlowOrScalarValue(raw, ctx, line, builder, undefined, offset), builder.null(line));
|
package/dist/outline.js
CHANGED
|
@@ -1,136 +1,173 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { buildBlockScalar } from './block-scalar.js';
|
|
1
|
+
import { SourceBlockCursor } from './block-cursor.js';
|
|
2
|
+
import { parseBlockRange, parseInlineValue } from './block.js';
|
|
4
3
|
import { parseCore } from './core.js';
|
|
5
4
|
import { LimaError } from './errors.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { flowValueReader } from './flow.js';
|
|
6
|
+
import { NESTING_DEPTH_LIMIT, checkKeyLength, checkDuplicateKey, } from './normalize.js';
|
|
8
7
|
import { parse } from './references2.js';
|
|
9
8
|
import { scanKeys } from './scanner.js';
|
|
10
|
-
import {
|
|
9
|
+
import { stripCommentKeepEscapes, stripKeyQuotes } from './scalars.js';
|
|
10
|
+
import { isBlockScalarMarker } from './structure.js';
|
|
11
11
|
/** Core §3 normalization, with a boundary map back to the BOM-free physical input. */
|
|
12
12
|
function normalize(input) {
|
|
13
|
-
|
|
13
|
+
const units = [];
|
|
14
14
|
const offsets = [];
|
|
15
15
|
let leading = true;
|
|
16
16
|
for (let i = 0; i < input.length; i++) {
|
|
17
17
|
const c = input[i];
|
|
18
18
|
if (c === '\r' || c === '\n') {
|
|
19
|
-
while (
|
|
20
|
-
|
|
19
|
+
while (units.at(-1) === ' ') {
|
|
20
|
+
units.pop();
|
|
21
21
|
offsets.pop();
|
|
22
22
|
}
|
|
23
23
|
offsets.push(i);
|
|
24
|
-
|
|
24
|
+
units.push('\n');
|
|
25
25
|
if (c === '\r' && input[i + 1] === '\n')
|
|
26
26
|
i++;
|
|
27
27
|
leading = true;
|
|
28
28
|
}
|
|
29
29
|
else if (leading && c === '\t') {
|
|
30
|
-
|
|
30
|
+
units.push(' ', ' ');
|
|
31
31
|
offsets.push(i, i);
|
|
32
32
|
}
|
|
33
33
|
else {
|
|
34
|
-
|
|
34
|
+
units.push(c);
|
|
35
35
|
offsets.push(i);
|
|
36
36
|
if (c !== ' ')
|
|
37
37
|
leading = false;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
while (
|
|
41
|
-
|
|
40
|
+
while (units.at(-1) === ' ') {
|
|
41
|
+
units.pop();
|
|
42
42
|
offsets.pop();
|
|
43
43
|
}
|
|
44
44
|
offsets.push(input.length);
|
|
45
|
-
return { source, offsets };
|
|
45
|
+
return { source: units.join(''), offsets };
|
|
46
46
|
}
|
|
47
47
|
/** Editor positions count UTF-16 units, including CRLF's two units. */
|
|
48
48
|
function spanReader(input, offsets) {
|
|
49
49
|
const lines = [0];
|
|
50
|
+
const lineNumbers = new Uint32Array(input.length + 1);
|
|
51
|
+
let line = 0;
|
|
50
52
|
for (let i = 0; i < input.length; i++) {
|
|
53
|
+
lineNumbers[i] = line;
|
|
51
54
|
if (input[i] === '\r') {
|
|
52
55
|
if (input[i + 1] === '\n')
|
|
53
|
-
i
|
|
56
|
+
lineNumbers[++i] = line;
|
|
54
57
|
lines.push(i + 1);
|
|
58
|
+
line++;
|
|
55
59
|
}
|
|
56
|
-
else if (input[i] === '\n')
|
|
60
|
+
else if (input[i] === '\n') {
|
|
57
61
|
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;
|
|
62
|
+
line++;
|
|
68
63
|
}
|
|
69
|
-
|
|
64
|
+
}
|
|
65
|
+
lineNumbers[input.length] = line;
|
|
66
|
+
const point = (offset) => {
|
|
67
|
+
const line = lineNumbers[offset];
|
|
68
|
+
return { line: line + 1, column: offset - lines[line] + 1, offset };
|
|
70
69
|
};
|
|
71
70
|
return (start, end) => ({ start: point(offsets[start]),
|
|
72
71
|
end: point(end > start ? offsets[end - 1] + 1 : offsets[end]) });
|
|
73
72
|
}
|
|
74
|
-
/**
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 });
|
|
73
|
+
/** Pair each reader event with the value built immediately after it. */
|
|
74
|
+
function outlineBuilder(comment) {
|
|
75
|
+
const pending = [];
|
|
76
|
+
const failed = new WeakSet();
|
|
77
|
+
const leaf = () => ({});
|
|
78
|
+
const entry = (value) => {
|
|
79
|
+
const position = pending.pop();
|
|
80
|
+
if (!position)
|
|
81
|
+
throw new Error('Lima: missing outline key position');
|
|
82
|
+
return { position, value };
|
|
83
|
+
};
|
|
106
84
|
return {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
85
|
+
positions: true,
|
|
86
|
+
sourcePositions: {
|
|
87
|
+
key: (key, start, end, separator, line) => pending.push({ key, start, end, separator, line, duplicates: false }),
|
|
88
|
+
comment,
|
|
89
|
+
recover: (read, fallback) => {
|
|
90
|
+
const depth = pending.length;
|
|
91
|
+
try {
|
|
92
|
+
const value = read();
|
|
93
|
+
if (pending.length !== depth)
|
|
94
|
+
throw new Error('Lima: unbalanced outline key positions');
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
pending.length = depth;
|
|
99
|
+
if (!(error instanceof LimaError) || error.code === 'RESOURCE_LIMIT')
|
|
100
|
+
throw error;
|
|
101
|
+
if (fallback !== null && typeof fallback === 'object')
|
|
102
|
+
failed.add(fallback);
|
|
103
|
+
return fallback;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
114
106
|
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
107
|
+
null: leaf, bool: leaf, int: leaf, float: leaf, string: leaf, instant: leaf,
|
|
108
|
+
// A failed mapping value keeps its key; an unreadable whole item is skipped.
|
|
109
|
+
array: (items) => ({ items: items.filter((item) => !failed.has(item)) }), createMapping: () => [],
|
|
110
|
+
createMappingWith: (_, value) => [entry(value)],
|
|
111
|
+
hasMappingKey: () => {
|
|
112
|
+
const position = pending.at(-1);
|
|
113
|
+
if (!position)
|
|
114
|
+
throw new Error('Lima: missing outline duplicate position');
|
|
115
|
+
position.duplicates = true;
|
|
116
|
+
return false; // diagnostics are collected in source order below
|
|
121
117
|
},
|
|
122
|
-
|
|
118
|
+
setMapping: (entries, _, value) => { entries.push(entry(value)); },
|
|
119
|
+
mappingMaxDepth: () => 0, mapping: (entries) => ({ entries }),
|
|
123
120
|
};
|
|
124
121
|
}
|
|
122
|
+
/** Consume scanner ranges once; nested grammar stays in the block and flow readers. */
|
|
123
|
+
function readOutline(source, builder, ctx, emit, walk) {
|
|
124
|
+
const positions = builder.sourcePositions;
|
|
125
|
+
if (!positions)
|
|
126
|
+
throw new Error('Lima: missing outline source positions');
|
|
127
|
+
const readValue = flowValueReader(positions);
|
|
128
|
+
const matches = scanKeys(source);
|
|
129
|
+
const preamble = new SourceBlockCursor(source, 0, matches[0]?.matchStart ?? source.length, positions);
|
|
130
|
+
while (preamble.next()) { }
|
|
131
|
+
const topKeys = new Set();
|
|
132
|
+
for (let i = 0; i < matches.length; i++) {
|
|
133
|
+
const match = matches[i];
|
|
134
|
+
const end = matches[i + 1]?.matchStart ?? source.length;
|
|
135
|
+
const key = match.unquoted ?? match.singleQuoted ?? stripKeyQuotes(source.slice(match.matchStart, match.sepStart - 1));
|
|
136
|
+
checkDuplicateKey(topKeys.has(key), key, match.line, ctx);
|
|
137
|
+
topKeys.add(key);
|
|
138
|
+
emit({ key, start: match.matchStart, end: match.sepStart - 1,
|
|
139
|
+
separator: match.sepStart - 1, line: match.line, duplicates: true }, []);
|
|
140
|
+
const cursor = new SourceBlockCursor(source, match.isBlock ? match.rawStart : match.matchStart, end, positions);
|
|
141
|
+
if (match.isBlock) {
|
|
142
|
+
const value = parseBlockRange(source, match.rawStart, end, ctx, match.line + 1, builder, { mayExceed: false }, cursor);
|
|
143
|
+
if (value)
|
|
144
|
+
walk(value, [key]);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
cursor.next();
|
|
148
|
+
cursor.excludeCommentsBefore(match.rawStart);
|
|
149
|
+
const raw = source.slice(match.rawStart, cursor.lineEnd);
|
|
150
|
+
const value = stripCommentKeepEscapes(raw).trim();
|
|
151
|
+
const column = match.rawStart - match.matchStart + raw.length - raw.trimStart().length;
|
|
152
|
+
walk(parseInlineValue(value, isBlockScalarMarker(source, match.rawStart, cursor.lineEnd), 0, match.line, column, raw, cursor, ctx, builder, readValue), [key]);
|
|
153
|
+
}
|
|
154
|
+
while (cursor.valid)
|
|
155
|
+
cursor.next();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
125
158
|
/** Source structure for editors; references are only evaluated by the diagnostic oracle. */
|
|
126
159
|
export function outline(frontMatter) {
|
|
127
160
|
const diagnostics = [];
|
|
128
161
|
const keys = [];
|
|
129
162
|
const comments = [];
|
|
130
163
|
const result = { offsetUnit: 'utf16-code-unit', keys, comments, diagnostics };
|
|
164
|
+
const diagnosticKeys = new Set();
|
|
131
165
|
const addWarning = (warning) => {
|
|
132
|
-
|
|
133
|
-
|
|
166
|
+
const identity = JSON.stringify([warning.code, warning.line, warning.key]);
|
|
167
|
+
if (diagnosticKeys.has(identity))
|
|
168
|
+
return;
|
|
169
|
+
diagnosticKeys.add(identity);
|
|
170
|
+
diagnostics.push(warning);
|
|
134
171
|
};
|
|
135
172
|
let limited = false;
|
|
136
173
|
try {
|
|
@@ -139,7 +176,7 @@ export function outline(frontMatter) {
|
|
|
139
176
|
catch (error) {
|
|
140
177
|
if (!(error instanceof LimaError))
|
|
141
178
|
throw error;
|
|
142
|
-
|
|
179
|
+
addWarning({ ...error, message: error.message });
|
|
143
180
|
limited = error.code === 'RESOURCE_LIMIT';
|
|
144
181
|
}
|
|
145
182
|
// Also check limits beyond strict mode's first error, and collect duplicate
|
|
@@ -153,179 +190,51 @@ export function outline(frontMatter) {
|
|
|
153
190
|
if (error.code === 'RESOURCE_LIMIT') {
|
|
154
191
|
limited = true;
|
|
155
192
|
if (!diagnostics.some((d) => d.code === error.code && d.line === error.line))
|
|
156
|
-
|
|
193
|
+
addWarning({ ...error, message: error.message });
|
|
157
194
|
}
|
|
158
195
|
}
|
|
159
196
|
if (limited)
|
|
160
197
|
return result;
|
|
161
198
|
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
199
|
const input = frontMatter.startsWith('\uFEFF') ? frontMatter.slice(1) : frontMatter;
|
|
166
200
|
const { source, offsets } = normalize(input);
|
|
167
201
|
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
202
|
const ctx = { strict: false, onWarning: (warning) => addWarning(warning) };
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
203
|
+
const builder = outlineBuilder((start, end) => {
|
|
204
|
+
const comment = span(start, end);
|
|
205
|
+
let physicalEnd = comment.end.offset;
|
|
206
|
+
while (physicalEnd < input.length && input[physicalEnd] !== '\r' && input[physicalEnd] !== '\n')
|
|
207
|
+
physicalEnd++;
|
|
208
|
+
comments.push({ start: comment.start, end: { ...comment.end,
|
|
209
|
+
column: comment.end.column + physicalEnd - comment.end.offset, offset: physicalEnd } });
|
|
210
|
+
});
|
|
211
|
+
const emit = (position, path) => {
|
|
212
|
+
checkKeyLength(position.key, position.line);
|
|
213
|
+
const colon = span(position.separator, position.separator + 1);
|
|
178
214
|
const separator = input[colon.end.offset] === ' '
|
|
179
215
|
? { start: colon.start, end: { ...colon.end, column: colon.end.column + 1, offset: colon.end.offset + 1 } }
|
|
180
216
|
: colon;
|
|
181
|
-
keys.push({ key:
|
|
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
|
-
}
|
|
217
|
+
keys.push({ key: position.key, path: [...path, position.key], span: span(position.start, position.end), separator });
|
|
193
218
|
};
|
|
194
|
-
const walk = (value, path,
|
|
219
|
+
const walk = (value, path, depth = 0) => {
|
|
195
220
|
if (value.entries || value.items)
|
|
196
221
|
depth++;
|
|
197
222
|
if (depth > NESTING_DEPTH_LIMIT)
|
|
198
223
|
throw new LimaError({ code: 'RESOURCE_LIMIT', line: 1,
|
|
199
224
|
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line 1` });
|
|
200
|
-
let keyColumn;
|
|
201
225
|
const seen = new Set();
|
|
202
|
-
for (const
|
|
203
|
-
|
|
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)
|
|
226
|
+
for (const { position, value: child } of value.entries ?? []) {
|
|
227
|
+
if (position.start === position.end)
|
|
211
228
|
continue;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
position.after = site.start;
|
|
218
|
-
if (entry.value)
|
|
219
|
-
walk(entry.value, [...path, entry.key], sites, position, depth);
|
|
229
|
+
if (position.duplicates)
|
|
230
|
+
checkDuplicateKey(seen.has(position.key), position.key, position.line, ctx);
|
|
231
|
+
seen.add(position.key);
|
|
232
|
+
emit(position, path);
|
|
233
|
+
walk(child, [...path, position.key], depth);
|
|
220
234
|
}
|
|
221
|
-
value.items?.forEach((item, index) => walk(item, [...path, String(index)],
|
|
235
|
+
value.items?.forEach((item, index) => walk(item, [...path, String(index)], depth));
|
|
222
236
|
};
|
|
223
|
-
|
|
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);
|
|
237
|
+
readOutline(source, builder, ctx, emit, walk);
|
|
329
238
|
}
|
|
330
239
|
catch (error) {
|
|
331
240
|
if (!(error instanceof LimaError))
|
|
@@ -333,7 +242,7 @@ export function outline(frontMatter) {
|
|
|
333
242
|
keys.length = 0;
|
|
334
243
|
comments.length = 0;
|
|
335
244
|
if (!diagnostics.some((d) => d.code === error.code && d.line === error.line))
|
|
336
|
-
|
|
245
|
+
addWarning({ ...error, message: error.message });
|
|
337
246
|
}
|
|
338
247
|
return result;
|
|
339
248
|
}
|
package/dist/scalars.d.ts
CHANGED
|
@@ -120,6 +120,8 @@ export declare const closingQuoteIndex: (s: string, singleQuoteEscape?: boolean)
|
|
|
120
120
|
*/
|
|
121
121
|
export declare const isValidKey: (raw: string) => boolean;
|
|
122
122
|
export declare const unescapeDQ: (s: string, strict?: boolean, line?: number) => string;
|
|
123
|
+
/** UTF-16 offset of the first unescaped, unquoted hash, or -1. */
|
|
124
|
+
export declare const commentStart: (val: string) => number;
|
|
123
125
|
/**
|
|
124
126
|
* The value text with a trailing `#` comment removed, but with `\#`
|
|
125
127
|
* escapes left as written. This is the *physical* form a References 2.0
|