Custom Code Block
How to build copy buttons, collapse thresholds, and other code-block UI when Comark does not pass a raw `code` prop.
Comark stores fenced code under a <pre><code> subtree instead of duplicating it as a <pre> attribute. Syntax highlighting plugins such as Shiki and Rangi replace the code text with tokenized span nodes, which renderers expose through the default slot.
If you are migrating from MDC / Nuxt Content, a custom
ProsePre that did (props.code || '').split('\n') for copy or collapse will break. Reconstruct the source from the AST instead.What <pre> receives
Fence metadata is still on the node attrs and becomes component props:
| Prop | Origin |
|---|---|
language | fence info string |
filename | [name] in the fence |
highlights | {1,3-5} line highlights |
meta | leftover fence meta |
class | highlighter classes added by Shiki or Rangi |
style | optional Shiki or Rangi output when preStyles is enabled |
There is no code or lines prop. The <code> subtree lives in the default slot and remains available on the full AST node.
Pattern: __node + textContent
@comark/vue injects the full element node when your component declares a __node prop. Flatten it with textContent from comark/utils to recover the code contents without the fence or its final newline:
components/ProsePre.vue
<script setup lang="ts">
import { computed } from 'vue'
import { textContent } from 'comark/utils'
import type { ElementNode } from 'comark'
defineOptions({ inheritAttrs: false })
const props = defineProps<{
__node?: ElementNode
language?: string
filename?: string
}>()
const COLLAPSE_THRESHOLD = 15
const code = computed(() => (props.__node ? textContent(props.__node) : ''))
const lines = computed(() => code.value.split('\n').length)
const isLong = computed(() => lines.value > COLLAPSE_THRESHOLD)
async function copy() {
await navigator.clipboard.writeText(code.value)
}
</script>
<template>
<div class="relative group">
<button type="button" @click="copy">Copy</button>
<div :class="isLong && 'max-h-[360px] overflow-hidden'">
<pre
v-bind="$attrs"
:language="props.language"
:filename="props.filename"
><slot /></pre>
</div>
</div>
</template>This works during SSR, stays in sync when __node changes and does not depend on the rendered DOM.
In React, TypeScript prop declarations are erased at runtime. Accept
__node in your component props and add a runtime marker with ProsePre.propTypes = { __node: () => null } so @comark/react injects the node, then call textContent(__node).