IDEA-347679 support for converting <pre><code> and <blockquote><pre> to styled code blocks (in rendered mode)

GitOrigin-RevId: b94b84dd4137f354e1bf32d581b9f833a9858185
This commit is contained in:
Piotr Tomiak
2024-03-01 13:21:42 +01:00
committed by intellij-monorepo-bot
parent 12b3e35613
commit 9b5c821b7e
6 changed files with 240 additions and 105 deletions

View File

@@ -0,0 +1,3 @@
<html><head><base href="placeholder"></head><body><div class='definition-separated'><pre><span style="color:#000080;font-weight:bold;">class</span> <span style="color:#000000;">Main</span></pre></div><div class='content'>
<p>Test <div class='styled-code'><pre style="padding: 0px; margin: 0px"> foo</pre></div>
</div>

View File

@@ -0,0 +1,9 @@
/**
* Test <blockquote>
* <pre> foo
* </pre>
* </blockquote>
*/
class Ma<caret>in {
}

View File

@@ -128,6 +128,7 @@ public class JavaDocInfoGeneratorTest extends JavaCodeInsightTestCase {
public void testInlineTagIndex() { useJava9(); doTestClass(); }
public void testInlineTagSummary() { useJava10(); doTestClass(); }
public void testLeadingSpacesInPre() { doTestClass(); }
public void testBlockquotePre() { doTestAtCaret(); }
public void testPreInDeprecated() { doTestClass(); }
public void testEscapeHtmlInCode() { doTestClass(); }
public void testEscapeAngleBracketsInCode() { doTestClass(); }

View File

@@ -2,6 +2,7 @@
package com.intellij.codeInsight.documentation
import com.intellij.lang.documentation.DocumentationMarkup.*
import com.intellij.lang.documentation.QuickDocHighlightingHelper
import com.intellij.lang.documentation.QuickDocHighlightingHelper.getDefaultDocCodeStyles
import com.intellij.lang.documentation.QuickDocHighlightingHelper.getDefaultFormattingStyles
import com.intellij.openapi.editor.colors.EditorColorsManager
@@ -9,6 +10,7 @@ import com.intellij.openapi.module.ModuleTypeManager
import com.intellij.openapi.module.UnknownModuleType
import com.intellij.ui.ColorUtil
import com.intellij.ui.scale.JBUIScale.scale
import com.intellij.util.containers.CollectionFactory
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.ui.ExtendableHTMLViewFactory
import com.intellij.util.ui.ExtendableHTMLViewFactory.Extensions.icons
@@ -119,14 +121,40 @@ object DocumentationHtmlUtil {
return result
}
private val dropPrecedingEmptyParagraphTags = CollectionFactory.createCharSequenceSet(false).also {
it.addAll(listOf("ul", "ol", "h1", "h2", "h3", "h4", "h5", "h6", "p", "tr", "td"))
}
/**
* This method allows preprocessing HTML code before feeding the DocumentationEditorPane with it.
*
* It performs some string transformations required to work around the limitations of the HTML support implementation.
*/
@JvmStatic
@Contract(pure = true)
fun addWordBreaks(text: String): String {
val codePoints = text.codePoints().iterator()
if (!codePoints.hasNext()) return ""
val result = StringBuilder(text.length + 50)
var codePoint = codePoints.nextInt()
val tagName = StringBuilder()
fun transpileForHtmlEditorPaneInput(text: String): String =
HtmlEditorPaneInputTranspiler(text).process()
/**
* Transpiler performs some simple lexing to understand where are tags, attributes and text.
*
* For performance reasons, all the following actions are applied in a single run:
* - Add `<wbr>` after `.` if surrounded by letters
* - Add `<wbr>` after `]`, `)` or `/` followed by a char or digit
* - Remove empty <p> before some tags - workaround for Swing html renderer not removing empty paragraphs before non-inline tags
* - Replace `<blockquote>\\s*<pre>` with [QuickDocHighlightingHelper.CODE_BLOCK_PREFIX]
* - Replace `</pre>\\s*</blockquote>` with [QuickDocHighlightingHelper.CODE_BLOCK_SUFFIX]
* - Replace `<pre><code>` with [QuickDocHighlightingHelper.CODE_BLOCK_PREFIX]
* - Replace `</code></pre>` with [QuickDocHighlightingHelper.CODE_BLOCK_SUFFIX]
*/
private class HtmlEditorPaneInputTranspiler(text: String) {
private val codePoints = text.codePoints().iterator()
private val result = StringBuilder(text.length + 50)
private var codePoint = codePoints.nextInt()
private var openingTag = false
private val tagStart = StringBuilder()
private val tagName = StringBuilder()
private val tagBuffer = StringBuilder()
fun next(builder: StringBuilder = result) {
builder.appendCodePoint(codePoint)
@@ -136,97 +164,200 @@ object DocumentationHtmlUtil {
-1
}
while (codePoint >= 0) {
// break after dot if surrounded by letters
when {
Character.isLetter(codePoint) -> {
next()
if (codePoint == '.'.code) {
next()
if (Character.isLetter(codePoint)) {
result.append("<wbr>")
}
}
fun readTagStart() {
assert(codePoint == '<'.code)
tagStart.clear()
tagName.clear()
next(tagStart)
if (codePoint == '/'.code) {
openingTag = false
next(tagStart)
}
else if (codePoint == '!'.code) {
next(tagStart)
if (consume("--", tagStart)) {
skipUntil("->", tagStart)
}
// break after ], ) or / followed by a char or digit
codePoint == ')'.code || codePoint == ']'.code || codePoint == '/'.code -> {
next()
if (Character.isLetterOrDigit(codePoint)) {
result.append("<wbr>")
}
else {
openingTag = true
}
if (!Character.isLetter(codePoint))
return
while (Character.isLetterOrDigit(codePoint) || codePoint == '-'.code) {
next(tagName)
}
tagStart.append(tagName)
}
fun consume(text: String, builder: StringBuilder): Boolean {
for (c in text) {
if (codePoint != c.code) return false
next(builder)
}
return true
}
fun skipUntil(text: String, builder: StringBuilder) {
loop@ while (codePoint >= 0) {
for (i in text.indices) {
val c = text[i]
if (codePoint != c.code) {
if (i == 0) next(builder)
continue@loop
}
next(builder)
}
// skip tag
codePoint == '<'.code -> {
next()
if (codePoint == '/'.code)
next()
if (!Character.isLetter(codePoint))
continue
tagName.clear()
while (Character.isLetterOrDigit(codePoint) || codePoint == '-'.code) {
next(tagName)
}
result.append(tagName)
if (tagName.contentEquals("style", true)
|| tagName.contentEquals("title", true)
|| tagName.contentEquals("script", true)) {
val curTag = tagName.toString()
do {
if (codePoint == '<'.code) {
next()
if (codePoint == '/'.code) {
next()
tagName.clear()
while (Character.isLetterOrDigit(codePoint) || codePoint == '-'.code) {
next(tagName)
}
result.append(tagName)
if (tagName.contentEquals(curTag, true)) {
while (codePoint >= 0 && codePoint != '>'.code) {
next()
}
break
}
}
}
else next()
}
while (true)
}
else {
while (codePoint >= 0) {
when (codePoint) {
'>'.code -> {
next()
break
}
'\''.code, '"'.code -> {
val quoteStyle = codePoint
next()
while (codePoint >= 0) {
when (codePoint) {
'\\'.code -> {
next()
if (codePoint >= 0)
next()
}
quoteStyle -> {
next()
break
}
else -> next()
}
}
}
else -> next()
}
}
}
}
else -> next()
return
}
}
return result.toString()
fun skipToTagEnd(builder: StringBuilder) {
while (codePoint >= 0) {
when (codePoint) {
'>'.code -> {
next(builder)
break
}
'\''.code, '"'.code -> {
val quoteStyle = codePoint
next(builder)
while (codePoint >= 0) {
when (codePoint) {
'\\'.code -> {
next(builder)
if (codePoint >= 0)
next(builder)
}
quoteStyle -> {
next(builder)
break
}
else -> next(builder)
}
}
}
else -> next(builder)
}
}
}
fun handleTag() {
val isP = tagName.contentEquals("p", true)
val isPre = tagName.contentEquals("pre", true)
val isCode = tagName.contentEquals("code", true)
val isBlockquote = tagName.contentEquals("blockquote", true)
val isOpeningTag = openingTag
if (!isP && !isPre && !(isCode && !isOpeningTag) && !(isBlockquote && isOpeningTag)) {
result.append(tagStart)
skipToTagEnd(result)
return
}
tagBuffer.clear()
tagBuffer.append(tagStart)
skipToTagEnd(tagBuffer)
if (isP || isBlockquote || (isPre && !isOpeningTag)) {
// Skip whitespace
while (codePoint >= 0 && Character.isWhitespace(codePoint)) {
next(tagBuffer)
}
}
if (codePoint == '<'.code) {
readTagStart()
if (isP) {
// Remove empty <p> before some tags - workaround for Swing html renderer not removing empty paragraphs before non-inline tags
if (tagName !in dropPrecedingEmptyParagraphTags) {
result.append(tagBuffer)
}
handleTag()
}
else {
// Replace <blockquote>\\s*<pre> with QuickDocHighlightingHelper.CODE_BLOCK_PREFIX
// Replace </pre>\\s*</blockquote> with QuickDocHighlightingHelper.CODE_BLOCK_SUFFIX
// Replace <pre><code> with QuickDocHighlightingHelper.CODE_BLOCK_PREFIX
// Replace </code></pre> with QuickDocHighlightingHelper.CODE_BLOCK_SUFFIX
val nextTag = if (isPre) {
if (isOpeningTag) "code" else "blockquote"
}
else "pre"
if (tagName.contentEquals(nextTag, true) && (isOpeningTag == openingTag)) {
skipToTagEnd(tagBuffer)
if (isCode || (isPre && !isOpeningTag)) {
// trim trailing whitespace
result.setLength(result.indexOfLast { !Character.isWhitespace(it) } + 1)
}
result.append(if (isOpeningTag) QuickDocHighlightingHelper.CODE_BLOCK_PREFIX else QuickDocHighlightingHelper.CODE_BLOCK_SUFFIX)
}
else {
result.append(tagBuffer)
handleTag()
}
}
}
else {
result.append(tagBuffer)
}
}
fun process(): String {
if (!codePoints.hasNext()) return ""
while (codePoint >= 0) {
when {
// break after dot if surrounded by letters
Character.isLetter(codePoint) -> {
next()
if (codePoint == '.'.code) {
next()
if (Character.isLetter(codePoint)) {
result.append("<wbr>")
}
}
}
// break after ], ) or / followed by a char or digit
codePoint == ')'.code || codePoint == ']'.code || codePoint == '/'.code -> {
next()
if (Character.isLetterOrDigit(codePoint)) {
result.append("<wbr>")
}
}
// process tags
codePoint == '<'.code -> {
readTagStart()
if (tagName.isEmpty()) {
result.append(tagStart)
continue
}
if (tagName.contentEquals("style", true)
|| tagName.contentEquals("title", true)
|| tagName.contentEquals("script", true)
|| tagName.contentEquals("textarea", true)) {
result.append(tagStart)
val curTag = tagName.toString()
do {
if (codePoint == '<'.code) {
readTagStart()
result.append(tagStart)
if (tagName.contentEquals(curTag, true) && !openingTag) {
skipUntil(">", result)
break
}
}
else next()
}
while (codePoint >= 0)
}
else
handleTag()
}
else -> next()
}
}
return result.toString()
}
}
}

View File

@@ -1970,22 +1970,10 @@ public class DocumentationManager extends DockablePopupManager<DocumentationComp
if (links != null) {
text += getBottom().child(links);
}
//workaround for Swing html renderer not removing empty paragraphs before non-inline tags
text = PARAGRAPH_BEFORE_NON_INLINE_TAG_PATTERN.matcher(text).replaceAll("$1");
text = addExternalLinksIcon(text);
//convert <blockquote><pre> and <pre><code> into code blocks
text = BLOCKQUOTE_PRE_CODE_OPEN_PATTERN.matcher(text).replaceAll(QuickDocHighlightingHelper.CODE_BLOCK_PREFIX);
text = BLOCKQUOTE_PRE_CODE_CLOSE_PATTERN.matcher(text).replaceAll(QuickDocHighlightingHelper.CODE_BLOCK_SUFFIX);
text = DocumentationHtmlUtil.addWordBreaks(text);
return text;
return DocumentationHtmlUtil.transpileForHtmlEditorPaneInput(text);
}
private static final Pattern PARAGRAPH_BEFORE_NON_INLINE_TAG_PATTERN = Pattern.compile("<p>\\s*(<(?:[uo]l|h\\d|p|tr|td))");
private static final Pattern BLOCKQUOTE_PRE_CODE_OPEN_PATTERN =
Pattern.compile("<blockquote>[\\s\\n\\r]*<pre>|<pre><code>", Pattern.MULTILINE);
private static final Pattern BLOCKQUOTE_PRE_CODE_CLOSE_PATTERN =
Pattern.compile("</pre>[\\s\\n\\r]*</blockquote>|</code></pre>", Pattern.MULTILINE);
private static int indexOfIgnoreQuotesType(@NotNull String text, @NotNull String substring) {
return StreamEx.of(
text.indexOf(substring),

View File

@@ -5,6 +5,7 @@ import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.documentation.DocFontSizePopup;
import com.intellij.codeInsight.documentation.DocumentationActionProvider;
import com.intellij.codeInsight.documentation.DocumentationFontSize;
import com.intellij.codeInsight.documentation.DocumentationHtmlUtil;
import com.intellij.icons.AllIcons;
import com.intellij.ide.ui.UISettings;
import com.intellij.lang.documentation.QuickDocHighlightingHelper;
@@ -306,6 +307,8 @@ public final class DocRenderer implements CustomFoldRegionRenderer {
text = unwrapTipsText(text);
text = ShortcutExtension.Companion.patchShortcutTags(text, false);
useTipsKit = true;
} else {
text = DocumentationHtmlUtil.transpileForHtmlEditorPaneInput(text);
}
EditorPane pane = new EditorPane(!reusable);
pane.setEditable(false);