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

commonMain.org.intellij.markdown.flavours.gfm.GFMGeneratingProviders.kt Maven / Gradle / Ivy

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

import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.ast.*
import org.intellij.markdown.ast.impl.ListCompositeNode
import org.intellij.markdown.ast.impl.ListItemCompositeNode
import org.intellij.markdown.html.GeneratingProvider
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.html.InlineHolderGeneratingProvider
import org.intellij.markdown.html.SimpleTagProvider
import org.intellij.markdown.html.entities.EntityConverter
import org.intellij.markdown.lexer.Compat.assert
import kotlin.text.Regex

internal class CheckedListItemGeneratingProvider : SimpleTagProvider("li") {
    override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
    }

    override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
        assert(node is ListItemCompositeNode)

        val checkBoxElement = node.findChildOfType(GFMTokenTypes.CHECK_BOX)
        val inputHtml: CharSequence
        val listItemClass: CharSequence?
        if (checkBoxElement != null) {
            listItemClass = "class=\"task-list-item\""
            val checkedString = getIsCheckedString(checkBoxElement, text)
            inputHtml = ""
        } else {
            listItemClass = null
            inputHtml = ""
        }

        visitor.consumeTagOpen(node, "li", listItemClass)

        val listNode = node.parent
        assert(listNode is ListCompositeNode)

        val isLoose = (listNode as ListCompositeNode).loose

        var flushedInput = false
        for (child in node.children) {
            if (child is LeafASTNode) {
                continue
            }
            if (!flushedInput) {
                if (child.type == MarkdownElementTypes.PARAGRAPH) {
                    SubParagraphGeneratingProvider(isLoose, inputHtml).
                            processNode(visitor, text, child)
                } else {
                    visitor.consumeHtml(inputHtml)
                    child.accept(visitor)
                }
                flushedInput = true
            } else {
                child.accept(visitor)
            }
        }

        closeTag(visitor, text, node)
    }

    private fun getIsCheckedString(node: ASTNode?, text: String): String {
        var isChecked = node?.getTextInNode(text)?.let {
            it.length > 1 && it[1] != ' '
        } == true

        val checkedString = if (isChecked) " checked" else ""
        return checkedString
    }

    private class SubParagraphGeneratingProvider(val wrapInParagraph: Boolean, val inputHtml: String)
    : InlineHolderGeneratingProvider() {
        override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
            if (wrapInParagraph) {
                visitor.consumeTagOpen(node, "p")
            }
            visitor.consumeHtml(inputHtml)
        }

        override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
            if (wrapInParagraph) {
                visitor.consumeTagClose("p")
            }
        }
    }
}

/**
 * Special version of [org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor.CodeSpanGeneratingProvider],
 * that will correctly escape table pipes if the code span is inside a table cell.
 */
open class TableAwareCodeSpanGeneratingProvider: GeneratingProvider {
    override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
        val isInsideTable = isInsideTable(node)
        val nodes = collectContentNodes(node)
        val output = nodes.joinToString(separator = "") { processChild(it, text, isInsideTable) }.trim()
        visitor.consumeTagOpen(node, "code")
        visitor.consumeHtml(output)
        visitor.consumeTagClose("code")
    }

    protected fun isInsideTable(node: ASTNode): Boolean {
        return node.getParentOfType(GFMTokenTypes.CELL) != null
    }

    protected fun collectContentNodes(node: ASTNode): List {
        check(node.children.size >= 2)
        return node.children.subList(1, node.children.size - 1)
    }

    protected fun processChild(node: ASTNode, text: String, isInsideTable: Boolean): CharSequence {
        if (!isInsideTable) {
            return HtmlGenerator.leafText(text, node, replaceEscapesAndEntities = false)
        }
        val nodeText = node.getTextInNode(text).toString()
        val escaped = nodeText.replace("\\|", "|")
        return EntityConverter.replaceEntities(escaped, processEntities = false, processEscapes = false)
    }
}

internal class TablesGeneratingProvider : GeneratingProvider {
    override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
        assert(node.type == GFMElementTypes.TABLE)

        val alignmentInfo = getAlignmentInfo(text, node)
        var rowsPopulated = 0

        visitor.consumeTagOpen(node, "table")
        for (child in node.children) {
            if (child.type == GFMElementTypes.HEADER) {
                visitor.consumeHtml("")
                populateRow(visitor, child, "th", alignmentInfo, -1)
                visitor.consumeHtml("")
            } else if (child.type == GFMElementTypes.ROW) {
                if (rowsPopulated == 0) {
                    visitor.consumeHtml("")
                }
                rowsPopulated++
                populateRow(visitor, child, "td", alignmentInfo, rowsPopulated)
            }
        }
        if (rowsPopulated > 0) {
            visitor.consumeHtml("")
        }
        visitor.consumeTagClose("table")
    }

    private fun populateRow(visitor: HtmlGenerator.HtmlGeneratingVisitor,
                            node: ASTNode,
                            cellName: String,
                            alignmentInfo: List,
                            rowNumber: Int) {
        val parityAttribute = if (rowNumber > 0 && rowNumber % 2 == 0) "class=\"intellij-row-even\"" else null

        visitor.consumeTagOpen(node, "tr", parityAttribute)
        for (child in node.children.filter { it.type == GFMTokenTypes.CELL }.withIndex()) {
            if (child.index >= alignmentInfo.size) {
                throw IllegalStateException("Too many cells in a row! Should check parser.")
            }

            val alignment = alignmentInfo[child.index]
            val alignmentAttribute = if (alignment.isDefault) null else "align=\"${alignment.htmlName}\""

            visitor.consumeTagOpen(child.value, cellName, alignmentAttribute)
            visitor.visitNode(child.value)
            visitor.consumeTagClose(cellName)
        }

        for (i in node.children.count { it.type == GFMTokenTypes.CELL }..alignmentInfo.size - 1) {
            visitor.consumeHtml("")
        }
        visitor.consumeTagClose("tr")
    }

    private fun getAlignmentInfo(text: String, node: ASTNode): List {
        val separatorRow = node.findChildOfType(GFMTokenTypes.TABLE_SEPARATOR)
                ?: throw IllegalStateException("Could not find table separator")

        val result = ArrayList()

        val cells = SPLIT_REGEX.split(separatorRow.getTextInNode(text))
        for (i in cells.indices) {
            val cell = cells[i]
            if (!cell.isBlank() || i in 1..cells.lastIndex - 1) {
                val trimmed = cell.trim()
                val starts = trimmed.startsWith(':')
                val ends = trimmed.endsWith(':')
                result.add(if (starts && ends) {
                    Alignment.CENTER
                } else if (starts) {
                    Alignment.LEFT
                } else if (ends) {
                    Alignment.RIGHT
                } else {
                    DEFAULT_ALIGNMENT
                })
            }
        }
        return result
    }

    enum class Alignment(val htmlName: String, val isDefault: Boolean) {
        LEFT("left", true),
        CENTER("center", false),
        RIGHT("right", false)
    }

    companion object {
        val DEFAULT_ALIGNMENT = Alignment.values().find { it.isDefault }
            ?: throw IllegalStateException("Must be default alignment")

        val SPLIT_REGEX = Regex("\\|")
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy