Switch from MD4C to CommonMark for markdown parsing

I wanted to use MD4C for the performance but unfortunately there seem to be issues with how it handles UTF-8 and how the JNI handles it. CommonMark will have to do for now at least
This commit is contained in:
William Brawner 2024-05-05 23:08:32 -06:00
parent 7ed94aebf4
commit b7c2e116cf
Signed by: wbrawner
GPG key ID: 8FF12381C6C90D35
14 changed files with 41 additions and 181 deletions

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "md4k/lib/md4c"]
path = md4k/lib/md4c
url = https://github.com/mity/md4c

View file

@ -111,7 +111,6 @@ play {
} }
dependencies { dependencies {
implementation(project(":md4k"))
implementation("androidx.compose.material3:material3-window-size-class-android:1.2.0") implementation("androidx.compose.material3:material3-window-size-class-android:1.2.0")
val navigationVersion = "2.7.2" val navigationVersion = "2.7.2"
implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion") implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion")
@ -138,6 +137,15 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.jakewharton.timber:timber:5.0.1")
implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.browser:browser:1.6.0") implementation("androidx.browser:browser:1.6.0")
val commonMarkVersion = "0.22.0"
implementation("org.commonmark:commonmark:$commonMarkVersion")
implementation("org.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion")
implementation("org.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion")
implementation("org.commonmark:commonmark-ext-autolink:$commonMarkVersion")
implementation("org.commonmark:commonmark-ext-task-list-items:$commonMarkVersion")
implementation("org.commonmark:commonmark-ext-yaml-front-matter:$commonMarkVersion")
implementation("org.commonmark:commonmark-ext-image-attributes:$commonMarkVersion")
implementation("org.commonmark:commonmark-ext-heading-anchor:$commonMarkVersion")
val composeBom = platform("androidx.compose:compose-bom:2023.08.00") val composeBom = platform("androidx.compose:compose-bom:2023.08.00")
implementation(composeBom) implementation(composeBom)
androidTestImplementation(composeBom) androidTestImplementation(composeBom)

View file

@ -12,17 +12,46 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import com.wbrawner.md4k.MD4K
import com.wbrawner.simplemarkdown.BuildConfig import com.wbrawner.simplemarkdown.BuildConfig
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.commonmark.ext.autolink.AutolinkExtension
import org.commonmark.ext.front.matter.YamlFrontMatterExtension
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.ext.heading.anchor.HeadingAnchorExtension
import org.commonmark.ext.image.attributes.ImageAttributesExtension
import org.commonmark.ext.task.list.items.TaskListItemsExtension
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
private val markdownExtensions = listOf(
AutolinkExtension.create(),
StrikethroughExtension.create(),
TablesExtension.create(),
HeadingAnchorExtension.create(),
YamlFrontMatterExtension.create(),
ImageAttributesExtension.create(),
TaskListItemsExtension.create(),
)
private val markdownParser = Parser.builder()
.extensions(markdownExtensions)
.build()
private val renderer = HtmlRenderer.builder()
.extensions(markdownExtensions)
.build()
@Composable @Composable
fun MarkdownText(modifier: Modifier = Modifier, markdown: String) { fun MarkdownText(modifier: Modifier = Modifier, markdown: String) {
val (html, setHtml) = remember { mutableStateOf("") } val (html, setHtml) = remember { mutableStateOf("") }
LaunchedEffect(markdown) { LaunchedEffect(markdown) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
setHtml(MD4K.toHtml(markdown)) val parsedHtml = renderer.render(
markdownParser.parse(markdown)
)
setHtml(parsedHtml)
} }
} }
HtmlText(modifier = modifier, html = html) HtmlText(modifier = modifier, html = html)
@ -42,7 +71,6 @@ fun HtmlText(html: String, modifier: Modifier = Modifier) {
| color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)}; | color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)};
|}""".trimMargin().wrapTag("style") |}""".trimMargin().wrapTag("style")
} }
AndroidView( AndroidView(
modifier = modifier, modifier = modifier,
factory = { context -> factory = { context ->

2
md4k/.gitignore vendored
View file

@ -1,2 +0,0 @@
/build
/.cxx

View file

@ -1,18 +0,0 @@
cmake_minimum_required(VERSION 3.22.1)
project("md4k")
add_subdirectory("lib/md4c")
set(BUILD_MD2HTML_EXECUTABLE OFF)
add_library(${CMAKE_PROJECT_NAME} SHARED
src/main/cpp/md4k.c)
target_link_libraries(${CMAKE_PROJECT_NAME}
md4c
md4c-html
)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC
${PROJECT_SOURCE_DIR}/lib/md4c/src
)

View file

@ -1,46 +0,0 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.wbrawner.md4k"
compileSdk = 34
defaultConfig {
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
externalNativeBuild {
cmake {
cppFlags("")
}
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
externalNativeBuild {
cmake {
path("CMakeLists.txt")
version = "3.22.1"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test:runner:1.5.2")
}

@ -1 +0,0 @@
Subproject commit 481fbfbdf72daab2912380d62bb5f2187d438408

View file

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,28 +0,0 @@
package com.wbrawner.md4k
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
@RunWith(Parameterized::class)
class MarkdownParserTest(private val markdown: String, private val html: String) {
@Test
fun testMarkdownToHtmlConversion() {
val parsedHtml = markdown.toHtml()
assert(parsedHtml == html) {
"""expected "$html", got "$parsedHtml""""
}
}
companion object {
@JvmStatic
@Parameters(name = "Markdown: {0}")
fun data(): Array<Array<String>> = arrayOf(
arrayOf("# Test", "<h1>Test</h1>\n"),
arrayOf("- [ ] Check this", "<ul>\n<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled>Check this</li>\n</ul>\n"),
arrayOf("- [x] Checked!", "<ul>\n<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled checked>Checked!</li>\n</ul>\n"),
)
}
}

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -1,41 +0,0 @@
#include <jni.h>
#include "md4c.h"
#include "md4c-html.h"
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
size_t length;
} HtmlBuffer;
void process_output(const MD_CHAR *output, MD_SIZE size, void *userdata) {
HtmlBuffer *buffer = (HtmlBuffer *) userdata;
buffer->data = realloc(buffer->data, buffer->length + size);
memcpy(buffer->data + buffer->length, output, size);
buffer->length += size;
}
jstring Java_com_wbrawner_md4k_MD4K_toHtml(
JNIEnv *env,
jobject this,
jstring markdown
) {
const char *nativeString = (*env)->GetStringUTFChars(env, markdown, NULL);
HtmlBuffer buffer;
buffer.data = malloc(0);
buffer.length = 0;
md_html(
nativeString,
strlen(nativeString),
&process_output,
&buffer,
MD_FLAG_PERMISSIVEAUTOLINKS | MD_FLAG_TABLES | MD_FLAG_STRIKETHROUGH |
MD_FLAG_TASKLISTS | MD_FLAG_LATEXMATHSPANS | MD_FLAG_WIKILINKS | MD_FLAG_UNDERLINE,
0
);
(*env)->ReleaseStringUTFChars(env, markdown, nativeString);
jstring html = (*env)->NewStringUTF(env, buffer.data);
free(buffer.data);
return html;
}

View file

@ -1,12 +0,0 @@
package com.wbrawner.md4k
object MD4K {
init {
System.loadLibrary("md4k")
}
external fun toHtml(markdown: String): String
}
fun String.toHtml() = MD4K.toHtml(this)

View file

@ -1 +1 @@
include(":app", ":md4k") include(":app")