@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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Optional frontmatter `imports` map: logical name → relative path string.
|
|
2
|
+
// Expanded by prepending resolved file contents to the body before directive `::include` resolution.
|
|
3
|
+
// Nested maps require JSON frontmatter today; minimal YAML is flat key/value only.
|
|
4
|
+
|
|
5
|
+
fn metaImportsValue(meta) {
|
|
6
|
+
if (meta === null || typeof meta !== "object") {
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
9
|
+
let ks = Object.keys(meta)
|
|
10
|
+
let i = 0
|
|
11
|
+
while (i < ks.length) {
|
|
12
|
+
if (ks[i] === "imports") {
|
|
13
|
+
return meta[ks[i]]
|
|
14
|
+
}
|
|
15
|
+
i = i + 1
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export fn applyMetaImportsToBody(meta, body, readPartial, allDiag) {
|
|
21
|
+
let b = body
|
|
22
|
+
if (b === null) {
|
|
23
|
+
b = ""
|
|
24
|
+
}
|
|
25
|
+
if (typeof b !== "string") {
|
|
26
|
+
b = String(b)
|
|
27
|
+
}
|
|
28
|
+
let im = metaImportsValue(meta)
|
|
29
|
+
if (im === null || typeof im !== "object") {
|
|
30
|
+
let r0 = {}
|
|
31
|
+
r0["body"] = b
|
|
32
|
+
return r0
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(im)) {
|
|
35
|
+
let d0 = {}
|
|
36
|
+
d0["level"] = "warn"
|
|
37
|
+
d0["message"] = "meta imports: expected object map, got array — ignored"
|
|
38
|
+
allDiag.push(d0)
|
|
39
|
+
let r1 = {}
|
|
40
|
+
r1["body"] = b
|
|
41
|
+
return r1
|
|
42
|
+
}
|
|
43
|
+
if (readPartial === null) {
|
|
44
|
+
let d1 = {}
|
|
45
|
+
d1["level"] = "warn"
|
|
46
|
+
d1["message"] = "meta imports present but readPartial is null — imports not expanded"
|
|
47
|
+
allDiag.push(d1)
|
|
48
|
+
let r2 = {}
|
|
49
|
+
r2["body"] = b
|
|
50
|
+
return r2
|
|
51
|
+
}
|
|
52
|
+
let keys = Object.keys(im)
|
|
53
|
+
let prep = ""
|
|
54
|
+
let ki = 0
|
|
55
|
+
while (ki < keys.length) {
|
|
56
|
+
let logical = keys[ki]
|
|
57
|
+
let pathVal = im[logical]
|
|
58
|
+
if (typeof pathVal !== "string") {
|
|
59
|
+
let dw = {}
|
|
60
|
+
dw["level"] = "warn"
|
|
61
|
+
dw["message"] = "meta imports." + String(logical) + ": expected string path"
|
|
62
|
+
allDiag.push(dw)
|
|
63
|
+
ki = ki + 1
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
let content = readPartial(pathVal)
|
|
67
|
+
if (content === null) {
|
|
68
|
+
let de = {}
|
|
69
|
+
de["level"] = "error"
|
|
70
|
+
de["message"] = "meta imports." + String(logical) + ": not found " + String(pathVal)
|
|
71
|
+
allDiag.push(de)
|
|
72
|
+
ki = ki + 1
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
let chunk = typeof content === "string" ? content : String(content)
|
|
76
|
+
if (prep.length > 0) {
|
|
77
|
+
prep = prep + "\n"
|
|
78
|
+
}
|
|
79
|
+
prep = prep + chunk
|
|
80
|
+
ki = ki + 1
|
|
81
|
+
}
|
|
82
|
+
let out = b
|
|
83
|
+
if (prep.length > 0) {
|
|
84
|
+
out = prep + "\n\n" + b
|
|
85
|
+
}
|
|
86
|
+
let rf = {}
|
|
87
|
+
rf["body"] = out
|
|
88
|
+
return rf
|
|
89
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { trim } from "./trim.tish"
|
|
2
|
+
import {
|
|
3
|
+
headingNode,
|
|
4
|
+
paragraphNode,
|
|
5
|
+
listNode,
|
|
6
|
+
listItemNode,
|
|
7
|
+
codeBlockNode,
|
|
8
|
+
directiveBlockNode,
|
|
9
|
+
directiveLeafNode,
|
|
10
|
+
includeNode,
|
|
11
|
+
blockQuoteNode,
|
|
12
|
+
thematicBreakNode,
|
|
13
|
+
orderedListNode,
|
|
14
|
+
tableNode,
|
|
15
|
+
tableCellNode
|
|
16
|
+
} from "./ast.tish"
|
|
17
|
+
import { parseDirectiveAttrs } from "./attrs.tish"
|
|
18
|
+
import { parseInlineSegments } from "./inline.tish"
|
|
19
|
+
|
|
20
|
+
// True if a line is an entire ASCII run of `c` (length >= 3), optionally with
|
|
21
|
+
// spaces between. Used for thematic breaks `---`, `***`, `___`.
|
|
22
|
+
fn isThematicBreak(t) {
|
|
23
|
+
if (t.length < 3) { return false }
|
|
24
|
+
let c0 = t.charAt(0)
|
|
25
|
+
if (c0 !== "-" && c0 !== "*" && c0 !== "_") { return false }
|
|
26
|
+
let count = 0
|
|
27
|
+
let i = 0
|
|
28
|
+
while (i < t.length) {
|
|
29
|
+
let c = t.charAt(i)
|
|
30
|
+
if (c === c0) { count = count + 1 }
|
|
31
|
+
else if (c !== " " && c !== "\t") { return false }
|
|
32
|
+
i = i + 1
|
|
33
|
+
}
|
|
34
|
+
return count >= 3
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Detect ordered-list item: e.g. "1. Item", "12) Item". Returns
|
|
38
|
+
// { start, body } if it matches, else null.
|
|
39
|
+
fn matchOrderedListItem(t) {
|
|
40
|
+
if (t.length === 0) { return null }
|
|
41
|
+
let i = 0
|
|
42
|
+
while (i < t.length) {
|
|
43
|
+
let c = t.charAt(i)
|
|
44
|
+
if (c < "0" || c > "9") { break }
|
|
45
|
+
i = i + 1
|
|
46
|
+
}
|
|
47
|
+
if (i === 0) { return null }
|
|
48
|
+
if (i >= t.length) { return null }
|
|
49
|
+
let punct = t.charAt(i)
|
|
50
|
+
if (punct !== "." && punct !== ")") { return null }
|
|
51
|
+
if ((i + 1) >= t.length) { return null }
|
|
52
|
+
if (t.charAt(i + 1) !== " ") { return null }
|
|
53
|
+
let r = {}
|
|
54
|
+
r["start"] = parseInt(t.slice(0, i))
|
|
55
|
+
r["body"] = trim(t.slice(i + 2))
|
|
56
|
+
return r
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse a GFM table delimiter row like `| --- | :---: | ---: |` and return
|
|
60
|
+
// an array of alignment strings ("left" | "center" | "right" | "default")
|
|
61
|
+
// or null if the line isn't a delimiter row.
|
|
62
|
+
fn parseTableDelimiter(t) {
|
|
63
|
+
let s = t
|
|
64
|
+
if (s.charAt(0) === "|") { s = s.slice(1) }
|
|
65
|
+
if (s.length > 0 && s.charAt(s.length - 1) === "|") { s = s.slice(0, s.length - 1) }
|
|
66
|
+
let cells = s.split("|")
|
|
67
|
+
let aligns = []
|
|
68
|
+
let i = 0
|
|
69
|
+
while (i < cells.length) {
|
|
70
|
+
let cell = trim(cells[i])
|
|
71
|
+
if (cell.length === 0) { return null }
|
|
72
|
+
let left = false
|
|
73
|
+
let right = false
|
|
74
|
+
if (cell.charAt(0) === ":") { left = true; cell = cell.slice(1) }
|
|
75
|
+
if (cell.length > 0 && cell.charAt(cell.length - 1) === ":") { right = true; cell = cell.slice(0, cell.length - 1) }
|
|
76
|
+
if (cell.length === 0) { return null }
|
|
77
|
+
let j = 0
|
|
78
|
+
while (j < cell.length) {
|
|
79
|
+
if (cell.charAt(j) !== "-") { return null }
|
|
80
|
+
j = j + 1
|
|
81
|
+
}
|
|
82
|
+
if (left && right) { aligns.push("center") }
|
|
83
|
+
else if (right) { aligns.push("right") }
|
|
84
|
+
else if (left) { aligns.push("left") }
|
|
85
|
+
else { aligns.push("default") }
|
|
86
|
+
i = i + 1
|
|
87
|
+
}
|
|
88
|
+
if (aligns.length === 0) { return null }
|
|
89
|
+
return aligns
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Collect a contiguous run of ordered-list items starting at `lines[i]`.
|
|
93
|
+
// Returns { start, items, nextI } so the caller never needs to keep
|
|
94
|
+
// intermediate `let`s around (Tish JS backend block-scopes nested lets).
|
|
95
|
+
fn collectOrderedListPack(lines, i) {
|
|
96
|
+
let first = matchOrderedListItem(lines[i].trim())
|
|
97
|
+
let items = []
|
|
98
|
+
items.push(listItemNode(parseInlineSegments(first["body"])))
|
|
99
|
+
let j = i + 1
|
|
100
|
+
while (j < lines.length) {
|
|
101
|
+
let Lo = lines[j].trim()
|
|
102
|
+
if (Lo === "") { break }
|
|
103
|
+
let nx = matchOrderedListItem(Lo)
|
|
104
|
+
if (nx === null) { break }
|
|
105
|
+
items.push(listItemNode(parseInlineSegments(nx["body"])))
|
|
106
|
+
j = j + 1
|
|
107
|
+
}
|
|
108
|
+
let r = {}
|
|
109
|
+
r["start"] = first["start"]
|
|
110
|
+
r["items"] = items
|
|
111
|
+
r["nextI"] = j
|
|
112
|
+
return r
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// True if `lines[i]` could be a GFM table header (contains a `|`) and
|
|
116
|
+
// `lines[i+1]` is a parseable delimiter row whose column count matches.
|
|
117
|
+
// Pulled out into a helper because Tish's JS backend block-scopes nested
|
|
118
|
+
// `let` declarations, which made the inline form ReferenceError at runtime.
|
|
119
|
+
fn canStartGfmTable(lines, i, t) {
|
|
120
|
+
if (t.indexOf("|") < 0) { return false }
|
|
121
|
+
if ((i + 1) >= lines.length) { return false }
|
|
122
|
+
let next = lines[i + 1].trim()
|
|
123
|
+
if (next.indexOf("-") < 0 || next.indexOf("|") < 0) { return false }
|
|
124
|
+
let aligns = parseTableDelimiter(next)
|
|
125
|
+
if (aligns === null) { return false }
|
|
126
|
+
let header = splitTableRow(t)
|
|
127
|
+
if (header.length !== aligns.length) { return false }
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Split a table row into trimmed cell strings (handles optional leading /
|
|
132
|
+
// trailing pipes; ignores escaped pipes \|).
|
|
133
|
+
fn splitTableRow(t) {
|
|
134
|
+
let s = t
|
|
135
|
+
if (s.length > 0 && s.charAt(0) === "|") { s = s.slice(1) }
|
|
136
|
+
if (s.length > 0 && s.charAt(s.length - 1) === "|") { s = s.slice(0, s.length - 1) }
|
|
137
|
+
let cells = []
|
|
138
|
+
let buf = ""
|
|
139
|
+
let i = 0
|
|
140
|
+
while (i < s.length) {
|
|
141
|
+
let c = s.charAt(i)
|
|
142
|
+
if (c === "\\" && (i + 1) < s.length && s.charAt(i + 1) === "|") {
|
|
143
|
+
buf = buf + "|"
|
|
144
|
+
i = i + 2
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
if (c === "|") {
|
|
148
|
+
cells.push(trim(buf))
|
|
149
|
+
buf = ""
|
|
150
|
+
i = i + 1
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
buf = buf + c
|
|
154
|
+
i = i + 1
|
|
155
|
+
}
|
|
156
|
+
cells.push(trim(buf))
|
|
157
|
+
return cells
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fn parseDirectiveOpenLine(t) {
|
|
161
|
+
let rest = t.slice(3).trim()
|
|
162
|
+
let nameEnd = 0
|
|
163
|
+
while (nameEnd < rest.length) {
|
|
164
|
+
let ch = rest.charAt(nameEnd)
|
|
165
|
+
if (ch === " " || ch === "\t" || ch === "{") {
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
nameEnd = nameEnd + 1
|
|
169
|
+
}
|
|
170
|
+
let name = trim(rest.slice(0, nameEnd))
|
|
171
|
+
let after = trim(rest.slice(nameEnd))
|
|
172
|
+
let attrs = {}
|
|
173
|
+
if (after.startsWith("{")) {
|
|
174
|
+
attrs = parseDirectiveAttrs(after.slice(after.indexOf("{")))
|
|
175
|
+
}
|
|
176
|
+
let p = {}
|
|
177
|
+
p["name"] = name
|
|
178
|
+
p["attrs"] = attrs
|
|
179
|
+
return p
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export fn isBlockStart(t) {
|
|
183
|
+
if (t.startsWith("#")) {
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
if (t.startsWith("```")) {
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
if (t.startsWith(":::")) {
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
if (t.startsWith("::") && !t.startsWith(":::")) {
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
if (t.startsWith("- ") || t.startsWith("* ")) {
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
if (t.startsWith("> ") || t === ">") {
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
if (matchOrderedListItem(t) !== null) {
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
if (isThematicBreak(t)) {
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn parseMarkdownToBlocksInner(body) {
|
|
211
|
+
let children = []
|
|
212
|
+
if (body === null) {
|
|
213
|
+
return children
|
|
214
|
+
}
|
|
215
|
+
let lines = (typeof body === "string" ? body : String(body)).split("\n")
|
|
216
|
+
let i = 0
|
|
217
|
+
let directiveStack = []
|
|
218
|
+
|
|
219
|
+
while (i < lines.length) {
|
|
220
|
+
let line = lines[i]
|
|
221
|
+
let t = line.trim()
|
|
222
|
+
|
|
223
|
+
if (t === "") {
|
|
224
|
+
i = i + 1
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (directiveStack.length > 0) {
|
|
229
|
+
if (t === ":::") {
|
|
230
|
+
let frame = directiveStack.pop()
|
|
231
|
+
let innerBody = frame.lines.join("\n")
|
|
232
|
+
let innerBlocks = parseMarkdownToBlocksInner(innerBody)
|
|
233
|
+
if (frame.name === "include") {
|
|
234
|
+
let p = frame.attrs.path
|
|
235
|
+
if (p === null) {
|
|
236
|
+
p = ""
|
|
237
|
+
}
|
|
238
|
+
children.push(includeNode(String(p), true, null, innerBlocks))
|
|
239
|
+
} else {
|
|
240
|
+
children.push(directiveBlockNode(frame.name, frame.attrs, innerBlocks))
|
|
241
|
+
}
|
|
242
|
+
i = i + 1
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (t.startsWith(":::")) {
|
|
246
|
+
let parsed = parseDirectiveOpenLine(t)
|
|
247
|
+
let fr = {}
|
|
248
|
+
fr.name = parsed.name
|
|
249
|
+
fr.attrs = parsed.attrs
|
|
250
|
+
fr.lines = []
|
|
251
|
+
directiveStack.push(fr)
|
|
252
|
+
i = i + 1
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
let top = directiveStack[directiveStack.length - 1]
|
|
256
|
+
top.lines.push(line)
|
|
257
|
+
i = i + 1
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (t.startsWith("```")) {
|
|
262
|
+
let lang = t.slice(3).trim()
|
|
263
|
+
let acc = []
|
|
264
|
+
i = i + 1
|
|
265
|
+
while (i < lines.length) {
|
|
266
|
+
let L = lines[i]
|
|
267
|
+
if (L.trim() === "```") {
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
acc.push(L)
|
|
271
|
+
i = i + 1
|
|
272
|
+
}
|
|
273
|
+
if (i < lines.length) {
|
|
274
|
+
i = i + 1
|
|
275
|
+
}
|
|
276
|
+
children.push(codeBlockNode(lang, acc.join("\n")))
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (t.startsWith(":::")) {
|
|
281
|
+
let parsed = parseDirectiveOpenLine(t)
|
|
282
|
+
let fr2 = {}
|
|
283
|
+
fr2.name = parsed.name
|
|
284
|
+
fr2.attrs = parsed.attrs
|
|
285
|
+
fr2.lines = []
|
|
286
|
+
directiveStack.push(fr2)
|
|
287
|
+
i = i + 1
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (t.startsWith("::") && !t.startsWith(":::")) {
|
|
292
|
+
let rest = t.slice(2).trim()
|
|
293
|
+
let nameEnd = 0
|
|
294
|
+
while (nameEnd < rest.length) {
|
|
295
|
+
let ch2 = rest.charAt(nameEnd)
|
|
296
|
+
if (ch2 === "{" || ch2 === " " || ch2 === "\t") {
|
|
297
|
+
break
|
|
298
|
+
}
|
|
299
|
+
nameEnd = nameEnd + 1
|
|
300
|
+
}
|
|
301
|
+
let dname = trim(rest.slice(0, nameEnd))
|
|
302
|
+
let after2 = trim(rest.slice(nameEnd))
|
|
303
|
+
let attrs2 = {}
|
|
304
|
+
if (after2.startsWith("{")) {
|
|
305
|
+
attrs2 = parseDirectiveAttrs(after2)
|
|
306
|
+
}
|
|
307
|
+
if (dname === "include") {
|
|
308
|
+
let p = attrs2.path
|
|
309
|
+
if (p === null) {
|
|
310
|
+
p = ""
|
|
311
|
+
}
|
|
312
|
+
children.push(includeNode(String(p), false, "unresolved leaf include; expand source with readPartial or use :::include", []))
|
|
313
|
+
} else {
|
|
314
|
+
children.push(directiveLeafNode(dname, attrs2))
|
|
315
|
+
}
|
|
316
|
+
i = i + 1
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (t.startsWith("#")) {
|
|
321
|
+
let level = 0
|
|
322
|
+
let k = 0
|
|
323
|
+
while (k < t.length && t.charAt(k) === "#") {
|
|
324
|
+
level = level + 1
|
|
325
|
+
k = k + 1
|
|
326
|
+
}
|
|
327
|
+
if (level > 6) {
|
|
328
|
+
level = 6
|
|
329
|
+
}
|
|
330
|
+
let title = trim(t.slice(k))
|
|
331
|
+
children.push(headingNode(level, parseInlineSegments(title)))
|
|
332
|
+
i = i + 1
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (t.startsWith("- ") || t.startsWith("* ")) {
|
|
337
|
+
let items = []
|
|
338
|
+
while (i < lines.length) {
|
|
339
|
+
let L3 = lines[i].trim()
|
|
340
|
+
if (L3 === "") {
|
|
341
|
+
break
|
|
342
|
+
}
|
|
343
|
+
if (!L3.startsWith("- ") && !L3.startsWith("* ")) {
|
|
344
|
+
break
|
|
345
|
+
}
|
|
346
|
+
let itemText = trim(L3.slice(2))
|
|
347
|
+
items.push(listItemNode(parseInlineSegments(itemText)))
|
|
348
|
+
i = i + 1
|
|
349
|
+
}
|
|
350
|
+
children.push(listNode(false, items))
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (matchOrderedListItem(t) !== null) {
|
|
355
|
+
let pack = collectOrderedListPack(lines, i)
|
|
356
|
+
children.push(orderedListNode(pack["start"], pack["items"]))
|
|
357
|
+
i = pack["nextI"]
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (t.startsWith("> ") || t === ">") {
|
|
362
|
+
let qbuf = []
|
|
363
|
+
while (i < lines.length) {
|
|
364
|
+
let Lq = lines[i]
|
|
365
|
+
let Tq = Lq.trim()
|
|
366
|
+
if (Tq === "") { break }
|
|
367
|
+
if (Tq.startsWith("> ")) { qbuf.push(Tq.slice(2)) }
|
|
368
|
+
else if (Tq === ">") { qbuf.push("") }
|
|
369
|
+
else { break }
|
|
370
|
+
i = i + 1
|
|
371
|
+
}
|
|
372
|
+
let inner = parseMarkdownToBlocksInner(qbuf.join("\n"))
|
|
373
|
+
children.push(blockQuoteNode(inner))
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (isThematicBreak(t)) {
|
|
378
|
+
children.push(thematicBreakNode())
|
|
379
|
+
i = i + 1
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (canStartGfmTable(lines, i, t)) {
|
|
384
|
+
let aligns = parseTableDelimiter(lines[i + 1].trim())
|
|
385
|
+
let headerCells = splitTableRow(t)
|
|
386
|
+
let headerNodes = []
|
|
387
|
+
let hi = 0
|
|
388
|
+
while (hi < headerCells.length) {
|
|
389
|
+
headerNodes.push(tableCellNode(parseInlineSegments(headerCells[hi])))
|
|
390
|
+
hi = hi + 1
|
|
391
|
+
}
|
|
392
|
+
let rows = []
|
|
393
|
+
i = i + 2
|
|
394
|
+
while (i < lines.length) {
|
|
395
|
+
let Lt = lines[i].trim()
|
|
396
|
+
if (Lt === "" || Lt.indexOf("|") < 0) { break }
|
|
397
|
+
let cells = splitTableRow(Lt)
|
|
398
|
+
let cellNodes = []
|
|
399
|
+
let ci3 = 0
|
|
400
|
+
while (ci3 < cells.length) {
|
|
401
|
+
cellNodes.push(tableCellNode(parseInlineSegments(cells[ci3])))
|
|
402
|
+
ci3 = ci3 + 1
|
|
403
|
+
}
|
|
404
|
+
rows.push(cellNodes)
|
|
405
|
+
i = i + 1
|
|
406
|
+
}
|
|
407
|
+
children.push(tableNode(aligns, headerNodes, rows))
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let para = []
|
|
412
|
+
while (i < lines.length) {
|
|
413
|
+
let L4 = lines[i]
|
|
414
|
+
if (L4.trim() === "") {
|
|
415
|
+
break
|
|
416
|
+
}
|
|
417
|
+
if (isBlockStart(L4.trim())) {
|
|
418
|
+
break
|
|
419
|
+
}
|
|
420
|
+
para.push(L4)
|
|
421
|
+
i = i + 1
|
|
422
|
+
}
|
|
423
|
+
if (para.length > 0) {
|
|
424
|
+
let ptext = para.join("\n")
|
|
425
|
+
children.push(paragraphNode(parseInlineSegments(ptext)))
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return children
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export fn parseMarkdownToBlocks(body) {
|
|
433
|
+
return parseMarkdownToBlocksInner(body)
|
|
434
|
+
}
|
package/src/trim.tish
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Shared whitespace trim for bundled JS (duplicate `trim` top-level names break `tish build --target js`).
|
|
2
|
+
|
|
3
|
+
export fn trim(s) {
|
|
4
|
+
let a = 0
|
|
5
|
+
let b = s.length
|
|
6
|
+
while (a < b) {
|
|
7
|
+
let c = s.charAt(a)
|
|
8
|
+
if (c !== " " && c !== "\t" && c !== "\r" && c !== "\n") {
|
|
9
|
+
break
|
|
10
|
+
}
|
|
11
|
+
a = a + 1
|
|
12
|
+
}
|
|
13
|
+
while (b > a) {
|
|
14
|
+
let c2 = s.charAt(b - 1)
|
|
15
|
+
if (c2 !== " " && c2 !== "\t" && c2 !== "\r" && c2 !== "\n") {
|
|
16
|
+
break
|
|
17
|
+
}
|
|
18
|
+
b = b - 1
|
|
19
|
+
}
|
|
20
|
+
return s.slice(a, b)
|
|
21
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Well-known meta keys; optional strict mode.
|
|
2
|
+
// Missing keys: direct `obj["k"]` throws in Tish — use safeGet for optional fields.
|
|
3
|
+
|
|
4
|
+
fn safeGet(obj, key) {
|
|
5
|
+
let ks = Object.keys(obj)
|
|
6
|
+
let i = 0
|
|
7
|
+
while (i < ks.length) {
|
|
8
|
+
if (ks[i] === key) {
|
|
9
|
+
return obj[ks[i]]
|
|
10
|
+
}
|
|
11
|
+
i = i + 1
|
|
12
|
+
}
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn diag(level, message) {
|
|
17
|
+
let d = {}
|
|
18
|
+
d["level"] = level
|
|
19
|
+
d["message"] = message
|
|
20
|
+
return d
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export fn validateMeta(meta, options) {
|
|
24
|
+
let diagnostics = []
|
|
25
|
+
if (meta === null || typeof meta !== "object") {
|
|
26
|
+
let r = {}
|
|
27
|
+
r["diagnostics"] = diagnostics
|
|
28
|
+
return r
|
|
29
|
+
}
|
|
30
|
+
let strict = false
|
|
31
|
+
if (options !== null && safeGet(options, "strictMeta") === true) {
|
|
32
|
+
strict = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let keys = Object.keys(meta)
|
|
36
|
+
let ki = 0
|
|
37
|
+
while (ki < keys.length) {
|
|
38
|
+
let k = keys[ki]
|
|
39
|
+
if (!isKnownMetaKey(k)) {
|
|
40
|
+
if (strict) {
|
|
41
|
+
diagnostics.push(diag("error", "Unknown meta key (strict): " + k))
|
|
42
|
+
} else {
|
|
43
|
+
diagnostics.push(diag("warn", "Unknown meta key: " + k))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
ki = ki + 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let dt = safeGet(meta, "doc_type")
|
|
50
|
+
if (dt === null) {
|
|
51
|
+
dt = safeGet(meta, "kind")
|
|
52
|
+
}
|
|
53
|
+
if (dt !== null && String(dt) !== "") {
|
|
54
|
+
if (!isKnownDocType(String(dt))) {
|
|
55
|
+
diagnostics.push(diag("warn", "Unknown doc_type/kind: " + String(dt) + " (expected note|deck|resume|whitepaper)"))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let r2 = {}
|
|
60
|
+
r2["diagnostics"] = diagnostics
|
|
61
|
+
return r2
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn isKnownMetaKey(k) {
|
|
65
|
+
if (k === "title") {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
if (k === "description") {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
if (k === "doc_type") {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
if (k === "kind") {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
if (k === "theme") {
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
if (k === "layout") {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
if (k === "author") {
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
if (k === "date") {
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
if (k === "tags") {
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
if (k === "imports") {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
if (k === "export") {
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
if (k === "pdf") {
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn isKnownDocType(dt) {
|
|
105
|
+
if (dt === "note") {
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
if (dt === "deck") {
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
if (dt === "resume") {
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
if (dt === "whitepaper") {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Minimal YAML subset: flat `key: value` lines; `#` comments; quoted values.
|
|
2
|
+
|
|
3
|
+
import { trim } from "./trim.tish"
|
|
4
|
+
|
|
5
|
+
export fn parseSimpleYamlBlock(text) {
|
|
6
|
+
let out = {}
|
|
7
|
+
let lines = text.split("\n")
|
|
8
|
+
let i = 0
|
|
9
|
+
while (i < lines.length) {
|
|
10
|
+
let line = lines[i]
|
|
11
|
+
let t = trim(line)
|
|
12
|
+
if (t === "" || t.charAt(0) === "#") {
|
|
13
|
+
i = i + 1
|
|
14
|
+
continue
|
|
15
|
+
}
|
|
16
|
+
let colon = t.indexOf(":")
|
|
17
|
+
if (colon <= 0) {
|
|
18
|
+
throw new Error("Unsupported YAML line (expected key: value): " + t)
|
|
19
|
+
}
|
|
20
|
+
let key = trim(t.slice(0, colon))
|
|
21
|
+
let rest = trim(t.slice(colon + 1))
|
|
22
|
+
if (key === "") {
|
|
23
|
+
throw new Error("Empty YAML key")
|
|
24
|
+
}
|
|
25
|
+
if (rest.charAt(0) === "\"" && rest.charAt(rest.length - 1) === "\"") {
|
|
26
|
+
out[key] = rest.slice(1, rest.length - 1)
|
|
27
|
+
} else if (rest.charAt(0) === "'" && rest.charAt(rest.length - 1) === "'") {
|
|
28
|
+
out[key] = rest.slice(1, rest.length - 1)
|
|
29
|
+
} else if (rest === "true") {
|
|
30
|
+
out[key] = true
|
|
31
|
+
} else if (rest === "false") {
|
|
32
|
+
out[key] = false
|
|
33
|
+
} else if (rest === "null") {
|
|
34
|
+
out[key] = null
|
|
35
|
+
} else {
|
|
36
|
+
let n = parseFloat(rest)
|
|
37
|
+
if (!isNaN(n) && String(n) === rest) {
|
|
38
|
+
out[key] = n
|
|
39
|
+
} else {
|
|
40
|
+
out[key] = rest
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
i = i + 1
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|