diff --git a/.idea/modules.xml b/.idea/modules.xml
index c9d414abd3cec..9dc3ce9e528ac 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -1389,6 +1389,8 @@
+
+
diff --git a/BUILD.bazel b/BUILD.bazel
index 5344e377b6d58..cbec2484886fb 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -963,6 +963,8 @@ jvm_library(
"//platform/jewel/markdown/core:jewel-markdown-core-tests_test_lib",
"//platform/jewel/markdown/extensions/autolink:jewel-markdown-extensions-autolink-tests",
"//platform/jewel/markdown/extensions/autolink:jewel-markdown-extensions-autolink-tests_test_lib",
+ "//platform/jewel/markdown/extensions/front-matter:jewel-markdown-extensions-frontMatter-tests",
+ "//platform/jewel/markdown/extensions/front-matter:jewel-markdown-extensions-frontMatter-tests_test_lib",
"//platform/jewel/markdown/extensions/gfm-alerts:jewel-markdown-extensions-gfmAlerts-tests",
"//platform/jewel/markdown/extensions/gfm-alerts:jewel-markdown-extensions-gfmAlerts-tests_test_lib",
"//platform/jewel/markdown/extensions/gfm-tables:jewel-markdown-extensions-gfmTables-tests",
diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt
index 3a4812f10c786..b4186579450bd 100644
--- a/build/bazel-generated-file-list.txt
+++ b/build/bazel-generated-file-list.txt
@@ -656,6 +656,7 @@ platform/jewel/int-ui/int-ui-standalone
platform/jewel/int-ui/int-ui-standalone-tests
platform/jewel/markdown/core
platform/jewel/markdown/extensions/autolink
+platform/jewel/markdown/extensions/front-matter
platform/jewel/markdown/extensions/gfm-alerts
platform/jewel/markdown/extensions/gfm-strikethrough
platform/jewel/markdown/extensions/gfm-tables
diff --git a/intellij.idea.community.main.tests.iml b/intellij.idea.community.main.tests.iml
index 6e2b14fbfcc6c..0ef2710778655 100644
--- a/intellij.idea.community.main.tests.iml
+++ b/intellij.idea.community.main.tests.iml
@@ -210,6 +210,7 @@
+
diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt
index 668c97c30e266..3508f804988dc 100644
--- a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt
+++ b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt
@@ -320,6 +320,7 @@ object CommunityModuleSets {
module("intellij.platform.jewel.markdown.extensions.gfmAlerts")
module("intellij.platform.jewel.markdown.extensions.gfmTables")
module("intellij.platform.jewel.markdown.extensions.gfmStrikethrough")
+ module("intellij.platform.jewel.markdown.extensions.frontMatter")
module("intellij.platform.jewel.markdown.extensions.images")
module("intellij.platform.jewel.markdown.core")
}
diff --git a/platform/compose/markdown/BUILD.bazel b/platform/compose/markdown/BUILD.bazel
index d337b5a10cb40..565799e90cbec 100644
--- a/platform/compose/markdown/BUILD.bazel
+++ b/platform/compose/markdown/BUILD.bazel
@@ -19,6 +19,7 @@ jvm_library(
"//platform/jewel/markdown/extensions/autolink",
"//platform/jewel/markdown/extensions/gfm-alerts",
"//platform/jewel/markdown/extensions/gfm-strikethrough",
+ "//platform/jewel/markdown/extensions/front-matter",
"//platform/jewel/markdown/extensions/gfm-tables",
"//platform/jewel/markdown/extensions/images",
"//platform/jewel/markdown/ide-laf-bridge-styling",
@@ -29,6 +30,7 @@ jvm_library(
"//platform/jewel/markdown/extensions/autolink",
"//platform/jewel/markdown/extensions/gfm-alerts",
"//platform/jewel/markdown/extensions/gfm-strikethrough",
+ "//platform/jewel/markdown/extensions/front-matter",
"//platform/jewel/markdown/extensions/gfm-tables",
"//platform/jewel/markdown/extensions/images",
"//platform/jewel/markdown/ide-laf-bridge-styling",
@@ -52,6 +54,8 @@ jvm_library(
"//platform/jewel/markdown/extensions/gfm-alerts:gfm-alerts_test_lib",
"//platform/jewel/markdown/extensions/gfm-strikethrough",
"//platform/jewel/markdown/extensions/gfm-strikethrough:gfm-strikethrough_test_lib",
+ "//platform/jewel/markdown/extensions/front-matter",
+ "//platform/jewel/markdown/extensions/front-matter:front-matter_test_lib",
"//platform/jewel/markdown/extensions/gfm-tables",
"//platform/jewel/markdown/extensions/gfm-tables:gfm-tables_test_lib",
"//platform/jewel/markdown/extensions/images",
@@ -66,6 +70,7 @@ jvm_library(
"//platform/jewel/markdown/extensions/autolink:autolink_test_lib",
"//platform/jewel/markdown/extensions/gfm-alerts:gfm-alerts_test_lib",
"//platform/jewel/markdown/extensions/gfm-strikethrough:gfm-strikethrough_test_lib",
+ "//platform/jewel/markdown/extensions/front-matter:front-matter_test_lib",
"//platform/jewel/markdown/extensions/gfm-tables:gfm-tables_test_lib",
"//platform/jewel/markdown/extensions/images:images_test_lib",
"//platform/jewel/markdown/ide-laf-bridge-styling:ide-laf-bridge-styling_test_lib",
diff --git a/platform/compose/markdown/intellij.platform.compose.markdown.iml b/platform/compose/markdown/intellij.platform.compose.markdown.iml
index c87e8d54ee67f..cea53fa454d96 100644
--- a/platform/compose/markdown/intellij.platform.compose.markdown.iml
+++ b/platform/compose/markdown/intellij.platform.compose.markdown.iml
@@ -33,6 +33,7 @@
+
diff --git a/platform/compose/markdown/resources/intellij.platform.compose.markdown.xml b/platform/compose/markdown/resources/intellij.platform.compose.markdown.xml
index 0b8a412ede405..3c43e474e3e12 100644
--- a/platform/compose/markdown/resources/intellij.platform.compose.markdown.xml
+++ b/platform/compose/markdown/resources/intellij.platform.compose.markdown.xml
@@ -6,6 +6,7 @@
+
diff --git a/platform/jewel/markdown/extensions/front-matter/BUILD.bazel b/platform/jewel/markdown/extensions/front-matter/BUILD.bazel
new file mode 100644
index 0000000000000..95f30de3d50ce
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/BUILD.bazel
@@ -0,0 +1,144 @@
+### auto-generated section `build intellij.platform.jewel.markdown.extensions.frontMatter` start
+load("//build:compiler-options.bzl", "create_kotlinc_options")
+load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
+
+create_kotlinc_options(
+ name = "custom_front-matter",
+ opt_in = [
+ "androidx.compose.ui.ExperimentalComposeUiApi",
+ "androidx.compose.foundation.ExperimentalFoundationApi",
+ "org.jetbrains.jewel.foundation.ExperimentalJewelApi",
+ "org.jetbrains.jewel.foundation.InternalJewelApi",
+ ],
+ x_context_parameters = True,
+ x_explicit_api_mode = "strict"
+)
+
+resourcegroup(
+ name = "front-matter_resources",
+ srcs = glob(["src/main/resources/**/*"]),
+ strip_prefix = "src/main/resources"
+)
+
+jvm_library(
+ name = "front-matter",
+ module_name = "intellij.platform.jewel.markdown.extensions.frontMatter",
+ visibility = ["//visibility:public"],
+ srcs = glob(["src/main/kotlin/**/*.kt", "src/main/kotlin/**/*.java", "src/main/kotlin/**/*.form"], allow_empty = True),
+ resources = [":front-matter_resources"],
+ kotlinc_opts = ":custom_front-matter",
+ deps = [
+ "@lib//:kotlin-stdlib",
+ "//libraries/kotlinx/coroutines/core",
+ "@lib//:jetbrains-annotations",
+ "//platform/jewel/markdown/core",
+ "//platform/jewel/markdown/extensions/gfm-tables",
+ "//platform/jewel/ui",
+ "//platform/jewel/foundation",
+ "//libraries/compose-foundation-desktop",
+ "//libraries/compose-runtime-desktop",
+ ],
+ plugins = ["@lib//:compose-plugin"]
+)
+
+jvm_library(
+ name = "front-matter_test_lib",
+ module_name = "intellij.platform.jewel.markdown.extensions.frontMatter",
+ visibility = ["//visibility:public"],
+ srcs = glob([], allow_empty = True),
+ kotlinc_opts = ":custom_front-matter",
+ runtime_deps = [
+ ":front-matter",
+ "//libraries/kotlinx/coroutines/core:core_test_lib",
+ "//platform/jewel/markdown/core:core_test_lib",
+ "//platform/jewel/markdown/extensions/gfm-tables:gfm-tables_test_lib",
+ "//platform/jewel/ui:ui_test_lib",
+ "//platform/jewel/foundation:foundation_test_lib",
+ "//libraries/compose-foundation-desktop:compose-foundation-desktop_test_lib",
+ "//libraries/compose-runtime-desktop:compose-runtime-desktop_test_lib",
+ "//libraries/compose-foundation-desktop-junit",
+ "//libraries/compose-foundation-desktop-junit:compose-foundation-desktop-junit_test_lib",
+ "//libraries/junit4",
+ "//libraries/junit4:junit4_test_lib",
+ ],
+ plugins = ["@lib//:compose-plugin"]
+)
+### auto-generated section `build intellij.platform.jewel.markdown.extensions.frontMatter` end
+
+### auto-generated section `iml intellij.platform.jewel.markdown.extensions.frontMatter` start
+exports_files([
+ "intellij.platform.jewel.markdown.extensions.frontMatter.iml",
+], visibility = ["//visibility:public"])
+### auto-generated section `iml intellij.platform.jewel.markdown.extensions.frontMatter` end
+
+### auto-generated section `build intellij.platform.jewel.markdown.extensions.frontMatter.tests` start
+create_kotlinc_options(
+ name = "custom_jewel-markdown-extensions-frontMatter-tests",
+ opt_in = [
+ "androidx.compose.ui.ExperimentalComposeUiApi",
+ "androidx.compose.foundation.ExperimentalFoundationApi",
+ "org.jetbrains.jewel.foundation.ExperimentalJewelApi",
+ "org.jetbrains.jewel.foundation.InternalJewelApi",
+ ],
+ x_context_parameters = True,
+ x_explicit_api_mode = "strict"
+)
+
+jvm_library(
+ name = "jewel-markdown-extensions-frontMatter-tests",
+ module_name = "intellij.platform.jewel.markdown.extensions.frontMatter.tests",
+ visibility = ["//visibility:public"],
+ srcs = glob([], allow_empty = True),
+ plugins = ["@lib//:compose-plugin"]
+)
+
+jvm_library(
+ name = "jewel-markdown-extensions-frontMatter-tests_test_lib",
+ visibility = ["//visibility:public"],
+ srcs = glob(["src/test/kotlin/**/*.kt", "src/test/kotlin/**/*.java", "src/test/kotlin/**/*.form"], allow_empty = True),
+ kotlinc_opts = ":custom_jewel-markdown-extensions-frontMatter-tests",
+ associates = [
+ "//platform/jewel/markdown/extensions/front-matter",
+ "//platform/jewel/markdown/extensions/front-matter:front-matter_test_lib",
+ ],
+ deps = [
+ "@lib//:kotlin-stdlib",
+ "//libraries/kotlinx/coroutines/core",
+ "//libraries/kotlinx/coroutines/core:core_test_lib",
+ "@lib//:jetbrains-annotations",
+ "//platform/jewel/markdown/core",
+ "//platform/jewel/markdown/core:core_test_lib",
+ "//platform/jewel/markdown/extensions/gfm-tables",
+ "//platform/jewel/markdown/extensions/gfm-tables:gfm-tables_test_lib",
+ "//platform/jewel/ui",
+ "//platform/jewel/ui:ui_test_lib",
+ "//platform/jewel/foundation",
+ "//platform/jewel/foundation:foundation_test_lib",
+ "//libraries/compose-foundation-desktop",
+ "//libraries/compose-foundation-desktop:compose-foundation-desktop_test_lib",
+ "//libraries/compose-runtime-desktop",
+ "//libraries/compose-runtime-desktop:compose-runtime-desktop_test_lib",
+ "//libraries/compose-foundation-desktop-junit",
+ "//libraries/compose-foundation-desktop-junit:compose-foundation-desktop-junit_test_lib",
+ "//libraries/junit4",
+ "//libraries/junit4:junit4_test_lib",
+ ],
+ runtime_deps = [":jewel-markdown-extensions-frontMatter-tests"],
+ plugins = ["@lib//:compose-plugin"]
+)
+### auto-generated section `build intellij.platform.jewel.markdown.extensions.frontMatter.tests` end
+
+### auto-generated section `iml intellij.platform.jewel.markdown.extensions.frontMatter.tests` start
+exports_files([
+ "intellij.platform.jewel.markdown.extensions.frontMatter.tests.iml",
+], visibility = ["//visibility:public"])
+### auto-generated section `iml intellij.platform.jewel.markdown.extensions.frontMatter.tests` end
+
+### auto-generated section `test intellij.platform.jewel.markdown.extensions.frontMatter.tests` start
+load("@community//build:tests-options.bzl", "jps_test")
+
+jps_test(
+ name = "jewel-markdown-extensions-frontMatter-tests_test",
+ runtime_deps = [":jewel-markdown-extensions-frontMatter-tests_test_lib"]
+)
+### auto-generated section `test intellij.platform.jewel.markdown.extensions.frontMatter.tests` end
\ No newline at end of file
diff --git a/platform/jewel/markdown/extensions/front-matter/api-dump-experimental.txt b/platform/jewel/markdown/extensions/front-matter/api-dump-experimental.txt
new file mode 100644
index 0000000000000..d100ccac3a75a
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/api-dump-experimental.txt
@@ -0,0 +1,7 @@
+*f:org.jetbrains.jewel.markdown.extensions.frontmatter.FrontMatterProcessorExtension
+- org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension
+- sf:$stable:I
+- sf:INSTANCE:org.jetbrains.jewel.markdown.extensions.frontmatter.FrontMatterProcessorExtension
+- getBlockProcessorExtension():org.jetbrains.jewel.markdown.extensions.MarkdownBlockProcessorExtension
+- getParserExtension():org.commonmark.parser.Parser$ParserExtension
+- getTextRendererExtension():org.commonmark.renderer.text.TextContentRenderer$TextContentRendererExtension
diff --git a/platform/jewel/markdown/extensions/front-matter/api-dump.txt b/platform/jewel/markdown/extensions/front-matter/api-dump.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/platform/jewel/markdown/extensions/front-matter/build.gradle.kts b/platform/jewel/markdown/extensions/front-matter/build.gradle.kts
new file mode 100644
index 0000000000000..f963410a433f5
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/build.gradle.kts
@@ -0,0 +1,21 @@
+import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
+
+plugins {
+ jewel
+ `jewel-check-public-api`
+ alias(libs.plugins.composeDesktop)
+ alias(libs.plugins.compose.compiler)
+}
+
+dependencies {
+ implementation(projects.markdown.core)
+ implementation(projects.markdown.extensions.gfmTables)
+
+ testImplementation(compose.desktop.uiTestJUnit4)
+}
+
+publicApiValidation {
+ excludedClassRegexes = setOf("org.jetbrains.jewel.markdown.extensions.frontmatter.*")
+}
+
+composeCompiler { featureFlags.add(ComposeFeatureFlag.OptimizeNonSkippingGroups) }
diff --git a/platform/jewel/markdown/extensions/front-matter/exposed-third-party-api.txt b/platform/jewel/markdown/extensions/front-matter/exposed-third-party-api.txt
new file mode 100644
index 0000000000000..0d417a3c3847f
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/exposed-third-party-api.txt
@@ -0,0 +1 @@
+org/commonmark/**
diff --git a/platform/jewel/markdown/extensions/front-matter/intellij.platform.jewel.markdown.extensions.frontMatter.iml b/platform/jewel/markdown/extensions/front-matter/intellij.platform.jewel.markdown.extensions.frontMatter.iml
new file mode 100644
index 0000000000000..45017f543af6e
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/intellij.platform.jewel.markdown.extensions.frontMatter.iml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.3.20/kotlin-compose-compiler-plugin-2.3.20.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/jewel/markdown/extensions/front-matter/intellij.platform.jewel.markdown.extensions.frontMatter.tests.iml b/platform/jewel/markdown/extensions/front-matter/intellij.platform.jewel.markdown.extensions.frontMatter.tests.iml
new file mode 100644
index 0000000000000..1c602f5cce956
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/intellij.platform.jewel.markdown.extensions.frontMatter.tests.iml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.3.20/kotlin-compose-compiler-plugin-2.3.20.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-0.36.0.txt b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-0.36.0.txt
new file mode 100644
index 0000000000000..06dabba19c93d
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-0.36.0.txt
@@ -0,0 +1,12 @@
+// Signature format: 4.0
+package org.jetbrains.jewel.markdown.extensions.frontmatter {
+
+ @SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class FrontMatterProcessorExtension implements org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension {
+ property public org.jetbrains.jewel.markdown.extensions.MarkdownBlockProcessorExtension blockProcessorExtension;
+ property public org.commonmark.parser.Parser.ParserExtension parserExtension;
+ property public org.commonmark.renderer.text.TextContentRenderer.TextContentRendererExtension textRendererExtension;
+ field public static final org.jetbrains.jewel.markdown.extensions.frontmatter.FrontMatterProcessorExtension INSTANCE;
+ }
+
+}
+
diff --git a/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-stable-0.36.0.txt b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-stable-0.36.0.txt
new file mode 100644
index 0000000000000..e6f50d0d0fd11
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-stable-0.36.0.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-baseline-current.txt b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-baseline-current.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-baseline-stable-current.txt b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-baseline-stable-current.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/platform/jewel/markdown/extensions/front-matter/module-content.yaml b/platform/jewel/markdown/extensions/front-matter/module-content.yaml
new file mode 100644
index 0000000000000..ac81d8d6922c3
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/module-content.yaml
@@ -0,0 +1,3 @@
+- name: dist.all/lib/intellij.platform.jewel.markdown.extensions.frontMatter.jar
+ modules:
+ - name: intellij.platform.jewel.markdown.extensions.frontMatter
diff --git a/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterBlock.kt b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterBlock.kt
new file mode 100644
index 0000000000000..ea73bacccd00b
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterBlock.kt
@@ -0,0 +1,5 @@
+package org.jetbrains.jewel.markdown.extensions.frontmatter
+
+import org.commonmark.node.CustomBlock
+
+internal class FrontMatterBlock : CustomBlock()
diff --git a/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterBlockParser.kt b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterBlockParser.kt
new file mode 100644
index 0000000000000..74e700304c83b
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterBlockParser.kt
@@ -0,0 +1,278 @@
+package org.jetbrains.jewel.markdown.extensions.frontmatter
+
+import org.commonmark.node.Block
+import org.commonmark.parser.block.AbstractBlockParser
+import org.commonmark.parser.block.AbstractBlockParserFactory
+import org.commonmark.parser.block.BlockContinue
+import org.commonmark.parser.block.BlockStart
+import org.commonmark.parser.block.MatchedBlockParser
+import org.commonmark.parser.block.ParserState
+
+internal class FrontMatterBlockParser : AbstractBlockParser() {
+ private val fmBlock = FrontMatterBlock()
+
+ private var currentBlock: CurrentBlock = CurrentBlock.KeyValue()
+ set(value) {
+ flushCurrentKey()
+ field = value
+ }
+
+ private var done = false
+
+ override fun getBlock(): Block = fmBlock
+
+ override fun tryContinue(state: ParserState): BlockContinue? {
+ if (done) return BlockContinue.none()
+
+ val line = state.line.content.toString()
+
+ if (line.trimEnd() == "---") {
+ flushAll()
+ done = true
+ return BlockContinue.finished()
+ }
+
+ if (!parseLine(line)) return BlockContinue.none()
+
+ return BlockContinue.atIndex(state.index)
+ }
+
+ private fun parseLine(line: String): Boolean =
+ when (val block = currentBlock) {
+ is CurrentBlock.KeyValue -> parseKeyLine(line, block)
+ is CurrentBlock.Scalar -> parseScalarLine(line, block)
+ }
+
+ private fun parseScalarLine(line: String, block: CurrentBlock.Scalar): Boolean {
+ val trimmed = line.trimEnd()
+
+ val indent =
+ block.indent
+ ?: run {
+ // Detect indent from first non-empty content line
+ if (trimmed.isEmpty()) {
+ block.lines.add("")
+ return true
+ }
+ val indent = line.indentWidth()
+ if (indent < block.minimumIndent) {
+ flushAll()
+ val keyBlock = currentBlock as? CurrentBlock.KeyValue ?: return false
+ return parseKeyLine(line, keyBlock)
+ }
+ block.indent = indent
+ indent
+ }
+
+ // Blank lines are always part of the scalar content
+ if (trimmed.isEmpty()) {
+ block.lines.add("")
+ return true
+ }
+
+ val lineIndent = line.indentWidth()
+ if (lineIndent >= indent) {
+ block.lines.add(line.substring(indent))
+ return true
+ }
+
+ // Line is not indented enough -- end of block scalar
+ flushAll()
+ val keyBlock = currentBlock as? CurrentBlock.KeyValue ?: return false
+ return parseKeyLine(line, keyBlock)
+ }
+
+ private fun parseKeyLine(line: String, block: CurrentBlock.KeyValue): Boolean {
+ val trimmed = line.trimEnd()
+
+ if (trimmed.isBlank()) {
+ return true
+ }
+
+ val listItemMatch = LIST_ITEM_REGEX.matchEntire(trimmed)
+ // If a list item, add it to the current block
+ if (listItemMatch != null) {
+ if (block.currentKey == null) return false
+ block.currentIsList = true
+ block.currentValues.add(parseStringValue(listItemMatch.groupValues[1]))
+ return true
+ }
+
+ val keyValue = trimmed.split(':', limit = 2)
+ if (keyValue.size < 2) {
+ // not a "key: value" pair
+ return false
+ }
+
+ val (key, value) = keyValue.map { it.trim() }
+
+ if (key.isBlank() || key.startsWith('-')) {
+ return false
+ }
+
+ if (value.isBlank()) {
+ // Bare "key:" without the "value" -- could be on the next line
+ currentBlock = CurrentBlock.KeyValue(currentKey = key)
+ return true
+ }
+
+ // Check for the block scalar indicator (e.g. "key: >+"
+ val blockScalarMatch = BLOCK_SCALAR_REGEX.matchEntire(value)
+ if (blockScalarMatch != null) {
+ currentBlock =
+ CurrentBlock.Scalar(
+ currentKey = key,
+ indicator = blockScalarMatch.groupValues[1][0],
+ chomping = blockScalarMatch.groupValues[2].firstOrNull(),
+ minimumIndent = line.indentWidth() + 1,
+ )
+ return true
+ }
+
+ // Simple "key: value"
+ currentBlock = CurrentBlock.KeyValue(currentKey = key, currentValues = mutableListOf(parseStringValue(value)))
+ return true
+ }
+
+ private fun flushBlockScalar() {
+ val block = currentBlock as? CurrentBlock.Scalar ?: return
+ val indicator = block.indicator
+ val chomping = block.chomping
+ val lines = block.lines.toMutableList()
+
+ // Count and remove trailing blank lines
+ var trailingBlanks = 0
+ for (i in lines.lastIndex downTo 0) {
+ if (lines[i].isNotEmpty()) break
+ trailingBlanks++
+ }
+
+ // Build the content text (without trailing blanks)
+ if (trailingBlanks == lines.size) {
+ // Empty block scalar -- no content
+ currentBlock =
+ CurrentBlock.KeyValue(
+ currentKey = block.currentKey,
+ currentValues = block.currentValues.toMutableList(),
+ )
+ return
+ }
+
+ val contentLines = lines.subList(0, lines.size - trailingBlanks)
+
+ val contentText =
+ when (indicator) {
+ '|' -> contentLines.joinToString("\n")
+ '>' -> foldLines(contentLines)
+ else -> contentLines.joinToString("\n")
+ }
+
+ // Apply chomping to determine trailing newlines
+ val value =
+ when (chomping) {
+ '-' -> contentText
+ '+' -> contentText + "\n".repeat(trailingBlanks + 1)
+ else -> contentText + "\n" // clip: single trailing newline
+ }
+
+ currentBlock = CurrentBlock.KeyValue(currentKey = block.currentKey, currentValues = mutableListOf(value))
+ }
+
+ private fun foldLines(lines: List): String {
+ return buildString {
+ var i = 0
+ var hasTextOnLine = false
+ while (i < lines.size) {
+ val line = lines[i]
+ if (line.isEmpty()) {
+ // Blank line -- paragraph break
+ appendLine()
+ appendLine()
+ hasTextOnLine = false
+ // Skip consecutive blank lines
+ while (i + 1 < lines.size && lines[i + 1].isEmpty()) {
+ i++
+ }
+ } else {
+ if (hasTextOnLine) {
+ append(" ")
+ }
+ append(line)
+ hasTextOnLine = !line.endsWith("\n")
+ }
+ i++
+ }
+ }
+ }
+
+ private fun flushCurrentKey() {
+ val block = currentBlock as? CurrentBlock.KeyValue ?: return
+ val key = block.currentKey ?: return
+ val node = FrontMatterNode(key, block.currentValues.toList(), block.currentIsList)
+ fmBlock.appendChild(node)
+ block.currentKey = null
+ block.currentValues.clear()
+ block.currentIsList = false
+ }
+
+ override fun closeBlock() {
+ flushAll()
+ }
+
+ private fun flushAll() {
+ flushBlockScalar()
+ flushCurrentKey()
+ }
+
+ private sealed interface CurrentBlock {
+ class KeyValue(
+ var currentKey: String? = null,
+ var currentValues: MutableList = mutableListOf(),
+ var currentIsList: Boolean = false,
+ ) : CurrentBlock
+
+ class Scalar(
+ var currentKey: String,
+ var currentValues: MutableList = mutableListOf(),
+ var indicator: Char,
+ var chomping: Char?,
+ var minimumIndent: Int = 1,
+ var indent: Int? = null,
+ var lines: MutableList = mutableListOf(),
+ ) : CurrentBlock
+ }
+
+ internal class Factory : AbstractBlockParserFactory() {
+ override fun tryStart(state: ParserState, matchedBlockParser: MatchedBlockParser): BlockStart? {
+ // Front matter must be at the very beginning of the document
+ val parent = matchedBlockParser.matchedBlockParser.block
+ if (parent !is org.commonmark.node.Document) return BlockStart.none()
+ if (parent.firstChild != null) return BlockStart.none()
+
+ val line = state.line.content.toString()
+ if (state.nextNonSpaceIndex != 0) return BlockStart.none()
+
+ if (line.trimEnd() == "---") {
+ return BlockStart.of(FrontMatterBlockParser()).atIndex(state.line.content.length)
+ }
+ return BlockStart.none()
+ }
+ }
+
+ companion object {
+ private val BLOCK_SCALAR_REGEX = Regex("^([|>])([+-]?)$")
+ private val LIST_ITEM_REGEX = Regex("^\\s*-\\s+(.+)$")
+
+ private fun parseStringValue(raw: String): String {
+ val trimmed = raw.trim()
+ if (trimmed.length >= 2 && (trimmed.surroundedBy('\'') || trimmed.surroundedBy('"'))) {
+ return trimmed.substring(1, trimmed.length - 1)
+ }
+ return trimmed
+ }
+
+ private fun String.indentWidth(): Int = indexOfFirst { !it.isWhitespace() }.let { if (it == -1) length else it }
+
+ private fun String.surroundedBy(char: Char): Boolean = first() == char && last() == char
+ }
+}
diff --git a/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterNode.kt b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterNode.kt
new file mode 100644
index 0000000000000..1a68726403536
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterNode.kt
@@ -0,0 +1,5 @@
+package org.jetbrains.jewel.markdown.extensions.frontmatter
+
+import org.commonmark.node.CustomNode
+
+internal class FrontMatterNode(val key: String, val values: List, val isList: Boolean) : CustomNode()
diff --git a/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterProcessorExtension.kt b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterProcessorExtension.kt
new file mode 100644
index 0000000000000..f56a501680116
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterProcessorExtension.kt
@@ -0,0 +1,113 @@
+package org.jetbrains.jewel.markdown.extensions.frontmatter
+
+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.annotations.ApiStatus
+import org.jetbrains.jewel.foundation.ExperimentalJewelApi
+import org.jetbrains.jewel.markdown.InlineMarkdown
+import org.jetbrains.jewel.markdown.MarkdownBlock
+import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock.UnorderedList
+import org.jetbrains.jewel.markdown.MarkdownBlock.ListItem
+import org.jetbrains.jewel.markdown.extensions.MarkdownBlockProcessorExtension
+import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension
+import org.jetbrains.jewel.markdown.extensions.github.tables.TableBlock
+import org.jetbrains.jewel.markdown.extensions.github.tables.TableCell
+import org.jetbrains.jewel.markdown.extensions.github.tables.TableRow
+import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
+
+/**
+ * Adds support for YAML front matter metadata blocks. Front matter is a common way to add metadata to Markdown
+ * documents, delimited by `---` markers at the beginning of the file.
+ *
+ * Front matter metadata is rendered as a headerless two-column table where the first column contains the keys and the
+ * second column contains the values. When a value is a list, it is rendered as an unordered list within the value cell.
+ */
+@ApiStatus.Experimental
+@ExperimentalJewelApi
+public object FrontMatterProcessorExtension : MarkdownProcessorExtension {
+ override val parserExtension: ParserExtension = FrontMatterParserExtension
+ override val textRendererExtension: TextContentRendererExtension = FrontMatterParserExtension
+
+ override val blockProcessorExtension: MarkdownBlockProcessorExtension = FrontMatterBlockProcessorExtension
+
+ private object FrontMatterBlockProcessorExtension : MarkdownBlockProcessorExtension {
+ override fun canProcess(block: CustomBlock): Boolean = block is FrontMatterBlock
+
+ override fun processMarkdownBlock(
+ block: CustomBlock,
+ processor: MarkdownProcessor,
+ ): MarkdownBlock.CustomBlock? {
+ val frontMatterBlock = block as FrontMatterBlock
+ val entries = frontMatterBlock.collectEntries()
+ if (entries.isEmpty()) return null
+
+ val rows =
+ entries.mapIndexed { rowIndex, entry ->
+ TableRow(
+ rowIndex = rowIndex,
+ cells =
+ listOf(
+ TableCell(
+ rowIndex = rowIndex,
+ columnIndex = 0,
+ content = textAsCellContent(entry.key),
+ alignment = null,
+ ),
+ TableCell(
+ rowIndex = rowIndex,
+ columnIndex = 1,
+ content = createValueContent(entry),
+ alignment = null,
+ ),
+ ),
+ )
+ }
+
+ return TableBlock(header = null, rows = rows)
+ }
+
+ private fun createValueContent(entry: FrontMatterNode): MarkdownBlock =
+ if (entry.isList) {
+ UnorderedList(
+ children = entry.values.map { value -> ListItem(textAsCellContent(value)) },
+ isTight = true,
+ marker = "-",
+ )
+ } else {
+ textAsCellContent(entry.values.firstOrNull().orEmpty())
+ }
+
+ private fun textAsCellContent(value: String): MarkdownBlock =
+ MarkdownBlock.Paragraph(listOf(InlineMarkdown.Text(value)))
+
+ private fun FrontMatterBlock.collectEntries(): List = buildList {
+ forEachChild { child ->
+ if (child is FrontMatterNode) {
+ add(child)
+ }
+ }
+ }
+
+ private inline fun Node.forEachChild(action: (Node) -> Unit) {
+ var child = firstChild
+ while (child != null) {
+ action(child)
+ child = child.next
+ }
+ }
+ }
+}
+
+private object FrontMatterParserExtension : ParserExtension, TextContentRendererExtension {
+ override fun extend(parserBuilder: Builder) {
+ parserBuilder.customBlockParserFactory(FrontMatterBlockParser.Factory())
+ }
+
+ override fun extend(rendererBuilder: TextContentRenderer.Builder) {
+ // No-op: front matter is not rendered as text
+ }
+}
diff --git a/platform/jewel/markdown/extensions/front-matter/src/main/resources/intellij.platform.jewel.markdown.extensions.frontMatter.xml b/platform/jewel/markdown/extensions/front-matter/src/main/resources/intellij.platform.jewel.markdown.extensions.frontMatter.xml
new file mode 100644
index 0000000000000..3eb4b1a92d1b7
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/main/resources/intellij.platform.jewel.markdown.extensions.frontMatter.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/platform/jewel/markdown/extensions/front-matter/src/test/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterProcessorExtensionTest.kt b/platform/jewel/markdown/extensions/front-matter/src/test/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterProcessorExtensionTest.kt
new file mode 100644
index 0000000000000..e9b9aa43df4d3
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/test/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterProcessorExtensionTest.kt
@@ -0,0 +1,491 @@
+package org.jetbrains.jewel.markdown.extensions.frontmatter
+
+import org.jetbrains.jewel.foundation.ExperimentalJewelApi
+import org.jetbrains.jewel.markdown.InlineMarkdown
+import org.jetbrains.jewel.markdown.MarkdownBlock
+import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock.UnorderedList
+import org.jetbrains.jewel.markdown.MarkdownBlock.ListItem
+import org.jetbrains.jewel.markdown.extensions.github.tables.TableBlock
+import org.jetbrains.jewel.markdown.extensions.github.tables.TableCell
+import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@OptIn(ExperimentalJewelApi::class)
+public class FrontMatterProcessorExtensionTest {
+ private val processor = MarkdownProcessor(listOf(FrontMatterProcessorExtension))
+
+ @Test
+ public fun `simple key-value block is parsed as headerless two-column table`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: Hello World
+ |author: John Doe
+ |---
+ |
+ |Some content.
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertEquals(2, table.rowCount)
+ assertScalarRow(table, rowIndex = 0, key = "title", value = "Hello World")
+ assertScalarRow(table, rowIndex = 1, key = "author", value = "John Doe")
+ }
+
+ @Test
+ public fun `single key block is parsed as single table row`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: Bye World
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertEquals(1, table.rowCount)
+ assertScalarRow(table, rowIndex = 0, key = "title", value = "Bye World")
+ }
+
+ @Test
+ public fun `empty block produces nothing`() {
+ val rawMarkdown =
+ """
+ |---
+ |---
+ |
+ |Some content.
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ // Empty block should be skipped, only the paragraph should remain
+ assertTrue(blocks.none { it is TableBlock })
+ }
+
+ @Test
+ public fun `lists are rendered as unordered list`() {
+ val rawMarkdown =
+ """
+ |---
+ |numbers:
+ | - unus
+ | - duo
+ | - tres
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertEquals(1, table.rowCount)
+ assertListRow(table, rowIndex = 0, key = "numbers", values = listOf("unus", "duo", "tres"))
+ }
+
+ @Test
+ public fun `single-item lists are rendered as unordered list`() {
+ val rawMarkdown =
+ """
+ |---
+ |the number:
+ | - nil
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertEquals(1, table.rowCount)
+ assertListRow(table, rowIndex = 0, key = "the number", values = listOf("nil"))
+ }
+
+ @Test
+ public fun `mixed scalar and list values`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: Nothingness
+ |tags:
+ | - suspense
+ | - unsettling
+ |author: You don't want to know
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertEquals(3, table.rowCount)
+ assertScalarRow(table, rowIndex = 0, key = "title", value = "Nothingness")
+ assertListRow(table, rowIndex = 1, key = "tags", values = listOf("suspense", "unsettling"))
+ assertScalarRow(table, rowIndex = 2, key = "author", value = "You don't want to know")
+ }
+
+ @Test
+ public fun `block parsing stops on non-front-matter syntax`() {
+ val rawMarkdown =
+ """
+ |---
+ |reveal: The Secret to Live Forever Is
+ |# Oh no, a Markdown heading stands in the way! The FrontMatter block skedaddles.
+ |
+ |Still not a FrontMatter block.
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks[0].assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertScalarRow(table, rowIndex = 0, key = "reveal", value = "The Secret to Live Forever Is")
+
+ blocks[1].assertIs()
+ blocks[2].assertIs()
+ }
+
+ @Test
+ public fun `content after block is preserved`() {
+ val rawMarkdown =
+ """
+ |---
+ |Value: key
+ |---
+ |
+ |# Heading
+ |
+ |Paragraph text.
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ blocks[0].assertIs()
+ blocks[1].assertIs()
+ blocks[2].assertIs()
+ }
+
+ @Test
+ public fun `block not at document start is not parsed`() {
+ val rawMarkdown =
+ """
+ |Some content first.
+ |
+ |---
+ |i: am not, in fact, frontmatter
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ // When block is not at the start, the --- are treated as thematic breaks
+ assertTrue(blocks.none { it is TableBlock })
+ }
+
+ @Test
+ public fun `list items with colon are treated as plain text values`() {
+ val rawMarkdown =
+ """
+ |---
+ |WANTED:
+ | - name: me =)
+ | - bounty: heavenly delight
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertListRow(table, rowIndex = 0, key = "WANTED", values = listOf("name: me =)", "bounty: heavenly delight"))
+ }
+
+ @Test
+ public fun `literal block scalar preserves newlines`() {
+ val rawMarkdown =
+ """
+ |---
+ |lines: |
+ | Line five
+ | Line seventy-three
+ | Line minus one
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "lines", value = "Line five\nLine seventy-three\nLine minus one\n")
+ }
+
+ @Test
+ public fun `literal block scalar preserves leading blank lines`() {
+ val rawMarkdown =
+ """
+ |---
+ |more: |
+ |
+ | Third line
+ | Second line
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "more", value = "\nThird line\nSecond line\n")
+ }
+
+ @Test
+ public fun `literal block scalar with strip chomping`() {
+ val rawMarkdown =
+ """
+ |---
+ |more: |-
+ | Line A
+ | Line GMaj7
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "more", value = "Line A\nLine GMaj7")
+ }
+
+ @Test
+ public fun `literal block scalar with keep chomping`() {
+ val rawMarkdown =
+ """
+ |---
+ |twolines: |+
+ | Line
+ | Line too
+ |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "twolines", value = "Line\nLine too\n\n")
+ }
+
+ @Test
+ public fun `folded block scalar joins consecutive lines`() {
+ val rawMarkdown =
+ """
+ |---
+ |alphabet: >
+ | ABCDEFG
+ | HIJKLMNOP
+ | QRS
+ | TUV
+ | WXYZ
+ |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "alphabet", value = "ABCDEFG HIJKLMNOP QRS TUV WXYZ\n")
+ }
+
+ @Test
+ public fun `folded block scalar with strip chomping`() {
+ val rawMarkdown =
+ """
+ |---
+ |alphabet: >-
+ | ABCDEFG
+ | HIJKLMNOP
+ | QRS
+ | TUV
+ | WXYZ
+ |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "alphabet", value = "ABCDEFG HIJKLMNOP QRS TUV WXYZ")
+ }
+
+ @Test
+ public fun `folded block scalar with keep chomping`() {
+ val rawMarkdown =
+ """
+ |---
+ |alphabet: >+
+ | ABCDEFG
+ | HIJKLMNOP
+ | QRS
+ | TUV
+ | WXYZ
+ |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "alphabet", value = "ABCDEFG HIJKLMNOP QRS TUV WXYZ\n\n")
+ }
+
+ @Test
+ public fun `folded block scalar with blank lines creates paragraph breaks`() {
+ val rawMarkdown =
+ """
+ |---
+ |alphabet: >
+ | 1234
+ | 5.
+ |
+ | 67!
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "alphabet", value = "1234 5.\n\n67!\n")
+ }
+
+ @Test
+ public fun `empty block scalar followed by end marker`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: No
+ |why: |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertEquals(2, table.rowCount)
+ assertScalarRow(table, rowIndex = 0, key = "title", value = "No")
+ assertScalarRow(table, rowIndex = 1, key = "why", value = "")
+ }
+
+ @Test
+ public fun `blank folded strip scalar followed by key starts a new key-value pair`() {
+ val rawMarkdown =
+ """
+ |---
+ |sneaky: >-
+ |
+ |ok: not sneaky
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertEquals(2, table.rowCount)
+ assertScalarRow(table, rowIndex = 0, key = "sneaky", value = "")
+ assertScalarRow(table, rowIndex = 1, key = "ok", value = "not sneaky")
+ }
+
+ @Test
+ public fun `block scalar followed by another key`() {
+ val rawMarkdown =
+ """
+ |---
+ |yes: |
+ | no
+ | maybe
+ |no: yes
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertNull(table.header)
+ assertEquals(2, table.rowCount)
+ assertScalarRow(table, rowIndex = 0, key = "yes", value = "no\nmaybe\n")
+ assertScalarRow(table, rowIndex = 1, key = "no", value = "yes")
+ }
+
+ @Test
+ public fun `quoted values are unquoted`() {
+ val rawMarkdown =
+ """
+ |---
+ |line1: "line1: "
+ |lineE: 'lineE: '
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertScalarRow(table, rowIndex = 0, key = "line1", value = "line1: ")
+ assertScalarRow(table, rowIndex = 1, key = "lineE", value = "lineE: ")
+ }
+
+ private fun assertScalarRow(table: TableBlock, rowIndex: Int, key: String, value: String) {
+ val row = table.rows[rowIndex]
+ assertCellText(key, row.cells[0])
+ assertCellText(value, row.cells[1])
+ }
+
+ private fun assertListRow(table: TableBlock, rowIndex: Int, key: String, values: List) {
+ val row = table.rows[rowIndex]
+ assertCellText(key, row.cells[0])
+
+ val list = row.cells[1].content.assertIs()
+ assertTrue(list.isTight)
+ assertEquals("-", list.marker)
+ assertEquals(values.size, list.children.size)
+ list.children.forEachIndexed { index, item -> assertListItemText(values[index], item) }
+ }
+
+ private fun assertListItemText(expected: String, item: ListItem) {
+ assertEquals(0, item.level)
+ assertEquals(1, item.children.size)
+ assertCellText(expected, item.children.single().assertIs())
+ }
+
+ private fun assertCellText(expected: String, cell: TableCell) {
+ assertCellText(expected, cell.content.assertIs())
+ }
+
+ private fun assertCellText(expected: String, paragraph: MarkdownBlock.Paragraph) {
+ val textContent = paragraph.inlineContent.filterIsInstance()
+ assertTrue(textContent.isNotEmpty())
+ assertEquals(expected, textContent.first().content)
+ }
+
+ private inline fun Any.assertIs(): T {
+ assertTrue(
+ "An instance of ${this::class.qualifiedName} cannot be cast to ${T::class.qualifiedName}: $this",
+ this is T,
+ )
+ return this as T
+ }
+}
diff --git a/platform/jewel/markdown/extensions/gfm-tables/api-dump-experimental.txt b/platform/jewel/markdown/extensions/gfm-tables/api-dump-experimental.txt
index f55441de29a95..8083fcc3e88db 100644
--- a/platform/jewel/markdown/extensions/gfm-tables/api-dump-experimental.txt
+++ b/platform/jewel/markdown/extensions/gfm-tables/api-dump-experimental.txt
@@ -48,3 +48,55 @@
- s:getEntries():kotlin.enums.EnumEntries
- s:valueOf(java.lang.String):org.jetbrains.jewel.markdown.extensions.github.tables.RowBackgroundStyle
- s:values():org.jetbrains.jewel.markdown.extensions.github.tables.RowBackgroundStyle[]
+*f:org.jetbrains.jewel.markdown.extensions.github.tables.TableBlock
+- org.jetbrains.jewel.markdown.MarkdownBlock$CustomBlock
+- sf:$stable:I
+- (org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader,java.util.List):V
+- f:component1():org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader
+- f:component2():java.util.List
+- f:copy(org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader,java.util.List):org.jetbrains.jewel.markdown.extensions.github.tables.TableBlock
+- bs:copy$default(org.jetbrains.jewel.markdown.extensions.github.tables.TableBlock,org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader,java.util.List,I,java.lang.Object):org.jetbrains.jewel.markdown.extensions.github.tables.TableBlock
+- equals(java.lang.Object):Z
+- f:getColumnCount():I
+- f:getHeader():org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader
+- f:getRowCount():I
+- f:getRows():java.util.List
+- hashCode():I
+*f:org.jetbrains.jewel.markdown.extensions.github.tables.TableCell
+- org.jetbrains.jewel.markdown.MarkdownBlock$CustomBlock
+- sf:$stable:I
+- (I,I,java.util.List,androidx.compose.ui.Alignment$Horizontal):V
+- f:component1():I
+- f:component2():I
+- f:component3():java.util.List
+- f:component4():androidx.compose.ui.Alignment$Horizontal
+- f:copy(I,I,java.util.List,androidx.compose.ui.Alignment$Horizontal):org.jetbrains.jewel.markdown.extensions.github.tables.TableCell
+- bs:copy$default(org.jetbrains.jewel.markdown.extensions.github.tables.TableCell,I,I,java.util.List,androidx.compose.ui.Alignment$Horizontal,I,java.lang.Object):org.jetbrains.jewel.markdown.extensions.github.tables.TableCell
+- equals(java.lang.Object):Z
+- f:getAlignment():androidx.compose.ui.Alignment$Horizontal
+- f:getColumnIndex():I
+- f:getContent():java.util.List
+- f:getRowIndex():I
+- hashCode():I
+*f:org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader
+- org.jetbrains.jewel.markdown.MarkdownBlock$CustomBlock
+- sf:$stable:I
+- (java.util.List):V
+- f:component1():java.util.List
+- f:copy(java.util.List):org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader
+- bs:copy$default(org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader,java.util.List,I,java.lang.Object):org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader
+- equals(java.lang.Object):Z
+- f:getCells():java.util.List
+- hashCode():I
+*f:org.jetbrains.jewel.markdown.extensions.github.tables.TableRow
+- org.jetbrains.jewel.markdown.MarkdownBlock$CustomBlock
+- sf:$stable:I
+- (I,java.util.List):V
+- f:component1():I
+- f:component2():java.util.List
+- f:copy(I,java.util.List):org.jetbrains.jewel.markdown.extensions.github.tables.TableRow
+- bs:copy$default(org.jetbrains.jewel.markdown.extensions.github.tables.TableRow,I,java.util.List,I,java.lang.Object):org.jetbrains.jewel.markdown.extensions.github.tables.TableRow
+- equals(java.lang.Object):Z
+- f:getCells():java.util.List
+- f:getRowIndex():I
+- hashCode():I
diff --git a/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-api-0.36.0.txt b/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-api-0.36.0.txt
index 7e897c6e99e4b..243c1494d5df8 100644
--- a/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-api-0.36.0.txt
+++ b/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-api-0.36.0.txt
@@ -62,5 +62,56 @@ package org.jetbrains.jewel.markdown.extensions.github.tables {
enum_constant public static final org.jetbrains.jewel.markdown.extensions.github.tables.RowBackgroundStyle Striped;
}
+ @SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class TableBlock implements org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock {
+ ctor public TableBlock(org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader? header, java.util.List rows);
+ method public org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader? component1();
+ method public java.util.List component2();
+ method public org.jetbrains.jewel.markdown.extensions.github.tables.TableBlock copy(optional org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader? header, optional java.util.List rows);
+ method public int getColumnCount();
+ method public org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader? getHeader();
+ method public int getRowCount();
+ method public java.util.List getRows();
+ property public int columnCount;
+ property public org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader? header;
+ property public int rowCount;
+ property public java.util.List rows;
+ }
+
+ @SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class TableCell implements org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock {
+ ctor public TableCell(int rowIndex, int columnIndex, org.jetbrains.jewel.markdown.MarkdownBlock content, androidx.compose.ui.Alignment.Horizontal? alignment);
+ method public int component1();
+ method public int component2();
+ method public org.jetbrains.jewel.markdown.MarkdownBlock component3();
+ method public androidx.compose.ui.Alignment.Horizontal? component4();
+ method public org.jetbrains.jewel.markdown.extensions.github.tables.TableCell copy(optional int rowIndex, optional int columnIndex, optional org.jetbrains.jewel.markdown.MarkdownBlock content, optional androidx.compose.ui.Alignment.Horizontal? alignment);
+ method public androidx.compose.ui.Alignment.Horizontal? getAlignment();
+ method public int getColumnIndex();
+ method public org.jetbrains.jewel.markdown.MarkdownBlock getContent();
+ method public int getRowIndex();
+ property public androidx.compose.ui.Alignment.Horizontal? alignment;
+ property public int columnIndex;
+ property public org.jetbrains.jewel.markdown.MarkdownBlock content;
+ property public int rowIndex;
+ }
+
+ @SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class TableHeader implements org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock {
+ ctor public TableHeader(java.util.List cells);
+ method public java.util.List component1();
+ method public org.jetbrains.jewel.markdown.extensions.github.tables.TableHeader copy(optional java.util.List cells);
+ method public java.util.List getCells();
+ property public java.util.List cells;
+ }
+
+ @SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class TableRow implements org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock {
+ ctor public TableRow(int rowIndex, java.util.List cells);
+ method public int component1();
+ method public java.util.List component2();
+ method public org.jetbrains.jewel.markdown.extensions.github.tables.TableRow copy(optional int rowIndex, optional java.util.List cells);
+ method public java.util.List getCells();
+ method public int getRowIndex();
+ property public java.util.List cells;
+ property public int rowIndex;
+ }
+
}
diff --git a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableBlockRenderer.kt b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableBlockRenderer.kt
index b79e6248e6f67..fb51cd1272888 100644
--- a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableBlockRenderer.kt
+++ b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableBlockRenderer.kt
@@ -15,7 +15,6 @@ import org.jetbrains.annotations.ApiStatus
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
@@ -56,8 +55,8 @@ internal class GitHubTableBlockRenderer(
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.
+ // Header cells produced by table parsing are represented as Paragraph blocks,
+ // so overriding Paragraph styling is enough here.
MarkdownStyling(
rootStyling.blockVerticalSpacing,
MarkdownStyling.Paragraph(semiboldInlinesStyling),
@@ -75,52 +74,17 @@ internal class GitHubTableBlockRenderer(
val rows =
remember(tableBlock, blockRenderer, inlineRenderer, tableStyling, enabled, onUrlClick) {
- val headerCells =
- tableBlock.header.cells.map Unit> { cell ->
- {
- Cell(
- cell = cell,
- backgroundColor = tableStyling.colors.rowBackgroundColor,
- padding = tableStyling.metrics.cellPadding,
- defaultAlignment = tableStyling.metrics.headerDefaultCellContentAlignment,
- blockRenderer = headerRenderer,
- enabled = enabled,
- paragraphStyling = headerRenderer.rootStyling.paragraph,
- onUrlClick = onUrlClick,
- )
- }
+ buildList Unit>> {
+ if (tableBlock.header != null) {
+ add(headerCells(tableBlock.header, headerRenderer, enabled, onUrlClick))
}
- 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,
- paragraphStyling = rootStyling.paragraph,
- onUrlClick = onUrlClick,
- )
- }
+ val rowsCells =
+ tableBlock.rows.map Unit>> { row ->
+ rowCells(row, blockRenderer, enabled, onUrlClick)
}
- }
-
- listOf(headerCells) + rowsCells
+ addAll(rowsCells)
+ }
}
BasicTableLayout(
@@ -133,6 +97,57 @@ internal class GitHubTableBlockRenderer(
)
}
+ private fun headerCells(
+ header: TableHeader,
+ headerRenderer: MarkdownBlockRenderer,
+ enabled: Boolean,
+ onUrlClick: (String) -> Unit,
+ ): List<@Composable (() -> Unit)> =
+ header.cells.map {
+ {
+ Cell(
+ cell = it,
+ backgroundColor = tableStyling.colors.rowBackgroundColor,
+ padding = tableStyling.metrics.cellPadding,
+ defaultAlignment = tableStyling.metrics.headerDefaultCellContentAlignment,
+ blockRenderer = headerRenderer,
+ enabled = enabled,
+ onUrlClick = onUrlClick,
+ )
+ }
+ }
+
+ private fun rowCells(
+ row: TableRow,
+ blockRenderer: MarkdownBlockRenderer,
+ enabled: Boolean,
+ onUrlClick: (String) -> Unit,
+ ): List<@Composable (() -> Unit)> =
+ row.cells.map Unit> { cell ->
+ {
+ val backgroundColor =
+ if (tableStyling.colors.rowBackgroundStyle == RowBackgroundStyle.Striped) {
+ if (cell.rowIndex % 2 == 1) {
+ 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,
+ )
+ }
+ }
+
private fun InlinesStyling.withFontWeight(newFontWeight: FontWeight) =
InlinesStyling(
textStyle = textStyle.copy(fontWeight = newFontWeight),
@@ -156,20 +171,13 @@ internal class GitHubTableBlockRenderer(
defaultAlignment: Alignment.Horizontal,
blockRenderer: MarkdownBlockRenderer,
enabled: Boolean,
- paragraphStyling: MarkdownStyling.Paragraph,
onUrlClick: (String) -> Unit,
) {
Box(
modifier = Modifier.background(backgroundColor).padding(padding).clipToBounds(),
contentAlignment = (cell.alignment ?: defaultAlignment).asContentAlignment(),
) {
- blockRenderer.RenderParagraph(
- block = MarkdownBlock.Paragraph(cell.content),
- styling = paragraphStyling,
- enabled = enabled,
- onUrlClick = onUrlClick,
- modifier = Modifier,
- )
+ blockRenderer.RenderBlock(cell.content, enabled, onUrlClick, Modifier)
}
}
diff --git a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension.kt b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension.kt
index 4088a6cdabf79..5f64ea88e1820 100644
--- a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension.kt
+++ b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/GitHubTableProcessorExtension.kt
@@ -64,7 +64,7 @@ public object GitHubTableProcessorExtension : MarkdownProcessorExtension {
TableCell(
rowIndex = 0,
columnIndex = columnIndex,
- content = cell.readInlineMarkdown(processor),
+ content = inlinesAsCellContent(cell.readInlineMarkdown(processor)),
alignment = getAlignment(cell),
)
}
@@ -77,7 +77,7 @@ public object GitHubTableProcessorExtension : MarkdownProcessorExtension {
TableCell(
rowIndex = rowIndex + 1, // The header is row zero
columnIndex = columnIndex,
- content = cell.readInlineMarkdown(processor),
+ content = inlinesAsCellContent(cell.readInlineMarkdown(processor)),
alignment = getAlignment(cell),
)
},
@@ -133,6 +133,8 @@ private object GitHubTablesCommonMarkExtension : ParserExtension, TextContentRen
}
}
+private fun inlinesAsCellContent(inlines: List): MarkdownBlock = MarkdownBlock.Paragraph(inlines)
+
private object GitHubTablesHtmlConverterExtension : MarkdownHtmlConverterExtension {
override val supportedTags: Set
get() = setOf("table")
@@ -151,14 +153,18 @@ private object GitHubTablesHtmlConverterExtension : MarkdownHtmlConverterExtensi
val htmlRows = tbody.children.filterIsInstance().filter { it.tag == "tr" }
if (htmlRows.isEmpty()) return null
val markdownRows: List> = buildList {
- for (i in 0..htmlRows.lastIndex) {
+ for ((i, element) in htmlRows.withIndex()) {
add(
- htmlRows[i]
+ element
.rowElements()
.mapIndexed { index, rowCell ->
val inlines = convertInlines(rowCell.children)
- if (inlines.isEmpty()) return@mapIndexed TableCell(i, index, emptyList(), null)
- TableCell(rowIndex = i, columnIndex = index, content = inlines, alignment = null)
+ TableCell(
+ rowIndex = i,
+ columnIndex = index,
+ content = inlinesAsCellContent(inlines),
+ alignment = null,
+ )
}
.toMutableList()
)
@@ -167,7 +173,14 @@ private object GitHubTablesHtmlConverterExtension : MarkdownHtmlConverterExtensi
val maxColumns = markdownRows.maxOf { it.size }
for ((rowIndex, row) in markdownRows.withIndex()) {
for (columnIndex in row.size until maxColumns) {
- row.add(TableCell(rowIndex, columnIndex, emptyList(), null))
+ row.add(
+ TableCell(
+ rowIndex = rowIndex,
+ columnIndex = columnIndex,
+ content = inlinesAsCellContent(emptyList()),
+ alignment = null,
+ )
+ )
}
}
val header = TableHeader(markdownRows.first())
diff --git a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableBlock.kt b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableBlock.kt
index eacaeb1188a72..610c770f321a7 100644
--- a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableBlock.kt
+++ b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableBlock.kt
@@ -1,22 +1,33 @@
// 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 org.jetbrains.annotations.ApiStatus
+import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.markdown.MarkdownBlock
-internal data class TableBlock(val header: TableHeader, val rows: List) : MarkdownBlock.CustomBlock {
- val rowCount: Int = rows.size + 1 // We always have a header
+@ApiStatus.Experimental
+@ExperimentalJewelApi
+public data class TableBlock(val header: TableHeader?, val rows: List) : MarkdownBlock.CustomBlock {
+ val rowCount: Int = rows.size + if (header != null) 1 else 0
val columnCount: Int
init {
- require(header.cells.isNotEmpty()) { "Header cannot be empty" }
- val headerColumns = header.cells.size
+ if (header != null) {
+ require(header.cells.isNotEmpty()) { "Header cannot be empty" }
+ }
+ require(header != null || rows.isNotEmpty()) { "Table must have a header or at least one row" }
- if (rows.isNotEmpty()) {
- val bodyColumns = rows.first().cells.size
- require(rows.all { it.cells.size == bodyColumns }) { "Inconsistent cell count in table body" }
+ val headerColumns = header?.cells?.size
+ val bodyColumns = rows.firstOrNull()?.cells?.size
+
+ if (headerColumns != null && bodyColumns != null) {
require(headerColumns == bodyColumns) { "Inconsistent cell count between table body and header" }
}
+ if (rows.isNotEmpty()) {
+ val firstRowSize = rows.first().cells.size
+ require(rows.all { it.cells.size == firstRowSize }) { "Inconsistent cell count in table body" }
+ }
- columnCount = headerColumns
+ columnCount = headerColumns ?: bodyColumns ?: error("Table must have at least one row or header")
}
}
diff --git a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableCell.kt b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableCell.kt
index 480efbca94f87..58aead70d4f89 100644
--- a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableCell.kt
+++ b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableCell.kt
@@ -2,12 +2,15 @@
package org.jetbrains.jewel.markdown.extensions.github.tables
import androidx.compose.ui.Alignment
-import org.jetbrains.jewel.markdown.InlineMarkdown
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.markdown.MarkdownBlock
-internal data class TableCell(
+@ApiStatus.Experimental
+@ExperimentalJewelApi
+public data class TableCell(
val rowIndex: Int,
val columnIndex: Int,
- val content: List,
+ val content: MarkdownBlock,
val alignment: Alignment.Horizontal?,
) : MarkdownBlock.CustomBlock
diff --git a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableHeader.kt b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableHeader.kt
index 09ae42b450359..ba0ea9ae5cd70 100644
--- a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableHeader.kt
+++ b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableHeader.kt
@@ -1,6 +1,10 @@
// 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 org.jetbrains.annotations.ApiStatus
+import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.markdown.MarkdownBlock
-internal data class TableHeader(val cells: List) : MarkdownBlock.CustomBlock
+@ApiStatus.Experimental
+@ExperimentalJewelApi
+public data class TableHeader(val cells: List) : MarkdownBlock.CustomBlock
diff --git a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableRow.kt b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableRow.kt
index 7c0b9a4a4a8d7..39413f0989da1 100644
--- a/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableRow.kt
+++ b/platform/jewel/markdown/extensions/gfm-tables/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/github/tables/TableRow.kt
@@ -1,6 +1,10 @@
// 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 org.jetbrains.annotations.ApiStatus
+import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.markdown.MarkdownBlock
-internal data class TableRow(val rowIndex: Int, val cells: List) : MarkdownBlock.CustomBlock
+@ApiStatus.Experimental
+@ExperimentalJewelApi
+public data class TableRow(val rowIndex: Int, val cells: List) : MarkdownBlock.CustomBlock
diff --git a/platform/jewel/samples/standalone/BUILD.bazel b/platform/jewel/samples/standalone/BUILD.bazel
index 561ba6215e3c3..c0f1f7f31e3ce 100644
--- a/platform/jewel/samples/standalone/BUILD.bazel
+++ b/platform/jewel/samples/standalone/BUILD.bazel
@@ -1,3 +1,12 @@
+load("@rules_java//java:defs.bzl", "java_binary")
+
+java_binary(
+ name = "standalone_run",
+ main_class = "org.jetbrains.jewel.samples.standalone.MainKt",
+ visibility = ["//visibility:public"],
+ runtime_deps = [":standalone"],
+)
+
### auto-generated section `build intellij.platform.jewel.samples.standalone` start
load("//build:compiler-options.bzl", "create_kotlinc_options")
load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
@@ -43,6 +52,7 @@ jvm_library(
"//platform/jewel/markdown/extensions/gfm-alerts",
"//platform/jewel/markdown/extensions/gfm-tables",
"//platform/jewel/markdown/extensions/gfm-strikethrough",
+ "//platform/jewel/markdown/extensions/front-matter",
"//libraries/coil",
"//platform/jewel/markdown/extensions/images",
"//platform/jewel/int-ui/int-ui-standalone:jewel-intUi-standalone",
@@ -79,6 +89,7 @@ jvm_library(
"//platform/jewel/markdown/extensions/gfm-alerts:gfm-alerts_test_lib",
"//platform/jewel/markdown/extensions/gfm-tables:gfm-tables_test_lib",
"//platform/jewel/markdown/extensions/gfm-strikethrough:gfm-strikethrough_test_lib",
+ "//platform/jewel/markdown/extensions/front-matter:front-matter_test_lib",
"//libraries/coil:coil_test_lib",
"//platform/jewel/markdown/extensions/images:images_test_lib",
"//platform/jewel/int-ui/int-ui-standalone:jewel-intUi-standalone_test_lib",
diff --git a/platform/jewel/samples/standalone/build.gradle.kts b/platform/jewel/samples/standalone/build.gradle.kts
index 4b36f6f0222ef..9a85a1fb7e60f 100644
--- a/platform/jewel/samples/standalone/build.gradle.kts
+++ b/platform/jewel/samples/standalone/build.gradle.kts
@@ -15,6 +15,7 @@ dependencies {
implementation(projects.markdown.extensions.autolink)
implementation(projects.markdown.extensions.gfmAlerts)
implementation(projects.markdown.extensions.gfmStrikethrough)
+ implementation(projects.markdown.extensions.frontMatter)
implementation(projects.markdown.extensions.gfmTables)
implementation(projects.markdown.extensions.images)
implementation(projects.markdown.intUiStandaloneStyling)
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 911cfe576d2e3..e2b653f265d9f 100644
--- a/platform/jewel/samples/standalone/intellij.platform.jewel.samples.standalone.iml
+++ b/platform/jewel/samples/standalone/intellij.platform.jewel.samples.standalone.iml
@@ -45,6 +45,7 @@
+
diff --git a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/markdown/MarkdownPreview.kt b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/markdown/MarkdownPreview.kt
index af4c2a331db40..0775347320c59 100644
--- a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/markdown/MarkdownPreview.kt
+++ b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/markdown/MarkdownPreview.kt
@@ -32,6 +32,7 @@ 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.extensions.autolink.AutolinkProcessorExtension
+import org.jetbrains.jewel.markdown.extensions.frontmatter.FrontMatterProcessorExtension
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
@@ -63,6 +64,7 @@ internal fun MarkdownPreview(rawMarkdown: CharSequence, modifier: Modifier = Mod
MarkdownProcessor(
listOf(
AutolinkProcessorExtension,
+ FrontMatterProcessorExtension,
GitHubAlertProcessorExtension,
GitHubStrikethroughProcessorExtension(),
GitHubTableProcessorExtension,
diff --git a/platform/jewel/samples/standalone/src/main/resources/intellij.platform.jewel.samples.standalone.xml b/platform/jewel/samples/standalone/src/main/resources/intellij.platform.jewel.samples.standalone.xml
index 671c2dc57a669..5629f6c737edc 100644
--- a/platform/jewel/samples/standalone/src/main/resources/intellij.platform.jewel.samples.standalone.xml
+++ b/platform/jewel/samples/standalone/src/main/resources/intellij.platform.jewel.samples.standalone.xml
@@ -9,6 +9,7 @@
+
diff --git a/platform/jewel/settings.gradle.kts b/platform/jewel/settings.gradle.kts
index 53fb03bcc128b..6a8515df5bb7a 100644
--- a/platform/jewel/settings.gradle.kts
+++ b/platform/jewel/settings.gradle.kts
@@ -43,6 +43,7 @@ include(
":markdown:extensions:autolink",
":markdown:extensions:gfm-alerts",
":markdown:extensions:gfm-strikethrough",
+ ":markdown:extensions:front-matter",
":markdown:extensions:gfm-tables",
":markdown:extensions:images",
":markdown:int-ui-standalone-styling",
diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.compose.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.compose.xml
index 9cbc2280cdef4..d6aaccc0b3a96 100644
--- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.compose.xml
+++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.compose.xml
@@ -16,6 +16,7 @@
+
diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml
index a24403fec6393..c959cc858aa90 100644
--- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml
+++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml
@@ -308,6 +308,7 @@
+