Markdown
Render markdown content with Cocoar Design System styling. The system is split into two packages: a framework-agnostic parser and a Vue component.
Separate Packages
pnpm add @cocoar/vue-markdown @cocoar/vue-markdown-coreThen load the shared block stylesheet once at app entry, alongside @cocoar/vue-ui/styles:
/* app/main.css */
@import "@cocoar/vue-ui/styles";
@import "@cocoar/vue-markdown/styles";The same stylesheet is consumed by @cocoar/vue-markdown-editor — viewer and editor render identical output for every node type.
Quick Start
Parse a markdown string, then render it:
<template>
<CoarMarkdown :doc="doc" />
</template>
<script setup lang="ts">
import { parse } from '@cocoar/vue-markdown-core';
import { CoarMarkdown } from '@cocoar/vue-markdown';
const doc = parse(`
# Hello World
This is **bold** and *italic* text with a [link](https://example.com).
- Item one
- Item two
- Item three
`, { gfm: true });
</script>Parsing (@cocoar/vue-markdown-core)
parse(markdown, options?)
Converts a markdown string into a MarkdownDocument tree.
import { parse } from '@cocoar/vue-markdown-core';
const doc = parse('# Title\n\nParagraph text.', { gfm: true });| Option | Type | Default | Description |
|---|---|---|---|
gfm | boolean | false | Enable GitHub Flavored Markdown (tables, strikethrough, task lists) |
serialize(doc, options?)
Convert a document tree back to a markdown string:
import { serialize } from '@cocoar/vue-markdown-core';
const markdown = serialize(doc, { gfm: true });transform(doc, ...transforms)
Apply transformations to the document tree:
import { parse, transform, type MarkdownTransform } from '@cocoar/vue-markdown-core';
const addPrefix: MarkdownTransform = (doc) => ({
...doc,
nodes: doc.nodes.map(node => {
if (node.type === 'heading') {
return { ...node, text: `[Docs] ${node.text}` };
}
return node;
}),
});
const doc = transform(parse(markdown), addPrefix);Rendering (@cocoar/vue-markdown)
CoarMarkdown
| Prop | Type | Description |
|---|---|---|
doc | MarkdownDocument | Pre-parsed markdown document |
renderers | MarkdownViewerRenderers | (optional) Per-instance renderer override. See Custom renderers below. |
Custom renderers (registry)
Every node type — headings, paragraphs, code blocks, tables, lists, even inline marks like <em> — is rendered by a swappable Vue component. The package exports the full default registry so you can override just one slot while keeping the rest of the Cocoar look:
<script setup lang="ts">
import { CoarMarkdown, defaultMarkdownRenderers } from '@cocoar/vue-markdown';
import MyHighlightedCodeBlock from './MyHighlightedCodeBlock.vue';
const renderers = {
...defaultMarkdownRenderers,
codeBlock: MyHighlightedCodeBlock, // Swap just the code-block slot
};
</script>
<template>
<CoarMarkdown :doc="doc" :renderers="renderers" />
</template>For app-wide overrides, provide the registry once at startup:
import { MARKDOWN_RENDERERS_KEY, defaultMarkdownRenderers } from '@cocoar/vue-markdown';
app.provide(MARKDOWN_RENDERERS_KEY, {
...defaultMarkdownRenderers,
codeBlock: MyHighlightedCodeBlock,
});Resolution order: per-instance prop → app-level inject → built-in defaults.
Renderer contract
Each renderer receives:
interface MarkdownRendererProps {
/** The AST node currently being rendered. */
node: MarkdownNode;
/** Recursive child renderer — call to render `node.children`. */
renderChildren: () => VNode[];
/** Render an arbitrary list of nodes through the registry. Used by `DefaultTable`
* to render each cell's inline content while keeping the `<thead>/<tbody>` shape
* under the renderer's control. */
renderNodes: (nodes: readonly MarkdownNode[]) => VNode[];
}A custom renderer is a regular Vue component that emits the right semantic HTML for its node type. Use renderChildren() for the typical "wrap children in a tag" case; reach for renderNodes(...) only when the rendered structure isn't a flat children list (the GFM table is the canonical example).
Why the registry matters
The same registry is consumed by @cocoar/vue-markdown-editor. Overriding codeBlock in your app's provide flips the rendering both in the viewer and in the editor's render mode (the cursor-out state of the in-editor code block). Output stays in sync without any duplicated wiring.
Supported Elements
| Markdown | HTML | Notes |
|---|---|---|
# Heading | <h1> - <h6> | With anchor IDs |
**bold** | <strong> | |
*italic* | <em> | |
`code` | <code> | Inline code |
| Code blocks | <CoarCodeBlock> | With language highlighting |
[text](url) | <a> | External links open in new tab |
 | <img> | Lazy loaded |
> quote | <blockquote> | Styled with left border |
| Lists | <ul> / <ol> | Including task lists |
| Tables | <CoarTable> | GFM tables with alignment |
~~strike~~ | <del> | GFM strikethrough |
--- | <hr> | Thematic break |
Links
Links are handled intelligently:
- External (
https://...): Opens in new tab withrel="noopener noreferrer" - Hash anchors (
#section): Resolved relative to current page - Relative paths (
./page): Local navigation
Node Types
type MarkdownNodeType =
| 'heading' | 'paragraph' | 'blockquote'
| 'list' | 'listItem'
| 'codeBlock' | 'table' | 'tableRow' | 'tableCell'
| 'thematicBreak' | 'lineBreak'
| 'text' | 'emphasis' | 'strong' | 'strikethrough'
| 'inlineCode' | 'link' | 'image';
interface MarkdownNode {
id: string;
type: MarkdownNodeType;
children?: readonly MarkdownNode[];
text?: string;
attrs?: Record<string, unknown>;
position?: MarkdownPosition;
}
interface MarkdownDocument {
nodes: readonly MarkdownNode[];
}Theming
The markdown component uses CSS custom properties for theming:
/* Override in your app */
.coar-markdown {
--coar-markdown-text: var(--coar-text-neutral-primary);
--coar-markdown-link: var(--coar-text-accent);
--coar-markdown-border: var(--coar-border-neutral);
}