feat(markdown): Migrate to comark for markdown rendering

This commit is contained in:
Zoe
2026-08-11 03:18:28 -05:00
parent c9e48687ef
commit 4b99921023
10 changed files with 441 additions and 337 deletions
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { renderMath } from 'comark/plugins/math';
const props = defineProps<{
content: string;
display: boolean;
}>();
const html = computed(() => renderMath(props.content, props.display, { throwOnError: false }));
</script>
<template>
<span v-if="!display" class="math-inline" v-html="html"></span>
<div v-else class="math-block" v-html="html"></div>
</template>
+66 -81
View File
@@ -1,5 +1,8 @@
<script setup lang="ts">
import { h, Text, computed } from 'vue';
import type { MarkdownAstNode, MarkdownElementNode } from '~/utils/markdown';
import { createMarkdownParser, getNodeAttributes, getNodeChildren, getNodeTag, splitParagraphBreaks, textContent, toVueAttributes } from '~/utils/markdown';
import { createTextVNode, h, ref, watch } from 'vue';
import MarkdownMath from './Math.vue';
import MarkdownShikiHighlight from './ShikiHighlight.vue';
const props = defineProps<{
@@ -8,98 +11,80 @@ const props = defineProps<{
id: string;
}>();
const { $remark } = useNuxtApp();
const parser = createMarkdownParser();
const parse = async (content: string, finished: boolean) => {
const tree = await parser(content, { streaming: !finished });
return splitParagraphBreaks(tree);
};
// this function effectively removes lazy concetenation from the markdown
// normally in markdown, if you have two lines that are separated by only
// one new line. E.g.:
//
// This is some markdown text but I split
// it into two lines so it's easier to read in the editor
//
// they are *usually* concatenated into one line, however,
// since we are a web editor, with line wrapping, that behavior
// in undesirable, so pre preserve newlines by just maiking
// every new line two newlines (as long as we arent in a codeblock)
// function preprocessMarkdown(doc: string) {
// const lines = doc.split('\n');
// let inCode = false;
// let result: string[] = [];
const parsed = ref(await parse(props.content, props.finished));
let parseVersion = 0;
// for (let i = 0; i < lines.length; i++) {
// const fullLine = lines[i]!;
// const match = fullLine.match(/^( {0,3})(.*)/);
// const content = match?.[2] || '';
// if (content.startsWith('```')) {
// if (!inCode) {
// inCode = true;
// } else {
// inCode = false;
// }
// result.push(fullLine);
// continue;
// }
// if (inCode) {
// result.push(fullLine);
// } else {
// if (content.trim().length === 0) {
// result.push('');
// } else {
// if (result.length > 0 && result[result.length - 1] !== '') {
// result.push('');
// }
// result.push(fullLine);
// }
// }
// }
// return result.join('\n');
// }
const ast = computed(() => {
const mdast = $remark.parse(props.content);
return $remark.runSync(mdast);
});
const renderNode = (node: any, index: number): any => {
if (node.type === 'text' || node.type === 'raw') return h(Text, node.value);
if (node.type === 'element') {
if (node.tagName === 'code') {
const isBlock = node.position?.start.line !== node.position?.end.line;
if (isBlock && node.children?.[0]?.type === 'text') {
return h(MarkdownShikiHighlight, {
key: `code-${index}`,
code: node.children[0].value,
language: node.properties?.className?.[0]?.replace('language-', '') || 'text'
});
}
watch(
[() => props.content, () => props.finished],
async ([content, finished]) => {
const version = ++parseVersion;
const tree = await parse(content, finished);
if (version === parseVersion) {
parsed.value = tree;
}
},
);
const children = node.children?.map((child: any, i: number) => renderNode(child, i)) || [];
return h(
node.tagName,
{ ...node.properties, key: `${node.tagName}-${index}` },
children
);
const renderNode = (node: MarkdownAstNode, path: string): any => {
if (typeof node === 'string') {
return createTextVNode(node);
}
return null;
if (!Array.isArray(node)) {
return null;
}
if (node[0] === null) {
return null;
}
const tag = getNodeTag(node);
const attributes = getNodeAttributes(node);
if (tag === 'pre') {
const language = typeof attributes.language === 'string' ? attributes.language : 'text';
return h(MarkdownShikiHighlight, {
key: path,
code: textContent(node),
language,
});
}
if (tag === 'math') {
return h(MarkdownMath, {
key: path,
content: typeof attributes.content === 'string' ? attributes.content : textContent(node),
display: typeof attributes.class === 'string' && attributes.class.includes('block'),
});
}
const children = getNodeChildren(node)
.map((child, index) => renderNode(child, `${path}.${index}`))
.filter((child) => child !== null);
return h(
tag,
{ ...toVueAttributes(attributes), key: path },
children,
);
};
const render = () => {
const children = ast.value?.children?.flatMap(renderNode) || [];
const children = parsed.value.nodes
.map((node, index) => renderNode(node, `${props.id}.${index}`))
.filter((child) => child !== null);
return h('div', { class: 'prose-wrapper' }, [
h('article', { class: 'markdown-body' }, children)
h('article', { class: 'markdown-body' }, children),
]);
}
};
</script>
<template>
<render />
</template>
</template>