All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.org.intellij.markdown.html.HtmlGenerator.kt Maven / Gradle / Ivy

There is a newer version: 0.7.3
Show newest version
package org.intellij.markdown.html

import org.intellij.markdown.IElementType
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.acceptChildren
import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.ast.visitors.RecursiveVisitor
import org.intellij.markdown.flavours.MarkdownFlavourDescriptor
import org.intellij.markdown.html.entities.EntityConverter
import org.intellij.markdown.parser.LinkMap

typealias AttributesCustomizer = (node: ASTNode, tagName: CharSequence, attributes: Iterable) -> Iterable
val DUMMY_ATTRIBUTES_CUSTOMIZER: AttributesCustomizer = { _, _, attributes -> attributes }

class HtmlGenerator(private val markdownText: String,
                           private val root: ASTNode,
                           private val providers: Map,
                           private val includeSrcPositions: Boolean = false) {

    constructor(markdownText: String,
                root: ASTNode,
                flavour: MarkdownFlavourDescriptor,
                includeSrcPositions: Boolean = false)
    : this(markdownText,
            root,
            flavour.createHtmlGeneratingProviders(LinkMap.buildLinkMap(root, markdownText), null),
            includeSrcPositions)  
    
    private val htmlString: StringBuilder = StringBuilder()

    @Deprecated("To be removed, pass custom visitor instead",
            ReplaceWith("generateHtml(HtmlGeneratingVisitor)"))
    fun generateHtml(customizer: AttributesCustomizer): String {
        HtmlGeneratingVisitor(DefaultTagRenderer(customizer, includeSrcPositions)).visitNode(root)
        return htmlString.toString()
    }

    fun generateHtml(tagRenderer: TagRenderer =
                             DefaultTagRenderer(DUMMY_ATTRIBUTES_CUSTOMIZER, includeSrcPositions)): String {
        HtmlGeneratingVisitor(tagRenderer).visitNode(root)
        return htmlString.toString()
    }

    inner class HtmlGeneratingVisitor(private val tagRenderer: TagRenderer) : RecursiveVisitor() {
        override fun visitNode(node: ASTNode) {
            providers[node.type]?.processNode(this, markdownText, node)
                    ?: node.acceptChildren(this)
        }

        fun visitLeaf(node: ASTNode) {
            providers[node.type]?.processNode(this, markdownText, node)
                    ?: consumeHtml(leafText(markdownText, node))
        }

        fun consumeTagOpen(node: ASTNode,
                                tagName: CharSequence,
                                vararg attributes: CharSequence?,
                                autoClose: Boolean = false) {
            htmlString.append(tagRenderer.openTag(node, tagName, *attributes, autoClose = autoClose))
        }

        fun consumeTagClose(tagName: CharSequence) {
            htmlString.append(tagRenderer.closeTag(tagName))
        }

        fun consumeHtml(html: CharSequence) {
            htmlString.append(tagRenderer.printHtml(html))
        }
    }

    open class DefaultTagRenderer(protected val customizer: AttributesCustomizer,
                                  protected val includeSrcPositions: Boolean) : TagRenderer {
        override fun openTag(node: ASTNode, tagName: CharSequence, vararg attributes: CharSequence?, autoClose: Boolean): CharSequence {
            return buildString {
                append("<$tagName")
                for (attribute in customizer.invoke(node, tagName, attributes.asIterable())) {
                    if (attribute != null) {
                        append(" $attribute")
                    }
                }
                if (includeSrcPositions) {
                    append(" ${getSrcPosAttribute(node)}")
                }

                if (autoClose) {
                    append(" />")
                } else {
                    append(">")
                }
            }
        }

        override fun closeTag(tagName: CharSequence): CharSequence = ""

        override fun printHtml(html: CharSequence): CharSequence = html
    }

    interface TagRenderer {
        fun openTag(node: ASTNode,
                    tagName: CharSequence,
                    vararg attributes: CharSequence?,
                    autoClose: Boolean = false): CharSequence

        fun closeTag(tagName: CharSequence): CharSequence

        fun printHtml(html: CharSequence): CharSequence
    }

    companion object {
        val SRC_ATTRIBUTE_NAME = "md-src-pos"

        fun leafText(text: String, node: ASTNode, replaceEscapesAndEntities: Boolean = true): CharSequence {
            if (node.type == MarkdownTokenTypes.BLOCK_QUOTE) {
                return ""
            }
            return EntityConverter.replaceEntities(node.getTextInNode(text), replaceEscapesAndEntities, replaceEscapesAndEntities)
        }

        fun getSrcPosAttribute(node: ASTNode): CharSequence {
            return "$SRC_ATTRIBUTE_NAME=\"${node.startOffset}..${node.endOffset}\""
        }

        fun trimIndents(text: CharSequence, indent: Int): CharSequence {
            if (indent == 0) {
                return text
            }

            val buffer = StringBuilder()

            var lastFlushed = 0
            var offset = 0
            while (offset < text.length) {
                if (offset == 0 || text[offset - 1] == '\n') {
                    buffer.append(text.subSequence(lastFlushed, offset))
                    var indentEaten = 0

                    eatIndentLoop@
                    while (indentEaten < indent && offset < text.length) {
                        when (text[offset]) {
                            ' ' -> indentEaten++
                            '\t' -> indentEaten += 4 - indentEaten % 4
                            else -> break@eatIndentLoop
                        }
                        offset++
                    }

                    if (indentEaten > indent) {
                        buffer.append(" ".repeat(indentEaten - indent))
                    }
                    lastFlushed = offset
                }

                offset++
            }

            buffer.append(text.subSequence(lastFlushed, text.length))
            return buffer
        }

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy