diff --git a/.idea/modules.xml b/.idea/modules.xml index 1990c3ca8659a..cfa50d921e349 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -741,6 +741,7 @@ + diff --git a/platform/jewel/art/docs/custom-chrome.png b/platform/jewel/art/docs/custom-chrome.png index a8a331a18fe9e..92274515e5d03 100644 Binary files a/platform/jewel/art/docs/custom-chrome.png and b/platform/jewel/art/docs/custom-chrome.png differ diff --git a/platform/jewel/art/docs/gfm-table-dark.png b/platform/jewel/art/docs/gfm-table-dark.png new file mode 100644 index 0000000000000..64fe6bd200a09 Binary files /dev/null and b/platform/jewel/art/docs/gfm-table-dark.png differ diff --git a/platform/jewel/art/docs/gfm-table-light.png b/platform/jewel/art/docs/gfm-table-light.png new file mode 100644 index 0000000000000..23c914fe7f6a5 Binary files /dev/null and b/platform/jewel/art/docs/gfm-table-light.png differ diff --git a/platform/jewel/art/docs/markdown-renderer.png b/platform/jewel/art/docs/markdown-renderer.png new file mode 100644 index 0000000000000..e56d4765a56ef Binary files /dev/null and b/platform/jewel/art/docs/markdown-renderer.png differ diff --git a/platform/jewel/art/docs/merge-dialog.png b/platform/jewel/art/docs/merge-dialog.png index d2bc1749ee1df..3ddcbefbd27c3 100644 Binary files a/platform/jewel/art/docs/merge-dialog.png and b/platform/jewel/art/docs/merge-dialog.png differ diff --git a/platform/jewel/foundation/api/foundation.api b/platform/jewel/foundation/api/foundation.api index 16651c5879052..068b52ab234c5 100644 --- a/platform/jewel/foundation/api/foundation.api +++ b/platform/jewel/foundation/api/foundation.api @@ -241,7 +241,7 @@ public final class org/jetbrains/jewel/foundation/code/MimeTypeKt { } public abstract interface class org/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter { - public abstract fun highlight-C7ITchA (Ljava/lang/String;Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public abstract fun highlight-zTGadEY (Ljava/lang/String;Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; } public final class org/jetbrains/jewel/foundation/code/highlighting/CodeHighlighterKt { @@ -251,7 +251,11 @@ public final class org/jetbrains/jewel/foundation/code/highlighting/CodeHighligh public final class org/jetbrains/jewel/foundation/code/highlighting/NoOpCodeHighlighter : org/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter { public static final field $stable I public static final field INSTANCE Lorg/jetbrains/jewel/foundation/code/highlighting/NoOpCodeHighlighter; - public fun highlight-C7ITchA (Ljava/lang/String;Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public fun highlight-zTGadEY (Ljava/lang/String;Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; +} + +public final class org/jetbrains/jewel/foundation/layout/BasicTableLayoutKt { + public static final fun BasicTableLayout-yE4rkUQ (IIJLandroidx/compose/ui/Modifier;FLjava/util/List;Landroidx/compose/runtime/Composer;II)V } public class org/jetbrains/jewel/foundation/lazy/DefaultMacOsSelectableColumnKeybindings : org/jetbrains/jewel/foundation/lazy/DefaultSelectableColumnKeybindings { diff --git a/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter.kt b/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter.kt index aa638f587295d..15c37fa4b6f1a 100644 --- a/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter.kt +++ b/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter.kt @@ -24,7 +24,7 @@ public interface CodeHighlighter { * * @see [NoOpCodeHighlighter] */ - public fun highlight(code: String, mimeType: MimeType): Flow + public fun highlight(code: String, mimeType: MimeType?): Flow } public val LocalCodeHighlighter: ProvidableCompositionLocal = staticCompositionLocalOf { diff --git a/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/NoOpCodeHighlighter.kt b/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/NoOpCodeHighlighter.kt index b99d90f800a78..1b21b1c68e70e 100644 --- a/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/NoOpCodeHighlighter.kt +++ b/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/NoOpCodeHighlighter.kt @@ -6,5 +6,5 @@ import kotlinx.coroutines.flow.flowOf import org.jetbrains.jewel.foundation.code.MimeType public object NoOpCodeHighlighter : CodeHighlighter { - override fun highlight(code: String, mimeType: MimeType): Flow = flowOf(AnnotatedString(code)) + override fun highlight(code: String, mimeType: MimeType?): Flow = flowOf(AnnotatedString(code)) } diff --git a/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/layout/BasicTableLayout.kt b/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/layout/BasicTableLayout.kt new file mode 100644 index 0000000000000..eb7d6d2d72b0b --- /dev/null +++ b/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/layout/BasicTableLayout.kt @@ -0,0 +1,222 @@ +package org.jetbrains.jewel.foundation.layout + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * A simple table that sizes columns to take as much room as they need. If the horizontal space available is less than + * what the cells would take, all columns are sized proportionally to their intrinsic width so that they still can fit + * the available width. + * + * Cells **must** only contain one top-level component. If you need your cells to contain more than one, wrap your cell + * content in a [`Box`][Box], [`Column`][androidx.compose.foundation.layout.Column], + * [`Row`][androidx.compose.foundation.layout.Row], etc. + * + * Incoming height constraints are ignored. The table will always take up as much vertical room as it needs. If you want + * to constrain the table height consider wrapping it in a + * [`VerticallyScrollableContainer`][org.jetbrains.jewel.ui.component.VerticallyScrollableContainer]. + * + * @param rowCount The number of rows this table has. + * @param columnCount The number of columns this table has. + * @param cellBorderColor The color of the cell borders. Set to [Color.Unspecified] to avoid drawing the borders — in + * which case, the [cellBorderWidth] acts as a padding. + * @param modifier Modifier to apply to the table. + * @param cellBorderWidth The width of the table's borders. + * @param rows The rows that make up the table. Each row is a list of composables, one per row cell. + */ +@Suppress("KDocUnresolvedReference") +@Composable +public fun BasicTableLayout( + rowCount: Int, + columnCount: Int, + cellBorderColor: Color, + modifier: Modifier = Modifier, + cellBorderWidth: Dp = 1.dp, + rows: List Unit>>, +) { + var rowHeights by remember { mutableStateOf(emptyList()) } + var columnWidths by remember { mutableStateOf(emptyList()) } + + Layout( + modifier = + modifier.thenIf(rowHeights.size == rowCount && columnWidths.size == columnCount) { + drawTableBorders(cellBorderColor, cellBorderWidth, rowHeights, columnWidths) + }, + content = { rows.forEach { row -> row.forEach { cell -> cell() } } }, + measurePolicy = { measurables, incomingConstraints -> + require(rows.size == rowCount) { "Found ${rows.size} rows, but expected $rowCount." } + require(measurables.size == rowCount * columnCount) { + "Found ${measurables.size} cells, but expected ${rowCount * columnCount}." + } + + val intrinsicColumnWidths = IntArray(columnCount) + rows.forEachIndexed { rowIndex, row -> + require(row.size == columnCount) { + "Row $rowIndex contains ${row.size} cells, but it should have $columnCount cells." + } + + row.forEachIndexed { columnIndex, cell -> + // Subcompose each cell individually + val measurable = measurables[rowIndex * columnIndex] + + // Store the intrinsic width for each column, assuming we have infinite + // vertical space available to display each cell (which we do) + val intrinsicCellWidth = measurable.maxIntrinsicWidth(Int.MAX_VALUE) + intrinsicColumnWidths[columnIndex] = + intrinsicColumnWidths[columnIndex].coerceAtLeast(intrinsicCellWidth) + + measurable + } + } + + // The available width we can assign to cells is equal to the max width from the + // incoming + // constraints, minus the vertical borders applied between columns and to the sides of + // the + // table + val cellBorderWidthPx = cellBorderWidth.roundToPx() + val totalHorizontalBordersWidth = cellBorderWidthPx * (columnCount + 1) + val minTableIntrinsicWidth = intrinsicColumnWidths.sum() + totalHorizontalBordersWidth + val availableWidth = incomingConstraints.maxWidth + + // We want to size the columns as a ratio of their intrinsic size to the available width + // if there is not enough room to show them all, or as their intrinsic width if they all + // fit + var tableWidth = 0 + + if (minTableIntrinsicWidth <= availableWidth) { + // We have enough room for all columns, use intrinsic column sizes + tableWidth = minTableIntrinsicWidth + } else { + // We can't fit all columns in the available width; set their size proportionally + // to the intrinsic width, so they all fit within the available horizontal space + val scaleRatio = availableWidth.toFloat() / minTableIntrinsicWidth + for (i in 0 until columnCount) { + // By truncating the decimal side, we may end up a few pixels short than the + // available width, but at least we're never exceeding it. + intrinsicColumnWidths[i] = (intrinsicColumnWidths[i] * scaleRatio).toInt() + tableWidth += intrinsicColumnWidths[i] + } + tableWidth += totalHorizontalBordersWidth + } + columnWidths = intrinsicColumnWidths.toList() + + // The height of each row is the maximum intrinsic height of their cells, calculated + // from + // the (possibly scaled) intrinsic column widths we just computed + val intrinsicRowHeights = IntArray(rowCount) + var tableHeight = 0 + measurables.chunked(columnCount).mapIndexed { rowIndex, rowMeasurables -> + val rowHeight = + rowMeasurables + .mapIndexed { columnIndex, cellMeasurable -> + val columnWidth = columnWidths[columnIndex] + cellMeasurable.maxIntrinsicHeight(columnWidth) + } + .max() + + tableHeight += rowHeight + intrinsicRowHeights[rowIndex] = rowHeight + } + rowHeights = intrinsicRowHeights.toList() + + // Add the horizontal borders drawn between rows and on top and bottom of the table + tableHeight += cellBorderWidthPx * (rowCount + 1) + + // Measure all cells, using the fixed constraints we calculated for each row and column + val placeables = + measurables.chunked(columnCount).mapIndexed { rowIndex, cellMeasurables -> + cellMeasurables.mapIndexed { columnIndex, cellMeasurable -> + val cellConstraints = Constraints.fixed(columnWidths[columnIndex], rowHeights[rowIndex]) + cellMeasurable.measure(cellConstraints) + } + } + + layout(tableWidth, tableHeight) { + // Place cells. We start by leaving space for the top and start-side borders + var y = cellBorderWidthPx + + placeables.forEachIndexed { rowIndex, cellPlaceables -> + var x = cellBorderWidthPx + + var rowHeight = 0 + cellPlaceables.forEach { cellPlaceable -> + cellPlaceable.placeRelative(x, y) + x += cellBorderWidthPx + x += cellPlaceable.width + rowHeight = cellPlaceable.height.coerceAtLeast(rowHeight) + } + + y += cellBorderWidthPx + y += rowHeight + } + } + }, + ) +} + +private fun Modifier.drawTableBorders( + cellBorderColor: Color, + cellBorderWidth: Dp, + rowHeights: List, + columnWidths: List, +) = drawBehind { + val borderWidthPx = cellBorderWidth.toPx() + val halfBorderWidthPx = borderWidthPx / 2f + + // First, draw the outer border + drawRect( + color = cellBorderColor, + topLeft = Offset(halfBorderWidthPx, halfBorderWidthPx), + size = Size(size.width - borderWidthPx, size.height - borderWidthPx), + style = Stroke(width = borderWidthPx), + ) + + // Then, draw all horizontal borders below rows. + // No need to draw the last horizontal border as it's covered by the border rect + var y = halfBorderWidthPx + val endX = size.width - borderWidthPx + + for (i in 0 until rowHeights.lastIndex) { + y += rowHeights[i].toFloat() + borderWidthPx + drawLine( + color = cellBorderColor, + start = Offset(halfBorderWidthPx, y), + end = Offset(endX, y), + strokeWidth = borderWidthPx, + ) + } + + // Lastly, draw all vertical borders to the end of columns + // (minus the last one, as before) + var x = halfBorderWidthPx + val endY = size.height - borderWidthPx + + for (i in 0 until columnWidths.lastIndex) { + x += columnWidths[i].toFloat() + borderWidthPx + drawLine( + color = cellBorderColor, + start = Offset(x, halfBorderWidthPx), + end = Offset(x, endY), + strokeWidth = borderWidthPx, + ) + } +} + +// TODO remove this once thenIf is moved to foundation +private inline fun Modifier.thenIf(precondition: Boolean, action: Modifier.() -> Modifier): Modifier = + if (precondition) action() else this diff --git a/platform/jewel/gradle/libs.versions.toml b/platform/jewel/gradle/libs.versions.toml index 8787ebe45de52..921fd3a896df8 100644 --- a/platform/jewel/gradle/libs.versions.toml +++ b/platform/jewel/gradle/libs.versions.toml @@ -19,6 +19,7 @@ poko = "0.17.1" [libraries] commonmark-core = { module = "org.commonmark:commonmark", version.ref = "commonmark" } commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" } +commonmark-ext-gfm-tables = { module = "org.commonmark:commonmark-ext-gfm-tables", version.ref = "commonmark" } filePicker = { module = "com.darkrockstudios:mpfilepicker", version = "3.1.0" } diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/code/highlighting/LexerBasedCodeHighlighter.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/code/highlighting/LexerBasedCodeHighlighter.kt index 4706c3cfad4b7..506e7466c0e5b 100644 --- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/code/highlighting/LexerBasedCodeHighlighter.kt +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/code/highlighting/LexerBasedCodeHighlighter.kt @@ -34,8 +34,8 @@ internal class LexerBasedCodeHighlighter( private val reHighlightingRequests: Flow, private val highlightDispatcher: CoroutineDispatcher = Dispatchers.Default, ) : CodeHighlighter { - override fun highlight(code: String, mimeType: MimeType): Flow { - val language = mimeType.toLanguageOrNull() ?: return flowOf(AnnotatedString(code)) + override fun highlight(code: String, mimeType: MimeType?): Flow { + val language = mimeType?.toLanguageOrNull() ?: return flowOf(AnnotatedString(code)) val fileExtension = language.associatedFileType?.defaultExtension ?: return flowOf(AnnotatedString(code)) val virtualFile = LightVirtualFile("markdown_code_block_${code.hashCode()}.$fileExtension", language, code) val colorScheme = EditorColorsManager.getInstance().globalScheme diff --git a/platform/jewel/int-ui/int-ui-standalone/api/int-ui-standalone.api b/platform/jewel/int-ui/int-ui-standalone/api/int-ui-standalone.api index 4b17de4433824..f524db9bf67b4 100644 --- a/platform/jewel/int-ui/int-ui-standalone/api/int-ui-standalone.api +++ b/platform/jewel/int-ui/int-ui-standalone/api/int-ui-standalone.api @@ -591,14 +591,22 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiWarningBann } public final class org/jetbrains/jewel/intui/standalone/theme/IntUiGlobalColorsKt { - public static final fun dark-GyCwops (Lorg/jetbrains/jewel/foundation/BorderColors$Companion;JJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/BorderColors; - public static final fun dark-Hformbs (Lorg/jetbrains/jewel/foundation/OutlineColors$Companion;JJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/OutlineColors; - public static final fun dark-Hformbs (Lorg/jetbrains/jewel/foundation/TextColors$Companion;JJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/TextColors; - public static final fun dark-yrwZFoE (Lorg/jetbrains/jewel/foundation/GlobalColors$Companion;Lorg/jetbrains/jewel/foundation/BorderColors;Lorg/jetbrains/jewel/foundation/OutlineColors;Lorg/jetbrains/jewel/foundation/TextColors;JLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/GlobalColors; - public static final fun light-GyCwops (Lorg/jetbrains/jewel/foundation/BorderColors$Companion;JJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/BorderColors; - public static final fun light-Hformbs (Lorg/jetbrains/jewel/foundation/OutlineColors$Companion;JJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/OutlineColors; - public static final fun light-Hformbs (Lorg/jetbrains/jewel/foundation/TextColors$Companion;JJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/TextColors; - public static final fun light-yrwZFoE (Lorg/jetbrains/jewel/foundation/GlobalColors$Companion;Lorg/jetbrains/jewel/foundation/BorderColors;Lorg/jetbrains/jewel/foundation/OutlineColors;Lorg/jetbrains/jewel/foundation/TextColors;JLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/foundation/GlobalColors; + public static final fun dark-jdqzblg (Lorg/jetbrains/jewel/foundation/OutlineColors$Companion;JJJJJ)Lorg/jetbrains/jewel/foundation/OutlineColors; + public static final fun dark-jdqzblg (Lorg/jetbrains/jewel/foundation/TextColors$Companion;JJJJJ)Lorg/jetbrains/jewel/foundation/TextColors; + public static synthetic fun dark-jdqzblg$default (Lorg/jetbrains/jewel/foundation/OutlineColors$Companion;JJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/foundation/OutlineColors; + public static synthetic fun dark-jdqzblg$default (Lorg/jetbrains/jewel/foundation/TextColors$Companion;JJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/foundation/TextColors; + public static final fun dark-xwkQ0AY (Lorg/jetbrains/jewel/foundation/GlobalColors$Companion;Lorg/jetbrains/jewel/foundation/BorderColors;Lorg/jetbrains/jewel/foundation/OutlineColors;Lorg/jetbrains/jewel/foundation/TextColors;J)Lorg/jetbrains/jewel/foundation/GlobalColors; + public static synthetic fun dark-xwkQ0AY$default (Lorg/jetbrains/jewel/foundation/GlobalColors$Companion;Lorg/jetbrains/jewel/foundation/BorderColors;Lorg/jetbrains/jewel/foundation/OutlineColors;Lorg/jetbrains/jewel/foundation/TextColors;JILjava/lang/Object;)Lorg/jetbrains/jewel/foundation/GlobalColors; + public static final fun dark-zSO0fhY (Lorg/jetbrains/jewel/foundation/BorderColors$Companion;JJJ)Lorg/jetbrains/jewel/foundation/BorderColors; + public static synthetic fun dark-zSO0fhY$default (Lorg/jetbrains/jewel/foundation/BorderColors$Companion;JJJILjava/lang/Object;)Lorg/jetbrains/jewel/foundation/BorderColors; + public static final fun light-jdqzblg (Lorg/jetbrains/jewel/foundation/OutlineColors$Companion;JJJJJ)Lorg/jetbrains/jewel/foundation/OutlineColors; + public static final fun light-jdqzblg (Lorg/jetbrains/jewel/foundation/TextColors$Companion;JJJJJ)Lorg/jetbrains/jewel/foundation/TextColors; + public static synthetic fun light-jdqzblg$default (Lorg/jetbrains/jewel/foundation/OutlineColors$Companion;JJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/foundation/OutlineColors; + public static synthetic fun light-jdqzblg$default (Lorg/jetbrains/jewel/foundation/TextColors$Companion;JJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/foundation/TextColors; + public static final fun light-xwkQ0AY (Lorg/jetbrains/jewel/foundation/GlobalColors$Companion;Lorg/jetbrains/jewel/foundation/BorderColors;Lorg/jetbrains/jewel/foundation/OutlineColors;Lorg/jetbrains/jewel/foundation/TextColors;J)Lorg/jetbrains/jewel/foundation/GlobalColors; + public static synthetic fun light-xwkQ0AY$default (Lorg/jetbrains/jewel/foundation/GlobalColors$Companion;Lorg/jetbrains/jewel/foundation/BorderColors;Lorg/jetbrains/jewel/foundation/OutlineColors;Lorg/jetbrains/jewel/foundation/TextColors;JILjava/lang/Object;)Lorg/jetbrains/jewel/foundation/GlobalColors; + public static final fun light-zSO0fhY (Lorg/jetbrains/jewel/foundation/BorderColors$Companion;JJJ)Lorg/jetbrains/jewel/foundation/BorderColors; + public static synthetic fun light-zSO0fhY$default (Lorg/jetbrains/jewel/foundation/BorderColors$Companion;JJJILjava/lang/Object;)Lorg/jetbrains/jewel/foundation/BorderColors; } public final class org/jetbrains/jewel/intui/standalone/theme/IntUiGlobalMetricsKt { diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiGlobalColors.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiGlobalColors.kt index a6461a75ad75b..480e2910c45c9 100644 --- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiGlobalColors.kt +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiGlobalColors.kt @@ -1,6 +1,5 @@ package org.jetbrains.jewel.intui.standalone.theme -import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import org.jetbrains.jewel.foundation.BorderColors import org.jetbrains.jewel.foundation.GlobalColors @@ -9,7 +8,6 @@ import org.jetbrains.jewel.foundation.TextColors import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme -@Composable public fun GlobalColors.Companion.light( borders: BorderColors = BorderColors.light(), outlines: OutlineColors = OutlineColors.light(), @@ -17,7 +15,6 @@ public fun GlobalColors.Companion.light( paneBackground: Color = IntUiLightTheme.colors.gray(13), ): GlobalColors = GlobalColors(borders = borders, outlines = outlines, text = text, panelBackground = paneBackground) -@Composable public fun GlobalColors.Companion.dark( borders: BorderColors = BorderColors.dark(), outlines: OutlineColors = OutlineColors.dark(), @@ -25,21 +22,18 @@ public fun GlobalColors.Companion.dark( paneBackground: Color = IntUiDarkTheme.colors.gray(2), ): GlobalColors = GlobalColors(borders = borders, outlines = outlines, text = text, panelBackground = paneBackground) -@Composable public fun BorderColors.Companion.light( normal: Color = IntUiLightTheme.colors.gray(12), focused: Color = IntUiLightTheme.colors.gray(14), disabled: Color = IntUiLightTheme.colors.gray(11), ): BorderColors = BorderColors(normal, focused, disabled) -@Composable public fun BorderColors.Companion.dark( normal: Color = IntUiDarkTheme.colors.gray(1), focused: Color = IntUiDarkTheme.colors.gray(2), disabled: Color = IntUiDarkTheme.colors.gray(4), ): BorderColors = BorderColors(normal, focused, disabled) -@Composable public fun TextColors.Companion.light( normal: Color = IntUiLightTheme.colors.gray(1), selected: Color = IntUiLightTheme.colors.gray(1), @@ -48,7 +42,6 @@ public fun TextColors.Companion.light( error: Color = IntUiLightTheme.colors.red(4), ): TextColors = TextColors(normal, selected, disabled, info, error) -@Composable public fun TextColors.Companion.dark( normal: Color = IntUiDarkTheme.colors.gray(12), selected: Color = IntUiDarkTheme.colors.gray(12), @@ -57,7 +50,6 @@ public fun TextColors.Companion.dark( error: Color = IntUiDarkTheme.colors.red(7), ): TextColors = TextColors(normal, selected, disabled, info, error) -@Composable public fun OutlineColors.Companion.light( focused: Color = IntUiLightTheme.colors.blue(4), focusedWarning: Color = IntUiLightTheme.colors.yellow(4), @@ -66,7 +58,6 @@ public fun OutlineColors.Companion.light( error: Color = IntUiLightTheme.colors.red(9), ): OutlineColors = OutlineColors(focused, focusedWarning, focusedError, warning, error) -@Composable public fun OutlineColors.Companion.dark( focused: Color = IntUiDarkTheme.colors.blue(6), focusedWarning: Color = IntUiDarkTheme.colors.yellow(4), diff --git a/platform/jewel/markdown/README.md b/platform/jewel/markdown/README.md index c41644a86d724..954cee544b720 100644 --- a/platform/jewel/markdown/README.md +++ b/platform/jewel/markdown/README.md @@ -12,12 +12,13 @@ Additional supported Markdown, via extensions: * Alerts ([GitHub Flavored Markdown][alerts-specs]) — see [`extension-gfm-alerts`](extension/gfm-alerts) * Autolink (standard CommonMark, but provided as extension) — see [`extension-autolink`](extension/autolink) +* Tables ([GitHub Flavored Markdown](https://github.github.com/gfm/#tables-extension-)) — see [ + `extension-gfm-tables`](extension/gfm-tables) [alerts-specs]: https://github.com/orgs/community/discussions/16925 On the roadmap, but not currently supported — in no particular order: -* Tables ([GitHub Flavored Markdown](https://github.github.com/gfm/#tables-extension-)) * Strikethrough ([GitHub Flavored Markdown](https://github.github.com/gfm/#strikethrough-extension-)) * Image loading (via [Coil 3](https://coil-kt.github.io/coil/upgrading_to_coil3/)) * Task list items ([GitHub Flavored Markdown](https://github.github.com/gfm/#task-list-items-extension-)) @@ -119,7 +120,7 @@ those. By default, the processor will ignore any kind of Markdown it doesn't support. To support additional features, such as ones found in GitHub Flavored Markdown, you can use extensions. If you don't specify any extension, the processor will be restricted to the [CommonMark specs](https://specs.commonmark.org) as supported by -[`commonmark-java`](https://github.com/commonmark/commonmark-java). +[`commonmark-java`](https://github.com/commonmark/commonmark-java). > [!NOTE] > Images are not supported yet, even if they are part of the CommonMark specs. @@ -136,7 +137,7 @@ val processor = MarkdownProcessor(parsingExtensions) // Where the rendering happens... val blockRenderer = remember(markdownStyling, isDark) { val rendererExtensions = listOf(/*...*/) - + if (isDark) { MarkdownBlockRenderer.dark(rendererExtensions) } else { @@ -156,5 +157,6 @@ own inline renderer, this is something to be careful about. You can see this in action running the Standalone sample, and selecting Markdown from the top-left menu. -The following image shows Jewel Markdown rendering this very Jewel Markdown README. -![Image showing the Markdown showcase from the Jewel standalone sample](https://github.com/JetBrains/jewel/assets/19003/67e2cc4e-c9b8-454b-884a-bba526ad2fe4) +The following image shows the Jewel Markdown renderer displaying the Jewel readme. + +![Image showing the Markdown renderer page from the Jewel standalone sample](../art/docs/markdown-renderer.png) diff --git a/platform/jewel/markdown/core/api/core.api b/platform/jewel/markdown/core/api/core.api index 0c38528e9c194..cc78884d4b6f6 100644 --- a/platform/jewel/markdown/core/api/core.api +++ b/platform/jewel/markdown/core/api/core.api @@ -321,10 +321,16 @@ public final class org/jetbrains/jewel/markdown/processing/MarkdownProcessor { public fun ()V public fun (Ljava/util/List;Lorg/jetbrains/jewel/markdown/MarkdownMode;Lorg/commonmark/parser/Parser;)V public synthetic fun (Ljava/util/List;Lorg/jetbrains/jewel/markdown/MarkdownMode;Lorg/commonmark/parser/Parser;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getExtensions ()Ljava/util/List; public final fun processChildren (Lorg/commonmark/node/Node;)Ljava/util/List; public final fun processMarkdownDocument (Ljava/lang/String;)Ljava/util/List; } +public final class org/jetbrains/jewel/markdown/processing/ProcessingUtilKt { + public static final fun readInlineContent (Lorg/commonmark/node/Node;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;)Ljava/util/List; + public static final fun toInlineMarkdownOrNull (Lorg/commonmark/node/Node;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;)Lorg/jetbrains/jewel/markdown/InlineMarkdown; +} + public class org/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer : org/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/markdown/rendering/DefaultInlineMarkdownRenderer$Companion; @@ -340,6 +346,11 @@ public class org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer public fun (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Ljava/util/List;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;)V public synthetic fun (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Ljava/util/List;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V protected final fun MaybeScrollingContainer (ZLandroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public fun createCopy (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Ljava/util/List;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;)Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer; + public fun getInlineRenderer ()Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer; + public fun getRendererExtensions ()Ljava/util/List; + public fun getRootStyling ()Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling; + public fun plus (Lorg/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension;)Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer; public fun render (Ljava/util/List;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$BlockQuote;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$BlockQuote;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V @@ -354,8 +365,8 @@ public class org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$ListItem;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$Paragraph;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Paragraph;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V - public fun render-EWr_ITI (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V public fun renderThematicBreak (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$ThematicBreak;Landroidx/compose/runtime/Composer;I)V + public fun renderWithMimeType-EWr_ITI (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V } public abstract interface class org/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer { @@ -393,6 +404,11 @@ public final class org/jetbrains/jewel/markdown/rendering/InlinesStyling$Compani public abstract interface class org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer { public static final field Companion Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer$Companion; + public abstract fun createCopy (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Ljava/util/List;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;)Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer; + public abstract fun getInlineRenderer ()Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer; + public abstract fun getRendererExtensions ()Ljava/util/List; + public abstract fun getRootStyling ()Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling; + public abstract fun plus (Lorg/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension;)Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer; public abstract fun render (Ljava/util/List;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public abstract fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$BlockQuote;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$BlockQuote;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public abstract fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V @@ -413,6 +429,10 @@ public abstract interface class org/jetbrains/jewel/markdown/rendering/MarkdownB public final class org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer$Companion { } +public final class org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer$DefaultImpls { + public static synthetic fun createCopy$default (Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Ljava/util/List;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer; +} + public final class org/jetbrains/jewel/markdown/rendering/MarkdownStyling { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Companion; @@ -783,7 +803,7 @@ public class org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRende public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$IndentedCodeBlock;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Indented;Landroidx/compose/runtime/Composer;I)V public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$Heading;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Heading$HN;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$Paragraph;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Paragraph;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V - public fun render-EWr_ITI (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V + public fun renderWithMimeType-EWr_ITI (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V } public abstract class org/jetbrains/jewel/markdown/scrolling/ScrollingSynchronizer { diff --git a/platform/jewel/markdown/core/build.gradle.kts b/platform/jewel/markdown/core/build.gradle.kts index 135428bcd81ad..19e3dfb770d5b 100644 --- a/platform/jewel/markdown/core/build.gradle.kts +++ b/platform/jewel/markdown/core/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { } publicApiValidation { - // TODO Oleg remove this once migrated to value classes excludedClassRegexes = setOf("org.jetbrains.jewel.markdown.MarkdownBlock.*") } diff --git a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt index ea1796f76fc2f..d0f19f7df8686 100644 --- a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt +++ b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt @@ -56,7 +56,7 @@ import org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer */ @ExperimentalJewelApi public class MarkdownProcessor( - private val extensions: List = emptyList(), + public val extensions: List = emptyList(), private val markdownMode: MarkdownMode = MarkdownMode.Standalone, private val commonMarkParser: Parser = MarkdownParserFactory.create(markdownMode is MarkdownMode.EditorPreview, extensions), @@ -267,14 +267,14 @@ public class MarkdownProcessor( } private fun Paragraph.toMarkdownParagraph(): MarkdownBlock.Paragraph = - MarkdownBlock.Paragraph(readInlineContent().toList()) + MarkdownBlock.Paragraph(readInlineContent(this@MarkdownProcessor).toList()) private fun BlockQuote.toMarkdownBlockQuote(): MarkdownBlock.BlockQuote = MarkdownBlock.BlockQuote(processChildren(this)) private fun Heading.toMarkdownHeadingOrNull(): MarkdownBlock.Heading? { if (level < 1 || level > 6) return null - return MarkdownBlock.Heading(inlineContent = readInlineContent().toList(), level = level) + return MarkdownBlock.Heading(inlineContent = readInlineContent(this@MarkdownProcessor).toList(), level = level) } private fun FencedCodeBlock.toMarkdownCodeBlockOrNull(): CodeBlock.FencedCodeBlock = @@ -345,7 +345,5 @@ public class MarkdownProcessor( return MarkdownBlock.HtmlBlock(literal.trimEnd('\n')) } - private fun Block.readInlineContent() = readInlineContent(this@MarkdownProcessor, extensions) - private data class State(val lines: List, val blocks: List, val indexes: List>) } diff --git a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/ProcessingUtil.kt b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/ProcessingUtil.kt index b0f65dcfb9509..fee1d83bcf505 100644 --- a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/ProcessingUtil.kt +++ b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/ProcessingUtil.kt @@ -11,53 +11,39 @@ import org.commonmark.node.Node import org.commonmark.node.SoftLineBreak as CMSoftLineBreak import org.commonmark.node.StrongEmphasis as CMStrongEmphasis import org.commonmark.node.Text as CMText -import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.jewel.markdown.InlineMarkdown import org.jetbrains.jewel.markdown.WithInlineMarkdown import org.jetbrains.jewel.markdown.WithTextContent -import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension -@VisibleForTesting -internal fun Node.readInlineContent( - markdownProcessor: MarkdownProcessor, - extensions: List, -): List = buildList { +public fun Node.readInlineContent(markdownProcessor: MarkdownProcessor): List = buildList { var current = this@readInlineContent.firstChild while (current != null) { - val inline = current.toInlineMarkdownOrNull(markdownProcessor, extensions) + val inline = current.toInlineMarkdownOrNull(markdownProcessor) if (inline != null) add(inline) current = current.next } } -@VisibleForTesting -internal fun Node.toInlineMarkdownOrNull( - markdownProcessor: MarkdownProcessor, - extensions: List, -) = +public fun Node.toInlineMarkdownOrNull(markdownProcessor: MarkdownProcessor): InlineMarkdown? = when (this) { is CMText -> InlineMarkdown.Text(literal) is CMLink -> InlineMarkdown.Link( destination = destination, title = title, - inlineContent = readInlineContent(markdownProcessor, extensions), + inlineContent = readInlineContent(markdownProcessor), ) is CMEmphasis -> - InlineMarkdown.Emphasis( - delimiter = openingDelimiter, - inlineContent = readInlineContent(markdownProcessor, extensions), - ) + InlineMarkdown.Emphasis(delimiter = openingDelimiter, inlineContent = readInlineContent(markdownProcessor)) - is CMStrongEmphasis -> - InlineMarkdown.StrongEmphasis(openingDelimiter, readInlineContent(markdownProcessor, extensions)) + is CMStrongEmphasis -> InlineMarkdown.StrongEmphasis(openingDelimiter, readInlineContent(markdownProcessor)) is CMCode -> InlineMarkdown.Code(literal) is CMHtmlInline -> InlineMarkdown.HtmlInline(literal) is CMImage -> { - val inlineContent = readInlineContent(markdownProcessor, extensions) + val inlineContent = readInlineContent(markdownProcessor) InlineMarkdown.Image( source = destination, alt = inlineContent.renderAsSimpleText().trim(), @@ -69,7 +55,7 @@ internal fun Node.toInlineMarkdownOrNull( is CMHardLineBreak -> InlineMarkdown.HardLineBreak is CMSoftLineBreak -> InlineMarkdown.SoftLineBreak is CMCustomNode -> - extensions + markdownProcessor.extensions .find { it.inlineProcessorExtension?.canProcess(this) == true } ?.inlineProcessorExtension ?.processInlineMarkdown(this, markdownProcessor) diff --git a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt index 08ed08e3de867..a34f852a1f3d7 100644 --- a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt +++ b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt @@ -60,11 +60,15 @@ import org.jetbrains.jewel.ui.component.Divider import org.jetbrains.jewel.ui.component.HorizontallyScrollableContainer import org.jetbrains.jewel.ui.component.Text +/** + * Default implementation of [MarkdownBlockRenderer] that uses the provided styling, extensions, and inline renderer to + * render [MarkdownBlock]s into Compose UI elements. + */ @ExperimentalJewelApi public open class DefaultMarkdownBlockRenderer( - private val rootStyling: MarkdownStyling, - private val rendererExtensions: List = emptyList(), - private val inlineRenderer: InlineMarkdownRenderer = DefaultInlineMarkdownRenderer(rendererExtensions), + override val rootStyling: MarkdownStyling, + override val rendererExtensions: List = emptyList(), + override val inlineRenderer: InlineMarkdownRenderer = DefaultInlineMarkdownRenderer(rendererExtensions), ) : MarkdownBlockRenderer { @Composable override fun render( @@ -365,7 +369,7 @@ public open class DefaultMarkdownBlockRenderer( ) } - render(block, mimeType, styling) + renderWithMimeType(block, mimeType, styling) if (styling.infoPosition.verticalAlignment == Alignment.Bottom) { FencedBlockInfo( @@ -381,7 +385,11 @@ public open class DefaultMarkdownBlockRenderer( } @Composable - public open fun render(block: FencedCodeBlock, mimeType: MimeType, styling: MarkdownStyling.Code.Fenced) { + public open fun renderWithMimeType( + block: FencedCodeBlock, + mimeType: MimeType, + styling: MarkdownStyling.Code.Fenced, + ) { val content = block.content val highlightedCode by LocalCodeHighlighter.current.highlight(content, mimeType).collectAsState(AnnotatedString(content)) @@ -451,4 +459,19 @@ public open class DefaultMarkdownBlockRenderer( content() } } + + public override fun createCopy( + rootStyling: MarkdownStyling?, + rendererExtensions: List?, + inlineRenderer: InlineMarkdownRenderer?, + ): MarkdownBlockRenderer = + DefaultMarkdownBlockRenderer( + rootStyling ?: this.rootStyling, + rendererExtensions ?: this.rendererExtensions, + inlineRenderer ?: this.inlineRenderer, + ) + + @ExperimentalJewelApi + override operator fun plus(extension: MarkdownRendererExtension): MarkdownBlockRenderer = + DefaultMarkdownBlockRenderer(rootStyling, rendererExtensions = rendererExtensions + extension, inlineRenderer) } diff --git a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer.kt b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer.kt index b66ebc4979a6a..13704adedf7ee 100644 --- a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer.kt +++ b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer.kt @@ -13,9 +13,20 @@ import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock.OrderedList import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock.UnorderedList import org.jetbrains.jewel.markdown.MarkdownBlock.ListItem import org.jetbrains.jewel.markdown.MarkdownBlock.Paragraph - +import org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension + +/** + * Renders one or more [MarkdownBlock]s into a Compose UI. + * + * @see render + */ +@Suppress("ComposableNaming") @ExperimentalJewelApi public interface MarkdownBlockRenderer { + public val rootStyling: MarkdownStyling + public val rendererExtensions: List + public val inlineRenderer: InlineMarkdownRenderer + @Composable public fun render( blocks: List, @@ -103,5 +114,17 @@ public interface MarkdownBlockRenderer { @Composable public fun render(block: HtmlBlock, styling: MarkdownStyling.HtmlBlock) + /** + * Creates a copy of this instance, using the provided non-null parameters, or the current values for the null ones. + */ + public fun createCopy( + rootStyling: MarkdownStyling? = null, + rendererExtensions: List? = null, + inlineRenderer: InlineMarkdownRenderer? = null, + ): MarkdownBlockRenderer + + /** Creates a copy of this [MarkdownBlockRenderer] with the same properties, plus the provided [extension]. */ + @ExperimentalJewelApi public operator fun plus(extension: MarkdownRendererExtension): MarkdownBlockRenderer + public companion object } diff --git a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer.kt b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer.kt index a1dc3ce32ff77..18717c8151431 100644 --- a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer.kt +++ b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer.kt @@ -74,11 +74,11 @@ public open class ScrollSyncMarkdownBlockRenderer( } @Composable - override fun render(block: FencedCodeBlock, mimeType: MimeType, styling: MarkdownStyling.Code.Fenced) { + override fun renderWithMimeType(block: FencedCodeBlock, mimeType: MimeType, styling: MarkdownStyling.Code.Fenced) { val synchronizer = (JewelTheme.markdownMode as? MarkdownMode.EditorPreview)?.scrollingSynchronizer ?: run { - super.render(block, mimeType, styling) + super.renderWithMimeType(block, mimeType, styling) return } diff --git a/platform/jewel/markdown/extension/gfm-alerts/build.gradle.kts b/platform/jewel/markdown/extension/gfm-alerts/build.gradle.kts index 918e727a6e249..802a08600d14c 100644 --- a/platform/jewel/markdown/extension/gfm-alerts/build.gradle.kts +++ b/platform/jewel/markdown/extension/gfm-alerts/build.gradle.kts @@ -13,7 +13,6 @@ dependencies { } publicApiValidation { - // TODO Oleg remove this once migrated to value classes excludedClassRegexes = setOf("org.jetbrains.jewel.markdown.extensions.github.alerts.*") } diff --git a/platform/jewel/markdown/extension/gfm-tables/README.md b/platform/jewel/markdown/extension/gfm-tables/README.md new file mode 100644 index 0000000000000..8c6bc26f05aae --- /dev/null +++ b/platform/jewel/markdown/extension/gfm-tables/README.md @@ -0,0 +1,43 @@ +# GitHub Flavored Markdown — tables extension + +This extension adds support for tables, a [GFM extension](https://github.github.com/gfm/#tables-extension-) over +CommonMark. Tables are parsed by the `commonmark-ext-gfm-tables` library, and rendered using a themed +[`BasicTableLayout`](../../../foundation/src/main/kotlin/org/jetbrains/jewel/foundation/layout/BasicTableLayout.kt). + +The default table styling matches the GitHub styling: + +| Light theme | Dark theme | +|-------------------------------------------------------------------------|-----------------------------------------------------------------------| +| ![Screenshot in the light theme](../../../art/docs/gfm-table-light.png) | ![Screenshot in the dark theme](../../../art/docs/gfm-table-dark.png) | + +## Usage + +To use the tables extension, you need to add the `GitHubTableProcessorExtension` to your `MarkdownProcessor`, and the +`GitHubTableRendererExtension` to the `MarkdownBlockRenderer`. For example, in standalone mode: + +```kotlin +val isDark = JewelTheme.isDark + +val markdownStyling = remember(isDark) { if (isDark) MarkdownStyling.dark() else MarkdownStyling.light() } + +val processor = remember { MarkdownProcessor(listOf(GitHubTableProcessorExtension)) } + +val blockRenderer = + remember(markdownStyling) { + if (isDark) { + MarkdownBlockRenderer.dark( + styling = markdownStyling, + rendererExtensions = listOf(GitHubTableRendererExtension(GfmTableStyling.dark(), markdownStyling)), + ) + } else { + MarkdownBlockRenderer.light( + styling = markdownStyling, + rendererExtensions = listOf(GitHubTableRendererExtension(GfmTableStyling.light(), markdownStyling)), + ) + } + } + +ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { + // Your UI that renders Markdown goes here +} +``` diff --git a/platform/jewel/markdown/extension/gfm-tables/api/gfm-tables.api b/platform/jewel/markdown/extension/gfm-tables/api/gfm-tables.api new file mode 100644 index 0000000000000..80434a86cf946 --- /dev/null +++ b/platform/jewel/markdown/extension/gfm-tables/api/gfm-tables.api @@ -0,0 +1,79 @@ +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors { + public static final field $stable I + public static final field Companion Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors$Companion; + public synthetic fun (JJJLorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getAlternateRowBackgroundColor-0d7_KjU ()J + public final fun getBorderColor-0d7_KjU ()J + public final fun getRowBackgroundColor-0d7_KjU ()J + public final fun getRowBackgroundStyle ()Lorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors$Companion { +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics { + public static final field $stable I + public static final field Companion Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics$Companion; + public synthetic fun (FLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Horizontal;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getBorderWidth-D9Ej5fM ()F + public final fun getCellPadding ()Landroidx/compose/foundation/layout/PaddingValues; + public final fun getDefaultCellContentAlignment ()Landroidx/compose/ui/Alignment$Horizontal; + public final fun getHeaderDefaultCellContentAlignment ()Landroidx/compose/ui/Alignment$Horizontal; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics$Companion { +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling { + public static final field $stable I + public static final field Companion Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling$Companion; + public synthetic fun (JJJLorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle;FLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/text/font/FontWeight;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics;Landroidx/compose/ui/text/font/FontWeight;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getColors ()Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors; + public final fun getHeaderBaseFontWeight ()Landroidx/compose/ui/text/font/FontWeight; + public final fun getMetrics ()Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling$Companion { +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableBlockRenderer : org/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension { + public static final field $stable I + public fun (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling;)V + public fun canRender (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CustomBlock;)Z + public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CustomBlock;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension : org/jetbrains/jewel/markdown/extensions/MarkdownProcessorExtension { + public static final field $stable I + public static final field INSTANCE Lorg/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension; + public fun getBlockProcessorExtension ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockProcessorExtension; + public fun getInlineProcessorExtension ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownInlineProcessorExtension; + public fun getParserExtension ()Lorg/commonmark/parser/Parser$ParserExtension; + public fun getTextRendererExtension ()Lorg/commonmark/renderer/text/TextContentRenderer$TextContentRendererExtension; +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableRendererExtension : org/jetbrains/jewel/markdown/extensions/MarkdownRendererExtension { + public static final field $stable I + public fun (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;)V + public fun getBlockRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownBlockRendererExtension; + public fun getInlineRenderer ()Lorg/jetbrains/jewel/markdown/extensions/MarkdownInlineRendererExtension; +} + +public final class org/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle : java/lang/Enum { + public static final field Normal Lorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle; + public static final field Striped Lorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle; + public static fun values ()[Lorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle; +} + diff --git a/platform/jewel/markdown/extension/gfm-tables/build.gradle.kts b/platform/jewel/markdown/extension/gfm-tables/build.gradle.kts new file mode 100644 index 0000000000000..b73c9d355ad4e --- /dev/null +++ b/platform/jewel/markdown/extension/gfm-tables/build.gradle.kts @@ -0,0 +1,27 @@ +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag + +plugins { + jewel + `jewel-publish` + `jewel-check-public-api` + alias(libs.plugins.composeDesktop) + alias(libs.plugins.compose.compiler) +} + +dependencies { + implementation(projects.markdown.core) + implementation(libs.commonmark.ext.gfm.tables) + + testImplementation(compose.desktop.uiTestJUnit4) +} + +publicApiValidation { + excludedClassRegexes = setOf("org.jetbrains.jewel.markdown.extensions.github.tables.*") +} + +publishing.publications.named("main") { + val ijpTarget = project.property("ijp.target") as String + artifactId = "jewel-markdown-extension-${project.name}-$ijpTarget" +} + +composeCompiler { featureFlags.add(ComposeFeatureFlag.OptimizeNonSkippingGroups) } diff --git a/platform/jewel/markdown/extension/gfm-tables/intellij.platform.jewel.markdown.extension.gfmTables.iml b/platform/jewel/markdown/extension/gfm-tables/intellij.platform.jewel.markdown.extension.gfmTables.iml new file mode 100644 index 0000000000000..185fa1181782b --- /dev/null +++ b/platform/jewel/markdown/extension/gfm-tables/intellij.platform.jewel.markdown.extension.gfmTables.iml @@ -0,0 +1,375 @@ + + + + + + + + + + + + + + + + + $MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.1.0/kotlin-compose-compiler-plugin-2.1.0.jar + $MAVEN_REPOSITORY$/dev/drewhamilton/poko/poko-compiler-plugin/0.18.2/poko-compiler-plugin-0.18.2.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + c53f675b2af4696e6022598145a44a9e9feb35216a188163ae5bf99a18e76bdb + + + + + + + + + + + + + + + + + + f1e334b73d49ceab00afe776a4c393de77b4fc805e9ff488dac596a2663b1dc1 + + + + + + + + + + + + + + + + + + 66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9 + + + + + + + + + + + + + + + + + + a7dfdfd2b9d6668c646275948eba357aab6407f635f2dc09fe90258a2f202337 + + + + + + + + + + + + + + + + + + 34dfab5b4fa8b2913f00facdddc049e2fef4b176f8e7cde0ad306485674c3b5c + + + + + + + + + + + + + + + + + + 5c84c3bb1c1b636ae36030a0f98d4629afa29e192a9c78f57f22ce2c8ff6bc96 + + + + + + + + + + + + + + + + + + c106c0f705717889663555fe4cb7742612bd47b56a128bf7a9f589f24c64cb08 + + + + + + + + + + + + + + + + + + 8ff9a2b8c93b0287d8fa837d5b0917923168cb97dea1ada1a8d4aa7bb17c9f15 + + + + + + + + + + + + + + + + + + 41da5d61c56877b5b5d01e2a2bcb967cbfaf4d45c1a1aa124c0601c7a3de74e7 + + + + + + + + + + + + + + + + + + 7b0f19724082cbfcbc66e5abea2b9bc92cf08a1ea11e191933ed43801eb3cd05 + + + + + + + + + + + + + + + + + + bff3d1e895fd5abd54ee725dab59214acabf900e1a0784544d20cf20521bf9f3 + + + + + + + + + + + + + + + + + + 62c816073195cc0119dc1d66b178544bdb772ef8bee4879954528c6798b17ebe + + + + + + + + + + + + + + + + + + 5c84c3bb1c1b636ae36030a0f98d4629afa29e192a9c78f57f22ce2c8ff6bc96 + + + + + + + + + + + + + + + + + + c106c0f705717889663555fe4cb7742612bd47b56a128bf7a9f589f24c64cb08 + + + + + + + + + + + + + + + + + + 1b409141a9ee6ed38e76a8fb8f9cf280030bf45375ba7e07e60b2742039b573e + + + + + + + + + + + + + + + + + + c5cfad0b125e978d915f8b4c38630bc5d548e59fe8a0e838c0cc84283c605943 + + + + + + + + + + + + + + + + + + 5c84c3bb1c1b636ae36030a0f98d4629afa29e192a9c78f57f22ce2c8ff6bc96 + + + + + + + + + + + + + + + + + + + + + b54dc332f931e6d07c2766144c087b08f3693677e368151a67020b4e95bb4b99 + + + 679338e0b7fc15c02d275d598654b01a149893bc28a87992e90123c8d06af25b + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableBlockRenderer.kt b/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableBlockRenderer.kt new file mode 100644 index 0000000000000..380a7abab8928 --- /dev/null +++ b/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableBlockRenderer.kt @@ -0,0 +1,198 @@ +package org.jetbrains.jewel.markdown.extensions.github.tables + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import org.jetbrains.jewel.foundation.ExperimentalJewelApi +import org.jetbrains.jewel.foundation.layout.BasicTableLayout +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.markdown.MarkdownBlock +import org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock +import org.jetbrains.jewel.markdown.extensions.MarkdownBlockRendererExtension +import org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer +import org.jetbrains.jewel.markdown.rendering.InlinesStyling +import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer +import org.jetbrains.jewel.markdown.rendering.MarkdownStyling + +@OptIn(ExperimentalJewelApi::class) +public class GitHubTableBlockRenderer( + private val rootStyling: MarkdownStyling, + private val tableStyling: GfmTableStyling, +) : MarkdownBlockRendererExtension { + override fun canRender(block: CustomBlock): Boolean = block is TableBlock + + @Composable + override fun render( + block: CustomBlock, + blockRenderer: MarkdownBlockRenderer, + inlineRenderer: InlineMarkdownRenderer, + enabled: Boolean, + onUrlClick: (String) -> Unit, + onTextClick: () -> Unit, + ) { + val tableBlock = block as TableBlock + + // Headers usually have a tweaked font weight + val headerRootStyling = + remember(JewelTheme.name, blockRenderer, tableStyling.headerBaseFontWeight) { + val rootStyling = blockRenderer.rootStyling + val semiboldInlinesStyling = + rootStyling.paragraph.inlinesStyling.withFontWeight(tableStyling.headerBaseFontWeight) + + // Given cells can only contain inlines, and not block-level nodes, we are ok with + // only overriding the Paragraph styling. + MarkdownStyling( + rootStyling.blockVerticalSpacing, + MarkdownStyling.Paragraph(semiboldInlinesStyling), + rootStyling.heading, + rootStyling.blockQuote, + rootStyling.code, + rootStyling.list, + rootStyling.image, + rootStyling.thematicBreak, + rootStyling.htmlBlock, + ) + } + + val headerRenderer = remember(headerRootStyling) { blockRenderer.createCopy(rootStyling = headerRootStyling) } + + val rows = + remember(block, blockRenderer, inlineRenderer, tableStyling) { + val headerCells = + tableBlock.header.cells.map Unit> { cell -> + { + HeaderCell( + cell = cell, + backgroundColor = tableStyling.colors.rowBackgroundColor, + padding = tableStyling.metrics.cellPadding, + defaultAlignment = tableStyling.metrics.headerDefaultCellContentAlignment, + blockRenderer = headerRenderer, + enabled = enabled, + onUrlClick = onUrlClick, + onTextClick = onTextClick, + ) + } + } + + val rowsCells = + tableBlock.rows.map Unit>> { row -> + row.cells.map Unit> { cell -> + { + val backgroundColor = + if (tableStyling.colors.rowBackgroundStyle == RowBackgroundStyle.Striped) { + if (cell.rowIndex % 2 == 0) { + tableStyling.colors.alternateRowBackgroundColor + } else { + tableStyling.colors.rowBackgroundColor + } + } else { + tableStyling.colors.rowBackgroundColor + } + + Cell( + cell = cell, + backgroundColor = backgroundColor, + padding = tableStyling.metrics.cellPadding, + defaultAlignment = tableStyling.metrics.defaultCellContentAlignment, + blockRenderer = blockRenderer, + enabled = enabled, + onUrlClick = onUrlClick, + onTextClick = onTextClick, + ) + } + } + } + + listOf(headerCells) + rowsCells + } + + BasicTableLayout( + rowCount = tableBlock.rowCount, + columnCount = tableBlock.columnCount, + cellBorderColor = tableStyling.colors.borderColor, + cellBorderWidth = tableStyling.metrics.borderWidth, + rows = rows, + ) + } + + private fun InlinesStyling.withFontWeight(newFontWeight: FontWeight) = + InlinesStyling( + textStyle = textStyle.copy(fontWeight = newFontWeight), + inlineCode = inlineCode.copy(fontWeight = newFontWeight), + link = link.copy(fontWeight = newFontWeight), + linkDisabled = linkDisabled.copy(fontWeight = newFontWeight), + linkHovered = linkHovered.copy(fontWeight = newFontWeight), + linkFocused = linkFocused.copy(fontWeight = newFontWeight), + linkPressed = linkPressed.copy(fontWeight = newFontWeight), + linkVisited = linkVisited.copy(fontWeight = newFontWeight), + emphasis = emphasis.copy(fontWeight = newFontWeight), + strongEmphasis = strongEmphasis.copy(fontWeight = newFontWeight), + inlineHtml = inlineHtml.copy(fontWeight = newFontWeight), + renderInlineHtml = renderInlineHtml, + ) + + @Composable + private fun HeaderCell( + cell: TableCell, + backgroundColor: Color, + padding: PaddingValues, + defaultAlignment: Alignment.Horizontal, + blockRenderer: MarkdownBlockRenderer, + enabled: Boolean, + onUrlClick: (String) -> Unit, + onTextClick: () -> Unit, + ) { + Box( + modifier = Modifier.background(backgroundColor).padding(padding), + contentAlignment = (cell.alignment ?: defaultAlignment).asContentAlignment(), + ) { + blockRenderer.render( + MarkdownBlock.Paragraph(cell.content), + blockRenderer.rootStyling.paragraph, + enabled, + onUrlClick, + onTextClick, + ) + } + } + + @Composable + private fun Cell( + cell: TableCell, + backgroundColor: Color, + padding: PaddingValues, + defaultAlignment: Alignment.Horizontal, + blockRenderer: MarkdownBlockRenderer, + enabled: Boolean, + onUrlClick: (String) -> Unit, + onTextClick: () -> Unit, + ) { + Box( + modifier = Modifier.background(backgroundColor).padding(padding), + contentAlignment = (cell.alignment ?: defaultAlignment).asContentAlignment(), + ) { + blockRenderer.render( + MarkdownBlock.Paragraph(cell.content), + rootStyling.paragraph, + enabled, + onUrlClick, + onTextClick, + ) + } + } + + private fun Alignment.Horizontal.asContentAlignment() = + when { + this == Alignment.Start -> Alignment.TopStart + this == Alignment.CenterHorizontally -> Alignment.TopCenter + this == Alignment.End -> Alignment.TopEnd + else -> error("Unsupported alignment: $this") + } +} diff --git a/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension.kt b/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension.kt new file mode 100644 index 0000000000000..21dc1543b80e6 --- /dev/null +++ b/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension.kt @@ -0,0 +1,159 @@ +package org.jetbrains.jewel.markdown.extensions.github.tables + +import androidx.compose.ui.Alignment +import org.commonmark.ext.gfm.tables.TableBlock as CommonMarkTableBlock +import org.commonmark.ext.gfm.tables.TableBody as CommonMarkTableBody +import org.commonmark.ext.gfm.tables.TableCell as CommonMarkTableCell +import org.commonmark.ext.gfm.tables.TableCell.Alignment.CENTER +import org.commonmark.ext.gfm.tables.TableCell.Alignment.LEFT +import org.commonmark.ext.gfm.tables.TableCell.Alignment.RIGHT +import org.commonmark.ext.gfm.tables.TableHead as CommonMarkTableHeader +import org.commonmark.ext.gfm.tables.TableRow as CommonMarkTableRow +import org.commonmark.ext.gfm.tables.TablesExtension +import org.commonmark.node.CustomBlock +import org.commonmark.node.Node +import org.commonmark.parser.Parser.Builder +import org.commonmark.parser.Parser.ParserExtension +import org.commonmark.renderer.text.TextContentRenderer +import org.commonmark.renderer.text.TextContentRenderer.TextContentRendererExtension +import org.jetbrains.jewel.foundation.ExperimentalJewelApi +import org.jetbrains.jewel.markdown.InlineMarkdown +import org.jetbrains.jewel.markdown.MarkdownBlock +import org.jetbrains.jewel.markdown.extensions.MarkdownBlockProcessorExtension +import org.jetbrains.jewel.markdown.extensions.MarkdownBlockRendererExtension +import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension +import org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension +import org.jetbrains.jewel.markdown.processing.MarkdownProcessor +import org.jetbrains.jewel.markdown.processing.readInlineContent +import org.jetbrains.jewel.markdown.rendering.MarkdownStyling + +@OptIn(ExperimentalJewelApi::class) +public object GitHubTableProcessorExtension : MarkdownProcessorExtension { + override val parserExtension: ParserExtension = GitHubTablesCommonMarkExtension + override val textRendererExtension: TextContentRendererExtension = GitHubTablesCommonMarkExtension + + override val blockProcessorExtension: MarkdownBlockProcessorExtension = GitHubTablesProcessorExtension + + private object GitHubTablesProcessorExtension : MarkdownBlockProcessorExtension { + override fun canProcess(block: CustomBlock): Boolean = block is CommonMarkTableBlock + + override fun processMarkdownBlock( + block: CustomBlock, + processor: MarkdownProcessor, + ): MarkdownBlock.CustomBlock? { + val tableBlock = block as CommonMarkTableBlock + val header = tableBlock.firstChild as? CommonMarkTableHeader ?: return null + + val body = tableBlock.lastChild as? CommonMarkTableBody + + return try { + TableBlock( + TableHeader( + // The header contains only one CommonMarkTableRow + (header.firstChild as CommonMarkTableRow).mapCellsIndexed { columnIndex, cell -> + TableCell( + rowIndex = 0, + columnIndex = columnIndex, + content = cell.readInlineContent(processor), + alignment = getAlignment(cell), + ) + } + ), + body + ?.mapRowsIndexed { rowIndex, row -> + TableRow( + rowIndex = rowIndex, + row.mapCellsIndexed { columnIndex, cell -> + TableCell( + rowIndex = rowIndex + 1, // The header is row zero + columnIndex = columnIndex, + content = cell.readInlineContent(processor), + alignment = getAlignment(cell), + ) + }, + ) + } + .orEmpty(), + ) + } catch (_: IllegalArgumentException) { + null + } + } + + private fun getAlignment(cell: CommonMarkTableCell) = + when (cell.alignment) { + LEFT -> Alignment.Start + CENTER -> Alignment.CenterHorizontally + RIGHT -> Alignment.End + null -> null + } + + private inline fun CommonMarkTableRow.mapCellsIndexed( + mapper: (Int, CommonMarkTableCell) -> TableCell + ): List = buildList { + forEachChildIndexed { index, child -> if (child is CommonMarkTableCell) add(mapper(index, child)) } + } + + private inline fun CommonMarkTableBody.mapRowsIndexed( + mapper: (Int, CommonMarkTableRow) -> TableRow + ): List = buildList { + forEachChildIndexed { index, child -> if (child is CommonMarkTableRow) add(mapper(index, child)) } + } + + private inline fun Node.forEachChildIndexed(action: (Int, Node) -> Unit) { + var child = firstChild + + var index = 0 + while (child != null) { + action(index, child) + index++ + child = child.next + } + } + } +} + +private object GitHubTablesCommonMarkExtension : ParserExtension, TextContentRendererExtension { + override fun extend(parserBuilder: Builder) { + parserBuilder.extensions(listOf(TablesExtension.create())) + } + + override fun extend(rendererBuilder: TextContentRenderer.Builder) { + rendererBuilder.extensions(listOf(TablesExtension.create())) + } +} + +@OptIn(ExperimentalJewelApi::class) +public class GitHubTableRendererExtension(tableStyling: GfmTableStyling, rootStyling: MarkdownStyling) : + MarkdownRendererExtension { + override val blockRenderer: MarkdownBlockRendererExtension = GitHubTableBlockRenderer(rootStyling, tableStyling) +} + +internal data class TableBlock(val header: TableHeader, val rows: List) : MarkdownBlock.CustomBlock { + val rowCount: Int = rows.size + 1 // We always have a header + val columnCount: Int + + init { + require(header.cells.isNotEmpty()) { "Header cannot be empty" } + val headerColumns = header.cells.size + + if (rows.isNotEmpty()) { + val bodyColumns = rows.first().cells.size + require(rows.all { it.cells.size == bodyColumns }) { "Inconsistent cell count in table body" } + require(headerColumns == bodyColumns) { "Inconsistent cell count between table body and header" } + } + + columnCount = headerColumns + } +} + +internal data class TableHeader(val cells: List) : CustomBlock() + +internal data class TableRow(val rowIndex: Int, val cells: List) : CustomBlock() + +internal data class TableCell( + val rowIndex: Int, + val columnIndex: Int, + val content: List, + val alignment: Alignment.Horizontal?, +) : CustomBlock() diff --git a/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableStyling.kt b/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableStyling.kt new file mode 100644 index 0000000000000..238e14ccc90fa --- /dev/null +++ b/platform/jewel/markdown/extension/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableStyling.kt @@ -0,0 +1,72 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the +// Apache 2.0 license. +package org.jetbrains.jewel.markdown.extensions.github.tables + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import org.jetbrains.jewel.foundation.ExperimentalJewelApi +import org.jetbrains.jewel.foundation.GenerateDataFunctions + +@GenerateDataFunctions +public class GfmTableStyling( + public val colors: GfmTableColors, + public val metrics: GfmTableMetrics, + public val headerBaseFontWeight: FontWeight, +) { + @ExperimentalJewelApi + public constructor( + borderColor: Color, + rowBackgroundColor: Color, + alternateRowBackgroundColor: Color, + rowBackgroundStyle: RowBackgroundStyle, + borderWidth: Dp, + cellPadding: PaddingValues, + defaultCellContentAlignment: Alignment.Horizontal, + headerDefaultCellContentAlignment: Alignment.Horizontal, + headerBaseFontWeight: FontWeight, + ) : this( + colors = GfmTableColors(borderColor, rowBackgroundColor, alternateRowBackgroundColor, rowBackgroundStyle), + metrics = + GfmTableMetrics(borderWidth, cellPadding, defaultCellContentAlignment, headerDefaultCellContentAlignment), + headerBaseFontWeight = headerBaseFontWeight, + ) + + public companion object +} + +@GenerateDataFunctions +public class GfmTableColors( + public val borderColor: Color, + public val rowBackgroundColor: Color, + public val alternateRowBackgroundColor: Color, + public val rowBackgroundStyle: RowBackgroundStyle, +) { + public companion object +} + +public enum class RowBackgroundStyle { + /** + * All rows have the same background color, [GfmTableColors.rowBackgroundColor]. In this style, + * [GfmTableColors.alternateRowBackgroundColor] is ignored. + */ + Normal, + + /** + * Rows have alternate colors. Odd rows use [GfmTableColors.rowBackgroundColor] and even rows use + * [GfmTableColors.alternateRowBackgroundColor]. + */ + Striped, +} + +@GenerateDataFunctions +public class GfmTableMetrics( + public val borderWidth: Dp, + public val cellPadding: PaddingValues, + public val defaultCellContentAlignment: Alignment.Horizontal, + public val headerDefaultCellContentAlignment: Alignment.Horizontal, +) { + public companion object +} diff --git a/platform/jewel/markdown/ide-laf-bridge-styling/api/ide-laf-bridge-styling.api b/platform/jewel/markdown/ide-laf-bridge-styling/api/ide-laf-bridge-styling.api index f6d7cfbc72f6b..c8fd643b876d4 100644 --- a/platform/jewel/markdown/ide-laf-bridge-styling/api/ide-laf-bridge-styling.api +++ b/platform/jewel/markdown/ide-laf-bridge-styling/api/ide-laf-bridge-styling.api @@ -66,3 +66,12 @@ public final class org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/ public static synthetic fun create-gaOEZmc$default (Lorg/jetbrains/jewel/markdown/extensions/github/alerts/WarningAlertStyling$Companion;Landroidx/compose/foundation/layout/PaddingValues;FJLandroidx/compose/ui/graphics/PathEffect;ILandroidx/compose/ui/text/TextStyle;Lorg/jetbrains/jewel/ui/icon/IconKey;JJILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/alerts/WarningAlertStyling; } +public final class org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/github/tables/BridgeGitHubTableStylingKt { + public static final fun create (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling$Companion;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics;Landroidx/compose/ui/text/font/FontWeight;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling; + public static synthetic fun create$default (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling$Companion;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics;Landroidx/compose/ui/text/font/FontWeight;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling; + public static final fun create-f1JAnFk (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors$Companion;JJJLorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors; + public static synthetic fun create-f1JAnFk$default (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors$Companion;JJJLorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors; + public static final fun create-rAjV9yQ (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics$Companion;FLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Horizontal;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics; + public static synthetic fun create-rAjV9yQ$default (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics$Companion;FLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Horizontal;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics; +} + diff --git a/platform/jewel/markdown/ide-laf-bridge-styling/build.gradle.kts b/platform/jewel/markdown/ide-laf-bridge-styling/build.gradle.kts index b421ece07a4a2..0763451712b5e 100644 --- a/platform/jewel/markdown/ide-laf-bridge-styling/build.gradle.kts +++ b/platform/jewel/markdown/ide-laf-bridge-styling/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { api(projects.markdown.core) api(projects.ideLafBridge) compileOnly(projects.markdown.extension.gfmAlerts) + compileOnly(projects.markdown.extension.gfmTables) intellijPlatform { intellijIdeaCommunity(libs.versions.idea) diff --git a/platform/jewel/markdown/ide-laf-bridge-styling/intellij.platform.jewel.markdown.ideLafBridgeStyling.iml b/platform/jewel/markdown/ide-laf-bridge-styling/intellij.platform.jewel.markdown.ideLafBridgeStyling.iml index 387d0cfaed4fb..1e17770b6a539 100644 --- a/platform/jewel/markdown/ide-laf-bridge-styling/intellij.platform.jewel.markdown.ideLafBridgeStyling.iml +++ b/platform/jewel/markdown/ide-laf-bridge-styling/intellij.platform.jewel.markdown.ideLafBridgeStyling.iml @@ -353,5 +353,6 @@ + \ No newline at end of file diff --git a/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/Utils.kt b/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/Utils.kt new file mode 100644 index 0000000000000..64923dd1cdd73 --- /dev/null +++ b/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/Utils.kt @@ -0,0 +1,5 @@ +package org.jetbrains.jewel.intui.markdown.bridge.styling + +import com.intellij.ui.JBColor + +internal val isLightTheme = JBColor.isBright() diff --git a/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/github/alerts/BridgeGitHubAlertStyling.kt b/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/github/alerts/BridgeGitHubAlertStyling.kt index 9fcefc8236d44..89f626451107d 100644 --- a/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/github/alerts/BridgeGitHubAlertStyling.kt +++ b/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/github/alerts/BridgeGitHubAlertStyling.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.intellij.ui.JBColor +import org.jetbrains.jewel.intui.markdown.bridge.styling.isLightTheme import org.jetbrains.jewel.markdown.extensions.github.alerts.AlertStyling import org.jetbrains.jewel.markdown.extensions.github.alerts.CautionAlertStyling import org.jetbrains.jewel.markdown.extensions.github.alerts.GitHubAlertIcons @@ -29,7 +29,7 @@ public fun AlertStyling.Companion.create( public fun NoteAlertStyling.Companion.create( padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), lineWidth: Dp = 3.dp, - lineColor: Color = if (JBColor.isBright()) Color(0xFF0969DA) else Color(0xFF1F6EEB), + lineColor: Color = if (isLightTheme) Color(0xFF0969DA) else Color(0xFF1F6EEB), pathEffect: PathEffect? = null, strokeCap: StrokeCap = StrokeCap.Square, titleTextStyle: TextStyle = TextStyle(fontWeight = FontWeight.Medium, color = lineColor), @@ -52,7 +52,7 @@ public fun NoteAlertStyling.Companion.create( public fun TipAlertStyling.Companion.create( padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), lineWidth: Dp = 3.dp, - lineColor: Color = if (JBColor.isBright()) Color(0xFF1F883D) else Color(0xFF238636), + lineColor: Color = if (isLightTheme) Color(0xFF1F883D) else Color(0xFF238636), pathEffect: PathEffect? = null, strokeCap: StrokeCap = StrokeCap.Square, titleTextStyle: TextStyle = TextStyle(fontWeight = FontWeight.Medium, color = lineColor), @@ -75,7 +75,7 @@ public fun TipAlertStyling.Companion.create( public fun ImportantAlertStyling.Companion.create( padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), lineWidth: Dp = 3.dp, - lineColor: Color = if (JBColor.isBright()) Color(0xFF8250DF) else Color(0xFF8957E5), + lineColor: Color = if (isLightTheme) Color(0xFF8250DF) else Color(0xFF8957E5), pathEffect: PathEffect? = null, strokeCap: StrokeCap = StrokeCap.Square, titleTextStyle: TextStyle = TextStyle(fontWeight = FontWeight.Medium, color = lineColor), @@ -98,7 +98,7 @@ public fun ImportantAlertStyling.Companion.create( public fun WarningAlertStyling.Companion.create( padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), lineWidth: Dp = 3.dp, - lineColor: Color = if (JBColor.isBright()) Color(0xFF9A6601) else Color(0xFF9E6A02), + lineColor: Color = if (isLightTheme) Color(0xFF9A6601) else Color(0xFF9E6A02), pathEffect: PathEffect? = null, strokeCap: StrokeCap = StrokeCap.Square, titleTextStyle: TextStyle = TextStyle(fontWeight = FontWeight.Medium, color = lineColor), @@ -121,7 +121,7 @@ public fun WarningAlertStyling.Companion.create( public fun CautionAlertStyling.Companion.create( padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), lineWidth: Dp = 3.dp, - lineColor: Color = if (JBColor.isBright()) Color(0xFFCF222E) else Color(0xFFDA3633), + lineColor: Color = if (isLightTheme) Color(0xFFCF222E) else Color(0xFFDA3633), pathEffect: PathEffect? = null, strokeCap: StrokeCap = StrokeCap.Square, titleTextStyle: TextStyle = TextStyle(fontWeight = FontWeight.Medium, color = lineColor), diff --git a/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/github/tables/BridgeGitHubTableStyling.kt b/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/github/tables/BridgeGitHubTableStyling.kt new file mode 100644 index 0000000000000..e8a1009637cad --- /dev/null +++ b/platform/jewel/markdown/ide-laf-bridge-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/bridge/styling/extensions/github/tables/BridgeGitHubTableStyling.kt @@ -0,0 +1,35 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the +// Apache 2.0 license. +package org.jetbrains.jewel.intui.markdown.bridge.styling.extensions.github.tables + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.intui.markdown.bridge.styling.isLightTheme +import org.jetbrains.jewel.markdown.extensions.github.tables.GfmTableColors +import org.jetbrains.jewel.markdown.extensions.github.tables.GfmTableMetrics +import org.jetbrains.jewel.markdown.extensions.github.tables.GfmTableStyling +import org.jetbrains.jewel.markdown.extensions.github.tables.RowBackgroundStyle + +public fun GfmTableStyling.Companion.create( + colors: GfmTableColors = GfmTableColors.create(), + metrics: GfmTableMetrics = GfmTableMetrics.create(), + headerBaseFontWeight: FontWeight = FontWeight.SemiBold, +): GfmTableStyling = GfmTableStyling(colors, metrics, headerBaseFontWeight) + +public fun GfmTableColors.Companion.create( + borderColor: Color = if (isLightTheme) Color(0xffd1d9e0) else Color(0xff3d444d), + rowBackgroundColor: Color = Color.Unspecified, + alternateRowBackgroundColor: Color = if (isLightTheme) Color(0xfff6f8fa) else Color(0xff151b23), + rowBackgroundStyle: RowBackgroundStyle = RowBackgroundStyle.Striped, +) = GfmTableColors(borderColor, rowBackgroundColor, alternateRowBackgroundColor, rowBackgroundStyle) + +public fun GfmTableMetrics.Companion.create( + borderWidth: Dp = 1.dp, + cellPadding: PaddingValues = PaddingValues(horizontal = 13.dp, vertical = 6.dp), + defaultCellContentAlignment: Alignment.Horizontal = Alignment.Start, + headerDefaultCellContentAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, +) = GfmTableMetrics(borderWidth, cellPadding, defaultCellContentAlignment, headerDefaultCellContentAlignment) diff --git a/platform/jewel/markdown/int-ui-standalone-styling/api/int-ui-standalone-styling.api b/platform/jewel/markdown/int-ui-standalone-styling/api/int-ui-standalone-styling.api index 4e4693c68c7cd..4eacec1d82d24 100644 --- a/platform/jewel/markdown/int-ui-standalone-styling/api/int-ui-standalone-styling.api +++ b/platform/jewel/markdown/int-ui-standalone-styling/api/int-ui-standalone-styling.api @@ -118,3 +118,16 @@ public final class org/jetbrains/jewel/intui/markdown/standalone/styling/extensi public static synthetic fun light-gaOEZmc$default (Lorg/jetbrains/jewel/markdown/extensions/github/alerts/WarningAlertStyling$Companion;Landroidx/compose/foundation/layout/PaddingValues;FJLandroidx/compose/ui/graphics/PathEffect;ILandroidx/compose/ui/text/TextStyle;Lorg/jetbrains/jewel/ui/icon/IconKey;JJILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/alerts/WarningAlertStyling; } +public final class org/jetbrains/jewel/intui/markdown/standalone/styling/extensions/github/tables/IntUiGitHubTableStylingKt { + public static final fun dark (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling$Companion;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics;Landroidx/compose/ui/text/font/FontWeight;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling; + public static synthetic fun dark$default (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling$Companion;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics;Landroidx/compose/ui/text/font/FontWeight;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling; + public static final fun dark-f1JAnFk (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors$Companion;JJJLorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors; + public static synthetic fun dark-f1JAnFk$default (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors$Companion;JJJLorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors; + public static final fun defaults-rAjV9yQ (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics$Companion;FLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Horizontal;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics; + public static synthetic fun defaults-rAjV9yQ$default (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics$Companion;FLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Alignment$Horizontal;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics; + public static final fun light (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling$Companion;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics;Landroidx/compose/ui/text/font/FontWeight;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling; + public static synthetic fun light$default (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling$Companion;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors;Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableMetrics;Landroidx/compose/ui/text/font/FontWeight;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableStyling; + public static final fun light-f1JAnFk (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors$Companion;JJJLorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors; + public static synthetic fun light-f1JAnFk$default (Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors$Companion;JJJLorg/jetbrains/jewel/markdown/extensions/github/tables/RowBackgroundStyle;ILjava/lang/Object;)Lorg/jetbrains/jewel/markdown/extensions/github/tables/GfmTableColors; +} + diff --git a/platform/jewel/markdown/int-ui-standalone-styling/build.gradle.kts b/platform/jewel/markdown/int-ui-standalone-styling/build.gradle.kts index b80d7926a182c..15e3061835ad4 100644 --- a/platform/jewel/markdown/int-ui-standalone-styling/build.gradle.kts +++ b/platform/jewel/markdown/int-ui-standalone-styling/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { api(projects.markdown.core) api(projects.intUi.intUiStandalone) compileOnly(projects.markdown.extension.gfmAlerts) + compileOnly(projects.markdown.extension.gfmTables) testImplementation(compose.desktop.uiTestJUnit4) } diff --git a/platform/jewel/markdown/int-ui-standalone-styling/intellij.platform.jewel.markdown.intUiStandaloneStyling.iml b/platform/jewel/markdown/int-ui-standalone-styling/intellij.platform.jewel.markdown.intUiStandaloneStyling.iml index 71810cfebe47b..4e6875943a92f 100644 --- a/platform/jewel/markdown/int-ui-standalone-styling/intellij.platform.jewel.markdown.intUiStandaloneStyling.iml +++ b/platform/jewel/markdown/int-ui-standalone-styling/intellij.platform.jewel.markdown.intUiStandaloneStyling.iml @@ -349,5 +349,6 @@ + \ No newline at end of file diff --git a/platform/jewel/markdown/int-ui-standalone-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/standalone/styling/extensions/github/tables/IntUiGitHubTableStyling.kt b/platform/jewel/markdown/int-ui-standalone-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/standalone/styling/extensions/github/tables/IntUiGitHubTableStyling.kt new file mode 100644 index 0000000000000..7c6667c2e6d56 --- /dev/null +++ b/platform/jewel/markdown/int-ui-standalone-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/standalone/styling/extensions/github/tables/IntUiGitHubTableStyling.kt @@ -0,0 +1,45 @@ +package org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.github.tables + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.markdown.extensions.github.tables.GfmTableColors +import org.jetbrains.jewel.markdown.extensions.github.tables.GfmTableMetrics +import org.jetbrains.jewel.markdown.extensions.github.tables.GfmTableStyling +import org.jetbrains.jewel.markdown.extensions.github.tables.RowBackgroundStyle + +public fun GfmTableStyling.Companion.light( + colors: GfmTableColors = GfmTableColors.light(), + metrics: GfmTableMetrics = GfmTableMetrics.defaults(), + headerBaseFontWeight: FontWeight = FontWeight.SemiBold, +): GfmTableStyling = GfmTableStyling(colors, metrics, headerBaseFontWeight) + +public fun GfmTableStyling.Companion.dark( + colors: GfmTableColors = GfmTableColors.dark(), + metrics: GfmTableMetrics = GfmTableMetrics.defaults(), + headerBaseFontWeight: FontWeight = FontWeight.SemiBold, +): GfmTableStyling = GfmTableStyling(colors, metrics, headerBaseFontWeight) + +public fun GfmTableColors.Companion.light( + borderColor: Color = Color(0xffd1d9e0), + rowBackgroundColor: Color = Color.Unspecified, + alternateRowBackgroundColor: Color = Color(0xfff6f8fa), + rowBackgroundStyle: RowBackgroundStyle = RowBackgroundStyle.Striped, +) = GfmTableColors(borderColor, rowBackgroundColor, alternateRowBackgroundColor, rowBackgroundStyle) + +public fun GfmTableColors.Companion.dark( + borderColor: Color = Color(0xff3d444d), + rowBackgroundColor: Color = Color.Unspecified, + alternateRowBackgroundColor: Color = Color(0xff151b23), + rowBackgroundStyle: RowBackgroundStyle = RowBackgroundStyle.Striped, +) = GfmTableColors(borderColor, rowBackgroundColor, alternateRowBackgroundColor, rowBackgroundStyle) + +public fun GfmTableMetrics.Companion.defaults( + borderWidth: Dp = 1.dp, + cellPadding: PaddingValues = PaddingValues(horizontal = 13.dp, vertical = 6.dp), + defaultCellContentAlignment: Alignment.Horizontal = Alignment.Start, + headerDefaultCellContentAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, +) = GfmTableMetrics(borderWidth, cellPadding, defaultCellContentAlignment, headerDefaultCellContentAlignment) diff --git a/platform/jewel/samples/standalone/build.gradle.kts b/platform/jewel/samples/standalone/build.gradle.kts index 64734a8c55286..442c490073f89 100644 --- a/platform/jewel/samples/standalone/build.gradle.kts +++ b/platform/jewel/samples/standalone/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(projects.markdown.intUiStandaloneStyling) implementation(projects.markdown.extension.gfmAlerts) implementation(projects.markdown.extension.autolink) + implementation(projects.markdown.extension.gfmTables) implementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") } implementation(compose.components.resources) implementation(libs.intellijPlatform.icons) @@ -62,4 +63,4 @@ tasks { setExecutable(javaLauncher.map { it.executablePath.asFile.absolutePath }.get()) } } -} \ No newline at end of file +} diff --git a/platform/jewel/samples/standalone/intellij.platform.jewel.samples.standalone.iml b/platform/jewel/samples/standalone/intellij.platform.jewel.samples.standalone.iml index 66308dfbc703f..47ee186bc7f78 100644 --- a/platform/jewel/samples/standalone/intellij.platform.jewel.samples.standalone.iml +++ b/platform/jewel/samples/standalone/intellij.platform.jewel.samples.standalone.iml @@ -137,5 +137,6 @@ + \ No newline at end of file diff --git a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownEditor.kt b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownEditor.kt index 73252dad6058e..13cf6b3d0f2cd 100644 --- a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownEditor.kt +++ b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownEditor.kt @@ -2,7 +2,6 @@ package org.jetbrains.jewel.samples.standalone.view.markdown import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -47,16 +46,14 @@ internal fun MarkdownEditor(state: TextFieldState, modifier: Modifier = Modifier @Composable private fun ControlsRow(modifier: Modifier = Modifier, onLoadMarkdown: (String) -> Unit) { - Row( - modifier.horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { + Row(modifier.horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically) { var showFilePicker by remember { mutableStateOf(false) } OutlinedButton(onClick = { showFilePicker = true }, modifier = Modifier.padding(start = 2.dp)) { Text("Load file...") } + Spacer(Modifier.width(10.dp)) + FilePicker(show = showFilePicker, fileExtensions = listOf("md")) { platformFile -> showFilePicker = false diff --git a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownPreview.kt b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownPreview.kt index ce782d8a2c373..08d5238797a41 100644 --- a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownPreview.kt +++ b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownPreview.kt @@ -24,14 +24,18 @@ import org.jetbrains.jewel.intui.markdown.standalone.light import org.jetbrains.jewel.intui.markdown.standalone.styling.dark import org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.github.alerts.dark import org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.github.alerts.light +import org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.github.tables.dark +import org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.github.tables.light import org.jetbrains.jewel.intui.markdown.standalone.styling.light import org.jetbrains.jewel.markdown.LazyMarkdown import org.jetbrains.jewel.markdown.MarkdownBlock import org.jetbrains.jewel.markdown.extension.autolink.AutolinkProcessorExtension -import org.jetbrains.jewel.markdown.extensions.LocalMarkdownMode import org.jetbrains.jewel.markdown.extensions.github.alerts.AlertStyling import org.jetbrains.jewel.markdown.extensions.github.alerts.GitHubAlertProcessorExtension import org.jetbrains.jewel.markdown.extensions.github.alerts.GitHubAlertRendererExtension +import org.jetbrains.jewel.markdown.extensions.github.tables.GfmTableStyling +import org.jetbrains.jewel.markdown.extensions.github.tables.GitHubTableProcessorExtension +import org.jetbrains.jewel.markdown.extensions.github.tables.GitHubTableRendererExtension import org.jetbrains.jewel.markdown.processing.MarkdownProcessor import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer import org.jetbrains.jewel.markdown.rendering.MarkdownStyling @@ -45,12 +49,15 @@ internal fun MarkdownPreview(modifier: Modifier = Modifier, rawMarkdown: CharSeq val markdownStyling = remember(isDark) { if (isDark) MarkdownStyling.dark() else MarkdownStyling.light() } var markdownBlocks by remember { mutableStateOf(emptyList()) } - val extensions = remember { listOf(GitHubAlertProcessorExtension, AutolinkProcessorExtension) } - val markdownMode = LocalMarkdownMode.current + // We are doing this here for the sake of simplicity. // In a real-world scenario you would be doing this outside your Composables, // potentially involving ViewModels, dependency injection, etc. - val processor = remember { MarkdownProcessor(extensions, markdownMode = markdownMode) } + val processor = remember { + MarkdownProcessor( + listOf(GitHubAlertProcessorExtension, AutolinkProcessorExtension, GitHubTableProcessorExtension) + ) + } LaunchedEffect(rawMarkdown) { // TODO you may want to debounce or drop on backpressure, in real usages. You should also @@ -61,16 +68,24 @@ internal fun MarkdownPreview(modifier: Modifier = Modifier, rawMarkdown: CharSeq } val blockRenderer = - remember(markdownStyling, extensions) { + remember(markdownStyling) { if (isDark) { MarkdownBlockRenderer.dark( styling = markdownStyling, - rendererExtensions = listOf(GitHubAlertRendererExtension(AlertStyling.dark(), markdownStyling)), + rendererExtensions = + listOf( + GitHubAlertRendererExtension(AlertStyling.dark(), markdownStyling), + GitHubTableRendererExtension(GfmTableStyling.dark(), markdownStyling), + ), ) } else { MarkdownBlockRenderer.light( styling = markdownStyling, - rendererExtensions = listOf(GitHubAlertRendererExtension(AlertStyling.light(), markdownStyling)), + rendererExtensions = + listOf( + GitHubAlertRendererExtension(AlertStyling.light(), markdownStyling), + GitHubTableRendererExtension(GfmTableStyling.light(), markdownStyling), + ), ) } } diff --git a/platform/jewel/settings.gradle.kts b/platform/jewel/settings.gradle.kts index c6fba3a6f1645..b7a3a1796155f 100644 --- a/platform/jewel/settings.gradle.kts +++ b/platform/jewel/settings.gradle.kts @@ -12,6 +12,9 @@ pluginManagement { gradlePluginPortal() mavenCentral() } + plugins { + kotlin("jvm") version "2.1.0" + } } dependencyResolutionManagement { @@ -28,6 +31,7 @@ dependencyResolutionManagement { plugins { id("com.gradle.enterprise") version "3.15.1" id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" + id("org.danilopianini.gradle-pre-commit-git-hooks") version "2.0.9" } include( @@ -40,6 +44,7 @@ include( ":markdown:core", ":markdown:extension:autolink", ":markdown:extension:gfm-alerts", + ":markdown:extension:gfm-tables", ":markdown:int-ui-standalone-styling", ":markdown:ide-laf-bridge-styling", ":samples:ide-plugin", @@ -48,8 +53,6 @@ include( ":ui-test", ) -project(":ide-laf-bridge-tests").projectDir = file("ide-laf-bridge/ide-laf-bridge-tests") - gradleEnterprise { buildScan { publishAlwaysIf(System.getenv("CI") == "true") @@ -58,4 +61,46 @@ gradleEnterprise { } } -include("ui-tests") +val isWindows + get() = System.getProperty("os.name").contains("win", true) + +val gradleCommand: String by +lazy(LazyThreadSafetyMode.NONE) { + val gradlewFilename = + if (isWindows) { + "gradlew.bat" + } else { + "gradlew" + } + + val gradlew = File(rootProject.projectDir, gradlewFilename) + if (gradlew.exists() && gradlew.isFile && gradlew.canExecute()) { + logger.info("Using gradlew wrapper at ${gradlew.invariantSeparatorsPath}") + gradlew.invariantSeparatorsPath + } else { + "gradle" + } +} + +val shebang = if (isWindows) "" else "#!/bin/sh" + +gitHooks { + hook("pre-push") { + from(shebang) { + // language=Shell Script + """ + |#### Note: this hook was autogenerated. You can edit it in settings.gradle.kts + |GRADLEW=$gradleCommand + |if ! ${'$'}GRADLEW ktfmtCheck ; then + | ${'$'}GRADLEW ktfmtFormat + | echo 1>&2 "\nktfmt found problems; commit the result and re-push" + | exit 1 + |fi + | + """ + .trimMargin() + } + } + + createHooks(overwriteExisting = true) +}