Use javascript markdown engine

This commit is contained in:
William Brawner 2023-11-05 21:21:02 -07:00
parent 9f7142b2ab
commit 32b4518bf7
14 changed files with 3908 additions and 105 deletions

View file

@ -1,6 +1,6 @@
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.* import java.util.Properties
plugins { plugins {
id("com.android.application") id("com.android.application")
@ -133,7 +133,6 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("com.google.android.material:material:1.9.0") implementation("com.google.android.material:material:1.9.0")
implementation("androidx.legacy:legacy-support-v13:1.0.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("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")

View file

@ -90,11 +90,11 @@ data 1|data 2|data 3
data 1|data 2|data 3 data 1|data 2|data 3
``` ```
Left Content|Center Content|Right Content | Left Content | Center Content | Right Content |
:--------|:--------:|--------: |:-------------|:--------------:|--------------:|
data 1|data 2|data 3 | data 1 | data 2 | data 3 |
data 1|data 2|data 3 | data 1 | data 2 | data 3 |
data 1|data 2|data 3 | data 1 | data 2 | data 3 |
### Images ### 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 (\`\`\`): Or by wrapping the code in three backticks (\`\`\`):
``` ```javascript
function helloWorld() { function helloWorld() {
console.log("Hello, world!") console.log("Hello, world!")
} }

View 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}

File diff suppressed because one or more lines are too long

View 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)
}

View 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
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

View file

@ -20,13 +20,13 @@ import androidx.compose.material.icons.filled.Help
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.PrivacyTip import androidx.compose.material.icons.filled.PrivacyTip
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController 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.SupportScreen
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
import com.wbrawner.simplemarkdown.utility.Preference import com.wbrawner.simplemarkdown.utility.Preference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) 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) WindowCompat.setDecorFitsSystemWindows(window, false)
val preferences = mutableMapOf<String, String>() val preferences = mutableMapOf<String, String>()
preferences["Autosave"] = preferenceHelper[Preference.AUTOSAVE_ENABLED].toString() preferences["Autosave"] = preferenceHelper[Preference.AUTOSAVE_ENABLED].toString()
@ -90,19 +66,45 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
.collectAsState() .collectAsState()
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED) val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
.collectAsState() .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 { SimpleMarkdownTheme {
val navController = rememberNavController() val navController = rememberNavController()
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Route.EDITOR.path, startDestination = Route.EDITOR.path,
enterTransition = { fadeIn( enterTransition = {
fadeIn(
animationSpec = tween( animationSpec = tween(
300, easing = LinearEasing 300, easing = LinearEasing
) )
) + slideIntoContainer( ) + slideIntoContainer(
animationSpec = tween(300, easing = EaseIn), animationSpec = tween(300, easing = EaseIn),
towards = AnimatedContentTransitionScope.SlideDirection.Start towards = AnimatedContentTransitionScope.SlideDirection.Start
) }, )
},
popEnterTransition = { EnterTransition.None }, popEnterTransition = { EnterTransition.None },
popExitTransition = { popExitTransition = {
fadeOut( fadeOut(
@ -120,7 +122,8 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
navController = navController, navController = navController,
viewModel = viewModel, viewModel = viewModel,
enableAutosave = autosaveEnabled, enableAutosave = autosaveEnabled,
enableReadability = readabilityEnabled enableReadability = readabilityEnabled,
darkMode = darkModePreference
) )
} }
composable(Route.SETTINGS.path) { composable(Route.SETTINGS.path) {
@ -130,13 +133,25 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
SupportScreen(navController = navController) SupportScreen(navController = navController)
} }
composable(Route.HELP.path) { 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) { 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) { 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
)
} }
} }
} }

View file

