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:
parent
7ed94aebf4
commit
b7c2e116cf
14 changed files with 41 additions and 181 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
||||||
[submodule "md4k/lib/md4c"]
|
|
||||||
path = md4k/lib/md4c
|
|
||||||
url = https://github.com/mity/md4c
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
2
md4k/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
/build
|
|
||||||
/.cxx
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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
|
|
21
md4k/proguard-rules.pro
vendored
21
md4k/proguard-rules.pro
vendored
|
@ -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
|
|
|
@ -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"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
</manifest>
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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)
|
|
|
@ -1 +1 @@
|
||||||
include(":app", ":md4k")
|
include(":app")
|
||||||
|
|
Loading…
Reference in a new issue