@tishlang/tishdoc-parse 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/README.md +55 -0
- package/package.json +36 -0
- package/src/ast.tish +181 -0
- package/src/attrs.tish +92 -0
- package/src/frontmatter.tish +160 -0
- package/src/includes.tish +180 -0
- package/src/inline.tish +380 -0
- package/src/main.tish +287 -0
- package/src/meta_imports.tish +89 -0
- package/src/parse_blocks.tish +434 -0
- package/src/trim.tish +21 -0
- package/src/validate_meta.tish +118 -0
- package/src/yaml_simple.tish +46 -0
package/src/inline.tish
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import {
|
|
2
|
+
textNode,
|
|
3
|
+
emphasisNode,
|
|
4
|
+
italicNode,
|
|
5
|
+
strikethroughNode,
|
|
6
|
+
codeSpanNode,
|
|
7
|
+
linkNode,
|
|
8
|
+
imageNode,
|
|
9
|
+
lineBreakNode
|
|
10
|
+
} from "./ast.tish"
|
|
11
|
+
|
|
12
|
+
// Inline parser. Handles: `code`, **strong**, *em* / _em_, ~~strike~~,
|
|
13
|
+
// [text](url), , <https://autolink>, hard line break (two
|
|
14
|
+
// trailing spaces + \n), and backslash escapes for special characters.
|
|
15
|
+
//
|
|
16
|
+
// Not full CommonMark — designed for typical READMEs. Unknown markup falls
|
|
17
|
+
// through as plain text.
|
|
18
|
+
|
|
19
|
+
fn isAsciiAlpha(c) {
|
|
20
|
+
if (c >= "a" && c <= "z") { return true }
|
|
21
|
+
if (c >= "A" && c <= "Z") { return true }
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn isAsciiAlphaNum(c) {
|
|
26
|
+
if (isAsciiAlpha(c)) { return true }
|
|
27
|
+
if (c >= "0" && c <= "9") { return true }
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Match a backtick-delimited code span: `code` or ``co`de`` (matches longest
|
|
32
|
+
// run of opening backticks). Returns { value, end } or null.
|
|
33
|
+
fn matchCodeSpan(s, start) {
|
|
34
|
+
let n = 0
|
|
35
|
+
while ((start + n) < s.length && s.charAt(start + n) === "`") {
|
|
36
|
+
n = n + 1
|
|
37
|
+
}
|
|
38
|
+
if (n === 0) { return null }
|
|
39
|
+
let i = start + n
|
|
40
|
+
while (i < s.length) {
|
|
41
|
+
if (s.charAt(i) === "`") {
|
|
42
|
+
let m = 0
|
|
43
|
+
while ((i + m) < s.length && s.charAt(i + m) === "`") {
|
|
44
|
+
m = m + 1
|
|
45
|
+
}
|
|
46
|
+
if (m === n) {
|
|
47
|
+
let inner = s.slice(start + n, i)
|
|
48
|
+
let r = {}
|
|
49
|
+
r["value"] = inner
|
|
50
|
+
r["end"] = i + n
|
|
51
|
+
return r
|
|
52
|
+
}
|
|
53
|
+
i = i + m
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
i = i + 1
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Match `[label](href "title")` or ``. `start` points at
|
|
62
|
+
// the leading `[` (caller skipped any `!`). Returns { label, dest, title, end }
|
|
63
|
+
// where `end` is the index AFTER the closing `)`.
|
|
64
|
+
fn matchLinkOrImage(s, start) {
|
|
65
|
+
if (start >= s.length || s.charAt(start) !== "[") { return null }
|
|
66
|
+
let depth = 1
|
|
67
|
+
let i = start + 1
|
|
68
|
+
while (i < s.length) {
|
|
69
|
+
let c = s.charAt(i)
|
|
70
|
+
if (c === "\\" && (i + 1) < s.length) {
|
|
71
|
+
i = i + 2
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (c === "[") { depth = depth + 1 }
|
|
75
|
+
else if (c === "]") {
|
|
76
|
+
depth = depth - 1
|
|
77
|
+
if (depth === 0) { break }
|
|
78
|
+
}
|
|
79
|
+
i = i + 1
|
|
80
|
+
}
|
|
81
|
+
if (i >= s.length) { return null }
|
|
82
|
+
let label = s.slice(start + 1, i)
|
|
83
|
+
let j = i + 1
|
|
84
|
+
if (j >= s.length || s.charAt(j) !== "(") { return null }
|
|
85
|
+
j = j + 1
|
|
86
|
+
// Skip leading whitespace inside (...)
|
|
87
|
+
while (j < s.length && (s.charAt(j) === " " || s.charAt(j) === "\t")) { j = j + 1 }
|
|
88
|
+
let dest = ""
|
|
89
|
+
if (j < s.length && s.charAt(j) === "<") {
|
|
90
|
+
j = j + 1
|
|
91
|
+
let dStart = j
|
|
92
|
+
while (j < s.length && s.charAt(j) !== ">" && s.charAt(j) !== "\n") {
|
|
93
|
+
j = j + 1
|
|
94
|
+
}
|
|
95
|
+
if (j >= s.length || s.charAt(j) !== ">") { return null }
|
|
96
|
+
dest = s.slice(dStart, j)
|
|
97
|
+
j = j + 1
|
|
98
|
+
} else {
|
|
99
|
+
let dStart = j
|
|
100
|
+
let pdepth = 0
|
|
101
|
+
while (j < s.length) {
|
|
102
|
+
let c = s.charAt(j)
|
|
103
|
+
if (c === "\\" && (j + 1) < s.length) { j = j + 2; continue }
|
|
104
|
+
if (c === "(") { pdepth = pdepth + 1 }
|
|
105
|
+
else if (c === ")") {
|
|
106
|
+
if (pdepth === 0) { break }
|
|
107
|
+
pdepth = pdepth - 1
|
|
108
|
+
} else if (c === " " || c === "\t" || c === "\n") {
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
j = j + 1
|
|
112
|
+
}
|
|
113
|
+
dest = s.slice(dStart, j)
|
|
114
|
+
}
|
|
115
|
+
// Optional title: "..." or '...' or (...)
|
|
116
|
+
let title = null
|
|
117
|
+
while (j < s.length && (s.charAt(j) === " " || s.charAt(j) === "\t")) { j = j + 1 }
|
|
118
|
+
if (j < s.length && (s.charAt(j) === "\"" || s.charAt(j) === "'")) {
|
|
119
|
+
let q = s.charAt(j)
|
|
120
|
+
j = j + 1
|
|
121
|
+
let tStart = j
|
|
122
|
+
while (j < s.length && s.charAt(j) !== q) {
|
|
123
|
+
if (s.charAt(j) === "\\" && (j + 1) < s.length) { j = j + 2; continue }
|
|
124
|
+
j = j + 1
|
|
125
|
+
}
|
|
126
|
+
if (j >= s.length) { return null }
|
|
127
|
+
title = s.slice(tStart, j)
|
|
128
|
+
j = j + 1
|
|
129
|
+
while (j < s.length && (s.charAt(j) === " " || s.charAt(j) === "\t")) { j = j + 1 }
|
|
130
|
+
}
|
|
131
|
+
if (j >= s.length || s.charAt(j) !== ")") { return null }
|
|
132
|
+
let r = {}
|
|
133
|
+
r["label"] = label
|
|
134
|
+
r["dest"] = dest
|
|
135
|
+
r["title"] = title
|
|
136
|
+
r["end"] = j + 1
|
|
137
|
+
return r
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Match an autolink `<https://example.com>` or `<mailto:a@b>` returning the
|
|
141
|
+
// URL and the index AFTER the closing `>`. Reject anything with whitespace.
|
|
142
|
+
fn matchAutolink(s, start) {
|
|
143
|
+
if (start >= s.length || s.charAt(start) !== "<") { return null }
|
|
144
|
+
let i = start + 1
|
|
145
|
+
let urlStart = i
|
|
146
|
+
while (i < s.length) {
|
|
147
|
+
let c = s.charAt(i)
|
|
148
|
+
if (c === ">") { break }
|
|
149
|
+
if (c === " " || c === "\t" || c === "\n" || c === "<") { return null }
|
|
150
|
+
i = i + 1
|
|
151
|
+
}
|
|
152
|
+
if (i >= s.length || s.charAt(i) !== ">") { return null }
|
|
153
|
+
let url = s.slice(urlStart, i)
|
|
154
|
+
let isUrl = false
|
|
155
|
+
if (url.length >= 8 && url.slice(0, 8) === "https://") { isUrl = true }
|
|
156
|
+
if (!isUrl && url.length >= 7 && url.slice(0, 7) === "http://") { isUrl = true }
|
|
157
|
+
if (!isUrl && url.length >= 7 && url.slice(0, 7) === "mailto:") { isUrl = true }
|
|
158
|
+
if (!isUrl) { return null }
|
|
159
|
+
let r = {}
|
|
160
|
+
r["url"] = url
|
|
161
|
+
r["end"] = i + 1
|
|
162
|
+
return r
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Match a delimiter run: count `n` consecutive copies of `ch` starting at i.
|
|
166
|
+
// Used for emphasis pairing (** **, * *, ~~ ~~). Returns the matching closing
|
|
167
|
+
// run such that the inner span has at least 1 char and contains no newline
|
|
168
|
+
// breaks for emphasis (we forbid intra-paragraph emphasis crossing a hard
|
|
169
|
+
// break for safety).
|
|
170
|
+
fn matchEmphasis(s, start, ch, want) {
|
|
171
|
+
// Skip the opening run.
|
|
172
|
+
let i = start + want
|
|
173
|
+
if (i >= s.length) { return null }
|
|
174
|
+
// Inner cannot start with whitespace for proper emphasis.
|
|
175
|
+
let firstInner = s.charAt(i)
|
|
176
|
+
if (firstInner === " " || firstInner === "\t" || firstInner === "\n") { return null }
|
|
177
|
+
while (i < s.length) {
|
|
178
|
+
let c = s.charAt(i)
|
|
179
|
+
if (c === "\\" && (i + 1) < s.length) { i = i + 2; continue }
|
|
180
|
+
if (c === "`") {
|
|
181
|
+
// Skip code span so inline code doesn't swallow our delimiter.
|
|
182
|
+
let cs = matchCodeSpan(s, i)
|
|
183
|
+
if (cs !== null) { i = cs["end"]; continue }
|
|
184
|
+
}
|
|
185
|
+
if (c === ch) {
|
|
186
|
+
let n = 0
|
|
187
|
+
while ((i + n) < s.length && s.charAt(i + n) === ch) { n = n + 1 }
|
|
188
|
+
if (n >= want) {
|
|
189
|
+
// The character before the closing run must not be whitespace for
|
|
190
|
+
// valid emphasis; inner must be non-empty.
|
|
191
|
+
let prev = s.charAt(i - 1)
|
|
192
|
+
if (prev !== " " && prev !== "\t" && prev !== "\n" && i > (start + want)) {
|
|
193
|
+
let inner = s.slice(start + want, i)
|
|
194
|
+
let r = {}
|
|
195
|
+
r["inner"] = inner
|
|
196
|
+
r["end"] = i + want
|
|
197
|
+
return r
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
i = i + n
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
i = i + 1
|
|
204
|
+
}
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn flushTextRun(out, run) {
|
|
209
|
+
if (run.length > 0) {
|
|
210
|
+
out.push(textNode(run))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn italicUnderscoreLeftOk(s, pos) {
|
|
215
|
+
if (pos === 0) { return true }
|
|
216
|
+
let prev = s.charAt(pos - 1)
|
|
217
|
+
if (isAsciiAlphaNum(prev)) { return false }
|
|
218
|
+
return true
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn italicUnderscoreRightOk(s, ch, endPos) {
|
|
222
|
+
if (ch !== "_") { return true }
|
|
223
|
+
if (endPos >= s.length) { return true }
|
|
224
|
+
let nx = s.charAt(endPos)
|
|
225
|
+
if (isAsciiAlphaNum(nx)) { return false }
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn isEscapable(nx) {
|
|
230
|
+
if (nx === "\\") { return true }
|
|
231
|
+
if (nx === "`") { return true }
|
|
232
|
+
if (nx === "*") { return true }
|
|
233
|
+
if (nx === "_") { return true }
|
|
234
|
+
if (nx === "{") { return true }
|
|
235
|
+
if (nx === "}") { return true }
|
|
236
|
+
if (nx === "[") { return true }
|
|
237
|
+
if (nx === "]") { return true }
|
|
238
|
+
if (nx === "(") { return true }
|
|
239
|
+
if (nx === ")") { return true }
|
|
240
|
+
if (nx === "#") { return true }
|
|
241
|
+
if (nx === "+") { return true }
|
|
242
|
+
if (nx === "-") { return true }
|
|
243
|
+
if (nx === ".") { return true }
|
|
244
|
+
if (nx === "!") { return true }
|
|
245
|
+
if (nx === "<") { return true }
|
|
246
|
+
if (nx === ">") { return true }
|
|
247
|
+
if (nx === "~") { return true }
|
|
248
|
+
if (nx === "|") { return true }
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export fn parseInlineSegments(line) {
|
|
253
|
+
let out = []
|
|
254
|
+
if (line === null) {
|
|
255
|
+
return out
|
|
256
|
+
}
|
|
257
|
+
let s = typeof line === "string" ? line : String(line)
|
|
258
|
+
let pos = 0
|
|
259
|
+
let run = ""
|
|
260
|
+
while (pos < s.length) {
|
|
261
|
+
let c = s.charAt(pos)
|
|
262
|
+
|
|
263
|
+
// Backslash escape — emit next char literally.
|
|
264
|
+
// Tish JS backend block-scopes nested `let`, so we evaluate the escape
|
|
265
|
+
// condition inline (no intermediate `let esc`) to avoid scope-leak.
|
|
266
|
+
if (c === "\\" && (pos + 1) < s.length && isEscapable(s.charAt(pos + 1))) {
|
|
267
|
+
run = run + s.charAt(pos + 1)
|
|
268
|
+
pos = pos + 2
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Hard line break (two trailing spaces + \n) inside a paragraph.
|
|
273
|
+
if (c === "\n") {
|
|
274
|
+
if (run.length >= 2 && run.slice(run.length - 2) === " ") {
|
|
275
|
+
run = run.slice(0, run.length - 2)
|
|
276
|
+
flushTextRun(out, run)
|
|
277
|
+
run = ""
|
|
278
|
+
out.push(lineBreakNode())
|
|
279
|
+
} else {
|
|
280
|
+
flushTextRun(out, run)
|
|
281
|
+
run = ""
|
|
282
|
+
out.push(textNode("\n"))
|
|
283
|
+
}
|
|
284
|
+
pos = pos + 1
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Inline code.
|
|
289
|
+
if (c === "`") {
|
|
290
|
+
let cs = matchCodeSpan(s, pos)
|
|
291
|
+
if (cs !== null) {
|
|
292
|
+
flushTextRun(out, run)
|
|
293
|
+
run = ""
|
|
294
|
+
out.push(codeSpanNode(cs["value"]))
|
|
295
|
+
pos = cs["end"]
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Image: 
|
|
301
|
+
if (c === "!" && (pos + 1) < s.length && s.charAt(pos + 1) === "[") {
|
|
302
|
+
let m = matchLinkOrImage(s, pos + 1)
|
|
303
|
+
if (m !== null) {
|
|
304
|
+
flushTextRun(out, run)
|
|
305
|
+
run = ""
|
|
306
|
+
out.push(imageNode(m["dest"], m["label"], m["title"]))
|
|
307
|
+
pos = m["end"]
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Link: [text](href)
|
|
313
|
+
if (c === "[") {
|
|
314
|
+
let lm = matchLinkOrImage(s, pos)
|
|
315
|
+
if (lm !== null) {
|
|
316
|
+
flushTextRun(out, run)
|
|
317
|
+
run = ""
|
|
318
|
+
let inner = parseInlineSegments(lm["label"])
|
|
319
|
+
out.push(linkNode(lm["dest"], lm["title"], inner))
|
|
320
|
+
pos = lm["end"]
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Autolink: <https://...>
|
|
326
|
+
if (c === "<") {
|
|
327
|
+
let al = matchAutolink(s, pos)
|
|
328
|
+
if (al !== null) {
|
|
329
|
+
flushTextRun(out, run)
|
|
330
|
+
run = ""
|
|
331
|
+
out.push(linkNode(al["url"], null, [textNode(al["url"])]))
|
|
332
|
+
pos = al["end"]
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Strong: **text**
|
|
338
|
+
if (c === "*" && (pos + 1) < s.length && s.charAt(pos + 1) === "*") {
|
|
339
|
+
let r = matchEmphasis(s, pos, "*", 2)
|
|
340
|
+
if (r !== null) {
|
|
341
|
+
flushTextRun(out, run)
|
|
342
|
+
run = ""
|
|
343
|
+
out.push(emphasisNode(parseInlineSegments(r["inner"])))
|
|
344
|
+
pos = r["end"]
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Italic: *text* or _text_
|
|
350
|
+
// For underscore italics, require non-alphanumeric on both sides
|
|
351
|
+
// (so identifiers like `foo_bar_baz` aren't broken).
|
|
352
|
+
if ((c === "*") || (c === "_" && italicUnderscoreLeftOk(s, pos))) {
|
|
353
|
+
let r2 = matchEmphasis(s, pos, c, 1)
|
|
354
|
+
if (r2 !== null && italicUnderscoreRightOk(s, c, r2["end"])) {
|
|
355
|
+
flushTextRun(out, run)
|
|
356
|
+
run = ""
|
|
357
|
+
out.push(italicNode(parseInlineSegments(r2["inner"])))
|
|
358
|
+
pos = r2["end"]
|
|
359
|
+
continue
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Strikethrough: ~~text~~
|
|
364
|
+
if (c === "~" && (pos + 1) < s.length && s.charAt(pos + 1) === "~") {
|
|
365
|
+
let rs = matchEmphasis(s, pos, "~", 2)
|
|
366
|
+
if (rs !== null) {
|
|
367
|
+
flushTextRun(out, run)
|
|
368
|
+
run = ""
|
|
369
|
+
out.push(strikethroughNode(parseInlineSegments(rs["inner"])))
|
|
370
|
+
pos = rs["end"]
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
run = run + c
|
|
376
|
+
pos = pos + 1
|
|
377
|
+
}
|
|
378
|
+
flushTextRun(out, run)
|
|
379
|
+
return out
|
|
380
|
+
}
|
package/src/main.tish
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { parseSimpleYamlBlock } from "./yaml_simple.tish"
|
|
2
|
+
import { resolveIncludesInBody } from "./includes.tish"
|
|
3
|
+
import { parseMarkdownToBlocks } from "./parse_blocks.tish"
|
|
4
|
+
import { documentNode } from "./ast.tish"
|
|
5
|
+
import { validateMeta } from "./validate_meta.tish"
|
|
6
|
+
import { applyMetaImportsToBody } from "./meta_imports.tish"
|
|
7
|
+
|
|
8
|
+
fn splitJsonFrontmatter(raw) {
|
|
9
|
+
if (!raw.startsWith("{")) {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
let depth = 0
|
|
13
|
+
let i = 0
|
|
14
|
+
let inStr = false
|
|
15
|
+
let strQuote = ""
|
|
16
|
+
let esc = false
|
|
17
|
+
while (i < raw.length) {
|
|
18
|
+
let c = raw.charAt(i)
|
|
19
|
+
if (inStr) {
|
|
20
|
+
if (esc) {
|
|
21
|
+
esc = false
|
|
22
|
+
} else if (c === "\\") {
|
|
23
|
+
esc = true
|
|
24
|
+
} else if (c === strQuote) {
|
|
25
|
+
inStr = false
|
|
26
|
+
}
|
|
27
|
+
i = i + 1
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if (c === "\"" || c === "'") {
|
|
31
|
+
inStr = true
|
|
32
|
+
strQuote = c
|
|
33
|
+
i = i + 1
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (c === "{") {
|
|
37
|
+
depth = depth + 1
|
|
38
|
+
} else if (c === "}") {
|
|
39
|
+
depth = depth - 1
|
|
40
|
+
if (depth === 0) {
|
|
41
|
+
let jsonStr = raw.slice(0, i + 1)
|
|
42
|
+
let rest = raw.slice(i + 1)
|
|
43
|
+
let k = 0
|
|
44
|
+
while (k < rest.length) {
|
|
45
|
+
let ch = rest.charAt(k)
|
|
46
|
+
if (ch !== " " && ch !== "\t" && ch !== "\r" && ch !== "\n") {
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
k = k + 1
|
|
50
|
+
}
|
|
51
|
+
rest = rest.slice(k)
|
|
52
|
+
if (rest.length > 0 && rest.charAt(0) !== "\n") {
|
|
53
|
+
rest = "\n" + rest
|
|
54
|
+
}
|
|
55
|
+
let jr = {}
|
|
56
|
+
jr["jsonStr"] = jsonStr
|
|
57
|
+
jr["rest"] = rest
|
|
58
|
+
return jr
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
i = i + 1
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn splitYamlFence(raw) {
|
|
67
|
+
let diagnostics = []
|
|
68
|
+
if (raw === null) {
|
|
69
|
+
let e0 = {}
|
|
70
|
+
e0["meta"] = {}
|
|
71
|
+
e0["body"] = ""
|
|
72
|
+
e0["metaRaw"] = ""
|
|
73
|
+
e0["diagnostics"] = diagnostics
|
|
74
|
+
e0["language"] = ""
|
|
75
|
+
e0["bodyStartLine1"] = 1
|
|
76
|
+
return e0
|
|
77
|
+
}
|
|
78
|
+
let s = typeof raw === "string" ? raw : String(raw)
|
|
79
|
+
let t = s.trim()
|
|
80
|
+
if (t.startsWith("{")) {
|
|
81
|
+
let rj = splitJsonFrontmatter(s)
|
|
82
|
+
if (rj !== null) {
|
|
83
|
+
let metaJ = {}
|
|
84
|
+
try {
|
|
85
|
+
metaJ = JSON.parse(rj["jsonStr"])
|
|
86
|
+
} catch (e) {
|
|
87
|
+
let de = {}
|
|
88
|
+
de["level"] = "error"
|
|
89
|
+
de["message"] = "Invalid JSON frontmatter"
|
|
90
|
+
diagnostics.push(de)
|
|
91
|
+
let ej = {}
|
|
92
|
+
ej["meta"] = {}
|
|
93
|
+
ej["body"] = s
|
|
94
|
+
ej["metaRaw"] = rj["jsonStr"]
|
|
95
|
+
ej["diagnostics"] = diagnostics
|
|
96
|
+
ej["language"] = "json"
|
|
97
|
+
ej["bodyStartLine1"] = 1
|
|
98
|
+
return ej
|
|
99
|
+
}
|
|
100
|
+
let okj = {}
|
|
101
|
+
okj["meta"] = metaJ
|
|
102
|
+
okj["body"] = rj["rest"]
|
|
103
|
+
okj["metaRaw"] = rj["jsonStr"]
|
|
104
|
+
okj["diagnostics"] = diagnostics
|
|
105
|
+
okj["language"] = "json"
|
|
106
|
+
let jsonLines = rj["jsonStr"].split("\n").length
|
|
107
|
+
okj["bodyStartLine1"] = jsonLines + 1
|
|
108
|
+
return okj
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let lines = s.split("\n")
|
|
113
|
+
if (lines.length === 0) {
|
|
114
|
+
let e1 = {}
|
|
115
|
+
e1["meta"] = {}
|
|
116
|
+
e1["body"] = s
|
|
117
|
+
e1["metaRaw"] = ""
|
|
118
|
+
e1["diagnostics"] = diagnostics
|
|
119
|
+
e1["language"] = ""
|
|
120
|
+
e1["bodyStartLine1"] = 1
|
|
121
|
+
return e1
|
|
122
|
+
}
|
|
123
|
+
if (!lines[0].startsWith("---")) {
|
|
124
|
+
let e2 = {}
|
|
125
|
+
e2["meta"] = {}
|
|
126
|
+
e2["body"] = s
|
|
127
|
+
e2["metaRaw"] = ""
|
|
128
|
+
e2["diagnostics"] = diagnostics
|
|
129
|
+
e2["language"] = ""
|
|
130
|
+
e2["bodyStartLine1"] = 1
|
|
131
|
+
return e2
|
|
132
|
+
}
|
|
133
|
+
let lang = "yaml"
|
|
134
|
+
let rest0 = lines[0].trim().slice(3).trim()
|
|
135
|
+
if (rest0 !== "") {
|
|
136
|
+
lang = rest0
|
|
137
|
+
}
|
|
138
|
+
let i = 1
|
|
139
|
+
let found = false
|
|
140
|
+
let matterLines = []
|
|
141
|
+
while (i < lines.length) {
|
|
142
|
+
let line = lines[i]
|
|
143
|
+
if (line.trim() === "---") {
|
|
144
|
+
found = true
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
matterLines.push(line)
|
|
148
|
+
i = i + 1
|
|
149
|
+
}
|
|
150
|
+
if (!found) {
|
|
151
|
+
let du = {}
|
|
152
|
+
du["level"] = "warn"
|
|
153
|
+
du["message"] = "Unclosed frontmatter fence"
|
|
154
|
+
diagnostics.push(du)
|
|
155
|
+
let e3 = {}
|
|
156
|
+
e3["meta"] = {}
|
|
157
|
+
e3["body"] = s
|
|
158
|
+
e3["metaRaw"] = ""
|
|
159
|
+
e3["diagnostics"] = diagnostics
|
|
160
|
+
e3["language"] = ""
|
|
161
|
+
e3["bodyStartLine1"] = 1
|
|
162
|
+
return e3
|
|
163
|
+
}
|
|
164
|
+
let matterStr = matterLines.join("\n")
|
|
165
|
+
let bodyLines = []
|
|
166
|
+
let j = i + 1
|
|
167
|
+
while (j < lines.length) {
|
|
168
|
+
bodyLines.push(lines[j])
|
|
169
|
+
j = j + 1
|
|
170
|
+
}
|
|
171
|
+
let body = bodyLines.join("\n")
|
|
172
|
+
let meta = {}
|
|
173
|
+
if (lang === "yaml" || lang === "yml") {
|
|
174
|
+
meta = parseSimpleYamlBlock(matterStr)
|
|
175
|
+
} else {
|
|
176
|
+
let dw = {}
|
|
177
|
+
dw["level"] = "warn"
|
|
178
|
+
dw["message"] = "Unsupported frontmatter fence language: " + lang
|
|
179
|
+
diagnostics.push(dw)
|
|
180
|
+
let e4 = {}
|
|
181
|
+
e4["meta"] = {}
|
|
182
|
+
e4["body"] = body
|
|
183
|
+
e4["metaRaw"] = matterStr
|
|
184
|
+
e4["diagnostics"] = diagnostics
|
|
185
|
+
e4["language"] = lang
|
|
186
|
+
e4["bodyStartLine1"] = i + 2
|
|
187
|
+
return e4
|
|
188
|
+
}
|
|
189
|
+
let ok = {}
|
|
190
|
+
ok["meta"] = meta
|
|
191
|
+
ok["body"] = body
|
|
192
|
+
ok["metaRaw"] = matterStr
|
|
193
|
+
ok["diagnostics"] = diagnostics
|
|
194
|
+
ok["language"] = lang
|
|
195
|
+
ok["bodyStartLine1"] = i + 2
|
|
196
|
+
return ok
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Options may omit keys; direct `opts["readPartial"]` throws if the key is absent (Tish).
|
|
200
|
+
fn normalizeParseOptions(options) {
|
|
201
|
+
let o = {}
|
|
202
|
+
o["readPartial"] = null
|
|
203
|
+
o["maxIncludeDepth"] = null
|
|
204
|
+
o["strictMeta"] = false
|
|
205
|
+
if (options === null) {
|
|
206
|
+
return o
|
|
207
|
+
}
|
|
208
|
+
let ks = Object.keys(options)
|
|
209
|
+
let ki = 0
|
|
210
|
+
while (ki < ks.length) {
|
|
211
|
+
let key = ks[ki]
|
|
212
|
+
if (key === "readPartial") {
|
|
213
|
+
o["readPartial"] = options[key]
|
|
214
|
+
}
|
|
215
|
+
if (key === "maxIncludeDepth") {
|
|
216
|
+
o["maxIncludeDepth"] = options[key]
|
|
217
|
+
}
|
|
218
|
+
if (key === "strictMeta") {
|
|
219
|
+
o["strictMeta"] = options[key]
|
|
220
|
+
}
|
|
221
|
+
ki = ki + 1
|
|
222
|
+
}
|
|
223
|
+
return o
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export fn parseDocument(source, options) {
|
|
227
|
+
let allDiag = []
|
|
228
|
+
let opts = normalizeParseOptions(options)
|
|
229
|
+
|
|
230
|
+
let fm = splitYamlFence(source)
|
|
231
|
+
let di = 0
|
|
232
|
+
while (di < fm["diagnostics"].length) {
|
|
233
|
+
allDiag.push(fm["diagnostics"][di])
|
|
234
|
+
di = di + 1
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let metaVm = validateMeta(fm["meta"], opts)
|
|
238
|
+
let mi = 0
|
|
239
|
+
while (mi < metaVm["diagnostics"].length) {
|
|
240
|
+
allDiag.push(metaVm["diagnostics"][mi])
|
|
241
|
+
mi = mi + 1
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let body = fm["body"]
|
|
245
|
+
let imp = applyMetaImportsToBody(fm["meta"], body, opts["readPartial"], allDiag)
|
|
246
|
+
body = imp["body"]
|
|
247
|
+
|
|
248
|
+
let incOpts = {}
|
|
249
|
+
incOpts["readPartial"] = opts["readPartial"]
|
|
250
|
+
incOpts["maxDepth"] = 16
|
|
251
|
+
let mdOpt = opts["maxIncludeDepth"]
|
|
252
|
+
if (mdOpt !== null) {
|
|
253
|
+
incOpts["maxDepth"] = mdOpt
|
|
254
|
+
}
|
|
255
|
+
incOpts["visited"] = []
|
|
256
|
+
incOpts["depth"] = 0
|
|
257
|
+
let inc = resolveIncludesInBody(body, incOpts)
|
|
258
|
+
let ii = 0
|
|
259
|
+
while (ii < inc["diagnostics"].length) {
|
|
260
|
+
allDiag.push(inc["diagnostics"][ii])
|
|
261
|
+
ii = ii + 1
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let bsl = fm["bodyStartLine1"]
|
|
265
|
+
if (bsl === null) {
|
|
266
|
+
bsl = 1
|
|
267
|
+
}
|
|
268
|
+
let blocks = parseMarkdownToBlocks(inc["body"])
|
|
269
|
+
let ast = documentNode(fm["meta"], blocks)
|
|
270
|
+
let pr = {}
|
|
271
|
+
pr["ast"] = ast
|
|
272
|
+
pr["diagnostics"] = allDiag
|
|
273
|
+
pr["bodyStartLine1"] = bsl
|
|
274
|
+
return pr
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
export fn stringifyDiagnostics(diagnostics) {
|
|
279
|
+
if (diagnostics === null) {
|
|
280
|
+
return JSON.stringify([])
|
|
281
|
+
}
|
|
282
|
+
return JSON.stringify(diagnostics)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export fn stringifyAst(ast) {
|
|
286
|
+
return JSON.stringify(ast)
|
|
287
|
+
}
|