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() 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 block = ContentBlockCode() block.append(ContentText(node.text)) parent.append(block) } MarkdownTokenTypes.TEXT -> { fun createEntityOrText(text: String): ContentNode { if (text == "&" || text == """ || text == "<" || text == ">") { return ContentEntity(text) } if (text == "&") { return ContentEntity("&") } 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) } }