backport the intellij markdown renderer (Apache 2.0)

This commit is contained in:
FalsePattern 2024-03-12 14:56:50 +01:00
parent 59d4dcc8bf
commit 52a6752f80
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
10 changed files with 808 additions and 2 deletions

View file

@ -13,6 +13,7 @@ plugins {
id("org.jetbrains.changelog") version("2.2.0") id("org.jetbrains.changelog") version("2.2.0")
id("org.jetbrains.grammarkit") version("2022.3.2.2") id("org.jetbrains.grammarkit") version("2022.3.2.2")
id("com.palantir.git-version") version("3.0.0") id("com.palantir.git-version") version("3.0.0")
id("org.jetbrains.kotlin.jvm") version("1.9.22") //Only used by backport module
} }
val gitVersion: groovy.lang.Closure<String> by extra val gitVersion: groovy.lang.Closure<String> by extra
@ -74,6 +75,7 @@ allprojects {
apply { apply {
plugin("org.jetbrains.grammarkit") plugin("org.jetbrains.grammarkit")
plugin("org.jetbrains.intellij") plugin("org.jetbrains.intellij")
plugin("org.jetbrains.kotlin.jvm")
} }
repositories { repositories {
mavenCentral() mavenCentral()
@ -156,6 +158,14 @@ allprojects {
verifyPlugin { verifyPlugin {
enabled = false enabled = false
} }
compileKotlin {
enabled = false
}
compileTestKotlin {
enabled = false
}
} }
} }
@ -181,6 +191,20 @@ project(":") {
} }
} }
project(":backports") {
tasks {
compileKotlin {
enabled = true
kotlinOptions.jvmTarget = "17"
}
compileTestKotlin {
enabled = true
kotlinOptions.jvmTarget = "17"
}
}
}
project(":debugger") { project(":debugger") {
dependencies { dependencies {
implementation(project(":zig")) implementation(project(":zig"))
@ -211,6 +235,7 @@ project(":lsp") {
} }
dependencies { dependencies {
implementation(project(":common")) implementation(project(":common"))
implementation(project(":backports"))
api(project(":lsp-common")) api(project(":lsp-common"))
api("org.apache.commons:commons-lang3:3.14.0") api("org.apache.commons:commons-lang3:3.14.0")
} }

View file

@ -0,0 +1,133 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.falsepattern.zigbrains.backports.com.intellij.lang.documentation
import com.intellij.lang.Language
import com.intellij.lang.documentation.DocumentationMarkup.*
import com.intellij.lang.documentation.DocumentationSettings.InlineCodeHighlightingMode
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.editor.richcopy.HtmlSyntaxInfoUtil
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.psi.PsiElement
import com.falsepattern.zigbrains.backports.com.intellij.ui.components.JBHtmlPaneStyleConfiguration
import com.falsepattern.zigbrains.backports.com.intellij.ui.components.JBHtmlPaneStyleConfiguration.*
import com.intellij.lang.documentation.DocumentationSettings
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.xml.util.XmlStringUtil
import org.jetbrains.annotations.ApiStatus.Internal
/**
* This class facilitates generation of highlighted text and code for Quick Documentation.
* It honors [DocumentationSettings], when highlighting code and links.
*/
object QuickDocHighlightingHelper {
const val CODE_BLOCK_PREFIX = "<pre><code>"
const val CODE_BLOCK_SUFFIX = "</code></pre>"
const val INLINE_CODE_PREFIX = "<code>"
const val INLINE_CODE_SUFFIX = "</code>"
/**
* The returned code block HTML (prefixed with [CODE_BLOCK_PREFIX] and suffixed with [CODE_BLOCK_SUFFIX])
* has syntax highlighted, if there is language provided and
* [DocumentationSettings.isHighlightingOfCodeBlocksEnabled] is `true`. The code block will
* be rendered with a special background if [DocumentationSettings.isCodeBackgroundEnabled] is `true`.
*
* Any special HTML characters, like `<` or `>` are escaped.
*/
@JvmStatic
@RequiresReadLock
fun getStyledCodeBlock(project: Project, language: Language?, code: @NlsSafe CharSequence): @NlsSafe String =
StringBuilder().apply { appendStyledCodeBlock(project, language, code) }.toString()
/**
* Appends code block HTML (prefixed with [CODE_BLOCK_PREFIX] and suffixed with [CODE_BLOCK_SUFFIX]),
* which has syntax highlighted, if there is language provided and
* [DocumentationSettings.isHighlightingOfCodeBlocksEnabled] is `true`. The code block will
* be rendered with a special background if [DocumentationSettings.isCodeBackgroundEnabled] is `true`.
*
* Any special HTML characters, like `<` or `>` are escaped.
*/
@JvmStatic
@RequiresReadLock
fun StringBuilder.appendStyledCodeBlock(project: Project, language: Language?, code: @NlsSafe CharSequence): @NlsSafe StringBuilder =
append(CODE_BLOCK_PREFIX)
.appendHighlightedCode(project, language, DocumentationSettings.isHighlightingOfCodeBlocksEnabled(), code, true)
.append(CODE_BLOCK_SUFFIX)
/**
* The returned inline code HTML (prefixed with [INLINE_CODE_PREFIX] and suffixed with [INLINE_CODE_SUFFIX])
* has syntax highlighted, if there is language provided and
* [DocumentationSettings.getInlineCodeHighlightingMode] is [DocumentationSettings.InlineCodeHighlightingMode.SEMANTIC_HIGHLIGHTING].
* The code block will be rendered with a special background if [DocumentationSettings.isCodeBackgroundEnabled] is `true` and
* [DocumentationSettings.getInlineCodeHighlightingMode] is not [DocumentationSettings.InlineCodeHighlightingMode.NO_HIGHLIGHTING].
*
* Any special HTML characters, like `<` or `>` are escaped.
*/
@JvmStatic
@RequiresReadLock
fun getStyledInlineCode(project: Project, language: Language?, @NlsSafe code: String): @NlsSafe String =
StringBuilder().apply { appendStyledInlineCode(project, language, code) }.toString()
/**
* Appends inline code HTML (prefixed with [INLINE_CODE_PREFIX] and suffixed with [INLINE_CODE_SUFFIX]),
* which has syntax highlighted, if there is language provided and
* [DocumentationSettings.getInlineCodeHighlightingMode] is [DocumentationSettings.InlineCodeHighlightingMode.SEMANTIC_HIGHLIGHTING].
* The code block will be rendered with a special background if [DocumentationSettings.isCodeBackgroundEnabled] is `true` and
* [DocumentationSettings.getInlineCodeHighlightingMode] is not [DocumentationSettings.InlineCodeHighlightingMode.NO_HIGHLIGHTING].
*
* Any special HTML characters, like `<` or `>` are escaped.
*/
@JvmStatic
@RequiresReadLock
fun StringBuilder.appendStyledInlineCode(project: Project, language: Language?, @NlsSafe code: String): StringBuilder =
append(INLINE_CODE_PREFIX)
.appendHighlightedCode(
project, language, DocumentationSettings.getInlineCodeHighlightingMode() == InlineCodeHighlightingMode.SEMANTIC_HIGHLIGHTING, code,
true)
.append(INLINE_CODE_SUFFIX)
/**
* Tries to guess a registered IDE language based. Useful e.g. for Markdown support
* to figure out a language to render a code block.
*/
@JvmStatic
fun guessLanguage(language: String?): Language? =
if (language == null)
null
else
Language
.findInstancesByMimeType(language)
.asSequence()
.plus(Language.findInstancesByMimeType("text/$language"))
.plus(
Language.getRegisteredLanguages()
.asSequence()
.filter { languageMatches(language, it) }
)
.firstOrNull()
private fun StringBuilder.appendHighlightedCode(project: Project, language: Language?, doHighlighting: Boolean,
code: CharSequence, isForRenderedDoc: Boolean): StringBuilder {
val processedCode = code.toString().trim('\n', '\r').replace(' ', ' ').trimEnd()
if (language != null && doHighlighting) {
HtmlSyntaxInfoUtil.appendHighlightedByLexerAndEncodedAsHtmlCodeSnippet(
this, project, language, processedCode,
DocumentationSettings.getHighlightingSaturation(isForRenderedDoc))
}
else {
append(XmlStringUtil.escapeString(processedCode.trimIndent()))
}
return this
}
private fun languageMatches(langType: String, language: Language): Boolean =
langType.equals(language.id, ignoreCase = true)
|| FileTypeManager.getInstance().getFileTypeByExtension(langType) === language.associatedFileType
}

View file

@ -0,0 +1,271 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc
import com.intellij.lang.Language
import com.intellij.lang.documentation.DocumentationSettings
import com.falsepattern.zigbrains.backports.com.intellij.lang.documentation.QuickDocHighlightingHelper
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl.DocFlavourDescriptor
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl.DocTagRenderer
import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiFile
import com.intellij.ui.ColorUtil
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.util.containers.CollectionFactory
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.ui.UIUtil
import org.intellij.markdown.IElementType
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.parser.MarkdownParser
import org.jetbrains.annotations.Contract
import org.jetbrains.annotations.Nls
import java.util.regex.Pattern
/**
* [DocMarkdownToHtmlConverter] handles conversion of Markdown text to HTML, which is intended
* to be displayed in Quick Doc popup, or inline in an editor.
*/
object DocMarkdownToHtmlConverter {
private val LOG = Logger.getInstance(DocMarkdownToHtmlConverter::class.java)
private val TAG_START_OR_CLOSE_PATTERN: Pattern = Pattern.compile("(<)/?(\\w+)[> ]")
internal val TAG_PATTERN: Pattern = Pattern.compile("^</?([a-z][a-z-_0-9]*)[^>]*>$", Pattern.CASE_INSENSITIVE)
private val FENCE_PATTERN = "\\s+```.*".toRegex()
private const val FENCED_CODE_BLOCK = "```"
private val HTML_DOC_SUBSTITUTIONS: Map<String, String> = mapOf(
"<em>" to "<i>",
"</em>" to "</i>",
"<strong>" to "<b>",
"</strong>" to "</b>",
": //" to "://", // Fix URL
"<p></p>" to "",
"</p>" to "",
"<br />" to "",
)
internal val ACCEPTABLE_BLOCK_TAGS: MutableSet<CharSequence> = CollectionFactory.createCharSequenceSet(false)
.apply {
addAll(listOf( // Text content
"blockquote", "dd", "dl", "dt",
"hr", "li", "ol", "ul", "pre", "p", // Table,
"caption", "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr"
))
}
internal val ACCEPTABLE_TAGS: Set<CharSequence> = CollectionFactory.createCharSequenceSet(false)
.apply {
addAll(ACCEPTABLE_BLOCK_TAGS)
addAll(listOf( // Content sectioning
"h1", "h2", "h3", "h4", "h5", "h6",
// Inline text semantic
"a", "b", "br", "code", "em", "i", "s", "span", "strong", "u", "wbr", "kbd", "samp",
// Image and multimedia
"img",
// Svg and math
"svg",
// Obsolete
"tt",
// special IJ tags
"shortcut", "icon"
))
}
/**
* Converts provided Markdown text to HTML. The results are intended to be used for Quick Documentation.
* If [defaultLanguage] is provided, it will be used for syntax coloring of inline code and code blocks, if language specifier is missing.
* Block and inline code syntax coloring is being done by [QuickDocHighlightingHelper], which honors [DocumentationSettings].
* Conversion must be run within a Read Action as it might require to create intermediate [PsiFile] to highlight block of code,
* or an inline code.
*/
@Contract(pure = true)
@JvmStatic
@RequiresReadLock
@JvmOverloads
fun convert(project: Project, @Nls markdownText: String, defaultLanguage: Language? = null): @Nls String {
val lines = markdownText.lines()
val minCommonIndent =
lines
.filter(String::isNotBlank)
.minOfOrNull { line -> line.indexOfFirst { !it.isWhitespace() }.let { if (it == -1) line.length else it } }
?: 0
val processedLines = ArrayList<String>(lines.size)
var isInCode = false
var isInTable = false
var tableFormats: List<String>? = null
for (i in lines.indices) {
var processedLine = lines[i].let { if (it.length <= minCommonIndent) "" else it.substring(minCommonIndent) }
processedLine = processedLine.trimEnd()
val count = StringUtil.getOccurrenceCount(processedLine, FENCED_CODE_BLOCK)
if (count > 0) {
isInCode = if (count % 2 == 0) isInCode else !isInCode
if (processedLine.matches(FENCE_PATTERN)) {
processedLine = processedLine.trim { it <= ' ' }
}
}
else if (!isInCode) {
// TODO merge custom table generation code with Markdown parser
val tableDelimiterIndex = processedLine.indexOf('|')
if (tableDelimiterIndex != -1) {
if (!isInTable) {
if (i + 1 < lines.size) {
tableFormats = parseTableFormats(splitTableCols(lines[i + 1]))
}
}
// create table only if we've successfully read the formats line
if (!ContainerUtil.isEmpty(tableFormats)) {
val parts = splitTableCols(processedLine)
if (isTableHeaderSeparator(parts)) continue
processedLine = getProcessedRow(project, defaultLanguage, isInTable, parts, tableFormats)
if (!isInTable) processedLine = "<table style=\"border: 0px;\" cellspacing=\"0\">$processedLine"
isInTable = true
}
}
else {
if (isInTable) processedLine += "</table>"
isInTable = false
tableFormats = null
}
}
processedLines.add(processedLine)
}
var normalizedMarkdown = StringUtil.join(processedLines, "\n")
if (isInTable) normalizedMarkdown += "</table>" //NON-NLS
return performConversion(project, defaultLanguage, normalizedMarkdown)?.trimEnd()
?: adjustHtml(replaceProhibitedTags(convertNewLinePlaceholdersToTags(markdownText), ContainerUtil.emptyList()))
}
private fun convertNewLinePlaceholdersToTags(generatedDoc: String): String {
return StringUtil.replace(generatedDoc, "\n", "\n<p>")
}
private fun parseTableFormats(cols: List<String>): List<String>? {
val formats = ArrayList<String>()
for (col in cols) {
if (!isHeaderSeparator(col)) return null
formats.add(parseFormat(col.trim { it <= ' ' }))
}
return formats
}
private fun isTableHeaderSeparator(parts: List<String>): Boolean =
parts.all { isHeaderSeparator(it) }
private fun isHeaderSeparator(s: String): Boolean =
s.trim { it <= ' ' }.removePrefix(":").removeSuffix(":").chars().allMatch { it == '-'.code }
private fun splitTableCols(processedLine: String): List<String> {
val parts = ArrayList(StringUtil.split(processedLine, "|"))
if (parts.isEmpty()) return parts
if (parts[0].isNullOrBlank())
parts.removeAt(0)
if (!parts.isEmpty() && parts[parts.size - 1].isNullOrBlank())
parts.removeAt(parts.size - 1)
return parts
}
private fun getProcessedRow(project: Project,
defaultLanguage: Language?,
isInTable: Boolean,
parts: List<String>,
tableFormats: List<String>?): String {
val openingTagStart = if (isInTable)
"<td style=\"$border\" "
else
"<th style=\"$border\" "
val closingTag = if (isInTable) "</td>" else "</th>"
val resultBuilder = StringBuilder("<tr style=\"$border\">$openingTagStart")
resultBuilder.append("align=\"").append(getAlign(0, tableFormats)).append("\">")
for (i in parts.indices) {
if (i > 0) {
resultBuilder.append(closingTag).append(openingTagStart).append("align=\"").append(getAlign(i, tableFormats)).append("\">")
}
resultBuilder.append(performConversion(project, defaultLanguage, parts[i].trim { it <= ' ' }))
}
resultBuilder.append(closingTag).append("</tr>")
return resultBuilder.toString()
}
private fun getAlign(index: Int, formats: List<String>?): String {
return if (formats == null || index >= formats.size) "left" else formats[index]
}
private fun parseFormat(format: String): String {
if (format.length <= 1) return "left"
val c0 = format[0]
val cE = format[format.length - 1]
return if (c0 == ':' && cE == ':') "center" else if (cE == ':') "right" else "left"
}
private val embeddedHtmlType = IElementType("ROOT")
private fun performConversion(project: Project, defaultLanguage: Language?, text: @Nls String): @NlsSafe String? {
try {
val flavour = DocFlavourDescriptor(project, defaultLanguage)
val parsedTree = MarkdownParser(flavour).parse(embeddedHtmlType, text, true)
return HtmlGenerator(text, parsedTree, flavour, false)
.generateHtml(DocTagRenderer(text))
}
catch (e: Exception) {
if (e is ControlFlowException) throw e
LOG.warn(e.message, e)
return null
}
}
private fun replaceProhibitedTags(line: String, skipRanges: List<TextRange>): @NlsSafe String {
val matcher = TAG_START_OR_CLOSE_PATTERN.matcher(line)
val builder = StringBuilder(line)
var diff = 0
l@ while (matcher.find()) {
val tagName = matcher.group(2)
if (ACCEPTABLE_TAGS.contains(tagName)) continue
val startOfTag = matcher.start(2)
for (range in skipRanges) {
if (range.contains(startOfTag)) {
continue@l
}
}
val start = matcher.start(1) + diff
if (StringUtil.toLowerCase(tagName) == "div") {
val isOpenTag = !matcher.group(0).contains("/")
val end = start + (if (isOpenTag) 5 else 6)
val replacement = if (isOpenTag) "<span>" else "</span>"
builder.replace(start, end, replacement)
diff += 1
}
else {
builder.replace(start, start + 1, "&lt;")
diff += 3
}
}
return builder.toString()
}
@Contract(pure = true)
private fun adjustHtml(html: String): String {
var str = html
for ((key, value) in HTML_DOC_SUBSTITUTIONS) {
if (str.indexOf(key) > 0) {
str = str.replace(key, value)
}
}
return str.trim { it <= ' ' }
}
private val border: String
get() = "margin: 0; border: 1px solid; border-color: #" + ColorUtil
.toHex(UIUtil.getTooltipSeparatorColor()) + "; border-spacing: 0; border-collapse: collapse;vertical-align: baseline;"
}

View file

@ -0,0 +1,83 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl
import com.intellij.lang.Language
import com.intellij.openapi.project.Project
import org.intellij.markdown.IElementType
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.flavours.commonmark.CommonMarkMarkerProcessor
import org.intellij.markdown.flavours.gfm.GFMConstraints
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.html.GeneratingProvider
import org.intellij.markdown.html.ImageGeneratingProvider
import org.intellij.markdown.html.ReferenceLinksGeneratingProvider
import org.intellij.markdown.html.makeXssSafe
import org.intellij.markdown.parser.LinkMap
import org.intellij.markdown.parser.MarkerProcessor
import org.intellij.markdown.parser.MarkerProcessorFactory
import org.intellij.markdown.parser.ProductionHolder
import org.intellij.markdown.parser.constraints.CommonMarkdownConstraints
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
import org.intellij.markdown.parser.markerblocks.providers.HtmlBlockProvider
import java.net.URI
private val baseHtmlGeneratingProvidersMap =
GFMFlavourDescriptor().createHtmlGeneratingProviders(LinkMap(emptyMap()), null) + hashMapOf(
MarkdownTokenTypes.HTML_TAG to DocSanitizingTagGeneratingProvider(),
MarkdownElementTypes.PARAGRAPH to DocParagraphGeneratingProvider(),
)
internal class DocFlavourDescriptor(private val project: Project, private val defaultLanguage: Language?) : GFMFlavourDescriptor() {
override val markerProcessorFactory: MarkerProcessorFactory
get() = object : MarkerProcessorFactory {
override fun createMarkerProcessor(productionHolder: ProductionHolder): MarkerProcessor<*> =
DocumentationMarkerProcessor(productionHolder, GFMConstraints.BASE)
}
override fun createHtmlGeneratingProviders(linkMap: LinkMap, baseURI: URI?): Map<IElementType, GeneratingProvider> =
MergedMap(hashMapOf(MarkdownElementTypes.FULL_REFERENCE_LINK to
ReferenceLinksGeneratingProvider(linkMap, baseURI, absolutizeAnchorLinks).makeXssSafe(useSafeLinks),
MarkdownElementTypes.SHORT_REFERENCE_LINK to
ReferenceLinksGeneratingProvider(linkMap, baseURI, absolutizeAnchorLinks).makeXssSafe(useSafeLinks),
MarkdownElementTypes.IMAGE to ImageGeneratingProvider(linkMap, baseURI).makeXssSafe(useSafeLinks),
MarkdownElementTypes.CODE_BLOCK to DocCodeBlockGeneratingProvider(project, defaultLanguage),
MarkdownElementTypes.CODE_FENCE to DocCodeBlockGeneratingProvider(project, defaultLanguage),
MarkdownElementTypes.CODE_SPAN to DocCodeSpanGeneratingProvider(project, defaultLanguage)),
baseHtmlGeneratingProvidersMap)
private class DocumentationMarkerProcessor(productionHolder: ProductionHolder,
constraintsBase: CommonMarkdownConstraints) : CommonMarkMarkerProcessor(productionHolder,
constraintsBase) {
override fun getMarkerBlockProviders(): List<MarkerBlockProvider<StateInfo>> =
super.getMarkerBlockProviders().filter { it !is HtmlBlockProvider } + DocHtmlBlockProvider
}
private class MergedMap(val map1: Map<IElementType, GeneratingProvider>,
val map2: Map<IElementType, GeneratingProvider>) : Map<IElementType, GeneratingProvider> {
override fun isEmpty(): Boolean =
map1.isEmpty() && map2.isEmpty()
override fun get(key: IElementType): GeneratingProvider? =
map1[key] ?: map2[key]
override fun containsValue(value: GeneratingProvider): Boolean =
map1.containsValue(value) || map2.containsValue(value)
override fun containsKey(key: IElementType): Boolean =
map1.containsKey(key) || map2.containsKey(key)
override val entries: Set<Map.Entry<IElementType, GeneratingProvider>>
get() = throw UnsupportedOperationException()
override val keys: Set<IElementType>
get() = throw UnsupportedOperationException()
override val size: Int
get() = throw UnsupportedOperationException()
override val values: Collection<GeneratingProvider>
get() = throw UnsupportedOperationException()
}
}

View file

@ -0,0 +1,104 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
import com.intellij.lang.Language
import com.falsepattern.zigbrains.backports.com.intellij.lang.documentation.QuickDocHighlightingHelper
import com.falsepattern.zigbrains.backports.com.intellij.lang.documentation.QuickDocHighlightingHelper.guessLanguage
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.containers.CollectionFactory
import com.intellij.util.containers.ContainerUtil
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.LeafASTNode
import org.intellij.markdown.ast.accept
import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.html.GeneratingProvider
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.html.TrimmingInlineHolderProvider
private val TAG_REPLACE_MAP = CollectionFactory.createCharSequenceMap<String>(false).also {
it["div"] = "span"
it["em"] = "i"
it["strong"] = "b"
}
internal class DocSanitizingTagGeneratingProvider : GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val nodeText = node.getTextInNode(text)
if (nodeText.contentEquals("</p>", true)) return
val matcher = DocMarkdownToHtmlConverter.TAG_PATTERN.matcher(nodeText)
if (matcher.matches()) {
val tagName = matcher.group(1)
val replaceWith = TAG_REPLACE_MAP[tagName]
if (replaceWith != null) {
visitor.consumeHtml(nodeText.subSequence(0, matcher.start(1)))
visitor.consumeHtml(replaceWith)
visitor.consumeHtml(nodeText.subSequence(matcher.end(1), nodeText.length))
return
}
if (DocMarkdownToHtmlConverter.ACCEPTABLE_TAGS.contains(tagName)) {
visitor.consumeHtml(nodeText)
return
}
}
visitor.consumeHtml(StringUtil.escapeXmlEntities(nodeText.toString()))
}
}
internal class DocCodeBlockGeneratingProvider(private val project: Project, private val defaultLanguage: Language?) : GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val contents = StringBuilder()
var language: String? = null
node.children.forEach { child ->
when (child.type) {
MarkdownTokenTypes.CODE_FENCE_CONTENT, MarkdownTokenTypes.CODE_LINE, MarkdownTokenTypes.EOL ->
contents.append(child.getTextInNode(text))
MarkdownTokenTypes.FENCE_LANG ->
language = HtmlGenerator.leafText(text, child).toString().trim().takeWhile { !it.isWhitespace() }
}
}
visitor.consumeHtml(QuickDocHighlightingHelper.getStyledCodeBlock(
project, guessLanguage(language) ?: defaultLanguage, contents.toString()))
}
}
internal class DocCodeSpanGeneratingProvider(private val project: Project, private val defaultLanguage: Language?) : GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val nodes = node.children.subList(1, node.children.size - 1)
val output = nodes
.filter { it.type != MarkdownTokenTypes.BLOCK_QUOTE }
.joinToString(separator = "") { it.getTextInNode(text) }.trim()
visitor.consumeHtml(QuickDocHighlightingHelper.getStyledInlineCode(project, defaultLanguage, output))
}
}
internal class DocParagraphGeneratingProvider : TrimmingInlineHolderProvider() {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagOpen(node, "p")
}
override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagClose("p")
}
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val childrenToRender = childrenToRender(node)
if (childrenToRender.isEmpty()) return
openTag(visitor, text, node)
for (child in childrenToRender(node)) {
if (child is LeafASTNode) {
visitor.visitLeaf(child)
} else {
child.accept(visitor)
}
}
closeTag(visitor, text, node)
}
}

View file

@ -0,0 +1,69 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
import org.intellij.markdown.lexer.Compat.assert
import org.intellij.markdown.parser.LookaheadText
import org.intellij.markdown.parser.MarkerProcessor
import org.intellij.markdown.parser.ProductionHolder
import org.intellij.markdown.parser.constraints.MarkdownConstraints
import org.intellij.markdown.parser.markerblocks.MarkerBlock
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
import org.intellij.markdown.parser.markerblocks.impl.HtmlBlockMarkerBlock
internal object DocHtmlBlockProvider : MarkerBlockProvider<MarkerProcessor.StateInfo> {
override fun createMarkerBlocks(pos: LookaheadText.Position,
productionHolder: ProductionHolder,
stateInfo: MarkerProcessor.StateInfo): List<MarkerBlock> {
val matchingGroup = matches(pos, stateInfo.currentConstraints)
if (matchingGroup in 0..3) {
return listOf(HtmlBlockMarkerBlock(stateInfo.currentConstraints, productionHolder, OPEN_CLOSE_REGEXES[matchingGroup].second, pos))
}
return emptyList()
}
override fun interruptsParagraph(pos: LookaheadText.Position, constraints: MarkdownConstraints): Boolean {
return matches(pos, constraints) in 0..4
}
private fun matches(pos: LookaheadText.Position, constraints: MarkdownConstraints): Int {
if (!MarkerBlockProvider.isStartOfLineWithConstraints(pos, constraints)) {
return -1
}
val text = pos.currentLineFromPosition
val offset = MarkerBlockProvider.passSmallIndent(text)
if (offset >= text.length || text[offset] != '<') {
return -1
}
val matchResult = FIND_START_REGEX.find(text.substring(offset))
?: return -1
assert(matchResult.groups.size == OPEN_CLOSE_REGEXES.size + 2) { "There are some excess capturing groups probably!" }
for (i in OPEN_CLOSE_REGEXES.indices) {
if (matchResult.groups[i + 2] != null) {
return i
}
}
assert(false) { "Match found but all groups are empty!" }
return -1
}
/** see {@link http://spec.commonmark.org/0.21/#html-blocks}
*
* nulls mean "Next line should be blank"
* */
private val OPEN_CLOSE_REGEXES: List<Pair<Regex, Regex?>> = listOf(
Pair(Regex("<(?:script|pre|style)(?: |>|$)", RegexOption.IGNORE_CASE),
Regex("</(?:script|style|pre)>", RegexOption.IGNORE_CASE)),
Pair(Regex("<!--"), Regex("-->")),
Pair(Regex("<\\?"), Regex("\\?>")),
Pair(Regex("<![A-Z]"), Regex(">")),
Pair(Regex("<!\\[CDATA\\["), Regex("]]>")),
Pair(Regex("</?(?:${DocMarkdownToHtmlConverter.ACCEPTABLE_BLOCK_TAGS.joinToString("|")})(?: |/?>|$)", RegexOption.IGNORE_CASE), null)
)
private val FIND_START_REGEX = Regex(
"^(${OPEN_CLOSE_REGEXES.joinToString(separator = "|", transform = { "(${it.first.pattern})" })})"
)
}

View file

@ -0,0 +1,50 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.impl
import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.html.DUMMY_ATTRIBUTES_CUSTOMIZER
import org.intellij.markdown.html.HtmlGenerator
internal class DocTagRenderer(private val wholeText: String)
: HtmlGenerator.DefaultTagRenderer(DUMMY_ATTRIBUTES_CUSTOMIZER, false) {
override fun openTag(node: ASTNode, tagName: CharSequence,
vararg attributes: CharSequence?,
autoClose: Boolean): CharSequence {
if (tagName.contentEquals("p", true)) {
val first = node.children.firstOrNull()
if (first != null && first.type === MarkdownTokenTypes.HTML_TAG) {
val text = first.getTextInNode(wholeText)
val matcher = DocMarkdownToHtmlConverter.TAG_PATTERN.matcher(text)
if (matcher.matches()) {
val nestedTag = matcher.group(1)
if (DocMarkdownToHtmlConverter.ACCEPTABLE_BLOCK_TAGS.contains(nestedTag)) {
return ""
}
}
}
}
if (tagName.contentEquals("code", true) && node.type === MarkdownTokenTypes.CODE_FENCE_CONTENT) {
return ""
}
return super.openTag(node, convertTag(tagName), *attributes, autoClose = autoClose)
}
override fun closeTag(tagName: CharSequence): CharSequence {
if (tagName.contentEquals("p", true)) return ""
return super.closeTag(convertTag(tagName))
}
private fun convertTag(tagName: CharSequence): CharSequence {
if (tagName.contentEquals("strong", true)) {
return "b"
}
else if (tagName.contentEquals("em", true)) {
return "i"
}
return tagName
}
}

View file

@ -0,0 +1,71 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.falsepattern.zigbrains.backports.com.intellij.ui.components
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.colors.TextAttributesKey
import java.util.*
data class JBHtmlPaneStyleConfiguration(
val colorScheme: EditorColorsScheme = EditorColorsManager.getInstance().globalScheme,
val editorInlineContext: Boolean = false,
val inlineCodeParentSelectors: List<String> = listOf(""),
val largeCodeFontSizeSelectors: List<String> = emptyList(),
val enableInlineCodeBackground: Boolean = true,
val enableCodeBlocksBackground: Boolean = true,
val useFontLigaturesInCode: Boolean = false,
/** Unscaled */
val spaceBeforeParagraph: Int = defaultSpaceBeforeParagraph,
/** Unscaled */
val spaceAfterParagraph: Int = defaultSpaceAfterParagraph,
val controlStyleOverrides: ControlStyleOverrides? = null,
) {
override fun equals(other: Any?): Boolean =
other is JBHtmlPaneStyleConfiguration
&& colorSchemesEqual(colorScheme, other.colorScheme)
&& inlineCodeParentSelectors == other.inlineCodeParentSelectors
&& largeCodeFontSizeSelectors == other.largeCodeFontSizeSelectors
&& enableInlineCodeBackground == other.enableInlineCodeBackground
&& enableCodeBlocksBackground == other.enableCodeBlocksBackground
&& useFontLigaturesInCode == other.useFontLigaturesInCode
&& spaceBeforeParagraph == other.spaceBeforeParagraph
&& spaceAfterParagraph == other.spaceAfterParagraph
private fun colorSchemesEqual(colorScheme: EditorColorsScheme, colorScheme2: EditorColorsScheme): Boolean =
// Update here when more colors are used from the colorScheme
colorScheme.defaultBackground.rgb == colorScheme2.defaultBackground.rgb
&& colorScheme.defaultForeground.rgb == colorScheme2.defaultForeground.rgb
&& ControlKind.entries.all {
colorScheme.getAttributes(it.colorSchemeKey) ==
colorScheme2.getAttributes(it.colorSchemeKey)
}
override fun hashCode(): Int =
Objects.hash(colorScheme.defaultBackground.rgb and 0xffffff,
colorScheme.defaultForeground.rgb and 0xffffff,
inlineCodeParentSelectors, largeCodeFontSizeSelectors,
enableInlineCodeBackground, enableCodeBlocksBackground,
useFontLigaturesInCode, spaceBeforeParagraph, spaceAfterParagraph)
data class ControlStyleOverrides(
val controlKindSuffix: String,
val overrides: Map<ControlKind, Collection<ControlProperty>>
)
enum class ControlKind(val id: String, val colorSchemeKey: TextAttributesKey) {
}
enum class ControlProperty(val id: String) {
}
companion object {
@JvmStatic
val defaultSpaceBeforeParagraph: Int get() = 4
@JvmStatic
val defaultSpaceAfterParagraph: Int get() = 4
}
}

View file

@ -23,7 +23,7 @@ import com.falsepattern.zigbrains.lsp.requests.Timeout;
import com.falsepattern.zigbrains.lsp.requests.Timeouts; import com.falsepattern.zigbrains.lsp.requests.Timeouts;
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils; import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils; import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter; import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter;
import com.intellij.model.Pointer; import com.intellij.model.Pointer;
import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.TextRange;

View file

@ -25,7 +25,7 @@ import com.intellij.codeInsight.hints.declarative.InlayTreeSink;
import com.intellij.codeInsight.hints.declarative.InlineInlayPosition; import com.intellij.codeInsight.hints.declarative.InlineInlayPosition;
import com.intellij.codeInsight.hints.declarative.OwnBypassCollector; import com.intellij.codeInsight.hints.declarative.OwnBypassCollector;
import com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayHintsPassFactory; import com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayHintsPassFactory;
import com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter; import com.falsepattern.zigbrains.backports.com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter;
import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.Editor;
import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFile;