Skip to content

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

bash
pnpm add @cocoar/vue-markdown @cocoar/vue-markdown-core

Then load the shared block stylesheet once at app entry, alongside @cocoar/vue-ui/styles:

css
/* 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:

vue
<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.

ts
import { parse } from '@cocoar/vue-markdown-core';

const doc = parse('# Title\n\nParagraph text.', { gfm: true });
OptionTypeDefaultDescription
gfmbooleanfalseEnable GitHub Flavored Markdown (tables, strikethrough, task lists)

serialize(doc, options?)

Convert a document tree back to a markdown string:

ts
import { serialize } from '@cocoar/vue-markdown-core';

const markdown = serialize(doc, { gfm: true });

transform(doc, ...transforms)

Apply transformations to the document tree:

ts
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

PropTypeDescription
docMarkdownDocumentPre-parsed markdown document
renderersMarkdownViewerRenderers(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:

vue
<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:

ts
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:

ts
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

MarkdownHTMLNotes
# 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
![alt](src)<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 are handled intelligently:

  • External (https://...): Opens in new tab with rel="noopener noreferrer"
  • Hash anchors (#section): Resolved relative to current page
  • Relative paths (./page): Local navigation

Node Types

ts
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:

css
/* 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);
}

Released under the Apache-2.0 License.