Use javascript markdown engine
This commit is contained in:
parent
9f7142b2ab
commit
32b4518bf7
14 changed files with 3908 additions and 105 deletions
|
@ -1,6 +1,6 @@
|
|||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
|
@ -133,7 +133,6 @@ dependencies {
|
|||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("com.google.android.material:material:1.9.0")
|
||||
implementation("androidx.legacy:legacy-support-v13:1.0.0")
|
||||
implementation("com.commonsware.cwac:anddown:0.4.0")
|
||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.browser:browser:1.6.0")
|
||||
|
|
|
@ -90,11 +90,11 @@ data 1|data 2|data 3
|
|||
data 1|data 2|data 3
|
||||
```
|
||||
|
||||
Left Content|Center Content|Right Content
|
||||
:--------|:--------:|--------:
|
||||
data 1|data 2|data 3
|
||||
data 1|data 2|data 3
|
||||
data 1|data 2|data 3
|
||||
| Left Content | Center Content | Right Content |
|
||||
|:-------------|:--------------:|--------------:|
|
||||
| data 1 | data 2 | data 3 |
|
||||
| data 1 | data 2 | data 3 |
|
||||
| data 1 | data 2 | data 3 |
|
||||
|
||||
### Images
|
||||
|
||||
|
@ -116,7 +116,7 @@ In addition to the monospace inline element, code blocks can be created by inden
|
|||
|
||||
Or by wrapping the code in three backticks (\`\`\`):
|
||||
|
||||
```
|
||||
```javascript
|
||||
function helloWorld() {
|
||||
console.log("Hello, world!")
|
||||
}
|
||||
|
|
9
app/src/main/assets/highlight.css
Normal file
9
app/src/main/assets/highlight.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*!
|
||||
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}
|
1207
app/src/main/assets/highlight.js
Normal file
1207
app/src/main/assets/highlight.js
Normal file
File diff suppressed because one or more lines are too long
14
app/src/main/assets/markdown.js
Normal file
14
app/src/main/assets/markdown.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
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)
|
||||
}
|
96
app/src/main/assets/marked-highlight.js
Normal file
96
app/src/main/assets/marked-highlight.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
(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;
|
||||
|
||||
}));
|
2408
app/src/main/assets/marked.js
Normal file
2408
app/src/main/assets/marked.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -20,13 +20,13 @@ import androidx.compose.material.icons.filled.Help
|
|||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.PrivacyTip
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
|
@ -39,42 +39,18 @@ import com.wbrawner.simplemarkdown.ui.SettingsScreen
|
|||
import com.wbrawner.simplemarkdown.ui.SupportScreen
|
||||
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
||||
import com.wbrawner.simplemarkdown.utility.Preference
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
||||
private val viewModel: MarkdownViewModel by viewModels { MarkdownViewModel.factory(fileHelper, preferenceHelper) }
|
||||
private val viewModel: MarkdownViewModel by viewModels {
|
||||
MarkdownViewModel.factory(
|
||||
fileHelper,
|
||||
preferenceHelper
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
val darkMode = withContext(Dispatchers.IO) {
|
||||
val darkModeValue = preferenceHelper[Preference.DARK_MODE] as String
|
||||
|
||||
return@withContext when {
|
||||
darkModeValue.equals(
|
||||
getString(R.string.pref_value_light),
|
||||
ignoreCase = true
|
||||
) -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
|
||||
darkModeValue.equals(
|
||||
getString(R.string.pref_value_dark),
|
||||
ignoreCase = true
|
||||
) -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
|
||||
else -> {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(darkMode)
|
||||
}
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val preferences = mutableMapOf<String, String>()
|
||||
preferences["Autosave"] = preferenceHelper[Preference.AUTOSAVE_ENABLED].toString()
|
||||
|
@ -90,19 +66,45 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
|||
.collectAsState()
|
||||
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
||||
.collectAsState()
|
||||
val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE)
|
||||
.collectAsState()
|
||||
LaunchedEffect(darkModePreference) {
|
||||
val darkMode = when {
|
||||
darkModePreference.equals(
|
||||
getString(R.string.pref_value_light),
|
||||
ignoreCase = true
|
||||
) -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
|
||||
darkModePreference.equals(
|
||||
getString(R.string.pref_value_dark),
|
||||
ignoreCase = true
|
||||
) -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
|
||||
else -> {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(darkMode)
|
||||
}
|
||||
SimpleMarkdownTheme {
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Route.EDITOR.path,
|
||||
enterTransition = { fadeIn(
|
||||
animationSpec = tween(
|
||||
300, easing = LinearEasing
|
||||
enterTransition = {
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
300, easing = LinearEasing
|
||||
)
|
||||
) + slideIntoContainer(
|
||||
animationSpec = tween(300, easing = EaseIn),
|
||||
towards = AnimatedContentTransitionScope.SlideDirection.Start
|
||||
)
|
||||
) + slideIntoContainer(
|
||||
animationSpec = tween(300, easing = EaseIn),
|
||||
towards = AnimatedContentTransitionScope.SlideDirection.Start
|
||||
) },
|
||||
},
|
||||
popEnterTransition = { EnterTransition.None },
|
||||
popExitTransition = {
|
||||
fadeOut(
|
||||
|
@ -120,7 +122,8 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
|||
navController = navController,
|
||||
viewModel = viewModel,
|
||||
enableAutosave = autosaveEnabled,
|
||||
enableReadability = readabilityEnabled
|
||||
enableReadability = readabilityEnabled,
|
||||
darkMode = darkModePreference
|
||||
)
|
||||
}
|
||||
composable(Route.SETTINGS.path) {
|
||||
|
@ -130,13 +133,25 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
|||
SupportScreen(navController = navController)
|
||||
}
|
||||
composable(Route.HELP.path) {
|
||||
MarkdownInfoScreen(title = Route.HELP.title, file = "Cheatsheet.md", navController = navController)
|
||||
MarkdownInfoScreen(
|
||||
title = Route.HELP.title,
|
||||
file = "Cheatsheet.md",
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
composable(Route.ABOUT.path) {
|
||||
MarkdownInfoScreen(title = Route.ABOUT.title, file = "Libraries.md", navController = navController)
|
||||
MarkdownInfoScreen(
|
||||
title = Route.ABOUT.title,
|
||||
file = "Libraries.md",
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
composable(Route.PRIVACY.path) {
|
||||
MarkdownInfoScreen(title = Route.PRIVACY.title, file = "Privacy Policy.md", navController = navController)
|
||||
MarkdownInfoScreen(
|
||||
title = Route.PRIVACY.title,
|
||||
file = "Privacy Policy.md",
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,12 @@ class MarkdownViewModel(
|
|||
val effects = _effects.asSharedFlow()
|
||||
private val saveMutex = Mutex()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
load(null)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMarkdown(markdown: String?) {
|
||||
this@MarkdownViewModel._markdown.emit(markdown ?: "")
|
||||
isDirty.set(true)
|
||||
|
|
|
@ -9,10 +9,11 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
|
@ -38,6 +39,7 @@ import androidx.compose.material3.TabRow
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -51,8 +53,8 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
|
@ -78,6 +80,7 @@ fun MainScreen(
|
|||
viewModel: MarkdownViewModel,
|
||||
enableAutosave: Boolean,
|
||||
enableReadability: Boolean,
|
||||
darkMode: String,
|
||||
) {
|
||||
var lockSwiping by remember { mutableStateOf(false) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
@ -242,8 +245,15 @@ fun MainScreen(
|
|||
if (page == 0) {
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp),
|
||||
.fillMaxSize(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
errorIndicatorColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
),
|
||||
value = textFieldValue,
|
||||
onValueChange = {
|
||||
textFieldValue = if (enableReadability) {
|
||||
|
@ -265,7 +275,7 @@ fun MainScreen(
|
|||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
|
||||
)
|
||||
} else {
|
||||
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown)
|
||||
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown, darkMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -293,6 +303,21 @@ fun MarkdownNavigationDrawer(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = {
|
||||
DismissibleDrawerSheet {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(96.dp),
|
||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "Simple Markdown",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
Route.entries.forEach { route ->
|
||||
if (route == Route.EDITOR) {
|
||||
return@forEach
|
||||
|
|
|
@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavController
|
||||
|
@ -30,15 +28,16 @@ fun MarkdownInfoScreen(
|
|||
}
|
||||
) { paddingValues ->
|
||||
val context = LocalContext.current
|
||||
var markdown by remember { mutableStateOf("") }
|
||||
val (markdown, setMarkdown) = remember { mutableStateOf("") }
|
||||
LaunchedEffect(file) {
|
||||
markdown = context.assets.readAssetToString(file)?.toHtml()?: "Failed to load $file"
|
||||
setMarkdown(context.assets.readAssetToString(file)?.toHtml() ?: "Failed to load $file")
|
||||
}
|
||||
MarkdownPreview(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
markdown = markdown
|
||||
markdown = markdown,
|
||||
"Auto"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +1,93 @@
|
|||
package com.wbrawner.simplemarkdown.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.wbrawner.simplemarkdown.BuildConfig
|
||||
import com.wbrawner.simplemarkdown.R
|
||||
import com.wbrawner.simplemarkdown.utility.toHtml
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Reader
|
||||
|
||||
private const val container = "<main id=\"content\"></main>"
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Composable
|
||||
fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String, darkMode: String) {
|
||||
val materialColors = MaterialTheme.colorScheme
|
||||
val style = remember(darkMode) {
|
||||
"""body {
|
||||
| background: #${materialColors.surface.toArgb().toHexString().substring(2)};
|
||||
| color: #${materialColors.onSurface.toArgb().toHexString().substring(2)};
|
||||
|}
|
||||
|pre {
|
||||
| background: #${materialColors.surfaceVariant.toArgb().toHexString().substring(2)};
|
||||
| color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)};
|
||||
|}""".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
|
||||
var style by remember { mutableStateOf("") }
|
||||
LaunchedEffect(context) {
|
||||
val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
|| context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
val defaultCssId = if (isNightMode) {
|
||||
R.string.pref_custom_css_default_dark
|
||||
} else {
|
||||
R.string.pref_custom_css_default
|
||||
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")
|
||||
}
|
||||
val css = withContext(Dispatchers.IO) {
|
||||
@Suppress("ConstantConditionIf")
|
||||
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
|
||||
context.getString(defaultCssId)
|
||||
} else {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(
|
||||
context.getString(R.string.pref_custom_css),
|
||||
context.getString(defaultCssId)
|
||||
)
|
||||
}
|
||||
}
|
||||
style = "<style>$css</style>"
|
||||
}
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context ->
|
||||
WebView(context)
|
||||
},
|
||||
update = { preview ->
|
||||
coroutineScope.launch {
|
||||
preview.loadDataWithBaseURL(null,
|
||||
style + markdown.toHtml(),
|
||||
"text/html",
|
||||
"UTF-8", null
|
||||
val content =
|
||||
highlightCss + style + container + marked + markedHighlight + highlightJs + markdownJs + markdownUpdateJs
|
||||
|
||||
WebView(context).apply {
|
||||
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
preview.setBackgroundColor(0x01000000)
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
isNestedScrollingEnabled = false
|
||||
settings.javaScriptEnabled = true
|
||||
loadDataWithBaseURL(null, content, "text/html", "UTF-8", null)
|
||||
}
|
||||
},
|
||||
update = { webView ->
|
||||
val content =
|
||||
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 Context.assetToString(fileName: String): String =
|
||||
assets.open(fileName).reader().use(Reader::readText)
|
|
@ -1,7 +1,7 @@
|
|||
package com.wbrawner.simplemarkdown.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -36,6 +36,10 @@ class AndroidFileHelper(private val context: Context) : FileHelper {
|
|||
|
||||
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
|
||||
val uri = source.toString().toUri()
|
||||
context.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
context.contentResolver.openFileDescriptor(uri, "r")
|
||||
?.use {
|
||||
uri.getName(context) to FileInputStream(it.fileDescriptor).reader()
|
||||
|
|
|
@ -17,9 +17,6 @@ allprojects {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://s3.amazonaws.com/repo.commonsware.com/")
|
||||
}
|
||||
maven {
|
||||
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue