diff --git a/.idea/modules.xml b/.idea/modules.xml
index 507277ff85732..e9b6b977f584a 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -1255,6 +1255,7 @@
+
diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt
index d7d04fadb3551..32e135a0faa40 100644
--- a/build/bazel-generated-file-list.txt
+++ b/build/bazel-generated-file-list.txt
@@ -646,6 +646,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/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt
index d5e6ba323dac1..7ab5db82bb812 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
@@ -319,6 +319,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 9f7962440b166..af316de30b2c4 100644
--- a/platform/compose/markdown/BUILD.bazel
+++ b/platform/compose/markdown/BUILD.bazel
@@ -18,6 +18,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",
@@ -28,6 +29,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",
@@ -51,6 +53,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",
@@ -65,6 +69,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 6af18c0e4ca20..e8b4c3d29235d 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/gradle/libs.versions.toml b/platform/jewel/gradle/libs.versions.toml
index abb7e33534606..bacef708d89ec 100644
--- a/platform/jewel/gradle/libs.versions.toml
+++ b/platform/jewel/gradle/libs.versions.toml
@@ -32,6 +32,7 @@ commonmark-core = { module = "org.commonmark:commonmark", version.ref = "commonm
commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" }
commonmark-ext-gfm-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" }
commonmark-ext-gfm-tables = { module = "org.commonmark:commonmark-ext-gfm-tables", version.ref = "commonmark" }
+commonmark-ext-yaml-front-matter = { module = "org.commonmark:commonmark-ext-yaml-front-matter", version.ref = "commonmark" }
detekt-api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" }
detekt-core = { module = "io.gitlab.arturbosch.detekt:detekt-core", version.ref = "detekt" }
detekt-test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" }
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..0beeff5f822fa
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/BUILD.bazel
@@ -0,0 +1,83 @@
+### 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",
+ visibility = ["//visibility:public"],
+ srcs = glob(["src/test/kotlin/**/*.kt", "src/test/kotlin/**/*.java", "src/test/kotlin/**/*.form"], allow_empty = True),
+ kotlinc_opts = ":custom_front-matter",
+ associates = [":front-matter"],
+ 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",
+ ],
+ plugins = ["@lib//:compose-plugin"]
+)
+### auto-generated section `build intellij.platform.jewel.markdown.extensions.frontMatter` end
+
+### auto-generated section `test intellij.platform.jewel.markdown.extensions.frontMatter` start
+load("@community//build:tests-options.bzl", "jps_test")
+
+jps_test(
+ name = "front-matter_test",
+ runtime_deps = [":front-matter_test_lib"]
+)
+### auto-generated section `test intellij.platform.jewel.markdown.extensions.frontMatter` 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..c247f0d533e4e
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/intellij.platform.jewel.markdown.extensions.frontMatter.iml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.3.10-RC/kotlin-compose-compiler-plugin-2.3.10-RC.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-0.34.0.txt b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-0.34.0.txt
new file mode 100644
index 0000000000000..06dabba19c93d
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-0.34.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.34.0.txt b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-stable-0.34.0.txt
new file mode 100644
index 0000000000000..e6f50d0d0fd11
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/metalava/front-matter-api-stable-0.34.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..7b0767a62323f
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterBlockParser.kt
@@ -0,0 +1,272 @@
+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()
+
+ // Detect indent from first non-empty content line
+ if (block.indent == null) {
+ 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
+ }
+
+ val indent = block.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.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())
+ fmBlock.appendChild(node)
+ block.currentKey = null
+ block.currentValues.clear()
+ }
+
+ override fun closeBlock() {
+ flushAll()
+ }
+
+ private fun flushAll() {
+ flushBlockScalar()
+ flushCurrentKey()
+ }
+
+ private sealed interface CurrentBlock {
+ class KeyValue(var currentKey: String? = null, var currentValues: MutableList = mutableListOf()) :
+ 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..b713cc82961cc
--- /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) : 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..7c114eb8e14eb
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/main/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterProcessorExtension.kt
@@ -0,0 +1,111 @@
+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.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.TableHeader
+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 two-row table: the first row (header) contains the keys and the second row
+ * contains the values. When a value is a list, it is rendered as a single-row nested table within the 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 headerCells =
+ entries.mapIndexed { index, (key, _) ->
+ TableCell(rowIndex = 0, columnIndex = index, content = textAsCellContent(key), alignment = null)
+ }
+
+ val valueCells =
+ entries.mapIndexed { index, (_, values) -> createValueCell(columnIndex = index, values = values) }
+ val dataRow = TableRow(rowIndex = 0, cells = valueCells)
+
+ return TableBlock(header = TableHeader(headerCells), rows = listOf(dataRow))
+ }
+
+ private fun createValueCell(columnIndex: Int, values: List): TableCell =
+ if (values.size <= 1) {
+ TableCell(
+ rowIndex = 1,
+ columnIndex = columnIndex,
+ content = textAsCellContent(values.firstOrNull().orEmpty()),
+ alignment = null,
+ )
+ } else {
+ // Multiple values are rendered as a headerless nested table (single data row)
+ val valueCells =
+ values.mapIndexed { index, value ->
+ TableCell(
+ rowIndex = 0,
+ columnIndex = index,
+ content = textAsCellContent(value),
+ alignment = null,
+ )
+ }
+ val nestedTable = TableBlock(header = null, rows = listOf(TableRow(rowIndex = 0, cells = valueCells)))
+ TableCell(rowIndex = 1, columnIndex = columnIndex, content = nestedTable, alignment = null)
+ }
+
+ 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.key to child.values)
+ }
+ }
+ }
+
+ 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..5a13383f94d49
--- /dev/null
+++ b/platform/jewel/markdown/extensions/front-matter/src/test/kotlin/org/jetbrains/jewel/markdown/extensions/frontmatter/FrontMatterProcessorExtensionTest.kt
@@ -0,0 +1,523 @@
+package org.jetbrains.jewel.markdown.extensions.frontmatter
+
+import org.jetbrains.jewel.markdown.InlineMarkdown
+import org.jetbrains.jewel.markdown.MarkdownBlock
+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.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@OptIn(org.jetbrains.jewel.foundation.ExperimentalJewelApi::class)
+public class FrontMatterProcessorExtensionTest {
+ private val processor = MarkdownProcessor(listOf(FrontMatterProcessorExtension))
+
+ @Test
+ public fun `simple key-value front matter is parsed as two-row 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)
+ assertEquals(1, table.rowCount - 1)
+
+ // Verify header contains keys
+ assertCellText("title", table.header.asNotNull().cells[0])
+ assertCellText("author", table.header.asNotNull().cells[1])
+
+ // Verify single data row contains values
+ assertCellText("Hello World", table.rows[0].cells[0])
+ assertCellText("John Doe", table.rows[0].cells[1])
+ }
+
+ @Test
+ public fun `single key front matter is parsed as two-row table`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: My Document
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(1, table.columnCount)
+ assertEquals(1, table.rowCount - 1)
+ assertCellText("title", table.header.asNotNull().cells[0])
+ assertCellText("My Document", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `empty front matter produces no block`() {
+ val rawMarkdown =
+ """
+ |---
+ |---
+ |
+ |Some content.
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ // Empty front matter should be skipped, only the paragraph should remain
+ assertTrue(blocks.none { it is TableBlock })
+ }
+
+ @Test
+ public fun `list values are rendered as single-row nested table`() {
+ val rawMarkdown =
+ """
+ |---
+ |tags:
+ | - kotlin
+ | - compose
+ | - jewel
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(1, table.columnCount)
+ assertEquals(1, table.rowCount - 1)
+
+ // Header should contain the key
+ assertCellText("tags", table.header.asNotNull().cells[0])
+
+ // Value cell should contain a nested table
+ val valueCell = table.rows[0].cells[0]
+ val nestedTable = valueCell.content.assertIs()
+ assertEquals(3, nestedTable.columnCount)
+ assertNull(nestedTable.header)
+ assertEquals(1, nestedTable.rowCount)
+ assertCellText("kotlin", nestedTable.rows[0].cells[0])
+ assertCellText("compose", nestedTable.rows[0].cells[1])
+ assertCellText("jewel", nestedTable.rows[0].cells[2])
+ }
+
+ @Test
+ public fun `mixed scalar and list values`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: My Post
+ |tags:
+ | - one
+ | - two
+ |author: Someone
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(3, table.columnCount)
+ assertEquals(1, table.rowCount - 1)
+
+ // Header contains all keys
+ assertCellText("title", table.header.asNotNull().cells[0])
+ assertCellText("tags", table.header.asNotNull().cells[1])
+ assertCellText("author", table.header.asNotNull().cells[2])
+
+ // Scalar value — inline content
+ assertCellText("My Post", table.rows[0].cells[0])
+ table.rows[0].cells[0].content.assertIs()
+
+ // List value — block content with nested single-row table
+ val nestedTable = table.rows[0].cells[1].content.assertIs()
+ assertEquals(2, nestedTable.columnCount)
+ assertNull(nestedTable.header)
+ assertCellText("one", nestedTable.rows[0].cells[0])
+ assertCellText("two", nestedTable.rows[0].cells[1])
+
+ // Scalar value after list
+ assertCellText("Someone", table.rows[0].cells[2])
+ }
+
+ @Test
+ public fun `front matter parsing stops on non-front-matter syntax`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: Test
+ |# Heading that should not be part of front matter
+ |
+ |Paragraph text.
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks[0].assertIs()
+ assertEquals(1, table.columnCount)
+ assertCellText("title", table.header.asNotNull().cells[0])
+ assertCellText("Test", table.rows[0].cells[0])
+
+ blocks[1].assertIs()
+ blocks[2].assertIs()
+ }
+
+ @Test
+ public fun `content after front matter is preserved`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: Test
+ |---
+ |
+ |# Heading
+ |
+ |Paragraph text.
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ blocks[0].assertIs()
+ blocks[1].assertIs()
+ blocks[2].assertIs()
+ }
+
+ @Test
+ public fun `front matter not at document start is not parsed`() {
+ val rawMarkdown =
+ """
+ |Some content first.
+ |
+ |---
+ |title: Test
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ // When front matter is not at the start, the --- are treated as thematic breaks
+ assertTrue(blocks.none { it is TableBlock })
+ }
+
+ @Test
+ public fun `front matter with many keys produces a table`() {
+ val rawMarkdown =
+ """
+ |---
+ |key1: value1
+ |key2: value2
+ |key3:
+ | - item1
+ | - item2
+ | - item3
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(3, table.columnCount)
+ assertEquals(1, table.rowCount - 1)
+
+ // Header row: keys
+ assertCellText("key1", table.header.asNotNull().cells[0])
+ assertCellText("key2", table.header.asNotNull().cells[1])
+ assertCellText("key3", table.header.asNotNull().cells[2])
+
+ // Data row: values
+ assertCellText("value1", table.rows[0].cells[0])
+ assertCellText("value2", table.rows[0].cells[1])
+
+ // List value: single-row nested table
+ val listCell = table.rows[0].cells[2]
+ val nestedTable = listCell.content.assertIs()
+ assertEquals(3, nestedTable.columnCount)
+ assertNull(nestedTable.header)
+ assertEquals(1, nestedTable.rowCount)
+ assertCellText("item1", nestedTable.rows[0].cells[0])
+ assertCellText("item2", nestedTable.rows[0].cells[1])
+ assertCellText("item3", nestedTable.rows[0].cells[2])
+ }
+
+ @Test
+ public fun `list items with colon are treated as plain text values`() {
+ val rawMarkdown =
+ """
+ |---
+ |tags:
+ | - team: platform
+ | - owner: jewel
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(1, table.columnCount)
+ assertCellText("tags", table.header.asNotNull().cells[0])
+
+ val valueCell = table.rows[0].cells[0]
+ val nestedTable = valueCell.content.assertIs()
+ assertEquals(2, nestedTable.columnCount)
+ assertNull(nestedTable.header)
+ assertEquals(1, nestedTable.rowCount)
+ assertCellText("team: platform", nestedTable.rows[0].cells[0])
+ assertCellText("owner: jewel", nestedTable.rows[0].cells[1])
+ }
+
+ @Test
+ public fun `literal block scalar preserves newlines`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: |
+ | Line one
+ | Line two
+ | Line three
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("description", table.header.asNotNull().cells[0])
+ assertCellText("Line one\nLine two\nLine three\n", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `literal block scalar preserves leading blank lines`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: |
+ |
+ | First line
+ | Second line
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("description", table.header.asNotNull().cells[0])
+ assertCellText("\nFirst line\nSecond line\n", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `literal block scalar with strip chomping`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: |-
+ | Line one
+ | Line two
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("Line one\nLine two", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `literal block scalar with keep chomping`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: |+
+ | Line one
+ | Line two
+ |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("Line one\nLine two\n\n", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `folded block scalar joins consecutive lines`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: >
+ | This is a long
+ | description that
+ | spans multiple lines
+ |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("This is a long description that spans multiple lines\n", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `folded block scalar with strip chomping`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: >-
+ | This is a long
+ | description that
+ | spans multiple lines
+ |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("This is a long description that spans multiple lines", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `folded block scalar with keep chomping`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: >+
+ | Line one
+ | Line two
+ |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("Line one Line two\n\n", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `folded block scalar with blank lines creates paragraph breaks`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: >
+ | Paragraph one
+ | continued.
+ |
+ | Paragraph two.
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("Paragraph one continued.\n\nParagraph two.\n", table.rows[0].cells[0])
+ }
+
+ @Test
+ public fun `empty block scalar followed by end marker`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: Test
+ |description: |
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertCellText("title", table.header.asNotNull().cells[0])
+ assertCellText("description", table.header.asNotNull().cells[1])
+ assertCellText("Test", table.rows[0].cells[0])
+ assertCellText("", table.rows[0].cells[1])
+ }
+
+ @Test
+ public fun `blank folded strip scalar followed by key starts a new key-value pair`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: >-
+ |
+ |name: Hello World!
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertCellText("description", table.header.asNotNull().cells[0])
+ assertCellText("name", table.header.asNotNull().cells[1])
+ assertCellText("", table.rows[0].cells[0])
+ assertCellText("Hello World!", table.rows[0].cells[1])
+ }
+
+ @Test
+ public fun `block scalar followed by another key`() {
+ val rawMarkdown =
+ """
+ |---
+ |description: |
+ | Some text
+ | More text
+ |author: Jane
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertEquals(2, table.columnCount)
+ assertCellText("description", table.header.asNotNull().cells[0])
+ assertCellText("author", table.header.asNotNull().cells[1])
+ assertCellText("Some text\nMore text\n", table.rows[0].cells[0])
+ assertCellText("Jane", table.rows[0].cells[1])
+ }
+
+ @Test
+ public fun `quoted values are unquoted`() {
+ val rawMarkdown =
+ """
+ |---
+ |title: "Hello World"
+ |subtitle: 'Single Quoted'
+ |---
+ """
+ .trimMargin()
+ val blocks = processor.processMarkdownDocument(rawMarkdown)
+
+ val table = blocks.first().assertIs()
+ assertCellText("Hello World", table.rows[0].cells[0])
+ assertCellText("Single Quoted", table.rows[0].cells[1])
+ }
+
+ private fun assertCellText(expected: String, cell: TableCell) {
+ val paragraph = cell.content.assertIs()
+ val textContent = paragraph.inlineContent.filterIsInstance()
+ assertTrue(textContent.isNotEmpty())
+ assertEquals(expected, textContent.first().content)
+ }
+
+ private fun T?.asNotNull(): T {
+ assertNotNull(this)
+ @Suppress("UNCHECKED_CAST")
+ return this as T
+ }
+
+ 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/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 62ec428e1fab8..1a202d4c20c55 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) {
- 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 fcb5abbca2a7b..a6562515b824a 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 2e62fda7c28b7..7f16b24788563 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 2931a57bff13f..e7637c96a97d7 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 018a4ab7f4a1e..36efa4a1b3afa 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
@@ -302,6 +302,7 @@
+