@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/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
- export const parseFlowMapping = (val, ctx, line, builder, source) => {
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 = stripKeyQuotes(keyRaw, ctx.strict, line);
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 { BlockCursor } from './block-cursor.js';
2
- import { findKeySep, parseCursorBlock } from './block.js';
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 { FlowCursor, flowItemSeparator, parseFlowOrScalarValue } from './flow.js';
7
- import { byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, checkKeyLength, checkDuplicateKey, } from './normalize.js';
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 { isValidKey, stripComment, stripCommentKeepEscapes, stripKeyQuotes } from './scalars.js';
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
- let source = '';
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 (source.endsWith(' ')) {
20
- source = source.slice(0, -1);
19
+ while (units.at(-1) === ' ') {
20
+ units.pop();
21
21
  offsets.pop();
22
22
  }
23
23
  offsets.push(i);
24
- source += '\n';
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
- source += ' ';
30
+ units.push(' ', ' ');
31
31
  offsets.push(i, i);
32
32
  }
33
33
  else {
34
- source += c;
34
+ units.push(c);
35
35
  offsets.push(i);
36
36
  if (c !== ' ')
37
37
  leading = false;
38
38
  }
39
39
  }
40
- while (source.endsWith(' ')) {
41
- source = source.slice(0, -1);
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
- return { line: low + 1, column: offset - lines[low] + 1, offset };
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
- /** 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 });
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
- 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
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
- 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 });
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
- mappingMaxDepth: () => 0, mapping: (entries, line) => ({ line, entries }),
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
- if (!diagnostics.some((d) => d.code === warning.code && d.line === warning.line && d.key === warning.key))
133
- diagnostics.push(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);
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
- diagnostics.push({ ...error, message: error.message });
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
- diagnostics.push({ ...error, message: error.message });
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 topKeys = new Set();
175
- const emit = (site, path) => {
176
- checkKeyLength(site.key, site.line);
177
- const colon = span(site.colon, site.colon + 1);
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: 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
- }
217
+ keys.push({ key: position.key, path: [...path, position.key], span: span(position.start, position.end), separator });
193
218
  };
194
- const walk = (value, path, sites, position, depth = 0) => {
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 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)
226
+ for (const { position, value: child } of value.entries ?? []) {
227
+ if (position.start === position.end)
211
228
  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);
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)], sites, position, depth));
235
+ value.items?.forEach((item, index) => walk(item, [...path, String(index)], depth));
222
236
  };
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);
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
- diagnostics.push({ ...error, message: error.message });
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