@limaformat/lima 0.7.1 → 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 +17 -0
- package/README.md +25 -1
- package/dist/block-cursor.d.ts +15 -0
- package/dist/block-cursor.js +46 -0
- package/dist/block.d.ts +12 -1
- package/dist/block.js +294 -283
- package/dist/builder.d.ts +18 -0
- package/dist/flow.d.ts +24 -4
- package/dist/flow.js +21 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/outline.d.ts +28 -0
- package/dist/outline.js +248 -0
- 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,8 +1,28 @@
|
|
|
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
|
-
|
|
6
|
-
export declare
|
|
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
|
+
}
|
|
17
|
+
export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource, offset?: number) => 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;
|
|
24
|
+
export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource, offset?: number) => V | null;
|
|
7
25
|
/** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
|
|
8
|
-
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
|
@@ -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;
|
|
@@ -118,7 +118,7 @@ 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;
|
|
@@ -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.
|
|
@@ -174,11 +174,18 @@ 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/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,248 @@
|
|
|
1
|
+
import { SourceBlockCursor } from './block-cursor.js';
|
|
2
|
+
import { parseBlockRange, parseInlineValue } from './block.js';
|
|
3
|
+
import { parseCore } from './core.js';
|
|
4
|
+
import { LimaError } from './errors.js';
|
|
5
|
+
import { flowValueReader } from './flow.js';
|
|
6
|
+
import { NESTING_DEPTH_LIMIT, checkKeyLength, checkDuplicateKey, } from './normalize.js';
|
|
7
|
+
import { parse } from './references2.js';
|
|
8
|
+
import { scanKeys } from './scanner.js';
|
|
9
|
+
import { stripCommentKeepEscapes, stripKeyQuotes } from './scalars.js';
|
|
10
|
+
import { isBlockScalarMarker } from './structure.js';
|
|
11
|
+
/** Core §3 normalization, with a boundary map back to the BOM-free physical input. */
|
|
12
|
+
function normalize(input) {
|
|
13
|
+
const units = [];
|
|
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 (units.at(-1) === ' ') {
|
|
20
|
+
units.pop();
|
|
21
|
+
offsets.pop();
|
|
22
|
+
}
|
|
23
|
+
offsets.push(i);
|
|
24
|
+
units.push('\n');
|
|
25
|
+
if (c === '\r' && input[i + 1] === '\n')
|
|
26
|
+
i++;
|
|
27
|
+
leading = true;
|
|
28
|
+
}
|
|
29
|
+
else if (leading && c === '\t') {
|
|
30
|
+
units.push(' ', ' ');
|
|
31
|
+
offsets.push(i, i);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
units.push(c);
|
|
35
|
+
offsets.push(i);
|
|
36
|
+
if (c !== ' ')
|
|
37
|
+
leading = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
while (units.at(-1) === ' ') {
|
|
41
|
+
units.pop();
|
|
42
|
+
offsets.pop();
|
|
43
|
+
}
|
|
44
|
+
offsets.push(input.length);
|
|
45
|
+
return { source: units.join(''), offsets };
|
|
46
|
+
}
|
|
47
|
+
/** Editor positions count UTF-16 units, including CRLF's two units. */
|
|
48
|
+
function spanReader(input, offsets) {
|
|
49
|
+
const lines = [0];
|
|
50
|
+
const lineNumbers = new Uint32Array(input.length + 1);
|
|
51
|
+
let line = 0;
|
|
52
|
+
for (let i = 0; i < input.length; i++) {
|
|
53
|
+
lineNumbers[i] = line;
|
|
54
|
+
if (input[i] === '\r') {
|
|
55
|
+
if (input[i + 1] === '\n')
|
|
56
|
+
lineNumbers[++i] = line;
|
|
57
|
+
lines.push(i + 1);
|
|
58
|
+
line++;
|
|
59
|
+
}
|
|
60
|
+
else if (input[i] === '\n') {
|
|
61
|
+
lines.push(i + 1);
|
|
62
|
+
line++;
|
|
63
|
+
}
|
|
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 };
|
|
69
|
+
};
|
|
70
|
+
return (start, end) => ({ start: point(offsets[start]),
|
|
71
|
+
end: point(end > start ? offsets[end - 1] + 1 : offsets[end]) });
|
|
72
|
+
}
|
|
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
|
+
};
|
|
84
|
+
return {
|
|
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
|
+
},
|
|
106
|
+
},
|
|
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
|
|
117
|
+
},
|
|
118
|
+
setMapping: (entries, _, value) => { entries.push(entry(value)); },
|
|
119
|
+
mappingMaxDepth: () => 0, mapping: (entries) => ({ entries }),
|
|
120
|
+
};
|
|
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
|
+
}
|
|
158
|
+
/** Source structure for editors; references are only evaluated by the diagnostic oracle. */
|
|
159
|
+
export function outline(frontMatter) {
|
|
160
|
+
const diagnostics = [];
|
|
161
|
+
const keys = [];
|
|
162
|
+
const comments = [];
|
|
163
|
+
const result = { offsetUnit: 'utf16-code-unit', keys, comments, diagnostics };
|
|
164
|
+
const diagnosticKeys = new Set();
|
|
165
|
+
const addWarning = (warning) => {
|
|
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);
|
|
171
|
+
};
|
|
172
|
+
let limited = false;
|
|
173
|
+
try {
|
|
174
|
+
parse(frontMatter, { strict: true });
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (!(error instanceof LimaError))
|
|
178
|
+
throw error;
|
|
179
|
+
addWarning({ ...error, message: error.message });
|
|
180
|
+
limited = error.code === 'RESOURCE_LIMIT';
|
|
181
|
+
}
|
|
182
|
+
// Also check limits beyond strict mode's first error, and collect duplicate
|
|
183
|
+
// warnings without resolving references in the tolerant structural pass.
|
|
184
|
+
try {
|
|
185
|
+
parseCore(frontMatter, { onWarning: (warning) => addWarning(warning) });
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (!(error instanceof LimaError))
|
|
189
|
+
throw error;
|
|
190
|
+
if (error.code === 'RESOURCE_LIMIT') {
|
|
191
|
+
limited = true;
|
|
192
|
+
if (!diagnostics.some((d) => d.code === error.code && d.line === error.line))
|
|
193
|
+
addWarning({ ...error, message: error.message });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (limited)
|
|
197
|
+
return result;
|
|
198
|
+
try {
|
|
199
|
+
const input = frontMatter.startsWith('\uFEFF') ? frontMatter.slice(1) : frontMatter;
|
|
200
|
+
const { source, offsets } = normalize(input);
|
|
201
|
+
const span = spanReader(input, offsets);
|
|
202
|
+
const ctx = { strict: false, onWarning: (warning) => addWarning(warning) };
|
|
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);
|
|
214
|
+
const separator = input[colon.end.offset] === ' '
|
|
215
|
+
? { start: colon.start, end: { ...colon.end, column: colon.end.column + 1, offset: colon.end.offset + 1 } }
|
|
216
|
+
: colon;
|
|
217
|
+
keys.push({ key: position.key, path: [...path, position.key], span: span(position.start, position.end), separator });
|
|
218
|
+
};
|
|
219
|
+
const walk = (value, path, depth = 0) => {
|
|
220
|
+
if (value.entries || value.items)
|
|
221
|
+
depth++;
|
|
222
|
+
if (depth > NESTING_DEPTH_LIMIT)
|
|
223
|
+
throw new LimaError({ code: 'RESOURCE_LIMIT', line: 1,
|
|
224
|
+
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line 1` });
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
for (const { position, value: child } of value.entries ?? []) {
|
|
227
|
+
if (position.start === position.end)
|
|
228
|
+
continue;
|
|
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);
|
|
234
|
+
}
|
|
235
|
+
value.items?.forEach((item, index) => walk(item, [...path, String(index)], depth));
|
|
236
|
+
};
|
|
237
|
+
readOutline(source, builder, ctx, emit, walk);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (!(error instanceof LimaError))
|
|
241
|
+
throw error;
|
|
242
|
+
keys.length = 0;
|
|
243
|
+
comments.length = 0;
|
|
244
|
+
if (!diagnostics.some((d) => d.code === error.code && d.line === error.line))
|
|
245
|
+
addWarning({ ...error, message: error.message });
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
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
|
package/dist/scalars.js
CHANGED
|
@@ -436,19 +436,8 @@ export const unescapeDQ = (s, strict = false, line = 0) => {
|
|
|
436
436
|
}
|
|
437
437
|
});
|
|
438
438
|
};
|
|
439
|
-
/**
|
|
440
|
-
|
|
441
|
-
* escapes left as written. This is the *physical* form a References 2.0
|
|
442
|
-
* token's column is measured against (§2.4): the comment is not part of
|
|
443
|
-
* the value (so its `${…}`-shaped text must not be scanned as a token),
|
|
444
|
-
* but a `\#` before a token still occupies its two source columns.
|
|
445
|
-
*
|
|
446
|
-
* `trimEnd()` is exactly the project whitespace set (ECMAScript WhiteSpace
|
|
447
|
-
* + LineTerminator = `isTrimWhitespace`) — a U+00A0 / FEFF between the
|
|
448
|
-
* value and the `#` is not part of the value. The Rust and Go ports must
|
|
449
|
-
* use `is_trim_whitespace` here, not a host `trim_end` or `" \t"`.
|
|
450
|
-
*/
|
|
451
|
-
export const stripCommentKeepEscapes = (val) => {
|
|
439
|
+
/** UTF-16 offset of the first unescaped, unquoted hash, or -1. */
|
|
440
|
+
export const commentStart = (val) => {
|
|
452
441
|
let quote = 0;
|
|
453
442
|
for (let i = 0; i < val.length; i++) {
|
|
454
443
|
const cc = val.charCodeAt(i);
|
|
@@ -465,10 +454,26 @@ export const stripCommentKeepEscapes = (val) => {
|
|
|
465
454
|
i++;
|
|
466
455
|
}
|
|
467
456
|
else if (cc === 35) {
|
|
468
|
-
return
|
|
457
|
+
return i;
|
|
469
458
|
}
|
|
470
459
|
}
|
|
471
|
-
return
|
|
460
|
+
return -1;
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* The value text with a trailing `#` comment removed, but with `\#`
|
|
464
|
+
* escapes left as written. This is the *physical* form a References 2.0
|
|
465
|
+
* token's column is measured against (§2.4): the comment is not part of
|
|
466
|
+
* the value (so its `${…}`-shaped text must not be scanned as a token),
|
|
467
|
+
* but a `\#` before a token still occupies its two source columns.
|
|
468
|
+
*
|
|
469
|
+
* `trimEnd()` is exactly the project whitespace set (ECMAScript WhiteSpace
|
|
470
|
+
* + LineTerminator = `isTrimWhitespace`) — a U+00A0 / FEFF between the
|
|
471
|
+
* value and the `#` is not part of the value. The Rust and Go ports must
|
|
472
|
+
* use `is_trim_whitespace` here, not a host `trim_end` or `" \t"`.
|
|
473
|
+
*/
|
|
474
|
+
export const stripCommentKeepEscapes = (val) => {
|
|
475
|
+
const start = commentStart(val);
|
|
476
|
+
return start === -1 ? val : val.slice(0, start).trimEnd();
|
|
472
477
|
};
|
|
473
478
|
/**
|
|
474
479
|
* The value text with a trailing `#` comment removed and each `\#` *outside*
|
package/package.json
CHANGED