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 {
implementation(project(":md4k"))
implementation("androidx.compose.material3:material3-window-size-class-android:1.2.0")
val navigationVersion = "2.7.2"
implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion")
@ -138,6 +137,15 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1")
implementation("androidx.core:core-ktx:1.12.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")
implementation(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.toArgb
import androidx.compose.ui.viewinterop.AndroidView
import com.wbrawner.md4k.MD4K
import com.wbrawner.simplemarkdown.BuildConfig
import kotlinx.coroutines.Dispatchers
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
fun MarkdownText(modifier: Modifier = Modifier, markdown: String) {
val (html, setHtml) = remember { mutableStateOf("") }
LaunchedEffect(markdown) {
withContext(Dispatchers.IO) {
setHtml(MD4K.toHtml(markdown))
val parsedHtml = renderer.render(
markdownParser.parse(markdown)
)
setHtml(parsedHtml)
}
}
HtmlText(modifier = modifier, html = html)
@ -42,7 +71,6 @@ fun HtmlText(html: String, modifier: Modifier = Modifier) {
| color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)};
|}""".trimMargin().wrapTag("style")
}
AndroidView(
modifier = modifier,
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")