@jarenjs/md 0.56.0 → 0.67.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/README.md +25 -2
- package/dist/types/compiler.d.ts +2 -2
- package/dist/types/loader.d.ts +1 -1
- package/dist/types/utils.d.ts +4 -2
- package/docs/LOADER.md +13 -4
- package/docs/MD-FORMAT.md +12 -4
- package/package.json +4 -4
- package/src/compiler.js +4 -3
- package/src/frontmatter.js +36 -25
- package/src/loader.js +0 -0
- package/src/utils.js +9 -4
package/README.md
CHANGED
|
@@ -139,7 +139,9 @@ mdToVnode(doc, { headingIds: true, slugPrefix: 'user-content-' });
|
|
|
139
139
|
|
|
140
140
|
Repeated headings are numbered the way GitHub numbers them (`setup`,
|
|
141
141
|
`setup-1`, `setup-2`), and a heading with no slug-worthy text (`## ***`)
|
|
142
|
-
lands on `section`.
|
|
142
|
+
lands on `section`. Numbered ids are reserved too: headings `Foo`, `Foo`,
|
|
143
|
+
`Foo-1` receive `foo`, `foo-1`, `foo-1-1`, so every permalink remains unique.
|
|
144
|
+
Ids are **off by default on purpose**: CommonMark
|
|
143
145
|
renders a heading as `<h1>Foo</h1>`, so emitting one by default would put
|
|
144
146
|
the conformance score below at odds with what the package produces. The
|
|
145
147
|
rules are normative in [MD-FORMAT.md](docs/MD-FORMAT.md) §4.5; the slug
|
|
@@ -315,7 +317,9 @@ createApp(appDoc, {
|
|
|
315
317
|
- A document whose frontmatter declares a schema (`form:` or `$schema`)
|
|
316
318
|
feeds [`@jarenjs/forms`](../../packages/forms) through `mdToForm(doc, forms)` —
|
|
317
319
|
`{ schema, fields, data }`, with the forms module injected so this
|
|
318
|
-
package stays dependency-free.
|
|
320
|
+
package stays dependency-free. When frontmatter omits `data`, the built
|
|
321
|
+
form model supplies schema defaults and `const` values; explicit data,
|
|
322
|
+
including `null`, is preserved.
|
|
319
323
|
- In an [`@jarenjs/app`](../../packages/app) document, a view can be a JSLT
|
|
320
324
|
stylesheet over a loaded MdDocument: register `loadMarkdown` as an
|
|
321
325
|
async effect that dispatches the plain `MdDocument` into the state,
|
|
@@ -537,6 +541,25 @@ Measured, not claimed — `npm run benchmark:markdown`, <!--fact:md.measured-->2
|
|
|
537
541
|
tag's attributes — is normalized away on every engine's output, not
|
|
538
542
|
just this one's.
|
|
539
543
|
|
|
544
|
+
## Exports
|
|
545
|
+
|
|
546
|
+
Every subpath a consumer can import, derived from the manifest by
|
|
547
|
+
`npm run docs:derive` (`npm run docs:check` fails when the two drift):
|
|
548
|
+
|
|
549
|
+
<!--fact:exports.md-->
|
|
550
|
+
| Import | Kind | Declarations |
|
|
551
|
+
|---|---|---|
|
|
552
|
+
| `@jarenjs/md` | JavaScript | declared |
|
|
553
|
+
| `@jarenjs/md/plugins` | JavaScript | declared |
|
|
554
|
+
| `@jarenjs/md/html` | JavaScript | declared |
|
|
555
|
+
| `@jarenjs/md/mdx` | JavaScript | declared |
|
|
556
|
+
| `@jarenjs/md/directives` | JavaScript | declared |
|
|
557
|
+
| `@jarenjs/md/component` | JavaScript | declared |
|
|
558
|
+
| `@jarenjs/md/styles/md.css` | asset | — |
|
|
559
|
+
| `@jarenjs/md/schemas/jaren-md-ast.schema.json` | schema | — |
|
|
560
|
+
| `@jarenjs/md/package.json` | metadata | — |
|
|
561
|
+
<!--/fact-->
|
|
562
|
+
|
|
540
563
|
## Development
|
|
541
564
|
|
|
542
565
|
Tests live in the repository root: [`test/md/`](../../test/md)
|
package/dist/types/compiler.d.ts
CHANGED
|
@@ -128,12 +128,12 @@ export declare function frontmatterExternals(frontmatter: any): Record<string, a
|
|
|
128
128
|
*
|
|
129
129
|
* @param {MdDocument} doc
|
|
130
130
|
* @param {{ buildFormModel: (schema: any) => any,
|
|
131
|
-
* createInitialData: (
|
|
131
|
+
* createInitialData: (fields: any) => any }} forms
|
|
132
132
|
* @returns {{ schema: any, fields: any, data: any } | null}
|
|
133
133
|
*/
|
|
134
134
|
export declare function mdToForm(doc: MdDocument, forms: {
|
|
135
135
|
buildFormModel: (schema: any) => any;
|
|
136
|
-
createInitialData: (
|
|
136
|
+
createInitialData: (fields: any) => any;
|
|
137
137
|
}): {
|
|
138
138
|
schema: any;
|
|
139
139
|
fields: any;
|
package/dist/types/loader.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `loadMarkdown` resolves any URL the platform `fetch` accepts into a
|
|
5
5
|
* compiled document. The shared LRU cache keys on normalized URL +
|
|
6
|
-
*
|
|
6
|
+
* compile options, remembers `ETag`/`Last-Modified` validators, shares
|
|
7
7
|
* in-flight fetches, and revalidates stale entries in the background.
|
|
8
8
|
* When the response body is a `ReadableStream` the parser runs
|
|
9
9
|
* block-by-block as chunks arrive — `streamMarkdown` exposes that as
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -20,8 +20,10 @@ export { hashContent, fnv1a, FNV1A_OFFSET_BASIS, slugify } from '@jarenjs/core/s
|
|
|
20
20
|
*
|
|
21
21
|
* Both emitters mint ids, so the rule lives here once: slug the text,
|
|
22
22
|
* substitute `section` when nothing slug-worthy survives, number
|
|
23
|
-
* repeats the way GitHub numbers them (`setup`, `setup-1`, `setup-2`)
|
|
24
|
-
*
|
|
23
|
+
* repeats the way GitHub numbers them (`setup`, `setup-1`, `setup-2`),
|
|
24
|
+
* reserving every emitted slug so a literal `setup-1` cannot collide
|
|
25
|
+
* with a numbered `setup`, and prefix the result. The COUNTER belongs
|
|
26
|
+
* to the caller — one map per
|
|
25
27
|
* emission, never shared with another numbering (a block key's hash and
|
|
26
28
|
* a slug share a namespace only by accident, and a collision there would
|
|
27
29
|
* shift an unrelated heading's number).
|
package/docs/LOADER.md
CHANGED
|
@@ -32,14 +32,23 @@ remembers the response's `ETag` and `Last-Modified`; a cache hit
|
|
|
32
32
|
resolves immediately with the cached compiled document, and a
|
|
33
33
|
revalidating fetch (`If-None-Match`/`If-Modified-Since`) only replaces
|
|
34
34
|
the entry when the origin answers with new content (`200` with a
|
|
35
|
-
different validator). Entries also key on
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
different validator). Entries also key on every compile-relevant option:
|
|
36
|
+
GFM and frontmatter parsing, source retention, HTML handling, heading and
|
|
37
|
+
footnote policies, and vnode keys. Plugins and the injected TOML/URL
|
|
38
|
+
callbacks key by identity, so equally named plugins with different
|
|
39
|
+
implementations never alias. Treat plugin objects and callbacks as
|
|
40
|
+
immutable for the lifetime of a compiled document.
|
|
38
41
|
|
|
39
42
|
- `createMdCache(limit = 64)` — an LRU cache instance; the default
|
|
40
43
|
shared instance is exported as `defaultMdCache`.
|
|
41
44
|
- `cache: false` — bypass entirely.
|
|
42
|
-
- `cache.delete(url)`
|
|
45
|
+
- `cache.delete(url)` — invalidate the normalized URL's default-options
|
|
46
|
+
entry; `cache.clear()` invalidates every entry, including option variants.
|
|
47
|
+
|
|
48
|
+
Invalidation also revokes a pending load or background revalidation's
|
|
49
|
+
right to update that entry. Its original caller still receives its
|
|
50
|
+
response, but an older completion cannot restore a cleared entry,
|
|
51
|
+
overwrite a newer document, or evict a newer load on failure.
|
|
43
52
|
|
|
44
53
|
Concurrent `loadMarkdown` calls for the same key share one in-flight
|
|
45
54
|
fetch (the promise itself is cached), so a burst of loads costs one
|
package/docs/MD-FORMAT.md
CHANGED
|
@@ -101,11 +101,15 @@ The built-in parser handles, from scratch and dependency-free:
|
|
|
101
101
|
`yes`/`no`, sexagesimals, hex ints and timestamps — is a **string**.
|
|
102
102
|
- **Quoted strings** — double quotes with JSON-style escapes
|
|
103
103
|
(`\n \t \r \b \f \0 \\ \" \uXXXX`), single quotes with `''` → `'`.
|
|
104
|
+
An opening quote requires a closing quote; an unterminated string is
|
|
105
|
+
a located frontmatter error.
|
|
104
106
|
- **Block maps and sequences** by indentation (spaces only). A sequence
|
|
105
107
|
MAY sit at the same indent as its parent key. `- key: value` opens an
|
|
106
108
|
inline map item.
|
|
107
109
|
- **Flow collections** — `[a, b]` and `{a: 1}`, nesting freely, spanning
|
|
108
|
-
multiple lines while brackets remain open.
|
|
110
|
+
multiple lines while brackets remain open. Plain values retain internal
|
|
111
|
+
colons (`[a:b]`, `{url: http://host}`); shorthand map entries inside
|
|
112
|
+
sequences (`[a: b]`) are outside the subset and are rejected.
|
|
109
113
|
- **Block scalars** — literal `|` and folded `>`, each with the `-`
|
|
110
114
|
chomp (drop the final newline). The `+` chomp and explicit indent
|
|
111
115
|
indicators are not supported.
|
|
@@ -133,7 +137,10 @@ with dotted paths, bare/quoted/dotted keys, basic and literal strings,
|
|
|
133
137
|
integers (decimal/hex/octal/binary with `_`), floats, booleans, flow
|
|
134
138
|
arrays (multi-line), inline tables and comments. Datetimes and any other
|
|
135
139
|
unrecognized value are kept as verbatim **strings**; multi-line strings
|
|
136
|
-
are not supported.
|
|
140
|
+
are not supported. Comments end at each physical line, including within
|
|
141
|
+
multi-line arrays. Dotted keys in an inline table preserve sibling
|
|
142
|
+
members, and table names such as `__proto__` are ordinary own properties.
|
|
143
|
+
An unterminated quoted string is a located frontmatter error.
|
|
137
144
|
|
|
138
145
|
### 3.4 Frontmatter as ambient variables
|
|
139
146
|
|
|
@@ -288,8 +295,9 @@ GitHub, in an editor preview and in this renderer:
|
|
|
288
295
|
The result MAY be empty (`## ***`); an emitter MUST then substitute
|
|
289
296
|
`section`, because a heading with no landing place cannot be linked.
|
|
290
297
|
Within one document, the **second** heading yielding a given identifier
|
|
291
|
-
MUST get `-1` appended, the third `-2`, and so on,
|
|
292
|
-
|
|
298
|
+
MUST get `-1` appended, the third `-2`, and so on, skipping any identifier
|
|
299
|
+
already emitted (including numbered identifiers from another heading).
|
|
300
|
+
Count over the substituted value so `***` twice yields `section` and `section-1`. The
|
|
293
301
|
counter is per emission and MUST NOT be shared with any other numbering
|
|
294
302
|
in the emitter.
|
|
295
303
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/md",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.67.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"prepack": "npm run build:types"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@jarenjs/core": "^0.
|
|
77
|
-
"@jarenjs/mermaid": "^0.
|
|
78
|
-
"@jarenjs/view": "^0.
|
|
76
|
+
"@jarenjs/core": "^0.67.0",
|
|
77
|
+
"@jarenjs/mermaid": "^0.67.0",
|
|
78
|
+
"@jarenjs/view": "^0.67.0"
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/compiler.js
CHANGED
|
@@ -152,7 +152,7 @@ export function frontmatterExternals(frontmatter) {
|
|
|
152
152
|
*
|
|
153
153
|
* @param {MdDocument} doc
|
|
154
154
|
* @param {{ buildFormModel: (schema: any) => any,
|
|
155
|
-
* createInitialData: (
|
|
155
|
+
* createInitialData: (fields: any) => any }} forms
|
|
156
156
|
* @returns {{ schema: any, fields: any, data: any } | null}
|
|
157
157
|
*/
|
|
158
158
|
export function mdToForm(doc, forms) {
|
|
@@ -162,6 +162,7 @@ export function mdToForm(doc, forms) {
|
|
|
162
162
|
? fm.form
|
|
163
163
|
: typeof fm.$schema === 'object' && fm.$schema !== null ? fm.$schema : null;
|
|
164
164
|
if (schema === null) return null;
|
|
165
|
-
const
|
|
166
|
-
|
|
165
|
+
const fields = forms.buildFormModel(schema);
|
|
166
|
+
const data = fm.data !== undefined ? fm.data : forms.createInitialData(fields);
|
|
167
|
+
return { schema, fields, data };
|
|
167
168
|
}
|
package/src/frontmatter.js
CHANGED
|
@@ -403,7 +403,7 @@ function splitYamlKey(line, indent, lineNo) {
|
|
|
403
403
|
if (key.length > 1) {
|
|
404
404
|
const q = key.charCodeAt(0);
|
|
405
405
|
if ((q === 0x22 || q === 0x27) && key.charCodeAt(key.length - 1) === q) {
|
|
406
|
-
key = String(parseQuoted(key, 0, /** @type {'"'|"'"} */ (key[0])).value);
|
|
406
|
+
key = String(parseQuoted(key, 0, /** @type {'"'|"'"} */ (key[0]), lineNo).value);
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
let restColumn = indent + colon + 1;
|
|
@@ -462,7 +462,7 @@ function parseYamlFlowOrScalar(state, rest, lineNo) {
|
|
|
462
462
|
const flow = parseFlowValue(text, 0, lineNo);
|
|
463
463
|
return flow.value;
|
|
464
464
|
}
|
|
465
|
-
return parseYamlScalar(stripComment(rest));
|
|
465
|
+
return parseYamlScalar(stripComment(rest), lineNo);
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
/**
|
|
@@ -509,13 +509,14 @@ function stripComment(text) {
|
|
|
509
509
|
/**
|
|
510
510
|
* Parse a scalar: quoted string, null, boolean, number, or plain string.
|
|
511
511
|
* @param {string} text trimmed scalar text
|
|
512
|
+
* @param {number} lineNo
|
|
512
513
|
* @returns {any}
|
|
513
514
|
*/
|
|
514
|
-
function parseYamlScalar(text) {
|
|
515
|
+
function parseYamlScalar(text, lineNo) {
|
|
515
516
|
if (text === '') return null;
|
|
516
517
|
const c0 = text.charCodeAt(0);
|
|
517
518
|
if (c0 === 0x22 || c0 === 0x27) {
|
|
518
|
-
return parseQuoted(text, 0, /** @type {'"'|"'"} */ (text[0])).value;
|
|
519
|
+
return parseQuoted(text, 0, /** @type {'"'|"'"} */ (text[0]), lineNo).value;
|
|
519
520
|
}
|
|
520
521
|
switch (text) {
|
|
521
522
|
case 'null': case 'Null': case 'NULL': case '~': return null;
|
|
@@ -533,9 +534,10 @@ function parseYamlScalar(text) {
|
|
|
533
534
|
* @param {string} text
|
|
534
535
|
* @param {number} pos
|
|
535
536
|
* @param {'"'|"'"} quote
|
|
537
|
+
* @param {number} lineNo
|
|
536
538
|
* @returns {{ value: string, end: number }}
|
|
537
539
|
*/
|
|
538
|
-
function parseQuoted(text, pos, quote) {
|
|
540
|
+
function parseQuoted(text, pos, quote, lineNo) {
|
|
539
541
|
let out = '';
|
|
540
542
|
let i = pos + 1;
|
|
541
543
|
while (i < text.length) {
|
|
@@ -569,7 +571,7 @@ function parseQuoted(text, pos, quote) {
|
|
|
569
571
|
out += ch;
|
|
570
572
|
i++;
|
|
571
573
|
}
|
|
572
|
-
|
|
574
|
+
throw new MdFrontmatterError('unterminated quoted string', lineNo);
|
|
573
575
|
}
|
|
574
576
|
|
|
575
577
|
/**
|
|
@@ -578,26 +580,29 @@ function parseQuoted(text, pos, quote) {
|
|
|
578
580
|
* @param {string} text
|
|
579
581
|
* @param {number} pos
|
|
580
582
|
* @param {number} lineNo
|
|
583
|
+
* @param {boolean} [key=false] whether a colon ends a map key
|
|
581
584
|
* @returns {{ value: any, end: number }}
|
|
582
585
|
*/
|
|
583
|
-
function parseFlowValue(text, pos, lineNo) {
|
|
586
|
+
function parseFlowValue(text, pos, lineNo, key = false) {
|
|
584
587
|
while (pos < text.length && text.charCodeAt(pos) === 0x20) pos++;
|
|
585
588
|
const c = text.charCodeAt(pos);
|
|
586
589
|
if (c === 0x5B /* [ */) return parseFlowSeq(text, pos, lineNo);
|
|
587
590
|
if (c === 0x7B /* { */) return parseFlowMap(text, pos, lineNo);
|
|
588
591
|
if (c === 0x22 || c === 0x27) {
|
|
589
|
-
return parseQuoted(text, pos, /** @type {'"'|"'"} */ (text[pos]));
|
|
592
|
+
return parseQuoted(text, pos, /** @type {'"'|"'"} */ (text[pos]), lineNo);
|
|
590
593
|
}
|
|
591
594
|
let end = pos;
|
|
592
595
|
let depth = 0;
|
|
593
596
|
while (end < text.length) {
|
|
594
597
|
const cc = text.charCodeAt(end);
|
|
595
|
-
if (depth === 0 && (cc === 0x2C || cc === 0x5D || cc === 0x7D
|
|
598
|
+
if (depth === 0 && (cc === 0x2C || cc === 0x5D || cc === 0x7D)) break;
|
|
599
|
+
if (depth === 0 && cc === 0x3A
|
|
600
|
+
&& (key || end + 1 === text.length || /[\s,[\]{}]/.test(text[end + 1]))) break;
|
|
596
601
|
if (cc === 0x5B || cc === 0x7B) depth++;
|
|
597
602
|
else if (cc === 0x5D || cc === 0x7D) depth--;
|
|
598
603
|
end++;
|
|
599
604
|
}
|
|
600
|
-
return { value: parseYamlScalar(text.slice(pos, end).trim()), end };
|
|
605
|
+
return { value: parseYamlScalar(text.slice(pos, end).trim(), lineNo), end };
|
|
601
606
|
}
|
|
602
607
|
|
|
603
608
|
/**
|
|
@@ -620,6 +625,7 @@ function parseFlowArray(text, pos, lineNo, parseItem, unterminated) {
|
|
|
620
625
|
if (i >= text.length) throw new MdFrontmatterError(unterminated, lineNo);
|
|
621
626
|
if (text.charCodeAt(i) === 0x5D /* ] */) return { value: out, end: i + 1 };
|
|
622
627
|
const item = parseItem(text, i, lineNo);
|
|
628
|
+
if (item.end <= i) throw new MdFrontmatterError('expected a flow array item', lineNo);
|
|
623
629
|
out.push(item.value);
|
|
624
630
|
i = item.end;
|
|
625
631
|
}
|
|
@@ -651,7 +657,7 @@ function parseFlowMap(text, pos, lineNo) {
|
|
|
651
657
|
while (i < text.length && (text.charCodeAt(i) === 0x20 || text.charCodeAt(i) === 0x2C)) i++;
|
|
652
658
|
if (i >= text.length) throw new MdFrontmatterError('unterminated flow map', lineNo);
|
|
653
659
|
if (text.charCodeAt(i) === 0x7D /* } */) return { value: out, end: i + 1 };
|
|
654
|
-
const key = parseFlowValue(text, i, lineNo);
|
|
660
|
+
const key = parseFlowValue(text, i, lineNo, true);
|
|
655
661
|
i = key.end;
|
|
656
662
|
while (i < text.length && text.charCodeAt(i) === 0x20) i++;
|
|
657
663
|
if (text.charCodeAt(i) !== 0x3A /* : */) {
|
|
@@ -699,16 +705,15 @@ export function parseTomlSubset(text) {
|
|
|
699
705
|
const eq = findTomlEquals(line);
|
|
700
706
|
if (eq === -1) throw new MdFrontmatterError(`expected 'key = value', got '${line}'`, no);
|
|
701
707
|
const path = parseTomlKeyPath(line.slice(0, eq), no);
|
|
702
|
-
let valueText = line.slice(eq + 1).trim();
|
|
703
|
-
//
|
|
704
|
-
while (flowDepth(
|
|
705
|
-
valueText += ' ' + lines[++no].trim();
|
|
708
|
+
let valueText = stripTomlComment(line.slice(eq + 1)).trim();
|
|
709
|
+
// Comments end at each physical line, before continuation lines join.
|
|
710
|
+
while (flowDepth(valueText) > 0 && no + 1 < lines.length) {
|
|
711
|
+
valueText += ' ' + stripTomlComment(lines[++no]).trim();
|
|
706
712
|
}
|
|
707
|
-
valueText = stripTomlComment(valueText).trim();
|
|
708
713
|
let target = table;
|
|
709
714
|
for (let i = 0; i < path.length - 1; i++) {
|
|
710
715
|
const step = path[i];
|
|
711
|
-
if (!(step
|
|
716
|
+
if (!Object.hasOwn(target, step) || typeof target[step] !== 'object') {
|
|
712
717
|
const next = {};
|
|
713
718
|
setObjectMember(target, step, next);
|
|
714
719
|
target = next;
|
|
@@ -772,7 +777,7 @@ function parseTomlKeyPath(text, no) {
|
|
|
772
777
|
while (i < text.length && text.charCodeAt(i) === 0x20) i++;
|
|
773
778
|
const c = text.charCodeAt(i);
|
|
774
779
|
if (c === 0x22 || c === 0x27) {
|
|
775
|
-
const q = parseQuoted(text, i, /** @type {'"'|"'"} */ (text[i]));
|
|
780
|
+
const q = parseQuoted(text, i, /** @type {'"'|"'"} */ (text[i]), no);
|
|
776
781
|
out.push(q.value);
|
|
777
782
|
i = q.end;
|
|
778
783
|
}
|
|
@@ -812,7 +817,7 @@ function descendTomlTable(root, path, isArray, no) {
|
|
|
812
817
|
let target = root;
|
|
813
818
|
for (let i = 0; i < path.length - 1; i++) {
|
|
814
819
|
const step = path[i];
|
|
815
|
-
let next = target[step];
|
|
820
|
+
let next = Object.hasOwn(target, step) ? target[step] : undefined;
|
|
816
821
|
if (next === undefined) {
|
|
817
822
|
next = {};
|
|
818
823
|
setObjectMember(target, step, next);
|
|
@@ -827,7 +832,7 @@ function descendTomlTable(root, path, isArray, no) {
|
|
|
827
832
|
}
|
|
828
833
|
const leaf = path[path.length - 1];
|
|
829
834
|
if (isArray) {
|
|
830
|
-
let arr = target[leaf];
|
|
835
|
+
let arr = Object.hasOwn(target, leaf) ? target[leaf] : undefined;
|
|
831
836
|
if (arr === undefined) {
|
|
832
837
|
arr = [];
|
|
833
838
|
setObjectMember(target, leaf, arr);
|
|
@@ -837,7 +842,7 @@ function descendTomlTable(root, path, isArray, no) {
|
|
|
837
842
|
arr.push(fresh);
|
|
838
843
|
return fresh;
|
|
839
844
|
}
|
|
840
|
-
let next = target[leaf];
|
|
845
|
+
let next = Object.hasOwn(target, leaf) ? target[leaf] : undefined;
|
|
841
846
|
if (next === undefined) {
|
|
842
847
|
next = {};
|
|
843
848
|
setObjectMember(target, leaf, next);
|
|
@@ -861,7 +866,7 @@ function parseTomlValue(text, no) {
|
|
|
861
866
|
if (text === '') throw new MdFrontmatterError('missing value', no);
|
|
862
867
|
const c0 = text.charCodeAt(0);
|
|
863
868
|
if (c0 === 0x22 || c0 === 0x27) {
|
|
864
|
-
return parseQuoted(text, 0, /** @type {'"'|"'"} */ (text[0])).value;
|
|
869
|
+
return parseQuoted(text, 0, /** @type {'"'|"'"} */ (text[0]), no).value;
|
|
865
870
|
}
|
|
866
871
|
if (c0 === 0x5B /* [ */) return parseTomlArray(text, no).value;
|
|
867
872
|
if (c0 === 0x7B /* { */) return parseTomlInline(text, no).value;
|
|
@@ -907,8 +912,14 @@ function parseTomlInline(text, no) {
|
|
|
907
912
|
const item = parseTomlItem(text, end + 1, no);
|
|
908
913
|
let target = out;
|
|
909
914
|
for (let p = 0; p < path.length - 1; p++) {
|
|
910
|
-
const
|
|
911
|
-
|
|
915
|
+
const step = path[p];
|
|
916
|
+
let next = Object.hasOwn(target, step) ? target[step] : undefined;
|
|
917
|
+
if (next === undefined) {
|
|
918
|
+
next = {};
|
|
919
|
+
setObjectMember(target, step, next);
|
|
920
|
+
}
|
|
921
|
+
if (next === null || typeof next !== 'object' || Array.isArray(next))
|
|
922
|
+
throw new MdFrontmatterError(`'${step}' is not a table`, no);
|
|
912
923
|
target = next;
|
|
913
924
|
}
|
|
914
925
|
setObjectMember(target, path[path.length - 1], item.value);
|
|
@@ -935,7 +946,7 @@ function parseTomlItem(text, pos, no) {
|
|
|
935
946
|
return { value: inner.value, end: pos + inner.end };
|
|
936
947
|
}
|
|
937
948
|
if (c === 0x22 || c === 0x27) {
|
|
938
|
-
return parseQuoted(text, pos, /** @type {'"'|"'"} */ (text[pos]));
|
|
949
|
+
return parseQuoted(text, pos, /** @type {'"'|"'"} */ (text[pos]), no);
|
|
939
950
|
}
|
|
940
951
|
let end = pos;
|
|
941
952
|
while (end < text.length) {
|
package/src/loader.js
CHANGED
|
Binary file
|
package/src/utils.js
CHANGED
|
@@ -25,8 +25,10 @@ export { hashContent, fnv1a, FNV1A_OFFSET_BASIS, slugify } from '@jarenjs/core/s
|
|
|
25
25
|
*
|
|
26
26
|
* Both emitters mint ids, so the rule lives here once: slug the text,
|
|
27
27
|
* substitute `section` when nothing slug-worthy survives, number
|
|
28
|
-
* repeats the way GitHub numbers them (`setup`, `setup-1`, `setup-2`)
|
|
29
|
-
*
|
|
28
|
+
* repeats the way GitHub numbers them (`setup`, `setup-1`, `setup-2`),
|
|
29
|
+
* reserving every emitted slug so a literal `setup-1` cannot collide
|
|
30
|
+
* with a numbered `setup`, and prefix the result. The COUNTER belongs
|
|
31
|
+
* to the caller — one map per
|
|
30
32
|
* emission, never shared with another numbering (a block key's hash and
|
|
31
33
|
* a slug share a namespace only by accident, and a collision there would
|
|
32
34
|
* shift an unrelated heading's number).
|
|
@@ -38,9 +40,12 @@ export { hashContent, fnv1a, FNV1A_OFFSET_BASIS, slugify } from '@jarenjs/core/s
|
|
|
38
40
|
*/
|
|
39
41
|
export function headingId(text, seen, prefix) {
|
|
40
42
|
const base = slugify(text) || 'section';
|
|
41
|
-
|
|
43
|
+
let count = seen.get(base) ?? 0;
|
|
44
|
+
let slug = count === 0 ? base : base + '-' + count;
|
|
45
|
+
while (seen.has(slug)) slug = base + '-' + ++count;
|
|
42
46
|
seen.set(base, count + 1);
|
|
43
|
-
|
|
47
|
+
if (slug !== base) seen.set(slug, 1);
|
|
48
|
+
return prefix + slug;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/**
|