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
15 changes: 15 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,18 @@ jobs:
- name: Run tests
uses: gradle/actions/setup-gradle@v4
- run: ./gradlew :library:${{ matrix.target }}

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- uses: gradle/actions/setup-gradle@v4
- name: Formatting and static analysis
run: ./gradlew ktfmtCheck detekt
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "markitdown"]
path = markitdown
url = https://github.com/microsoft/markitdown
[submodule "anydoc"]
path = anydoc
url = https://github.com/firecrawl/anydoc.git
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ dependencies {
### JVM

```kotlin
import io.github.lemcoder.mikromarkdown.MarkItDown
import io.github.lemcoder.mikromarkdown.MikroMarkdown

val mid = MarkItDown()
val mid = MikroMarkdown()

// from file path
val result = mid.convert("/path/to/document.docx")
Expand All @@ -78,10 +78,10 @@ println(result.title) // nullable, extracted from document metadata
### Android

```kotlin
import io.github.lemcoder.mikromarkdown.MarkItDown
import io.github.lemcoder.mikromarkdown.MikroMarkdown

// pass Context to enable PDF support
val mid = MarkItDown(context)
val mid = MikroMarkdown(context)

val result = mid.convert(file.absolutePath)
```
Expand All @@ -101,7 +101,7 @@ class MyConverter : DocumentConverter {
}
}

val mid = MarkItDown()
val mid = MikroMarkdown()
mid.register(MyConverter()) // default priority 0.0
mid.register(FallbackConverter(), priority = 10.0) // higher = later
```
Expand All @@ -126,7 +126,22 @@ mid.register(HtmlConverter())
| `UnsupportedFormatException` | No registered converter accepted the input |
| `FileConversionException` | Converter threw during conversion |

Both extend `MarkItDownException`.
Both extend `MikroMarkdownException`.

## Code quality

| tool | task | what it guards |
|---|---|---|
| [ktfmt](https://github.com/facebook/ktfmt) | `./gradlew ktfmtFormat` / `ktfmtCheck` | formatting (kotlinlang style, 120 columns) |
| [detekt](https://detekt.dev) | `./gradlew detekt` | static analysis; overrides in `config/detekt/detekt.yml` |
| [Konsist](https://docs.konsist.lemonappdev.com) | `./gradlew :library:jvmTest --tests '*ArchitectureTest*'` | pipeline boundaries |

`./gradlew check` runs all three. The Konsist rules encode the architecture: the model depends on
nothing, converters never import the renderer, and Markdown syntax appears only under `render/` —
so a converter cannot quietly start building Markdown strings again.

ktfmt-gradle only derives tasks for the common and JVM source sets, so `library/build.gradle.kts`
registers matching tasks for the Android ones.

## Benchmark

Expand Down
1 change: 1 addition & 0 deletions anydoc
Submodule anydoc added at 4a45ad
32 changes: 32 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,36 @@ plugins {
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.ktfmt)
alias(libs.plugins.detekt)
}

allprojects {
apply(plugin = rootProject.libs.plugins.ktfmt.get().pluginId)
apply(plugin = rootProject.libs.plugins.detekt.get().pluginId)

ktfmt {
// Matches the existing sources: 4-space indent, no import reordering surprises.
kotlinLangStyle()
maxWidth = 120
}

detekt {
buildUponDefaultConfig = true
parallel = true
config.setFrom(rootProject.file("config/detekt/detekt.yml"))
basePath = rootProject.projectDir.absolutePath
// Generated sources and the vendored reference checkouts are not ours to lint.
source.setFrom(files("src").filter { it.exists() })
}

tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
jvmTarget = "21"
reports {
html.required = true
sarif.required = true
md.required = false
txt.required = false
}
}
}
14 changes: 3 additions & 11 deletions cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,13 @@ plugins {
application
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }

application {
mainClass = "com.mikromarkdown.cli.MainKt"
}
application { mainClass = "com.mikromarkdown.cli.MainKt" }

dependencies {
implementation(project(":library"))
implementation(libs.clikt)
}

tasks.test {
useJUnitPlatform()
}
tasks.test { useJUnitPlatform() }
33 changes: 1 addition & 32 deletions cli/src/main/kotlin/com/mikromarkdown/cli/Main.kt
Original file line number Diff line number Diff line change
@@ -1,43 +1,12 @@
package com.mikromarkdown.cli

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.path
import io.github.lemcoder.mikromarkdown.MarkItDown
import io.github.lemcoder.mikromarkdown.StreamInfo

class MarkItDownCommand : CliktCommand(name = "markitdown") {
private val file by argument("FILE", help = "Input file (reads stdin if omitted)").path(mustExist = true).optional()
private val output by option("-o", "--output", help = "Output file (default: stdout)").path()
private val extension by option("-x", "--extension", help = "File extension hint (e.g. html)")
private val mimeType by option("-m", "--mime-type", help = "MIME type hint (e.g. text/html)")

override fun run() {
val markItDown = MarkItDown()

val result = if (file != null) {
markItDown.convert(file!!.toFile().absolutePath)
} else {
val info = StreamInfo(extension = extension, mimetype = mimeType)
markItDown.convert(System.`in`.readBytes(), info)
}

if (output != null) {
output!!.toFile().writeText(result.markdown)
} else {
print(result.markdown)
}
}
}

fun main(args: Array<String>) {
// PDFBox pulls in the Log4j API; without a provider it writes a banner to stdout,
// which would corrupt the Markdown we print there.
System.setProperty("log4j2.loggerContextFactory", "org.apache.logging.log4j.simple.SimpleLoggerContextFactory")
System.setProperty("log4j2.simplelogLevel", "OFF")
System.setProperty("log4j2.statusLoggerLevel", "OFF")
MarkItDownCommand().main(args)
MikroMarkdownCommand().main(args)
}
34 changes: 34 additions & 0 deletions cli/src/main/kotlin/com/mikromarkdown/cli/MikroMarkdownCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.mikromarkdown.cli

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.path
import io.github.lemcoder.mikromarkdown.MikroMarkdown
import io.github.lemcoder.mikromarkdown.StreamInfo

class MikroMarkdownCommand : CliktCommand(name = "mikromarkdown") {
private val file by argument("FILE", help = "Input file (reads stdin if omitted)").path(mustExist = true).optional()
private val output by option("-o", "--output", help = "Output file (default: stdout)").path()
private val extension by option("-x", "--extension", help = "File extension hint (e.g. html)")
private val mimeType by option("-m", "--mime-type", help = "MIME type hint (e.g. text/html)")

override fun run() {
val mikroMarkdown = MikroMarkdown()

val result =
if (file != null) {
mikroMarkdown.convert(file!!.toFile().absolutePath)
} else {
val info = StreamInfo(extension = extension, mimetype = mimeType)
mikroMarkdown.convert(System.`in`.readBytes(), info)
}

if (output != null) {
output!!.toFile().writeText(result.markdown)
} else {
print(result.markdown)
}
}
}
48 changes: 48 additions & 0 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Overrides on top of detekt's default config (buildUponDefaultConfig = true).
# Only rules that clash with deliberate choices in this codebase are relaxed.

build:
maxIssues: 0

complexity:
# Parsers walk large format-specific type hierarchies; splitting them further hurts readability.
CyclomaticComplexMethod:
threshold: 25
LongMethod:
threshold: 90
LongParameterList:
functionThreshold: 8
constructorThreshold: 10
NestedBlockDepth:
threshold: 6
TooManyFunctions:
thresholdInFiles: 30
thresholdInClasses: 30
thresholdInObjects: 30
ignorePrivate: true

exceptions:
# Format libraries throw broadly; converters degrade gracefully rather than propagate.
TooGenericExceptionCaught:
active: false
SwallowedException:
active: false

naming:
FunctionNaming:
# Factory functions mirror the type they build: MikroMarkdown(), document {}.
functionPattern: '[a-zA-Z][a-zA-Z0-9]*'

style:
MagicNumber:
active: false
MaxLineLength:
maxLineLength: 120
ReturnCount:
max: 6
ForbiddenComment:
active: false
UnusedPrivateMember:
active: true
LoopWithTooManyJumpStatements:
maxJumpCount: 4
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ poi = "5.5.1"
tika = "3.3.0"
clikt = "5.1.0"
coreKtx = "1.7.0"
detekt = "1.23.8"
ktfmt = "0.27.0"
konsist = "0.17.3"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
Expand All @@ -33,10 +36,13 @@ poi-ooxml = { module = "org.apache.poi:poi-ooxml", version.ref = "poi" }
tika-core = { module = "org.apache.tika:tika-core", version.ref = "tika" }
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "coreKtx" }
konsist = { module = "com.lemonappdev:konsist", version.ref = "konsist" }

[plugins]
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
kotlinx-resources = { id = "com.goncalossilva.resources", version.ref = "kotlinx-resources" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" }
46 changes: 31 additions & 15 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@ plugins {
}

group = "io.github.lemcoder"

version = "0.1.0"

kotlin {
jvm {
compilerOptions {
jvmTarget = JvmTarget.JVM_21
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
compilerOptions { jvmTarget = JvmTarget.JVM_21 }
testRuns["test"].executionTask.configure { useJUnitPlatform() }
}

androidLibrary {
Expand All @@ -25,19 +22,13 @@ kotlin {
minSdk = libs.versions.android.minSdk.get().toInt()

withHostTestBuilder {}.configure {}
withDeviceTestBuilder {
sourceSetTreeName = "test"
}
withDeviceTestBuilder { sourceSetTreeName = "test" }

compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
compilerOptions { jvmTarget = JvmTarget.JVM_11 }
}

sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.io.core)
}
commonMain.dependencies { implementation(libs.kotlinx.io.core) }

jvmMain.dependencies {
implementation(libs.jsoup)
Expand All @@ -64,6 +55,31 @@ kotlin {
jvmTest.dependencies {
implementation(libs.junit.jupiter)
implementation(libs.kotlin.test)
implementation(libs.konsist)
}
}
}

// ktfmt-gradle only derives tasks for the common and JVM source sets, so the Android
// ones — where half the converters live — would go unformatted and unchecked.
run {
val androidSources = fileTree("src") { include("android*/**/*.kt") }
val template = tasks.named<com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask>("ktfmtFormatKmpCommonMain")

val formatAndroid =
tasks.register<com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask>("ktfmtFormatAndroidSourceSets") {
ktfmtClasspath.from(template.map { it.ktfmtClasspath })
formattingOptionsBean.set(template.flatMap { it.formattingOptionsBean })
setSource(androidSources)
}
val checkAndroid =
tasks.register<com.ncorti.ktfmt.gradle.tasks.KtfmtCheckTask>("ktfmtCheckAndroidSourceSets") {
ktfmtClasspath.from(template.map { it.ktfmtClasspath })
formattingOptionsBean.set(template.flatMap { it.formattingOptionsBean })
setSource(androidSources)
}

tasks.named("ktfmtFormat") { dependsOn(formatAndroid) }
tasks.named("ktfmtCheck") { dependsOn(checkAndroid) }
tasks.named("check") { dependsOn(checkAndroid) }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package io.github.lemcoder.mikromarkdown

actual fun testMarkItDown(): MikroMarkdown {
return MarkItDown(context = null)
}
actual fun testMikroMarkdown(): MikroMarkdown {
return MikroMarkdown(context = null)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.lemcoder.mikromarkdown

actual fun testMikroMarkdown(): MikroMarkdown = MikroMarkdown(context = null)
Loading
Loading