@jongleberry/vurst-markdown 0.3.0 → 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 CHANGED
@@ -1,9 +1,22 @@
1
1
  {
2
2
  "name": "@jongleberry/vurst-markdown",
3
- "version": "0.3.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