feat(markdown): Migrate to comark for markdown rendering
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user