91 lines
2.6 KiB
Vue
91 lines
2.6 KiB
Vue
<script setup lang="ts">
|
|
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<{
|
|
content: string;
|
|
finished: boolean;
|
|
id: string;
|
|
}>();
|
|
|
|
const parser = createMarkdownParser();
|
|
const parse = async (content: string, finished: boolean) => {
|
|
const tree = await parser(content, { streaming: !finished });
|
|
return splitParagraphBreaks(tree);
|
|
};
|
|
|
|
const parsed = ref(await parse(props.content, props.finished));
|
|
let parseVersion = 0;
|
|
|
|
watch(
|
|
[() => props.content, () => props.finished],
|
|
async ([content, finished]) => {
|
|
const version = ++parseVersion;
|
|
const tree = await parse(content, finished);
|
|
if (version === parseVersion) {
|
|
parsed.value = tree;
|
|
}
|
|
},
|
|
);
|
|
|
|
const renderNode = (node: MarkdownAstNode, path: string): any => {
|
|
if (typeof node === 'string') {
|
|
return createTextVNode(node);
|
|
}
|
|
|
|
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 = 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),
|
|
]);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<render />
|
|
</template>
|