@limaformat/lima 0.1.1 → 0.2.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/CHANGELOG.md +14 -0
- package/README.md +14 -11
- package/dist/builder.d.ts +9 -1
- package/dist/core.js +25 -4
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -5
- package/dist/reference-tokens2.d.ts +12 -0
- package/dist/reference-tokens2.js +37 -0
- package/dist/references.d.ts +66 -1
- package/dist/references.js +9 -7
- package/dist/references2.d.ts +13 -0
- package/dist/references2.js +295 -0
- package/dist/scalars.d.ts +5 -0
- package/dist/scalars.js +26 -3
- package/package.json +22 -21
package/CHANGELOG.md
CHANGED
|
@@ -12,8 +12,22 @@ are both frozen regardless of what this file shows.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.2.0] — 2026-08-11
|
|
16
|
+
|
|
15
17
|
### Changed
|
|
16
18
|
|
|
19
|
+
- **Breaking:** `parse(input, options?)` now implements Lima References 2.0.
|
|
20
|
+
Document references use `$(key)` instead of `($key)`, and partial
|
|
21
|
+
references use `$(:key)` instead of `(%key)`. References 1.0 tokens are
|
|
22
|
+
literal text through the public 2.0 API.
|
|
23
|
+
- `parseReferences(input, options?)` is now a deprecated compatibility alias
|
|
24
|
+
for `parse` with identical References 2.0 results and diagnostics. It is not
|
|
25
|
+
a References 1.0 mode.
|
|
26
|
+
- `ParseOptions.mode` selects `"references"` (the default) or `"core"`.
|
|
27
|
+
Core mode uses the same reference-unaware path as `parseCore`; supplying
|
|
28
|
+
`partials` in Core mode is invalid. `parseCore` itself is unchanged.
|
|
29
|
+
- References 2.0 adds bounded transitive resolution (at most three reference
|
|
30
|
+
edges), partial mapping traversal, and `$(:namespace/name.path)` partials.
|
|
17
31
|
- Error message prefix `LIMA:` → `Lima:` throughout (e.g. `LIMA: duplicate
|
|
18
32
|
key "a" at line 3` → `Lima: duplicate key "a" at line 3`), plus internal
|
|
19
33
|
code comments — naming-consistency fix, no functional change. `.code`
|
package/README.md
CHANGED
|
@@ -35,30 +35,33 @@ tags:
|
|
|
35
35
|
`parse` accepts the raw content **between** the frontmatter delimiters
|
|
36
36
|
(`---`) — stripping them is the caller's job.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Three functions are exported:
|
|
39
39
|
|
|
40
40
|
- `parseCore(input, options?)` — the base format only.
|
|
41
|
-
- `parse(input, options?)`
|
|
42
|
-
|
|
41
|
+
- `parse(input, options?)` — the primary References 2.0 parser; adds
|
|
42
|
+
`$(key)` document references and `$(:key)` external partials.
|
|
43
|
+
- `parseReferences(input, options?)` — deprecated compatibility alias for
|
|
44
|
+
`parse` with the same References 2.0 semantics.
|
|
43
45
|
|
|
44
46
|
```ts
|
|
45
47
|
parse(frontmatter, { strict: true })
|
|
46
48
|
parse(frontmatter, { partials: { author: 'Alice' } })
|
|
49
|
+
parse(frontmatter, { mode: 'core' }) // same reference-unaware path as parseCore
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
mode, and the
|
|
51
|
-
|
|
52
|
+
The **[guide](https://github.com/limaformat/lima/blob/main/docs/guide.md)**
|
|
53
|
+
covers Core and References 2.0 syntax, resource limits, strict mode, and the
|
|
54
|
+
public API. The normative rules are defined in the
|
|
55
|
+
**[References 2.0 specification](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)**.
|
|
52
56
|
|
|
53
57
|
Why Lima exists, the case against YAML, and security rationale:
|
|
54
58
|
**[repository README](https://github.com/limaformat/lima#readme)**.
|
|
55
59
|
|
|
56
60
|
The [Lima Core 1.0](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
|
|
57
|
-
and [Lima References
|
|
58
|
-
specifications are the
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(count pinned by a test) shared with any other Lima implementation.
|
|
61
|
+
and [Lima References 2.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-2.0-spec.md)
|
|
62
|
+
specifications are the source of truth for these public entry points. The
|
|
63
|
+
frozen References 1.0 implementation remains available internally to run its
|
|
64
|
+
independent conformance suite.
|
|
62
65
|
|
|
63
66
|
Migrating existing YAML frontmatter:
|
|
64
67
|
[docs/migrating-from-yaml.md](https://github.com/limaformat/lima/blob/main/docs/migrating-from-yaml.md).
|
package/dist/builder.d.ts
CHANGED
|
@@ -25,11 +25,13 @@
|
|
|
25
25
|
* therefore performance — is identical either way, verified by benchmark.
|
|
26
26
|
*/
|
|
27
27
|
export interface ValueBuilder<V, M = Map<string, V>> {
|
|
28
|
+
/** Opt-in for References' annotated builder; native Core avoids source-map work. */
|
|
29
|
+
readonly tracksStringSourcePositions?: boolean;
|
|
28
30
|
null(line: number): V;
|
|
29
31
|
bool(value: boolean, line: number): V;
|
|
30
32
|
int(value: number, line: number): V;
|
|
31
33
|
float(value: number, line: number): V;
|
|
32
|
-
string(value: string, line: number, quoted: boolean): V;
|
|
34
|
+
string(value: string, line: number, quoted: boolean, sourceSpans?: StringSourceSpan[]): V;
|
|
33
35
|
instant(value: Date, line: number): V;
|
|
34
36
|
array(items: V[], line: number): V;
|
|
35
37
|
createMapping(): M;
|
|
@@ -39,3 +41,9 @@ export interface ValueBuilder<V, M = Map<string, V>> {
|
|
|
39
41
|
mappingMaxDepth(entries: M, depthOf: (value: V) => number): number;
|
|
40
42
|
mapping(entries: M, line: number): V;
|
|
41
43
|
}
|
|
44
|
+
/** Maps a span in a decoded scalar back to its physical source line/column. */
|
|
45
|
+
export type StringSourceSpan = {
|
|
46
|
+
start: number;
|
|
47
|
+
line: number;
|
|
48
|
+
sourceOffset: number;
|
|
49
|
+
};
|
package/dist/core.js
CHANGED
|
@@ -239,7 +239,9 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
239
239
|
minIndent = Math.min(minIndent, key.length + 2);
|
|
240
240
|
const trimAmt = minIndent > 1 && isFinite(minIndent) ? minIndent : 0;
|
|
241
241
|
const mergedLines = [];
|
|
242
|
-
|
|
242
|
+
const mergedLineSpans = builder.tracksStringSourcePositions ? [] : undefined;
|
|
243
|
+
for (let bodyIndex = 0; bodyIndex < bodyLines.length; bodyIndex++) {
|
|
244
|
+
const bodyLine = bodyLines[bodyIndex];
|
|
243
245
|
const lineLen = bodyLine.length;
|
|
244
246
|
let start = trimAmt < lineLen ? trimAmt : lineLen;
|
|
245
247
|
const isContinuation = bodyLine.charCodeAt(start) === 94 && bodyLine.charCodeAt(start + 1) === 94; // ^^
|
|
@@ -251,22 +253,41 @@ const parseCoreGeneric = (frontMatter, ctx, builder, computeDepth) => {
|
|
|
251
253
|
const content = bodyLine.slice(start, end);
|
|
252
254
|
if (isContinuation) {
|
|
253
255
|
if (mergedLines.length > 0) {
|
|
254
|
-
if (content)
|
|
256
|
+
if (content) {
|
|
257
|
+
const outputStart = mergedLines[mergedLines.length - 1].length + 1;
|
|
255
258
|
mergedLines[mergedLines.length - 1] += ' ' + content;
|
|
259
|
+
mergedLineSpans?.[mergedLineSpans.length - 1].push({
|
|
260
|
+
start: outputStart, line: line + 1 + bodyIndex, sourceOffset: start,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
256
263
|
}
|
|
257
264
|
else {
|
|
258
265
|
mergedLines.push(content);
|
|
266
|
+
mergedLineSpans?.push(content ? [{ start: 0, line: line + 1 + bodyIndex, sourceOffset: start }] : []);
|
|
259
267
|
}
|
|
260
268
|
}
|
|
261
269
|
else {
|
|
262
270
|
mergedLines.push(content);
|
|
271
|
+
mergedLineSpans?.push(content ? [{ start: 0, line: line + 1 + bodyIndex, sourceOffset: start }] : []);
|
|
263
272
|
}
|
|
264
273
|
}
|
|
265
|
-
while (mergedLines.length > 0 && mergedLines[mergedLines.length - 1] === '')
|
|
274
|
+
while (mergedLines.length > 0 && mergedLines[mergedLines.length - 1] === '') {
|
|
266
275
|
mergedLines.pop();
|
|
276
|
+
mergedLineSpans?.pop();
|
|
277
|
+
}
|
|
267
278
|
const joined = mergedLines.join('\n');
|
|
279
|
+
let sourceSpans;
|
|
280
|
+
if (mergedLineSpans) {
|
|
281
|
+
sourceSpans = [];
|
|
282
|
+
let outputStart = 0;
|
|
283
|
+
for (let i = 0; i < mergedLines.length; i++) {
|
|
284
|
+
for (const span of mergedLineSpans[i])
|
|
285
|
+
sourceSpans.push({ ...span, start: outputStart + span.start });
|
|
286
|
+
outputStart += mergedLines[i].length + 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
268
289
|
checkScalarLimit(LString(joined), line);
|
|
269
|
-
builder.setMapping(root, key, builder.string(joined, line, false));
|
|
290
|
+
builder.setMapping(root, key, builder.string(joined, line + 1, false, sourceSpans));
|
|
270
291
|
}
|
|
271
292
|
};
|
|
272
293
|
// A recognised top-level entry needs at least one UTF-16 unit of key text,
|
package/dist/index.d.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* Public entry point. The implementation is split per Appendix B's own
|
|
5
5
|
* layering:
|
|
6
6
|
* - `core.ts` — Lima Core 1.0, reference-unaware by construction.
|
|
7
|
-
* - `
|
|
8
|
-
*
|
|
7
|
+
* - `references2.ts` — the References 2.0 extension layered on Core.
|
|
8
|
+
* - `references.ts` — the frozen internal References 1.0 implementation.
|
|
9
9
|
* - `value.ts` — the shared Lima Value Model both build on.
|
|
10
10
|
*
|
|
11
|
-
* `parse`
|
|
12
|
-
*
|
|
11
|
+
* `parse` is the primary References 2.0 entry point. `parseReferences` is
|
|
12
|
+
* retained as its deprecated compatibility alias.
|
|
13
13
|
*/
|
|
14
14
|
export { parseCore, type CoreOptions, type Diagnostic } from './core.js';
|
|
15
|
-
export { parseReferences, parse, type
|
|
15
|
+
export { parseReferences, parse, type ParseMode, type ParseOptions } from './references2.js';
|
|
16
|
+
export type ReferencesOptions = import('./references2.js').ParseOptions;
|
package/dist/index.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* Public entry point. The implementation is split per Appendix B's own
|
|
5
5
|
* layering:
|
|
6
6
|
* - `core.ts` — Lima Core 1.0, reference-unaware by construction.
|
|
7
|
-
* - `
|
|
8
|
-
*
|
|
7
|
+
* - `references2.ts` — the References 2.0 extension layered on Core.
|
|
8
|
+
* - `references.ts` — the frozen internal References 1.0 implementation.
|
|
9
9
|
* - `value.ts` — the shared Lima Value Model both build on.
|
|
10
10
|
*
|
|
11
|
-
* `parse`
|
|
12
|
-
*
|
|
11
|
+
* `parse` is the primary References 2.0 entry point. `parseReferences` is
|
|
12
|
+
* retained as its deprecated compatibility alias.
|
|
13
13
|
*/
|
|
14
14
|
export { parseCore } from './core.js';
|
|
15
|
-
export { parseReferences, parse } from './
|
|
15
|
+
export { parseReferences, parse } from './references2.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Reference tokens recorded while the annotated Core tree is being built. */
|
|
2
|
+
import type { StringSourceSpan } from './builder.js';
|
|
3
|
+
export type ReferenceToken2 = {
|
|
4
|
+
token: string;
|
|
5
|
+
index: number;
|
|
6
|
+
offset: number;
|
|
7
|
+
line: number;
|
|
8
|
+
documentPath?: string;
|
|
9
|
+
partialPath?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const PURE_REFERENCE_2: RegExp;
|
|
12
|
+
export declare const scanReferenceTokens2: (value: string, firstLine: number, sourceSpans?: StringSourceSpan[]) => ReferenceToken2[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const SEGMENT = '[a-zA-Z0-9_][a-zA-Z0-9_:-]*';
|
|
2
|
+
const DOC_PATH = `${SEGMENT}(?:\\.${SEGMENT})*`;
|
|
3
|
+
const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
4
|
+
const PARTIAL_PATH = `${PARTIAL_NAME}(?:\\.${SEGMENT})*`;
|
|
5
|
+
export const PURE_REFERENCE_2 = new RegExp(`^\\$\\((?:(${DOC_PATH})|:(${PARTIAL_PATH}))\\)$`);
|
|
6
|
+
const REFERENCE_2 = new RegExp(`\\$\\((?:(${DOC_PATH})|:(${PARTIAL_PATH}))\\)`, 'g');
|
|
7
|
+
const NO_TOKENS = [];
|
|
8
|
+
export const scanReferenceTokens2 = (value, firstLine, sourceSpans) => {
|
|
9
|
+
if (!value.includes('$('))
|
|
10
|
+
return NO_TOKENS;
|
|
11
|
+
REFERENCE_2.lastIndex = 0;
|
|
12
|
+
const result = [];
|
|
13
|
+
let line = firstLine;
|
|
14
|
+
let lineStart = 0;
|
|
15
|
+
let scanned = 0;
|
|
16
|
+
let spanIndex = 0;
|
|
17
|
+
for (const match of value.matchAll(REFERENCE_2)) {
|
|
18
|
+
const index = match.index ?? 0;
|
|
19
|
+
while (scanned < index) {
|
|
20
|
+
if (value.charCodeAt(scanned) === 10) {
|
|
21
|
+
line++;
|
|
22
|
+
lineStart = scanned + 1;
|
|
23
|
+
}
|
|
24
|
+
scanned++;
|
|
25
|
+
}
|
|
26
|
+
while (sourceSpans && spanIndex + 1 < sourceSpans.length && sourceSpans[spanIndex + 1].start <= index)
|
|
27
|
+
spanIndex++;
|
|
28
|
+
const source = sourceSpans?.[spanIndex];
|
|
29
|
+
result.push({
|
|
30
|
+
token: match[0], index,
|
|
31
|
+
line: source?.line ?? line,
|
|
32
|
+
offset: source ? source.sourceOffset + index - source.start : index - lineStart,
|
|
33
|
+
...(match[1] !== undefined ? { documentPath: match[1] } : { partialPath: match[2] }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
};
|
package/dist/references.d.ts
CHANGED
|
@@ -20,8 +20,71 @@
|
|
|
20
20
|
* There is no module-level mutable state: diagnostics are collected in a
|
|
21
21
|
* `ResolutionContext` created fresh per call and threaded explicitly.
|
|
22
22
|
*/
|
|
23
|
-
import { type
|
|
23
|
+
import { type LimaValue } from './value.js';
|
|
24
|
+
import { type PositionedValue, type Diagnostic, type InsertedAt, type NativeValue } from './core.js';
|
|
24
25
|
type Meta = Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Structural deep copy for document-derived targets — preserves each leaf's
|
|
28
|
+
* own line/quoted (§4.2/R-104: still eligible for further resolution) and
|
|
29
|
+
* any existing `insertedAt` provenance from an earlier, nested resolution
|
|
30
|
+
* (R-112: a reference copied wholesale into a new position can itself
|
|
31
|
+
* already contain the result of an inner reference that resolved first —
|
|
32
|
+
* both insertion sites remain identifiable participants after the copy).
|
|
33
|
+
*/
|
|
34
|
+
export declare const deepCopyPositioned: (v: PositionedValue) => PositionedValue;
|
|
35
|
+
/**
|
|
36
|
+
* Wraps a partial's ingested value into the annotated representation, with
|
|
37
|
+
* every string leaf marked permanently inert (§3.8: "no traversal into
|
|
38
|
+
* partial values" — the resolution phases must never rediscover a
|
|
39
|
+
* reference-like substring inside partial content). Called once per partial
|
|
40
|
+
* name at ingestion (see the call site), producing the one canonical tree
|
|
41
|
+
* every pure reference to that partial retrieves — deep-copying it on every
|
|
42
|
+
* such reference (§3.1) is the resolveTree call site's job, not this one.
|
|
43
|
+
*/
|
|
44
|
+
export declare const partialToPositioned: (v: LimaValue, line: number) => PositionedValue;
|
|
45
|
+
/**
|
|
46
|
+
* References §5/R-112 (nesting depth), §6.2 (node count), and the public
|
|
47
|
+
* result shape (native conversion) computed together in one recursive pass
|
|
48
|
+
* over the final, post-substitution tree — instead of three independent
|
|
49
|
+
* full-tree walks (a depth-only pass, a count-only pass, then
|
|
50
|
+
* `toNativeFromPositioned`), each of which would otherwise revisit every
|
|
51
|
+
* node of what can be a large, reference-expanded result. Depth attribution
|
|
52
|
+
* needs more than a depth number: it needs to know which reference
|
|
53
|
+
* insertions lie on the actual deepest path, so the earliest of those (by
|
|
54
|
+
* line) can be blamed if the limit is exceeded — ties for "deepest child"
|
|
55
|
+
* all count, since a violation can be reached via more than one
|
|
56
|
+
* maximal-depth branch. `native` is still built unconditionally even
|
|
57
|
+
* though a depth/node-count violation elsewhere in the document means it
|
|
58
|
+
* will be discarded and never returned — cheaper than adding a second
|
|
59
|
+
* conditional branch to every call site, and the discarded case is the
|
|
60
|
+
* rare (error) path, not the one this pass exists to speed up.
|
|
61
|
+
*
|
|
62
|
+
* Each array/mapping branch tracks a running max depth and its participant
|
|
63
|
+
* list in a single loop over its children, instead of first materializing a
|
|
64
|
+
* full array of child results and then doing separate `.map()`/`Math.max`/
|
|
65
|
+
* `.filter().flatMap()` passes over it — for a wide sibling group (many
|
|
66
|
+
* children at the same depth, the common shape for flat documents with many
|
|
67
|
+
* top-level references) that would mean an extra full pass per level on top
|
|
68
|
+
* of the traversal itself. A running accumulator only ever copies-on-write:
|
|
69
|
+
* `.slice()` when a strictly greater depth replaces the running set, plain
|
|
70
|
+
* `.push()` onto that fresh copy for a tie at the current max — never a
|
|
71
|
+
* mutation of a child's own returned array. Reusing a child's array in
|
|
72
|
+
* place would currently be safe (each node is visited exactly once, no
|
|
73
|
+
* aliasing left after the pure-reference deep-copy fix), but relying on
|
|
74
|
+
* that invariant here has no upside worth the risk in a file that has
|
|
75
|
+
* already had one real aliasing bug.
|
|
76
|
+
*/
|
|
77
|
+
export type FinalizedValue = {
|
|
78
|
+
native: NativeValue;
|
|
79
|
+
nodeCount: number;
|
|
80
|
+
depth: number;
|
|
81
|
+
deepestParticipants: InsertedAt[];
|
|
82
|
+
};
|
|
83
|
+
export declare const finalizePositioned: (v: PositionedValue) => FinalizedValue;
|
|
84
|
+
/** Earliest (lowest-line) participant, or null when none exist — R-113's "line 1" fallback applies then. */
|
|
85
|
+
export declare const earliestParticipant: (participants: InsertedAt[]) => InsertedAt | null;
|
|
86
|
+
/** Node-count attribution for the RESOURCE_LIMIT error path: every reference insertion anywhere in the tree contributes to the total. */
|
|
87
|
+
export declare const collectAllParticipants: (v: PositionedValue, acc: InsertedAt[]) => void;
|
|
25
88
|
export type ReferencesOptions = {
|
|
26
89
|
/** Named values available via `(%key)` references. */
|
|
27
90
|
partials?: Meta;
|
|
@@ -33,4 +96,6 @@ export declare const parseReferences: <T extends Record<string, unknown> = Meta>
|
|
|
33
96
|
/** Backward-compatible primary entry point — References layered on Core. */
|
|
34
97
|
export declare const parse: typeof parseReferences;
|
|
35
98
|
export type ParseOptions = ReferencesOptions;
|
|
99
|
+
/** Internal conformance entry point for the frozen References 1.0 suite. */
|
|
100
|
+
export declare const parseReferencesV1: typeof parseReferences;
|
|
36
101
|
export {};
|
package/dist/references.js
CHANGED
|
@@ -110,14 +110,14 @@ const isReferenceFreeP = (v) => {
|
|
|
110
110
|
* already contain the result of an inner reference that resolved first —
|
|
111
111
|
* both insertion sites remain identifiable participants after the copy).
|
|
112
112
|
*/
|
|
113
|
-
const deepCopyPositioned = (v) => {
|
|
113
|
+
export const deepCopyPositioned = (v) => {
|
|
114
114
|
switch (v.kind) {
|
|
115
|
-
case 'array': return { kind: 'array', items: v.items.map(deepCopyPositioned), line: v.line, insertedAt: v.insertedAt };
|
|
115
|
+
case 'array': return { kind: 'array', items: v.items.map(deepCopyPositioned), line: v.line, references2Active: v.references2Active, insertedAt: v.insertedAt };
|
|
116
116
|
case 'mapping': {
|
|
117
117
|
const entries = new Map();
|
|
118
118
|
for (const [k, c] of v.entries)
|
|
119
119
|
entries.set(k, deepCopyPositioned(c));
|
|
120
|
-
return { kind: 'mapping', entries, line: v.line, insertedAt: v.insertedAt };
|
|
120
|
+
return { kind: 'mapping', entries, line: v.line, references2Active: v.references2Active, insertedAt: v.insertedAt };
|
|
121
121
|
}
|
|
122
122
|
case 'instant': return { kind: 'instant', value: new Date(v.value.getTime()), line: v.line, insertedAt: v.insertedAt };
|
|
123
123
|
default: return v;
|
|
@@ -132,7 +132,7 @@ const deepCopyPositioned = (v) => {
|
|
|
132
132
|
* every pure reference to that partial retrieves — deep-copying it on every
|
|
133
133
|
* such reference (§3.1) is the resolveTree call site's job, not this one.
|
|
134
134
|
*/
|
|
135
|
-
const partialToPositioned = (v, line) => {
|
|
135
|
+
export const partialToPositioned = (v, line) => {
|
|
136
136
|
switch (v.kind) {
|
|
137
137
|
case 'null': return { kind: 'null', line };
|
|
138
138
|
case 'bool': return { kind: 'bool', value: v.value, line };
|
|
@@ -282,7 +282,7 @@ const resolveTree = (node, lookup, partials, ctx) => {
|
|
|
282
282
|
}
|
|
283
283
|
return node; // null/bool/int/float/instant — nothing to resolve
|
|
284
284
|
};
|
|
285
|
-
const finalizePositioned = (v) => {
|
|
285
|
+
export const finalizePositioned = (v) => {
|
|
286
286
|
const own = v.insertedAt ? [v.insertedAt] : [];
|
|
287
287
|
if (v.kind === 'array') {
|
|
288
288
|
if (v.items.length === 0)
|
|
@@ -334,9 +334,9 @@ const finalizePositioned = (v) => {
|
|
|
334
334
|
return { native: toNativeFromPositioned(v), nodeCount: 1, depth: 0, deepestParticipants: own };
|
|
335
335
|
};
|
|
336
336
|
/** Earliest (lowest-line) participant, or null when none exist — R-113's "line 1" fallback applies then. */
|
|
337
|
-
const earliestParticipant = (participants) => participants.length === 0 ? null : participants.reduce((a, b) => (b.line < a.line ? b : a));
|
|
337
|
+
export const earliestParticipant = (participants) => participants.length === 0 ? null : participants.reduce((a, b) => (b.line < a.line ? b : a));
|
|
338
338
|
/** Node-count attribution for the RESOURCE_LIMIT error path: every reference insertion anywhere in the tree contributes to the total. */
|
|
339
|
-
const collectAllParticipants = (v, acc) => {
|
|
339
|
+
export const collectAllParticipants = (v, acc) => {
|
|
340
340
|
if (v.insertedAt)
|
|
341
341
|
acc.push(v.insertedAt);
|
|
342
342
|
if (v.kind === 'array')
|
|
@@ -520,3 +520,5 @@ export const parseReferences = (frontMatter, options) => {
|
|
|
520
520
|
};
|
|
521
521
|
/** Backward-compatible primary entry point — References layered on Core. */
|
|
522
522
|
export const parse = parseReferences;
|
|
523
|
+
/** Internal conformance entry point for the frozen References 1.0 suite. */
|
|
524
|
+
export const parseReferencesV1 = parseReferences;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Diagnostic } from './core.js';
|
|
2
|
+
type Meta = Record<string, unknown>;
|
|
3
|
+
export type ParseMode = 'references' | 'core';
|
|
4
|
+
export type ParseOptions = {
|
|
5
|
+
mode?: ParseMode;
|
|
6
|
+
partials?: Meta;
|
|
7
|
+
strict?: boolean;
|
|
8
|
+
onWarning?: (diagnostic: Diagnostic) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare const parse: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: ParseOptions) => T;
|
|
11
|
+
/** @deprecated Use parse(). */
|
|
12
|
+
export declare const parseReferences: typeof parse;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/** Lima References 2.0 resolver. References 1.0 remains isolated in references.ts. */
|
|
2
|
+
import { canonicalString, codepointLength, countNodes, ingestPartialValue, PARTIAL_COUNT_LIMIT, PARTIAL_NAME_LENGTH_LIMIT, PARTIAL_NODE_LIMIT, RESULT_NODE_LIMIT, SCALAR_LENGTH_LIMIT, } from './value.js';
|
|
3
|
+
import { NESTING_DEPTH_LIMIT, parseCore, parseCoreWithPositions, toPlainValue, } from './core.js';
|
|
4
|
+
import { hasActiveReferences2 } from './scalars.js';
|
|
5
|
+
import { LimaError } from './errors.js';
|
|
6
|
+
import { collectAllParticipants, deepCopyPositioned, earliestParticipant, finalizePositioned, partialToPositioned, } from './references.js';
|
|
7
|
+
const emptyMapping = () => Object.create(null);
|
|
8
|
+
const PARTIAL_NAME = '[a-zA-Z0-9_][a-zA-Z0-9_:/-]*';
|
|
9
|
+
const PARTIAL_NAME_RE = new RegExp(`^${PARTIAL_NAME}$`);
|
|
10
|
+
const MAX_EDGES = 3;
|
|
11
|
+
const addDiagnostic = (ctx, diagnostic, offset = 0) => {
|
|
12
|
+
ctx.diagnostics.push({ ...diagnostic, offset });
|
|
13
|
+
};
|
|
14
|
+
const tokenMatches = (node) => node.references2 ?? [];
|
|
15
|
+
const lookupPath = (root, segments) => {
|
|
16
|
+
let current = root;
|
|
17
|
+
for (const segment of segments) {
|
|
18
|
+
if (current?.kind !== 'mapping')
|
|
19
|
+
return undefined;
|
|
20
|
+
current = current.entries.get(segment);
|
|
21
|
+
}
|
|
22
|
+
return current;
|
|
23
|
+
};
|
|
24
|
+
const documentTarget = (root, path) => {
|
|
25
|
+
const segments = path.split('.');
|
|
26
|
+
const value = root.get(segments[0]);
|
|
27
|
+
return value === undefined ? undefined : { root: value, tail: segments.slice(1) };
|
|
28
|
+
};
|
|
29
|
+
const lookupPartial = (partials, path) => {
|
|
30
|
+
const dot = path.indexOf('.');
|
|
31
|
+
if (dot < 0)
|
|
32
|
+
return partials.get(path);
|
|
33
|
+
return lookupPath(partials.get(path.slice(0, dot)), path.slice(dot + 1).split('.'));
|
|
34
|
+
};
|
|
35
|
+
const isActiveString = (node) => node.kind === 'string' && !node.quoted;
|
|
36
|
+
const scalarText = (value, token, line, offset, ctx) => {
|
|
37
|
+
if (value.kind === 'mapping') {
|
|
38
|
+
addDiagnostic(ctx, {
|
|
39
|
+
code: 'INVALID_INTERPOLATION', line, token,
|
|
40
|
+
message: `Lima: invalid interpolation of "${token}" at line ${line}: mapping cannot be interpolated into a string`,
|
|
41
|
+
}, offset);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (value.kind === 'array') {
|
|
45
|
+
if (value.items.some((item) => item.kind === 'array' || item.kind === 'mapping')) {
|
|
46
|
+
addDiagnostic(ctx, {
|
|
47
|
+
code: 'INVALID_INTERPOLATION', line, token,
|
|
48
|
+
message: `Lima: invalid interpolation of "${token}" at line ${line}: array contains a nested array or mapping`,
|
|
49
|
+
}, offset);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return value.items.map((item) => canonicalString(toPlainValue(item))).join(', ');
|
|
53
|
+
}
|
|
54
|
+
return canonicalString(toPlainValue(value));
|
|
55
|
+
};
|
|
56
|
+
const resolveNodeUncached = (node, document, partials, ctx, remainingEdges, stack) => {
|
|
57
|
+
if (node.kind === 'array') {
|
|
58
|
+
if (!hasActiveReferences2(node))
|
|
59
|
+
return { value: node, complete: true };
|
|
60
|
+
let complete = true;
|
|
61
|
+
const items = node.items.map((item) => {
|
|
62
|
+
const result = resolveNode(item, document, partials, ctx, remainingEdges, stack);
|
|
63
|
+
complete &&= result.complete;
|
|
64
|
+
if (isActiveString(item) && result.value.kind === 'array') {
|
|
65
|
+
const match = tokenMatches(item)[0];
|
|
66
|
+
const token = match?.token ?? item.value;
|
|
67
|
+
addDiagnostic(ctx, {
|
|
68
|
+
code: 'INVALID_REFERENCE_SHAPE', line: item.line, token,
|
|
69
|
+
message: `Lima: reference "${token}" resolves to an array, which cannot be inserted as a sequence item at line ${item.line}`,
|
|
70
|
+
});
|
|
71
|
+
return item;
|
|
72
|
+
}
|
|
73
|
+
return result.value;
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
value: { ...node, items, references2Active: items.some(hasActiveReferences2) || undefined },
|
|
77
|
+
complete,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (node.kind === 'mapping') {
|
|
81
|
+
if (!hasActiveReferences2(node))
|
|
82
|
+
return { value: node, complete: true };
|
|
83
|
+
let complete = true;
|
|
84
|
+
const entries = new Map();
|
|
85
|
+
for (const [key, child] of node.entries) {
|
|
86
|
+
const result = resolveNode(child, document, partials, ctx, remainingEdges, stack);
|
|
87
|
+
complete &&= result.complete;
|
|
88
|
+
entries.set(key, result.value);
|
|
89
|
+
}
|
|
90
|
+
let references2Active = false;
|
|
91
|
+
for (const value of entries.values()) {
|
|
92
|
+
if (hasActiveReferences2(value)) {
|
|
93
|
+
references2Active = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { value: { ...node, entries, references2Active: references2Active || undefined }, complete };
|
|
98
|
+
}
|
|
99
|
+
if (!isActiveString(node))
|
|
100
|
+
return { value: node, complete: true };
|
|
101
|
+
const recorded = tokenMatches(node);
|
|
102
|
+
const pure = recorded.length === 1 && recorded[0].index === 0 && recorded[0].token.length === node.value.length
|
|
103
|
+
? recorded[0]
|
|
104
|
+
: undefined;
|
|
105
|
+
if (pure) {
|
|
106
|
+
if (remainingEdges === 0)
|
|
107
|
+
return { value: node, complete: false };
|
|
108
|
+
const documentLookup = pure.documentPath !== undefined ? documentTarget(document, pure.documentPath) : undefined;
|
|
109
|
+
const target = pure.documentPath !== undefined ? documentLookup?.root : lookupPartial(partials, pure.partialPath);
|
|
110
|
+
if (target === undefined || stack.has(target))
|
|
111
|
+
return { value: node, complete: false };
|
|
112
|
+
if (pure.partialPath !== undefined) {
|
|
113
|
+
return {
|
|
114
|
+
value: { ...deepCopyPositioned(target), insertedAt: { line: pure.line, token: pure.token } },
|
|
115
|
+
complete: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
stack.add(target);
|
|
119
|
+
const result = resolveNode(target, document, partials, ctx, remainingEdges - 1, stack);
|
|
120
|
+
stack.delete(target);
|
|
121
|
+
if (!result.complete)
|
|
122
|
+
return { value: node, complete: false };
|
|
123
|
+
const selected = lookupPath(result.value, documentLookup?.tail ?? []);
|
|
124
|
+
if (selected === undefined)
|
|
125
|
+
return { value: node, complete: false };
|
|
126
|
+
return {
|
|
127
|
+
value: { ...deepCopyPositioned(selected), insertedAt: { line: pure.line, token: pure.token } },
|
|
128
|
+
complete: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const matches = recorded;
|
|
132
|
+
if (matches.length === 0)
|
|
133
|
+
return { value: node, complete: true };
|
|
134
|
+
let complete = true;
|
|
135
|
+
let cursor = 0;
|
|
136
|
+
let output = '';
|
|
137
|
+
const unresolvedTokens = [];
|
|
138
|
+
for (const match of matches) {
|
|
139
|
+
const offset = match.index;
|
|
140
|
+
output += node.value.slice(cursor, offset);
|
|
141
|
+
const documentLookup = match.documentPath !== undefined ? documentTarget(document, match.documentPath) : undefined;
|
|
142
|
+
const target = match.documentPath !== undefined ? documentLookup?.root : lookupPartial(partials, match.partialPath);
|
|
143
|
+
let replacement = null;
|
|
144
|
+
if (remainingEdges > 0 && target !== undefined && !stack.has(target)) {
|
|
145
|
+
if (match.partialPath !== undefined)
|
|
146
|
+
replacement = scalarText(target, match.token, match.line, match.offset, ctx);
|
|
147
|
+
else {
|
|
148
|
+
stack.add(target);
|
|
149
|
+
const result = resolveNode(target, document, partials, ctx, remainingEdges - 1, stack);
|
|
150
|
+
stack.delete(target);
|
|
151
|
+
const selected = result.complete ? lookupPath(result.value, documentLookup?.tail ?? []) : undefined;
|
|
152
|
+
if (selected !== undefined)
|
|
153
|
+
replacement = scalarText(selected, match.token, match.line, match.offset, ctx);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (replacement === null) {
|
|
157
|
+
complete = false;
|
|
158
|
+
unresolvedTokens.push({ ...match, index: output.length });
|
|
159
|
+
output += match.token;
|
|
160
|
+
}
|
|
161
|
+
else
|
|
162
|
+
output += replacement;
|
|
163
|
+
cursor = offset + match.token.length;
|
|
164
|
+
}
|
|
165
|
+
output += node.value.slice(cursor);
|
|
166
|
+
if (codepointLength(output) > SCALAR_LENGTH_LIMIT) {
|
|
167
|
+
const first = matches[0];
|
|
168
|
+
addDiagnostic(ctx, {
|
|
169
|
+
code: 'RESOURCE_LIMIT', line: first.line, token: first.token,
|
|
170
|
+
message: `Lima: scalar exceeds maximum length of ${SCALAR_LENGTH_LIMIT} code points at line ${first.line}`,
|
|
171
|
+
}, first.offset);
|
|
172
|
+
}
|
|
173
|
+
return { value: { ...node, value: output, references2: unresolvedTokens }, complete };
|
|
174
|
+
};
|
|
175
|
+
/** Successful and failed dependency results are immutable and budget-specific. */
|
|
176
|
+
const resolveNode = (node, document, partials, ctx, remainingEdges, stack) => {
|
|
177
|
+
const cacheable = stack.size > 1 && (node.kind === 'array' || node.kind === 'mapping' ||
|
|
178
|
+
(isActiveString(node) && tokenMatches(node).length > 0));
|
|
179
|
+
if (!cacheable)
|
|
180
|
+
return resolveNodeUncached(node, document, partials, ctx, remainingEdges, stack);
|
|
181
|
+
const byBudget = ctx.cache.get(node);
|
|
182
|
+
const cached = byBudget?.get(remainingEdges);
|
|
183
|
+
if (cached !== undefined)
|
|
184
|
+
return cached;
|
|
185
|
+
const result = resolveNodeUncached(node, document, partials, ctx, remainingEdges, stack);
|
|
186
|
+
if (byBudget !== undefined)
|
|
187
|
+
byBudget.set(remainingEdges, result);
|
|
188
|
+
else
|
|
189
|
+
ctx.cache.set(node, new Map([[remainingEdges, result]]));
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
192
|
+
const validatePartials = (raw) => {
|
|
193
|
+
const names = Object.keys(raw);
|
|
194
|
+
if (names.length > PARTIAL_COUNT_LIMIT) {
|
|
195
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', message: `Lima: too many partials (max ${PARTIAL_COUNT_LIMIT})` });
|
|
196
|
+
}
|
|
197
|
+
for (const name of names) {
|
|
198
|
+
if (!PARTIAL_NAME_RE.test(name)) {
|
|
199
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', partial: name, message: `Lima: invalid partial name "${name}"` });
|
|
200
|
+
}
|
|
201
|
+
if (codepointLength(name) > PARTIAL_NAME_LENGTH_LIMIT) {
|
|
202
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', partial: name, message: `Lima: invalid partial name "${name}": exceeds maximum length of ${PARTIAL_NAME_LENGTH_LIMIT} code points` });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const ingested = new Map();
|
|
206
|
+
for (const name of names)
|
|
207
|
+
ingested.set(name, ingestPartialValue(raw[name], name, name));
|
|
208
|
+
let nodes = 0;
|
|
209
|
+
for (const value of ingested.values())
|
|
210
|
+
nodes += countNodes(value);
|
|
211
|
+
if (nodes > PARTIAL_NODE_LIMIT) {
|
|
212
|
+
throw new LimaError({ code: 'INVALID_PARTIAL', message: `Lima: partials exceed the combined maximum of ${PARTIAL_NODE_LIMIT} value nodes` });
|
|
213
|
+
}
|
|
214
|
+
const result = new Map();
|
|
215
|
+
for (const [name, value] of ingested)
|
|
216
|
+
result.set(name, partialToPositioned(value, 0));
|
|
217
|
+
return result;
|
|
218
|
+
};
|
|
219
|
+
const scanUnresolved = (node, ctx) => {
|
|
220
|
+
if (isActiveString(node)) {
|
|
221
|
+
for (const match of tokenMatches(node)) {
|
|
222
|
+
addDiagnostic(ctx, {
|
|
223
|
+
code: 'UNRESOLVED_REFERENCE', line: match.line, token: match.token,
|
|
224
|
+
message: `Lima: unresolved reference "${match.token}" at line ${match.line}`,
|
|
225
|
+
}, match.offset);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (node.kind === 'array')
|
|
230
|
+
for (const item of node.items)
|
|
231
|
+
scanUnresolved(item, ctx);
|
|
232
|
+
if (node.kind === 'mapping')
|
|
233
|
+
for (const child of node.entries.values())
|
|
234
|
+
scanUnresolved(child, ctx);
|
|
235
|
+
};
|
|
236
|
+
export const parse = (frontMatter, options) => {
|
|
237
|
+
if (options?.mode !== undefined && options.mode !== 'core' && options.mode !== 'references') {
|
|
238
|
+
throw new TypeError(`Lima: invalid parse mode "${String(options.mode)}"`);
|
|
239
|
+
}
|
|
240
|
+
if (options?.mode === 'core') {
|
|
241
|
+
if (Object.prototype.hasOwnProperty.call(options, 'partials')) {
|
|
242
|
+
throw new TypeError('Lima: partials cannot be supplied in core mode');
|
|
243
|
+
}
|
|
244
|
+
// Core ignores the additional `mode` property. Passing the same object
|
|
245
|
+
// avoids allocating a projected options object on this hot dispatch path.
|
|
246
|
+
return parseCore(frontMatter, options);
|
|
247
|
+
}
|
|
248
|
+
const partials = validatePartials(options?.partials ?? {});
|
|
249
|
+
if (!frontMatter.includes('$(')) {
|
|
250
|
+
return parseCore(frontMatter, options);
|
|
251
|
+
}
|
|
252
|
+
const document = parseCoreWithPositions(frontMatter, { strict: options?.strict ?? false, onWarning: options?.onWarning });
|
|
253
|
+
const ctx = { diagnostics: [], cache: new WeakMap() };
|
|
254
|
+
const resolved = new Map();
|
|
255
|
+
for (const [key, value] of document) {
|
|
256
|
+
const stack = new Set([value]);
|
|
257
|
+
resolved.set(key, resolveNode(value, document, partials, ctx, MAX_EDGES, stack).value);
|
|
258
|
+
}
|
|
259
|
+
if (options?.strict)
|
|
260
|
+
for (const value of resolved.values())
|
|
261
|
+
scanUnresolved(value, ctx);
|
|
262
|
+
if (ctx.diagnostics.length > 0) {
|
|
263
|
+
ctx.diagnostics.sort((a, b) => a.line - b.line || a.offset - b.offset);
|
|
264
|
+
throw new LimaError(ctx.diagnostics[0]);
|
|
265
|
+
}
|
|
266
|
+
const finalized = [...resolved].map(([key, value]) => [key, finalizePositioned(value)]);
|
|
267
|
+
const depth = finalized.length === 0 ? 0 : Math.max(...finalized.map(([, value]) => value.depth));
|
|
268
|
+
if (depth > NESTING_DEPTH_LIMIT) {
|
|
269
|
+
const participants = finalized.filter(([, value]) => value.depth === depth).flatMap(([, value]) => value.deepestParticipants);
|
|
270
|
+
const winner = earliestParticipant(participants);
|
|
271
|
+
throw new LimaError({
|
|
272
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
273
|
+
message: `Lima: nesting depth exceeds maximum of ${NESTING_DEPTH_LIMIT} at line ${winner?.line ?? 1}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
let nodeCount = 1;
|
|
277
|
+
for (const [, value] of finalized)
|
|
278
|
+
nodeCount += value.nodeCount;
|
|
279
|
+
if (nodeCount > RESULT_NODE_LIMIT) {
|
|
280
|
+
const participants = [];
|
|
281
|
+
for (const value of resolved.values())
|
|
282
|
+
collectAllParticipants(value, participants);
|
|
283
|
+
const winner = earliestParticipant(participants);
|
|
284
|
+
throw new LimaError({
|
|
285
|
+
code: 'RESOURCE_LIMIT', line: winner?.line ?? 1, token: winner?.token,
|
|
286
|
+
message: `Lima: result exceeds maximum size of ${RESULT_NODE_LIMIT} total nodes at line ${winner?.line ?? 1}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const out = emptyMapping();
|
|
290
|
+
for (const [key, value] of finalized)
|
|
291
|
+
out[key] = value.native;
|
|
292
|
+
return out;
|
|
293
|
+
};
|
|
294
|
+
/** @deprecated Use parse(). */
|
|
295
|
+
export const parseReferences = parse;
|
package/dist/scalars.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { type LimaValue } from './value.js';
|
|
8
8
|
import { type ParseContext } from './normalize.js';
|
|
9
9
|
import type { ValueBuilder } from './builder.js';
|
|
10
|
+
import { type ReferenceToken2 } from './reference-tokens2.js';
|
|
10
11
|
/**
|
|
11
12
|
* `insertedAt` is never set by Core — it's a References-only annotation
|
|
12
13
|
* (see references.ts's `resolveTree`), stamped on the root of a value
|
|
@@ -45,6 +46,7 @@ export type PositionedValue = {
|
|
|
45
46
|
value: string;
|
|
46
47
|
line: number;
|
|
47
48
|
quoted: boolean;
|
|
49
|
+
references2?: ReferenceToken2[];
|
|
48
50
|
insertedAt?: InsertedAt;
|
|
49
51
|
} | {
|
|
50
52
|
kind: 'instant';
|
|
@@ -55,13 +57,16 @@ export type PositionedValue = {
|
|
|
55
57
|
kind: 'array';
|
|
56
58
|
items: PositionedValue[];
|
|
57
59
|
line: number;
|
|
60
|
+
references2Active?: boolean;
|
|
58
61
|
insertedAt?: InsertedAt;
|
|
59
62
|
} | {
|
|
60
63
|
kind: 'mapping';
|
|
61
64
|
entries: Map<string, PositionedValue>;
|
|
62
65
|
line: number;
|
|
66
|
+
references2Active?: boolean;
|
|
63
67
|
insertedAt?: InsertedAt;
|
|
64
68
|
};
|
|
69
|
+
export declare const hasActiveReferences2: (value: PositionedValue) => boolean;
|
|
65
70
|
/** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
|
|
66
71
|
export declare const positionedBuilder: ValueBuilder<PositionedValue>;
|
|
67
72
|
/** Strips position/quoted-origin annotations, recursively — the public parseCore() projection. */
|
package/dist/scalars.js
CHANGED
|
@@ -7,15 +7,29 @@
|
|
|
7
7
|
import { LNull, LBool, LFloat, LInt, LString, LInstant } from './value.js';
|
|
8
8
|
import { checkStringLimit } from './normalize.js';
|
|
9
9
|
import { LimaError } from './errors.js';
|
|
10
|
+
import { scanReferenceTokens2 } from './reference-tokens2.js';
|
|
11
|
+
export const hasActiveReferences2 = (value) => value.kind === 'string' ? (value.references2?.length ?? 0) > 0
|
|
12
|
+
: value.kind === 'array' || value.kind === 'mapping' ? value.references2Active === true
|
|
13
|
+
: false;
|
|
10
14
|
/** The `ValueBuilder<PositionedValue>` — reconstructs today's annotated tree exactly, for References. */
|
|
11
15
|
export const positionedBuilder = {
|
|
16
|
+
tracksStringSourcePositions: true,
|
|
12
17
|
null: (line) => ({ kind: 'null', line }),
|
|
13
18
|
bool: (value, line) => ({ kind: 'bool', value, line }),
|
|
14
19
|
int: (value, line) => ({ kind: 'int', value, line }),
|
|
15
20
|
float: (value, line) => ({ kind: 'float', value, line }),
|
|
16
|
-
string: (value, line, quoted) =>
|
|
21
|
+
string: (value, line, quoted, sourceSpans) => {
|
|
22
|
+
const references2 = quoted ? undefined : scanReferenceTokens2(value, line, sourceSpans);
|
|
23
|
+
return {
|
|
24
|
+
kind: 'string', value, line, quoted,
|
|
25
|
+
...(references2 && references2.length > 0 ? { references2 } : {}),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
17
28
|
instant: (value, line) => ({ kind: 'instant', value, line }),
|
|
18
|
-
array: (items, line) => ({
|
|
29
|
+
array: (items, line) => ({
|
|
30
|
+
kind: 'array', items, line,
|
|
31
|
+
...(items.some(hasActiveReferences2) ? { references2Active: true } : {}),
|
|
32
|
+
}),
|
|
19
33
|
createMapping: () => new Map(),
|
|
20
34
|
createMappingWith: (key, value) => new Map([[key, value]]),
|
|
21
35
|
hasMappingKey: (entries, key) => entries.has(key),
|
|
@@ -28,7 +42,16 @@ export const positionedBuilder = {
|
|
|
28
42
|
}
|
|
29
43
|
return max;
|
|
30
44
|
},
|
|
31
|
-
mapping: (entries, line) =>
|
|
45
|
+
mapping: (entries, line) => {
|
|
46
|
+
let references2Active = false;
|
|
47
|
+
for (const value of entries.values()) {
|
|
48
|
+
if (hasActiveReferences2(value)) {
|
|
49
|
+
references2Active = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { kind: 'mapping', entries, line, ...(references2Active ? { references2Active: true } : {}) };
|
|
54
|
+
},
|
|
32
55
|
};
|
|
33
56
|
/** Strips position/quoted-origin annotations, recursively — the public parseCore() projection. */
|
|
34
57
|
export const toPlainValue = (v) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@limaformat/lima",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lima frontmatter parser — LIMA Is Metadata Annotation",
|
|
5
5
|
"keywords": ["frontmatter", "parser", "yaml", "markdown", "metadata"],
|
|
6
6
|
"homepage": "https://limaformat.dev",
|
|
@@ -20,24 +20,25 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"files": ["dist", "README.md", "LICENSE", "CHANGELOG.md"],
|
|
23
|
-
"engines": {
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
23
|
+
"engines": { "node": ">=18" },
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
|
+
"pretest": "bun run build",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "bun run typecheck && bun test",
|
|
30
|
+
"bench": "bun run bench/index.ts",
|
|
31
|
+
"bench:vs-yaml": "bun run bench/vs-yaml.ts",
|
|
32
|
+
"bench:vs-yaml:node": "node bench/vs-yaml-node.mjs",
|
|
33
|
+
"bench:cross-language": "bun run bench/cross-language.ts",
|
|
34
|
+
"bench:differential": "bun run bench/differential.ts",
|
|
35
|
+
"wordcount": "bun run scripts/wordcount.ts"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/bun": "^1.3.14",
|
|
39
|
+
"@types/js-yaml": "^4.0.9",
|
|
40
|
+
"js-yaml": "^5.2.3",
|
|
41
|
+
"typescript": "^7.0.2",
|
|
42
|
+
"typescript-classic": "npm:typescript@^5.9.3"
|
|
43
|
+
}
|
|
43
44
|
}
|