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 {
|
||||
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)
|
||||
|
|
|
@ -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
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