WEB-44476 JS Markdown - improve rendering of code blocks and inline code

GitOrigin-RevId: 76e36379ac6c36f8dabcb25dc4a29c5ec9ba51cb
This commit is contained in:
Piotr Tomiak
2024-01-16 22:16:59 +01:00
committed by intellij-monorepo-bot
parent 3f641e6857
commit 0a5092b5c3
5 changed files with 128 additions and 2 deletions

View File

@@ -77,7 +77,8 @@ public abstract class DocumentationEditorPane extends JEditorPane implements Dis
}
setBackground(BACKGROUND_COLOR);
HTMLEditorKit editorKit = new HTMLEditorKitBuilder()
.replaceViewFactoryExtensions(DocumentationHtmlUtil.getIconsExtension(iconResolver), Extensions.BASE64_IMAGES)
.replaceViewFactoryExtensions(DocumentationHtmlUtil.getIconsExtension(iconResolver), Extensions.BASE64_IMAGES,
Extensions.INLINE_VIEW_EX)
.withFontResolver(EditorCssFontResolver.getGlobalInstance()).build();
addDocumentationPaneDefaultCssRules(editorKit);

View File

@@ -15,5 +15,6 @@
<orderEntry type="library" name="jetbrains.markdown" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="library" scope="TEST" name="kotlin-test" level="project" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
</component>
</module>

View File

@@ -92,7 +92,7 @@ object DocMarkdownToHtmlConverter {
}
}
// create table only if we've successfully read the formats line
if (!ContainerUtil.isEmpty<String?>(tableFormats)) {
if (!ContainerUtil.isEmpty(tableFormats)) {
val parts = splitTableCols(processedLine)
if (isTableHeaderSeparator(parts)) continue
processedLine = getProcessedRow(isInTable, parts, tableFormats)

View File

@@ -2,6 +2,7 @@
package com.intellij.markdown.utils.doc.impl
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
@@ -26,6 +27,8 @@ internal class DocFlavourDescriptor : GFMFlavourDescriptor() {
override fun createHtmlGeneratingProviders(linkMap: LinkMap, baseURI: URI?): Map<IElementType, GeneratingProvider> {
val result = HashMap(super.createHtmlGeneratingProviders(linkMap, baseURI))
result[MarkdownTokenTypes.HTML_TAG] = DocSanitizingTagGeneratingProvider()
result[MarkdownElementTypes.CODE_FENCE] = DocCodeFenceGeneratingProvider()
result[MarkdownElementTypes.CODE_SPAN] = DocCodeSpanGeneratingProvider()
return result
}

View File

@@ -1,12 +1,26 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.markdown.utils.doc.impl
import com.intellij.lang.Language
import com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.FontPreferences
import com.intellij.openapi.editor.richcopy.HtmlSyntaxInfoUtil
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.DefaultProjectFactory
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiFileFactory
import com.intellij.ui.ColorUtil
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.html.GeneratingProvider
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.html.entities.EntityConverter
import java.awt.Color
import kotlin.math.abs
internal class DocSanitizingTagGeneratingProvider : GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
@@ -27,4 +41,111 @@ internal class DocSanitizingTagGeneratingProvider : GeneratingProvider {
}
visitor.consumeHtml(StringUtil.escapeXmlEntities(nodeText.toString()))
}
}
internal class DocCodeFenceGeneratingProvider : GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val indentBefore = node.getTextInNode(text).commonPrefixWith(" ".repeat(10)).length
var state = 0
var childrenToConsider = node.children
if (childrenToConsider.last().type == MarkdownTokenTypes.CODE_FENCE_END) {
childrenToConsider = childrenToConsider.subList(0, childrenToConsider.size - 1)
}
val contents = StringBuilder()
var language: String? = null
for (child in childrenToConsider) {
if (state == 1 && child.type in listOf(MarkdownTokenTypes.CODE_FENCE_CONTENT,
MarkdownTokenTypes.EOL)) {
contents.append(HtmlGenerator.trimIndents(child.getTextInNode(text), indentBefore))
}
if (state == 0 && child.type == MarkdownTokenTypes.FENCE_LANG) {
language = HtmlGenerator.leafText(text, child).toString().trim().split(' ')[0]
}
if (state == 0 && child.type == MarkdownTokenTypes.EOL) {
state = 1
}
}
val trimmedContents = contents.toString().trim('\n', '\r')
val coloredCode =
language
?.toIdeLanguage()
?.let {
HtmlSyntaxInfoUtil.getHtmlContent(
PsiFileFactory.getInstance(DefaultProjectFactory.getInstance().defaultProject)
.createFileFromText(it, trimmedContents),
trimmedContents,
null,
EditorColorsManager.getInstance().globalScheme,
0,
trimmedContents.length
)
}
?.toString()
?: EntityConverter.replaceEntities(contents, false, false)
visitor.consumeHtml(wrapWithCodeBackground(coloredCode, true))
}
private fun String.toIdeLanguage(): Language? =
Language.findInstancesByMimeType(this)
.asSequence()
.plus(Language.findInstancesByMimeType("text/$this"))
.plus(
Language.getRegisteredLanguages()
.asSequence()
.filter { languageMatches(this, it) }
)
.firstOrNull()
private fun languageMatches(langType: String, language: Language): Boolean =
langType.equals(language.id, ignoreCase = true)
|| FileTypeManager.getInstance().getFileTypeByExtension(langType) === language.associatedFileType
}
internal class DocCodeSpanGeneratingProvider : 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.consumeHtml(wrapWithCodeBackground(output, false))
}
}
private fun wrapWithCodeBackground(codeInnerHtml: String, isBlock: Boolean): String {
val globalScheme = EditorColorsManager.getInstance().globalScheme
val codeBg = getCodeFragmentBg()
val codeBgHex = "#${UIUtil.colorToHex(codeBg)}"
val codeFgHex = "#${UIUtil.colorToHex(globalScheme.defaultForeground)}"
val fontFamily = globalScheme.editorFontName
val fontSizePercent = Math.round(100.0 * (FontPreferences.DEFAULT_FONT_SIZE - 1) / FontPreferences.DEFAULT_FONT_SIZE).toInt()
return if (isBlock)
"<div style=\"background-color: $codeBgHex; color: $codeFgHex; margin: 5px 0px 5px 10px; padding: 6px; font-family: $fontFamily; font-size: $fontSizePercent%;\">" +
"<pre style=\"padding: 0px; margin: 0px\">${codeInnerHtml}</pre>" +
"</div>"
else
"<span style=\"background-color: $codeBgHex; color: $codeFgHex; font-family: $fontFamily; font-size: $fontSizePercent%; padding: 1px 4px; margin: 1px 0px;\">$codeInnerHtml</span>"
}
private fun getCodeFragmentBg(): Color {
// Documentation renders with ToolTipActionBackground color,
// so it's best to use the same color
val actionBg = JBUI.CurrentTheme.Editor.Tooltip.BACKGROUND
val tooltipBg = UIUtil.getToolTipActionBackground()
val tooltipLuminance = ColorUtil.getLuminance(tooltipBg)
val actionLuminance = ColorUtil.getLuminance(actionBg)
val diff = tooltipLuminance - actionLuminance
if (tooltipLuminance > 0.5) {
if (abs(diff) < 0.1) {
if (diff < 0) {
return ColorUtil.hackBrightness(actionBg, 1, 0.95f)
}
else {
return ColorUtil.hackBrightness(actionBg, 1, 1.02f)
}
}
}
return actionBg
}