@ -33,6 +33,12 @@ class MarkdownViewModel(
val effects = _effects.asSharedFlow() val effects = _effects.asSharedFlow()
private val saveMutex = Mutex() private val saveMutex = Mutex()
init {
viewModelScope.launch {
load(null)
}
}
suspend fun updateMarkdown(markdown: String?) { suspend fun updateMarkdown(markdown: String?) {
this@MarkdownViewModel._markdown.emit(markdown ?: "") this@MarkdownViewModel._markdown.emit(markdown ?: "")
isDirty.set(true) isDirty.set(true)

View file

@ -9,10 +9,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack 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.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -51,8 +53,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
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.SolidColor
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -78,6 +80,7 @@ fun MainScreen(
viewModel: MarkdownViewModel, viewModel: MarkdownViewModel,
enableAutosave: Boolean, enableAutosave: Boolean,
enableReadability: Boolean, enableReadability: Boolean,
darkMode: String,
) { ) {
var lockSwiping by remember { mutableStateOf(false) } var lockSwiping by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -242,8 +245,15 @@ fun MainScreen(
if (page == 0) { if (page == 0) {
TextField( TextField(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize(),
.padding(8.dp), 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, value = textFieldValue,
onValueChange = { onValueChange = {
textFieldValue = if (enableReadability) { textFieldValue = if (enableReadability) {
@ -265,7 +275,7 @@ fun MainScreen(
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences) keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
) )
} else { } else {
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown) MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown, darkMode)
} }
} }
} }
@ -293,6 +303,21 @@ fun MarkdownNavigationDrawer(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = { DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = {
DismissibleDrawerSheet { 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 -> Route.entries.forEach { route ->
if (route == Route.EDITOR) { if (route == Route.EDITOR) {
return@forEach return@forEach

View file

@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController import androidx.navigation.NavController
@ -30,15 +28,16 @@ fun MarkdownInfoScreen(
} }
) { paddingValues -> ) { paddingValues ->
val context = LocalContext.current val context = LocalContext.current
var markdown by remember { mutableStateOf("") } val (markdown, setMarkdown) = remember { mutableStateOf("") }
LaunchedEffect(file) { LaunchedEffect(file) {
markdown = context.assets.readAssetToString(file)?.toHtml()?: "Failed to load $file" setMarkdown(context.assets.readAssetToString(file)?.toHtml() ?: "Failed to load $file")
} }
MarkdownPreview( MarkdownPreview(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues),
markdown = markdown markdown = markdown,
"Auto"
) )
} }
} }

View file

@ -1,69 +1,93 @@
package com.wbrawner.simplemarkdown.ui package com.wbrawner.simplemarkdown.ui
import android.content.res.Configuration import android.content.Context
import android.view.ViewGroup
import android.webkit.WebView import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.layout.fillMaxSize
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.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.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.preference.PreferenceManager
import com.wbrawner.simplemarkdown.BuildConfig import com.wbrawner.simplemarkdown.BuildConfig
import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.utility.toHtml
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.Reader
private const val container = "<main id=\"content\"></main>"
@OptIn(ExperimentalStdlibApi::class)
@Composable @Composable
fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String) { fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String, darkMode: String) {
val coroutineScope = rememberCoroutineScope() val materialColors = MaterialTheme.colorScheme
val context = LocalContext.current val style = remember(darkMode) {
var style by remember { mutableStateOf("") } """body {
LaunchedEffect(context) { | background: #${materialColors.surface.toArgb().toHexString().substring(2)};
val isNightMode = AppCompatDelegate.getDefaultNightMode() == | color: #${materialColors.onSurface.toArgb().toHexString().substring(2)};
AppCompatDelegate.MODE_NIGHT_YES |}
|| context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES |pre {
val defaultCssId = if (isNightMode) { | background: #${materialColors.surfaceVariant.toArgb().toHexString().substring(2)};
R.string.pref_custom_css_default_dark | color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)};
} else { |}""".trimMargin().wrapTag("style")
R.string.pref_custom_css_default
} }
val css = withContext(Dispatchers.IO) { var marked by remember { mutableStateOf("") }
@Suppress("ConstantConditionIf") var markedHighlight by remember { mutableStateOf("") }
if (!BuildConfig.ENABLE_CUSTOM_CSS) { var highlightJs by remember { mutableStateOf("") }
context.getString(defaultCssId) var highlightCss by remember { mutableStateOf("") }
} else { var markdownJs by remember { mutableStateOf("") }
PreferenceManager.getDefaultSharedPreferences(context) val markdownUpdateJs by remember(markdown) {
.getString( mutableStateOf(
context.getString(R.string.pref_custom_css), "setMarkdown(`${
context.getString(defaultCssId) 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")
} }
style = "<style>$css</style>"
} }
AndroidView( AndroidView(
modifier = modifier, modifier = modifier,
factory = { context -> factory = { context ->
WebView(context) val content =
}, highlightCss + style + container + marked + markedHighlight + highlightJs + markdownJs + markdownUpdateJs
update = { preview ->
coroutineScope.launch { WebView(context).apply {
preview.loadDataWithBaseURL(null, WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
style + markdown.toHtml(), layoutParams = ViewGroup.LayoutParams(
"text/html", ViewGroup.LayoutParams.MATCH_PARENT,
"UTF-8", null 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)

View file

@ -1,7 +1,7 @@
package com.wbrawner.simplemarkdown.utility package com.wbrawner.simplemarkdown.utility
import android.content.Context import android.content.Context
import android.net.Uri import android.content.Intent
import androidx.core.net.toUri import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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) { override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
val uri = source.toString().toUri() 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") context.contentResolver.openFileDescriptor(uri, "r")
?.use { ?.use {
uri.getName(context) to FileInputStream(it.fileDescriptor).reader() uri.getName(context) to FileInputStream(it.fileDescriptor).reader()

View file

@ -17,9 +17,6 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven {
url = uri("https://s3.amazonaws.com/repo.commonsware.com/")
}
maven { maven {
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
} }