Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,110 @@
|
||||
// The whole document as one page or as text (MARK-11).
|
||||
import { unified } from 'unified'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import { fromDom } from 'hast-util-from-dom'
|
||||
import rehypeStringify from 'rehype-stringify'
|
||||
import { toText } from 'hast-util-to-text'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { VFile } from 'vfile'
|
||||
import { bundle } from './diagrams.js'
|
||||
|
||||
const markdown = () => unified().use(remarkParse).use(remarkGfm).use(remarkMath)
|
||||
|
||||
/** Mermaid drawn for paper; a broken one stays code. */
|
||||
async function draw(tree) {
|
||||
const blocks = []
|
||||
visit(tree, 'code', (node) => {
|
||||
if (node.lang === 'mermaid') blocks.push(node)
|
||||
})
|
||||
const failed = []
|
||||
if (!blocks.length) return failed
|
||||
const paint = await bundle()
|
||||
for (const node of blocks) {
|
||||
try {
|
||||
const svg = await paint(node.value, 'paper')
|
||||
Object.assign(node, { type: 'html', value: `<figure class="diagram">${svg}</figure>` })
|
||||
} catch (error) {
|
||||
// Lines only: Mermaid's message quotes the document.
|
||||
console.warn(`ely: a diagram did not draw: ${error.name}`)
|
||||
failed.push(node.position.start.line)
|
||||
}
|
||||
}
|
||||
return failed
|
||||
}
|
||||
|
||||
/** A `srcset`'s first URL, split as the spec does. */
|
||||
function first(srcset) {
|
||||
const url = srcset.replace(/^[\s,]+/, '').split(/\s/)[0]
|
||||
return url.replace(/,+$/, '')
|
||||
}
|
||||
|
||||
/** One `src` per image, swapped for the app's mark. */
|
||||
function marked(html) {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
const token = `ely-image-${crypto.randomUUID()}-`
|
||||
// Responsive sources go: the page carries one file per image.
|
||||
for (const source of doc.querySelectorAll('picture > source')) source.remove()
|
||||
const images = []
|
||||
for (const image of doc.querySelectorAll('img')) {
|
||||
const srcset = image.getAttribute('srcset')
|
||||
image.removeAttribute('srcset')
|
||||
image.removeAttribute('sizes')
|
||||
const src = image.getAttribute('src') || (srcset && first(srcset))
|
||||
if (!src) continue
|
||||
images.push(src)
|
||||
image.setAttribute('src', token + (images.length - 1))
|
||||
}
|
||||
return { html: doc.body.innerHTML, images, token }
|
||||
}
|
||||
|
||||
/** KaTeX's failures: line and its message, never logged. */
|
||||
function formulas(file) {
|
||||
return file.messages
|
||||
.filter((message) => message.source === 'rehype-katex')
|
||||
.map((message) => ({
|
||||
line: message.line,
|
||||
error: message.cause?.rawMessage ?? message.cause?.name ?? message.reason
|
||||
}))
|
||||
}
|
||||
|
||||
/** `body` as HTML, images marked; the failed diagrams and formulas. */
|
||||
export async function html(body) {
|
||||
const tree = markdown().parse(body)
|
||||
displayed(tree, body)
|
||||
const failed = await draw(tree)
|
||||
const page = unified()
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeKatex)
|
||||
.use(rehypeStringify, { allowDangerousHtml: true })
|
||||
const file = new VFile()
|
||||
const done = page.stringify(await page.run(tree, file), file)
|
||||
return { ...marked(done), failed, formulas: formulas(file) }
|
||||
}
|
||||
|
||||
/** `$$…$$` inline displays, as in the editor. */
|
||||
function displayed(tree, body) {
|
||||
visit(tree, 'inlineMath', (node) => {
|
||||
const at = node.position?.start.offset ?? 0
|
||||
if (body.startsWith('$$', at)) {
|
||||
node.data.hProperties = { className: ['language-math', 'math-display'] }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** `body` as plain text: blocks on lines, math as TeX. */
|
||||
export async function text(body) {
|
||||
const page = unified().use(remarkRehype, { allowDangerousHtml: true })
|
||||
const hast = await page.run(markdown().parse(body))
|
||||
// Raw HTML alone goes through the DOM; blocks keep lines.
|
||||
visit(hast, 'raw', (node, index, parent) => {
|
||||
const parsed = new DOMParser().parseFromString(node.value, 'text/html').body
|
||||
const shown = fromDom(parsed).children
|
||||
parent.children.splice(index, 1, ...shown)
|
||||
return index + shown.length
|
||||
})
|
||||
return toText(hast)
|
||||
}
|
||||
Reference in new issue
Block a user