@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,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
+ }