@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/CHANGELOG.md +29 -0
- package/LICENSE +15 -0
- package/README.md +70 -0
- package/dist/block-cursor.d.ts +24 -0
- package/dist/block-cursor.js +71 -0
- package/dist/block.d.ts +18 -0
- package/dist/block.js +305 -0
- package/dist/builder.d.ts +41 -0
- package/dist/builder.js +1 -0
- package/dist/chars.d.ts +2 -0
- package/dist/chars.js +4 -0
- package/dist/core.d.ts +73 -0
- package/dist/core.js +439 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +24 -0
- package/dist/flow.d.ts +7 -0
- package/dist/flow.js +162 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/normalize.d.ts +41 -0
- package/dist/normalize.js +67 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.js +522 -0
- package/dist/scalars.d.ts +89 -0
- package/dist/scalars.js +407 -0
- package/dist/scanner.d.ts +82 -0
- package/dist/scanner.js +204 -0
- package/dist/value.d.ts +95 -0
- package/dist/value.js +185 -0
- package/package.json +41 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@limaformat/lima` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
This package's version number tracks its own release history and is
|
|
7
|
+
independent of the Lima specification version — [Lima Core
|
|
8
|
+
1.0](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
|
|
9
|
+
and [Lima References
|
|
10
|
+
1.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-1.0-spec.md)
|
|
11
|
+
are both frozen regardless of what this file shows.
|
|
12
|
+
|
|
13
|
+
## [0.1.0] — Unreleased
|
|
14
|
+
|
|
15
|
+
Initial release.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `parseCore(input, options?)` — Lima Core 1.0 only, no reference
|
|
20
|
+
resolution.
|
|
21
|
+
- `parse(input, options?)` / `parseReferences(input, options?)` — Core plus
|
|
22
|
+
the References 1.0 extension (`($key)` document references, `(%key)`
|
|
23
|
+
external partials).
|
|
24
|
+
- Zero runtime dependencies.
|
|
25
|
+
- Verified against the shared, implementation-independent conformance
|
|
26
|
+
corpus (250 cases as of this release; count pinned by a test — see
|
|
27
|
+
[`docs/guide.md`](https://github.com/limaformat/lima/blob/main/docs/guide.md)
|
|
28
|
+
and
|
|
29
|
+
[`docs/corpus-design/`](https://github.com/limaformat/lima/tree/main/docs/corpus-design)).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Kortstiege
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @limaformat/lima
|
|
2
|
+
|
|
3
|
+
**LIMA Is Metadata Annotation** — a small, predictable frontmatter format.
|
|
4
|
+
A deliberate, focused subset of YAML: the part frontmatter actually needs,
|
|
5
|
+
with well-defined types, no surprises, and zero runtime dependencies.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @limaformat/lima
|
|
9
|
+
# or
|
|
10
|
+
bun add @limaformat/lima
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { parse } from '@limaformat/lima'
|
|
15
|
+
|
|
16
|
+
const result = parse(`
|
|
17
|
+
title: Hello World
|
|
18
|
+
published: 2024-03-01
|
|
19
|
+
draft: false
|
|
20
|
+
tags:
|
|
21
|
+
- javascript
|
|
22
|
+
- webdev
|
|
23
|
+
`)
|
|
24
|
+
|
|
25
|
+
// {
|
|
26
|
+
// title: 'Hello World',
|
|
27
|
+
// published: 2024-03-01T00:00:00.000Z, // a Date
|
|
28
|
+
// draft: false,
|
|
29
|
+
// tags: ['javascript', 'webdev']
|
|
30
|
+
// }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`parse` accepts the raw content **between** the frontmatter delimiters
|
|
34
|
+
(`---`) — stripping them is the caller's job.
|
|
35
|
+
|
|
36
|
+
Two functions are exported:
|
|
37
|
+
|
|
38
|
+
- `parseCore(input, options?)` — the base format only.
|
|
39
|
+
- `parse(input, options?)` (alias for `parseReferences`) — adds
|
|
40
|
+
`($key)` document references and `(%key)` external partials.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
parse(frontmatter, { strict: true })
|
|
44
|
+
parse(frontmatter, { partials: { author: 'Alice' } })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Full syntax reference, the References extension, resource limits, strict
|
|
48
|
+
mode, and the complete API:
|
|
49
|
+
**[docs/guide.md](https://github.com/limaformat/lima/blob/main/docs/guide.md)**.
|
|
50
|
+
|
|
51
|
+
Why Lima exists, the case against YAML, and security rationale:
|
|
52
|
+
**[repository README](https://github.com/limaformat/lima#readme)**.
|
|
53
|
+
|
|
54
|
+
The [Lima Core 1.0](https://github.com/limaformat/lima/blob/main/docs/lima-core-1.0-spec.md)
|
|
55
|
+
and [Lima References 1.0](https://github.com/limaformat/lima/blob/main/docs/lima-references-1.0-spec.md)
|
|
56
|
+
specifications are the normative source of truth — this package implements
|
|
57
|
+
them exactly, verified against a
|
|
58
|
+
[250-case conformance corpus](https://github.com/limaformat/lima/tree/main/corpus)
|
|
59
|
+
(count pinned by a test) shared with any other Lima implementation.
|
|
60
|
+
|
|
61
|
+
Migrating existing YAML frontmatter:
|
|
62
|
+
[docs/migrating-from-yaml.md](https://github.com/limaformat/lima/blob/main/docs/migrating-from-yaml.md).
|
|
63
|
+
Static site generator integration status:
|
|
64
|
+
[docs/integrations.md](https://github.com/limaformat/lima/blob/main/docs/integrations.md).
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
ISC — see
|
|
69
|
+
[LICENSE](https://github.com/limaformat/lima/blob/main/js/LICENSE) (also
|
|
70
|
+
included in this package as `LICENSE`).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocation-free physical-line cursor over one top-level block extent.
|
|
3
|
+
* Positions are UTF-16 code-unit offsets. Structural boundaries are ASCII;
|
|
4
|
+
* indentation additionally follows the complete whitespace predicate used by
|
|
5
|
+
* `trimStart()`, matching the existing block grammar.
|
|
6
|
+
*/
|
|
7
|
+
export declare class BlockCursor {
|
|
8
|
+
readonly source: string;
|
|
9
|
+
readonly rangeStart: number;
|
|
10
|
+
readonly rangeEnd: number;
|
|
11
|
+
lineStart: number;
|
|
12
|
+
lineEnd: number;
|
|
13
|
+
contentStart: number;
|
|
14
|
+
indent: number;
|
|
15
|
+
asciiIndent: number;
|
|
16
|
+
lineIndex: number;
|
|
17
|
+
valid: boolean;
|
|
18
|
+
maxIndent: number;
|
|
19
|
+
private nextStart;
|
|
20
|
+
constructor(source: string, rangeStart: number, rangeEnd: number);
|
|
21
|
+
next(): boolean;
|
|
22
|
+
get empty(): boolean;
|
|
23
|
+
get firstCode(): number;
|
|
24
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isTrimWhitespace } from './chars.js';
|
|
2
|
+
/**
|
|
3
|
+
* Allocation-free physical-line cursor over one top-level block extent.
|
|
4
|
+
* Positions are UTF-16 code-unit offsets. Structural boundaries are ASCII;
|
|
5
|
+
* indentation additionally follows the complete whitespace predicate used by
|
|
6
|
+
* `trimStart()`, matching the existing block grammar.
|
|
7
|
+
*/
|
|
8
|
+
export class BlockCursor {
|
|
9
|
+
source;
|
|
10
|
+
rangeStart;
|
|
11
|
+
rangeEnd;
|
|
12
|
+
lineStart = 0;
|
|
13
|
+
lineEnd = 0;
|
|
14
|
+
contentStart = 0;
|
|
15
|
+
indent = 0;
|
|
16
|
+
asciiIndent = 0;
|
|
17
|
+
lineIndex = -1;
|
|
18
|
+
valid = false;
|
|
19
|
+
maxIndent = 0;
|
|
20
|
+
nextStart;
|
|
21
|
+
constructor(source, rangeStart, rangeEnd) {
|
|
22
|
+
this.source = source;
|
|
23
|
+
this.rangeStart = rangeStart;
|
|
24
|
+
this.rangeEnd = rangeEnd;
|
|
25
|
+
this.nextStart = rangeStart;
|
|
26
|
+
}
|
|
27
|
+
next() {
|
|
28
|
+
if (this.nextStart >= this.rangeEnd) {
|
|
29
|
+
this.valid = false;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const start = this.nextStart;
|
|
33
|
+
const newline = this.source.indexOf('\n', start);
|
|
34
|
+
const end = newline === -1 || newline >= this.rangeEnd ? this.rangeEnd : newline;
|
|
35
|
+
let content = start;
|
|
36
|
+
while (content < end && this.source.charCodeAt(content) === 0x20)
|
|
37
|
+
content++;
|
|
38
|
+
const asciiEnd = content;
|
|
39
|
+
// After ordinary space indentation, printable ASCII starts content
|
|
40
|
+
// immediately. Only ASCII control whitespace or non-ASCII code units
|
|
41
|
+
// need the complete trimStart-compatible predicate.
|
|
42
|
+
if (content < end) {
|
|
43
|
+
let code = this.source.charCodeAt(content);
|
|
44
|
+
if ((code >= 0x09 && code <= 0x0d) || code > 0x7f) {
|
|
45
|
+
do {
|
|
46
|
+
if (!isTrimWhitespace(code))
|
|
47
|
+
break;
|
|
48
|
+
content++;
|
|
49
|
+
code = this.source.charCodeAt(content);
|
|
50
|
+
} while (content < end);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.lineStart = start;
|
|
54
|
+
this.lineEnd = end;
|
|
55
|
+
this.contentStart = content;
|
|
56
|
+
this.asciiIndent = asciiEnd - start;
|
|
57
|
+
this.indent = content - start;
|
|
58
|
+
if (this.indent > this.maxIndent)
|
|
59
|
+
this.maxIndent = this.indent;
|
|
60
|
+
this.lineIndex++;
|
|
61
|
+
this.valid = true;
|
|
62
|
+
this.nextStart = newline === -1 || newline >= this.rangeEnd ? this.rangeEnd : newline + 1;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
get empty() {
|
|
66
|
+
return this.contentStart === this.lineEnd;
|
|
67
|
+
}
|
|
68
|
+
get firstCode() {
|
|
69
|
+
return this.source.charCodeAt(this.contentStart);
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/block.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core §7 block collections (sequences and mappings) — a direct structural
|
|
3
|
+
* port of the legacy block parser, generic over its output representation
|
|
4
|
+
* (see `builder.ts`) so `parseCore` can build the public native result
|
|
5
|
+
* directly, without also building the References-only annotated
|
|
6
|
+
* `PositionedValue` tree it never needs. No reference-resolution concerns
|
|
7
|
+
* at all (no `resolve()` call anywhere, no array-as-sequence-item
|
|
8
|
+
* reference-shape check — that error class cannot occur here since Core
|
|
9
|
+
* never resolves a reference in the first place).
|
|
10
|
+
*/
|
|
11
|
+
import { type ParseContext } from './normalize.js';
|
|
12
|
+
import type { ValueBuilder } from './builder.js';
|
|
13
|
+
export declare const findKeySep: (s: string) => number;
|
|
14
|
+
/** Complete block grammar over one UTF-16 source range. */
|
|
15
|
+
export type BlockDepthRisk = {
|
|
16
|
+
mayExceed: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare const parseBlockRange: <V, M>(source: string, start: number, end: number, ctx: ParseContext, baseLine: number, builder: ValueBuilder<V, M>, depthRisk: BlockDepthRisk) => V | null;
|
package/dist/block.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core §7 block collections (sequences and mappings) — a direct structural
|
|
3
|
+
* port of the legacy block parser, generic over its output representation
|
|
4
|
+
* (see `builder.ts`) so `parseCore` can build the public native result
|
|
5
|
+
* directly, without also building the References-only annotated
|
|
6
|
+
* `PositionedValue` tree it never needs. No reference-resolution concerns
|
|
7
|
+
* at all (no `resolve()` call anywhere, no array-as-sequence-item
|
|
8
|
+
* reference-shape check — that error class cannot occur here since Core
|
|
9
|
+
* never resolves a reference in the first place).
|
|
10
|
+
*/
|
|
11
|
+
import { LString } from './value.js';
|
|
12
|
+
import { checkKeyLength, checkDuplicateKey, checkScalarLimit, NESTING_DEPTH_LIMIT } from './normalize.js';
|
|
13
|
+
import { stripKeyQuotes, unescapeDQ, stripComment, parseQuotedOrTyped, } from './scalars.js';
|
|
14
|
+
import { parseFlowMapping, parseFlowOrScalarValue } from './flow.js';
|
|
15
|
+
import { LimaError } from './errors.js';
|
|
16
|
+
import { isTrimWhitespace } from './chars.js';
|
|
17
|
+
import { BlockCursor } from './block-cursor.js';
|
|
18
|
+
const DASH_PREFIX_RE = /^-\s+/;
|
|
19
|
+
export const findKeySep = (s) => {
|
|
20
|
+
const first = s.charCodeAt(0);
|
|
21
|
+
if (first === 39 || first === 34) {
|
|
22
|
+
let i = 1;
|
|
23
|
+
while (i < s.length && s.charCodeAt(i) !== first)
|
|
24
|
+
i++;
|
|
25
|
+
if (s.charCodeAt(i + 1) === 58 && s.charCodeAt(i + 2) === 32)
|
|
26
|
+
return i + 1;
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
return s.indexOf(': ');
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Recursively parses a block value from a mutable physical-line cursor over
|
|
33
|
+
* the original source. Strings are materialized only when grammar or scalar
|
|
34
|
+
* parsing needs their content.
|
|
35
|
+
*/
|
|
36
|
+
/** `source.slice(start, end).trim()` with no temporary untrimmed substring. */
|
|
37
|
+
const trimSlice = (source, start, end) => {
|
|
38
|
+
if (start < end) {
|
|
39
|
+
const first = source.charCodeAt(start);
|
|
40
|
+
const last = source.charCodeAt(end - 1);
|
|
41
|
+
// Printable ASCII cannot be consumed by trim(), except U+0020 SPACE.
|
|
42
|
+
// Canonical frontmatter keys/values overwhelmingly have printable,
|
|
43
|
+
// non-space ASCII boundaries, so avoid both Unicode predicate calls.
|
|
44
|
+
if (first > 0x20 && first < 0x7f && last > 0x20 && last < 0x7f) {
|
|
45
|
+
return source.slice(start, end);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
while (start < end && isTrimWhitespace(source.charCodeAt(start)))
|
|
49
|
+
start++;
|
|
50
|
+
while (end > start && isTrimWhitespace(source.charCodeAt(end - 1)))
|
|
51
|
+
end--;
|
|
52
|
+
return source.slice(start, end);
|
|
53
|
+
};
|
|
54
|
+
const cursorContent = (cursor) => cursor.source.slice(cursor.contentStart, cursor.lineEnd);
|
|
55
|
+
const cursorAfterDash = (cursor) => {
|
|
56
|
+
const start = cursor.contentStart;
|
|
57
|
+
const end = cursor.lineEnd;
|
|
58
|
+
if (start + 1 === end)
|
|
59
|
+
return '';
|
|
60
|
+
let content = start + 1;
|
|
61
|
+
if (!isTrimWhitespace(cursor.source.charCodeAt(content)))
|
|
62
|
+
return cursor.source.slice(start, end);
|
|
63
|
+
while (content < end && isTrimWhitespace(cursor.source.charCodeAt(content)))
|
|
64
|
+
content++;
|
|
65
|
+
return cursor.source.slice(content, end);
|
|
66
|
+
};
|
|
67
|
+
/** Shared block grammar consuming one mutable physical-line cursor. */
|
|
68
|
+
const parseCursorBlock = (cursor, baseIndent, ctx, baseLine, builder) => {
|
|
69
|
+
let items = null;
|
|
70
|
+
let entries = null;
|
|
71
|
+
let pendingItem = null;
|
|
72
|
+
const startLine = cursor.lineIndex;
|
|
73
|
+
while (cursor.valid) {
|
|
74
|
+
const line = baseLine + cursor.lineIndex;
|
|
75
|
+
if (cursor.empty || cursor.firstCode === 35) {
|
|
76
|
+
cursor.next();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const indent = cursor.indent;
|
|
80
|
+
if (indent < baseIndent)
|
|
81
|
+
break;
|
|
82
|
+
if (indent > baseIndent) {
|
|
83
|
+
const trimmed = cursorContent(cursor);
|
|
84
|
+
if (items !== null && pendingItem !== null) {
|
|
85
|
+
const colonPos = findKeySep(trimmed);
|
|
86
|
+
if (colonPos !== -1) {
|
|
87
|
+
const key = stripKeyQuotes(trimSlice(trimmed, 0, colonPos));
|
|
88
|
+
checkKeyLength(key, () => line);
|
|
89
|
+
let raw = trimSlice(trimmed, colonPos + 2, trimmed.length);
|
|
90
|
+
if (raw.includes('#'))
|
|
91
|
+
raw = stripComment(raw);
|
|
92
|
+
builder.setMapping(pendingItem, key, parseFlowOrScalarValue(raw, ctx, line, builder));
|
|
93
|
+
cursor.next();
|
|
94
|
+
}
|
|
95
|
+
else if (trimmed.endsWith(':')) {
|
|
96
|
+
const key = stripKeyQuotes(trimSlice(trimmed, 0, trimmed.length - 1));
|
|
97
|
+
checkKeyLength(key, () => line);
|
|
98
|
+
cursor.next();
|
|
99
|
+
while (cursor.valid && cursor.empty)
|
|
100
|
+
cursor.next();
|
|
101
|
+
if (cursor.valid && cursor.indent > indent) {
|
|
102
|
+
const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
|
|
103
|
+
builder.setMapping(pendingItem, key, nested ?? builder.null(line));
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
builder.setMapping(pendingItem, key, builder.null(line));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (ctx.strict)
|
|
110
|
+
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
111
|
+
message: `LIMA: unexpected syntax in array item continuation at line ${line}: "${trimmed}"` });
|
|
112
|
+
cursor.next();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
if (ctx.strict)
|
|
117
|
+
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
118
|
+
message: `LIMA: unexpected indentation at line ${line}: "${trimmed}"` });
|
|
119
|
+
cursor.next();
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (cursor.firstCode === 45) {
|
|
124
|
+
if (pendingItem !== null) {
|
|
125
|
+
items.push(builder.mapping(pendingItem, line));
|
|
126
|
+
pendingItem = null;
|
|
127
|
+
}
|
|
128
|
+
if (items === null)
|
|
129
|
+
items = [];
|
|
130
|
+
if (entries !== null) {
|
|
131
|
+
if (ctx.strict)
|
|
132
|
+
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
133
|
+
message: `LIMA: mixed array and map entries for the same key at line ${line}` });
|
|
134
|
+
cursor.next();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
let afterDash = cursorAfterDash(cursor);
|
|
138
|
+
if (afterDash.includes('#'))
|
|
139
|
+
afterDash = stripComment(afterDash);
|
|
140
|
+
const first = afterDash.charCodeAt(0);
|
|
141
|
+
if (first !== 34 && first !== 39 && first !== 45 && first !== 123 &&
|
|
142
|
+
afterDash.indexOf(': ') === -1 && !afterDash.endsWith(':')) {
|
|
143
|
+
items.push(parseQuotedOrTyped(afterDash, ctx, line, false, builder));
|
|
144
|
+
cursor.next();
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const flowMap = parseFlowMapping(afterDash, ctx, line, builder);
|
|
148
|
+
const colonPos = findKeySep(afterDash);
|
|
149
|
+
if (flowMap !== null) {
|
|
150
|
+
items.push(flowMap);
|
|
151
|
+
cursor.next();
|
|
152
|
+
}
|
|
153
|
+
else if (afterDash === '-' || DASH_PREFIX_RE.test(afterDash)) {
|
|
154
|
+
if (ctx.strict)
|
|
155
|
+
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
156
|
+
message: `LIMA: nested block sequence at line ${line}: "${cursorContent(cursor)}"` });
|
|
157
|
+
items.push(builder.null(line));
|
|
158
|
+
cursor.next();
|
|
159
|
+
while (cursor.valid) {
|
|
160
|
+
if (cursor.empty || cursor.source.charCodeAt(cursor.contentStart) === 35) {
|
|
161
|
+
cursor.next();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (cursor.indent <= baseIndent)
|
|
165
|
+
break;
|
|
166
|
+
cursor.next();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (colonPos !== -1) {
|
|
170
|
+
const keyFirst = afterDash.charCodeAt(0);
|
|
171
|
+
const keyLast = afterDash.charCodeAt(colonPos - 1);
|
|
172
|
+
const keyRaw = keyFirst > 0x20 && keyFirst < 0x7f && keyLast > 0x20 && keyLast < 0x7f
|
|
173
|
+
? afterDash.slice(0, colonPos) : trimSlice(afterDash, 0, colonPos);
|
|
174
|
+
const key = keyFirst === 34 || keyFirst === 39 ? stripKeyQuotes(keyRaw) : keyRaw;
|
|
175
|
+
checkKeyLength(key, () => line);
|
|
176
|
+
const valueStart = colonPos + 2;
|
|
177
|
+
const valueFirst = afterDash.charCodeAt(valueStart);
|
|
178
|
+
const valueLast = afterDash.charCodeAt(afterDash.length - 1);
|
|
179
|
+
const raw = valueFirst > 0x20 && valueFirst < 0x7f && valueLast > 0x20 && valueLast < 0x7f
|
|
180
|
+
? afterDash.slice(valueStart) : trimSlice(afterDash, valueStart, afterDash.length);
|
|
181
|
+
pendingItem = builder.createMappingWith(key, parseFlowOrScalarValue(raw, ctx, line, builder));
|
|
182
|
+
cursor.next();
|
|
183
|
+
while (cursor.valid && cursor.indent > baseIndent) {
|
|
184
|
+
const continuationLine = baseLine + cursor.lineIndex;
|
|
185
|
+
const start = cursor.contentStart, end = cursor.lineEnd;
|
|
186
|
+
const cfirst = cursor.source.charCodeAt(start);
|
|
187
|
+
if (cfirst === 34 || cfirst === 39 || cfirst === 35)
|
|
188
|
+
break;
|
|
189
|
+
const sep = cursor.source.indexOf(': ', start);
|
|
190
|
+
if (sep === -1 || sep >= end)
|
|
191
|
+
break;
|
|
192
|
+
const keyLast = cursor.source.charCodeAt(sep - 1);
|
|
193
|
+
const ckey = cfirst > 0x20 && cfirst < 0x7f && keyLast > 0x20 && keyLast < 0x7f
|
|
194
|
+
? cursor.source.slice(start, sep) : trimSlice(cursor.source, start, sep);
|
|
195
|
+
if (!ckey)
|
|
196
|
+
break;
|
|
197
|
+
checkKeyLength(ckey, () => continuationLine);
|
|
198
|
+
const valueStart = sep + 2;
|
|
199
|
+
const valueFirst = cursor.source.charCodeAt(valueStart);
|
|
200
|
+
const valueLast = cursor.source.charCodeAt(end - 1);
|
|
201
|
+
let value = valueFirst > 0x20 && valueFirst < 0x7f && valueLast > 0x20 && valueLast < 0x7f
|
|
202
|
+
? cursor.source.slice(valueStart, end) : trimSlice(cursor.source, valueStart, end);
|
|
203
|
+
if (value.includes('#'))
|
|
204
|
+
value = stripComment(value);
|
|
205
|
+
builder.setMapping(pendingItem, ckey, parseFlowOrScalarValue(value, ctx, continuationLine, builder));
|
|
206
|
+
cursor.next();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (afterDash.endsWith(':')) {
|
|
210
|
+
const key = stripKeyQuotes(trimSlice(afterDash, 0, afterDash.length - 1));
|
|
211
|
+
checkKeyLength(key, () => line);
|
|
212
|
+
cursor.next();
|
|
213
|
+
while (cursor.valid && cursor.empty)
|
|
214
|
+
cursor.next();
|
|
215
|
+
if (cursor.valid && cursor.indent > baseIndent) {
|
|
216
|
+
const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
|
|
217
|
+
pendingItem = builder.createMappingWith(key, nested ?? builder.null(line));
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
pendingItem = builder.createMappingWith(key, builder.null(line));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const q = afterDash.charCodeAt(0);
|
|
224
|
+
if ((q === 34 || q === 39) && afterDash.charCodeAt(afterDash.length - 1) === q) {
|
|
225
|
+
const inner = afterDash.slice(1, -1);
|
|
226
|
+
const value = q === 34 ? unescapeDQ(inner, ctx.strict, line) : inner.replace(/\\'/g, "'");
|
|
227
|
+
checkScalarLimit(LString(value), line);
|
|
228
|
+
items.push(builder.string(value, line, true));
|
|
229
|
+
}
|
|
230
|
+
else
|
|
231
|
+
items.push(parseQuotedOrTyped(afterDash, ctx, line, false, builder));
|
|
232
|
+
cursor.next();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const trimmed = cursorContent(cursor);
|
|
237
|
+
if (items !== null) {
|
|
238
|
+
if (ctx.strict)
|
|
239
|
+
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
240
|
+
message: `LIMA: mixed map and array entries for the same key at line ${line}` });
|
|
241
|
+
cursor.next();
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const colonPos = findKeySep(trimmed);
|
|
245
|
+
if (colonPos !== -1) {
|
|
246
|
+
if (entries === null)
|
|
247
|
+
entries = builder.createMapping();
|
|
248
|
+
const key = stripKeyQuotes(trimSlice(trimmed, 0, colonPos));
|
|
249
|
+
checkKeyLength(key, () => line);
|
|
250
|
+
if (ctx.strict || ctx.onWarning !== undefined)
|
|
251
|
+
checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
|
|
252
|
+
let raw = trimSlice(trimmed, colonPos + 2, trimmed.length);
|
|
253
|
+
if (raw.includes('#'))
|
|
254
|
+
raw = stripComment(raw);
|
|
255
|
+
builder.setMapping(entries, key, parseFlowOrScalarValue(raw, ctx, line, builder));
|
|
256
|
+
cursor.next();
|
|
257
|
+
}
|
|
258
|
+
else if (trimmed.endsWith(':')) {
|
|
259
|
+
if (entries === null)
|
|
260
|
+
entries = builder.createMapping();
|
|
261
|
+
const key = stripKeyQuotes(trimSlice(trimmed, 0, trimmed.length - 1));
|
|
262
|
+
checkKeyLength(key, () => line);
|
|
263
|
+
if (ctx.strict || ctx.onWarning !== undefined)
|
|
264
|
+
checkDuplicateKey(builder.hasMappingKey(entries, key), key, line, ctx);
|
|
265
|
+
cursor.next();
|
|
266
|
+
while (cursor.valid && cursor.empty)
|
|
267
|
+
cursor.next();
|
|
268
|
+
if (cursor.valid && cursor.indent > baseIndent) {
|
|
269
|
+
const nested = parseCursorBlock(cursor, cursor.indent, ctx, baseLine, builder);
|
|
270
|
+
builder.setMapping(entries, key, nested ?? builder.null(line));
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
builder.setMapping(entries, key, builder.null(line));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
if (ctx.strict)
|
|
277
|
+
throw new LimaError({ code: 'INVALID_INDENTATION', line,
|
|
278
|
+
message: `LIMA: indented freetext without a block scalar marker at line ${line}: "${trimmed}"` });
|
|
279
|
+
cursor.next();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (pendingItem !== null)
|
|
284
|
+
items.push(builder.mapping(pendingItem, baseLine + startLine));
|
|
285
|
+
return items !== null ? builder.array(items, baseLine + startLine) :
|
|
286
|
+
entries !== null ? builder.mapping(entries, baseLine + startLine) : null;
|
|
287
|
+
};
|
|
288
|
+
export const parseBlockRange = (source, start, end, ctx, baseLine, builder, depthRisk) => {
|
|
289
|
+
const cursor = new BlockCursor(source, start, end);
|
|
290
|
+
if (!cursor.next())
|
|
291
|
+
return null;
|
|
292
|
+
while (cursor.valid && cursor.empty)
|
|
293
|
+
cursor.next();
|
|
294
|
+
if (!cursor.valid)
|
|
295
|
+
return null;
|
|
296
|
+
const baseIndent = cursor.asciiIndent;
|
|
297
|
+
const value = parseCursorBlock(cursor, baseIndent, ctx, baseLine, builder);
|
|
298
|
+
// Recursive block containers require strictly increasing integer
|
|
299
|
+
// indentation. At one indentation level a sequence-item mapping can add
|
|
300
|
+
// one container and Core flow syntax at most two more, so delta + 4 is a
|
|
301
|
+
// conservative depth bound below the document root.
|
|
302
|
+
if (cursor.maxIndent - baseIndent + 4 > NESTING_DEPTH_LIMIT)
|
|
303
|
+
depthRisk.mayExceed = true;
|
|
304
|
+
return value;
|
|
305
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameterizes the Core grammar (`scalars.ts`, `flow.ts`, `block.ts`) over
|
|
3
|
+
* its output representation, so the same parsing control flow can produce
|
|
4
|
+
* either the References-needed annotated `PositionedValue` tree or, for
|
|
5
|
+
* `parseCore`, the public native result directly — without a second,
|
|
6
|
+
* hand-copied parser that could drift from the first (see core.ts's
|
|
7
|
+
* `nativeBuilder` and scalars.ts's `positionedBuilder`).
|
|
8
|
+
*
|
|
9
|
+
* `M` is the builder's own in-progress mapping accumulator type — a bare
|
|
10
|
+
* `Map<string, V>` for `positionedBuilder` (later wrapped into a
|
|
11
|
+
* `PositionedValue` mapping node by `mapping()`), a prototype-free
|
|
12
|
+
* `Record<string, V>` for `nativeBuilder` (where the accumulator already
|
|
13
|
+
* *is* the finished public shape, so `mapping()` is a no-op pass-through —
|
|
14
|
+
* this is what lets `parseCore` skip building and copying an intermediate
|
|
15
|
+
* `Map` for every mapping, including the top-level document root).
|
|
16
|
+
*
|
|
17
|
+
* Claude Code review note: an earlier version of this interface typed the
|
|
18
|
+
* accumulator as `unknown`, requiring every concrete builder to `as`-cast
|
|
19
|
+
* it back to its real type at every one of `createMapping`/`hasMappingKey`/
|
|
20
|
+
* `setMapping`/`mappingMaxDepth`/`mapping` — full call-site type-checking on
|
|
21
|
+
* `block.ts`/`flow.ts`/`scalars.ts` (they only ever go through these
|
|
22
|
+
* methods, never touch `M` directly) with none of the safety lost in
|
|
23
|
+
* exchange, purely by making `M` a second generic parameter instead. This
|
|
24
|
+
* is compile-time-only: generics are erased, so the emitted JS — and
|
|
25
|
+
* therefore performance — is identical either way, verified by benchmark.
|
|
26
|
+
*/
|
|
27
|
+
export interface ValueBuilder<V, M = Map<string, V>> {
|
|
28
|
+
null(line: number): V;
|
|
29
|
+
bool(value: boolean, line: number): V;
|
|
30
|
+
int(value: number, line: number): V;
|
|
31
|
+
float(value: number, line: number): V;
|
|
32
|
+
string(value: string, line: number, quoted: boolean): V;
|
|
33
|
+
instant(value: Date, line: number): V;
|
|
34
|
+
array(items: V[], line: number): V;
|
|
35
|
+
createMapping(): M;
|
|
36
|
+
createMappingWith(key: string, value: V): M;
|
|
37
|
+
hasMappingKey(entries: M, key: string): boolean;
|
|
38
|
+
setMapping(entries: M, key: string, value: V): void;
|
|
39
|
+
mappingMaxDepth(entries: M, depthOf: (value: V) => number): number;
|
|
40
|
+
mapping(entries: M, line: number): V;
|
|
41
|
+
}
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/chars.d.ts
ADDED
package/dist/chars.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Exact ECMAScript WhiteSpace + LineTerminator set for one UTF-16 code unit. */
|
|
2
|
+
export const isTrimWhitespace = (c) => c === 0x0009 || (c >= 0x000a && c <= 0x000d) || c === 0x0020 || c === 0x00a0 ||
|
|
3
|
+
c === 0x1680 || (c >= 0x2000 && c <= 0x200a) || c === 0x2028 || c === 0x2029 ||
|
|
4
|
+
c === 0x202f || c === 0x205f || c === 0x3000 || c === 0xfeff;
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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 { type LimaValue } from './value.js';
|
|
22
|
+
import { type ParseContext, type Diagnostic } from './normalize.js';
|
|
23
|
+
import { type PositionedValue } from './scalars.js';
|
|
24
|
+
import type { ValueBuilder } from './builder.js';
|
|
25
|
+
export type { Diagnostic, ParseContext } from './normalize.js';
|
|
26
|
+
export { NESTING_DEPTH_LIMIT, SCALAR_LENGTH_LIMIT } from './normalize.js';
|
|
27
|
+
export type { PositionedValue, InsertedAt } from './scalars.js';
|
|
28
|
+
export { toPlainValue } from './scalars.js';
|
|
29
|
+
type Meta = Record<string, unknown>;
|
|
30
|
+
export type CoreOptions = {
|
|
31
|
+
strict?: boolean;
|
|
32
|
+
/** Core §11.2: callback for non-strict warnings (e.g. duplicate keys). Discarded if omitted. */
|
|
33
|
+
onWarning?: (diagnostic: Diagnostic) => void;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Parses LIMA Core 1.0 syntax into the internal annotated value tree —
|
|
37
|
+
* every node carrying its source line, string leaves additionally carrying
|
|
38
|
+
* whether they came from quoted syntax. `($key)`/`(%key)` text is left
|
|
39
|
+
* exactly as written; nothing here ever inspects or resolves it. The
|
|
40
|
+
* primitive the References layer (`references.ts`) builds on.
|
|
41
|
+
*/
|
|
42
|
+
export declare const parseCoreWithPositions: (frontMatter: string, ctx: ParseContext) => Map<string, PositionedValue>;
|
|
43
|
+
/** The public result shape (Core §11.1): every value `toNative*` can produce. */
|
|
44
|
+
export type NativeValue = null | boolean | number | string | Date | NativeValue[] | {
|
|
45
|
+
[key: string]: NativeValue;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The `ValueBuilder<NativeValue>` — `parseCore`'s fast path. Every scalar
|
|
49
|
+
* builder is the identity function: unlike `positionedBuilder`, there is no
|
|
50
|
+
* wrapper object to allocate at all, only the value itself. `array` and
|
|
51
|
+
* `mapping` build the exact public shape directly (a real array; a
|
|
52
|
+
* prototype-free object per Core §11.1), so `parseCore` never needs a
|
|
53
|
+
* separate conversion pass over an intermediate tree afterward.
|
|
54
|
+
*/
|
|
55
|
+
export declare const nativeBuilder: ValueBuilder<NativeValue, Record<string, NativeValue>>;
|
|
56
|
+
/** Converts a Lima value to a plain, native JS value (the public result shape). */
|
|
57
|
+
export declare const toNative: (v: LimaValue) => NativeValue;
|
|
58
|
+
/**
|
|
59
|
+
* `toNative(toPlainValue(v))` in one pass instead of two full tree walks —
|
|
60
|
+
* matters most for large, reference-expanded results (many copies of a
|
|
61
|
+
* sizeable partial), where the position/quoted-stripping pass and the
|
|
62
|
+
* native-conversion pass would otherwise each independently visit every
|
|
63
|
+
* node of the same, potentially large, final tree.
|
|
64
|
+
*/
|
|
65
|
+
export declare const toNativeFromPositioned: (v: PositionedValue) => NativeValue;
|
|
66
|
+
/**
|
|
67
|
+
* Public Core 1.0 entry point. Never resolves `($key)`/`(%key)` text — see
|
|
68
|
+
* the module doc comment. Equivalent in observable behavior to calling
|
|
69
|
+
* `parseReferences()` with no partials on a document that happens to
|
|
70
|
+
* contain no references, except that here reference-shaped text is
|
|
71
|
+
* guaranteed to always pass through unresolved, even in strict mode.
|
|
72
|
+
*/
|
|
73
|
+
export declare const parseCore: <T extends Record<string, unknown> = Meta>(frontMatter: string, options?: CoreOptions) => T;
|