feat(jbm): support for katex and tables
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
0cd2bc14de
commit
c8cb3c4aff
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title></title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("/_android_res/font/inter_regular.ttf");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("/_android_res/font/inter_bold.ttf");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fragment Mono";
|
||||
src: url("/_android_res/font/fragmentmono_regular.ttf");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body, html {
|
||||
font-family: "Inter", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: "Fragment Mono", monospace;
|
||||
}
|
||||
</style>
|
||||
<script src="/_android_assets/embedded/katex.min.js"></script>
|
||||
<link rel="stylesheet" href="/_android_assets/embedded/katex.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="katex"></div>
|
||||
<script>
|
||||
window.addEventListener("load", () => {
|
||||
katex.render(Bridge.getSource(), document.querySelector("#katex"), {
|
||||
throwOnError: false,
|
||||
maxExpand: 50
|
||||
})
|
||||
document.body.style.color = Bridge.getForegroundColour()
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title></title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("/_android_res/font/inter_regular.ttf");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter Display";
|
||||
src: url("/_android_res/font/inter_display_semibold.ttf");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("/_android_res/font/inter_bold.ttf");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fragment Mono";
|
||||
src: url("/_android_res/font/fragmentmono_regular.ttf");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body, html {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
/* No margins anywhere we don't want them */
|
||||
body, html, div, table {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: {primary};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: "Fragment Mono", monospace;
|
||||
}
|
||||
|
||||
#markdown {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#markdown img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#markdown h2 {
|
||||
font-size: 1.2em;
|
||||
font-family: "Inter Display", sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#markdown p, #markdown li {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#markdown table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#markdown table, #markdown th, #markdown td {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
#markdown th, #markdown td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
img.user-avatar {
|
||||
width: calc(1.5em * 0.9);
|
||||
height: calc(1.5em * 0.9);
|
||||
vertical-align: middle;
|
||||
border-radius: 9999px;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script defer type="module">
|
||||
import {micromark} from "/_android_assets/embedded/micromark.bundle.js"
|
||||
import {gfmHtml, gfm} from "/_android_assets/embedded/micromark-gfm.bundle.js"
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
document.body.style.color = Bridge.getForegroundColour()
|
||||
const dynStyles = `
|
||||
#markdown a {
|
||||
color: ${Bridge.getPrimaryColour()};
|
||||
}
|
||||
#markdown table {
|
||||
border-color: ${Bridge.getForegroundColour()};
|
||||
}
|
||||
#markdown th, #markdown td {
|
||||
border-color: ${Bridge.getForegroundColour()};
|
||||
}
|
||||
a.mention {
|
||||
color: ${Bridge.getPrimaryColour()};
|
||||
background-color: ${Bridge.getMentionBackgroundColour()};
|
||||
padding: 0.25em;
|
||||
}
|
||||
`
|
||||
|
||||
const style = document.createElement("style")
|
||||
style.innerHTML = dynStyles
|
||||
document.head.appendChild(style)
|
||||
|
||||
const htmlSource = micromark(Bridge.getSource(), {
|
||||
extensions: [gfm()],
|
||||
htmlExtensions: [gfmHtml()]
|
||||
})
|
||||
const divWithMarkdown = document.createElement("div")
|
||||
divWithMarkdown.id = "markdown"
|
||||
divWithMarkdown.innerHTML = htmlSource
|
||||
|
||||
// Remove all image tags, as they are not supported
|
||||
divWithMarkdown.querySelectorAll("img").forEach(img => img.remove())
|
||||
// For all <a> tags, rewrite any that include the private _android_assets and _android_res
|
||||
// directories
|
||||
divWithMarkdown.querySelectorAll("a").forEach(a => {
|
||||
if (a.href.includes("_android_assets") || a.href.includes("_android_res")) {
|
||||
a.href = "#"
|
||||
}
|
||||
})
|
||||
|
||||
const ULID_REGEXP = /[0-9A-HJKMNP-TV-Z]{26}/g
|
||||
// Custom emote regex, a ULID wrapped in colons
|
||||
const CUSTOM_EMOTE_REGEXP = new RegExp(`:${ULID_REGEXP.source}:`, "g")
|
||||
// User mention regex, a ULID wrapped in angle brackets with a @ before the ULID
|
||||
const USER_MENTION_REGEXP = new RegExp(`<@${ULID_REGEXP.source}>`, "g")
|
||||
// Channel mention regex, a ULID wrapped in angle brackets with a # before the ULID
|
||||
const CHANNEL_MENTION_REGEXP = new RegExp(`<#${ULID_REGEXP.source}>`, "g")
|
||||
|
||||
// For anything that contains text, replace the custom emote and user/channel mentions
|
||||
// First, replace the custom emotes
|
||||
divWithMarkdown.querySelectorAll("p, li, h1, h2, h3, h4, h5, h6, th, td").forEach(element => {
|
||||
element.innerHTML = element.innerHTML
|
||||
.replace(CUSTOM_EMOTE_REGEXP, match => {
|
||||
const ulid = match.slice(1, -1)
|
||||
const img = document.createElement("img")
|
||||
img.src = Bridge.getCustomEmoteUrl(ulid)
|
||||
img.style.verticalAlign = "middle"
|
||||
img.style.width = "1.5em"
|
||||
img.style.height = "1.5em"
|
||||
// Screen readers must ignore the image as we cannot reliably provide a name
|
||||
img.alt = ""
|
||||
img.setAttribute("aria-hidden", "true")
|
||||
return img.outerHTML
|
||||
})
|
||||
})
|
||||
// Next we replace the user mentions. Micromark thinks they are emails so we search
|
||||
// for <a> tags that lead to @<ULID>
|
||||
divWithMarkdown.querySelectorAll("a").forEach(a => {
|
||||
if (a.href.startsWith("mailto:")) {
|
||||
const remainingIsUlid = a.href.slice(7).match(ULID_REGEXP)
|
||||
if (remainingIsUlid) {
|
||||
const ulid = remainingIsUlid[0]
|
||||
|
||||
// First we empty the <a> tag
|
||||
a.href = "#"
|
||||
a.innerHTML = ""
|
||||
|
||||
// Then we add an image with the user's avatar
|
||||
const img = document.createElement("img")
|
||||
img.src = Bridge.userAvatar(ulid)
|
||||
img.classList.add("user-avatar")
|
||||
a.appendChild(img)
|
||||
|
||||
// Finally we put the user's name
|
||||
const textNode = document.createTextNode(Bridge.resolveUserMention(ulid))
|
||||
a.appendChild(textNode)
|
||||
|
||||
// We also add a class to the <a> tag so we can style it
|
||||
a.classList.add("mention")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
document.body.appendChild(divWithMarkdown)
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import chat.revolt.activities.InviteActivity
|
||||
import chat.revolt.api.REVOLT_APP
|
||||
import chat.revolt.api.REVOLT_FILES
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.internals.ResourceLocations
|
||||
import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState
|
||||
import chat.revolt.markdown.jbm.MentionResolver
|
||||
import chat.revolt.markdown.jbm.asHexString
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun FallbackRenderer(content: String, modifier: Modifier = Modifier) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val mdState = LocalJBMarkdownTreeState.current
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
val assetLoader = WebViewAssetLoader.Builder()
|
||||
.setDomain(Uri.parse(REVOLT_APP).host!!)
|
||||
.addPathHandler(
|
||||
"/_android_assets/",
|
||||
WebViewAssetLoader.AssetsPathHandler(context)
|
||||
)
|
||||
.addPathHandler(
|
||||
"/_android_res/",
|
||||
WebViewAssetLoader.ResourcesPathHandler(context)
|
||||
)
|
||||
.build()
|
||||
|
||||
webChromeClient = object : WebChromeClient() {}
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): WebResourceResponse? {
|
||||
return request?.let { assetLoader.shouldInterceptRequest(it.url) }
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
webResourceRequest: WebResourceRequest
|
||||
): Boolean {
|
||||
// Capture clicks on invite links
|
||||
if (webResourceRequest.url.host == "rvlt.gg" ||
|
||||
(
|
||||
webResourceRequest.url.host?.endsWith("revolt.chat") == true && webResourceRequest.url.path?.startsWith(
|
||||
"/invite"
|
||||
) == true
|
||||
)
|
||||
) {
|
||||
val intent = Intent(
|
||||
context,
|
||||
InviteActivity::class.java
|
||||
).setAction(Intent.ACTION_VIEW)
|
||||
|
||||
intent.data = webResourceRequest.url
|
||||
context.startActivity(intent)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, open the link in the browser using androidx.browser
|
||||
val customTab = CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(colors.background.toArgb())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
customTab.launchUrl(context, webResourceRequest.url)
|
||||
|
||||
// Prevent the WebView from navigating to the URL
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
setSupportZoom(false)
|
||||
setSupportMultipleWindows(false)
|
||||
isVerticalScrollBarEnabled = false
|
||||
isHorizontalScrollBarEnabled = false
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
}
|
||||
|
||||
addJavascriptInterface(
|
||||
object {
|
||||
@JavascriptInterface
|
||||
fun getSource(): String {
|
||||
return content
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getForegroundColour(): String {
|
||||
return colors.onBackground.asHexString()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getPrimaryColour(): String {
|
||||
return colors.primary.asHexString()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getMentionBackgroundColour(): String {
|
||||
return colors.primary.copy(alpha = 0.2f).asHexString()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getCustomEmoteUrl(emoteId: String): String {
|
||||
return "$REVOLT_FILES/emojis/$emoteId/original"
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun resolveUserMention(userId: String): String {
|
||||
return MentionResolver.resolveUser(userId, mdState.currentServer)
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun userAvatar(userId: String): String {
|
||||
return ResourceLocations.userAvatarUrl(RevoltAPI.userCache[userId])
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun resolveChannelMention(channelId: String): String {
|
||||
return MentionResolver.resolveChannel(channelId)
|
||||
}
|
||||
},
|
||||
"Bridge"
|
||||
)
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
|
||||
loadUrl(
|
||||
"$REVOLT_APP/_android_assets/markdown/markdown.html"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package chat.revolt.markdown.jbm
|
||||
|
||||
import FallbackRenderer
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
|
|
@ -22,6 +23,7 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -89,7 +91,6 @@ import dev.snipme.highlights.model.ColorHighlight
|
|||
import dev.snipme.highlights.model.SyntaxLanguage
|
||||
import dev.snipme.highlights.model.SyntaxThemes
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.logcat
|
||||
import org.intellij.markdown.MarkdownElementTypes
|
||||
import org.intellij.markdown.MarkdownTokenTypes
|
||||
import org.intellij.markdown.ast.ASTNode
|
||||
|
|
@ -205,18 +206,11 @@ private fun annotateText(
|
|||
)
|
||||
)
|
||||
|
||||
val member = state.currentServer?.let { serverId ->
|
||||
RevoltAPI.members.getMember(serverId, userId)
|
||||
}
|
||||
val mentionDisplay = member?.nickname
|
||||
?: RevoltAPI.userCache[userId]?.username
|
||||
?: "<@$userId>"
|
||||
|
||||
appendInlineContent(JBMAnnotations.JBMBackgroundRoundingStart.tag)
|
||||
append(" ")
|
||||
appendInlineContent(JBMAnnotations.UserAvatar.tag, userId)
|
||||
append(" ")
|
||||
append(mentionDisplay)
|
||||
appendInlineContent(JBMAnnotations.JBMBackgroundRoundingEnd.tag)
|
||||
append(MentionResolver.resolveUser(userId, state.currentServer))
|
||||
append(" ")
|
||||
|
||||
pop()
|
||||
pop()
|
||||
|
|
@ -243,13 +237,7 @@ private fun annotateText(
|
|||
)
|
||||
)
|
||||
|
||||
val channel = RevoltAPI.channelCache[channelId]
|
||||
val mentionDisplay = channel?.name?.let { name -> "#$name" }
|
||||
?: "<#$channelId>"
|
||||
|
||||
appendInlineContent(JBMAnnotations.JBMBackgroundRoundingStart.tag)
|
||||
append(mentionDisplay)
|
||||
appendInlineContent(JBMAnnotations.JBMBackgroundRoundingEnd.tag)
|
||||
append(MentionResolver.resolveChannel(channelId))
|
||||
|
||||
pop()
|
||||
pop()
|
||||
|
|
@ -950,12 +938,17 @@ private fun JBMBlock(node: ASTNode, modifier: Modifier, nestingCounter: Int = 0)
|
|||
MarkdownElementTypes.HTML_BLOCK,
|
||||
MarkdownElementTypes.LINK_DEFINITION,
|
||||
MarkdownTokenTypes.WHITE_SPACE -> {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||
fontSize = LocalTextStyle.current.fontSize * state.fontSizeMultiplier
|
||||
)
|
||||
) {
|
||||
JBMText(node, modifier)
|
||||
// If the only child is a BLOCK_MATH we render it instead
|
||||
if (node.children.size == 1 && node.children[0].type == GFMElementTypes.BLOCK_MATH) {
|
||||
JBMBlock(node.children[0], modifier)
|
||||
} else {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||
fontSize = LocalTextStyle.current.fontSize * state.fontSizeMultiplier
|
||||
)
|
||||
) {
|
||||
JBMText(node, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1074,6 +1067,41 @@ private fun JBMBlock(node: ASTNode, modifier: Modifier, nestingCounter: Int = 0)
|
|||
}
|
||||
}
|
||||
|
||||
GFMElementTypes.BLOCK_MATH -> {
|
||||
// No use using Katex in embedded because we don't want to
|
||||
// create WebViews when embedded
|
||||
if (!LocalJBMarkdownTreeState.current.embedded) {
|
||||
val mathContent =
|
||||
try {
|
||||
node.getTextInNode(state.sourceText).toString().removeSurrounding("$$")
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
KatexRenderer(mathContent, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
GFMElementTypes.TABLE -> {
|
||||
// Dito BLOCK_MATH
|
||||
if (!LocalJBMarkdownTreeState.current.embedded) {
|
||||
val tableContent = try {
|
||||
node.getTextInNode(state.sourceText).toString()
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
|
||||
FallbackRenderer(tableContent, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownTokenTypes.HORIZONTAL_RULE -> {
|
||||
HorizontalDivider(
|
||||
color = colorScheme.onSurface,
|
||||
thickness = 1.dp,
|
||||
modifier = modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
package chat.revolt.markdown.jbm
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import chat.revolt.activities.InviteActivity
|
||||
import chat.revolt.api.REVOLT_APP
|
||||
|
||||
internal fun Color.asHexString(): String {
|
||||
val argb = toArgb()
|
||||
val alpha = (argb shr 24 and 0xff) / 255.0f
|
||||
val red = argb shr 16 and 0xff
|
||||
val green = argb shr 8 and 0xff
|
||||
val blue = argb and 0xff
|
||||
return String.format("#%02x%02x%02x%02x", red, green, blue, (alpha * 255).toInt())
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun KatexRenderer(content: String, modifier: Modifier = Modifier) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
val assetLoader = WebViewAssetLoader.Builder()
|
||||
.setDomain(Uri.parse(REVOLT_APP).host!!)
|
||||
.addPathHandler(
|
||||
"/_android_assets/",
|
||||
WebViewAssetLoader.AssetsPathHandler(context)
|
||||
)
|
||||
.addPathHandler(
|
||||
"/_android_res/",
|
||||
WebViewAssetLoader.ResourcesPathHandler(context)
|
||||
)
|
||||
.build()
|
||||
|
||||
webChromeClient = object : WebChromeClient() {}
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): WebResourceResponse? {
|
||||
return request?.let { assetLoader.shouldInterceptRequest(it.url) }
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
webResourceRequest: WebResourceRequest
|
||||
): Boolean {
|
||||
// Capture clicks on invite links
|
||||
if (webResourceRequest.url.host == "rvlt.gg" ||
|
||||
(
|
||||
webResourceRequest.url.host?.endsWith("revolt.chat") == true && webResourceRequest.url.path?.startsWith(
|
||||
"/invite"
|
||||
) == true
|
||||
)
|
||||
) {
|
||||
val intent = Intent(
|
||||
context,
|
||||
InviteActivity::class.java
|
||||
).setAction(Intent.ACTION_VIEW)
|
||||
|
||||
intent.data = webResourceRequest.url
|
||||
context.startActivity(intent)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, open the link in the browser using androidx.browser
|
||||
val customTab = CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(colors.background.toArgb())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
customTab.launchUrl(context, webResourceRequest.url)
|
||||
|
||||
// Prevent the WebView from navigating to the URL
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
setSupportZoom(false)
|
||||
setSupportMultipleWindows(false)
|
||||
isVerticalScrollBarEnabled = false
|
||||
isHorizontalScrollBarEnabled = false
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
}
|
||||
|
||||
addJavascriptInterface(
|
||||
object {
|
||||
@JavascriptInterface
|
||||
fun getSource(): String {
|
||||
return content
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getForegroundColour(): String {
|
||||
return colors.onBackground.asHexString()
|
||||
}
|
||||
},
|
||||
"Bridge"
|
||||
)
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
|
||||
loadUrl(
|
||||
"$REVOLT_APP/_android_assets/katex/katex.html"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package chat.revolt.markdown.jbm
|
||||
|
||||
import chat.revolt.api.RevoltAPI
|
||||
|
||||
object MentionResolver {
|
||||
/**
|
||||
* Resolves a user mention to its fancy representation.
|
||||
* Note that this uses the new format without a leading @ unless the user is not found.
|
||||
*
|
||||
* @param userId The user ID to resolve.
|
||||
* @param serverId The server ID to resolve the user in.
|
||||
* @return The resolved user mention.
|
||||
*/
|
||||
fun resolveUser(userId: String, serverId: String? = null): String {
|
||||
val maybeMember = serverId?.let { RevoltAPI.members.getMember(serverId, userId) }
|
||||
return maybeMember?.nickname
|
||||
?: RevoltAPI.userCache[userId]?.username
|
||||
?: "<@$userId>"
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a channel mention to its fancy representation.
|
||||
*
|
||||
* @param channelId The channel ID to resolve.
|
||||
* @param serverId The server ID to resolve the channel in.
|
||||
* @return The resolved channel mention.
|
||||
*/
|
||||
fun resolveChannel(channelId: String): String {
|
||||
val channel = RevoltAPI.channelCache[channelId]
|
||||
return channel?.name?.let { name -> "#$name" } ?: "<#$channelId>"
|
||||
}
|
||||
}
|
||||
|
|
@ -46,20 +46,268 @@ const deps = [
|
|||
url: "https://cdn.jsdelivr.net/npm/katex@0.16.19/dist/katex.min.js",
|
||||
},
|
||||
{
|
||||
file: "micromark.bundle.js",
|
||||
url: "https://esm.sh/v135/micromark@4.0.1/es2022/micromark.bundle.mjs",
|
||||
file: "fonts/KaTeX_AMS-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_AMS-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_AMS-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_AMS-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_AMS-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_AMS-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Caligraphic-Bold.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Caligraphic-Bold.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Caligraphic-Bold.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Caligraphic-Bold.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Caligraphic-Bold.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Caligraphic-Bold.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Caligraphic-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Caligraphic-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Caligraphic-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Caligraphic-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Caligraphic-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Caligraphic-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Fraktur-Bold.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Fraktur-Bold.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Fraktur-Bold.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Fraktur-Bold.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Fraktur-Bold.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Fraktur-Bold.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Fraktur-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Fraktur-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Fraktur-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Fraktur-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Fraktur-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Fraktur-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Bold.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Bold.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Bold.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Bold.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Bold.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Bold.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-BoldItalic.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-BoldItalic.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-BoldItalic.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-BoldItalic.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-BoldItalic.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-BoldItalic.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Italic.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Italic.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Italic.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Italic.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Italic.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Italic.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Main-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Main-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Math-BoldItalic.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Math-BoldItalic.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Math-BoldItalic.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Math-BoldItalic.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Math-BoldItalic.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Math-BoldItalic.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Math-Italic.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Math-Italic.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Math-Italic.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Math-Italic.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Math-Italic.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Math-Italic.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Bold.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Bold.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Bold.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Bold.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Bold.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Bold.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Italic.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Italic.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Italic.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Italic.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Italic.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Italic.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_SansSerif-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_SansSerif-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Script-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Script-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Script-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Script-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Script-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Script-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size1-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size1-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size1-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size1-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size1-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size1-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size2-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size2-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size2-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size2-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size2-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size2-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size3-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size3-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size3-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size3-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size3-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size3-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size4-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size4-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size4-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size4-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Size4-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Size4-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Typewriter-Regular.ttf",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Typewriter-Regular.ttf",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Typewriter-Regular.woff",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Typewriter-Regular.woff",
|
||||
},
|
||||
{
|
||||
file: "fonts/KaTeX_Typewriter-Regular.woff2",
|
||||
url: "https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/fonts/KaTeX_Typewriter-Regular.woff2",
|
||||
},
|
||||
{
|
||||
file: "micromark.bundle.js",
|
||||
url: "https://esm.sh/v135/micromark@3.2.0/es2022/micromark.bundle.mjs",
|
||||
},
|
||||
{
|
||||
file: "micromark-gfm.bundle.js",
|
||||
url: "https://esm.sh/v135/micromark-extension-gfm@3.0.0/es2022/micromark-extension-gfm.bundle.mjs",
|
||||
}
|
||||
]
|
||||
|
||||
console.log("Will download the following files:")
|
||||
for (const dep of deps) {
|
||||
console.log(`- ${dep.file} from ${dep.url}`)
|
||||
}
|
||||
|
||||
console.log("Will download the above files.")
|
||||
if (!confirm("Continue?")) {
|
||||
console.log("Aborted.")
|
||||
Deno.exit(0)
|
||||
}
|
||||
|
||||
const fontsFolder = resolve(outputFolder, "fonts")
|
||||
Deno.mkdirSync(fontsFolder, { recursive: true })
|
||||
|
||||
for (const dep of deps) {
|
||||
const response = await fetch(dep.url)
|
||||
const data = await response.arrayBuffer()
|
||||
|
|
|
|||
Loading…
Reference in New Issue