@jongleberry/vurst-markdown 0.2.4 → 0.4.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/package.json +16 -1
- package/streaming-buffer.d.ts +13 -0
- package/streaming-buffer.js +337 -0
package/package.json
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jongleberry/vurst-markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Rust + N-API: Markdown chunking and rendering.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"require": "./index.js",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./streaming-buffer": {
|
|
14
|
+
"types": "./streaming-buffer.d.ts",
|
|
15
|
+
"require": "./streaming-buffer.js",
|
|
16
|
+
"default": "./streaming-buffer.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
7
20
|
"license": "MIT",
|
|
8
21
|
"engines": {
|
|
9
22
|
"node": ">= 18"
|
|
@@ -23,6 +36,8 @@
|
|
|
23
36
|
"files": [
|
|
24
37
|
"index.js",
|
|
25
38
|
"index.d.ts",
|
|
39
|
+
"streaming-buffer.js",
|
|
40
|
+
"streaming-buffer.d.ts",
|
|
26
41
|
"README.md",
|
|
27
42
|
"scripts/"
|
|
28
43
|
],
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface MarkdownStreamBuffer {
|
|
2
|
+
push(text: string): string[]
|
|
3
|
+
flush(): string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface MarkdownStreamBufferOptions {
|
|
7
|
+
maxHoldMs?: number
|
|
8
|
+
now?: () => number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export declare function createMarkdownStreamBuffer(
|
|
12
|
+
options?: MarkdownStreamBufferOptions,
|
|
13
|
+
): MarkdownStreamBuffer
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function isEscaped(text, index) {
|
|
4
|
+
let backslashes = 0
|
|
5
|
+
for (let cursor = index - 1; cursor >= 0 && text[cursor] === '\\'; cursor -= 1) {
|
|
6
|
+
backslashes += 1
|
|
7
|
+
}
|
|
8
|
+
return backslashes % 2 === 1
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function findBalancedEnd(text, start, open, close, codeSpans) {
|
|
12
|
+
let depth = 0
|
|
13
|
+
let codeSpanIndex = 0
|
|
14
|
+
while (codeSpanIndex < codeSpans.length && codeSpans[codeSpanIndex].end <= start) {
|
|
15
|
+
codeSpanIndex += 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let index = start
|
|
19
|
+
while (index < text.length) {
|
|
20
|
+
const codeSpan = codeSpans[codeSpanIndex]
|
|
21
|
+
if (codeSpan !== undefined && index === codeSpan.start) {
|
|
22
|
+
index = codeSpan.end
|
|
23
|
+
codeSpanIndex += 1
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
if (text[index] === '\\') {
|
|
27
|
+
index += 2
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if (text[index] === open) {
|
|
31
|
+
depth += 1
|
|
32
|
+
} else if (text[index] === close) {
|
|
33
|
+
depth -= 1
|
|
34
|
+
if (depth === 0) return index
|
|
35
|
+
}
|
|
36
|
+
index += 1
|
|
37
|
+
}
|
|
38
|
+
return -1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findLinkDestinationEnd(text, start) {
|
|
42
|
+
let depth = 0
|
|
43
|
+
let quote = null
|
|
44
|
+
let titleMayStart = false
|
|
45
|
+
let index = start
|
|
46
|
+
while (index < text.length) {
|
|
47
|
+
const character = text[index]
|
|
48
|
+
if (character === '\\') {
|
|
49
|
+
index += 2
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if (quote !== null) {
|
|
53
|
+
if (character === quote) quote = null
|
|
54
|
+
index += 1
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if (character === ' ' || character === '\t' || character === '\n') {
|
|
58
|
+
titleMayStart = true
|
|
59
|
+
index += 1
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (titleMayStart && (character === '"' || character === "'")) {
|
|
63
|
+
quote = character
|
|
64
|
+
titleMayStart = false
|
|
65
|
+
} else if (character === '(') {
|
|
66
|
+
titleMayStart = false
|
|
67
|
+
depth += 1
|
|
68
|
+
} else if (character === ')') {
|
|
69
|
+
depth -= 1
|
|
70
|
+
if (depth === 0) return index
|
|
71
|
+
} else {
|
|
72
|
+
titleMayStart = false
|
|
73
|
+
}
|
|
74
|
+
index += 1
|
|
75
|
+
}
|
|
76
|
+
return -1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function advanceCodeSpanIndex(codeSpans, codeSpanIndex, index) {
|
|
80
|
+
while (codeSpanIndex < codeSpans.length && codeSpans[codeSpanIndex].end <= index) {
|
|
81
|
+
codeSpanIndex += 1
|
|
82
|
+
}
|
|
83
|
+
return codeSpanIndex
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function inspectLink(text, index, codeSpans) {
|
|
87
|
+
const labelEnd = findBalancedEnd(text, index, '[', ']', codeSpans)
|
|
88
|
+
const opener =
|
|
89
|
+
index > 0 && text[index - 1] === '!' && !isEscaped(text, index - 1) ? index - 1 : index
|
|
90
|
+
if (labelEnd === -1 || labelEnd === text.length - 1) {
|
|
91
|
+
return { opener, end: -1, nextSearchStart: index + 1 }
|
|
92
|
+
}
|
|
93
|
+
if (text[labelEnd + 1] !== '(') {
|
|
94
|
+
return { opener: null, end: -1, nextSearchStart: labelEnd + 1 }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const destinationEnd = findLinkDestinationEnd(text, labelEnd + 1)
|
|
98
|
+
return {
|
|
99
|
+
opener,
|
|
100
|
+
end: destinationEnd === -1 ? -1 : destinationEnd + 1,
|
|
101
|
+
nextSearchStart: destinationEnd === -1 ? labelEnd + 1 : destinationEnd + 1,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findLinkRanges(text, codeSpans) {
|
|
106
|
+
const complete = []
|
|
107
|
+
const unsafe = []
|
|
108
|
+
let searchStart = 0
|
|
109
|
+
let codeSpanIndex = 0
|
|
110
|
+
while (searchStart < text.length) {
|
|
111
|
+
const index = text.indexOf('[', searchStart)
|
|
112
|
+
if (index === -1) break
|
|
113
|
+
|
|
114
|
+
codeSpanIndex = advanceCodeSpanIndex(codeSpans, codeSpanIndex, index)
|
|
115
|
+
const codeSpan = codeSpans[codeSpanIndex]
|
|
116
|
+
if (codeSpan !== undefined && codeSpan.start < index) {
|
|
117
|
+
searchStart = codeSpan.end
|
|
118
|
+
codeSpanIndex += 1
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if (isEscaped(text, index)) {
|
|
122
|
+
searchStart = index + 1
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const link = inspectLink(text, index, codeSpans)
|
|
127
|
+
if (link.opener !== null && link.end === -1) unsafe.push(link.opener)
|
|
128
|
+
if (link.end >= 0) complete.push({ start: link.opener, end: link.end })
|
|
129
|
+
searchStart = link.nextSearchStart
|
|
130
|
+
}
|
|
131
|
+
return { complete, unsafe }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function findBacktickRun(text, searchStart) {
|
|
135
|
+
const start = text.indexOf('`', searchStart)
|
|
136
|
+
if (start === -1) return null
|
|
137
|
+
|
|
138
|
+
let end = start + 1
|
|
139
|
+
while (text[end] === '`') end += 1
|
|
140
|
+
return { start, end, length: end - start }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function findUnescapedBacktickRun(text, searchStart) {
|
|
144
|
+
let run = findBacktickRun(text, searchStart)
|
|
145
|
+
while (run !== null && isEscaped(text, run.start)) {
|
|
146
|
+
run = findBacktickRun(text, run.end)
|
|
147
|
+
}
|
|
148
|
+
return run
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findCodeSpans(text) {
|
|
152
|
+
const complete = []
|
|
153
|
+
let searchStart = 0
|
|
154
|
+
while (searchStart < text.length) {
|
|
155
|
+
const opener = findUnescapedBacktickRun(text, searchStart)
|
|
156
|
+
if (opener === null) return { complete, unclosedStart: -1 }
|
|
157
|
+
|
|
158
|
+
let closingSearchStart = opener.end
|
|
159
|
+
while (closingSearchStart < text.length) {
|
|
160
|
+
const closer = findBacktickRun(text, closingSearchStart)
|
|
161
|
+
if (closer === null) return { complete, unclosedStart: opener.start }
|
|
162
|
+
if (closer.length === opener.length) {
|
|
163
|
+
complete.push({ start: opener.start, end: closer.end })
|
|
164
|
+
searchStart = closer.end
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
closingSearchStart = closer.end
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (closingSearchStart >= text.length) return { complete, unclosedStart: opener.start }
|
|
171
|
+
}
|
|
172
|
+
return { complete, unclosedStart: -1 }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function findHtmlTagEnd(text, opener) {
|
|
176
|
+
let quote = null
|
|
177
|
+
let index = opener + 1
|
|
178
|
+
while (index < text.length) {
|
|
179
|
+
const character = text[index]
|
|
180
|
+
if (quote !== null) {
|
|
181
|
+
if (character === quote) quote = null
|
|
182
|
+
index += 1
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
if (character === '"' || character === "'") {
|
|
186
|
+
quote = character
|
|
187
|
+
} else if (character === '>') {
|
|
188
|
+
return index
|
|
189
|
+
} else if (character === '<') {
|
|
190
|
+
return -1
|
|
191
|
+
}
|
|
192
|
+
index += 1
|
|
193
|
+
}
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findHtmlTagRanges(text, codeSpans) {
|
|
198
|
+
const complete = []
|
|
199
|
+
const unsafe = []
|
|
200
|
+
let codeSpanIndex = 0
|
|
201
|
+
let searchStart = 0
|
|
202
|
+
while (searchStart < text.length) {
|
|
203
|
+
const index = text.indexOf('<', searchStart)
|
|
204
|
+
if (index === -1) break
|
|
205
|
+
|
|
206
|
+
codeSpanIndex = advanceCodeSpanIndex(codeSpans, codeSpanIndex, index)
|
|
207
|
+
const codeSpan = codeSpans[codeSpanIndex]
|
|
208
|
+
if (codeSpan !== undefined && codeSpan.start < index) {
|
|
209
|
+
searchStart = codeSpan.end
|
|
210
|
+
codeSpanIndex += 1
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const tagEnd = findHtmlTagEnd(text, index)
|
|
215
|
+
if (tagEnd === null) {
|
|
216
|
+
unsafe.push(index)
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
if (tagEnd === -1) {
|
|
220
|
+
searchStart = index + 1
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
complete.push({ start: index, end: tagEnd + 1 })
|
|
224
|
+
searchStart = tagEnd + 1
|
|
225
|
+
}
|
|
226
|
+
return { complete, unsafe }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isInsideCompleteRange(index, ranges) {
|
|
230
|
+
return ranges.some((range) => range.start <= index && index < range.end)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function findFirstTopLevelUnsafe(candidates, enclosingRanges) {
|
|
234
|
+
let rangeIndex = 0
|
|
235
|
+
for (const candidate of candidates) {
|
|
236
|
+
while (
|
|
237
|
+
rangeIndex < enclosingRanges.length &&
|
|
238
|
+
enclosingRanges[rangeIndex].end <= candidate
|
|
239
|
+
) {
|
|
240
|
+
rangeIndex += 1
|
|
241
|
+
}
|
|
242
|
+
const range = enclosingRanges[rangeIndex]
|
|
243
|
+
if (range === undefined || candidate < range.start) return candidate
|
|
244
|
+
}
|
|
245
|
+
return -1
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function findSafeBoundary(text) {
|
|
249
|
+
const codeSpans = findCodeSpans(text)
|
|
250
|
+
const opaqueCodeSpans = [...codeSpans.complete]
|
|
251
|
+
if (codeSpans.unclosedStart >= 0) {
|
|
252
|
+
opaqueCodeSpans.push({ start: codeSpans.unclosedStart, end: text.length })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const links = findLinkRanges(text, opaqueCodeSpans)
|
|
256
|
+
const htmlTags = findHtmlTagRanges(text, opaqueCodeSpans)
|
|
257
|
+
const completeOuterRanges = [...links.complete, ...htmlTags.complete]
|
|
258
|
+
const unclosedCodeIsNested =
|
|
259
|
+
codeSpans.unclosedStart >= 0 &&
|
|
260
|
+
isInsideCompleteRange(codeSpans.unclosedStart, completeOuterRanges)
|
|
261
|
+
const firstUnsafeLink = findFirstTopLevelUnsafe(links.unsafe, htmlTags.complete)
|
|
262
|
+
const firstUnsafeHtmlTag = findFirstTopLevelUnsafe(htmlTags.unsafe, links.complete)
|
|
263
|
+
|
|
264
|
+
let boundary = text.length
|
|
265
|
+
if (firstUnsafeLink >= 0) boundary = Math.min(boundary, firstUnsafeLink)
|
|
266
|
+
if (codeSpans.unclosedStart >= 0 && !unclosedCodeIsNested) {
|
|
267
|
+
boundary = Math.min(boundary, codeSpans.unclosedStart)
|
|
268
|
+
}
|
|
269
|
+
if (firstUnsafeHtmlTag >= 0) boundary = Math.min(boundary, firstUnsafeHtmlTag)
|
|
270
|
+
if (boundary < text.length) return boundary
|
|
271
|
+
|
|
272
|
+
if (text.endsWith('\\')) return text.length - 1
|
|
273
|
+
|
|
274
|
+
return text.length
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function createMarkdownStreamBuffer(options = {}) {
|
|
278
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
279
|
+
throw new TypeError('options must be an object')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const { maxHoldMs = 50, now = Date.now } = options
|
|
283
|
+
if (!Number.isFinite(maxHoldMs) || maxHoldMs < 0) {
|
|
284
|
+
throw new RangeError('maxHoldMs must be a finite nonnegative number')
|
|
285
|
+
}
|
|
286
|
+
if (typeof now !== 'function') {
|
|
287
|
+
throw new TypeError('now must be a function')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let pending = ''
|
|
291
|
+
let heldSince = null
|
|
292
|
+
|
|
293
|
+
function drain() {
|
|
294
|
+
const output = pending
|
|
295
|
+
pending = ''
|
|
296
|
+
heldSince = null
|
|
297
|
+
return output
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function emitSafePrefix(output) {
|
|
301
|
+
if (pending.length === 0) {
|
|
302
|
+
heldSince = null
|
|
303
|
+
return [output]
|
|
304
|
+
}
|
|
305
|
+
if (heldSince === null) heldSince = now()
|
|
306
|
+
if (now() - heldSince >= maxHoldMs) return [output, drain()]
|
|
307
|
+
return [output]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
push(text) {
|
|
312
|
+
if (typeof text !== 'string') {
|
|
313
|
+
throw new TypeError('text must be a string')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
pending += text
|
|
317
|
+
if (pending.length === 0) return []
|
|
318
|
+
const boundary = findSafeBoundary(pending)
|
|
319
|
+
if (boundary === 0) {
|
|
320
|
+
if (heldSince === null) heldSince = now()
|
|
321
|
+
if (now() - heldSince >= maxHoldMs) {
|
|
322
|
+
return [drain()]
|
|
323
|
+
}
|
|
324
|
+
return []
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const output = pending.slice(0, boundary)
|
|
328
|
+
pending = pending.slice(boundary)
|
|
329
|
+
return emitSafePrefix(output)
|
|
330
|
+
},
|
|
331
|
+
flush() {
|
|
332
|
+
return drain()
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
exports.createMarkdownStreamBuffer = createMarkdownStreamBuffer
|