feat: enhance markdown rendering
Adds remark-breaks for newline-to-br conversion and a custom remarkSplitBlocks plugin to split paragraphs at hard breaks. Code blocks over 15KB are truncated with a 'Show all' toggle for better performance.
This commit is contained in:
@@ -5,16 +5,31 @@ const props = defineProps<{ code: string; language: string }>();
|
|||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
|
const MAX_LENGTH = 15000;
|
||||||
|
|
||||||
const renderId = `${useId()}-${hashSync(props.code + props.language)}`;
|
const renderId = `${useId()}-${hashSync(props.code + props.language)}`;
|
||||||
const copied = ref(false);
|
const copied = ref(false);
|
||||||
const collapsed = ref(false);
|
const collapsed = ref(false);
|
||||||
|
const showFull = ref(false);
|
||||||
|
|
||||||
|
const isTruncated = computed(() => props.code.length > MAX_LENGTH);
|
||||||
|
const displayCode = computed(() => isTruncated.value && !showFull.value ? props.code.slice(0, MAX_LENGTH) : props.code);
|
||||||
|
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes}B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}kB`;
|
||||||
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||||
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullSizeFormatted = computed(() => formatSize(props.code.length));
|
||||||
|
|
||||||
const { $shiki } = useNuxtApp();
|
const { $shiki } = useNuxtApp();
|
||||||
|
|
||||||
const { data: parsed, clear } = await useAsyncData(`shiki-${renderId}`,
|
const { data: parsed, clear } = await useAsyncData(`shiki-${renderId}`,
|
||||||
() => parseCode(props.code, props.language.toLowerCase()),
|
() => parseCode(displayCode.value, props.language.toLowerCase()),
|
||||||
{
|
{
|
||||||
watch: [() => props.code],
|
watch: [displayCode],
|
||||||
dedupe: 'defer'
|
dedupe: 'defer'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -72,6 +87,14 @@ console.log("shiki codeblock rendered in", Date.now() - start);
|
|||||||
{{ parsed?.displayLang }}
|
{{ parsed?.displayLang }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<button v-if="isTruncated && !showFull" @click="showFull = true"
|
||||||
|
class="flex items-center px-1 gap-0.5 rounded-md @hover:bg-[var(--color-hover)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||||
|
Show all ({{ fullSizeFormatted }})
|
||||||
|
</button>
|
||||||
|
<button v-else-if="isTruncated" @click="showFull = false"
|
||||||
|
class="flex items-center px-1 gap-0.5 rounded-md @hover:bg-[var(--color-hover)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||||
|
Collapse
|
||||||
|
</button>
|
||||||
<button @click="copyCode()"
|
<button @click="copyCode()"
|
||||||
class="flex items-center px-1 gap-0.5 rounded-md @hover:bg-[var(--color-hover)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
class="flex items-center px-1 gap-0.5 rounded-md @hover:bg-[var(--color-hover)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||||
{{ copied ? 'Copied' : 'Copy' }}
|
{{ copied ? 'Copied' : 'Copy' }}
|
||||||
|
|||||||
@@ -1,16 +1,59 @@
|
|||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
|
import { visit } from 'unist-util-visit';
|
||||||
import remarkParse from 'remark-parse';
|
import remarkParse from 'remark-parse';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import remarkRehype from 'remark-rehype';
|
import remarkRehype from 'remark-rehype';
|
||||||
|
import remarkBreaks from 'remark-breaks';
|
||||||
import remarkMath from 'remark-math';
|
import remarkMath from 'remark-math';
|
||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
|
|
||||||
|
function remarkSplitBlocks() {
|
||||||
|
return (tree: any) => {
|
||||||
|
visit(tree, 'paragraph', (node, index, parent) => {
|
||||||
|
const breakIndex = node.children.findIndex((child: any) => child.type === 'break');
|
||||||
|
|
||||||
|
if (breakIndex !== -1) {
|
||||||
|
const beforeBreak = node.children.slice(0, breakIndex);
|
||||||
|
const afterBreak = node.children.slice(breakIndex + 1);
|
||||||
|
|
||||||
|
node.children = beforeBreak;
|
||||||
|
|
||||||
|
const newParagraph = {
|
||||||
|
type: 'paragraph',
|
||||||
|
children: afterBreak,
|
||||||
|
};
|
||||||
|
|
||||||
|
let rootChildIndex = -1;
|
||||||
|
if (parent.type === 'root') {
|
||||||
|
rootChildIndex = index!;
|
||||||
|
} else {
|
||||||
|
rootChildIndex = tree.children.findIndex((child: any) =>
|
||||||
|
child === parent || (child.children && child.children.includes(node))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rootChildIndex === -1) {
|
||||||
|
rootChildIndex = tree.children.indexOf(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootChildIndex !== -1) {
|
||||||
|
tree.children.splice(rootChildIndex + 1, 0, newParagraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
return index! + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
const remark =
|
const remark =
|
||||||
unified()
|
unified()
|
||||||
.use(remarkParse)
|
.use(remarkParse)
|
||||||
.use(remarkGfm)
|
.use(remarkGfm)
|
||||||
.use(remarkMath)
|
.use(remarkMath)
|
||||||
|
.use(remarkBreaks)
|
||||||
|
.use(remarkSplitBlocks)
|
||||||
.use(remarkRehype, { allowDangerousHtml: true })
|
.use(remarkRehype, { allowDangerousHtml: true })
|
||||||
.use(rehypeKatex, { output: 'mathml' });
|
.use(rehypeKatex, { output: 'mathml' });
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -117,10 +117,12 @@ export default defineNuxtConfig({
|
|||||||
'remark-parse',
|
'remark-parse',
|
||||||
'remark-rehype',
|
'remark-rehype',
|
||||||
'remark-math',
|
'remark-math',
|
||||||
|
'remark-breaks',
|
||||||
'rehype-katex',
|
'rehype-katex',
|
||||||
'unist-util-visit',
|
'unist-util-visit',
|
||||||
'comlink',
|
'comlink',
|
||||||
'nanoid'
|
'nanoid',
|
||||||
|
'drizzle-orm/pg-core'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
@@ -135,6 +137,12 @@ export default defineNuxtConfig({
|
|||||||
// viewTransition: true,
|
// viewTransition: true,
|
||||||
// },
|
// },
|
||||||
|
|
||||||
|
// nitro: {
|
||||||
|
// experimental: {
|
||||||
|
// wasm: true
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxt/hints',
|
'@nuxt/hints',
|
||||||
'@unocss/nuxt',
|
'@unocss/nuxt',
|
||||||
|
|||||||
Reference in New Issue
Block a user