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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import org.commonmark.node.Link
import org.commonmark.node.Node
import org.commonmark.node.Paragraph
import org.commonmark.node.Text
import org.commonmark.renderer.NodeRenderer
import org.commonmark.renderer.markdown.CoreMarkdownNodeRenderer
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext
import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory
import org.commonmark.renderer.markdown.MarkdownRenderer
import org.commonmark.renderer.text.TextContentRenderer
import java.text.BreakIterator
Expand All @@ -16,7 +20,11 @@ import java.util.Locale
* Conversions of parsed doc-comment Markdown into the strings a plugin emits.
*/
public object MarkdownText {
private val defaultMarkdownRenderer: MarkdownRenderer = MarkdownRenderer.builder().build()
private val bareUrl = Regex("""https?://[^\s<>\[\]]+""")
private val trailingPunctuation = charArrayOf('.', ',', ';', ':', '!', '?', ')', '\'', '"')

private val defaultMarkdownRenderer: MarkdownRenderer =
MarkdownRenderer.builder().nodeRendererFactory(BareUrlRendererFactory).build()
private val textRenderer: TextContentRenderer = TextContentRenderer.builder().stripNewlines(true).build()

/**
Expand Down Expand Up @@ -89,4 +97,39 @@ public object MarkdownText {
val end = iterator.next()
return if (end == BreakIterator.DONE) plain else plain.substring(0, end).trim()
}

/**
* Writes bare `http(s)://` URLs inside [Text] nodes verbatim. The core renderer backslash-escapes
* `_` and `*` in text, which would turn `ISO_8601` in a bare URL into `ISO\_8601`; the parser
* does not autolink bare URLs, so the escaped form is never read back and leaks into the output.
* All other text still goes through the core renderer's escaping.
*/
private object BareUrlRendererFactory : MarkdownNodeRendererFactory {
override fun create(context: MarkdownNodeRendererContext): NodeRenderer = BareUrlRenderer(context)

override fun getSpecialCharacters(): Set<Char> = emptySet()
}

private class BareUrlRenderer(private val context: MarkdownNodeRendererContext) : NodeRenderer {
private val core = CoreMarkdownNodeRenderer(context)

override fun getNodeTypes(): Set<Class<out Node>> = setOf(Text::class.java)

override fun render(node: Node) {
val literal = (node as Text).literal
var last = 0
for (match in bareUrl.findAll(literal)) {
val url = match.value.trimEnd(*trailingPunctuation)
if (url.length <= "https://".length) continue
core.writeText(literal.substring(last, match.range.first))
context.writer.raw(url)
last = match.range.first + url.length
}
core.writeText(literal.substring(last))
}

private fun CoreMarkdownNodeRenderer.writeText(text: String) {
if (text.isNotEmpty()) visit(Text(text))
}
}
}
7 changes: 7 additions & 0 deletions markdown/src/test/kotlin/ReferenceLinkTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,13 @@ class MarkdownTextTests :
MarkdownText.firstSentence(parser.parse("")) shouldBe ""
}

test("render leaves bare URLs unescaped but still escapes other text") {
val url = "https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations"
MarkdownText.render(parser.parse("See $url")) shouldBe "See $url"
MarkdownText.render(parser.parse("See $url.")) shouldBe "See $url."
MarkdownText.render(parser.parse("a \\_b\\_ and $url (c)")) shouldBe "a \\_b\\_ and $url (c)"
}

test("render trims trailing whitespace") {
MarkdownText.render(parser.parse("*line one* \n\n\n")) shouldBe "*line one*"
}
Expand Down
Loading