@brett_lamy/docstream 1.2.0 → 1.2.1

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "GitBook-aware readonly markdown and AI stream renderer.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -229,27 +229,54 @@ function serializeImage(n: InlineImageNode): string {
229
229
  return n.link ? `<a href="${n.link}">${img}</a>` : img
230
230
  }
231
231
 
232
- // Serializes inline nodes back to markdown.
232
+ // Serializes inline nodes back to markdown. Emphasis (bold / italic / strike) is kept open across
233
+ // adjacent runs that share it, so `**bold [link](url) inside**` — three runs under one bold — comes back
234
+ // as written instead of `**bold ****[link](url)**** inside**`.
235
+ type Emphasis = "strike" | "italic" | "bold"
236
+ const EMPHASIS: Emphasis[] = ["strike", "italic", "bold"] // outermost → innermost
237
+ const MARKER: Record<Emphasis, string> = { strike: "~~", italic: "_", bold: "**" }
238
+
239
+ function textBody(n: TextNode): string {
240
+ const s = n.code ? n.text : escapeText(n.text)
241
+ return n.code ? `\`${s}\`` : s
242
+ }
243
+
233
244
  export function serializeInline(nodes: Inline[]): string {
234
- return nodes
235
- .map((n) => {
236
- if (n.type === "image") return serializeImage(n)
237
- if (n.type === "reference") return serializeReference(n)
238
- // bare autolink: text identical to the URL, no other marks
239
- if (n.link && n.text === n.link && !n.bold && !n.italic && !n.strike && !n.code) {
240
- return n.link
241
- }
242
- let s = n.code ? n.text : escapeText(n.text)
243
- if (n.code) s = `\`${s}\``
244
- const linkInner = n.link && n.linkInner && (n.bold || n.italic || n.strike)
245
- if (linkInner) s = `[${s}](${n.link})`
246
- if (n.bold) s = `**${s}**`
247
- if (n.italic) s = `_${s}_`
248
- if (n.strike) s = `~~${s}~~`
249
- if (n.link && !linkInner) s = `[${s}](${n.link})`
250
- return s
251
- })
252
- .join("")
245
+ let out = ""
246
+ const open: Emphasis[] = []
247
+ const closeTo = (depth: number) => {
248
+ while (open.length > depth) out += MARKER[open.pop()!]
249
+ }
250
+ for (const n of nodes) {
251
+ if (n.type !== "text") {
252
+ closeTo(0)
253
+ out += n.type === "image" ? serializeImage(n) : serializeReference(n)
254
+ continue
255
+ }
256
+ // bare autolink: text identical to the URL, no other marks
257
+ if (n.link && n.text === n.link && !n.bold && !n.italic && !n.strike && !n.code) {
258
+ closeTo(0)
259
+ out += n.link
260
+ continue
261
+ }
262
+ const wanted = EMPHASIS.filter((m) => n[m])
263
+ // A link around its emphasis (`[**x**](url)`) is self-contained: close everything, wrap this run alone.
264
+ if (n.link && !(n.linkInner && wanted.length)) {
265
+ closeTo(0)
266
+ let s = textBody(n)
267
+ for (const m of [...wanted].reverse()) s = `${MARKER[m]}${s}${MARKER[m]}`
268
+ out += `[${s}](${n.link})`
269
+ continue
270
+ }
271
+ // Keep the longest prefix of open marks this run still wants, close the rest, open what's missing.
272
+ let keep = 0
273
+ while (keep < open.length && wanted.includes(open[keep])) keep++
274
+ closeTo(keep)
275
+ for (const m of wanted) if (!open.includes(m)) { open.push(m); out += MARKER[m] }
276
+ out += n.link ? `[${textBody(n)}](${n.link})` : textBody(n)
277
+ }
278
+ closeTo(0)
279
+ return out
253
280
  }
254
281
 
255
282
  export function plainText(nodes: Inline[]): string {