mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[JEWEL-1322] Default Markdown block renderer to the composition local
The Markdown composables defaulted their blockRenderer parameter to a bare DefaultMarkdownBlockRenderer with no renderer extensions, ignoring LocalMarkdownBlockRenderer. Any renderer set up via ProvideMarkdownStyling (e.g., with the GFM tables or alerts extensions) was silently dropped, and extension blocks were skipped during rendering. LazyMarkdown already read the composition local; now all three composables behave the same: explicit parameter first, then LocalMarkdownBlockRenderer. Also make the bridge styling isLightTheme helper re-read JBColor brightness on every access instead of caching the value at class-load time, so table stripe and alert colors follow IDE theme switches. closes https://github.com/JetBrains/intellij-community/pull/3591 (cherry picked from commit c539e6f7d4e5fff4f4d3a71ddd6bf45a96c394e3) (cherry picked from commit 19486a12a8ec7e9fddfa8a4738b8a9989bd662ed) IJ-MR-220560 GitOrigin-RevId: f6a060b88ed77660771500beafac6ee8f310df40
This commit is contained in:
committed by
intellij-monorepo-bot
parent
be88372f97
commit
53328934b1
@@ -214,8 +214,11 @@
|
||||
- equals(java.lang.Object):Z
|
||||
- hashCode():I
|
||||
f:org.jetbrains.jewel.markdown.MarkdownKt
|
||||
- *sf:LazyMarkdown(java.util.List,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.lazy.LazyListState,Z,Z,kotlin.jvm.functions.Function1,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
|
||||
- *sf:LazyMarkdown(java.util.List,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.lazy.LazyListState,Z,Z,kotlin.jvm.functions.Function1,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
|
||||
- *sf:Markdown(java.lang.String,androidx.compose.ui.Modifier,Z,Z,kotlinx.coroutines.CoroutineDispatcher,kotlin.jvm.functions.Function1,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
|
||||
- *sf:Markdown(java.lang.String,androidx.compose.ui.Modifier,Z,Z,kotlinx.coroutines.CoroutineDispatcher,kotlin.jvm.functions.Function1,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
|
||||
- *sf:Markdown(java.util.List,java.lang.String,androidx.compose.ui.Modifier,Z,Z,kotlin.jvm.functions.Function1,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
|
||||
- *sf:Markdown(java.util.List,java.lang.String,androidx.compose.ui.Modifier,Z,Z,kotlin.jvm.functions.Function1,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
|
||||
*:org.jetbrains.jewel.markdown.MarkdownMode
|
||||
*f:org.jetbrains.jewel.markdown.MarkdownMode$EditorPreview
|
||||
|
||||
@@ -203,9 +203,12 @@ package org.jetbrains.jewel.markdown {
|
||||
}
|
||||
|
||||
public final class MarkdownKt {
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void LazyMarkdown(java.util.List<? extends org.jetbrains.jewel.markdown.MarkdownBlock> blocks, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.lazy.LazyListState state, optional boolean enabled, optional boolean selectable, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void Markdown(@org.intellij.lang.annotations.Language("Markdown") String markdown, optional androidx.compose.ui.Modifier modifier, optional boolean selectable, optional boolean enabled, optional kotlinx.coroutines.CoroutineDispatcher processingDispatcher, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor processor, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void Markdown(java.util.List<? extends org.jetbrains.jewel.markdown.MarkdownBlock> markdownBlocks, String markdown, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean selectable, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void LazyMarkdown(java.util.List<? extends org.jetbrains.jewel.markdown.MarkdownBlock> blocks, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.lazy.LazyListState state, optional boolean enabled, optional boolean selectable, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
method @Deprecated @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void LazyMarkdown(java.util.List<? extends org.jetbrains.jewel.markdown.MarkdownBlock> blocks, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.lazy.LazyListState state, optional boolean enabled, optional boolean selectable, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void Markdown(@org.intellij.lang.annotations.Language("Markdown") String markdown, optional androidx.compose.ui.Modifier modifier, optional boolean selectable, optional boolean enabled, optional kotlinx.coroutines.CoroutineDispatcher processingDispatcher, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor processor, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
method @Deprecated @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void Markdown(@org.intellij.lang.annotations.Language("Markdown") String markdown, optional androidx.compose.ui.Modifier modifier, optional boolean selectable, optional boolean enabled, optional kotlinx.coroutines.CoroutineDispatcher processingDispatcher, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor processor, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void Markdown(java.util.List<? extends org.jetbrains.jewel.markdown.MarkdownBlock> markdownBlocks, String markdown, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean selectable, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
method @Deprecated @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void Markdown(java.util.List<? extends org.jetbrains.jewel.markdown.MarkdownBlock> markdownBlocks, String markdown, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean selectable, optional kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onUrlClick, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer blockRenderer);
|
||||
}
|
||||
|
||||
@SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public sealed interface MarkdownMode {
|
||||
|
||||
+206
-41
@@ -31,26 +31,31 @@ import org.jetbrains.jewel.markdown.extensions.markdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.extensions.markdownProcessor
|
||||
import org.jetbrains.jewel.markdown.extensions.markdownStyling
|
||||
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
|
||||
import org.jetbrains.jewel.markdown.rendering.DefaultMarkdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.rendering.MarkdownStyling
|
||||
|
||||
private const val STYLING_FROM_RENDERER_MESSAGE =
|
||||
"The block renderer now provides the styling to use, including the block spacing. Provide a renderer with the " +
|
||||
"desired styling instead, e.g. by using ProvideMarkdownStyling."
|
||||
|
||||
/**
|
||||
* A Composable that renders a Markdown string.
|
||||
*
|
||||
* For large amounts of Markdown, such as documents, you can consider using [LazyMarkdown] instead to get better
|
||||
* performance.
|
||||
*
|
||||
* Both the styling of the rendered blocks and the vertical spacing between them come from the [blockRenderer]'s
|
||||
* [rootStyling][MarkdownBlockRenderer.rootStyling].
|
||||
*
|
||||
* @param markdown The Markdown string to render.
|
||||
* @param modifier The modifier to apply to this layout node.
|
||||
* @param selectable Whether the text can be selected.
|
||||
* @param enabled Whether the rendered content is enabled.
|
||||
* @param processingDispatcher The dispatcher to use for processing the Markdown.
|
||||
* @param onUrlClick The callback to be invoked when a URL is clicked.
|
||||
* @param markdownStyling The styling to use for the rendered Markdown.
|
||||
* @param processor The processor to use for parsing the Markdown.
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks.
|
||||
* @see Markdown
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks, and the source of the styling to use.
|
||||
* @see LazyMarkdown
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
@@ -62,9 +67,8 @@ public fun Markdown(
|
||||
enabled: Boolean = true,
|
||||
processingDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
onUrlClick: (String) -> Unit = {},
|
||||
markdownStyling: MarkdownStyling = JewelTheme.markdownStyling,
|
||||
processor: MarkdownProcessor = JewelTheme.markdownProcessor,
|
||||
blockRenderer: MarkdownBlockRenderer = DefaultMarkdownBlockRenderer(markdownStyling),
|
||||
blockRenderer: MarkdownBlockRenderer = JewelTheme.markdownBlockRenderer,
|
||||
) {
|
||||
var markdownBlocks by remember { mutableStateOf(emptyList<MarkdownBlock>()) }
|
||||
LaunchedEffect(markdown, processor) {
|
||||
@@ -78,26 +82,77 @@ public fun Markdown(
|
||||
selectable = selectable,
|
||||
enabled = enabled,
|
||||
onUrlClick = onUrlClick,
|
||||
markdownStyling = markdownStyling,
|
||||
blockRenderer = blockRenderer,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Composable that renders a Markdown string.
|
||||
*
|
||||
* @param markdown The Markdown string to render.
|
||||
* @param modifier The modifier to apply to this layout node.
|
||||
* @param selectable Whether the text can be selected.
|
||||
* @param enabled Whether the rendered content is enabled.
|
||||
* @param processingDispatcher The dispatcher to use for processing the Markdown.
|
||||
* @param onUrlClick The callback to be invoked when a URL is clicked.
|
||||
* @param markdownStyling The styling to apply to the [blockRenderer] before rendering. Note that renderer extensions
|
||||
* keep the styling they were created with, so custom blocks are not affected by this.
|
||||
* @param processor The processor to use for parsing the Markdown.
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks.
|
||||
* @see Markdown
|
||||
*/
|
||||
@Deprecated(
|
||||
STYLING_FROM_RENDERER_MESSAGE,
|
||||
ReplaceWith(
|
||||
"Markdown(markdown, modifier, selectable, enabled, processingDispatcher, onUrlClick, processor, " +
|
||||
"blockRenderer.createCopy(rootStyling = markdownStyling))"
|
||||
),
|
||||
)
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
@Composable
|
||||
public fun Markdown(
|
||||
@Language("Markdown") markdown: String,
|
||||
modifier: Modifier = Modifier,
|
||||
selectable: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
processingDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
onUrlClick: (String) -> Unit = {},
|
||||
markdownStyling: MarkdownStyling = JewelTheme.markdownStyling,
|
||||
processor: MarkdownProcessor = JewelTheme.markdownProcessor,
|
||||
blockRenderer: MarkdownBlockRenderer = JewelTheme.markdownBlockRenderer,
|
||||
) {
|
||||
val styledRenderer = remember(blockRenderer, markdownStyling) { blockRenderer.withStyling(markdownStyling) }
|
||||
|
||||
Markdown(
|
||||
markdown = markdown,
|
||||
modifier = modifier,
|
||||
selectable = selectable,
|
||||
enabled = enabled,
|
||||
processingDispatcher = processingDispatcher,
|
||||
onUrlClick = onUrlClick,
|
||||
processor = processor,
|
||||
blockRenderer = styledRenderer,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Composable that renders a list of [MarkdownBlock]s in a column.
|
||||
*
|
||||
* For large amounts of Markdown, such as documents, you can consider using [LazyMarkdown] instead to get better
|
||||
* performance.
|
||||
*
|
||||
* Both the styling of the rendered blocks and the vertical spacing between them come from the [blockRenderer]'s
|
||||
* [rootStyling][MarkdownBlockRenderer.rootStyling].
|
||||
*
|
||||
* @param markdownBlocks The list of Markdown blocks to render.
|
||||
* @param markdown The original Markdown string.
|
||||
* @param modifier The modifier to apply to this layout node.
|
||||
* @param enabled Whether the rendered content is enabled.
|
||||
* @param selectable Whether the text can be selected.
|
||||
* @param onUrlClick The callback to be invoked when a URL is clicked.
|
||||
* @param markdownStyling The styling to use for the rendered Markdown.
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks.
|
||||
* @see Markdown
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks, and the source of the styling to use.
|
||||
* @see LazyMarkdown
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
@@ -109,15 +164,14 @@ public fun Markdown(
|
||||
enabled: Boolean = true,
|
||||
selectable: Boolean = false,
|
||||
onUrlClick: (String) -> Unit = {},
|
||||
markdownStyling: MarkdownStyling = JewelTheme.markdownStyling,
|
||||
blockRenderer: MarkdownBlockRenderer = DefaultMarkdownBlockRenderer(markdownStyling),
|
||||
blockRenderer: MarkdownBlockRenderer = JewelTheme.markdownBlockRenderer,
|
||||
) {
|
||||
// We keep the existing behavior in terms of where the rawMarkdown semantic is applied to
|
||||
MaybeSelectable(selectable, Modifier.thenIf(selectable) { semantics { rawMarkdown = markdown } }) {
|
||||
@Suppress("ModifierNotUsedAtRoot") // Intentional
|
||||
Column(
|
||||
modifier.thenIf(!selectable) { semantics { rawMarkdown = markdown } },
|
||||
verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing),
|
||||
verticalArrangement = Arrangement.spacedBy(blockRenderer.rootStyling.blockVerticalSpacing),
|
||||
) {
|
||||
for (block in markdownBlocks) {
|
||||
blockRenderer.RenderBlock(block, enabled, onUrlClick, Modifier)
|
||||
@@ -126,12 +180,62 @@ public fun Markdown(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Composable that renders a list of [MarkdownBlock]s in a column.
|
||||
*
|
||||
* @param markdownBlocks The list of Markdown blocks to render.
|
||||
* @param markdown The original Markdown string.
|
||||
* @param modifier The modifier to apply to this layout node.
|
||||
* @param enabled Whether the rendered content is enabled.
|
||||
* @param selectable Whether the text can be selected.
|
||||
* @param onUrlClick The callback to be invoked when a URL is clicked.
|
||||
* @param markdownStyling The styling to apply to the [blockRenderer] before rendering. Note that renderer extensions
|
||||
* keep the styling they were created with, so custom blocks are not affected by this.
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks.
|
||||
* @see Markdown
|
||||
*/
|
||||
@Deprecated(
|
||||
STYLING_FROM_RENDERER_MESSAGE,
|
||||
ReplaceWith(
|
||||
"Markdown(markdownBlocks, markdown, modifier, enabled, selectable, onUrlClick, " +
|
||||
"blockRenderer.createCopy(rootStyling = markdownStyling))"
|
||||
),
|
||||
)
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
@Composable
|
||||
public fun Markdown(
|
||||
markdownBlocks: List<MarkdownBlock>,
|
||||
markdown: String,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
selectable: Boolean = false,
|
||||
onUrlClick: (String) -> Unit = {},
|
||||
markdownStyling: MarkdownStyling = JewelTheme.markdownStyling,
|
||||
blockRenderer: MarkdownBlockRenderer = JewelTheme.markdownBlockRenderer,
|
||||
) {
|
||||
val styledRenderer = remember(blockRenderer, markdownStyling) { blockRenderer.withStyling(markdownStyling) }
|
||||
|
||||
Markdown(
|
||||
markdownBlocks = markdownBlocks,
|
||||
markdown = markdown,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
selectable = selectable,
|
||||
onUrlClick = onUrlClick,
|
||||
blockRenderer = styledRenderer,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Composable that renders a list of [MarkdownBlock]s in a lazy-loading column.
|
||||
*
|
||||
* For small amounts of Markdown, such as UI text, you should consider using [Markdown] instead to get better
|
||||
* performance.
|
||||
*
|
||||
* Both the styling of the rendered blocks and the vertical spacing between them come from the [blockRenderer]'s
|
||||
* [rootStyling][MarkdownBlockRenderer.rootStyling].
|
||||
*
|
||||
* @param blocks The list of Markdown blocks to render.
|
||||
* @param modifier The modifier to apply to this layout node.
|
||||
* @param contentPadding The padding to apply to the content.
|
||||
@@ -139,12 +243,77 @@ public fun Markdown(
|
||||
* @param enabled Whether the rendered content is enabled.
|
||||
* @param selectable Whether the text can be selected.
|
||||
* @param onUrlClick The callback to be invoked when a URL is clicked.
|
||||
* @param markdownStyling The styling to use for the rendered Markdown.
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks.
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks, and the source of the styling to use.
|
||||
* @see Markdown
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
@Composable
|
||||
public fun LazyMarkdown(
|
||||
blocks: List<MarkdownBlock>,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
state: LazyListState = rememberLazyListState(),
|
||||
enabled: Boolean = true,
|
||||
selectable: Boolean = false,
|
||||
onUrlClick: (String) -> Unit = {},
|
||||
blockRenderer: MarkdownBlockRenderer = JewelTheme.markdownBlockRenderer,
|
||||
) {
|
||||
val blockVerticalSpacing = blockRenderer.rootStyling.blockVerticalSpacing
|
||||
|
||||
MaybeSelectable(selectable, modifier) {
|
||||
LazyColumn(state = state, contentPadding = contentPadding) {
|
||||
itemsIndexed(blocks) { index, block ->
|
||||
blockRenderer.RenderBlock(
|
||||
block = block,
|
||||
enabled = enabled,
|
||||
onUrlClick = onUrlClick,
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
top =
|
||||
if (index == 0) {
|
||||
0.dp
|
||||
} else {
|
||||
(blockVerticalSpacing / 2)
|
||||
},
|
||||
bottom =
|
||||
if (index == blocks.lastIndex) {
|
||||
0.dp
|
||||
} else {
|
||||
(blockVerticalSpacing / 2)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Composable that renders a list of [MarkdownBlock]s in a lazy-loading column.
|
||||
*
|
||||
* @param blocks The list of Markdown blocks to render.
|
||||
* @param modifier The modifier to apply to this layout node.
|
||||
* @param contentPadding The padding to apply to the content.
|
||||
* @param state The state of the lazy list.
|
||||
* @param enabled Whether the rendered content is enabled.
|
||||
* @param selectable Whether the text can be selected.
|
||||
* @param onUrlClick The callback to be invoked when a URL is clicked.
|
||||
* @param markdownStyling The styling to apply to the [blockRenderer] before rendering. Note that renderer extensions
|
||||
* keep the styling they were created with, so custom blocks are not affected by this.
|
||||
* @param blockRenderer The renderer to use for rendering the Markdown blocks.
|
||||
* @see LazyMarkdown
|
||||
*/
|
||||
@Deprecated(
|
||||
STYLING_FROM_RENDERER_MESSAGE,
|
||||
ReplaceWith(
|
||||
"LazyMarkdown(blocks, modifier, contentPadding, state, enabled, selectable, onUrlClick, " +
|
||||
"blockRenderer.createCopy(rootStyling = markdownStyling))"
|
||||
),
|
||||
)
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
@Composable
|
||||
public fun LazyMarkdown(
|
||||
blocks: List<MarkdownBlock>,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -156,34 +325,30 @@ public fun LazyMarkdown(
|
||||
markdownStyling: MarkdownStyling = JewelTheme.markdownStyling,
|
||||
blockRenderer: MarkdownBlockRenderer = JewelTheme.markdownBlockRenderer,
|
||||
) {
|
||||
MaybeSelectable(selectable, modifier) {
|
||||
LazyColumn(state = state, contentPadding = contentPadding) {
|
||||
itemsIndexed(blocks) { index, block ->
|
||||
blockRenderer.RenderBlock(
|
||||
block = block,
|
||||
enabled = enabled,
|
||||
onUrlClick = onUrlClick,
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
top =
|
||||
if (index == 0) {
|
||||
0.dp
|
||||
} else {
|
||||
(markdownStyling.blockVerticalSpacing / 2)
|
||||
},
|
||||
bottom =
|
||||
if (index == blocks.lastIndex) {
|
||||
0.dp
|
||||
} else {
|
||||
(markdownStyling.blockVerticalSpacing / 2)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val styledRenderer = remember(blockRenderer, markdownStyling) { blockRenderer.withStyling(markdownStyling) }
|
||||
|
||||
LazyMarkdown(
|
||||
blocks = blocks,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
state = state,
|
||||
enabled = enabled,
|
||||
selectable = selectable,
|
||||
onUrlClick = onUrlClick,
|
||||
blockRenderer = styledRenderer,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [MarkdownBlockRenderer] that uses [styling] as its [MarkdownBlockRenderer.rootStyling], reusing this
|
||||
* instance when it already does, to avoid pointless copies in the common case.
|
||||
*
|
||||
* Note that renderer extensions capture their own styling when they are created, so custom blocks keep rendering with
|
||||
* the styling their extension was built with.
|
||||
*/
|
||||
private fun MarkdownBlockRenderer.withStyling(styling: MarkdownStyling): MarkdownBlockRenderer =
|
||||
if (rootStyling == styling) this else createCopy(rootStyling = styling)
|
||||
|
||||
@Composable
|
||||
private fun MaybeSelectable(selectable: Boolean, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||
val movableContent = remember { movableContentOf(content) }
|
||||
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.markdown
|
||||
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.test.ComposeUiTest
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.getBoundsInRoot
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownProcessor
|
||||
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownStyling
|
||||
import org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension
|
||||
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
|
||||
import org.jetbrains.jewel.markdown.rendering.DefaultMarkdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer
|
||||
import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.rendering.MarkdownStyling
|
||||
import org.jetbrains.jewel.markdown.testing.MarkdownTestTheme
|
||||
import org.jetbrains.jewel.markdown.testing.createMarkdownTestStyling
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Tests that the styling used when rendering comes from the [MarkdownBlockRenderer], and not from a second, independent
|
||||
* source that could disagree with it.
|
||||
*
|
||||
* The observable proxy for "which styling was used" is the vertical gap between two blocks, since
|
||||
* `blockVerticalSpacing` is the one styling value these composables read directly.
|
||||
*/
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
public class MarkdownStylingSourceTest {
|
||||
@Test
|
||||
public fun `Markdown takes block spacing from the renderer, not from the styling composition local`() {
|
||||
runComposeUiTest {
|
||||
val blocks = MarkdownProcessor().processMarkdownDocument(TWO_PARAGRAPHS)
|
||||
// The local styling and the renderer's styling deliberately disagree, so the gap tells us which one won.
|
||||
val renderer = DefaultMarkdownBlockRenderer(createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP))
|
||||
val localStyling = createMarkdownTestStyling(blockVerticalSpacing = LOCAL_GAP)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalMarkdownStyling provides localStyling,
|
||||
LocalMarkdownProcessor provides MarkdownProcessor(),
|
||||
LocalMarkdownBlockRenderer provides renderer,
|
||||
) {
|
||||
Markdown(markdownBlocks = blocks, markdown = TWO_PARAGRAPHS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
assertGapBetweenParagraphs(RENDERER_GAP)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public fun `LazyMarkdown takes block spacing from the renderer, not from the styling composition local`() {
|
||||
runComposeUiTest {
|
||||
val blocks = MarkdownProcessor().processMarkdownDocument(TWO_PARAGRAPHS)
|
||||
val renderer = DefaultMarkdownBlockRenderer(createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP))
|
||||
val localStyling = createMarkdownTestStyling(blockVerticalSpacing = LOCAL_GAP)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalMarkdownStyling provides localStyling,
|
||||
LocalMarkdownBlockRenderer provides renderer,
|
||||
) {
|
||||
LazyMarkdown(blocks = blocks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
// LazyMarkdown splits the spacing into half above and half below each item, so the total gap still matches.
|
||||
assertGapBetweenParagraphs(RENDERER_GAP)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public fun `Markdown uses the styling of an explicitly passed renderer`() {
|
||||
runComposeUiTest {
|
||||
val blocks = MarkdownProcessor().processMarkdownDocument(TWO_PARAGRAPHS)
|
||||
val renderer = DefaultMarkdownBlockRenderer(createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP))
|
||||
val localStyling = createMarkdownTestStyling(blockVerticalSpacing = LOCAL_GAP)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
CompositionLocalProvider(LocalMarkdownStyling provides localStyling) {
|
||||
// No styling argument, so this binds to the overload without one: the renderer decides.
|
||||
Markdown(markdownBlocks = blocks, markdown = TWO_PARAGRAPHS, blockRenderer = renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
assertGapBetweenParagraphs(RENDERER_GAP)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("DEPRECATION") // Testing the deprecated overload's behavior on purpose
|
||||
public fun `deprecated Markdown overload applies the explicit styling to the renderer`() {
|
||||
runComposeUiTest {
|
||||
val blocks = MarkdownProcessor().processMarkdownDocument(TWO_PARAGRAPHS)
|
||||
// The renderer starts with the "wrong" spacing: only re-styling it can produce EXPLICIT_GAP below.
|
||||
val renderer = DefaultMarkdownBlockRenderer(createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP))
|
||||
val explicitStyling = createMarkdownTestStyling(blockVerticalSpacing = EXPLICIT_GAP)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
Markdown(
|
||||
markdownBlocks = blocks,
|
||||
markdown = TWO_PARAGRAPHS,
|
||||
markdownStyling = explicitStyling,
|
||||
blockRenderer = renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
assertGapBetweenParagraphs(EXPLICIT_GAP)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
// DEPRECATION: testing the deprecated overload's behavior on purpose. InjectDispatcher: the dispatcher is the point
|
||||
// here, as it is what keeps the parsing synchronous and the test deterministic.
|
||||
@Suppress("DEPRECATION", "InjectDispatcher")
|
||||
public fun `deprecated Markdown string overload applies the explicit styling to the renderer`() {
|
||||
runComposeUiTest {
|
||||
val renderer = DefaultMarkdownBlockRenderer(createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP))
|
||||
val explicitStyling = createMarkdownTestStyling(blockVerticalSpacing = EXPLICIT_GAP)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
CompositionLocalProvider(LocalMarkdownProcessor provides MarkdownProcessor()) {
|
||||
Markdown(
|
||||
markdown = TWO_PARAGRAPHS,
|
||||
// Unconfined keeps the parsing synchronous, so the blocks are ready once we are idle.
|
||||
processingDispatcher = Dispatchers.Unconfined,
|
||||
markdownStyling = explicitStyling,
|
||||
blockRenderer = renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
assertGapBetweenParagraphs(EXPLICIT_GAP)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("DEPRECATION") // Testing the deprecated overload's behavior on purpose
|
||||
public fun `deprecated LazyMarkdown overload applies the explicit styling to the renderer`() {
|
||||
runComposeUiTest {
|
||||
val blocks = MarkdownProcessor().processMarkdownDocument(TWO_PARAGRAPHS)
|
||||
val renderer = DefaultMarkdownBlockRenderer(createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP))
|
||||
val explicitStyling = createMarkdownTestStyling(blockVerticalSpacing = EXPLICIT_GAP)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
LazyMarkdown(blocks = blocks, markdownStyling = explicitStyling, blockRenderer = renderer)
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
assertGapBetweenParagraphs(EXPLICIT_GAP)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("DEPRECATION") // Testing the deprecated overload's behavior on purpose
|
||||
public fun `the renderer is not copied when the explicit styling already matches it`() {
|
||||
runComposeUiTest {
|
||||
val blocks = MarkdownProcessor().processMarkdownDocument(TWO_PARAGRAPHS)
|
||||
val styling = createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP)
|
||||
val renderer = CopyCountingBlockRenderer(styling)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
// The very same styling instance the renderer already holds: there is nothing to re-style.
|
||||
Markdown(
|
||||
markdownBlocks = blocks,
|
||||
markdown = TWO_PARAGRAPHS,
|
||||
markdownStyling = styling,
|
||||
blockRenderer = renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
assertEquals("The renderer should have been reused as-is", 0, renderer.copyCount)
|
||||
assertGapBetweenParagraphs(RENDERER_GAP)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("DEPRECATION") // Testing the deprecated overload's behavior on purpose
|
||||
public fun `the renderer is copied once when the explicit styling differs`() {
|
||||
runComposeUiTest {
|
||||
val blocks = MarkdownProcessor().processMarkdownDocument(TWO_PARAGRAPHS)
|
||||
val renderer = CopyCountingBlockRenderer(createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP))
|
||||
val explicitStyling = createMarkdownTestStyling(blockVerticalSpacing = EXPLICIT_GAP)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
Markdown(
|
||||
markdownBlocks = blocks,
|
||||
markdown = TWO_PARAGRAPHS,
|
||||
markdownStyling = explicitStyling,
|
||||
blockRenderer = renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
assertEquals("The renderer should have been copied exactly once", 1, renderer.copyCount)
|
||||
assertGapBetweenParagraphs(EXPLICIT_GAP)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("DEPRECATION") // Testing the deprecated overload's behavior on purpose
|
||||
public fun `the styled renderer is reused across recompositions`() {
|
||||
runComposeUiTest {
|
||||
val blocks = MarkdownProcessor().processMarkdownDocument(TWO_PARAGRAPHS)
|
||||
val renderer = CopyCountingBlockRenderer(createMarkdownTestStyling(blockVerticalSpacing = RENDERER_GAP))
|
||||
val explicitStyling = createMarkdownTestStyling(blockVerticalSpacing = EXPLICIT_GAP)
|
||||
var enabled by mutableStateOf(true)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
Markdown(
|
||||
markdownBlocks = blocks,
|
||||
markdown = TWO_PARAGRAPHS,
|
||||
enabled = enabled,
|
||||
markdownStyling = explicitStyling,
|
||||
blockRenderer = renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
assertEquals("The first composition should copy the renderer once", 1, renderer.copyCount)
|
||||
|
||||
// Force a recomposition: without a remember, the styled copy would be rebuilt every time, which would both
|
||||
// churn allocations and hand a brand new renderer identity to everything downstream.
|
||||
enabled = false
|
||||
waitForIdle()
|
||||
|
||||
assertEquals("Recomposing should not rebuild the styled renderer", 1, renderer.copyCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeUiTest.assertGapBetweenParagraphs(expected: Dp) {
|
||||
val first = onNodeWithText(FIRST_PARAGRAPH).getBoundsInRoot()
|
||||
val second = onNodeWithText(SECOND_PARAGRAPH).getBoundsInRoot()
|
||||
val actual = second.top - first.bottom
|
||||
|
||||
assertEquals("Unexpected gap between the two paragraphs", expected.value, actual.value, TOLERANCE)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val FIRST_PARAGRAPH = "Alpha"
|
||||
private const val SECOND_PARAGRAPH = "Bravo"
|
||||
private const val TWO_PARAGRAPHS = "$FIRST_PARAGRAPH\n\n$SECOND_PARAGRAPH"
|
||||
|
||||
/** Rounding when converting between Dp and pixels can cost a fraction of a Dp. */
|
||||
private const val TOLERANCE = 1f
|
||||
|
||||
private val LOCAL_GAP = 7.dp
|
||||
private val RENDERER_GAP = 31.dp
|
||||
private val EXPLICIT_GAP = 53.dp
|
||||
}
|
||||
}
|
||||
|
||||
/** A [DefaultMarkdownBlockRenderer] that counts how many times it has been copied via [createCopy]. */
|
||||
private class CopyCountingBlockRenderer(styling: MarkdownStyling) : DefaultMarkdownBlockRenderer(styling) {
|
||||
var copyCount: Int = 0
|
||||
private set
|
||||
|
||||
override fun createCopy(
|
||||
rootStyling: MarkdownStyling?,
|
||||
rendererExtensions: List<MarkdownRendererExtension>?,
|
||||
inlineRenderer: InlineMarkdownRenderer?,
|
||||
): MarkdownBlockRenderer {
|
||||
copyCount++
|
||||
return super.createCopy(rootStyling, rendererExtensions, inlineRenderer)
|
||||
}
|
||||
}
|
||||
+33
@@ -3,6 +3,7 @@
|
||||
package org.jetbrains.jewel.markdown.extensions.github.tables
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -15,6 +16,10 @@ import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.jewel.markdown.Markdown
|
||||
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownProcessor
|
||||
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownStyling
|
||||
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
|
||||
import org.jetbrains.jewel.markdown.rendering.DefaultMarkdownBlockRenderer
|
||||
import org.jetbrains.jewel.markdown.testing.MarkdownTestTheme
|
||||
@@ -115,6 +120,34 @@ public class GitHubTableBlockRendererRememberKeysTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public fun `Markdown picks up block renderer from composition local`() {
|
||||
runComposeUiTest {
|
||||
val styling = createMarkdownTestStyling()
|
||||
val processor = MarkdownProcessor(listOf(GitHubTableProcessorExtension))
|
||||
val renderer =
|
||||
DefaultMarkdownBlockRenderer(
|
||||
styling,
|
||||
listOf(GitHubTableRendererExtension(createTableStyling(), styling)),
|
||||
)
|
||||
|
||||
setContent {
|
||||
MarkdownTestTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalMarkdownStyling provides styling,
|
||||
LocalMarkdownProcessor provides processor,
|
||||
LocalMarkdownBlockRenderer provides renderer,
|
||||
) {
|
||||
Markdown("| a | b |\n|---|---|\n| c | d |")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitForIdle()
|
||||
onNodeWithText("c").assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTableStyling() =
|
||||
GfmTableStyling(
|
||||
colors =
|
||||
|
||||
+2
-1
@@ -2,4 +2,5 @@ package org.jetbrains.jewel.intui.markdown.bridge.styling
|
||||
|
||||
import com.intellij.ui.JBColor
|
||||
|
||||
internal val isLightTheme = JBColor.isBright()
|
||||
internal val isLightTheme: Boolean
|
||||
get() = JBColor.isBright()
|
||||
|
||||
+7
-2
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import org.jetbrains.jewel.foundation.BorderColors
|
||||
@@ -128,7 +129,11 @@ fun createMarkdownTestThemeDefinition(): ThemeDefinition =
|
||||
disabledAppearanceValues = DisabledAppearanceValues(brightness = 33, contrast = -35, alpha = 100),
|
||||
)
|
||||
|
||||
fun createMarkdownTestStyling(codeEditorTextStyle: TextStyle = TextStyle.Default): MarkdownStyling {
|
||||
/** Creates a minimal [MarkdownStyling] with stub span styles for use in Markdown rendering tests. */
|
||||
fun createMarkdownTestStyling(
|
||||
codeEditorTextStyle: TextStyle = TextStyle.Default,
|
||||
blockVerticalSpacing: Dp = 8.dp,
|
||||
): MarkdownStyling {
|
||||
val mockSpanStyle = SpanStyle(Color.Black)
|
||||
val inlinesStyling =
|
||||
InlinesStyling(
|
||||
@@ -145,7 +150,7 @@ fun createMarkdownTestStyling(codeEditorTextStyle: TextStyle = TextStyle.Default
|
||||
inlineHtml = mockSpanStyle,
|
||||
)
|
||||
return MarkdownStyling(
|
||||
blockVerticalSpacing = 8.dp,
|
||||
blockVerticalSpacing = blockVerticalSpacing,
|
||||
paragraph = MarkdownStyling.Paragraph(inlinesStyling),
|
||||
heading =
|
||||
MarkdownStyling.Heading(
|
||||
|
||||
Reference in New Issue
Block a user