@jarenjs/md 0.34.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 +520 -0
- package/dist/types/ast.d.ts +181 -0
- package/dist/types/bake.d.ts +61 -0
- package/dist/types/compiler.d.ts +141 -0
- package/dist/types/component/index.d.ts +101 -0
- package/dist/types/directives.d.ts +126 -0
- package/dist/types/entities.d.ts +40 -0
- package/dist/types/footnotes.d.ts +83 -0
- package/dist/types/frontmatter.d.ts +67 -0
- package/dist/types/html.d.ts +72 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/loader.d.ts +84 -0
- package/dist/types/mdx.d.ts +45 -0
- package/dist/types/parser.d.ts +116 -0
- package/dist/types/plugins/highlight.d.ts +64 -0
- package/dist/types/plugins/index.d.ts +64 -0
- package/dist/types/plugins/mermaid.d.ts +12 -0
- package/dist/types/scanner.d.ts +240 -0
- package/dist/types/to-html.d.ts +104 -0
- package/dist/types/to-md.d.ts +23 -0
- package/dist/types/to-vnode.d.ts +161 -0
- package/dist/types/utils.d.ts +63 -0
- package/docs/LOADER.md +92 -0
- package/docs/MD-FORMAT.md +502 -0
- package/docs/PLUGINS.md +277 -0
- package/package.json +80 -0
- package/schemas/jaren-md-ast.schema.json +296 -0
- package/src/ast.js +346 -0
- package/src/bake.js +104 -0
- package/src/compiler.js +167 -0
- package/src/component/index.js +191 -0
- package/src/directives.js +371 -0
- package/src/entities.js +107 -0
- package/src/footnotes.js +180 -0
- package/src/frontmatter.js +947 -0
- package/src/html.js +281 -0
- package/src/index.js +76 -0
- package/src/loader.js +0 -0
- package/src/mdx.js +219 -0
- package/src/parser.js +1685 -0
- package/src/plugins/highlight.js +325 -0
- package/src/plugins/index.js +75 -0
- package/src/plugins/mermaid.js +14 -0
- package/src/scanner.js +832 -0
- package/src/to-html.js +425 -0
- package/src/to-md.js +396 -0
- package/src/to-vnode.js +766 -0
- package/src/utils.js +107 -0
- package/styles/md.css +238 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Frontmatter extraction: YAML subset, JSON and TOML → plain JSON.
|
|
4
|
+
*
|
|
5
|
+
* Frontmatter is detected at the very top of the source only:
|
|
6
|
+
*
|
|
7
|
+
* - `---` opens a YAML-subset block, closed by `---` or `...`
|
|
8
|
+
* - `---json` opens a JSON block, closed by `---`
|
|
9
|
+
* - `{` (as the first character) opens a JSON object closed by a
|
|
10
|
+
* line that is exactly `}`
|
|
11
|
+
* - `+++` opens a TOML block, closed by `+++`
|
|
12
|
+
*
|
|
13
|
+
* Whatever the syntax, the result normalizes to one plain JSON value on
|
|
14
|
+
* the document. The parsers are written from scratch and dependency-free;
|
|
15
|
+
* an external TOML parser (e.g. `parseToml` from `@jarenjs/josl`) can be
|
|
16
|
+
* injected through `options.toml` to replace the built-in TOML subset.
|
|
17
|
+
*
|
|
18
|
+
* The YAML subset (normative limits in docs/MD-FORMAT.md §3):
|
|
19
|
+
* scalars (null/booleans/numbers/strings), single- and double-quoted
|
|
20
|
+
* strings, block maps and sequences by indentation, flow arrays and
|
|
21
|
+
* maps (multi-line while brackets are open), literal `|` and folded `>`
|
|
22
|
+
* block scalars with `-` chomping, and `#` comments. No anchors, no
|
|
23
|
+
* aliases, no tags, no multi-document streams, no complex keys.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { setObjectMember } from '@jarenjs/core/object';
|
|
27
|
+
import { countIndent, isBlankLine } from './utils.js';
|
|
28
|
+
|
|
29
|
+
/** Raised for malformed frontmatter inside a detected fence. */
|
|
30
|
+
export class MdFrontmatterError extends Error {
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} message
|
|
33
|
+
* @param {number} line 0-based line index inside the frontmatter block
|
|
34
|
+
*/
|
|
35
|
+
constructor(message, line) {
|
|
36
|
+
super(`md frontmatter: ${message} (line ${line + 1})`);
|
|
37
|
+
this.name = 'MdFrontmatterError';
|
|
38
|
+
this.line = line;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RE_NUMBER = /^[+-]?(?:\d+|\d*\.\d+|\d+\.\d*)(?:[eE][+-]?\d+)?$/;
|
|
43
|
+
const RE_TOML_NUMBER = /^[+-]?(?:0x[0-9a-fA-F_]+|0o[0-7_]+|0b[01_]+|(?:\d[\d_]*)(?:\.[\d_]+)?(?:[eE][+-]?[\d_]+)?)$/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Split frontmatter off the top of a Markdown source.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} source
|
|
49
|
+
* @param {{ toml?: (text: string) => any }} [options]
|
|
50
|
+
* @returns {{ data: any, body: string, lang: 'yaml'|'json'|'toml'|null }}
|
|
51
|
+
*/
|
|
52
|
+
export function parseFrontmatter(source, options = undefined) {
|
|
53
|
+
if (source.length === 0) return { data: null, body: source, lang: null };
|
|
54
|
+
const c0 = source.charCodeAt(0);
|
|
55
|
+
if (c0 === 0x2D /* - */) {
|
|
56
|
+
if (startsWithLine(source, '---json')) {
|
|
57
|
+
const block = sliceFenced(source, '---json'.length, '---');
|
|
58
|
+
if (block !== null) {
|
|
59
|
+
return { data: parseJsonBlock(block.text), body: block.body, lang: 'json' };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (startsWithLine(source, '---')) {
|
|
63
|
+
const block = sliceFenced(source, 3, '---', '...');
|
|
64
|
+
if (block !== null) {
|
|
65
|
+
return { data: parseYamlSubset(block.text), body: block.body, lang: 'yaml' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (c0 === 0x2B /* + */ && startsWithLine(source, '+++')) {
|
|
70
|
+
const block = sliceFenced(source, 3, '+++');
|
|
71
|
+
if (block !== null) {
|
|
72
|
+
const toml = options !== undefined && typeof options.toml === 'function'
|
|
73
|
+
? options.toml
|
|
74
|
+
: parseTomlSubset;
|
|
75
|
+
return { data: toml(block.text), body: block.body, lang: 'toml' };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (c0 === 0x7B /* { */) {
|
|
79
|
+
const block = sliceJsonObject(source);
|
|
80
|
+
if (block !== null) {
|
|
81
|
+
return { data: block.data, body: block.body, lang: 'json' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { data: null, body: source, lang: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Does the source start with `marker` as a complete first line?
|
|
89
|
+
* @param {string} source
|
|
90
|
+
* @param {string} marker
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
function startsWithLine(source, marker) {
|
|
94
|
+
if (!source.startsWith(marker)) return false;
|
|
95
|
+
const next = source.charCodeAt(marker.length);
|
|
96
|
+
return Number.isNaN(next) || next === 0x0A || next === 0x0D;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Slice the text between an opening marker (already matched at position
|
|
101
|
+
* 0, `openLength` chars) and the first closing marker line. Returns
|
|
102
|
+
* `null` when no closing line exists — the document has no frontmatter.
|
|
103
|
+
* @param {string} source
|
|
104
|
+
* @param {number} openLength
|
|
105
|
+
* @param {...string} closers
|
|
106
|
+
* @returns {{ text: string, body: string } | null}
|
|
107
|
+
*/
|
|
108
|
+
function sliceFenced(source, openLength, ...closers) {
|
|
109
|
+
let pos = source.indexOf('\n', openLength);
|
|
110
|
+
if (pos === -1) return null;
|
|
111
|
+
const start = pos + 1;
|
|
112
|
+
while (pos !== -1) {
|
|
113
|
+
const lineStart = pos + 1;
|
|
114
|
+
let lineEnd = source.indexOf('\n', lineStart);
|
|
115
|
+
const hardEnd = lineEnd === -1 ? source.length : lineEnd;
|
|
116
|
+
const line = source.slice(lineStart, hardEnd).replace(/[ \t\r]+$/, '');
|
|
117
|
+
if (closers.includes(line)) {
|
|
118
|
+
return {
|
|
119
|
+
text: source.slice(start, lineStart),
|
|
120
|
+
body: lineEnd === -1 ? '' : source.slice(lineEnd + 1),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
pos = lineEnd;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse the JSON frontmatter form that starts at `{` on line one and
|
|
130
|
+
* closes at the first line that is exactly `}`. Returns `null` when the
|
|
131
|
+
* shape does not hold (the `{` was just paragraph text).
|
|
132
|
+
* @param {string} source
|
|
133
|
+
* @returns {{ data: any, body: string } | null}
|
|
134
|
+
*/
|
|
135
|
+
function sliceJsonObject(source) {
|
|
136
|
+
let pos = 0;
|
|
137
|
+
while (pos < source.length) {
|
|
138
|
+
let lineEnd = source.indexOf('\n', pos);
|
|
139
|
+
const hardEnd = lineEnd === -1 ? source.length : lineEnd;
|
|
140
|
+
const line = source.slice(pos, hardEnd).replace(/[ \t\r]+$/, '');
|
|
141
|
+
if (line === '}') {
|
|
142
|
+
try {
|
|
143
|
+
return {
|
|
144
|
+
data: JSON.parse(source.slice(0, hardEnd)),
|
|
145
|
+
body: lineEnd === -1 ? '' : source.slice(lineEnd + 1),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (lineEnd === -1) break;
|
|
153
|
+
pos = lineEnd + 1;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse a `---json` block (JSON.parse with a located error).
|
|
160
|
+
* @param {string} text
|
|
161
|
+
* @returns {any}
|
|
162
|
+
*/
|
|
163
|
+
function parseJsonBlock(text) {
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(text);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
throw new MdFrontmatterError(
|
|
169
|
+
`invalid JSON: ${/** @type {Error} */ (err).message}`, 0);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ------------------------------------------------------------------
|
|
174
|
+
// YAML subset
|
|
175
|
+
// ------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Parse the YAML subset into plain JSON.
|
|
179
|
+
* @param {string} text
|
|
180
|
+
* @returns {any}
|
|
181
|
+
*/
|
|
182
|
+
export function parseYamlSubset(text) {
|
|
183
|
+
const lines = text.split('\n');
|
|
184
|
+
for (let i = 0; i < lines.length; i++) {
|
|
185
|
+
const line = lines[i];
|
|
186
|
+
if (line.endsWith('\r')) lines[i] = line.slice(0, -1);
|
|
187
|
+
}
|
|
188
|
+
const state = { lines, pos: 0 };
|
|
189
|
+
skipYamlVoid(state);
|
|
190
|
+
if (state.pos >= lines.length) return {};
|
|
191
|
+
const value = parseYamlNode(state, countIndent(lines[state.pos]));
|
|
192
|
+
skipYamlVoid(state);
|
|
193
|
+
if (state.pos < lines.length) {
|
|
194
|
+
throw new MdFrontmatterError('trailing content after the root value', state.pos);
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @typedef {{ lines: string[], pos: number }} YamlState
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Advance past blank and comment-only lines.
|
|
205
|
+
* @param {YamlState} state
|
|
206
|
+
*/
|
|
207
|
+
function skipYamlVoid(state) {
|
|
208
|
+
while (state.pos < state.lines.length) {
|
|
209
|
+
const line = state.lines[state.pos];
|
|
210
|
+
if (!isBlankLine(line) && line.charCodeAt(countIndent(line)) !== 0x23 /* # */) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
state.pos++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Parse the block node whose first line sits at `indent`.
|
|
219
|
+
* @param {YamlState} state
|
|
220
|
+
* @param {number} indent
|
|
221
|
+
* @returns {any}
|
|
222
|
+
*/
|
|
223
|
+
function parseYamlNode(state, indent) {
|
|
224
|
+
const line = state.lines[state.pos];
|
|
225
|
+
const content = line.slice(indent);
|
|
226
|
+
return isSeqDash(content)
|
|
227
|
+
? parseYamlSeq(state, indent)
|
|
228
|
+
: parseYamlMap(state, indent);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Is this trimmed-left content a sequence entry (`- item` or a lone `-`)?
|
|
233
|
+
* @param {string} content
|
|
234
|
+
* @returns {boolean}
|
|
235
|
+
*/
|
|
236
|
+
function isSeqDash(content) {
|
|
237
|
+
return content.charCodeAt(0) === 0x2D
|
|
238
|
+
&& (content.length === 1 || content.charCodeAt(1) === 0x20);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Parse a block map at `indent`.
|
|
243
|
+
* @param {YamlState} state
|
|
244
|
+
* @param {number} indent
|
|
245
|
+
* @returns {Record<string, any>}
|
|
246
|
+
*/
|
|
247
|
+
function parseYamlMap(state, indent) {
|
|
248
|
+
/** @type {Record<string, any>} */
|
|
249
|
+
const out = {};
|
|
250
|
+
while (state.pos < state.lines.length) {
|
|
251
|
+
skipYamlVoid(state);
|
|
252
|
+
if (state.pos >= state.lines.length) break;
|
|
253
|
+
const line = state.lines[state.pos];
|
|
254
|
+
const li = countIndent(line);
|
|
255
|
+
if (li < indent) break;
|
|
256
|
+
if (li > indent) {
|
|
257
|
+
throw new MdFrontmatterError('unexpected indentation', state.pos);
|
|
258
|
+
}
|
|
259
|
+
if (isSeqDash(line.slice(indent))) break;
|
|
260
|
+
const entry = splitYamlKey(line, indent, state.pos);
|
|
261
|
+
state.pos++;
|
|
262
|
+
setObjectMember(out, entry.key, parseYamlValue(state, entry.rest, indent));
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Parse a block sequence at `indent`.
|
|
269
|
+
* @param {YamlState} state
|
|
270
|
+
* @param {number} indent
|
|
271
|
+
* @returns {any[]}
|
|
272
|
+
*/
|
|
273
|
+
function parseYamlSeq(state, indent) {
|
|
274
|
+
/** @type {any[]} */
|
|
275
|
+
const out = [];
|
|
276
|
+
while (state.pos < state.lines.length) {
|
|
277
|
+
skipYamlVoid(state);
|
|
278
|
+
if (state.pos >= state.lines.length) break;
|
|
279
|
+
const line = state.lines[state.pos];
|
|
280
|
+
const li = countIndent(line);
|
|
281
|
+
if (li < indent) break;
|
|
282
|
+
const content = line.slice(indent);
|
|
283
|
+
if (li > indent || !isSeqDash(content)) break;
|
|
284
|
+
let restColumn = indent + 1;
|
|
285
|
+
while (restColumn < line.length && line.charCodeAt(restColumn) === 0x20) restColumn++;
|
|
286
|
+
const rest = line.slice(restColumn);
|
|
287
|
+
if (rest === '' || rest.charCodeAt(0) === 0x23 /* # */) {
|
|
288
|
+
// `-` alone: the item is the following deeper block (or null).
|
|
289
|
+
state.pos++;
|
|
290
|
+
out.push(parseYamlNested(state, indent, null));
|
|
291
|
+
}
|
|
292
|
+
else if (findKeyColon(rest) !== -1) {
|
|
293
|
+
// `- key: value`: an inline map item; re-enter the map parser at
|
|
294
|
+
// the rest's column by blanking the dash out of the current line.
|
|
295
|
+
state.lines[state.pos] = ' '.repeat(restColumn) + rest;
|
|
296
|
+
out.push(parseYamlMap(state, restColumn));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
state.pos++;
|
|
300
|
+
out.push(parseYamlFlowOrScalar(state, rest, state.pos - 1));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Parse the value of a map entry: inline scalar/flow, block scalar, or
|
|
308
|
+
* a nested block on the following lines.
|
|
309
|
+
* @param {YamlState} state
|
|
310
|
+
* @param {string} rest text after `key:` (left-trimmed)
|
|
311
|
+
* @param {number} indent the map's indent
|
|
312
|
+
* @returns {any}
|
|
313
|
+
*/
|
|
314
|
+
function parseYamlValue(state, rest, indent) {
|
|
315
|
+
if (rest === '' || rest.charCodeAt(0) === 0x23 /* # */) {
|
|
316
|
+
return parseYamlNested(state, indent, null);
|
|
317
|
+
}
|
|
318
|
+
const c0 = rest.charCodeAt(0);
|
|
319
|
+
if (c0 === 0x7C /* | */ || c0 === 0x3E /* > */) {
|
|
320
|
+
const header = rest.split('#')[0].trim();
|
|
321
|
+
if (header === '|' || header === '|-' || header === '>' || header === '>-') {
|
|
322
|
+
return parseYamlBlockScalar(state, indent, header);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return parseYamlFlowOrScalar(state, rest, state.pos - 1);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Parse the nested block value after a key (or lone dash) at `indent`:
|
|
330
|
+
* a deeper block node, a sequence at the same indent, or `fallback`.
|
|
331
|
+
* @param {YamlState} state
|
|
332
|
+
* @param {number} indent
|
|
333
|
+
* @param {any} fallback
|
|
334
|
+
* @returns {any}
|
|
335
|
+
*/
|
|
336
|
+
function parseYamlNested(state, indent, fallback) {
|
|
337
|
+
const mark = state.pos;
|
|
338
|
+
skipYamlVoid(state);
|
|
339
|
+
if (state.pos < state.lines.length) {
|
|
340
|
+
const line = state.lines[state.pos];
|
|
341
|
+
const li = countIndent(line);
|
|
342
|
+
if (li > indent) return parseYamlNode(state, li);
|
|
343
|
+
if (li === indent && isSeqDash(line.slice(li))) return parseYamlSeq(state, li);
|
|
344
|
+
}
|
|
345
|
+
state.pos = mark;
|
|
346
|
+
return fallback;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Parse a `|`/`>` block scalar. The chomp `-` drops the final newline.
|
|
351
|
+
* @param {YamlState} state
|
|
352
|
+
* @param {number} indent indent of the owning key
|
|
353
|
+
* @param {string} header `|`, `|-`, `>` or `>-`
|
|
354
|
+
* @returns {string}
|
|
355
|
+
*/
|
|
356
|
+
function parseYamlBlockScalar(state, indent, header) {
|
|
357
|
+
/** @type {string[]} */
|
|
358
|
+
const raw = [];
|
|
359
|
+
let blockIndent = -1;
|
|
360
|
+
while (state.pos < state.lines.length) {
|
|
361
|
+
const line = state.lines[state.pos];
|
|
362
|
+
if (isBlankLine(line)) {
|
|
363
|
+
raw.push('');
|
|
364
|
+
state.pos++;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const li = countIndent(line);
|
|
368
|
+
if (li <= indent) break;
|
|
369
|
+
if (blockIndent === -1) blockIndent = li;
|
|
370
|
+
raw.push(line.slice(Math.min(li, blockIndent)));
|
|
371
|
+
state.pos++;
|
|
372
|
+
}
|
|
373
|
+
while (raw.length > 0 && raw[raw.length - 1] === '') raw.pop();
|
|
374
|
+
let text;
|
|
375
|
+
if (header.charCodeAt(0) === 0x7C /* | */) {
|
|
376
|
+
text = raw.join('\n');
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
text = '';
|
|
380
|
+
for (let i = 0; i < raw.length; i++) {
|
|
381
|
+
if (i === 0) text = raw[0];
|
|
382
|
+
else if (raw[i] === '' || raw[i - 1] === '') text += '\n' + raw[i];
|
|
383
|
+
else text += ' ' + raw[i];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return header.length === 2 ? text : text + '\n';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Split a map line into its key and the value text after the colon.
|
|
391
|
+
* @param {string} line
|
|
392
|
+
* @param {number} indent
|
|
393
|
+
* @param {number} lineNo
|
|
394
|
+
* @returns {{ key: string, rest: string, restColumn: number }}
|
|
395
|
+
*/
|
|
396
|
+
function splitYamlKey(line, indent, lineNo) {
|
|
397
|
+
const content = line.slice(indent);
|
|
398
|
+
const colon = findKeyColon(content);
|
|
399
|
+
if (colon === -1) {
|
|
400
|
+
throw new MdFrontmatterError(`expected 'key: value', got '${content.trim()}'`, lineNo);
|
|
401
|
+
}
|
|
402
|
+
let key = content.slice(0, colon).trim();
|
|
403
|
+
if (key.length > 1) {
|
|
404
|
+
const q = key.charCodeAt(0);
|
|
405
|
+
if ((q === 0x22 || q === 0x27) && key.charCodeAt(key.length - 1) === q) {
|
|
406
|
+
key = String(parseQuoted(key, 0, /** @type {'"'|"'"} */ (key[0])).value);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
let restColumn = indent + colon + 1;
|
|
410
|
+
while (restColumn < line.length && line.charCodeAt(restColumn) === 0x20) restColumn++;
|
|
411
|
+
return { key, rest: line.slice(restColumn), restColumn };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Find the `:` that separates a key from its value: followed by a space
|
|
416
|
+
* or end of line, outside quotes and flow brackets. Returns -1 when the
|
|
417
|
+
* content is not a map entry.
|
|
418
|
+
* @param {string} content
|
|
419
|
+
* @returns {number}
|
|
420
|
+
*/
|
|
421
|
+
function findKeyColon(content) {
|
|
422
|
+
let depth = 0;
|
|
423
|
+
let quote = 0;
|
|
424
|
+
for (let i = 0; i < content.length; i++) {
|
|
425
|
+
const c = content.charCodeAt(i);
|
|
426
|
+
if (quote !== 0) {
|
|
427
|
+
if (c === quote && !(quote === 0x22 && content.charCodeAt(i - 1) === 0x5C)) quote = 0;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (c === 0x22 || c === 0x27) quote = c;
|
|
431
|
+
else if (c === 0x5B || c === 0x7B) depth++;
|
|
432
|
+
else if (c === 0x5D || c === 0x7D) depth--;
|
|
433
|
+
else if (c === 0x3A && depth === 0) {
|
|
434
|
+
const next = content.charCodeAt(i + 1);
|
|
435
|
+
if (Number.isNaN(next) || next === 0x20 || next === 0x09) {
|
|
436
|
+
return i === 0 ? -1 : i;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return -1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Parse an inline value: flow collection (joining following lines while
|
|
445
|
+
* brackets stay open) or scalar.
|
|
446
|
+
* @param {YamlState} state
|
|
447
|
+
* @param {string} rest
|
|
448
|
+
* @param {number} lineNo
|
|
449
|
+
* @returns {any}
|
|
450
|
+
*/
|
|
451
|
+
function parseYamlFlowOrScalar(state, rest, lineNo) {
|
|
452
|
+
const c0 = rest.charCodeAt(0);
|
|
453
|
+
if (c0 === 0x5B /* [ */ || c0 === 0x7B /* { */) {
|
|
454
|
+
let text = rest;
|
|
455
|
+
while (flowDepth(text) > 0 && state.pos < state.lines.length) {
|
|
456
|
+
text += ' ' + state.lines[state.pos].trim();
|
|
457
|
+
state.pos++;
|
|
458
|
+
}
|
|
459
|
+
if (flowDepth(text) !== 0) {
|
|
460
|
+
throw new MdFrontmatterError('unterminated flow collection', lineNo);
|
|
461
|
+
}
|
|
462
|
+
const flow = parseFlowValue(text, 0, lineNo);
|
|
463
|
+
return flow.value;
|
|
464
|
+
}
|
|
465
|
+
return parseYamlScalar(stripComment(rest));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Net bracket depth of a line, ignoring brackets inside quotes.
|
|
470
|
+
* @param {string} text
|
|
471
|
+
* @returns {number}
|
|
472
|
+
*/
|
|
473
|
+
function flowDepth(text) {
|
|
474
|
+
let depth = 0;
|
|
475
|
+
let quote = 0;
|
|
476
|
+
for (let i = 0; i < text.length; i++) {
|
|
477
|
+
const c = text.charCodeAt(i);
|
|
478
|
+
if (quote !== 0) {
|
|
479
|
+
if (c === quote && !(quote === 0x22 && text.charCodeAt(i - 1) === 0x5C)) quote = 0;
|
|
480
|
+
}
|
|
481
|
+
else if (c === 0x22 || c === 0x27) quote = c;
|
|
482
|
+
else if (c === 0x5B || c === 0x7B) depth++;
|
|
483
|
+
else if (c === 0x5D || c === 0x7D) depth--;
|
|
484
|
+
}
|
|
485
|
+
return depth;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Strip a ` #comment` tail from a plain scalar (quote-aware).
|
|
490
|
+
* @param {string} text
|
|
491
|
+
* @returns {string}
|
|
492
|
+
*/
|
|
493
|
+
function stripComment(text) {
|
|
494
|
+
let quote = 0;
|
|
495
|
+
for (let i = 0; i < text.length; i++) {
|
|
496
|
+
const c = text.charCodeAt(i);
|
|
497
|
+
if (quote !== 0) {
|
|
498
|
+
if (c === quote && !(quote === 0x22 && text.charCodeAt(i - 1) === 0x5C)) quote = 0;
|
|
499
|
+
}
|
|
500
|
+
else if (c === 0x22 || c === 0x27) quote = c;
|
|
501
|
+
else if (c === 0x23 && i > 0) {
|
|
502
|
+
const prev = text.charCodeAt(i - 1);
|
|
503
|
+
if (prev === 0x20 || prev === 0x09) return text.slice(0, i).trimEnd();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return text.trimEnd();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Parse a scalar: quoted string, null, boolean, number, or plain string.
|
|
511
|
+
* @param {string} text trimmed scalar text
|
|
512
|
+
* @returns {any}
|
|
513
|
+
*/
|
|
514
|
+
function parseYamlScalar(text) {
|
|
515
|
+
if (text === '') return null;
|
|
516
|
+
const c0 = text.charCodeAt(0);
|
|
517
|
+
if (c0 === 0x22 || c0 === 0x27) {
|
|
518
|
+
return parseQuoted(text, 0, /** @type {'"'|"'"} */ (text[0])).value;
|
|
519
|
+
}
|
|
520
|
+
switch (text) {
|
|
521
|
+
case 'null': case 'Null': case 'NULL': case '~': return null;
|
|
522
|
+
case 'true': case 'True': case 'TRUE': return true;
|
|
523
|
+
case 'false': case 'False': case 'FALSE': return false;
|
|
524
|
+
default: break;
|
|
525
|
+
}
|
|
526
|
+
if (RE_NUMBER.test(text)) return Number(text);
|
|
527
|
+
return text;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Parse a quoted string starting at `pos`. Double quotes take JSON-style
|
|
532
|
+
* escapes; single quotes escape only `''` → `'`.
|
|
533
|
+
* @param {string} text
|
|
534
|
+
* @param {number} pos
|
|
535
|
+
* @param {'"'|"'"} quote
|
|
536
|
+
* @returns {{ value: string, end: number }}
|
|
537
|
+
*/
|
|
538
|
+
function parseQuoted(text, pos, quote) {
|
|
539
|
+
let out = '';
|
|
540
|
+
let i = pos + 1;
|
|
541
|
+
while (i < text.length) {
|
|
542
|
+
const ch = text[i];
|
|
543
|
+
if (ch === quote) {
|
|
544
|
+
if (quote === "'" && text[i + 1] === "'") {
|
|
545
|
+
out += "'";
|
|
546
|
+
i += 2;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
return { value: out, end: i + 1 };
|
|
550
|
+
}
|
|
551
|
+
if (quote === '"' && ch === '\\') {
|
|
552
|
+
const esc = text[i + 1];
|
|
553
|
+
switch (esc) {
|
|
554
|
+
case 'n': out += '\n'; break;
|
|
555
|
+
case 't': out += '\t'; break;
|
|
556
|
+
case 'r': out += '\r'; break;
|
|
557
|
+
case 'b': out += '\b'; break;
|
|
558
|
+
case 'f': out += '\f'; break;
|
|
559
|
+
case '0': out += '\0'; break;
|
|
560
|
+
case 'u':
|
|
561
|
+
out += String.fromCharCode(parseInt(text.slice(i + 2, i + 6), 16) || 0);
|
|
562
|
+
i += 4;
|
|
563
|
+
break;
|
|
564
|
+
default: out += esc ?? '';
|
|
565
|
+
}
|
|
566
|
+
i += 2;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
out += ch;
|
|
570
|
+
i++;
|
|
571
|
+
}
|
|
572
|
+
return { value: out, end: i };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Parse a flow value (`[...]`, `{...}`, quoted or plain scalar) inside a
|
|
577
|
+
* single-line flow text.
|
|
578
|
+
* @param {string} text
|
|
579
|
+
* @param {number} pos
|
|
580
|
+
* @param {number} lineNo
|
|
581
|
+
* @returns {{ value: any, end: number }}
|
|
582
|
+
*/
|
|
583
|
+
function parseFlowValue(text, pos, lineNo) {
|
|
584
|
+
while (pos < text.length && text.charCodeAt(pos) === 0x20) pos++;
|
|
585
|
+
const c = text.charCodeAt(pos);
|
|
586
|
+
if (c === 0x5B /* [ */) return parseFlowSeq(text, pos, lineNo);
|
|
587
|
+
if (c === 0x7B /* { */) return parseFlowMap(text, pos, lineNo);
|
|
588
|
+
if (c === 0x22 || c === 0x27) {
|
|
589
|
+
return parseQuoted(text, pos, /** @type {'"'|"'"} */ (text[pos]));
|
|
590
|
+
}
|
|
591
|
+
let end = pos;
|
|
592
|
+
let depth = 0;
|
|
593
|
+
while (end < text.length) {
|
|
594
|
+
const cc = text.charCodeAt(end);
|
|
595
|
+
if (depth === 0 && (cc === 0x2C || cc === 0x5D || cc === 0x7D || cc === 0x3A)) break;
|
|
596
|
+
if (cc === 0x5B || cc === 0x7B) depth++;
|
|
597
|
+
else if (cc === 0x5D || cc === 0x7D) depth--;
|
|
598
|
+
end++;
|
|
599
|
+
}
|
|
600
|
+
return { value: parseYamlScalar(text.slice(pos, end).trim()), end };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Parse a bracketed flow array `[a, b, ...]`; shared by the YAML flow
|
|
605
|
+
* sequence and the TOML flow array, which differ only in start offset,
|
|
606
|
+
* item parser and error message.
|
|
607
|
+
* @param {string} text
|
|
608
|
+
* @param {number} pos index of `[`
|
|
609
|
+
* @param {number} lineNo
|
|
610
|
+
* @param {(text: string, pos: number, lineNo: number) => { value: any, end: number }} parseItem
|
|
611
|
+
* @param {string} unterminated - error message for a missing `]`
|
|
612
|
+
* @returns {{ value: any[], end: number }}
|
|
613
|
+
*/
|
|
614
|
+
function parseFlowArray(text, pos, lineNo, parseItem, unterminated) {
|
|
615
|
+
/** @type {any[]} */
|
|
616
|
+
const out = [];
|
|
617
|
+
let i = pos + 1;
|
|
618
|
+
for (;;) {
|
|
619
|
+
while (i < text.length && (text.charCodeAt(i) === 0x20 || text.charCodeAt(i) === 0x2C)) i++;
|
|
620
|
+
if (i >= text.length) throw new MdFrontmatterError(unterminated, lineNo);
|
|
621
|
+
if (text.charCodeAt(i) === 0x5D /* ] */) return { value: out, end: i + 1 };
|
|
622
|
+
const item = parseItem(text, i, lineNo);
|
|
623
|
+
out.push(item.value);
|
|
624
|
+
i = item.end;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Parse a flow sequence `[a, b, ...]`.
|
|
630
|
+
* @param {string} text
|
|
631
|
+
* @param {number} pos index of `[`
|
|
632
|
+
* @param {number} lineNo
|
|
633
|
+
* @returns {{ value: any[], end: number }}
|
|
634
|
+
*/
|
|
635
|
+
function parseFlowSeq(text, pos, lineNo) {
|
|
636
|
+
return parseFlowArray(text, pos, lineNo, parseFlowValue, 'unterminated flow sequence');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Parse a flow map `{a: 1, b: 2}`.
|
|
641
|
+
* @param {string} text
|
|
642
|
+
* @param {number} pos index of `{`
|
|
643
|
+
* @param {number} lineNo
|
|
644
|
+
* @returns {{ value: Record<string, any>, end: number }}
|
|
645
|
+
*/
|
|
646
|
+
function parseFlowMap(text, pos, lineNo) {
|
|
647
|
+
/** @type {Record<string, any>} */
|
|
648
|
+
const out = {};
|
|
649
|
+
let i = pos + 1;
|
|
650
|
+
for (;;) {
|
|
651
|
+
while (i < text.length && (text.charCodeAt(i) === 0x20 || text.charCodeAt(i) === 0x2C)) i++;
|
|
652
|
+
if (i >= text.length) throw new MdFrontmatterError('unterminated flow map', lineNo);
|
|
653
|
+
if (text.charCodeAt(i) === 0x7D /* } */) return { value: out, end: i + 1 };
|
|
654
|
+
const key = parseFlowValue(text, i, lineNo);
|
|
655
|
+
i = key.end;
|
|
656
|
+
while (i < text.length && text.charCodeAt(i) === 0x20) i++;
|
|
657
|
+
if (text.charCodeAt(i) !== 0x3A /* : */) {
|
|
658
|
+
throw new MdFrontmatterError("expected ':' in flow map", lineNo);
|
|
659
|
+
}
|
|
660
|
+
const value = parseFlowValue(text, i + 1, lineNo);
|
|
661
|
+
setObjectMember(out, String(key.value), value.value);
|
|
662
|
+
i = value.end;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ------------------------------------------------------------------
|
|
667
|
+
// TOML subset (the built-in fallback; inject @jarenjs/josl's parseToml
|
|
668
|
+
// through options.toml for the full language)
|
|
669
|
+
// ------------------------------------------------------------------
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Parse the built-in TOML subset: `[table]` and `[[array-of-tables]]`
|
|
673
|
+
* headers with dotted paths, bare/quoted/dotted keys, basic and literal
|
|
674
|
+
* strings, integers (decimal/hex/octal/binary, `_` separators), floats,
|
|
675
|
+
* booleans, single- or multi-line flow arrays, inline tables, and `#`
|
|
676
|
+
* comments. Datetimes are kept as strings; multi-line strings are not
|
|
677
|
+
* supported (normative limits in docs/MD-FORMAT.md §3.3).
|
|
678
|
+
* @param {string} text
|
|
679
|
+
* @returns {Record<string, any>}
|
|
680
|
+
*/
|
|
681
|
+
export function parseTomlSubset(text) {
|
|
682
|
+
/** @type {Record<string, any>} */
|
|
683
|
+
const root = {};
|
|
684
|
+
let table = root;
|
|
685
|
+
const lines = text.split('\n');
|
|
686
|
+
for (let no = 0; no < lines.length; no++) {
|
|
687
|
+
let line = lines[no];
|
|
688
|
+
if (line.endsWith('\r')) line = line.slice(0, -1);
|
|
689
|
+
line = line.trim();
|
|
690
|
+
if (line === '' || line.charCodeAt(0) === 0x23 /* # */) continue;
|
|
691
|
+
if (line.charCodeAt(0) === 0x5B /* [ */) {
|
|
692
|
+
const isArray = line.charCodeAt(1) === 0x5B;
|
|
693
|
+
const close = line.indexOf(isArray ? ']]' : ']');
|
|
694
|
+
if (close === -1) throw new MdFrontmatterError('unterminated table header', no);
|
|
695
|
+
const path = parseTomlKeyPath(line.slice(isArray ? 2 : 1, close), no);
|
|
696
|
+
table = descendTomlTable(root, path, isArray, no);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const eq = findTomlEquals(line);
|
|
700
|
+
if (eq === -1) throw new MdFrontmatterError(`expected 'key = value', got '${line}'`, no);
|
|
701
|
+
const path = parseTomlKeyPath(line.slice(0, eq), no);
|
|
702
|
+
let valueText = line.slice(eq + 1).trim();
|
|
703
|
+
// Multi-line flow arrays / inline tables: join lines while open.
|
|
704
|
+
while (flowDepth(stripTomlComment(valueText)) > 0 && no + 1 < lines.length) {
|
|
705
|
+
valueText += ' ' + lines[++no].trim();
|
|
706
|
+
}
|
|
707
|
+
valueText = stripTomlComment(valueText).trim();
|
|
708
|
+
let target = table;
|
|
709
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
710
|
+
const step = path[i];
|
|
711
|
+
if (!(step in target) || typeof target[step] !== 'object') {
|
|
712
|
+
const next = {};
|
|
713
|
+
setObjectMember(target, step, next);
|
|
714
|
+
target = next;
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
target = target[step];
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
setObjectMember(target, path[path.length - 1], parseTomlValue(valueText, no));
|
|
721
|
+
}
|
|
722
|
+
return root;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Find the `=` of a key/value line, outside quotes.
|
|
727
|
+
* @param {string} line
|
|
728
|
+
* @returns {number}
|
|
729
|
+
*/
|
|
730
|
+
function findTomlEquals(line) {
|
|
731
|
+
let quote = 0;
|
|
732
|
+
for (let i = 0; i < line.length; i++) {
|
|
733
|
+
const c = line.charCodeAt(i);
|
|
734
|
+
if (quote !== 0) {
|
|
735
|
+
if (c === quote) quote = 0;
|
|
736
|
+
}
|
|
737
|
+
else if (c === 0x22 || c === 0x27) quote = c;
|
|
738
|
+
else if (c === 0x3D /* = */) return i;
|
|
739
|
+
}
|
|
740
|
+
return -1;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Strip a ` # comment` tail outside quotes.
|
|
745
|
+
* @param {string} text
|
|
746
|
+
* @returns {string}
|
|
747
|
+
*/
|
|
748
|
+
function stripTomlComment(text) {
|
|
749
|
+
let quote = 0;
|
|
750
|
+
for (let i = 0; i < text.length; i++) {
|
|
751
|
+
const c = text.charCodeAt(i);
|
|
752
|
+
if (quote !== 0) {
|
|
753
|
+
if (c === quote && !(quote === 0x22 && text.charCodeAt(i - 1) === 0x5C)) quote = 0;
|
|
754
|
+
}
|
|
755
|
+
else if (c === 0x22 || c === 0x27) quote = c;
|
|
756
|
+
else if (c === 0x23 /* # */) return text.slice(0, i);
|
|
757
|
+
}
|
|
758
|
+
return text;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Parse a dotted key path (`a.b."c.d"`).
|
|
763
|
+
* @param {string} text
|
|
764
|
+
* @param {number} no
|
|
765
|
+
* @returns {string[]}
|
|
766
|
+
*/
|
|
767
|
+
function parseTomlKeyPath(text, no) {
|
|
768
|
+
/** @type {string[]} */
|
|
769
|
+
const out = [];
|
|
770
|
+
let i = 0;
|
|
771
|
+
while (i < text.length) {
|
|
772
|
+
while (i < text.length && text.charCodeAt(i) === 0x20) i++;
|
|
773
|
+
const c = text.charCodeAt(i);
|
|
774
|
+
if (c === 0x22 || c === 0x27) {
|
|
775
|
+
const q = parseQuoted(text, i, /** @type {'"'|"'"} */ (text[i]));
|
|
776
|
+
out.push(q.value);
|
|
777
|
+
i = q.end;
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
let end = i;
|
|
781
|
+
while (end < text.length) {
|
|
782
|
+
const cc = text.charCodeAt(end);
|
|
783
|
+
if (cc === 0x2E /* . */ || cc === 0x20) break;
|
|
784
|
+
end++;
|
|
785
|
+
}
|
|
786
|
+
if (end === i) throw new MdFrontmatterError('empty key segment', no);
|
|
787
|
+
out.push(text.slice(i, end));
|
|
788
|
+
i = end;
|
|
789
|
+
}
|
|
790
|
+
while (i < text.length && text.charCodeAt(i) === 0x20) i++;
|
|
791
|
+
if (i < text.length) {
|
|
792
|
+
if (text.charCodeAt(i) !== 0x2E) {
|
|
793
|
+
throw new MdFrontmatterError(`unexpected '${text[i]}' in key`, no);
|
|
794
|
+
}
|
|
795
|
+
i++;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (out.length === 0) throw new MdFrontmatterError('empty key', no);
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Walk (creating) the table a `[header]` names; `[[header]]` appends a
|
|
804
|
+
* fresh table to the named array.
|
|
805
|
+
* @param {Record<string, any>} root
|
|
806
|
+
* @param {string[]} path
|
|
807
|
+
* @param {boolean} isArray
|
|
808
|
+
* @param {number} no
|
|
809
|
+
* @returns {Record<string, any>}
|
|
810
|
+
*/
|
|
811
|
+
function descendTomlTable(root, path, isArray, no) {
|
|
812
|
+
let target = root;
|
|
813
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
814
|
+
const step = path[i];
|
|
815
|
+
let next = target[step];
|
|
816
|
+
if (next === undefined) {
|
|
817
|
+
next = {};
|
|
818
|
+
setObjectMember(target, step, next);
|
|
819
|
+
}
|
|
820
|
+
else if (Array.isArray(next)) {
|
|
821
|
+
next = next[next.length - 1];
|
|
822
|
+
}
|
|
823
|
+
if (typeof next !== 'object' || next === null) {
|
|
824
|
+
throw new MdFrontmatterError(`'${step}' is not a table`, no);
|
|
825
|
+
}
|
|
826
|
+
target = next;
|
|
827
|
+
}
|
|
828
|
+
const leaf = path[path.length - 1];
|
|
829
|
+
if (isArray) {
|
|
830
|
+
let arr = target[leaf];
|
|
831
|
+
if (arr === undefined) {
|
|
832
|
+
arr = [];
|
|
833
|
+
setObjectMember(target, leaf, arr);
|
|
834
|
+
}
|
|
835
|
+
if (!Array.isArray(arr)) throw new MdFrontmatterError(`'${leaf}' is not an array of tables`, no);
|
|
836
|
+
const fresh = {};
|
|
837
|
+
arr.push(fresh);
|
|
838
|
+
return fresh;
|
|
839
|
+
}
|
|
840
|
+
let next = target[leaf];
|
|
841
|
+
if (next === undefined) {
|
|
842
|
+
next = {};
|
|
843
|
+
setObjectMember(target, leaf, next);
|
|
844
|
+
}
|
|
845
|
+
else if (Array.isArray(next)) {
|
|
846
|
+
next = next[next.length - 1];
|
|
847
|
+
}
|
|
848
|
+
if (typeof next !== 'object' || next === null) {
|
|
849
|
+
throw new MdFrontmatterError(`'${leaf}' is not a table`, no);
|
|
850
|
+
}
|
|
851
|
+
return next;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Parse a TOML value.
|
|
856
|
+
* @param {string} text
|
|
857
|
+
* @param {number} no
|
|
858
|
+
* @returns {any}
|
|
859
|
+
*/
|
|
860
|
+
function parseTomlValue(text, no) {
|
|
861
|
+
if (text === '') throw new MdFrontmatterError('missing value', no);
|
|
862
|
+
const c0 = text.charCodeAt(0);
|
|
863
|
+
if (c0 === 0x22 || c0 === 0x27) {
|
|
864
|
+
return parseQuoted(text, 0, /** @type {'"'|"'"} */ (text[0])).value;
|
|
865
|
+
}
|
|
866
|
+
if (c0 === 0x5B /* [ */) return parseTomlArray(text, no).value;
|
|
867
|
+
if (c0 === 0x7B /* { */) return parseTomlInline(text, no).value;
|
|
868
|
+
if (text === 'true') return true;
|
|
869
|
+
if (text === 'false') return false;
|
|
870
|
+
if (RE_TOML_NUMBER.test(text)) {
|
|
871
|
+
const plain = text.replace(/_/g, '');
|
|
872
|
+
return Number(plain);
|
|
873
|
+
}
|
|
874
|
+
// Datetimes and anything else the subset does not model: the verbatim
|
|
875
|
+
// string (the injectable @jarenjs/josl parser models them fully).
|
|
876
|
+
return text;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Parse a flow array `[a, b, ...]`.
|
|
881
|
+
* @param {string} text
|
|
882
|
+
* @param {number} no
|
|
883
|
+
* @returns {{ value: any[], end: number }}
|
|
884
|
+
*/
|
|
885
|
+
function parseTomlArray(text, no) {
|
|
886
|
+
return parseFlowArray(text, 0, no, parseTomlItem, 'unterminated array');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Parse an inline table `{a = 1, b = 2}`.
|
|
891
|
+
* @param {string} text
|
|
892
|
+
* @param {number} no
|
|
893
|
+
* @returns {{ value: Record<string, any>, end: number }}
|
|
894
|
+
*/
|
|
895
|
+
function parseTomlInline(text, no) {
|
|
896
|
+
/** @type {Record<string, any>} */
|
|
897
|
+
const out = {};
|
|
898
|
+
let i = 1;
|
|
899
|
+
for (;;) {
|
|
900
|
+
while (i < text.length && (text.charCodeAt(i) === 0x20 || text.charCodeAt(i) === 0x2C)) i++;
|
|
901
|
+
if (i >= text.length) throw new MdFrontmatterError('unterminated inline table', no);
|
|
902
|
+
if (text.charCodeAt(i) === 0x7D /* } */) return { value: out, end: i + 1 };
|
|
903
|
+
let end = i;
|
|
904
|
+
while (end < text.length && text.charCodeAt(end) !== 0x3D) end++;
|
|
905
|
+
if (end >= text.length) throw new MdFrontmatterError("expected '=' in inline table", no);
|
|
906
|
+
const path = parseTomlKeyPath(text.slice(i, end).trim(), no);
|
|
907
|
+
const item = parseTomlItem(text, end + 1, no);
|
|
908
|
+
let target = out;
|
|
909
|
+
for (let p = 0; p < path.length - 1; p++) {
|
|
910
|
+
const next = {};
|
|
911
|
+
setObjectMember(target, path[p], next);
|
|
912
|
+
target = next;
|
|
913
|
+
}
|
|
914
|
+
setObjectMember(target, path[path.length - 1], item.value);
|
|
915
|
+
i = item.end;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Parse one value inside a flow array or inline table.
|
|
921
|
+
* @param {string} text
|
|
922
|
+
* @param {number} pos
|
|
923
|
+
* @param {number} no
|
|
924
|
+
* @returns {{ value: any, end: number }}
|
|
925
|
+
*/
|
|
926
|
+
function parseTomlItem(text, pos, no) {
|
|
927
|
+
while (pos < text.length && text.charCodeAt(pos) === 0x20) pos++;
|
|
928
|
+
const c = text.charCodeAt(pos);
|
|
929
|
+
if (c === 0x5B /* [ */) {
|
|
930
|
+
const inner = parseTomlArray(text.slice(pos), no);
|
|
931
|
+
return { value: inner.value, end: pos + inner.end };
|
|
932
|
+
}
|
|
933
|
+
if (c === 0x7B /* { */) {
|
|
934
|
+
const inner = parseTomlInline(text.slice(pos), no);
|
|
935
|
+
return { value: inner.value, end: pos + inner.end };
|
|
936
|
+
}
|
|
937
|
+
if (c === 0x22 || c === 0x27) {
|
|
938
|
+
return parseQuoted(text, pos, /** @type {'"'|"'"} */ (text[pos]));
|
|
939
|
+
}
|
|
940
|
+
let end = pos;
|
|
941
|
+
while (end < text.length) {
|
|
942
|
+
const cc = text.charCodeAt(end);
|
|
943
|
+
if (cc === 0x2C || cc === 0x5D || cc === 0x7D) break;
|
|
944
|
+
end++;
|
|
945
|
+
}
|
|
946
|
+
return { value: parseTomlValue(text.slice(pos, end).trim(), no), end };
|
|
947
|
+
}
|