Skip to content

Commit

Permalink
feat(jbm): support for katex and tables
Browse files Browse the repository at this point in the history
Signed-off-by: Infi <[email protected]>
  • Loading branch information
infi committed Jan 2, 2025
1 parent 0cd2bc1 commit c8cb3c4
Show file tree
Hide file tree
Showing 7 changed files with 887 additions and 26 deletions.
54 changes: 54 additions & 0 deletions app/src/main/assets/katex/katex.html
Original file line number Diff line number Diff line change
@@ -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>
206 changes: 206 additions & 0 deletions app/src/main/assets/markdown/markdown.html
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit c8cb3c4

Please sign in to comment.