Use MD4C to convert markdown to HTML
This commit is contained in:
parent
37f0b8bae8
commit
208e0a1a6f
21 changed files with 188 additions and 3802 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "md4k/lib/md4c"]
|
||||||
|
path = md4k/lib/md4c
|
||||||
|
url = https://github.com/mity/md4c
|
|
@ -111,6 +111,7 @@ 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")
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
/*!
|
|
||||||
Theme: Default
|
|
||||||
Description: Original highlight.js style
|
|
||||||
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
|
|
||||||
Maintainer: @highlightjs/core-team
|
|
||||||
Website: https://highlightjs.org/
|
|
||||||
License: see project LICENSE
|
|
||||||
Touched: 2021
|
|
||||||
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
|
File diff suppressed because one or more lines are too long
|
@ -1,14 +0,0 @@
|
||||||
const localMarked = new marked.Marked(
|
|
||||||
markedHighlight.markedHighlight({
|
|
||||||
langPrefix: 'hljs language-',
|
|
||||||
highlight(code, lang) {
|
|
||||||
console.log(`highlighing code with $lang`)
|
|
||||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
||||||
return hljs.highlight(code, { language }).value;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
function setMarkdown(markdown) {
|
|
||||||
document.getElementById('content').innerHTML = marked.parse(markdown)
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
(function (global, factory) {
|
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
||||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
||||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.markedHighlight = {}));
|
|
||||||
})(this, (function (exports) { 'use strict';
|
|
||||||
|
|
||||||
function markedHighlight(options) {
|
|
||||||
if (typeof options === 'function') {
|
|
||||||
options = {
|
|
||||||
highlight: options
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options || typeof options.highlight !== 'function') {
|
|
||||||
throw new Error('Must provide highlight function');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof options.langPrefix !== 'string') {
|
|
||||||
options.langPrefix = 'language-';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
async: !!options.async,
|
|
||||||
walkTokens(token) {
|
|
||||||
if (token.type !== 'code') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lang = getLang(token);
|
|
||||||
|
|
||||||
if (options.async) {
|
|
||||||
return Promise.resolve(options.highlight(token.text, lang)).then(updateToken(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = options.highlight(token.text, lang);
|
|
||||||
if (code instanceof Promise) {
|
|
||||||
throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.');
|
|
||||||
}
|
|
||||||
updateToken(token)(code);
|
|
||||||
},
|
|
||||||
renderer: {
|
|
||||||
code(code, infoString, escaped) {
|
|
||||||
const lang = (infoString || '').match(/\S*/)[0];
|
|
||||||
const classAttr = lang
|
|
||||||
? ` class="${options.langPrefix}${escape(lang)}"`
|
|
||||||
: '';
|
|
||||||
code = code.replace(/\n$/, '');
|
|
||||||
return `<pre><code${classAttr}>${escaped ? code : escape(code, true)}\n</code></pre>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLang(token) {
|
|
||||||
return (token.lang || '').match(/\S*/)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateToken(token) {
|
|
||||||
return (code) => {
|
|
||||||
if (typeof code === 'string' && code !== token.text) {
|
|
||||||
token.escaped = true;
|
|
||||||
token.text = code;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// copied from marked helpers
|
|
||||||
const escapeTest = /[&<>"']/;
|
|
||||||
const escapeReplace = new RegExp(escapeTest.source, 'g');
|
|
||||||
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
|
|
||||||
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
|
|
||||||
const escapeReplacements = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
const getEscapeReplacement = (ch) => escapeReplacements[ch];
|
|
||||||
function escape(html, encode) {
|
|
||||||
if (encode) {
|
|
||||||
if (escapeTest.test(html)) {
|
|
||||||
return html.replace(escapeReplace, getEscapeReplacement);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (escapeTestNoEncode.test(html)) {
|
|
||||||
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.markedHighlight = markedHighlight;
|
|
||||||
|
|
||||||
}));
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,6 @@ import com.wbrawner.simplemarkdown.utility.FileHelper
|
||||||
import com.wbrawner.simplemarkdown.utility.Preference
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
|
@ -300,7 +300,7 @@ private fun MainScreen(
|
||||||
.width(1.dp)
|
.width(1.dp)
|
||||||
.background(color = MaterialTheme.colorScheme.primary)
|
.background(color = MaterialTheme.colorScheme.primary)
|
||||||
)
|
)
|
||||||
MarkdownPreview(
|
MarkdownText(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
|
@ -362,7 +362,7 @@ private fun TabbedMarkdownEditor(
|
||||||
enableReadability = enableReadability
|
enableReadability = enableReadability
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown)
|
MarkdownText(modifier = Modifier.fillMaxSize(), markdown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -436,30 +436,34 @@ fun MarkdownTopAppBar(
|
||||||
actions: (@Composable RowScope.() -> Unit)? = null
|
actions: (@Composable RowScope.() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
TopAppBar(title = {
|
TopAppBar(
|
||||||
Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
title = {
|
||||||
}, navigationIcon = {
|
Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
val (icon, contentDescription, onClick) = remember {
|
},
|
||||||
if (backAsUp) {
|
navigationIcon = {
|
||||||
Triple(Icons.AutoMirrored.Filled.ArrowBack, "Go Back", goBack)
|
val (icon, contentDescription, onClick) = remember {
|
||||||
} else {
|
if (backAsUp) {
|
||||||
Triple(
|
Triple(Icons.AutoMirrored.Filled.ArrowBack, "Go Back", goBack)
|
||||||
Icons.Default.Menu, "Main Menu"
|
} else {
|
||||||
) {
|
Triple(
|
||||||
coroutineScope.launch {
|
Icons.Default.Menu, "Main Menu"
|
||||||
if (drawerState?.isOpen == true) {
|
) {
|
||||||
drawerState.close()
|
coroutineScope.launch {
|
||||||
} else {
|
if (drawerState?.isOpen == true) {
|
||||||
drawerState?.open()
|
drawerState.close()
|
||||||
|
} else {
|
||||||
|
drawerState?.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
IconButton(onClick = { onClick() }) {
|
||||||
IconButton(onClick = { onClick() }) {
|
Icon(imageVector = icon, contentDescription = contentDescription)
|
||||||
Icon(imageVector = icon, contentDescription = contentDescription)
|
}
|
||||||
}
|
}, actions = actions ?: {},
|
||||||
}, actions = actions ?: {})
|
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -31,7 +31,7 @@ fun MarkdownInfoScreen(
|
||||||
LaunchedEffect(file) {
|
LaunchedEffect(file) {
|
||||||
setMarkdown(context.assets.readAssetToString(file) ?: "Failed to load $file")
|
setMarkdown(context.assets.readAssetToString(file) ?: "Failed to load $file")
|
||||||
}
|
}
|
||||||
MarkdownPreview(
|
MarkdownText(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues),
|
.padding(paddingValues),
|
||||||
|
|
|
@ -1,31 +1,36 @@
|
||||||
package com.wbrawner.simplemarkdown.ui
|
package com.wbrawner.simplemarkdown.ui
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
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.platform.LocalContext
|
|
||||||
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 java.io.Reader
|
|
||||||
|
|
||||||
private const val container = "<main id=\"content\"></main>"
|
@Composable
|
||||||
|
fun MarkdownText(modifier: Modifier = Modifier, markdown: String) {
|
||||||
|
val (html, setHtml) = remember { mutableStateOf("") }
|
||||||
|
LaunchedEffect(markdown) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
setHtml(MD4K.toHtml(markdown))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HtmlText(modifier = modifier, html = html)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String) {
|
fun HtmlText(html: String, modifier: Modifier = Modifier) {
|
||||||
val materialColors = MaterialTheme.colorScheme
|
val materialColors = MaterialTheme.colorScheme
|
||||||
val style = remember(isSystemInDarkTheme()) {
|
val style = remember(isSystemInDarkTheme()) {
|
||||||
"""body {
|
"""body {
|
||||||
|
@ -37,37 +42,10 @@ fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String) {
|
||||||
| color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)};
|
| color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)};
|
||||||
|}""".trimMargin().wrapTag("style")
|
|}""".trimMargin().wrapTag("style")
|
||||||
}
|
}
|
||||||
var marked by remember { mutableStateOf("") }
|
|
||||||
var markedHighlight by remember { mutableStateOf("") }
|
|
||||||
var highlightJs by remember { mutableStateOf("") }
|
|
||||||
var highlightCss by remember { mutableStateOf("") }
|
|
||||||
var markdownJs by remember { mutableStateOf("") }
|
|
||||||
val markdownUpdateJs by remember(markdown) {
|
|
||||||
mutableStateOf(
|
|
||||||
"setMarkdown(`${
|
|
||||||
markdown.replace(
|
|
||||||
"`",
|
|
||||||
"\\`"
|
|
||||||
)
|
|
||||||
}`)".wrapTag("script")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val context = LocalContext.current
|
|
||||||
LaunchedEffect(context) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
marked = context.assetToString("marked.js").wrapTag("script")
|
|
||||||
markedHighlight = context.assetToString("marked-highlight.js").wrapTag("script")
|
|
||||||
highlightJs = context.assetToString("highlight.js").wrapTag("script")
|
|
||||||
highlightCss = context.assetToString("highlight.css").wrapTag("style")
|
|
||||||
markdownJs = context.assetToString("markdown.js").wrapTag("script")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
val content =
|
|
||||||
highlightCss + style + container + marked + markedHighlight + highlightJs + markdownJs + markdownUpdateJs
|
|
||||||
|
|
||||||
WebView(context).apply {
|
WebView(context).apply {
|
||||||
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
@ -77,18 +55,13 @@ fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String) {
|
||||||
setBackgroundColor(Color.Transparent.toArgb())
|
setBackgroundColor(Color.Transparent.toArgb())
|
||||||
isNestedScrollingEnabled = false
|
isNestedScrollingEnabled = false
|
||||||
settings.javaScriptEnabled = true
|
settings.javaScriptEnabled = true
|
||||||
loadDataWithBaseURL(null, content, "text/html", "UTF-8", null)
|
loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { webView ->
|
update = { webView ->
|
||||||
val content =
|
webView.loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
||||||
highlightCss + style + container + marked + markedHighlight + highlightJs + markdownJs + markdownUpdateJs
|
|
||||||
webView.loadDataWithBaseURL(null, content, "text/html", "UTF-8", null)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.wrapTag(tag: String) = "<$tag>$this</$tag>"
|
private fun String.wrapTag(tag: String) = "<$tag>$this</$tag>"
|
||||||
|
|
||||||
private fun Context.assetToString(fileName: String): String =
|
|
||||||
assets.open(fileName).reader().use(Reader::readText)
|
|
2
md4k/.gitignore
vendored
Normal file
2
md4k/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/build
|
||||||
|
/.cxx
|
18
md4k/CMakeLists.txt
Normal file
18
md4k/CMakeLists.txt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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
|
||||||
|
)
|
41
md4k/build.gradle.kts
Normal file
41
md4k/build.gradle.kts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
0
md4k/consumer-rules.pro
Normal file
0
md4k/consumer-rules.pro
Normal file
1
md4k/lib/md4c
Submodule
1
md4k/lib/md4c
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 481fbfbdf72daab2912380d62bb5f2187d438408
|
21
md4k/proguard-rules.pro
vendored
Normal file
21
md4k/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# 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
|
4
md4k/src/main/AndroidManifest.xml
Normal file
4
md4k/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
41
md4k/src/main/cpp/md4k.c
Normal file
41
md4k/src/main/cpp/md4k.c
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#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;
|
||||||
|
}
|
12
md4k/src/main/java/com/wbrawner/md4k/MD4K.kt
Normal file
12
md4k/src/main/java/com/wbrawner/md4k/MD4K.kt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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")
|
include(":app", ":md4k")
|
||||||
|
|
Loading…
Reference in a new issue