@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.
@@ -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), ![alt](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 `![label](src "title")`. `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: ![alt](src)
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
+ }