@limaformat/lima 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js ADDED
@@ -0,0 +1,439 @@
1
+ /**
2
+ * LIMA Core 1.0 parser — reference-unaware by construction (Appendix B:
3
+ * reference resolution is exclusively the concern of the optional
4
+ * References extension). `($key)`/`(%key)`-shaped text is never
5
+ * recognised or coerced here; it simply falls through as an ordinary
6
+ * string, indistinguishable from any other unrecognised text.
7
+ *
8
+ * `parseCoreWithPositions` builds the internal annotated value tree — a
9
+ * `PositionedValue` per node, carrying the source line and (for strings)
10
+ * whether it came from quoted syntax. This one-pass output is the shared
11
+ * primitive the References layer (`references.ts`) builds on: it locates
12
+ * reference-shaped string leaves, and reads each site's line directly off
13
+ * its node instead of re-deriving position information after the fact.
14
+ * `parseCore` is the public, position-free projection of the same parse.
15
+ *
16
+ * The grammar itself lives in `normalize.ts` (shared context/limits),
17
+ * `scalars.ts` (dates, numbers, quoting, the `PositionedValue` tree type),
18
+ * `flow.ts` (`[...]`/`{...}`), and `block.ts` (indentation-based sequences
19
+ * and mappings) — this file is the orchestrator that ties them together.
20
+ */
21
+ import { LString } from './value.js';
22
+ import { checkKeyLength, checkScalarLimit, byteLength, DOCUMENT_SIZE_LIMIT, TOP_LEVEL_KEY_LIMIT, NESTING_DEPTH_LIMIT, } from './normalize.js';
23
+ import { unescapeDQ, stripComment, positionedBuilder, NO_SPAN_VALUE, parseSimpleScalarSpan, } from './scalars.js';
24
+ import { parseFlowOrScalarValue } from './flow.js';
25
+ import { parseBlockRange } from './block.js';
26
+ import { LimaError } from './errors.js';
27
+ import { KeyCursor, scanKeys } from './scanner.js';
28
+ export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
29
+ export { toPlainValue } from './scalars.js';
30
+ /** Every Lima mapping result must be a prototype-free object (Core §11.1). */
31
+ const emptyMapping = () => Object.create(null);
32
+ const depthOfPositioned = (v) => {
33
+ if (v.kind === 'array') {
34
+ let max = 0;
35
+ for (const item of v.items)
36
+ max = Math.max(max, depthOfPositioned(item));
37
+ return 1 + max;
38
+ }
39
+ if (v.kind === 'mapping') {
40
+ let max = 0;
41
+ for (const child of v.entries.values())
42
+ max = Math.max(max, depthOfPositioned(child));
43
+ return 1 + max;
44
+ }
45
+ return 0;
46
+ };
47
+ /**
48
+ * Same Core §9 depth measurement as `depthOfPositioned`, but over the
49
+ * untagged native representation `parseCore`'s fast path builds directly —
50
+ * no `.kind` to switch on, so containers are recognised structurally:
51
+ * `Array.isArray` for a sequence, else any non-`Date` object for a mapping
52
+ * (a `Date` is the one native object type that's a scalar leaf, not a
53
+ * container).
54
+ */
55
+ const depthOfNative = (v) => {
56
+ if (Array.isArray(v)) {
57
+ let max = 0;
58
+ for (const item of v)
59
+ max = Math.max(max, depthOfNative(item));
60
+ return 1 + max;
61
+ }
62
+ if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
63
+ let max = 0;
64
+ for (const key in v)
65
+ max = Math.max(max, depthOfNative(v[key]));
66
+ return 1 + max;
67
+ }
68
+ return 0;
69
+ };
70
+ const SPACE_BEFORE_COLON_RE = /^(?:'[^']*'|"(?:[^"\\]|\\.)*")[ \t]+:/;
71
+ const leadingSpaces = (line) => {
72
+ let i = 0;
73
+ while (i < line.length && line.charCodeAt(i) === 32)
74
+ i++;
75
+ return i;
76
+ };
77
+ /** Index right after the last non-space character — the `trimEnd()` boundary, without allocating. */
78
+ const trailingSpaceEnd = (line) => {
79
+ let i = line.length;
80
+ while (i > 0 && line.charCodeAt(i - 1) === 32)
81
+ i--;
82
+ return i;
83
+ };
84
+ const lineAt = (s, pos) => {
85
+ let n = 1;
86
+ for (let i = 0; i < pos; i++)
87
+ if (s.charCodeAt(i) === 10)
88
+ n++;
89
+ return n;
90
+ };
91
+ /**
92
+ * Parses LIMA Core 1.0 syntax, generic over the output representation
93
+ * (`builder` — see `builder.ts`): References needs the annotated
94
+ * `PositionedValue` tree (`parseCoreWithPositions` below fixes `V` to
95
+ * that), while `parseCore` fixes `V` to the public native shape directly,
96
+ * skipping the annotated tree — and the extra full-tree conversion pass
97
+ * out of it — entirely. One shared control flow either way, so the two
98
+ * never drift apart the way a hand-copied second parser could.
99
+ * `computeDepth` is similarly representation-specific (Core §9's
100
+ * pre-resolution nesting-depth check, run once here regardless of `V`):
101
+ * `depthOfPositioned` inspects a tagged union's `.kind`; `depthOfNative`
102
+ * inspects the untagged native shape structurally instead.
103
+ */
104
+ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
105
+ const { strict } = ctx;
106
+ // One UTF-16 code unit contributes at most three UTF-8 bytes (a valid
107
+ // surrogate pair contributes four bytes across two units). Below this
108
+ // bound the document cannot exceed §9's byte limit, so typical small
109
+ // frontmatter avoids allocating and filling TextEncoder's byte array.
110
+ if (frontMatter.length > Math.floor(DOCUMENT_SIZE_LIMIT / 3) &&
111
+ byteLength(frontMatter) > DOCUMENT_SIZE_LIMIT) {
112
+ throw new LimaError({
113
+ code: 'RESOURCE_LIMIT', line: 1,
114
+ message: `LIMA: document exceeds maximum size of ${DOCUMENT_SIZE_LIMIT} bytes at line 1`,
115
+ });
116
+ }
117
+ const root = builder.createMapping();
118
+ if (!frontMatter)
119
+ return root;
120
+ // Core flow syntax is capped at two container levels, far below §9's
121
+ // depth limit of 16. Only block syntax can possibly reach that limit;
122
+ // documents without a block value therefore need no final root walk.
123
+ // Once set, the existing exact traversal remains authoritative.
124
+ const blockDepthRisk = { mayExceed: false };
125
+ // Each of these three passes is skipped outright when a cheap upfront
126
+ // check proves it would be a no-op — a full-document regex `.replace()`
127
+ // always allocates a new string even when nothing actually changes, and
128
+ // real documents are overwhelmingly already LF-only, space-indented,
129
+ // and free of trailing whitespace (git normalizes line endings, most
130
+ // editors strip trailing whitespace on save). Each guard is sound (a
131
+ // negative guard never skips a pass that would have changed something):
132
+ // checked sequentially against the already-updated string, so e.g. the
133
+ // trailing-space guard correctly sees spaces the tab-expansion pass
134
+ // itself just created (an all-tab blank line expands to an all-space
135
+ // line, which then still needs trimming).
136
+ if (frontMatter.includes('\r'))
137
+ frontMatter = frontMatter.replace(/\r\n|\r/g, '\n');
138
+ if (frontMatter.includes('\t')) {
139
+ frontMatter = frontMatter.replace(/^([ \t]*)/gm, (leading) => (leading.includes('\t') ? leading.replace(/\t/g, ' ') : leading));
140
+ }
141
+ if (frontMatter.includes(' \n') || frontMatter.endsWith(' ')) {
142
+ // Capture-and-reinsert instead of a lookahead: `( +)(\n|$)` replaced
143
+ // with just group 2 removes the spaces and puts back whatever
144
+ // followed them (a newline, or nothing at end of string) — same
145
+ // result as ` +(?=\n|$)` → '', without relying on a construct RE2
146
+ // (and RE2-family engines like Rust's `regex` crate) don't support.
147
+ frontMatter = frontMatter.replace(/( +)(\n|$)/gm, '$2');
148
+ }
149
+ if (strict) {
150
+ let searchFrom = 0;
151
+ while (searchFrom <= frontMatter.length) {
152
+ const lineEnd = frontMatter.indexOf('\n', searchFrom);
153
+ const line = lineEnd === -1 ? frontMatter.slice(searchFrom) : frontMatter.slice(searchFrom, lineEnd);
154
+ if (SPACE_BEFORE_COLON_RE.test(line)) {
155
+ throw new LimaError({
156
+ code: 'INVALID_QUOTE', line: lineAt(frontMatter, searchFrom),
157
+ message: `LIMA: space between closing quote and colon at line ${lineAt(frontMatter, searchFrom)}`,
158
+ });
159
+ }
160
+ if (lineEnd === -1)
161
+ break;
162
+ searchFrom = lineEnd + 1;
163
+ }
164
+ }
165
+ const parseEntry = (key, line, rawStart, isBlock, inlineEnd, nextStart) => {
166
+ checkKeyLength(key, () => line);
167
+ if ((strict || ctx.onWarning !== undefined) && builder.hasMappingKey(root, key)) {
168
+ const diagnostic = {
169
+ code: 'DUPLICATE_KEY', line, key,
170
+ message: `LIMA: duplicate key "${key}" at line ${line} — last value wins`,
171
+ };
172
+ if (strict)
173
+ throw new LimaError(diagnostic);
174
+ // Public `Diagnostic` type is spec-frozen {message, line} (§11.2);
175
+ // see normalize.ts's checkDuplicateKeyMap for why the richer
176
+ // object is delivered anyway.
177
+ ctx.onWarning?.(diagnostic);
178
+ }
179
+ const firstNewline = inlineEnd ?? frontMatter.indexOf('\n', rawStart);
180
+ const newlineInRange = firstNewline < nextStart;
181
+ // Typical frontmatter uses one inline scalar per key. Avoid building
182
+ // and filling a temporary lines array for that overwhelmingly common
183
+ // case; the scanner-delimited raw slice contains at most its terminal
184
+ // newline here.
185
+ if (!isBlock && (!newlineInRange || firstNewline === nextStart - 1)) {
186
+ const valueEnd = newlineInRange ? firstNewline : nextStart;
187
+ const spanned = parseSimpleScalarSpan(frontMatter, rawStart, valueEnd, line, strict, builder);
188
+ if (spanned !== NO_SPAN_VALUE) {
189
+ builder.setMapping(root, key, spanned);
190
+ return;
191
+ }
192
+ const val = frontMatter.slice(rawStart, valueEnd);
193
+ const uncommented = val.includes('#') ? stripComment(val) : val;
194
+ builder.setMapping(root, key, parseFlowOrScalarValue(uncommented, ctx, line, builder));
195
+ return;
196
+ }
197
+ if (isBlock) {
198
+ const parsed = parseBlockRange(frontMatter, rawStart, nextStart, ctx, line + 1, builder, blockDepthRisk);
199
+ builder.setMapping(root, key, parsed ?? builder.null(line));
200
+ return;
201
+ }
202
+ const raw = frontMatter.slice(rawStart, nextStart);
203
+ const lines = [];
204
+ let lineStart = 0;
205
+ for (let j = 0; j <= raw.length; j++) {
206
+ if (j === raw.length || raw.charCodeAt(j) === 10) {
207
+ if (j > lineStart)
208
+ lines.push(raw.slice(lineStart, j));
209
+ lineStart = j + 1;
210
+ }
211
+ }
212
+ {
213
+ if (lines.length === 0) {
214
+ builder.setMapping(root, key, builder.null(line));
215
+ return;
216
+ }
217
+ const line0Trimmed = lines[0].trim();
218
+ if (lines.length === 1 || line0Trimmed !== '|') {
219
+ const line0 = lines[0];
220
+ const val = line0.includes('#') ? stripComment(line0) : line0;
221
+ builder.setMapping(root, key, parseFlowOrScalarValue(val, ctx, line, builder));
222
+ return;
223
+ }
224
+ // Multi-line string (`|` literal block scalar).
225
+ const bodyLines = raw.slice(raw.indexOf('\n') + 1).split('\n');
226
+ // Avoids trim()/trimStart()/trimEnd() — each allocates a whole new
227
+ // string just to measure or strip whitespace. leadingSpaces/
228
+ // trailingSpaceEnd compute the same boundaries by scanning char
229
+ // codes, and dedent+continuation-strip+trailing-trim collapse
230
+ // into a single final slice() per line instead of up to three.
231
+ let minIndent = Infinity;
232
+ for (const bodyLine of bodyLines) {
233
+ const indent = leadingSpaces(bodyLine);
234
+ if (indent === bodyLine.length)
235
+ continue; // blank (all spaces, or empty)
236
+ if (indent < minIndent)
237
+ minIndent = indent;
238
+ }
239
+ minIndent = Math.min(minIndent, key.length + 2);
240
+ const trimAmt = minIndent > 1 && isFinite(minIndent) ? minIndent : 0;
241
+ const mergedLines = [];
242
+ for (const bodyLine of bodyLines) {
243
+ const lineLen = bodyLine.length;
244
+ let start = trimAmt < lineLen ? trimAmt : lineLen;
245
+ const isContinuation = bodyLine.charCodeAt(start) === 94 && bodyLine.charCodeAt(start + 1) === 94; // ^^
246
+ if (isContinuation)
247
+ start += 2;
248
+ let end = trailingSpaceEnd(bodyLine);
249
+ if (end < start)
250
+ end = start;
251
+ const content = bodyLine.slice(start, end);
252
+ if (isContinuation) {
253
+ if (mergedLines.length > 0) {
254
+ if (content)
255
+ mergedLines[mergedLines.length - 1] += ' ' + content;
256
+ }
257
+ else {
258
+ mergedLines.push(content);
259
+ }
260
+ }
261
+ else {
262
+ mergedLines.push(content);
263
+ }
264
+ }
265
+ while (mergedLines.length > 0 && mergedLines[mergedLines.length - 1] === '')
266
+ mergedLines.pop();
267
+ const joined = mergedLines.join('\n');
268
+ checkScalarLimit(LString(joined), line);
269
+ builder.setMapping(root, key, builder.string(joined, line, false));
270
+ }
271
+ };
272
+ // A recognised top-level entry needs at least one UTF-16 unit of key text,
273
+ // `:`, and its terminating newline: three units. Consequently 129 entries
274
+ // need at least 3 * 129 = 387 units. Below that proven bound the limit
275
+ // cannot be exceeded; larger documents use the independent line-count proof.
276
+ let directKeyCursor = frontMatter.length < 3 * (TOP_LEVEL_KEY_LIMIT + 1);
277
+ if (!directKeyCursor && !strict && ctx.onWarning === undefined) {
278
+ let newlines = 0;
279
+ for (let pos = 0; pos < frontMatter.length; pos++) {
280
+ if (frontMatter.charCodeAt(pos) === 10 && ++newlines >= TOP_LEVEL_KEY_LIMIT)
281
+ break;
282
+ }
283
+ // Every top-level key occupies its own physical line. Fewer than 128
284
+ // newlines therefore proves that at most 128 key positions can exist;
285
+ // all other documents retain scanKeys()'s exact key-count check.
286
+ directKeyCursor = newlines < TOP_LEVEL_KEY_LIMIT;
287
+ }
288
+ if (!strict && ctx.onWarning === undefined && directKeyCursor) {
289
+ const cursor = new KeyCursor(frontMatter);
290
+ let hasToken = cursor.next();
291
+ if (!hasToken)
292
+ return root;
293
+ while (hasToken) {
294
+ const line = cursor.tokenLine;
295
+ const rawStart = cursor.rawStart;
296
+ const isBlock = cursor.isBlock;
297
+ const inlineEnd = cursor.inlineEnd < 0 ? undefined : cursor.inlineEnd;
298
+ const rawKey = frontMatter.slice(cursor.keyStart, cursor.keyEnd);
299
+ const key = cursor.keyKind === 2 ? unescapeDQ(rawKey) : rawKey;
300
+ hasToken = cursor.next();
301
+ parseEntry(key, line, rawStart, isBlock, inlineEnd, hasToken ? cursor.matchStart : frontMatter.length);
302
+ }
303
+ }
304
+ else {
305
+ const matches = scanKeys(frontMatter);
306
+ const keyCount = matches.length;
307
+ if (keyCount === 0)
308
+ return root;
309
+ if (keyCount > TOP_LEVEL_KEY_LIMIT) {
310
+ throw new LimaError({
311
+ code: 'RESOURCE_LIMIT', line: 1,
312
+ message: `LIMA: too many top-level key entries (max ${TOP_LEVEL_KEY_LIMIT}) at line 1`,
313
+ });
314
+ }
315
+ for (let i = 0; i < keyCount; i++) {
316
+ const m = matches[i];
317
+ const rawDQ = m.doubleQuotedRaw;
318
+ const key = m.unquoted ?? m.singleQuoted ?? (rawDQ !== undefined ? unescapeDQ(rawDQ) : undefined);
319
+ if (key !== undefined)
320
+ parseEntry(key, m.line, m.rawStart, m.isBlock, m.inlineEnd, i + 1 < keyCount ? matches[i + 1].matchStart : frontMatter.length);
321
+ }
322
+ }
323
+ // Core §9 nesting depth, over Core's own reference-inert tree. When
324
+ // References is layered on top, it re-checks depth on the final,
325
+ // post-substitution tree separately — substituted values can add depth
326
+ // this check cannot see yet.
327
+ if (blockDepthRisk.mayExceed && builder.mappingMaxDepth(root, computeDepth) > NESTING_DEPTH_LIMIT) {
328
+ throw new LimaError({
329
+ code: 'RESOURCE_LIMIT', line: 1,
330
+ message: `LIMA: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line 1`,
331
+ });
332
+ }
333
+ return root;
334
+ };
335
+ /**
336
+ * Parses LIMA Core 1.0 syntax into the internal annotated value tree —
337
+ * every node carrying its source line, string leaves additionally carrying
338
+ * whether they came from quoted syntax. `($key)`/`(%key)` text is left
339
+ * exactly as written; nothing here ever inspects or resolves it. The
340
+ * primitive the References layer (`references.ts`) builds on.
341
+ */
342
+ export const parseCoreWithPositions = (frontMatter, ctx) => parseCoreGeneric(frontMatter, ctx, positionedBuilder, depthOfPositioned);
343
+ /**
344
+ * The `ValueBuilder<NativeValue>` — `parseCore`'s fast path. Every scalar
345
+ * builder is the identity function: unlike `positionedBuilder`, there is no
346
+ * wrapper object to allocate at all, only the value itself. `array` and
347
+ * `mapping` build the exact public shape directly (a real array; a
348
+ * prototype-free object per Core §11.1), so `parseCore` never needs a
349
+ * separate conversion pass over an intermediate tree afterward.
350
+ */
351
+ export const nativeBuilder = {
352
+ null: () => null,
353
+ bool: (value) => value,
354
+ int: (value) => value,
355
+ float: (value) => value,
356
+ string: (value) => value,
357
+ instant: (value) => value,
358
+ array: (items) => items,
359
+ // A fresh, still-empty prototype-free object trivially satisfies any
360
+ // record type — the one honest, narrow cast this builder needs, versus
361
+ // the unwrap-an-opaque-`unknown`-and-hope casts the `unknown`-typed
362
+ // interface required at every method below.
363
+ createMapping: () => emptyMapping(),
364
+ createMappingWith: (key, value) => {
365
+ const entries = emptyMapping();
366
+ entries[key] = value;
367
+ return entries;
368
+ },
369
+ // Lima values can never be `undefined`, and every native mapping has a
370
+ // null prototype. A direct lookup therefore distinguishes absent from
371
+ // present keys without the considerably more expensive generic own-key
372
+ // helper (including for names such as "__proto__").
373
+ hasMappingKey: (entries, key) => entries[key] !== undefined,
374
+ setMapping: (entries, key, value) => { entries[key] = value; },
375
+ mappingMaxDepth: (entries, depthOf) => {
376
+ let max = 0;
377
+ for (const key in entries) {
378
+ const value = entries[key];
379
+ if (Array.isArray(value) || (value !== null && typeof value === 'object' && !(value instanceof Date))) {
380
+ max = Math.max(max, depthOf(value));
381
+ }
382
+ }
383
+ return max;
384
+ },
385
+ mapping: (entries) => entries,
386
+ };
387
+ /** Converts a Lima value to a plain, native JS value (the public result shape). */
388
+ export const toNative = (v) => {
389
+ switch (v.kind) {
390
+ case 'null': return null;
391
+ case 'bool': return v.value;
392
+ case 'int':
393
+ case 'float': return v.value;
394
+ case 'string': return v.value;
395
+ case 'instant': return v.value;
396
+ case 'array': return v.items.map(toNative);
397
+ case 'mapping': {
398
+ const out = emptyMapping();
399
+ for (const [k, c] of v.entries)
400
+ out[k] = toNative(c);
401
+ return out;
402
+ }
403
+ }
404
+ };
405
+ /**
406
+ * `toNative(toPlainValue(v))` in one pass instead of two full tree walks —
407
+ * matters most for large, reference-expanded results (many copies of a
408
+ * sizeable partial), where the position/quoted-stripping pass and the
409
+ * native-conversion pass would otherwise each independently visit every
410
+ * node of the same, potentially large, final tree.
411
+ */
412
+ export const toNativeFromPositioned = (v) => {
413
+ switch (v.kind) {
414
+ case 'null': return null;
415
+ case 'bool': return v.value;
416
+ case 'int':
417
+ case 'float': return v.value;
418
+ case 'string': return v.value;
419
+ case 'instant': return v.value;
420
+ case 'array': return v.items.map(toNativeFromPositioned);
421
+ case 'mapping': {
422
+ const out = emptyMapping();
423
+ for (const [k, c] of v.entries)
424
+ out[k] = toNativeFromPositioned(c);
425
+ return out;
426
+ }
427
+ }
428
+ };
429
+ /**
430
+ * Public Core 1.0 entry point. Never resolves `($key)`/`(%key)` text — see
431
+ * the module doc comment. Equivalent in observable behavior to calling
432
+ * `parseReferences()` with no partials on a document that happens to
433
+ * contain no references, except that here reference-shaped text is
434
+ * guaranteed to always pass through unresolved, even in strict mode.
435
+ */
436
+ export const parseCore = (frontMatter, options) => {
437
+ const ctx = { strict: options?.strict ?? false, onWarning: options?.onWarning };
438
+ return parseCoreGeneric(frontMatter, ctx, nativeBuilder, depthOfNative);
439
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Structured internal diagnostics. The public parser API (Core §11.3)
3
+ * remains a plain `Error` with just `.message` — Core §11.3 explicitly
4
+ * permits subclasses as long as `instanceof Error` and `.message` still
5
+ * work, so `LimaError` carries the same message text plus additional,
6
+ * non-normative fields (`code`, `line`, `token`, `key`, `partial`, `path`)
7
+ * that let a caller (or the conformance corpus runner, which imports this
8
+ * module directly rather than duplicating it) inspect *why* a parse failed
9
+ * without re-parsing the message string.
10
+ */
11
+ export type LimaDiagnosticCode = 'INVALID_ESCAPE' | 'INVALID_QUOTE' | 'INVALID_DATE' | 'INVALID_NUMBER' | 'INVALID_REFERENCE_SHAPE' | 'INVALID_INDENTATION' | 'INVALID_FLOW_SYNTAX' | 'DUPLICATE_KEY' | 'RESOURCE_LIMIT' | 'UNRESOLVED_REFERENCE' | 'INVALID_INTERPOLATION' | 'INVALID_PARTIAL';
12
+ export interface LimaDiagnostic {
13
+ code: LimaDiagnosticCode;
14
+ message: string;
15
+ line?: number;
16
+ column?: number;
17
+ token?: string;
18
+ key?: string;
19
+ partial?: string;
20
+ path?: string;
21
+ }
22
+ export declare class LimaError extends Error {
23
+ readonly code: LimaDiagnosticCode;
24
+ readonly line?: number;
25
+ readonly column?: number;
26
+ readonly token?: string;
27
+ readonly key?: string;
28
+ readonly partial?: string;
29
+ readonly path?: string;
30
+ constructor(diagnostic: LimaDiagnostic);
31
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Structured internal diagnostics. The public parser API (Core §11.3)
3
+ * remains a plain `Error` with just `.message` — Core §11.3 explicitly
4
+ * permits subclasses as long as `instanceof Error` and `.message` still
5
+ * work, so `LimaError` carries the same message text plus additional,
6
+ * non-normative fields (`code`, `line`, `token`, `key`, `partial`, `path`)
7
+ * that let a caller (or the conformance corpus runner, which imports this
8
+ * module directly rather than duplicating it) inspect *why* a parse failed
9
+ * without re-parsing the message string.
10
+ */
11
+ export class LimaError extends Error {
12
+ code;
13
+ line;
14
+ column;
15
+ token;
16
+ key;
17
+ partial;
18
+ path;
19
+ constructor(diagnostic) {
20
+ super(diagnostic.message);
21
+ this.name = 'LimaError';
22
+ Object.assign(this, diagnostic);
23
+ }
24
+ }
package/dist/flow.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** Core §15.8 flow collections: `[...]` sequences and `{...}` mappings. */
2
+ import { type ParseContext } from './normalize.js';
3
+ import type { ValueBuilder } from './builder.js';
4
+ export declare const parseFlowSequence: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>) => V[] | null;
5
+ export declare const parseFlowMapping: <V, M>(val: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>) => V | null;
6
+ /** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
7
+ export declare const parseFlowOrScalarValue: <V, M>(raw: string, ctx: ParseContext, line: number, builder: ValueBuilder<V, M>) => V;
package/dist/flow.js ADDED
@@ -0,0 +1,162 @@
1
+ /** Core §15.8 flow collections: `[...]` sequences and `{...}` mappings. */
2
+ import { checkKeyLength, checkDuplicateKey } from './normalize.js';
3
+ import { parseQuotedOrTyped, parseScalarValue, stripKeyQuotes } from './scalars.js';
4
+ import { LimaError } from './errors.js';
5
+ import { isTrimWhitespace } from './chars.js';
6
+ const trimStart = (source, start, end) => {
7
+ while (start < end && isTrimWhitespace(source.charCodeAt(start)))
8
+ start++;
9
+ return start;
10
+ };
11
+ const trimEnd = (source, start, end) => {
12
+ while (end > start && isTrimWhitespace(source.charCodeAt(end - 1)))
13
+ end--;
14
+ return end;
15
+ };
16
+ /** Stateful comma-item cursor over a flow container's original string. */
17
+ class FlowCursor {
18
+ source;
19
+ end;
20
+ itemStart = 0;
21
+ itemEnd = 0;
22
+ nextStart;
23
+ done = false;
24
+ constructor(source, start, end) {
25
+ this.source = source;
26
+ this.end = end;
27
+ this.nextStart = start;
28
+ }
29
+ next() {
30
+ if (this.done)
31
+ return false;
32
+ let quote = 0;
33
+ let depth = 0;
34
+ let pos = this.nextStart;
35
+ for (; pos < this.end; pos++) {
36
+ const code = this.source.charCodeAt(pos);
37
+ if (quote) {
38
+ if (code === 92)
39
+ pos++;
40
+ else if (code === quote)
41
+ quote = 0;
42
+ }
43
+ else if (code === 34 || code === 39)
44
+ quote = code;
45
+ else if (code === 91 || code === 123)
46
+ depth++;
47
+ else if (code === 93 || code === 125)
48
+ depth--;
49
+ else if (code === 44 && depth === 0)
50
+ break;
51
+ }
52
+ this.itemStart = trimStart(this.source, this.nextStart, pos);
53
+ this.itemEnd = trimEnd(this.source, this.itemStart, pos);
54
+ if (pos < this.end)
55
+ this.nextStart = pos + 1;
56
+ else
57
+ this.done = true;
58
+ return true;
59
+ }
60
+ get isLast() { return this.done; }
61
+ }
62
+ const isNestedFlowConstruct = (item) => (item.charCodeAt(0) === 91 && item.charCodeAt(item.length - 1) === 93) ||
63
+ (item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125);
64
+ export const parseFlowSequence = (val, ctx, line, builder) => {
65
+ if (val.charCodeAt(0) !== 91 || val.charCodeAt(val.length - 1) !== 93)
66
+ return null;
67
+ const innerStart = trimStart(val, 1, val.length - 1);
68
+ const innerEnd = trimEnd(val, innerStart, val.length - 1);
69
+ if (innerStart === innerEnd)
70
+ return [];
71
+ const items = [];
72
+ const cursor = new FlowCursor(val, innerStart, innerEnd);
73
+ let itemCount = 0;
74
+ while (cursor.next()) {
75
+ const start = cursor.itemStart, end = cursor.itemEnd;
76
+ itemCount++;
77
+ if (start === end && !ctx.strict && cursor.isLast && itemCount > 1)
78
+ break;
79
+ const item = val.slice(start, end);
80
+ if (!item) {
81
+ if (ctx.strict)
82
+ throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `LIMA: empty element in flow sequence at line ${line}` });
83
+ items.push(builder.null(line));
84
+ continue;
85
+ }
86
+ if (item.charCodeAt(0) === 91 && item.charCodeAt(item.length - 1) === 93) {
87
+ throw new LimaError({
88
+ code: 'INVALID_FLOW_SYNTAX', line,
89
+ message: `LIMA: nested flow sequence not permitted at line ${line}: "${item}"`,
90
+ });
91
+ }
92
+ if (item.charCodeAt(0) === 123 && item.charCodeAt(item.length - 1) === 125) {
93
+ const nested = parseFlowMapping(item, ctx, line, builder);
94
+ if (nested !== null) {
95
+ items.push(nested);
96
+ continue;
97
+ }
98
+ }
99
+ items.push(parseQuotedOrTyped(item, ctx, line, false, builder));
100
+ }
101
+ return items;
102
+ };
103
+ export const parseFlowMapping = (val, ctx, line, builder) => {
104
+ if (val.charCodeAt(0) !== 123 || val.charCodeAt(val.length - 1) !== 125)
105
+ return null;
106
+ const innerStart = trimStart(val, 1, val.length - 1);
107
+ const innerEnd = trimEnd(val, innerStart, val.length - 1);
108
+ const entries = builder.createMapping();
109
+ if (innerStart === innerEnd)
110
+ return builder.mapping(entries, line);
111
+ const cursor = new FlowCursor(val, innerStart, innerEnd);
112
+ while (cursor.next()) {
113
+ const itemStart = cursor.itemStart, itemEnd = cursor.itemEnd;
114
+ const item = val.slice(itemStart, itemEnd);
115
+ if (!item) {
116
+ if (ctx.strict)
117
+ throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `LIMA: empty element in flow mapping at line ${line}` });
118
+ continue;
119
+ }
120
+ const colonPos = val.indexOf(': ', itemStart);
121
+ if (colonPos === -1 || colonPos >= itemEnd) {
122
+ if (ctx.strict)
123
+ throw new LimaError({
124
+ code: 'INVALID_FLOW_SYNTAX', line,
125
+ message: `LIMA: invalid flow mapping item (missing ": ") at line ${line}: "${item}"`,
126
+ });
127
+ return null;
128
+ }
129
+ const keyStart = trimStart(val, itemStart, colonPos);
130
+ const keyEnd = trimEnd(val, keyStart, colonPos);
131
+ const key = stripKeyQuotes(val.slice(keyStart, keyEnd));
132
+ checkKeyLength(key, () => line);
133
+ if (ctx.strict || ctx.onWarning !== undefined) {
134
+ checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
135
+ }
136
+ const valueStart = trimStart(val, colonPos + 2, itemEnd);
137
+ const valueEnd = trimEnd(val, valueStart, itemEnd);
138
+ const rawVal = val.slice(valueStart, valueEnd);
139
+ if (isNestedFlowConstruct(rawVal)) {
140
+ throw new LimaError({ code: 'INVALID_FLOW_SYNTAX', line, message: `LIMA: invalid flow nesting at line ${line}: "${rawVal}"` });
141
+ }
142
+ builder.setMapping(entries, key, parseQuotedOrTyped(rawVal, ctx, line, false, builder));
143
+ }
144
+ return builder.mapping(entries, line);
145
+ };
146
+ /** Parses a value that may be a flow collection, without probing both flow parsers for ordinary scalars. */
147
+ export const parseFlowOrScalarValue = (raw, ctx, line, builder) => {
148
+ const first = raw.charCodeAt(0);
149
+ if (first === 91) {
150
+ const sequence = parseFlowSequence(raw, ctx, line, builder);
151
+ if (sequence !== null)
152
+ return builder.array(sequence, line);
153
+ return parseScalarValue(raw, ctx, line, builder);
154
+ }
155
+ else if (first === 123) {
156
+ const mapping = parseFlowMapping(raw, ctx, line, builder);
157
+ if (mapping !== null)
158
+ return mapping;
159
+ return parseScalarValue(raw, ctx, line, builder);
160
+ }
161
+ return parseQuotedOrTyped(raw, ctx, line, true, builder);
162
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * LIMA Parser — LIMA Is Metadata Annotation
3
+ *
4
+ * Public entry point. The implementation is split per Appendix B's own
5
+ * layering:
6
+ * - `core.ts` — LIMA Core 1.0, reference-unaware by construction.
7
+ * - `references.ts` — the optional References 1.0 extension, layered on
8
+ * top of Core's internal annotated value tree.
9
+ * - `value.ts` — the shared Lima Value Model both build on.
10
+ *
11
+ * `parse` (the historical default export) resolves references — it is
12
+ * `parseReferences` under a name kept for backward compatibility.
13
+ */
14
+ export { parseCore, type CoreOptions, type Diagnostic } from './core.js';
15
+ export { parseReferences, parse, type ReferencesOptions, type ParseOptions } from './references.js';