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:

PropOrigin
languagefence info string
filename[name] in the fence
highlights{1,3-5} line highlights
metaleftover fence meta
classhighlighter classes added by Shiki or Rangi
styleoptional 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).

See also

Shiki plugin

Themes, line highlights, and fence meta that become pre props.

Migration from MDC

Package and prose-component changes when moving off @nuxtjs/mdc.