@psray/markdown-editor
v0.2.1
Published
A self-contained Markdown editor and renderer for Nuxt 4 — CodeMirror 6 authoring, markdown-it rendering with callout/checklist/spoiler blocks, and built-in translations.
Maintainers
Readme
@psray/markdown-editor
A Markdown editor and renderer for Nuxt 4, packaged as a module. CodeMirror 6 for authoring, markdown-it for rendering, and a set of site-safe content blocks — callouts, collapsible details, checklists with a live completion count, block and inline spoilers, colored text, colored underlines and sized images.
Translations for Japanese, English, Korean, Simplified and Traditional Chinese
ship with the package, so it works with or without @nuxtjs/i18n.
Install
pnpm add @psray/markdown-editor// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@psray/markdown-editor'],
})Then point Tailwind at the package. Tailwind v4 skips node_modules, so without
this line the editor's own utility classes are never generated and the toolbar
renders unstyled:
/* app/assets/css/main.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@source "../../../node_modules/@psray/markdown-editor/dist";Adjust the relative path to reach your project root's node_modules. Tailwind
v4 and @tailwindcss/typography are peer dependencies — the package styles
itself from your theme rather than shipping a competing one.
Use
<script setup lang="ts">
const content = ref('')
</script>
<template>
<MarkdownEditor v-model="content" :disabled="saving" :error="error" />
<RichContent :content="content" format="markdown" />
</template>MarkdownEditor is the split-pane authoring surface; RichContent renders
stored Markdown (or trusted HTML) for readers. MarkdownGuide is the syntax
guide the editor's help banner links to.
The editor is heavy — CodeMirror and its Markdown grammar — so mount it as
<LazyMarkdownEditor> when it lives behind a dialog or a tab.
Components
| Component | Props |
| --- | --- |
| MarkdownEditor | v-model (required), disabled, error, placeholder, guideUrl |
| RichContent | content, format: 'markdown' \| 'html' |
| MarkdownGuide | brand, imageSrc, imageAlt |
Auto-imported helpers
renderMarkdown, markdownHeadings, markdownContainers,
markdownContainerSnippet, applyMarkdownEdit, applyMarkdownTextStyle,
MARKDOWN_TEXT_COLORS, MARKDOWN_TEXT_SIZES, MARKDOWN_UNDERLINE_COLORS.
renderMarkdown runs the same markdown-it instance the preview uses, so
server-rendered content and the live preview can never drift.
Headings and tables of contents
Every heading gets an id slugged from its text, numbered slug-1, slug-2
on repeats. Slugs keep letters from every script, so Japanese, Korean and
Chinese headings stay linkable. They are computed from the source alone, so a
#fragment link works on the server-rendered HTML rather than only after
hydration.
markdownHeadings(source) returns those same headings as
{ id, text, level }[] — build a contents list from it rather than from a
second Markdown parser, which is how a contents list ends up linking to
anchors that do not exist:
<script setup lang="ts">
const toc = computed(() => markdownHeadings(article.value.content))
</script>
<template>
<a v-for="item in toc" :key="item.id" :href="`#${item.id}`">{{ item.text }}</a>
<RichContent :content="article.content" format="markdown" />
</template>Options
export default defineNuxtConfig({
modules: ['@psray/markdown-editor'],
markdownEditor: {
components: true, // register the three components globally
prefix: '', // e.g. 'Psray' → <PsrayMarkdownEditor>
css: true, // inject the package stylesheet
defaultLocale: 'ja', // used when there is no i18n module, and as the last fallback
guideUrl: '/docs/markdown', // the editor's help banner target; false hides it
guidePage: false, // set a path to mount MarkdownGuide as a route
fallbackMessages: 'auto', // bundle defaultLocale's messages as a safety net
i18n: {
enabled: true,
localeMap: { 'zh-CN': 'zh-Hans' },
},
},
})Translations
With @nuxtjs/i18n installed, the package merges its catalogue into yours under
the markdown.* namespace, for every locale code you have configured that it has
a translation for. Your own catalogue takes precedence, so redefining a key
rewords the editor:
{ "markdown": { "editor": { "help": "How to write a guide" } } }If you spell a language differently, map it:
markdownEditor: { i18n: { localeMap: { 'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant' } } }Locale codes the package has no translation for — and hosts with no i18n module
at all — fall back to the bundled defaultLocale messages rather than rendering
raw keys. Those messages are only shipped when they could actually be reached:
a host running @nuxtjs/i18n whose locales this package fully covers pays
nothing for them. Force the decision either way with fallbackMessages.
Syntax
Everything CommonMark supports, plus:
::: info Before you begin
Requires chapter 3.
:::
::: checklist Collectibles
- [x] Found
- [ ] Missing
:::
::: details Hidden by default
Body text.
:::
::: spoiler Ending
Body text.
:::
::: steps
1. First
2. Second
:::
Inline spoiler: ||hidden text||
Highlight: ==marked==
Color, size, underline: [text]{color=red size=large underline=blue}
Image sizing: {width=320}Containers also nest — open the outer one with ::::.
html: false is set on the renderer and every attribute goes through a fixed
allow-list, so community-authored Markdown cannot inject elements or styles.
Links are rendered target="_blank" rel="noopener noreferrer".
Development
pnpm install
pnpm dev # playground at localhost:3000
pnpm test
pnpm buildThe playground deliberately ships no locale files of its own, so every string on
screen comes from the module's catalogue — which is the only honest test of the
i18n:registerModule wiring. It also configures zh-CN (exercising
localeMap) and fr (exercising the bundled fallback).
