commonMain.org.intellij.markdown.html.GeneratingProviders.kt Maven / Gradle / Ivy
package org.intellij.markdown.html
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.*
import org.intellij.markdown.ast.impl.ListCompositeNode
import org.intellij.markdown.ast.impl.ListItemCompositeNode
import org.intellij.markdown.html.entities.EntityConverter
import org.intellij.markdown.lexer.Compat.assert
import org.intellij.markdown.parser.LinkMap
import kotlin.text.Regex
abstract class OpenCloseGeneratingProvider : GeneratingProvider {
abstract fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode)
abstract fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode)
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
openTag(visitor, text, node)
node.acceptChildren(visitor)
closeTag(visitor, text, node)
}
}
abstract class InlineHolderGeneratingProvider : OpenCloseGeneratingProvider() {
open fun childrenToRender(node: ASTNode): List {
return node.children
}
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
openTag(visitor, text, node)
for (child in childrenToRender(node)) {
if (child is LeafASTNode) {
visitor.visitLeaf(child)
} else {
child.accept(visitor)
}
}
closeTag(visitor, text, node)
}
}
open class SimpleTagProvider(val tagName: String) : OpenCloseGeneratingProvider() {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagOpen(node, tagName)
}
override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagClose(tagName)
}
}
open class SimpleInlineTagProvider(val tagName: String, val renderFrom: Int = 0, val renderTo: Int = 0)
: InlineHolderGeneratingProvider() {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagOpen(node, tagName)
}
override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagClose(tagName)
}
override fun childrenToRender(node: ASTNode): List {
return node.children.subList(renderFrom, node.children.size + renderTo)
}
}
class CodeSpanGeneratingProvider: GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val nodes = node.children.subList(1, node.children.size - 1)
val output = nodes.joinToString(separator = "") { HtmlGenerator.leafText(text, it, false) }.trim()
visitor.consumeTagOpen(node, "code")
visitor.consumeHtml(output)
visitor.consumeTagClose("code")
}
}
open class TransparentInlineHolderProvider(renderFrom: Int = 0, renderTo: Int = 0)
: SimpleInlineTagProvider("", renderFrom, renderTo) {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
}
override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
}
}
open class TrimmingInlineHolderProvider : InlineHolderGeneratingProvider() {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
}
override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
}
override fun childrenToRender(node: ASTNode): List {
val children = node.children
var from = 0
while (from < children.size && children[from].type == MarkdownTokenTypes.WHITE_SPACE) {
from++
}
var to = children.size
while (to > from && children[to - 1].type == MarkdownTokenTypes.WHITE_SPACE) {
to--
}
return children.subList(from, to)
}
}
internal class ListItemGeneratingProvider : SimpleTagProvider("li") {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
assert(node is ListItemCompositeNode)
openTag(visitor, text, node)
val listNode = node.parent
assert(listNode is ListCompositeNode)
val isLoose = (listNode as ListCompositeNode).loose
for (child in node.children) {
if (child.type == MarkdownElementTypes.PARAGRAPH && !isLoose) {
SilentParagraphGeneratingProvider.processNode(visitor, text, child)
} else {
child.accept(visitor)
}
}
closeTag(visitor, text, node)
}
object SilentParagraphGeneratingProvider : InlineHolderGeneratingProvider() {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
}
override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
}
}
}
internal class HtmlBlockGeneratingProvider : GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
for (child in node.children) {
if (child.type in listOf(MarkdownTokenTypes.EOL, MarkdownTokenTypes.HTML_BLOCK_CONTENT)) {
visitor.consumeHtml(child.getTextInNode(text))
}
}
visitor.consumeHtml("\n")
}
}
internal class CodeFenceGeneratingProvider : GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val indentBefore = node.getTextInNode(text).commonPrefixWith(" ".repeat(10)).length
visitor.consumeHtml("")
var state = 0
var childrenToConsider = node.children
if (childrenToConsider.last().type == MarkdownTokenTypes.CODE_FENCE_END) {
childrenToConsider = childrenToConsider.subList(0, childrenToConsider.size - 1)
}
var lastChildWasContent = false
val attributes = ArrayList()
for (child in childrenToConsider) {
if (state == 1 && child.type in listOf(MarkdownTokenTypes.CODE_FENCE_CONTENT,
MarkdownTokenTypes.EOL)) {
visitor.consumeHtml(HtmlGenerator.trimIndents(HtmlGenerator.leafText(text, child, false), indentBefore))
lastChildWasContent = child.type == MarkdownTokenTypes.CODE_FENCE_CONTENT
}
if (state == 0 && child.type == MarkdownTokenTypes.FENCE_LANG) {
attributes.add("class=\"language-${
HtmlGenerator.leafText(text, child).toString().trim().split(' ')[0]
}\"")
}
if (state == 0 && child.type == MarkdownTokenTypes.EOL) {
visitor.consumeTagOpen(node, "code", *attributes.toTypedArray())
state = 1
}
}
if (state == 0) {
visitor.consumeTagOpen(node, "code", *attributes.toTypedArray())
}
if (lastChildWasContent) {
visitor.consumeHtml("\n")
}
visitor.consumeHtml("
")
}
}
abstract class LinkGeneratingProvider(val baseURI: URI?, val resolveAnchors: Boolean = false) : GeneratingProvider {
final override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val info = getRenderInfo(text, node)
?: return fallbackProvider.processNode(visitor, text, node)
renderLink(visitor, text, node, info)
}
protected fun makeAbsoluteUrl(destination : CharSequence) : CharSequence {
if (!resolveAnchors && destination.startsWith('#')) {
return destination
}
return baseURI?.resolveToStringSafe(destination.toString()) ?: destination
}
open fun renderLink(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode, info: RenderInfo) {
visitor.consumeTagOpen(node, "a", "href=\"${makeAbsoluteUrl(info.destination)}\"", info.title?.let { "title=\"$it\"" })
labelProvider.processNode(visitor, text, info.label)
visitor.consumeTagClose("a")
}
abstract fun getRenderInfo(text: String, node: ASTNode): RenderInfo?
data class RenderInfo(val label: ASTNode, val destination: CharSequence, val title: CharSequence?)
companion object {
val fallbackProvider = TransparentInlineHolderProvider()
val labelProvider = TransparentInlineHolderProvider(1, -1)
}
}
open class InlineLinkGeneratingProvider(baseURI: URI?, resolveAnchors: Boolean = false)
: LinkGeneratingProvider(baseURI, resolveAnchors) {
override fun getRenderInfo(text: String, node: ASTNode): RenderInfo? {
val label = node.findChildOfType(MarkdownElementTypes.LINK_TEXT)
?: return null
return RenderInfo(
label,
node.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)?.getTextInNode(text)?.let {
LinkMap.normalizeDestination(it, true)
} ?: "",
node.findChildOfType(MarkdownElementTypes.LINK_TITLE)?.getTextInNode(text)?.let {
LinkMap.normalizeTitle(it)
}
)
}
}
open class ReferenceLinksGeneratingProvider(private val linkMap: LinkMap, baseURI: URI?, resolveAnchors: Boolean = false)
: LinkGeneratingProvider(baseURI, resolveAnchors) {
override fun getRenderInfo(text: String, node: ASTNode): RenderInfo? {
val label = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_LABEL }
?: return null
val linkInfo = linkMap.getLinkInfo(label.getTextInNode(text))
?: return null
val linkTextNode = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT }
return RenderInfo(
linkTextNode ?: label,
EntityConverter.replaceEntities(linkInfo.destination, processEntities = true, processEscapes = true),
linkInfo.title?.let { EntityConverter.replaceEntities(it, processEntities = true, processEscapes = true) }
)
}
}
open class ImageGeneratingProvider(linkMap: LinkMap, baseURI: URI?) : LinkGeneratingProvider(baseURI) {
protected val referenceLinkProvider = ReferenceLinksGeneratingProvider(linkMap, baseURI)
protected val inlineLinkProvider = InlineLinkGeneratingProvider(baseURI)
override fun getRenderInfo(text: String, node: ASTNode): RenderInfo? {
node.findChildOfType(MarkdownElementTypes.INLINE_LINK)?.let { linkNode ->
return inlineLinkProvider.getRenderInfo(text, linkNode)
}
(node.findChildOfType(MarkdownElementTypes.FULL_REFERENCE_LINK)
?: node.findChildOfType(MarkdownElementTypes.SHORT_REFERENCE_LINK))
?.let { linkNode ->
return referenceLinkProvider.getRenderInfo(text, linkNode)
}
return null
}
override fun renderLink(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode, info: RenderInfo) {
visitor.consumeTagOpen(node, "img",
"src=\"${makeAbsoluteUrl(info.destination)}\"",
"alt=\"${getPlainTextFrom(info.label, text)}\"",
info.title?.let { "title=\"$it\"" },
autoClose = true)
}
private fun getPlainTextFrom(node: ASTNode, text: String): CharSequence {
return REGEX.replace(node.getTextInNode(text), "")
}
companion object {
val REGEX = Regex("[^a-zA-Z0-9 ]")
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy