backport the intellij markdown renderer (Apache 2.0)
This commit is contained in:
parent
59d4dcc8bf
commit
52a6752f80
10 changed files with 808 additions and 2 deletions
|
@ -13,6 +13,7 @@ plugins {
|
|||
id("org.jetbrains.changelog") version("2.2.0")
|
||||
id("org.jetbrains.grammarkit") version("2022.3.2.2")
|
||||
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
|
||||
|
@ -74,6 +75,7 @@ allprojects {
|
|||
apply {
|
||||
plugin("org.jetbrains.grammarkit")
|
||||
plugin("org.jetbrains.intellij")
|
||||
plugin("org.jetbrains.kotlin.jvm")
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
@ -156,6 +158,14 @@ allprojects {
|
|||
verifyPlugin {
|
||||
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") {
|
||||
dependencies {
|
||||
implementation(project(":zig"))
|
||||
|
@ -211,6 +235,7 @@ project(":lsp") {
|
|||
}
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation(project(":backports"))
|
||||
api(project(":lsp-common"))
|
||||
api("org.apache.commons:commons-lang3:3.14.0")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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, "<")
|
||||
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;"
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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})" })})"
|
||||
)
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -23,7 +23,7 @@ import com.falsepattern.zigbrains.lsp.requests.Timeout;
|
|||
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
|
||||
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
|
||||
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.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
|
|
|
@ -25,7 +25,7 @@ import com.intellij.codeInsight.hints.declarative.InlayTreeSink;
|
|||
import com.intellij.codeInsight.hints.declarative.InlineInlayPosition;
|
||||
import com.intellij.codeInsight.hints.declarative.OwnBypassCollector;
|
||||
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.editor.Editor;
|
||||
import com.intellij.psi.PsiFile;
|
||||
|
|
Loading…
Add table
Reference in a new issue