Files
zoeissleeping 9bd4b36094 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.
2026-04-27 12:06:30 -05:00

155 lines
5.3 KiB
Vue

<script setup lang="ts">
import { type Grammar } from 'shiki';
import { hashSync } from '~/utils/hash';
const props = defineProps<{ code: string; language: string }>();
const start = Date.now();
const MAX_LENGTH = 15000;
const renderId = `${useId()}-${hashSync(props.code + props.language)}`;
const copied = 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 { data: parsed, clear } = await useAsyncData(`shiki-${renderId}`,
() => parseCode(displayCode.value, props.language.toLowerCase()),
{
watch: [displayCode],
dedupe: 'defer'
}
);
const lineNumberWidth = computed(() => {
if (!parsed.value?.html) return 1;
// Count newlines in the generated HTML or the source code
// Using props.code is safer and faster than parsing the HTML string
return props.code.split('\n').length.toString().length;
});
async function parseCode(code: string, lang: string) {
let displayLang = lang;
try {
let shikiLang = await $shiki.getLanguage(lang);
displayLang = (shikiLang as unknown as Grammar).name;
} catch {
lang = 'text';
}
let html = await $shiki.codeToHtml(code.trim(), {
lang,
themes: { dark: 'vitesse-dark', light: 'vitesse-light' },
});
return { html, displayLang };
}
let copyTimeout: NodeJS.Timeout | null = null;
function copyCode() {
copied.value = true;
navigator.clipboard.writeText(props.code);
if (copyTimeout) clearTimeout(copyTimeout);
copyTimeout = setTimeout(() => {
copied.value = false;
copyTimeout = null;
}, 2000);
}
onUnmounted(() => {
if (copyTimeout) clearTimeout(copyTimeout);
clear();
});
console.log("shiki codeblock rendered in", Date.now() - start);
</script>
<template>
<div class="flex flex-col my-2 rounded-xl overflow-hidden">
<div class="flex items-center pl-3 pr-1.5 py-1.5 text-sm font-sans bg-[var(--color-hover)] justify-between">
<div class="case-capital">
{{ parsed?.displayLang }}
</div>
<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()"
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' }}
<span v-if="!copied" class="i-mynaui-copy text-4 text-[var(--text-secondary)]"></span>
<span v-else class="i-mynaui-check text-4 text-emerald-500"></span>
</button>
<button @click="collapsed = !collapsed"
class="flex items-center justify-center h-5.5 w-5.5 rounded-md @hover:bg-[var(--color-hover)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span
class="i-mynaui-chevron-down text-4 h-4 w-4 text-[var(--text-secondary)] transition-transform duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
:class="collapsed ? '-rotate-90' : ''"></span>
</button>
</div>
</div>
<div class="grid transition-all duration-350 ease-in-out"
:class="collapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'"
:style="`--line-number-width: ${lineNumberWidth}ch;`" :id="`code-${renderId}`">
<div class="overflow-hidden code-container" v-html="parsed?.html"></div>
</div>
</div>
</template>
<style>
.code-container>pre {
overflow: auto hidden;
min-height: 0;
scrollbar-width: thin;
padding: 1rem;
line-height: 0;
counter-reset: lines;
}
.code-container>pre>code .line::before {
counter-increment: lines;
content: counter(lines);
width: var(--line-number-width);
margin-right: 1.5rem;
display: inline-block;
text-align: right;
}
.dark .code-container>pre>code .line::before {
color: rgba(255, 255, 255, 0.25);
}
.light .code-container>pre>code .line::before {
color: rgba(0, 0, 0, 0.25);
}
.code-container>pre>code {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 0;
font-variant-ligatures: none;
}
</style>