Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,41 +5,50 @@ Parse, transform, and emit Markdown (or HTML) with a composable, AST-based API.

## Overview

markflow is built on [Flexmark-java](https://github.com/vsch/flexmark-java) and provides:
markflow is built on [commonmark-java](https://github.com/commonmark/commonmark-java) and provides:

- A clean Kotlin API for parsing Markdown into a mutable AST
- Composable transformation passes (heading manipulation, front matter promotion, etc.)
- Round-trip Markdown output via Flexmark's `Formatter`, with reliable front matter handling
- Round-trip Markdown output via commonmark-java's `MarkdownRenderer`, with reliable front matter handling
- A Gradle plugin exposing common transformations as cacheable, incremental tasks

It is **not** a static site generator. It is a document-processing library and build-tool
integration layer — the Markdown equivalent of what a CSS post-processor does for stylesheets.

## Design Decisions

### Parser: Flexmark-java
### Parser: commonmark-java

markflow uses [Flexmark-java](https://github.com/vsch/flexmark-java) as its parser and formatter.
Flexmark was chosen because:
markflow uses [commonmark-java](https://github.com/commonmark/commonmark-java) as its parser and
formatter. commonmark-java was chosen because:

- It has the most mature round-trip Markdown output (`Formatter`) of any JVM library
- Its extension API is clean and well-suited to custom AST node types
- It supports GFM tables, YAML front matter, and other common extensions out of the box
- It is Apache 2.0 licensed, compatible with all likely dependencies
- It is actively maintained (Atlassian origin, used by OpenJDK, Google, Gerrit)
- It is spec-compliant with [CommonMark](https://spec.commonmark.org/)
- It provides round-trip Markdown output via `MarkdownRenderer`
- It supports GFM tables, YAML front matter (including raw/opaque capture), strikethrough, footnotes,
and other common extensions out of the box
- It is BSD 2-Clause licensed, compatible with all likely dependencies

[JetBrains Markdown](https://github.com/JetBrains/markdown) was considered but ruled out: its AST
is largely immutable, it has no Markdown output support, and it has no front matter extension.
It remains the only JVM option for Kotlin Multiplatform targets, but KMP is not a current requirement.
**Alternatives considered:**

### Front Matter: Opaque Text Node
[Flexmark-java](https://github.com/vsch/flexmark-java) was the original choice and has a richer
extension ecosystem (TOC, definition lists, typographic quotes, etc.), but has been effectively
unmaintained since 2023 with 178+ open issues. The maintenance risk outweighs the extension breadth
for a library intended for long-term use.

Flexmark's built-in `YamlFrontMatterExtension` parses front matter into key/value pairs but does
not reliably round-trip it through the `Formatter`. markflow instead provides a custom extension
that captures the entire front matter block as a single opaque text node in the AST. This means:
[JetBrains Markdown](https://github.com/JetBrains/markdown) is actively developed but ruled out:
its AST is largely immutable, it has no Markdown output support, and it targets Kotlin Multiplatform
which is not a current requirement.

- The `Formatter` preserves front matter verbatim by default
### Front Matter: Opaque Capture via RawContentParser

commonmark-java's `YamlFrontMatterExtension` supports a `RawContentParser` mode that captures the
entire front matter block as a single raw string rather than parsing it into key/value pairs. This
means:

- The `MarkdownRenderer` preserves front matter verbatim by default
- Callers that need to inspect or modify front matter values do so via Jackson YAML, extracting
the raw text from the node, modifying it, and writing it back
the raw text, modifying it, and writing it back
- No front matter content is silently dropped or reordered

### Transformation Pipeline
Expand All@@ -59,18 +68,18 @@ markflow/

### `library`

The standalone Kotlin library JAR. Depends only on Flexmark-java and Jackson YAML. No build-tool
The standalone Kotlin library JAR. Depends only on commonmark-java and Jackson YAML. No build-tool
APIs. Usable from any JVM context: Gradle, Maven, a CLI, application code.

Key types:

| Type | Description |
|---|---|
| `MarkdownDocument` | A parsed document: mutable Flexmark `Document` + raw front matter string |
| `MarkdownDocument` | A parsed document: mutable commonmark-java `Document` + raw front matter string |
| `MarkdownParser` | Parses a `String` or `File` into a `MarkdownDocument` |
| `MarkdownTransformer` | Single-responsibility AST transformation pass |
| `MarkdownPipeline` | Ordered composition of `MarkdownTransformer` instances |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via Flexmark `Formatter`) or HTML (via `HtmlRenderer`) |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via `MarkdownRenderer`) or HTML (via `HtmlRenderer`) |
| `FrontMatter` | Typed view of the front matter block via Jackson YAML |

Built-in transformers:
Expand All@@ -82,7 +91,7 @@ Built-in transformers:
| `TableFormatter` | Pads GFM table columns to consistent widths |
| `SentencePerLineFormatter` | Splits paragraph text at sentence boundaries, one sentence per line |
| `TemplateSubstitutor` | Replaces `{{key}}` placeholders with provided values, skipping fenced code blocks |
| `ScreenshotPlaceholderExpander` | Expands `<!-- screenshot: name.png | alt -->` to `![alt](path/name.png)` |
| `HtmlCommentExpander` | Expands structured HTML comments to Markdown content via registered handlers |

### `gradle-plugin`

Expand DownExpand Up@@ -111,7 +120,7 @@ Sub-packages follow feature boundaries:
|---|---|
| `net.oxspring.markflow` | Public API: `MarkdownDocument`, `MarkdownParser`, `MarkdownPipeline`, `MarkdownFormatter` |
| `net.oxspring.markflow.transform` | Built-in `MarkdownTransformer` implementations |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, opaque front matter AST extension |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, front matter access utilities |
| `net.oxspring.markflow.gradle` | Gradle plugin and task implementations |

## MVP Scope (v1.0)
Expand All@@ -129,7 +138,7 @@ The v1.0 MVP targets feature parity with the custom Gradle tasks in the
markflow replaces this with a `ProcessMarkdownTask` configured with:

- `TemplateSubstitutor` (token substitution, fence-aware)
- `ScreenshotPlaceholderExpander`
- `HtmlCommentExpander` with a screenshot handler

### Replaces front matter title extraction in `BuildPdfTask`

Expand DownExpand Up@@ -158,16 +167,18 @@ markflow replaces this with a `ProcessMarkdownTask` configured with:
./gradlew build # assemble + check
```

Test framework: JUnit 5 (Jupiter) with AssertJ.
Test framework: JUnit 6 (Jupiter) with AssertJ.

## Key Dependencies

| Dependency | Version | License |
|---|---|---|
| Flexmark-java | 0.64.x | BSD 2-Clause |
| commonmark-java | 0.30.x | BSD 2-Clause |
| commonmark-ext-gfm-tables | 0.30.x | BSD 2-Clause |
| commonmark-ext-yaml-front-matter | 0.30.x | BSD 2-Clause |
| Jackson YAML | 2.x | Apache 2.0 |
| Kotlin | 2.x | Apache 2.0 |
| Gradle Plugin API | 8.x | Apache 2.0 |
| Gradle Plugin API | 9.x | Apache 2.0 |

## License

Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
[versions]
assertj = "3.27.7"
flexmark = "0.64.8"
commonmark = "0.30.0"
git-versioning = "6.4.4"
jacksonYaml = "2.22.2"
jackson-yaml = "2.22.2"
junit5 = "6.1.3"
kotlin = "2.4.10"
kover = "0.9.9"
ktlint-gradle = "14.2.0"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonYaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonYaml" }
commonmark = { module = "org.commonmark:commonmark", 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" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson-yaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-yaml" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }

[plugins]
git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versioning" }
Expand Down
4 changes: 3 additions & 1 deletion library/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,9 @@ plugins {
}

dependencies {
implementation(libs.flexmark.all)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.jackson.dataformat.yaml)
implementation(libs.jackson.module.kotlin)

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,41 +5,50 @@ Parse, transform, and emit Markdown (or HTML) with a composable, AST-based API.

## Overview

markflow is built on [Flexmark-java](https://github.com/vsch/flexmark-java) and provides:
markflow is built on [commonmark-java](https://github.com/commonmark/commonmark-java) and provides:

- A clean Kotlin API for parsing Markdown into a mutable AST
- Composable transformation passes (heading manipulation, front matter promotion, etc.)
- Round-trip Markdown output via Flexmark's `Formatter`, with reliable front matter handling
- Round-trip Markdown output via commonmark-java's `MarkdownRenderer`, with reliable front matter handling
- A Gradle plugin exposing common transformations as cacheable, incremental tasks

It is **not** a static site generator. It is a document-processing library and build-tool
integration layer — the Markdown equivalent of what a CSS post-processor does for stylesheets.

## Design Decisions

### Parser: Flexmark-java
### Parser: commonmark-java

markflow uses [Flexmark-java](https://github.com/vsch/flexmark-java) as its parser and formatter.
Flexmark was chosen because:
markflow uses [commonmark-java](https://github.com/commonmark/commonmark-java) as its parser and
formatter. commonmark-java was chosen because:

- It has the most mature round-trip Markdown output (`Formatter`) of any JVM library
- Its extension API is clean and well-suited to custom AST node types
- It supports GFM tables, YAML front matter, and other common extensions out of the box
- It is Apache 2.0 licensed, compatible with all likely dependencies
- It is actively maintained (Atlassian origin, used by OpenJDK, Google, Gerrit)
- It is spec-compliant with [CommonMark](https://spec.commonmark.org/)
- It provides round-trip Markdown output via `MarkdownRenderer`
- It supports GFM tables, YAML front matter (including raw/opaque capture), strikethrough, footnotes,
and other common extensions out of the box
- It is BSD 2-Clause licensed, compatible with all likely dependencies

[JetBrains Markdown](https://github.com/JetBrains/markdown) was considered but ruled out: its AST
is largely immutable, it has no Markdown output support, and it has no front matter extension.
It remains the only JVM option for Kotlin Multiplatform targets, but KMP is not a current requirement.
**Alternatives considered:**

### Front Matter: Opaque Text Node
[Flexmark-java](https://github.com/vsch/flexmark-java) was the original choice and has a richer
extension ecosystem (TOC, definition lists, typographic quotes, etc.), but has been effectively
unmaintained since 2023 with 178+ open issues. The maintenance risk outweighs the extension breadth
for a library intended for long-term use.

Flexmark's built-in `YamlFrontMatterExtension` parses front matter into key/value pairs but does
not reliably round-trip it through the `Formatter`. markflow instead provides a custom extension
that captures the entire front matter block as a single opaque text node in the AST. This means:
[JetBrains Markdown](https://github.com/JetBrains/markdown) is actively developed but ruled out:
its AST is largely immutable, it has no Markdown output support, and it targets Kotlin Multiplatform
which is not a current requirement.

- The `Formatter` preserves front matter verbatim by default
### Front Matter: Opaque Capture via RawContentParser

commonmark-java's `YamlFrontMatterExtension` supports a `RawContentParser` mode that captures the
entire front matter block as a single raw string rather than parsing it into key/value pairs. This
means:

- The `MarkdownRenderer` preserves front matter verbatim by default
- Callers that need to inspect or modify front matter values do so via Jackson YAML, extracting
the raw text from the node, modifying it, and writing it back
the raw text, modifying it, and writing it back
- No front matter content is silently dropped or reordered

### Transformation Pipeline
Expand All@@ -59,18 +68,18 @@ markflow/

### `library`

The standalone Kotlin library JAR. Depends only on Flexmark-java and Jackson YAML. No build-tool
The standalone Kotlin library JAR. Depends only on commonmark-java and Jackson YAML. No build-tool
APIs. Usable from any JVM context: Gradle, Maven, a CLI, application code.

Key types:

| Type | Description |
|---|---|
| `MarkdownDocument` | A parsed document: mutable Flexmark `Document` + raw front matter string |
| `MarkdownDocument` | A parsed document: mutable commonmark-java `Document` + raw front matter string |
| `MarkdownParser` | Parses a `String` or `File` into a `MarkdownDocument` |
| `MarkdownTransformer` | Single-responsibility AST transformation pass |
| `MarkdownPipeline` | Ordered composition of `MarkdownTransformer` instances |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via Flexmark `Formatter`) or HTML (via `HtmlRenderer`) |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via `MarkdownRenderer`) or HTML (via `HtmlRenderer`) |
| `FrontMatter` | Typed view of the front matter block via Jackson YAML |

Built-in transformers:
Expand All@@ -82,7 +91,7 @@ Built-in transformers:
| `TableFormatter` | Pads GFM table columns to consistent widths |
| `SentencePerLineFormatter` | Splits paragraph text at sentence boundaries, one sentence per line |
| `TemplateSubstitutor` | Replaces `{{key}}` placeholders with provided values, skipping fenced code blocks |
| `ScreenshotPlaceholderExpander` | Expands `<!-- screenshot: name.png | alt -->` to `![alt](path/name.png)` |
| `HtmlCommentExpander` | Expands structured HTML comments to Markdown content via registered handlers |

### `gradle-plugin`

Expand DownExpand Up@@ -111,7 +120,7 @@ Sub-packages follow feature boundaries:
|---|---|
| `net.oxspring.markflow` | Public API: `MarkdownDocument`, `MarkdownParser`, `MarkdownPipeline`, `MarkdownFormatter` |
| `net.oxspring.markflow.transform` | Built-in `MarkdownTransformer` implementations |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, opaque front matter AST extension |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, front matter access utilities |
| `net.oxspring.markflow.gradle` | Gradle plugin and task implementations |

## MVP Scope (v1.0)
Expand All@@ -129,7 +138,7 @@ The v1.0 MVP targets feature parity with the custom Gradle tasks in the
markflow replaces this with a `ProcessMarkdownTask` configured with:

- `TemplateSubstitutor` (token substitution, fence-aware)
- `ScreenshotPlaceholderExpander`
- `HtmlCommentExpander` with a screenshot handler

### Replaces front matter title extraction in `BuildPdfTask`

Expand DownExpand Up@@ -158,16 +167,18 @@ markflow replaces this with a `ProcessMarkdownTask` configured with:
./gradlew build # assemble + check
```

Test framework: JUnit 5 (Jupiter) with AssertJ.
Test framework: JUnit 6 (Jupiter) with AssertJ.

## Key Dependencies

| Dependency | Version | License |
|---|---|---|
| Flexmark-java | 0.64.x | BSD 2-Clause |
| commonmark-java | 0.30.x | BSD 2-Clause |
| commonmark-ext-gfm-tables | 0.30.x | BSD 2-Clause |
| commonmark-ext-yaml-front-matter | 0.30.x | BSD 2-Clause |
| Jackson YAML | 2.x | Apache 2.0 |
| Kotlin | 2.x | Apache 2.0 |
| Gradle Plugin API | 8.x | Apache 2.0 |
| Gradle Plugin API | 9.x | Apache 2.0 |

## License

Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
[versions]
assertj = "3.27.7"
flexmark = "0.64.8"
commonmark = "0.30.0"
git-versioning = "6.4.4"
jacksonYaml = "2.22.2"
jackson-yaml = "2.22.2"
junit5 = "6.1.3"
kotlin = "2.4.10"
kover = "0.9.9"
ktlint-gradle = "14.2.0"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonYaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonYaml" }
commonmark = { module = "org.commonmark:commonmark", 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" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson-yaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-yaml" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }

[plugins]
git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versioning" }
Expand Down
4 changes: 3 additions & 1 deletion library/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,9 @@ plugins {
}

dependencies {
implementation(libs.flexmark.all)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.jackson.dataformat.yaml)
implementation(libs.jackson.module.kotlin)

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,41 +5,50 @@ Parse, transform, and emit Markdown (or HTML) with a composable, AST-based API.

## Overview

markflow is built on [Flexmark-java](https://github.com/vsch/flexmark-java) and provides:
markflow is built on [commonmark-java](https://github.com/commonmark/commonmark-java) and provides:

- A clean Kotlin API for parsing Markdown into a mutable AST
- Composable transformation passes (heading manipulation, front matter promotion, etc.)
- Round-trip Markdown output via Flexmark's `Formatter`, with reliable front matter handling
- Round-trip Markdown output via commonmark-java's `MarkdownRenderer`, with reliable front matter handling
- A Gradle plugin exposing common transformations as cacheable, incremental tasks

It is **not** a static site generator. It is a document-processing library and build-tool
integration layer — the Markdown equivalent of what a CSS post-processor does for stylesheets.

## Design Decisions

### Parser: Flexmark-java
### Parser: commonmark-java

markflow uses [Flexmark-java](https://github.com/vsch/flexmark-java) as its parser and formatter.
Flexmark was chosen because:
markflow uses [commonmark-java](https://github.com/commonmark/commonmark-java) as its parser and
formatter. commonmark-java was chosen because:

- It has the most mature round-trip Markdown output (`Formatter`) of any JVM library
- Its extension API is clean and well-suited to custom AST node types
- It supports GFM tables, YAML front matter, and other common extensions out of the box
- It is Apache 2.0 licensed, compatible with all likely dependencies
- It is actively maintained (Atlassian origin, used by OpenJDK, Google, Gerrit)
- It is spec-compliant with [CommonMark](https://spec.commonmark.org/)
- It provides round-trip Markdown output via `MarkdownRenderer`
- It supports GFM tables, YAML front matter (including raw/opaque capture), strikethrough, footnotes,
and other common extensions out of the box
- It is BSD 2-Clause licensed, compatible with all likely dependencies

[JetBrains Markdown](https://github.com/JetBrains/markdown) was considered but ruled out: its AST
is largely immutable, it has no Markdown output support, and it has no front matter extension.
It remains the only JVM option for Kotlin Multiplatform targets, but KMP is not a current requirement.
**Alternatives considered:**

### Front Matter: Opaque Text Node
[Flexmark-java](https://github.com/vsch/flexmark-java) was the original choice and has a richer
extension ecosystem (TOC, definition lists, typographic quotes, etc.), but has been effectively
unmaintained since 2023 with 178+ open issues. The maintenance risk outweighs the extension breadth
for a library intended for long-term use.

Flexmark's built-in `YamlFrontMatterExtension` parses front matter into key/value pairs but does
not reliably round-trip it through the `Formatter`. markflow instead provides a custom extension
that captures the entire front matter block as a single opaque text node in the AST. This means:
[JetBrains Markdown](https://github.com/JetBrains/markdown) is actively developed but ruled out:
its AST is largely immutable, it has no Markdown output support, and it targets Kotlin Multiplatform
which is not a current requirement.

- The `Formatter` preserves front matter verbatim by default
### Front Matter: Opaque Capture via RawContentParser

commonmark-java's `YamlFrontMatterExtension` supports a `RawContentParser` mode that captures the
entire front matter block as a single raw string rather than parsing it into key/value pairs. This
means:

- The `MarkdownRenderer` preserves front matter verbatim by default
- Callers that need to inspect or modify front matter values do so via Jackson YAML, extracting
the raw text from the node, modifying it, and writing it back
the raw text, modifying it, and writing it back
- No front matter content is silently dropped or reordered

### Transformation Pipeline
Expand All@@ -59,18 +68,18 @@ markflow/

### `library`

The standalone Kotlin library JAR. Depends only on Flexmark-java and Jackson YAML. No build-tool
The standalone Kotlin library JAR. Depends only on commonmark-java and Jackson YAML. No build-tool
APIs. Usable from any JVM context: Gradle, Maven, a CLI, application code.

Key types:

| Type | Description |
|---|---|
| `MarkdownDocument` | A parsed document: mutable Flexmark `Document` + raw front matter string |
| `MarkdownDocument` | A parsed document: mutable commonmark-java `Document` + raw front matter string |
| `MarkdownParser` | Parses a `String` or `File` into a `MarkdownDocument` |
| `MarkdownTransformer` | Single-responsibility AST transformation pass |
| `MarkdownPipeline` | Ordered composition of `MarkdownTransformer` instances |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via Flexmark `Formatter`) or HTML (via `HtmlRenderer`) |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via `MarkdownRenderer`) or HTML (via `HtmlRenderer`) |
| `FrontMatter` | Typed view of the front matter block via Jackson YAML |

Built-in transformers:
Expand All@@ -82,7 +91,7 @@ Built-in transformers:
| `TableFormatter` | Pads GFM table columns to consistent widths |
| `SentencePerLineFormatter` | Splits paragraph text at sentence boundaries, one sentence per line |
| `TemplateSubstitutor` | Replaces `{{key}}` placeholders with provided values, skipping fenced code blocks |
| `ScreenshotPlaceholderExpander` | Expands `<!-- screenshot: name.png | alt -->` to `![alt](path/name.png)` |
| `HtmlCommentExpander` | Expands structured HTML comments to Markdown content via registered handlers |

### `gradle-plugin`

Expand DownExpand Up@@ -111,7 +120,7 @@ Sub-packages follow feature boundaries:
|---|---|
| `net.oxspring.markflow` | Public API: `MarkdownDocument`, `MarkdownParser`, `MarkdownPipeline`, `MarkdownFormatter` |
| `net.oxspring.markflow.transform` | Built-in `MarkdownTransformer` implementations |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, opaque front matter AST extension |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, front matter access utilities |
| `net.oxspring.markflow.gradle` | Gradle plugin and task implementations |

## MVP Scope (v1.0)
Expand All@@ -129,7 +138,7 @@ The v1.0 MVP targets feature parity with the custom Gradle tasks in the
markflow replaces this with a `ProcessMarkdownTask` configured with:

- `TemplateSubstitutor` (token substitution, fence-aware)
- `ScreenshotPlaceholderExpander`
- `HtmlCommentExpander` with a screenshot handler

### Replaces front matter title extraction in `BuildPdfTask`

Expand DownExpand Up@@ -158,16 +167,18 @@ markflow replaces this with a `ProcessMarkdownTask` configured with:
./gradlew build # assemble + check
```

Test framework: JUnit 5 (Jupiter) with AssertJ.
Test framework: JUnit 6 (Jupiter) with AssertJ.

## Key Dependencies

| Dependency | Version | License |
|---|---|---|
| Flexmark-java | 0.64.x | BSD 2-Clause |
| commonmark-java | 0.30.x | BSD 2-Clause |
| commonmark-ext-gfm-tables | 0.30.x | BSD 2-Clause |
| commonmark-ext-yaml-front-matter | 0.30.x | BSD 2-Clause |
| Jackson YAML | 2.x | Apache 2.0 |
| Kotlin | 2.x | Apache 2.0 |
| Gradle Plugin API | 8.x | Apache 2.0 |
| Gradle Plugin API | 9.x | Apache 2.0 |

## License

Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
[versions]
assertj = "3.27.7"
flexmark = "0.64.8"
commonmark = "0.30.0"
git-versioning = "6.4.4"
jacksonYaml = "2.22.2"
jackson-yaml = "2.22.2"
junit5 = "6.1.3"
kotlin = "2.4.10"
kover = "0.9.9"
ktlint-gradle = "14.2.0"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonYaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonYaml" }
commonmark = { module = "org.commonmark:commonmark", 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" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson-yaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-yaml" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }

[plugins]
git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versioning" }
Expand Down
4 changes: 3 additions & 1 deletion library/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,9 @@ plugins {
}

dependencies {
implementation(libs.flexmark.all)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.jackson.dataformat.yaml)
implementation(libs.jackson.module.kotlin)

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,41 +5,50 @@ Parse, transform, and emit Markdown (or HTML) with a composable, AST-based API.

## Overview

markflow is built on [Flexmark-java](https://github.com/vsch/flexmark-java) and provides:
markflow is built on [commonmark-java](https://github.com/commonmark/commonmark-java) and provides:

- A clean Kotlin API for parsing Markdown into a mutable AST
- Composable transformation passes (heading manipulation, front matter promotion, etc.)
- Round-trip Markdown output via Flexmark's `Formatter`, with reliable front matter handling
- Round-trip Markdown output via commonmark-java's `MarkdownRenderer`, with reliable front matter handling
- A Gradle plugin exposing common transformations as cacheable, incremental tasks

It is **not** a static site generator. It is a document-processing library and build-tool
integration layer — the Markdown equivalent of what a CSS post-processor does for stylesheets.

## Design Decisions

### Parser: Flexmark-java
### Parser: commonmark-java

markflow uses [Flexmark-java](https://github.com/vsch/flexmark-java) as its parser and formatter.
Flexmark was chosen because:
markflow uses [commonmark-java](https://github.com/commonmark/commonmark-java) as its parser and
formatter. commonmark-java was chosen because:

- It has the most mature round-trip Markdown output (`Formatter`) of any JVM library
- Its extension API is clean and well-suited to custom AST node types
- It supports GFM tables, YAML front matter, and other common extensions out of the box
- It is Apache 2.0 licensed, compatible with all likely dependencies
- It is actively maintained (Atlassian origin, used by OpenJDK, Google, Gerrit)
- It is spec-compliant with [CommonMark](https://spec.commonmark.org/)
- It provides round-trip Markdown output via `MarkdownRenderer`
- It supports GFM tables, YAML front matter (including raw/opaque capture), strikethrough, footnotes,
and other common extensions out of the box
- It is BSD 2-Clause licensed, compatible with all likely dependencies

[JetBrains Markdown](https://github.com/JetBrains/markdown) was considered but ruled out: its AST
is largely immutable, it has no Markdown output support, and it has no front matter extension.
It remains the only JVM option for Kotlin Multiplatform targets, but KMP is not a current requirement.
**Alternatives considered:**

### Front Matter: Opaque Text Node
[Flexmark-java](https://github.com/vsch/flexmark-java) was the original choice and has a richer
extension ecosystem (TOC, definition lists, typographic quotes, etc.), but has been effectively
unmaintained since 2023 with 178+ open issues. The maintenance risk outweighs the extension breadth
for a library intended for long-term use.

Flexmark's built-in `YamlFrontMatterExtension` parses front matter into key/value pairs but does
not reliably round-trip it through the `Formatter`. markflow instead provides a custom extension
that captures the entire front matter block as a single opaque text node in the AST. This means:
[JetBrains Markdown](https://github.com/JetBrains/markdown) is actively developed but ruled out:
its AST is largely immutable, it has no Markdown output support, and it targets Kotlin Multiplatform
which is not a current requirement.

- The `Formatter` preserves front matter verbatim by default
### Front Matter: Opaque Capture via RawContentParser

commonmark-java's `YamlFrontMatterExtension` supports a `RawContentParser` mode that captures the
entire front matter block as a single raw string rather than parsing it into key/value pairs. This
means:

- The `MarkdownRenderer` preserves front matter verbatim by default
- Callers that need to inspect or modify front matter values do so via Jackson YAML, extracting
the raw text from the node, modifying it, and writing it back
the raw text, modifying it, and writing it back
- No front matter content is silently dropped or reordered

### Transformation Pipeline
Expand All@@ -59,18 +68,18 @@ markflow/

### `library`

The standalone Kotlin library JAR. Depends only on Flexmark-java and Jackson YAML. No build-tool
The standalone Kotlin library JAR. Depends only on commonmark-java and Jackson YAML. No build-tool
APIs. Usable from any JVM context: Gradle, Maven, a CLI, application code.

Key types:

| Type | Description |
|---|---|
| `MarkdownDocument` | A parsed document: mutable Flexmark `Document` + raw front matter string |
| `MarkdownDocument` | A parsed document: mutable commonmark-java `Document` + raw front matter string |
| `MarkdownParser` | Parses a `String` or `File` into a `MarkdownDocument` |
| `MarkdownTransformer` | Single-responsibility AST transformation pass |
| `MarkdownPipeline` | Ordered composition of `MarkdownTransformer` instances |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via Flexmark `Formatter`) or HTML (via `HtmlRenderer`) |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via `MarkdownRenderer`) or HTML (via `HtmlRenderer`) |
| `FrontMatter` | Typed view of the front matter block via Jackson YAML |

Built-in transformers:
Expand All@@ -82,7 +91,7 @@ Built-in transformers:
| `TableFormatter` | Pads GFM table columns to consistent widths |
| `SentencePerLineFormatter` | Splits paragraph text at sentence boundaries, one sentence per line |
| `TemplateSubstitutor` | Replaces `{{key}}` placeholders with provided values, skipping fenced code blocks |
| `ScreenshotPlaceholderExpander` | Expands `<!-- screenshot: name.png | alt -->` to `![alt](path/name.png)` |
| `HtmlCommentExpander` | Expands structured HTML comments to Markdown content via registered handlers |

### `gradle-plugin`

Expand DownExpand Up@@ -111,7 +120,7 @@ Sub-packages follow feature boundaries:
|---|---|
| `net.oxspring.markflow` | Public API: `MarkdownDocument`, `MarkdownParser`, `MarkdownPipeline`, `MarkdownFormatter` |
| `net.oxspring.markflow.transform` | Built-in `MarkdownTransformer` implementations |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, opaque front matter AST extension |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, front matter access utilities |
| `net.oxspring.markflow.gradle` | Gradle plugin and task implementations |

## MVP Scope (v1.0)
Expand All@@ -129,7 +138,7 @@ The v1.0 MVP targets feature parity with the custom Gradle tasks in the
markflow replaces this with a `ProcessMarkdownTask` configured with:

- `TemplateSubstitutor` (token substitution, fence-aware)
- `ScreenshotPlaceholderExpander`
- `HtmlCommentExpander` with a screenshot handler

### Replaces front matter title extraction in `BuildPdfTask`

Expand DownExpand Up@@ -158,16 +167,18 @@ markflow replaces this with a `ProcessMarkdownTask` configured with:
./gradlew build # assemble + check
```

Test framework: JUnit 5 (Jupiter) with AssertJ.
Test framework: JUnit 6 (Jupiter) with AssertJ.

## Key Dependencies

| Dependency | Version | License |
|---|---|---|
| Flexmark-java | 0.64.x | BSD 2-Clause |
| commonmark-java | 0.30.x | BSD 2-Clause |
| commonmark-ext-gfm-tables | 0.30.x | BSD 2-Clause |
| commonmark-ext-yaml-front-matter | 0.30.x | BSD 2-Clause |
| Jackson YAML | 2.x | Apache 2.0 |
| Kotlin | 2.x | Apache 2.0 |
| Gradle Plugin API | 8.x | Apache 2.0 |
| Gradle Plugin API | 9.x | Apache 2.0 |

## License

Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
[versions]
assertj = "3.27.7"
flexmark = "0.64.8"
commonmark = "0.30.0"
git-versioning = "6.4.4"
jacksonYaml = "2.22.2"
jackson-yaml = "2.22.2"
junit5 = "6.1.3"
kotlin = "2.4.10"
kover = "0.9.9"
ktlint-gradle = "14.2.0"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonYaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonYaml" }
commonmark = { module = "org.commonmark:commonmark", 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" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson-yaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-yaml" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }

[plugins]
git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versioning" }
Expand Down
4 changes: 3 additions & 1 deletion library/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,9 @@ plugins {
}

dependencies {
implementation(libs.flexmark.all)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.jackson.dataformat.yaml)
implementation(libs.jackson.module.kotlin)

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,41 +5,50 @@ Parse, transform, and emit Markdown (or HTML) with a composable, AST-based API.

## Overview

markflow is built on [Flexmark-java](https://github.com/vsch/flexmark-java) and provides:
markflow is built on [commonmark-java](https://github.com/commonmark/commonmark-java) and provides:

- A clean Kotlin API for parsing Markdown into a mutable AST
- Composable transformation passes (heading manipulation, front matter promotion, etc.)
- Round-trip Markdown output via Flexmark's `Formatter`, with reliable front matter handling
- Round-trip Markdown output via commonmark-java's `MarkdownRenderer`, with reliable front matter handling
- A Gradle plugin exposing common transformations as cacheable, incremental tasks

It is **not** a static site generator. It is a document-processing library and build-tool
integration layer — the Markdown equivalent of what a CSS post-processor does for stylesheets.

## Design Decisions

### Parser: Flexmark-java
### Parser: commonmark-java

markflow uses [Flexmark-java](https://github.com/vsch/flexmark-java) as its parser and formatter.
Flexmark was chosen because:
markflow uses [commonmark-java](https://github.com/commonmark/commonmark-java) as its parser and
formatter. commonmark-java was chosen because:

- It has the most mature round-trip Markdown output (`Formatter`) of any JVM library
- Its extension API is clean and well-suited to custom AST node types
- It supports GFM tables, YAML front matter, and other common extensions out of the box
- It is Apache 2.0 licensed, compatible with all likely dependencies
- It is actively maintained (Atlassian origin, used by OpenJDK, Google, Gerrit)
- It is spec-compliant with [CommonMark](https://spec.commonmark.org/)
- It provides round-trip Markdown output via `MarkdownRenderer`
- It supports GFM tables, YAML front matter (including raw/opaque capture), strikethrough, footnotes,
and other common extensions out of the box
- It is BSD 2-Clause licensed, compatible with all likely dependencies

[JetBrains Markdown](https://github.com/JetBrains/markdown) was considered but ruled out: its AST
is largely immutable, it has no Markdown output support, and it has no front matter extension.
It remains the only JVM option for Kotlin Multiplatform targets, but KMP is not a current requirement.
**Alternatives considered:**

### Front Matter: Opaque Text Node
[Flexmark-java](https://github.com/vsch/flexmark-java) was the original choice and has a richer
extension ecosystem (TOC, definition lists, typographic quotes, etc.), but has been effectively
unmaintained since 2023 with 178+ open issues. The maintenance risk outweighs the extension breadth
for a library intended for long-term use.

Flexmark's built-in `YamlFrontMatterExtension` parses front matter into key/value pairs but does
not reliably round-trip it through the `Formatter`. markflow instead provides a custom extension
that captures the entire front matter block as a single opaque text node in the AST. This means:
[JetBrains Markdown](https://github.com/JetBrains/markdown) is actively developed but ruled out:
its AST is largely immutable, it has no Markdown output support, and it targets Kotlin Multiplatform
which is not a current requirement.

- The `Formatter` preserves front matter verbatim by default
### Front Matter: Opaque Capture via RawContentParser

commonmark-java's `YamlFrontMatterExtension` supports a `RawContentParser` mode that captures the
entire front matter block as a single raw string rather than parsing it into key/value pairs. This
means:

- The `MarkdownRenderer` preserves front matter verbatim by default
- Callers that need to inspect or modify front matter values do so via Jackson YAML, extracting
the raw text from the node, modifying it, and writing it back
the raw text, modifying it, and writing it back
- No front matter content is silently dropped or reordered

### Transformation Pipeline
Expand All@@ -59,18 +68,18 @@ markflow/

### `library`

The standalone Kotlin library JAR. Depends only on Flexmark-java and Jackson YAML. No build-tool
The standalone Kotlin library JAR. Depends only on commonmark-java and Jackson YAML. No build-tool
APIs. Usable from any JVM context: Gradle, Maven, a CLI, application code.

Key types:

| Type | Description |
|---|---|
| `MarkdownDocument` | A parsed document: mutable Flexmark `Document` + raw front matter string |
| `MarkdownDocument` | A parsed document: mutable commonmark-java `Document` + raw front matter string |
| `MarkdownParser` | Parses a `String` or `File` into a `MarkdownDocument` |
| `MarkdownTransformer` | Single-responsibility AST transformation pass |
| `MarkdownPipeline` | Ordered composition of `MarkdownTransformer` instances |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via Flexmark `Formatter`) or HTML (via `HtmlRenderer`) |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via `MarkdownRenderer`) or HTML (via `HtmlRenderer`) |
| `FrontMatter` | Typed view of the front matter block via Jackson YAML |

Built-in transformers:
Expand All@@ -82,7 +91,7 @@ Built-in transformers:
| `TableFormatter` | Pads GFM table columns to consistent widths |
| `SentencePerLineFormatter` | Splits paragraph text at sentence boundaries, one sentence per line |
| `TemplateSubstitutor` | Replaces `{{key}}` placeholders with provided values, skipping fenced code blocks |
| `ScreenshotPlaceholderExpander` | Expands `<!-- screenshot: name.png | alt -->` to `![alt](path/name.png)` |
| `HtmlCommentExpander` | Expands structured HTML comments to Markdown content via registered handlers |

### `gradle-plugin`

Expand DownExpand Up@@ -111,7 +120,7 @@ Sub-packages follow feature boundaries:
|---|---|
| `net.oxspring.markflow` | Public API: `MarkdownDocument`, `MarkdownParser`, `MarkdownPipeline`, `MarkdownFormatter` |
| `net.oxspring.markflow.transform` | Built-in `MarkdownTransformer` implementations |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, opaque front matter AST extension |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, front matter access utilities |
| `net.oxspring.markflow.gradle` | Gradle plugin and task implementations |

## MVP Scope (v1.0)
Expand All@@ -129,7 +138,7 @@ The v1.0 MVP targets feature parity with the custom Gradle tasks in the
markflow replaces this with a `ProcessMarkdownTask` configured with:

- `TemplateSubstitutor` (token substitution, fence-aware)
- `ScreenshotPlaceholderExpander`
- `HtmlCommentExpander` with a screenshot handler

### Replaces front matter title extraction in `BuildPdfTask`

Expand DownExpand Up@@ -158,16 +167,18 @@ markflow replaces this with a `ProcessMarkdownTask` configured with:
./gradlew build # assemble + check
```

Test framework: JUnit 5 (Jupiter) with AssertJ.
Test framework: JUnit 6 (Jupiter) with AssertJ.

## Key Dependencies

| Dependency | Version | License |
|---|---|---|
| Flexmark-java | 0.64.x | BSD 2-Clause |
| commonmark-java | 0.30.x | BSD 2-Clause |
| commonmark-ext-gfm-tables | 0.30.x | BSD 2-Clause |
| commonmark-ext-yaml-front-matter | 0.30.x | BSD 2-Clause |
| Jackson YAML | 2.x | Apache 2.0 |
| Kotlin | 2.x | Apache 2.0 |
| Gradle Plugin API | 8.x | Apache 2.0 |
| Gradle Plugin API | 9.x | Apache 2.0 |

## License

Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
[versions]
assertj = "3.27.7"
flexmark = "0.64.8"
commonmark = "0.30.0"
git-versioning = "6.4.4"
jacksonYaml = "2.22.2"
jackson-yaml = "2.22.2"
junit5 = "6.1.3"
kotlin = "2.4.10"
kover = "0.9.9"
ktlint-gradle = "14.2.0"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonYaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonYaml" }
commonmark = { module = "org.commonmark:commonmark", 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" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson-yaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-yaml" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }

[plugins]
git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versioning" }
Expand Down
4 changes: 3 additions & 1 deletion library/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,9 @@ plugins {
}

dependencies {
implementation(libs.flexmark.all)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.jackson.dataformat.yaml)
implementation(libs.jackson.module.kotlin)

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,41 +5,50 @@ Parse, transform, and emit Markdown (or HTML) with a composable, AST-based API.

## Overview

markflow is built on [Flexmark-java](https://github.com/vsch/flexmark-java) and provides:
markflow is built on [commonmark-java](https://github.com/commonmark/commonmark-java) and provides:

- A clean Kotlin API for parsing Markdown into a mutable AST
- Composable transformation passes (heading manipulation, front matter promotion, etc.)
- Round-trip Markdown output via Flexmark's `Formatter`, with reliable front matter handling
- Round-trip Markdown output via commonmark-java's `MarkdownRenderer`, with reliable front matter handling
- A Gradle plugin exposing common transformations as cacheable, incremental tasks

It is **not** a static site generator. It is a document-processing library and build-tool
integration layer — the Markdown equivalent of what a CSS post-processor does for stylesheets.

## Design Decisions

### Parser: Flexmark-java
### Parser: commonmark-java

markflow uses [Flexmark-java](https://github.com/vsch/flexmark-java) as its parser and formatter.
Flexmark was chosen because:
markflow uses [commonmark-java](https://github.com/commonmark/commonmark-java) as its parser and
formatter. commonmark-java was chosen because:

- It has the most mature round-trip Markdown output (`Formatter`) of any JVM library
- Its extension API is clean and well-suited to custom AST node types
- It supports GFM tables, YAML front matter, and other common extensions out of the box
- It is Apache 2.0 licensed, compatible with all likely dependencies
- It is actively maintained (Atlassian origin, used by OpenJDK, Google, Gerrit)
- It is spec-compliant with [CommonMark](https://spec.commonmark.org/)
- It provides round-trip Markdown output via `MarkdownRenderer`
- It supports GFM tables, YAML front matter (including raw/opaque capture), strikethrough, footnotes,
and other common extensions out of the box
- It is BSD 2-Clause licensed, compatible with all likely dependencies

[JetBrains Markdown](https://github.com/JetBrains/markdown) was considered but ruled out: its AST
is largely immutable, it has no Markdown output support, and it has no front matter extension.
It remains the only JVM option for Kotlin Multiplatform targets, but KMP is not a current requirement.
**Alternatives considered:**

### Front Matter: Opaque Text Node
[Flexmark-java](https://github.com/vsch/flexmark-java) was the original choice and has a richer
extension ecosystem (TOC, definition lists, typographic quotes, etc.), but has been effectively
unmaintained since 2023 with 178+ open issues. The maintenance risk outweighs the extension breadth
for a library intended for long-term use.

Flexmark's built-in `YamlFrontMatterExtension` parses front matter into key/value pairs but does
not reliably round-trip it through the `Formatter`. markflow instead provides a custom extension
that captures the entire front matter block as a single opaque text node in the AST. This means:
[JetBrains Markdown](https://github.com/JetBrains/markdown) is actively developed but ruled out:
its AST is largely immutable, it has no Markdown output support, and it targets Kotlin Multiplatform
which is not a current requirement.

- The `Formatter` preserves front matter verbatim by default
### Front Matter: Opaque Capture via RawContentParser

commonmark-java's `YamlFrontMatterExtension` supports a `RawContentParser` mode that captures the
entire front matter block as a single raw string rather than parsing it into key/value pairs. This
means:

- The `MarkdownRenderer` preserves front matter verbatim by default
- Callers that need to inspect or modify front matter values do so via Jackson YAML, extracting
the raw text from the node, modifying it, and writing it back
the raw text, modifying it, and writing it back
- No front matter content is silently dropped or reordered

### Transformation Pipeline
Expand All@@ -59,18 +68,18 @@ markflow/

### `library`

The standalone Kotlin library JAR. Depends only on Flexmark-java and Jackson YAML. No build-tool
The standalone Kotlin library JAR. Depends only on commonmark-java and Jackson YAML. No build-tool
APIs. Usable from any JVM context: Gradle, Maven, a CLI, application code.

Key types:

| Type | Description |
|---|---|
| `MarkdownDocument` | A parsed document: mutable Flexmark `Document` + raw front matter string |
| `MarkdownDocument` | A parsed document: mutable commonmark-java `Document` + raw front matter string |
| `MarkdownParser` | Parses a `String` or `File` into a `MarkdownDocument` |
| `MarkdownTransformer` | Single-responsibility AST transformation pass |
| `MarkdownPipeline` | Ordered composition of `MarkdownTransformer` instances |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via Flexmark `Formatter`) or HTML (via `HtmlRenderer`) |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via `MarkdownRenderer`) or HTML (via `HtmlRenderer`) |
| `FrontMatter` | Typed view of the front matter block via Jackson YAML |

Built-in transformers:
Expand All@@ -82,7 +91,7 @@ Built-in transformers:
| `TableFormatter` | Pads GFM table columns to consistent widths |
| `SentencePerLineFormatter` | Splits paragraph text at sentence boundaries, one sentence per line |
| `TemplateSubstitutor` | Replaces `{{key}}` placeholders with provided values, skipping fenced code blocks |
| `ScreenshotPlaceholderExpander` | Expands `<!-- screenshot: name.png | alt -->` to `![alt](path/name.png)` |
| `HtmlCommentExpander` | Expands structured HTML comments to Markdown content via registered handlers |

### `gradle-plugin`

Expand DownExpand Up@@ -111,7 +120,7 @@ Sub-packages follow feature boundaries:
|---|---|
| `net.oxspring.markflow` | Public API: `MarkdownDocument`, `MarkdownParser`, `MarkdownPipeline`, `MarkdownFormatter` |
| `net.oxspring.markflow.transform` | Built-in `MarkdownTransformer` implementations |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, opaque front matter AST extension |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, front matter access utilities |
| `net.oxspring.markflow.gradle` | Gradle plugin and task implementations |

## MVP Scope (v1.0)
Expand All@@ -129,7 +138,7 @@ The v1.0 MVP targets feature parity with the custom Gradle tasks in the
markflow replaces this with a `ProcessMarkdownTask` configured with:

- `TemplateSubstitutor` (token substitution, fence-aware)
- `ScreenshotPlaceholderExpander`
- `HtmlCommentExpander` with a screenshot handler

### Replaces front matter title extraction in `BuildPdfTask`

Expand DownExpand Up@@ -158,16 +167,18 @@ markflow replaces this with a `ProcessMarkdownTask` configured with:
./gradlew build # assemble + check
```

Test framework: JUnit 5 (Jupiter) with AssertJ.
Test framework: JUnit 6 (Jupiter) with AssertJ.

## Key Dependencies

| Dependency | Version | License |
|---|---|---|
| Flexmark-java | 0.64.x | BSD 2-Clause |
| commonmark-java | 0.30.x | BSD 2-Clause |
| commonmark-ext-gfm-tables | 0.30.x | BSD 2-Clause |
| commonmark-ext-yaml-front-matter | 0.30.x | BSD 2-Clause |
| Jackson YAML | 2.x | Apache 2.0 |
| Kotlin | 2.x | Apache 2.0 |
| Gradle Plugin API | 8.x | Apache 2.0 |
| Gradle Plugin API | 9.x | Apache 2.0 |

## License

Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
[versions]
assertj = "3.27.7"
flexmark = "0.64.8"
commonmark = "0.30.0"
git-versioning = "6.4.4"
jacksonYaml = "2.22.2"
jackson-yaml = "2.22.2"
junit5 = "6.1.3"
kotlin = "2.4.10"
kover = "0.9.9"
ktlint-gradle = "14.2.0"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonYaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonYaml" }
commonmark = { module = "org.commonmark:commonmark", 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" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson-yaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-yaml" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }

[plugins]
git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versioning" }
Expand Down
4 changes: 3 additions & 1 deletion library/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,9 @@ plugins {
}

dependencies {
implementation(libs.flexmark.all)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.jackson.dataformat.yaml)
implementation(libs.jackson.module.kotlin)

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,41 +5,50 @@ Parse, transform, and emit Markdown (or HTML) with a composable, AST-based API.

## Overview

markflow is built on [Flexmark-java](https://github.com/vsch/flexmark-java) and provides:
markflow is built on [commonmark-java](https://github.com/commonmark/commonmark-java) and provides:

- A clean Kotlin API for parsing Markdown into a mutable AST
- Composable transformation passes (heading manipulation, front matter promotion, etc.)
- Round-trip Markdown output via Flexmark's `Formatter`, with reliable front matter handling
- Round-trip Markdown output via commonmark-java's `MarkdownRenderer`, with reliable front matter handling
- A Gradle plugin exposing common transformations as cacheable, incremental tasks

It is **not** a static site generator. It is a document-processing library and build-tool
integration layer — the Markdown equivalent of what a CSS post-processor does for stylesheets.

## Design Decisions

### Parser: Flexmark-java
### Parser: commonmark-java

markflow uses [Flexmark-java](https://github.com/vsch/flexmark-java) as its parser and formatter.
Flexmark was chosen because:
markflow uses [commonmark-java](https://github.com/commonmark/commonmark-java) as its parser and
formatter. commonmark-java was chosen because:

- It has the most mature round-trip Markdown output (`Formatter`) of any JVM library
- Its extension API is clean and well-suited to custom AST node types
- It supports GFM tables, YAML front matter, and other common extensions out of the box
- It is Apache 2.0 licensed, compatible with all likely dependencies
- It is actively maintained (Atlassian origin, used by OpenJDK, Google, Gerrit)
- It is spec-compliant with [CommonMark](https://spec.commonmark.org/)
- It provides round-trip Markdown output via `MarkdownRenderer`
- It supports GFM tables, YAML front matter (including raw/opaque capture), strikethrough, footnotes,
and other common extensions out of the box
- It is BSD 2-Clause licensed, compatible with all likely dependencies

[JetBrains Markdown](https://github.com/JetBrains/markdown) was considered but ruled out: its AST
is largely immutable, it has no Markdown output support, and it has no front matter extension.
It remains the only JVM option for Kotlin Multiplatform targets, but KMP is not a current requirement.
**Alternatives considered:**

### Front Matter: Opaque Text Node
[Flexmark-java](https://github.com/vsch/flexmark-java) was the original choice and has a richer
extension ecosystem (TOC, definition lists, typographic quotes, etc.), but has been effectively
unmaintained since 2023 with 178+ open issues. The maintenance risk outweighs the extension breadth
for a library intended for long-term use.

Flexmark's built-in `YamlFrontMatterExtension` parses front matter into key/value pairs but does
not reliably round-trip it through the `Formatter`. markflow instead provides a custom extension
that captures the entire front matter block as a single opaque text node in the AST. This means:
[JetBrains Markdown](https://github.com/JetBrains/markdown) is actively developed but ruled out:
its AST is largely immutable, it has no Markdown output support, and it targets Kotlin Multiplatform
which is not a current requirement.

- The `Formatter` preserves front matter verbatim by default
### Front Matter: Opaque Capture via RawContentParser

commonmark-java's `YamlFrontMatterExtension` supports a `RawContentParser` mode that captures the
entire front matter block as a single raw string rather than parsing it into key/value pairs. This
means:

- The `MarkdownRenderer` preserves front matter verbatim by default
- Callers that need to inspect or modify front matter values do so via Jackson YAML, extracting
the raw text from the node, modifying it, and writing it back
the raw text, modifying it, and writing it back
- No front matter content is silently dropped or reordered

### Transformation Pipeline
Expand All@@ -59,18 +68,18 @@ markflow/

### `library`

The standalone Kotlin library JAR. Depends only on Flexmark-java and Jackson YAML. No build-tool
The standalone Kotlin library JAR. Depends only on commonmark-java and Jackson YAML. No build-tool
APIs. Usable from any JVM context: Gradle, Maven, a CLI, application code.

Key types:

| Type | Description |
|---|---|
| `MarkdownDocument` | A parsed document: mutable Flexmark `Document` + raw front matter string |
| `MarkdownDocument` | A parsed document: mutable commonmark-java `Document` + raw front matter string |
| `MarkdownParser` | Parses a `String` or `File` into a `MarkdownDocument` |
| `MarkdownTransformer` | Single-responsibility AST transformation pass |
| `MarkdownPipeline` | Ordered composition of `MarkdownTransformer` instances |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via Flexmark `Formatter`) or HTML (via `HtmlRenderer`) |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via `MarkdownRenderer`) or HTML (via `HtmlRenderer`) |
| `FrontMatter` | Typed view of the front matter block via Jackson YAML |

Built-in transformers:
Expand All@@ -82,7 +91,7 @@ Built-in transformers:
| `TableFormatter` | Pads GFM table columns to consistent widths |
| `SentencePerLineFormatter` | Splits paragraph text at sentence boundaries, one sentence per line |
| `TemplateSubstitutor` | Replaces `{{key}}` placeholders with provided values, skipping fenced code blocks |
| `ScreenshotPlaceholderExpander` | Expands `<!-- screenshot: name.png | alt -->` to `![alt](path/name.png)` |
| `HtmlCommentExpander` | Expands structured HTML comments to Markdown content via registered handlers |

### `gradle-plugin`

Expand DownExpand Up@@ -111,7 +120,7 @@ Sub-packages follow feature boundaries:
|---|---|
| `net.oxspring.markflow` | Public API: `MarkdownDocument`, `MarkdownParser`, `MarkdownPipeline`, `MarkdownFormatter` |
| `net.oxspring.markflow.transform` | Built-in `MarkdownTransformer` implementations |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, opaque front matter AST extension |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, front matter access utilities |
| `net.oxspring.markflow.gradle` | Gradle plugin and task implementations |

## MVP Scope (v1.0)
Expand All@@ -129,7 +138,7 @@ The v1.0 MVP targets feature parity with the custom Gradle tasks in the
markflow replaces this with a `ProcessMarkdownTask` configured with:

- `TemplateSubstitutor` (token substitution, fence-aware)
- `ScreenshotPlaceholderExpander`
- `HtmlCommentExpander` with a screenshot handler

### Replaces front matter title extraction in `BuildPdfTask`

Expand DownExpand Up@@ -158,16 +167,18 @@ markflow replaces this with a `ProcessMarkdownTask` configured with:
./gradlew build # assemble + check
```

Test framework: JUnit 5 (Jupiter) with AssertJ.
Test framework: JUnit 6 (Jupiter) with AssertJ.

## Key Dependencies

| Dependency | Version | License |
|---|---|---|
| Flexmark-java | 0.64.x | BSD 2-Clause |
| commonmark-java | 0.30.x | BSD 2-Clause |
| commonmark-ext-gfm-tables | 0.30.x | BSD 2-Clause |
| commonmark-ext-yaml-front-matter | 0.30.x | BSD 2-Clause |
| Jackson YAML | 2.x | Apache 2.0 |
| Kotlin | 2.x | Apache 2.0 |
| Gradle Plugin API | 8.x | Apache 2.0 |
| Gradle Plugin API | 9.x | Apache 2.0 |

## License

Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
[versions]
assertj = "3.27.7"
flexmark = "0.64.8"
commonmark = "0.30.0"
git-versioning = "6.4.4"
jacksonYaml = "2.22.2"
jackson-yaml = "2.22.2"
junit5 = "6.1.3"
kotlin = "2.4.10"
kover = "0.9.9"
ktlint-gradle = "14.2.0"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonYaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonYaml" }
commonmark = { module = "org.commonmark:commonmark", 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" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson-yaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-yaml" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }

[plugins]
git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versioning" }
Expand Down
4 changes: 3 additions & 1 deletion library/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,9 @@ plugins {
}

dependencies {
implementation(libs.flexmark.all)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.jackson.dataformat.yaml)
implementation(libs.jackson.module.kotlin)

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,41 +5,50 @@ Parse, transform, and emit Markdown (or HTML) with a composable, AST-based API.

## Overview

markflow is built on [Flexmark-java](https://github.com/vsch/flexmark-java) and provides:
markflow is built on [commonmark-java](https://github.com/commonmark/commonmark-java) and provides:

- A clean Kotlin API for parsing Markdown into a mutable AST
- Composable transformation passes (heading manipulation, front matter promotion, etc.)
- Round-trip Markdown output via Flexmark's `Formatter`, with reliable front matter handling
- Round-trip Markdown output via commonmark-java's `MarkdownRenderer`, with reliable front matter handling
- A Gradle plugin exposing common transformations as cacheable, incremental tasks

It is **not** a static site generator. It is a document-processing library and build-tool
integration layer — the Markdown equivalent of what a CSS post-processor does for stylesheets.

## Design Decisions

### Parser: Flexmark-java
### Parser: commonmark-java

markflow uses [Flexmark-java](https://github.com/vsch/flexmark-java) as its parser and formatter.
Flexmark was chosen because:
markflow uses [commonmark-java](https://github.com/commonmark/commonmark-java) as its parser and
formatter. commonmark-java was chosen because:

- It has the most mature round-trip Markdown output (`Formatter`) of any JVM library
- Its extension API is clean and well-suited to custom AST node types
- It supports GFM tables, YAML front matter, and other common extensions out of the box
- It is Apache 2.0 licensed, compatible with all likely dependencies
- It is actively maintained (Atlassian origin, used by OpenJDK, Google, Gerrit)
- It is spec-compliant with [CommonMark](https://spec.commonmark.org/)
- It provides round-trip Markdown output via `MarkdownRenderer`
- It supports GFM tables, YAML front matter (including raw/opaque capture), strikethrough, footnotes,
and other common extensions out of the box
- It is BSD 2-Clause licensed, compatible with all likely dependencies

[JetBrains Markdown](https://github.com/JetBrains/markdown) was considered but ruled out: its AST
is largely immutable, it has no Markdown output support, and it has no front matter extension.
It remains the only JVM option for Kotlin Multiplatform targets, but KMP is not a current requirement.
**Alternatives considered:**

### Front Matter: Opaque Text Node
[Flexmark-java](https://github.com/vsch/flexmark-java) was the original choice and has a richer
extension ecosystem (TOC, definition lists, typographic quotes, etc.), but has been effectively
unmaintained since 2023 with 178+ open issues. The maintenance risk outweighs the extension breadth
for a library intended for long-term use.

Flexmark's built-in `YamlFrontMatterExtension` parses front matter into key/value pairs but does
not reliably round-trip it through the `Formatter`. markflow instead provides a custom extension
that captures the entire front matter block as a single opaque text node in the AST. This means:
[JetBrains Markdown](https://github.com/JetBrains/markdown) is actively developed but ruled out:
its AST is largely immutable, it has no Markdown output support, and it targets Kotlin Multiplatform
which is not a current requirement.

- The `Formatter` preserves front matter verbatim by default
### Front Matter: Opaque Capture via RawContentParser

commonmark-java's `YamlFrontMatterExtension` supports a `RawContentParser` mode that captures the
entire front matter block as a single raw string rather than parsing it into key/value pairs. This
means:

- The `MarkdownRenderer` preserves front matter verbatim by default
- Callers that need to inspect or modify front matter values do so via Jackson YAML, extracting
the raw text from the node, modifying it, and writing it back
the raw text, modifying it, and writing it back
- No front matter content is silently dropped or reordered

### Transformation Pipeline
Expand All@@ -59,18 +68,18 @@ markflow/

### `library`

The standalone Kotlin library JAR. Depends only on Flexmark-java and Jackson YAML. No build-tool
The standalone Kotlin library JAR. Depends only on commonmark-java and Jackson YAML. No build-tool
APIs. Usable from any JVM context: Gradle, Maven, a CLI, application code.

Key types:

| Type | Description |
|---|---|
| `MarkdownDocument` | A parsed document: mutable Flexmark `Document` + raw front matter string |
| `MarkdownDocument` | A parsed document: mutable commonmark-java `Document` + raw front matter string |
| `MarkdownParser` | Parses a `String` or `File` into a `MarkdownDocument` |
| `MarkdownTransformer` | Single-responsibility AST transformation pass |
| `MarkdownPipeline` | Ordered composition of `MarkdownTransformer` instances |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via Flexmark `Formatter`) or HTML (via `HtmlRenderer`) |
| `MarkdownFormatter` | Emits a `MarkdownDocument` as Markdown (via `MarkdownRenderer`) or HTML (via `HtmlRenderer`) |
| `FrontMatter` | Typed view of the front matter block via Jackson YAML |

Built-in transformers:
Expand All@@ -82,7 +91,7 @@ Built-in transformers:
| `TableFormatter` | Pads GFM table columns to consistent widths |
| `SentencePerLineFormatter` | Splits paragraph text at sentence boundaries, one sentence per line |
| `TemplateSubstitutor` | Replaces `{{key}}` placeholders with provided values, skipping fenced code blocks |
| `ScreenshotPlaceholderExpander` | Expands `<!-- screenshot: name.png | alt -->` to `![alt](path/name.png)` |
| `HtmlCommentExpander` | Expands structured HTML comments to Markdown content via registered handlers |

### `gradle-plugin`

Expand DownExpand Up@@ -111,7 +120,7 @@ Sub-packages follow feature boundaries:
|---|---|
| `net.oxspring.markflow` | Public API: `MarkdownDocument`, `MarkdownParser`, `MarkdownPipeline`, `MarkdownFormatter` |
| `net.oxspring.markflow.transform` | Built-in `MarkdownTransformer` implementations |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, opaque front matter AST extension |
| `net.oxspring.markflow.frontmatter` | `FrontMatter`, front matter access utilities |
| `net.oxspring.markflow.gradle` | Gradle plugin and task implementations |

## MVP Scope (v1.0)
Expand All@@ -129,7 +138,7 @@ The v1.0 MVP targets feature parity with the custom Gradle tasks in the
markflow replaces this with a `ProcessMarkdownTask` configured with:

- `TemplateSubstitutor` (token substitution, fence-aware)
- `ScreenshotPlaceholderExpander`
- `HtmlCommentExpander` with a screenshot handler

### Replaces front matter title extraction in `BuildPdfTask`

Expand DownExpand Up@@ -158,16 +167,18 @@ markflow replaces this with a `ProcessMarkdownTask` configured with:
./gradlew build # assemble + check
```

Test framework: JUnit 5 (Jupiter) with AssertJ.
Test framework: JUnit 6 (Jupiter) with AssertJ.

## Key Dependencies

| Dependency | Version | License |
|---|---|---|
| Flexmark-java | 0.64.x | BSD 2-Clause |
| commonmark-java | 0.30.x | BSD 2-Clause |
| commonmark-ext-gfm-tables | 0.30.x | BSD 2-Clause |
| commonmark-ext-yaml-front-matter | 0.30.x | BSD 2-Clause |
| Jackson YAML | 2.x | Apache 2.0 |
| Kotlin | 2.x | Apache 2.0 |
| Gradle Plugin API | 8.x | Apache 2.0 |
| Gradle Plugin API | 9.x | Apache 2.0 |

## License

Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
[versions]
assertj = "3.27.7"
flexmark = "0.64.8"
commonmark = "0.30.0"
git-versioning = "6.4.4"
jacksonYaml = "2.22.2"
jackson-yaml = "2.22.2"
junit5 = "6.1.3"
kotlin = "2.4.10"
kover = "0.9.9"
ktlint-gradle = "14.2.0"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jacksonYaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonYaml" }
commonmark = { module = "org.commonmark:commonmark", 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" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson-yaml" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-yaml" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }

[plugins]
git-versioning = { id = "me.qoomon.git-versioning", version.ref = "git-versioning" }
Expand Down
4 changes: 3 additions & 1 deletion library/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,9 @@ plugins {
}

dependencies {
implementation(libs.flexmark.all)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.yaml.front.matter)
implementation(libs.jackson.dataformat.yaml)
implementation(libs.jackson.module.kotlin)

Expand Down