@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/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
- export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V[] | null;
6
- export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>, source?: ReferenceSource) => V | null;
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
- 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/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
@@ -14,3 +14,4 @@
14
14
  export { parseCore } from './core.js';
15
15
  export { LimaError } from './errors.js';
16
16
  export { parseReferences, parse } from './references2.js';
17
+ export { outline } from './outline.js';
@@ -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;
@@ -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
- * The value text with a trailing `#` comment removed, but with `\#`
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 val.slice(0, i).trimEnd();
457
+ return i;
469
458
  }
470
459
  }
471
- return val;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limaformat/lima",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Lima frontmatter parser — LIMA Is Metadata Annotation",
5
5
  "keywords": ["frontmatter", "parser", "yaml", "markdown", "metadata"],
6
6
  "homepage": "https://limaformat.dev",