[JEWEL-1345] Improve Markdown block renderer API

Keep parseEmbeddedHtml=false on the HtmlBlock no-op path (do not wrap
raw HTML in HtmlBlockWithAttributes/Paragraph), restore matching test
helpers, and strengthen the renderer regression coverage.

closes https://github.com/JetBrains/intellij-community/pull/3547

(cherry picked from commit 434b713d0ba2d1608f69cf90ce77a0c6b570ba51)


(cherry picked from commit 059c7ff3bc277a53d571c0a2977f8732996d969d)

IJ-MR-220560

GitOrigin-RevId: 5599220b7c3654143a1c4b66139832ec6c687dc6
This commit is contained in:
Sebastiano Poggi
2026-09-07 22:29:09 +00:00
committed by intellij-monorepo-bot
parent 0ae337cc9f
commit fdf4e18a78
10 changed files with 133 additions and 34 deletions
@@ -386,6 +386,7 @@ f:org.jetbrains.jewel.markdown.processing.ProcessingUtilKt
- RenderHeading(org.jetbrains.jewel.markdown.MarkdownBlock$Heading,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Heading$HN,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- RenderHeading(org.jetbrains.jewel.markdown.MarkdownBlock$Heading,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Heading,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- RenderHtmlBlock(org.jetbrains.jewel.markdown.MarkdownBlock$HtmlBlock,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$HtmlBlock,Z,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- RenderHtmlBlockWithAttributes(org.jetbrains.jewel.markdown.MarkdownBlock$HtmlBlockWithAttributes,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- RenderIndentedCodeBlock(org.jetbrains.jewel.markdown.MarkdownBlock$CodeBlock$IndentedCodeBlock,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Indented,Z,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- RenderList(org.jetbrains.jewel.markdown.MarkdownBlock$ListBlock,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$List,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- RenderListItem(org.jetbrains.jewel.markdown.MarkdownBlock$ListItem,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
@@ -399,6 +400,7 @@ f:org.jetbrains.jewel.markdown.processing.ProcessingUtilKt
- getRendererExtensions():java.util.List
- getRootStyling():org.jetbrains.jewel.markdown.rendering.MarkdownStyling
- *:plus(org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension):org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
- p:resolveImages(org.jetbrains.jewel.markdown.WithInlineMarkdown,androidx.compose.runtime.Composer,I):kotlin.Pair
*f:org.jetbrains.jewel.markdown.rendering.DefaultMarkdownBlockRenderer$Companion
*:org.jetbrains.jewel.markdown.rendering.ImageSourceResolver
- *sf:Companion:org.jetbrains.jewel.markdown.rendering.ImageSourceResolver$Companion
@@ -469,6 +471,10 @@ f:org.jetbrains.jewel.markdown.rendering.InlineMarkdownRendererKt
- a:RenderHeading(org.jetbrains.jewel.markdown.MarkdownBlock$Heading,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Heading$HN,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- a:RenderHeading(org.jetbrains.jewel.markdown.MarkdownBlock$Heading,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Heading,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- a:RenderHtmlBlock(org.jetbrains.jewel.markdown.MarkdownBlock$HtmlBlock,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$HtmlBlock,Z,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- RenderHtmlBlockWithAttributes(org.jetbrains.jewel.markdown.MarkdownBlock$HtmlBlockWithAttributes,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- b:RenderHtmlBlockWithAttributes(org.jetbrains.jewel.markdown.MarkdownBlock$HtmlBlockWithAttributes,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I,I):V
- RenderImagesOnlyParagraph(java.util.Map,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- b:RenderImagesOnlyParagraph(java.util.Map,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I,I):V
- a:RenderIndentedCodeBlock(org.jetbrains.jewel.markdown.MarkdownBlock$CodeBlock$IndentedCodeBlock,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Indented,Z,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- a:RenderList(org.jetbrains.jewel.markdown.MarkdownBlock$ListBlock,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$List,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
- a:RenderListItem(org.jetbrains.jewel.markdown.MarkdownBlock$ListItem,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I):V
@@ -484,6 +490,9 @@ f:org.jetbrains.jewel.markdown.rendering.InlineMarkdownRendererKt
- a:getRootStyling():org.jetbrains.jewel.markdown.rendering.MarkdownStyling
- *a:plus(org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension):org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
*f:org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer$Companion
*f:org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer$ComposeDefaultImpls
- sf:RenderHtmlBlockWithAttributes$default(org.jetbrains.jewel.markdown.MarkdownBlock$HtmlBlockWithAttributes,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
- sf:RenderImagesOnlyParagraph$default(java.util.Map,androidx.compose.ui.Modifier,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
*f:org.jetbrains.jewel.markdown.rendering.MarkdownStyling
- sf:$stable:I
- *sf:Companion:org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Companion
@@ -468,6 +468,7 @@ package org.jetbrains.jewel.markdown.rendering {
method public java.util.List<org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension> getRendererExtensions();
method public org.jetbrains.jewel.markdown.rendering.MarkdownStyling getRootStyling();
method @SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public operator org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer plus(org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension extension);
method @androidx.compose.runtime.Composable protected kotlin.Pair<java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent>,java.util.Set<java.lang.String>> resolveImages(org.jetbrains.jewel.markdown.WithInlineMarkdown blockInlineContent);
property public org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer inlineRenderer;
property public java.util.List<org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension> rendererExtensions;
property public org.jetbrains.jewel.markdown.rendering.MarkdownStyling rootStyling;
@@ -475,6 +476,8 @@ package org.jetbrains.jewel.markdown.rendering {
}
public static final class DefaultMarkdownBlockRenderer.Companion {
method protected androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign> getLocalTextAlignment();
property protected androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign> LocalTextAlignment;
}
@SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public interface ImageSourceResolver {
@@ -570,6 +573,8 @@ package org.jetbrains.jewel.markdown.rendering {
method @androidx.compose.runtime.Composable public void RenderHeading(org.jetbrains.jewel.markdown.MarkdownBlock.Heading block, org.jetbrains.jewel.markdown.rendering.MarkdownStyling.Heading styling, boolean enabled, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, androidx.compose.ui.Modifier modifier);
method @androidx.compose.runtime.Composable public void RenderHeading(org.jetbrains.jewel.markdown.MarkdownBlock.Heading block, org.jetbrains.jewel.markdown.rendering.MarkdownStyling.Heading.HN styling, boolean enabled, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, androidx.compose.ui.Modifier modifier);
method @androidx.compose.runtime.Composable public void RenderHtmlBlock(org.jetbrains.jewel.markdown.MarkdownBlock.HtmlBlock block, org.jetbrains.jewel.markdown.rendering.MarkdownStyling.HtmlBlock styling, boolean enabled, androidx.compose.ui.Modifier modifier);
method @androidx.compose.runtime.Composable public default void RenderHtmlBlockWithAttributes(org.jetbrains.jewel.markdown.MarkdownBlock.HtmlBlockWithAttributes block, boolean enabled, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional androidx.compose.ui.Modifier modifier);
method @androidx.compose.runtime.Composable public default void RenderImagesOnlyParagraph(java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> images, optional androidx.compose.ui.Modifier modifier);
method @androidx.compose.runtime.Composable public void RenderIndentedCodeBlock(org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock.IndentedCodeBlock block, org.jetbrains.jewel.markdown.rendering.MarkdownStyling.Code.Indented styling, boolean enabled, androidx.compose.ui.Modifier modifier);
method @androidx.compose.runtime.Composable public void RenderList(org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock block, org.jetbrains.jewel.markdown.rendering.MarkdownStyling.List styling, boolean enabled, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, androidx.compose.ui.Modifier modifier);
method @androidx.compose.runtime.Composable public void RenderListItem(org.jetbrains.jewel.markdown.MarkdownBlock.ListItem block, boolean enabled, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, androidx.compose.ui.Modifier modifier);
@@ -60,7 +60,8 @@ import org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
* provided by the [MarkdownParserFactory], but you can provide your own if you need to customize the parser — e.g.,
* to ignore certain tags. If [markdownMode] is `MarkdownMode.WithEditor`, make sure you set
* `includeSourceSpans(IncludeSourceSpans.BLOCKS)` on the parser.
* @param parseEmbeddedHtml If `true`, a subset of native HTML elements will be parsed as Markdown blocks.
* @param parseEmbeddedHtml If `true`, a subset of native HTML elements will be parsed as Markdown blocks. See
* [MarkdownHtmlConverter].
*/
@ApiStatus.Experimental
@ExperimentalJewelApi
@@ -71,7 +72,7 @@ public class MarkdownProcessor(
MarkdownParserFactory.create(optimizeEdits = markdownMode is MarkdownMode.EditorPreview, extensions),
private val parseEmbeddedHtml: Boolean = false,
) {
@Suppress("UnusedPrivateProperty") // languageRecognizer is only here for binary compat reasons
@Suppress("UnusedPrivateProperty", "DEPRECATION") // languageRecognizer is only here for binary compat reasons
@Deprecated(
"`languageRecognizer` is not necessary anymore. Use the constructor without it.",
replaceWith = ReplaceWith("MarkdownProcessor(extensions, markdownMode, commonMarkParser)"),
@@ -85,6 +86,7 @@ public class MarkdownProcessor(
parseEmbeddedHtml: Boolean = false,
) : this(extensions, markdownMode, commonMarkParser, parseEmbeddedHtml)
@Suppress("DEPRECATION")
@Deprecated("Use a version with a `parseEmbeddedHtml` parameter", level = DeprecationLevel.HIDDEN)
public constructor(
extensions: List<MarkdownProcessorExtension> = emptyList(),
@@ -94,6 +96,7 @@ public class MarkdownProcessor(
languageRecognizer: (String) -> MimeType? = { MimeType.Known.fromMarkdownLanguageName(it) },
) : this(extensions, markdownMode, commonMarkParser, languageRecognizer, false)
@Suppress("DEPRECATION")
@Deprecated("Use a version with a `parseEmbeddedHtml` parameter", level = DeprecationLevel.HIDDEN)
public constructor(
extensions: List<MarkdownProcessorExtension> = emptyList(),
@@ -351,7 +354,7 @@ public class MarkdownProcessor(
intermediateHtmlBlocks.mapNotNull { htmlElement ->
htmlConverter.convert(this@MarkdownProcessor, htmlElement) { newMdBlock, lines ->
when (newMdBlock) {
is MarkdownBlock.HtmlBlock -> return@convert newMdBlock
is MarkdownBlock.HtmlBlockWithAttributes -> return@convert newMdBlock
is MarkdownBlock.ListItem ->
return@convert newMdBlock // we convert the list item's inner block instead
else -> {
@@ -370,7 +373,7 @@ public class MarkdownProcessor(
MarkdownBlock.BlockQuote(processChildren(this))
private fun Heading.toMarkdownHeadingOrNull(): MarkdownBlock.Heading? {
if (level < 1 || level > 6) return null
if (level !in 1..6) return null
return MarkdownBlock.Heading(inlineContent = readInlineMarkdown(this@MarkdownProcessor), level = level)
}
@@ -380,6 +383,11 @@ public class MarkdownProcessor(
private fun IndentedCodeBlock.toMarkdownCodeBlockOrNull(): CodeBlock.IndentedCodeBlock =
CodeBlock.IndentedCodeBlock(literal.trimEnd('\n'))
private fun HtmlBlock.toMarkdownHtmlBlockOrNull(): MarkdownBlock.HtmlBlock? {
if (literal.isBlank()) return null
return MarkdownBlock.HtmlBlock(literal.trimEnd('\n'))
}
private fun BulletList.toMarkdownListOrNull(): ListBlock.UnorderedList? {
val children = processListItems()
if (children.isEmpty()) return null
@@ -443,11 +451,6 @@ public class MarkdownProcessor(
forEachChild { child -> child.traverseAll(action) }
}
private fun HtmlBlock.toMarkdownHtmlBlockOrNull(): MarkdownBlock.HtmlBlock? {
if (literal.isBlank()) return null
return MarkdownBlock.HtmlBlock(literal.trimEnd('\n'))
}
/** Creates a copy of this [MarkdownProcessor] with the same properties, plus the provided [extension]. */
@ApiStatus.Experimental
@ExperimentalJewelApi
@@ -17,7 +17,6 @@ import org.commonmark.node.Text as CMText
import org.commonmark.parser.beta.ParsedInline
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.util.JewelLogger
import org.jetbrains.jewel.markdown.DimensionSize
import org.jetbrains.jewel.markdown.InlineMarkdown
import org.jetbrains.jewel.markdown.WithInlineMarkdown
@@ -153,10 +152,6 @@ internal fun List<InlineMarkdown>.renderAsSimpleText(): String = buildString {
is WithTextContent -> append(node.content)
is InlineMarkdown.HardLineBreak -> append('\n')
is InlineMarkdown.SoftLineBreak -> append(' ')
else -> {
JewelLogger.getInstance("MarkdownProcessingUtil")
.debug("Ignoring node ${node.javaClass.simpleName} for text rendering")
}
}
}
}
@@ -10,7 +10,7 @@ import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
import org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
/**
* Defines a mechanism for converting HTML elements their child elements, and inline elements into supported Markdown
* Defines a mechanism for converting HTML elements, their child elements, and inline elements into supported Markdown
* elements (blocks or inlines).
*/
@ApiStatus.Experimental
@@ -76,7 +76,7 @@ internal class MarkdownHtmlConverter {
) ?: return null
val transformedBlock = transform(convertedBlock, htmlElement.lineRange)
return if (htmlElement.attributes.isEmpty()) {
if (htmlElement.attributes.isEmpty()) {
transformedBlock
} else {
transform(
@@ -92,11 +92,11 @@ internal class MarkdownHtmlConverter {
processor: MarkdownProcessor,
transform: (MarkdownBlock, IntRange) -> MarkdownBlock,
): List<MarkdownBlock> {
// Some HTML elements may make up a block (e.g. nested list), but some can be pure inlines.
// Some HTML elements may make up a block (e.g., nested list), but some can be pure inlines.
// For example, `<li><p>Hello</p></li>` is a block inside ListItem, but `<li><b>Hello<b></li>` is not.
// What's worse, `<li>Pay attention to <b>this</b>:<ol>...</ol></li>` combines both inlines and blocks.
// The idea here is to split the children into sublists of elements convertible to inline lists,
// separated by markdown blocks.
// separated by Markdown blocks.
val blocks = mutableListOf<MarkdownBlock>()
val currentInlineHtmlElements = mutableListOf<MarkdownHtmlNode>()
var currentLine = lineRange.first
@@ -88,6 +88,7 @@ import org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock.IndentedCodeBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.Heading
import org.jetbrains.jewel.markdown.MarkdownBlock.HtmlBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.HtmlBlockWithAttributes
import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock.OrderedList
import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock.UnorderedList
@@ -126,6 +127,8 @@ public open class DefaultMarkdownBlockRenderer(
override val rendererExtensions: List<MarkdownRendererExtension> = emptyList(),
override val inlineRenderer: InlineMarkdownRenderer = InlineMarkdownRenderer.create(rendererExtensions),
) : MarkdownBlockRenderer {
private val unsupportedBlockTypes = mutableSetOf<String>()
@Composable
override fun RenderBlocks(
blocks: List<MarkdownBlock>,
@@ -153,14 +156,12 @@ public open class DefaultMarkdownBlockRenderer(
is ListItem -> RenderListItem(block, enabled, onUrlClick, modifier)
is Paragraph -> RenderParagraph(block, rootStyling.paragraph, enabled, onUrlClick, modifier)
ThematicBreak -> RenderThematicBreak(rootStyling.thematicBreak, enabled, modifier)
is MarkdownBlock.HtmlBlockWithAttributes ->
RenderHtmlBlockWithAttributes(block, enabled, onUrlClick, modifier)
is HtmlBlockWithAttributes -> RenderHtmlBlockWithAttributes(block, enabled, onUrlClick, modifier)
is CustomBlock -> {
rendererExtensions
.find { it.blockRenderer?.canRender(block) == true }
?.blockRenderer
?.RenderCustomBlock(
val blockRenderer =
rendererExtensions.find { it.blockRenderer?.canRender(block) == true }?.blockRenderer
if (blockRenderer != null) {
blockRenderer.RenderCustomBlock(
block = block,
blockRenderer = this,
inlineRenderer = inlineRenderer,
@@ -168,10 +169,22 @@ public open class DefaultMarkdownBlockRenderer(
modifier = modifier,
onUrlClick = onUrlClick,
)
} else {
logUnsupportedBlock(block)
}
}
}
}
private fun logUnsupportedBlock(block: MarkdownBlock) {
// We do not render unsupported blocks; emit a one-off log per renderer instance (best-effort).
val simpleName = block::class.simpleName
if (simpleName != null && simpleName !in unsupportedBlockTypes) {
JewelLogger.getInstance(javaClass).warn("Cannot render unsupported block type: $simpleName.")
unsupportedBlockTypes += simpleName
}
}
@Composable
override fun RenderParagraph(
block: Paragraph,
@@ -660,7 +673,7 @@ public open class DefaultMarkdownBlockRenderer(
}
@Composable
private fun resolveImages(blockInlineContent: WithInlineMarkdown): ResolvedImages {
protected open fun resolveImages(blockInlineContent: WithInlineMarkdown): ResolvedImages {
val map = remember(blockInlineContent) { mutableStateMapOf<String, InlineTextContent>() }
val failedSources = remember(blockInlineContent) { mutableStateSetOf<String>() }
val imagesRenderer = rendererExtensions.firstNotNullOfOrNull { it.imageRendererExtension }
@@ -705,8 +718,8 @@ public open class DefaultMarkdownBlockRenderer(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
// We use movableContent so changing the flag doesn't reset the content
val movableContent = remember { movableContentOf { content() } }
// We use movableContent, so changing the flag doesn't reset the content
val movableContent = remember(content) { movableContentOf { content() } }
if (isScrollable) {
HorizontallyScrollableContainer(modifier) { movableContent() }
} else {
@@ -715,12 +728,21 @@ public open class DefaultMarkdownBlockRenderer(
}
@Composable
private fun RenderHtmlBlockWithAttributes(
block: MarkdownBlock.HtmlBlockWithAttributes,
override fun RenderHtmlBlockWithAttributes(
block: HtmlBlockWithAttributes,
enabled: Boolean,
onUrlClick: (String) -> Unit,
modifier: Modifier = Modifier,
modifier: Modifier,
) {
val mdBlock = block.mdBlock
if (
block.attributes.isEmpty() &&
mdBlock is Paragraph &&
mdBlock.inlineContent.singleOrNull() is InlineMarkdown.HtmlInline
) {
return
}
val textAlignment: TextAlign =
when (block.attributes["align"]) {
"left" -> TextAlign.Start
@@ -1052,8 +1074,11 @@ public open class DefaultMarkdownBlockRenderer(
DefaultMarkdownBlockRenderer(rootStyling, rendererExtensions = rendererExtensions + extension, inlineRenderer)
public companion object {
@Suppress("VariableNaming")
private val LocalTextAlignment: ProvidableCompositionLocal<TextAlign> = staticCompositionLocalOf {
/**
* Holds the current text alignment for the subtree of the composition. Used by the HTML parsing path to
* transmit text alignment down the tree.
*/
protected val LocalTextAlignment: ProvidableCompositionLocal<TextAlign> = staticCompositionLocalOf {
TextAlign.Start
}
}
@@ -1,5 +1,6 @@
package org.jetbrains.jewel.markdown.rendering
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -315,6 +316,38 @@ public interface MarkdownBlockRenderer {
modifier: Modifier,
)
/**
* Renders a [MarkdownBlock.HtmlBlockWithAttributes] into a Compose UI. Raw HTML blocks may be exposed as
* [MarkdownBlock.HtmlBlockWithAttributes] wrapping a paragraph with raw HTML inline content. Supported parsed HTML
* is converted into Markdown blocks and wrapped with attributes when applicable.
*
* @param block The HTML block to render.
* @param enabled True if the block should be enabled, false otherwise.
* @param onUrlClick The callback invoked when a link is clicked.
* @param modifier The modifier to be applied to the composable.
*/
@Composable
public fun RenderHtmlBlockWithAttributes(
block: MarkdownBlock.HtmlBlockWithAttributes,
enabled: Boolean,
onUrlClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
// No-op by default
}
/**
* Renders a paragraph containing only images into a Compose UI. This is delegated to from [RenderParagraph] when it
* only contains images, and uses a more efficient path than inline images in a text run.
*
* @param images The images to render.
* @param modifier The modifier to be applied to the composable.
*/
@Composable
public fun RenderImagesOnlyParagraph(images: Map<String, InlineTextContent>, modifier: Modifier = Modifier) {
// No-op by default
}
/**
* Creates a copy of this instance, using the provided non-null parameters, or the current values for the null ones.
*
@@ -111,6 +111,7 @@ public open class ScrollSyncMarkdownBlockRenderer(
}
}
@Suppress("DEPRECATION")
@Deprecated(
message =
"This class function is not scalable as it relies on a pre-resolved MimeType object. " +
@@ -67,13 +67,13 @@ private fun MarkdownBlock.findDifferenceWith(expected: MarkdownBlock, indentSize
return when (this) {
is Paragraph -> diffParagraph(this, expected, indent)
is BlockQuote -> children.findDifferences((expected as BlockQuote).children, indentSize)
is HtmlBlock -> diffHtmlBlock(this, expected, indent)
is FencedCodeBlock -> diffFencedCodeBlock(this, expected, indent)
is IndentedCodeBlock -> diffIndentedCodeBlock(this, expected, indent)
is Heading -> diffHeading(this, expected, indent)
is ListBlock -> diffList(this, expected, indentSize, indent)
is ListItem -> children.findDifferences((expected as ListItem).children, indentSize)
is ThematicBreak -> emptyList() // They can only differ in their node
is HtmlBlock -> diffHtmlBlock(this, expected, indent)
is HtmlBlockWithAttributes -> diffHtmlBlockWithAttributes(this, expected, indent)
else -> error("Unsupported MarkdownBlock: ${this.javaClass.name}")
}
@@ -8,10 +8,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.text.buildAnnotatedString
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
import org.jetbrains.jewel.markdown.testing.MarkdownTestTheme
import org.jetbrains.jewel.markdown.testing.createMarkdownTestStyling
@@ -126,6 +129,31 @@ public class DefaultMarkdownBlockRendererTest {
}
}
@Test
public fun `raw HTML blocks are not rendered by default`() {
runComposeUiTest {
// parseEmbeddedHtml defaults to false: keep producing HtmlBlock (renderer no-op),
// not HtmlBlockWithAttributes wrapping a Paragraph that could leak raw HTML text.
val processor = MarkdownProcessor(parseEmbeddedHtml = false)
val blocks = processor.processMarkdownDocument("<div>Raw HTML</div>\n\nVisible paragraph")
assertEquals(2, blocks.size)
assertTrue(blocks[0] is MarkdownBlock.HtmlBlock)
assertEquals("<div>Raw HTML</div>", (blocks[0] as MarkdownBlock.HtmlBlock).content)
setContent {
MarkdownTestTheme {
val renderer = DefaultMarkdownBlockRenderer(createMarkdownTestStyling(), emptyList())
renderer.RenderBlocks(blocks, enabled = true, onUrlClick = {}, modifier = Modifier)
}
}
onAllNodesWithText("<div>Raw HTML</div>").assertCountEquals(0)
onAllNodesWithText("Raw HTML").assertCountEquals(0)
onNodeWithText("Visible paragraph").assertExists()
}
}
@Test
public fun `appendInlineContent uses expected annotation tag`() {
val annotated = buildAnnotatedString { appendInlineContent("testId") }