feat: add better provider support, icons, regen, and a lot more
This commit is contained in:
@@ -19,6 +19,21 @@ const render = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.tagName === 'table') {
|
||||
return h('div', { class: 'table-wrapper' }, [
|
||||
h(
|
||||
'table',
|
||||
node.properties,
|
||||
node.children?.map((child: any, index: number) =>
|
||||
h(resolveComponent('MarkdownAstNode'), {
|
||||
node: child,
|
||||
key: `table-child-${index}`
|
||||
})
|
||||
)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
return h(
|
||||
node.tagName,
|
||||
node.properties,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { RootContent } from 'hast';
|
||||
import MarkdownShikiHighlight from './ShikiHighlight.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
@@ -92,13 +93,14 @@ watch(parts, async (newParts) => {
|
||||
hastParts.value = stableBase.concat(latestHast.children);
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prose-wrapper">
|
||||
<article class="markdown-body">
|
||||
<MarkdownAstNode v-for="(node, index) in hastParts" :key="`${id}-${index}`" :node="node" />
|
||||
<template v-for="(node, index) in hastParts" :key="`${id}-${index}`">
|
||||
<MarkdownAstNode :node="node" v-memo="[node.type === 'text' ? node.value : node.data]" />
|
||||
</template>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
@@ -227,11 +229,16 @@ blockquote {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
margin: calc(var(--spacing) * 4) 0;
|
||||
}
|
||||
|
||||
/* TODO: make these tables better, this is literally the first attempt from Gemini 3 flash */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: calc(var(--spacing) * 4) 0;
|
||||
font-size: 0.95rem;
|
||||
text-align: left;
|
||||
background-color: var(--color-base);
|
||||
@@ -283,6 +290,11 @@ label>span {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
@@ -3,51 +3,138 @@ import { hashSync } from '~/utils/hash';
|
||||
const props = defineProps<{ code: string; lang: string }>();
|
||||
|
||||
const renderId = hashSync(props.code + props.lang);
|
||||
const codeBlockRef = ref<HTMLDivElement | null>(null);
|
||||
const codeHeight: Ref<string | number> = ref('auto');
|
||||
const copied = ref(false);
|
||||
const collapsed = ref(false);
|
||||
|
||||
const { data: html } = useAsyncData<string>(`shiki-${renderId}`, async () => parseCode());
|
||||
const { data: parsed } = useAsyncData(`shiki-${renderId}`, async () => {
|
||||
const { html, displayLang } = await parseCode();
|
||||
return { html, displayLang };
|
||||
});
|
||||
const lineNumberWidth = computed(() => {
|
||||
if (!html.value) return 1;
|
||||
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;
|
||||
});
|
||||
|
||||
watch(() => props.code, async () => {
|
||||
html.value = await parseCode();
|
||||
|
||||
const { html: codeHtml } = await parseCode();
|
||||
parsed.value = { html: codeHtml, displayLang: parsed.value!.displayLang };
|
||||
});
|
||||
|
||||
async function parseCode() {
|
||||
const shiki = await getShikiHighlighter();
|
||||
let lang = props.lang.toLowerCase();
|
||||
let displayLang = lang;
|
||||
|
||||
try {
|
||||
shiki.getLanguage(lang);
|
||||
const shikiLang = shiki.getLanguage(lang);
|
||||
displayLang = shikiLang.name;
|
||||
} catch {
|
||||
lang = 'text';
|
||||
}
|
||||
return shiki.codeToHtml(props.code.trim(), {
|
||||
|
||||
const html = shiki.codeToHtml(props.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);
|
||||
}
|
||||
|
||||
function collapseCode() {
|
||||
if (!codeBlockRef.value) return;
|
||||
|
||||
collapsed.value = !collapsed.value;
|
||||
if (collapsed.value) {
|
||||
codeHeight.value = codeBlockRef.value.scrollHeight;
|
||||
nextTick(() => {
|
||||
// since we are changing the height of an element, even though its
|
||||
// to its own height, we are triggering a reflow, which means that
|
||||
// if we didnt use requestAnimationFrame, the height would be set
|
||||
// to 0 before the reflow is complete, which would cause the reflow
|
||||
// to be ignored, and another one to be triggered with the new height
|
||||
// of zero, causing the codeblock to snap shut immediately rather than
|
||||
// animating. Not requestAnimationFrame because it doesnt work
|
||||
// on firefox, but setTimeout works on both chrome and firefox
|
||||
setTimeout(() => {
|
||||
codeHeight.value = 0;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
codeBlockRef.value!.addEventListener('transitionend', () => {
|
||||
if (collapsed.value) return;
|
||||
codeHeight.value = 'auto';
|
||||
}, { once: true })
|
||||
|
||||
const targetHeight = codeBlockRef.value.scrollHeight;
|
||||
codeHeight.value = targetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const codeStyle = computed(() => {
|
||||
if (typeof codeHeight.value === 'number') {
|
||||
return `height: ${codeHeight.value}px;`;
|
||||
} else {
|
||||
return `height: ${codeHeight.value};`;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (copyTimeout) clearTimeout(copyTimeout);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl overflow-hidden code-container" :style="`--line-number-width: ${lineNumberWidth}ch`"
|
||||
:id="`code-${renderId}`" v-html="html">
|
||||
<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-highlight)] justify-between">
|
||||
<div class="capitalize">
|
||||
{{ parsed?.displayLang }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="copyCode()"
|
||||
class="flex items-center px-1 gap-0.5 rounded-md hover:bg-[var(--color-highlight)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
Copy
|
||||
<Icon v-if="!copied" name="mynaui:copy" class="text-4 text-[var(--color-text-subtle)]" />
|
||||
<Icon v-else name="mynaui:check" class="text-4 text-emerald-500" />
|
||||
</button>
|
||||
|
||||
<button @click="collapseCode()"
|
||||
class="flex items-center justify-center h-5.5 w-5.5 rounded-md hover:bg-[var(--color-highlight)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Icon name="mynaui:chevron-down"
|
||||
:class="['text-4 h-4 w-4 text-[var(--color-text-subtle)] transition-transform duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]', collapsed ? '-rotate-90' : '']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="codeBlockRef"
|
||||
class="font-mono overflow-hidden code-container transition-height duration-300 ease-in-out"
|
||||
:style="`--line-number-width: ${lineNumberWidth}ch; ${codeStyle}`" :id="`code-${renderId}`"
|
||||
v-html="parsed?.html">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.code-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.code-container>pre {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
padding: 1rem;
|
||||
line-height: 1.625;
|
||||
line-height: 0;
|
||||
counter-reset: lines;
|
||||
}
|
||||
|
||||
@@ -67,4 +154,11 @@ async function parseCode() {
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user