Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-links-preserve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"markdown-to-jsx": patch
---

Link destinations now preserve punctuation such as underscores while markdown is streamed, preventing generated links from being changed or broken.
23 changes: 23 additions & 0 deletions lib/src/parse.streaming.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'bun:test'
import { compiler as htmlCompiler } from './html.ts'
import { parser } from './parse.ts'
import type { MarkdownToJSX } from './types.ts'

describe('Streaming optimization - inline code', () => {
it('should remove incomplete inline code backtick and content', () => {
Expand Down Expand Up @@ -36,6 +37,28 @@ describe('Streaming optimization - inline code', () => {
})
})

describe('Streaming optimization - link destinations', () => {
it('should preserve underscores in complete link destinations', () => {
const url = 'https://example.com/search?geoShape=KLUv_WCPBdUbAFa'
const ast = parser(`[View results](${url})`, {
optimizeForStreaming: true,
})
const paragraph = ast[0] as MarkdownToJSX.ParagraphNode
const link = paragraph.children[0] as MarkdownToJSX.LinkNode

expect(link.target).toBe(url)
})

it('should still suppress delimiters in incomplete links', () => {
const html = htmlCompiler('[View_results](https://example.com/search?x=1', {
optimizeForStreaming: true,
})

expect(html).not.toContain('_')
expect(html).toContain('Viewresults')
})
})

describe('Streaming optimization - code blocks', () => {
it('should show incomplete code block content', () => {
const html = htmlCompiler('```js\nconst x = 1', {
Expand Down
19 changes: 15 additions & 4 deletions lib/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5306,7 +5306,7 @@ function _skipLinkOrImage(s: string, i: number, e: number): number {

// j is now past the ]
if (j >= e) {
return j // just [text]
return i // shortcut links may not resolve, so keep their text parseable
}

const nextChar = s.charCodeAt(j)
Expand All @@ -5329,7 +5329,7 @@ function _skipLinkOrImage(s: string, i: number, e: number): number {
}
j++
}
return j
return parenDepth === 0 ? j : i
}

// Reference link: [text][ref]
Expand All @@ -5346,10 +5346,10 @@ function _skipLinkOrImage(s: string, i: number, e: number): number {
}
j++
}
return j
return depth2 === 0 ? j : i
}

return j // shortcut link [text]
return i // shortcut links may not resolve, so keep their text parseable
}

/** Scan strikethrough ~~text~~ */
Expand Down Expand Up @@ -7224,6 +7224,17 @@ function parseInline(
var lastTilDbl = -1
for (var ei = 0; ei < content.length; ei++) {
var ec = content.charCodeAt(ei)
// Link destinations are opaque to emphasis parsing. Skip complete links
// and images so punctuation such as `_` in a URL is not treated as a
// streaming delimiter. Incomplete links return their start position from
// _skipLinkOrImage and continue through the normal suppression logic.
if (ec === $.CHAR_BRACKET_OPEN) {
var linkEnd = _skipLinkOrImage(content, ei, content.length)
if (linkEnd > ei) {
ei = linkEnd - 1
continue
}
}
if (ec === $.CHAR_ASTERISK) {
if (
ei + 1 < content.length &&
Expand Down
Loading