Vue Rendering Guide
Complete guide for rendering Markdown documents in Vue applications.
Table of Contents
- Basic Usage
- Custom Components
- Dynamic Component Resolution
- Slots Support
- Streaming Mode
- Prose Components
- Error Handling
- Props Access
Basic Usage
Use the Markdown component to render markdown:
<template>
<Markdown>{{ content }}</Markdown>
</template>
<script setup lang="ts">
import { Markdown } from '@comark/vue'
const content = `
# Hello World
This is **markdown** content.
::alert{type="info"}
Important message
::
`
</script>Custom Components
Map custom Vue components to Comark elements:
<template>
<Markdown :components="customComponents">{{ content }}</Markdown>
</template>
<script setup lang="ts">
import { Markdown } from '@comark/vue'
import CustomHeading from './CustomHeading.vue'
import CustomAlert from './CustomAlert.vue'
import CustomCard from './CustomCard.vue'
const customComponents = {
h1: CustomHeading,
h2: CustomHeading,
alert: CustomAlert,
card: CustomCard,
}
</script>Custom Component Example
<!-- CustomHeading.vue -->
<template>
<component :is="tag" :id="id" class="custom-heading">
<slot />
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
__node: any // Comark node
}>()
const tag = computed(() => props.__node[0])
const id = computed(() => props.__node[1]?.id)
</script>
<style scoped>
.custom-heading {
font-family: 'Inter', sans-serif;
font-weight: 700;
margin-bottom: 1rem;
}
</style>Alert Component Example
<!-- CustomAlert.vue -->
<template>
<div :class="`alert alert-${type}`" role="alert">
<div class="alert-icon">
<Icon :name="iconName" />
</div>
<div class="alert-content">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
type?: 'info' | 'warning' | 'error' | 'success'
__node?: any
}>()
const iconName = computed(() => {
switch (props.type) {
case 'info': return 'info-circle'
case 'warning': return 'exclamation-triangle'
case 'error': return 'times-circle'
case 'success': return 'check-circle'
default: return 'info-circle'
}
})
</script>
<style scoped>
.alert {
display: flex;
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.alert-info {
background-color: #e3f2fd;
color: #1976d2;
}
.alert-warning {
background-color: #fff3e0;
color: #f57c00;
}
</style>Dynamic Component Resolution
Load components dynamically using componentsManifest:
<template>
<Markdown
:components-manifest="loadComponent"
>{{ content }}</Markdown>
</template>
<script setup lang="ts">
import { Markdown } from '@comark/vue'
const componentMap = {
'alert': () => import('./Alert.vue'),
'card': () => import('./Card.vue'),
'button': () => import('./Button.vue'),
}
async function loadComponent(name: string) {
if (componentMap[name]) {
return componentMap[name]()
}
throw new Error(`Component ${name} not found`)
}
</script>Slots Support
Comark components with slots work seamlessly in Vue:
Markdown with Slots
::card
#header
## Card Title
#content
Main content here with **markdown** support
#footer
Footer text
::Custom Component with Slots
<!-- Card.vue -->
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-content">
<slot name="content" />
<!-- Default slot as fallback -->
<slot />
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
<style scoped>
.card {
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
}
.card-header {
background-color: #f9fafb;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.card-content {
padding: 1rem;
}
.card-footer {
background-color: #f9fafb;
padding: 1rem;
border-top: 1px solid #e5e7eb;
}
</style>Nested Slots
::tabs
#tab1
### First Tab
Content for tab 1
#tab2
### Second Tab
Content for tab 2
::<!-- Tabs.vue -->
<template>
<div class="tabs">
<div class="tab-headers">
<button
v-for="(slot, name) in $slots"
:key="name"
:class="{ active: activeTab === name }"
@click="activeTab = name"
>
{{ name }}
</button>
</div>
<div class="tab-content">
<component :is="() => $slots[activeTab]?.()" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const activeTab = ref('tab1')
</script>Streaming Mode
The Markdown component can be used with reactive content for streaming scenarios:
<template>
<div>
<Markdown>{{ content }}</Markdown>
<div v-if="isLoading">Loading...</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Markdown } from '@comark/vue'
const content = ref('')
const isLoading = ref(true)
async function loadContent() {
const response = await fetch('/api/content.md')
const reader = response.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
content.value += decoder.decode(value)
}
isLoading.value = false
}
loadContent()
</script>Prose Components
The Markdown component uses built-in prose styling automatically. You can override with custom components:
<template>
<Markdown :components="components">{{ content }}</Markdown>
</template>
<script setup lang="ts">
import { Markdown } from '@comark/vue'
import CustomAlert from './CustomAlert.vue'
const components = {
alert: CustomAlert, // Override or add custom components
}
</script>Error Handling
The MarkdownDocument component has built-in error capture via Vue's onErrorCaptured hook. Component rendering errors are caught automatically without crashing the application. You can also use Vue's native onErrorCaptured in a parent component to handle errors:
<template>
<Markdown :value="content" />
</template>
<script setup lang="ts">
import { onErrorCaptured } from 'vue'
import { Markdown } from '@comark/vue'
onErrorCaptured((error) => {
console.error('Component error:', error)
return false // prevent propagation
})
</script>Props Access
Custom components receive the original Comark node and parsed props:
<!-- CustomAlert.vue -->
<template>
<div :class="alertClasses" role="alert">
<slot />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
type?: string // From {type="info"}
bool?: boolean // From {bool} → :bool="true"
count?: number // From {:count="5"}
data?: object // From {:data='{"key":"val"}'}
__node?: any // Original Comark node
}>()
const alertClasses = computed(() => [
'alert',
`alert-${props.type || 'info'}`,
{ 'alert-important': props.bool }
])
</script>Property Parsing Rules
- Props starting with
:are parsed as booleans/JSON - Standard HTML attributes work normally
__nodeprovides access to the raw AST node
Accessing Node Structure
<script setup lang="ts">
const props = defineProps<{ __node?: any }>()
// Node structure: [tag, props, ...children]
const tag = computed(() => props.__node?.[0])
const nodeProps = computed(() => props.__node?.[1] || {})
const children = computed(() => props.__node?.slice(2) || [])
</script>Working with Complex Props
<!-- DataTable.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col">{{ col }}</th>
</tr>
</thead>
<tbody>
<slot />
</tbody>
</table>
</template>
<script setup lang="ts">
const props = defineProps<{
columns?: string[] // From {:columns='["Name","Age"]'}
sortable?: boolean // From {sortable}
__node?: any
}>()
</script>Usage in Markdown:
::data-table{:columns='["Name", "Age", "Email"]' sortable}
Table content here
::