Skip to content
Closed
13 changes: 11 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
kobweb = "0.23.2-SNAPSHOT"
#------------------------
commonmark = "0.24.0"
commonmark = "0.25.1"
compose = "1.8.0"
dokka = "2.0.0"
gradle-publish = "1.3.0"
Expand All @@ -24,8 +24,10 @@ vanniktech-publish = "0.34.0"
[libraries]
commonmark-core = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
commonmark-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" }
commonmark-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" }
commonmark-tables = { module = "org.commonmark:commonmark-ext-gfm-tables", version.ref = "commonmark" }
commonmark-tasklist = { module = "org.commonmark:commonmark-ext-task-list-items", version.ref = "commonmark" }
commonmark-footnotes = { module = "org.commonmark:commonmark-ext-footnotes", version.ref = "commonmark" }
compose-html-core = { module = "org.jetbrains.compose.html:html-core", version.ref = "compose" }
compose-compiler-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" }
Expand Down Expand Up @@ -62,7 +64,14 @@ truthish = { module = "com.varabyte.truthish:truthish", version.ref = "truthish"
vanniktech-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "vanniktech-publish" }

[bundles]
commonmark = ["commonmark-core", "commonmark-autolink", "commonmark-tables", "commonmark-tasklist"]
commonmark = [
"commonmark-core",
"commonmark-autolink",
"commonmark-tables",
"commonmark-tasklist",
"commonmark-strikethrough",
"commonmark-footnotes",
]
ktor = [
"ktor-serialization-json",
"ktor-server-auth",
Expand Down
3 changes: 2 additions & 1 deletion playground/site/src/jsMain/resources/markdown/Markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This site is generated from Markdown.

Create rich, dynamic web apps with ease, leveraging [Kotlin](https://kotlinlang.org/) and [Compose HTML](https://github.com/JetBrains/compose-multiplatform#compose-html).

Markdown of course supports **bold**, _italic_, and _**bold italic**_ text.
Markdown of course supports **bold**, _italic_, _**bold italic**_, and ~~strikethrough~~ text.

It also supports

Expand Down Expand Up @@ -45,6 +45,7 @@ You can link to other Markdown documents with their route overrides resolved cor
[documents/INDEX.md](documents/INDEX.md)<br>
[KotlinLanguage.md](jetbrains/KotlinLanguage.md) (`routeOverride: languages/kotlin`)<br>
[documents/Bananas.md](documents/Bananas.md) (`routeOverride: /fruits/`)<br>
[tests/unified_test.md](tests/unified_test.md) (`routeOverride: /example/`)<br>
[files/external.md](/files/external.md) (Linking to a Markdown file outside of the processed markdown files)<br>

You can use <span id="md-inline-demo">inlined html</span> tags. You can inspect this page to see that "inlined html" is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: Unified Markdown Processing Test
date: 'July 8, 2025'
description: Testing unified markdown processing with remark-gfm and rehype-raw plugins
tags: [ 'kobweb', 'commonmark', 'gfm', 'test', 'md', 'kotlin' ]
posted: true
---

# Unified Markdown Processing Test

This content demonstrates markdown features on the **Kobweb Framework**, which uses
a [CommonMark](https://commonmark.org/) parser
with [GitHub Flavored Markdown (GFM) extensions](https://github.github.com/gfm/).

## GitHub Flavored Markdown Features

### Strikethrough

~~This text should be struck through~~ or ~one tilde~

### Tables
| Feature | Status | Notes |
|------------------|------------------------------------|-------------------------------|
| Strikethrough | * [x] | Crosses out text |
| Tables | [x] | Organizes data in a grid |
| Task Lists |- [x] | Creates checklists |
| HTML in Markdown | - [x] <mark>highlighted text</mark> | Allows embedding raw HTML tags |
| Code Blocks | - [x] | Allows embedding code blocks |
| Links | - [x] | Allows embedding links |
| Emphasis | - [x] | Allows embedding emphasis |
| Column A | Column B | Column C | Column D |
|----------| :--------- |:--------:|---------:|
| Left | Left | Center | Right |
| Data 1 | Data 2 | Data 3 | Data 4 |
| Row 2 | Row 2 | Row 2 | Row 2 |

### Task Lists

- [x] Completed task
- [ ] Pending task
- [x] Another completed task
- [ ] Another pending task
- [x] Venus
- <input type="checkbox" disabled checked /> works

### HTML Elements in Markdown

This should work with <u>underlined text</u> and <mark>highlighted text</mark>.

<div style="background-color: #f0f0f0; padding: 10px; border-left: 4px solid #007acc;">
<strong>Note:</strong> This is HTML content inside markdown that should be preserved with rehype-raw.
</div>

### Code Blocks

```javascript
// This is a JavaScript code block
function hello() {
console.log("Hello from unified markdown processing!");
}
```

### Links and Autolinks

Regular link: [Kobweb Documentation](https://kobweb.varabyte.com)

Autolink: https://kobweb.varabyte.com and www.example.com.

### Emphasis and Strong

*Italic text* and **bold text** and ***bold italic text***

## Footnote

A note can have a simple footnote.[^1] Here are some more examples:

- A footnote with a link.[^link]
- A footnote with multiple paragraphs.[^multi]
- A footnote with a long text.[^long]

[^1]: This is the first simple footnote.

[^link]: This footnote contains a link to [Kobweb](https://kobweb.varabyte.com).

[^multi]: This footnote has multiple paragraphs.
[foo]: https://example.com/foo

This is the second paragraph of the footnote.

[^long]: Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem
aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim
ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit,
sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

---

✅ If you can see all the above features working correctly, then the CommonMark-java parser with its GFM extensions is functioning properly!
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import com.varabyte.kobwebx.gradle.markdown.frontmatter.FrontMatterBlock
import com.varabyte.kobwebx.gradle.markdown.handlers.MarkdownHandlers
import com.varabyte.kobwebx.gradle.markdown.handlers.NodeScope
import com.varabyte.kobwebx.gradle.markdown.util.NodeCache
import com.varabyte.kobwebx.gradle.markdown.util.escapeQuotes
import com.varabyte.kobwebx.gradle.markdown.util.unescapeQuotes
import org.commonmark.ext.footnotes.FootnoteDefinition
import org.commonmark.ext.footnotes.FootnoteReference
import org.commonmark.ext.gfm.strikethrough.Strikethrough
import org.commonmark.ext.gfm.tables.TableBlock
import org.commonmark.ext.gfm.tables.TableBody
import org.commonmark.ext.gfm.tables.TableCell
import org.commonmark.ext.gfm.tables.TableHead
import org.commonmark.ext.gfm.tables.TableRow
import org.commonmark.ext.task.list.items.TaskListItemMarker
import org.commonmark.node.AbstractVisitor
import org.commonmark.node.BlockQuote
import org.commonmark.node.BulletList
Expand Down Expand Up @@ -339,6 +342,7 @@ class KotlinRenderer internal constructor(
override fun visit(code: Code) {
doVisit(code, handlers.inlineCode)
}


override fun visit(emphasis: Emphasis) {
doVisit(emphasis, handlers.em)
Expand Down Expand Up @@ -397,7 +401,7 @@ class KotlinRenderer internal constructor(

val route = Route(
frontMatterData?.routeOverride
?: nodeCache.metadata.getValue(destinationNode).routeWithSlug!! // Guaranteed set for a page
?: nodeCache.metadata.getValue(destinationNode).routeWithSlug!! // Guaranteed set for a page
)
if (route.isDynamic) {
error("Markdown file link '${link.destination}' links to file with dynamic route override. This is not supported!")
Expand Down Expand Up @@ -434,24 +438,64 @@ class KotlinRenderer internal constructor(
}
}

private fun visit(strikethrough: Strikethrough) {
doVisit(strikethrough, handlers.strikethrough)
}

override fun visit(strongEmphasis: StrongEmphasis) {
doVisit(strongEmphasis, handlers.strong)
}

override fun visit(text: Text) {
doVisit(text, handlers.text)
}
override fun visit(customBlock: CustomBlock) {
when (customBlock) {
is KobwebCallBlock -> {
val visitor = KobwebCallBlockVisitor()
customBlock.accept(visitor)
visitor.call?.let { call ->
visit(call)
visitor.childrenNodes?.let { children ->
++indentCount
children.forEach { node -> node.accept(this) }
--indentCount
output.appendLine("$indent}")
}
}
}

is FrontMatterBlock -> {
// No-op. We don't need to do anything here because we already handled parsing front matter earlier.
}

is TableBlock -> visit(customBlock)
is FootnoteDefinition -> {
doVisit(customBlock, handlers.footnoteDefinition)
}

else -> {
val simple = customBlock::class.simpleName
val unhandledBlockName = simple!!
reporter.warn("Unhandled Markdown custom block: $unhandledBlockName. Consider reporting this at: https://github.com/varabyte/kobweb/issues/new?labels=bug&template=bug_report.md&title=Unhandled%20Markdown%20block%20%22$unhandledBlockName%22")

}
}
}

override fun visit(customNode: CustomNode) {
when (customNode) {
is KobwebCall -> {
output.appendLine("$indent${customNode.toFqn(projectGroup)}")
}

is Strikethrough -> visit(customNode)
is TableHead -> visit(customNode)
is TableBody -> visit(customNode)
is TableRow -> visit(customNode)
is TableCell -> visit(customNode)
is FootnoteReference -> visit(customNode)
is TaskListItemMarker -> visit(customNode)

else -> {
val unhandledNodeName = customNode::class.simpleName!!
Expand All @@ -460,6 +504,10 @@ class KotlinRenderer internal constructor(
}
}

private fun visit(footnoteReference: FootnoteReference) {
doVisit(footnoteReference, handlers.footnoteReference)
}

private fun visit(table: TableBlock) {
doVisit(table, handlers.table)
}
Expand All @@ -483,34 +531,8 @@ class KotlinRenderer internal constructor(
doVisit(tableCell, handlers.td)
}
}

override fun visit(customBlock: CustomBlock) {
when (customBlock) {
is KobwebCallBlock -> {
val visitor = KobwebCallBlockVisitor()
customBlock.accept(visitor)
visitor.call?.let { call ->
visit(call)
visitor.childrenNodes?.let { children ->
++indentCount
children.forEach { node -> node.accept(this) }
--indentCount
output.appendLine("$indent}")
}
}
}

is FrontMatterBlock -> {
// No-op. We don't need to do anything here because we already handled parsing front matter earlier.
}

is TableBlock -> visit(customBlock)

else -> {
val unhandledBlockName = customBlock::class.simpleName!!
reporter.warn("Unhandled Markdown custom block: $unhandledBlockName. Consider reporting this at: https://github.com/varabyte/kobweb/issues/new?labels=bug&template=bug_report.md&title=Unhandled%20Markdown%20block%20%22$unhandledBlockName%22")
}
}
private fun visit(taskListItemMarker: TaskListItemMarker) {
doVisit(taskListItemMarker, handlers.taskListItemMarker)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.varabyte.kobwebx.gradle.markdown.ext.kobwebcall.KobwebCallExtension
import com.varabyte.kobwebx.gradle.markdown.frontmatter.FrontMatterExtension
import org.commonmark.Extension
import org.commonmark.ext.autolink.AutolinkExtension
import org.commonmark.ext.footnotes.FootnotesExtension
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.ext.task.list.items.TaskListItemsExtension
import org.commonmark.parser.IncludeSourceSpans
Expand Down Expand Up @@ -37,6 +39,16 @@ abstract class MarkdownFeatures {
@get:Input
abstract val autolink: Property<Boolean>


/**
* If true, support footnotes like "Main text[^1]" with definitions "[^1]: Footnote text".
*
* Defaults to `true`.
*
* Note: Inline footnotes via `^[inline]` are not supported at this time.
*/
@get:Input
abstract val footnotes: Property<Boolean>
/**
* If true, support front matter (a header YAML block at the top of your markdown file with key/value pairs).
*
Expand Down Expand Up @@ -73,6 +85,15 @@ abstract class MarkdownFeatures {
*/
@get:Input
abstract val kobwebCallDelimiters: Property<Pair<Char, Char>>
/**
* If true, support GFM strikethrough syntax using double tildes, e.g. `~~text~~`.
*
* Defaults to `true`.
*
* @see <a href="https://github.com/commonmark/commonmark-java#strikethrough">Strikethrough</a>
*/
@get:Input
abstract val strikethrough: Property<Boolean>

/**
* If true, support creating tables via pipe syntax.
Expand Down Expand Up @@ -100,11 +121,14 @@ abstract class MarkdownFeatures {
@get:Input
abstract val taskList: Property<Boolean>


init {
autolink.convention(true)
footnotes.convention(true)
frontMatter.convention(true)
kobwebCall.convention(true)
kobwebCallDelimiters.convention('{' to '}')
strikethrough.convention(true)
tables.convention(true)
taskList.convention(true)
}
Expand All @@ -117,19 +141,26 @@ abstract class MarkdownFeatures {
if (autolink.get()) {
extensions.add(AutolinkExtension.create())
}
if (footnotes.get()) {
extensions.add(FootnotesExtension.create())
}
if (frontMatter.get()) {
extensions.add(FrontMatterExtension.create())
}
if (kobwebCall.get()) {
extensions.add(KobwebCallExtension.create(kobwebCallDelimiters.get()) { createParser() })
}
if (strikethrough.get()) {
extensions.add(StrikethroughExtension.create())
}
if (tables.get()) {
extensions.add(TablesExtension.create())
}
if (taskList.get()) {
extensions.add(TaskListItemsExtension.create())
}


return Parser.builder()
.extensions(extensions)
.includeSourceSpans(IncludeSourceSpans.BLOCKS_AND_INLINES)
Expand Down
Loading