blob: c60625a4a543219c0890883614266cdeabe15acc [file] [log] [blame]
package org.jetbrains.dokka
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.html.entities.EntityConverter
import org.intellij.markdown.parser.LinkMap
import java.util.*
class LinkResolver(private val linkMap: LinkMap, private val contentFactory: (String) -> ContentBlock) {
fun getLinkInfo(refLabel: String) = linkMap.getLinkInfo(refLabel)
fun resolve(href: String): ContentBlock = contentFactory(href)
}
fun buildContent(tree: MarkdownNode, linkResolver: LinkResolver, inline: Boolean = false): MutableContent {
val result = MutableContent()
if (inline) {
buildInlineContentTo(tree, result, linkResolver)
} else {
buildContentTo(tree, result, linkResolver)
}
return result
}
fun buildContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: LinkResolver) {
// println(tree.toTestString())
val nodeStack = ArrayDeque<ContentBlock>()
nodeStack.push(target)
tree.visit { node, processChildren ->
val parent = nodeStack.peek()
fun appendNodeWithChildren(content: ContentBlock) {
nodeStack.push(content)
processChildren()
parent.append(nodeStack.pop())
}
when (node.type) {
MarkdownElementTypes.ATX_1 -> appendNodeWithChildren(ContentHeading(1))
MarkdownElementTypes.ATX_2 -> appendNodeWithChildren(ContentHeading(2))
MarkdownElementTypes.ATX_3 -> appendNodeWithChildren(ContentHeading(3))
MarkdownElementTypes.ATX_4 -> appendNodeWithChildren(ContentHeading(4))
MarkdownElementTypes.ATX_5 -> appendNodeWithChildren(ContentHeading(5))
MarkdownElementTypes.ATX_6 -> appendNodeWithChildren(ContentHeading(6))
MarkdownElementTypes.UNORDERED_LIST -> appendNodeWithChildren(ContentUnorderedList())
MarkdownElementTypes.ORDERED_LIST -> appendNodeWithChildren(ContentOrderedList())
MarkdownElementTypes.LIST_ITEM -> appendNodeWithChildren(ContentListItem())
MarkdownElementTypes.EMPH -> appendNodeWithChildren(ContentEmphasis())
MarkdownElementTypes.STRONG -> appendNodeWithChildren(ContentStrong())
MarkdownElementTypes.CODE_SPAN -> {
val startDelimiter = node.child(MarkdownTokenTypes.BACKTICK)?.text
if (startDelimiter != null) {
val text = node.text.substring(startDelimiter.length).removeSuffix(startDelimiter)
val codeSpan = ContentCode().apply { append(ContentText(text)) }
parent.append(codeSpan)
}
}
MarkdownElementTypes.CODE_BLOCK,
MarkdownElementTypes.CODE_FENCE -> {
val language = node.child(MarkdownTokenTypes.FENCE_LANG)?.text?.trim() ?: ""
appendNodeWithChildren(ContentBlockCode(language))
}
MarkdownElementTypes.PARAGRAPH -> appendNodeWithChildren(ContentParagraph())
MarkdownElementTypes.INLINE_LINK -> {
val linkTextNode = node.child(MarkdownElementTypes.LINK_TEXT)
val destination = node.child(MarkdownElementTypes.LINK_DESTINATION)
if (linkTextNode != null) {
if (destination != null) {
val link = ContentExternalLink(destination.text)
renderLinkTextTo(linkTextNode, link, linkResolver)
parent.append(link)
} else {
val link = ContentExternalLink(linkTextNode.getLabelText())
renderLinkTextTo(linkTextNode, link, linkResolver)
parent.append(link)
}
}
}
MarkdownElementTypes.SHORT_REFERENCE_LINK,
MarkdownElementTypes.FULL_REFERENCE_LINK -> {
val labelElement = node.child(MarkdownElementTypes.LINK_LABEL)
if (labelElement != null) {
val linkInfo = linkResolver.getLinkInfo(labelElement.text)
val labelText = labelElement.getLabelText()
val link = linkInfo?.let { linkResolver.resolve(it.destination.toString()) } ?: linkResolver.resolve(labelText)
val linkText = node.child(MarkdownElementTypes.LINK_TEXT)
if (linkText != null) {
renderLinkTextTo(linkText, link, linkResolver)
} else {
link.append(ContentText(labelText))
}
parent.append(link)
}
}
MarkdownTokenTypes.WHITE_SPACE -> {
// Don't append first space if start of header (it is added during formatting later)
// v
// #### Some Heading
if (nodeStack.peek() !is ContentHeading || node.parent?.children?.first() != node) {
parent.append(ContentText(node.text))
}
}
MarkdownTokenTypes.EOL -> {
if ((keepEol(nodeStack.peek()) && node.parent?.children?.last() != node) ||
// Keep extra blank lines when processing lists (affects Markdown formatting)
(processingList(nodeStack.peek()) && node.previous?.type == MarkdownTokenTypes.EOL)) {
parent.append(ContentText(node.text))
}
}
MarkdownTokenTypes.CODE_LINE -> {
val content = ContentText(node.text)
if (parent is ContentBlockCode) {
parent.append(content)
} else {
parent.append(ContentBlockCode().apply { append(content) })
}
}
MarkdownTokenTypes.TEXT -> {
fun createEntityOrText(text: String): ContentNode {
if (text == "&amp;" || text == "&quot;" || text == "&lt;" || text == "&gt;") {
return ContentEntity(text)
}
if (text == "&") {
return ContentEntity("&amp;")
}
val decodedText = EntityConverter.replaceEntities(text, true, true)
if (decodedText != text) {
return ContentEntity(text)
}
return ContentText(text)
}
parent.append(createEntityOrText(node.text))
}
MarkdownTokenTypes.EMPH -> {
val parentNodeType = node.parent?.type
if (parentNodeType != MarkdownElementTypes.EMPH && parentNodeType != MarkdownElementTypes.STRONG) {
parent.append(ContentText(node.text))
}
}
MarkdownTokenTypes.COLON,
MarkdownTokenTypes.SINGLE_QUOTE,
MarkdownTokenTypes.DOUBLE_QUOTE,
MarkdownTokenTypes.LT,
MarkdownTokenTypes.GT,
MarkdownTokenTypes.LPAREN,
MarkdownTokenTypes.RPAREN,
MarkdownTokenTypes.LBRACKET,
MarkdownTokenTypes.RBRACKET,
MarkdownTokenTypes.EXCLAMATION_MARK,
MarkdownTokenTypes.BACKTICK,
MarkdownTokenTypes.CODE_FENCE_CONTENT -> {
parent.append(ContentText(node.text))
}
MarkdownElementTypes.LINK_DEFINITION -> {
}
else -> {
processChildren()
}
}
}
}
private fun MarkdownNode.getLabelText() = children.filter { it.type == MarkdownTokenTypes.TEXT || it.type == MarkdownTokenTypes.EMPH }.joinToString("") { it.text }
private fun keepEol(node: ContentNode) = node is ContentParagraph || node is ContentSection || node is ContentBlockCode
private fun processingList(node: ContentNode) = node is ContentOrderedList || node is ContentUnorderedList
fun buildInlineContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: LinkResolver) {
val inlineContent = tree.children.singleOrNull { it.type == MarkdownElementTypes.PARAGRAPH }?.children ?: listOf(tree)
inlineContent.forEach {
buildContentTo(it, target, linkResolver)
}
}
fun renderLinkTextTo(tree: MarkdownNode, target: ContentBlock, linkResolver: LinkResolver) {
val linkTextNodes = tree.children.drop(1).dropLast(1)
linkTextNodes.forEach {
buildContentTo(it, target, linkResolver)
}
}