Muya β a standalone Markdown editor for the web, extracted from MarkText.
Status: Muya is still under active development. APIs may change between minor versions and it is not yet recommended for production use.
- CommonMark + GFM blocks: paragraphs, ATX/Setext headings, bullet/ordered/task lists, code fences, tables, block quotes, horizontal rules, raw HTML, images.
- Inline formats with a floating toolbar, plus optional super/subscript, footnotes, front matter, and inline math.
- Diagrams & rich content: KaTeX math, Mermaid, Vega/Vega-Lite, PlantUML, and Prism syntax highlighting in code blocks.
- Markdown β HTML round-trip via
marked(read path) andturndown+joplin-turndown-plugin-gfm(write path).MarkdownToHtmlis exposed as a standalone utility. - Search and replace with regex support, plus undo/redo history.
- i18n out of the box: English, Chinese, and Japanese locales ship with the package.
- JSON state model built on
ot-json1/ot-text-unicodeβ wire it up to your own transport for collaborative editing. - TypeScript first β types ship in the package, no extra
@types/*install needed.
npm install @muyajs/core
# or
pnpm add @muyajs/coreMuya is a browser library and expects a bundler (Vite, webpack, Rollup, esbuild, β¦). The package ships ESM (lib/es), CJS (lib/cjs), UMD (lib/umd), and full TypeScript declarations (lib/types).
import {
CodeBlockLanguageSelector,
EmojiSelector,
ImageEditTool,
ImageResizeBar,
ImageToolBar,
InlineFormatToolbar,
Muya,
ParagraphFrontButton,
ParagraphFrontMenu,
ParagraphQuickInsertMenu,
PreviewToolBar,
TableColumnToolbar,
TableDragBar,
TableRowColumMenu,
zh,
} from '@muyajs/core';
import '@muyajs/core/lib/style.css';
// 1. Register the UI plugins you need (once, globally on the class).
Muya.use(EmojiSelector);
Muya.use(InlineFormatToolbar);
Muya.use(ImageToolBar);
Muya.use(ImageResizeBar);
Muya.use(ImageEditTool, {
imagePathPicker: async () => '/path/to/image.png',
imageAction: async () => 'https://example.com/uploaded.png',
});
Muya.use(CodeBlockLanguageSelector);
Muya.use(ParagraphFrontButton);
Muya.use(ParagraphFrontMenu);
Muya.use(ParagraphQuickInsertMenu);
Muya.use(TableColumnToolbar);
Muya.use(TableDragBar);
Muya.use(TableRowColumMenu);
Muya.use(PreviewToolBar);
// 2. Create the editor against an existing DOM node.
const container = document.querySelector('#editor') as HTMLElement;
const muya = new Muya(container, {
markdown: '# Hello, Muya',
});
// 3. Optional: switch the UI language before init.
muya.locale(zh);
// 4. Boot. Nothing renders until init() runs.
muya.init();A complete example, including a Vite project setup, lives under examples/.
The Muya instance returned from new Muya(el, options) exposes:
| Method | Purpose |
|---|---|
init() |
Mount the editor and instantiate registered UI plugins. |
locale(localeObject) |
Switch the UI locale. Use the bundled en, zh, ja exports or supply your own. |
getMarkdown() |
Serialize the current document to Markdown. |
getState() |
Return the underlying JSON state (the source of truth). |
setContent(content, autoFocus?) |
Replace the document with Markdown (string) or TState[]. |
undo() / redo() |
Step through the history stack. |
search(value, opts?) |
Run a search; opts includes { isRegexp, isCaseSensitive, isWholeWord, selectHighlight }. |
find('previous' | 'next') |
Move the active match. |
replace(value, { isSingle, isRegexp }) |
Replace the active match or all matches. |
selectAll() |
Select the entire document. |
on(event, fn) / off(event, fn) / once(event, fn) |
Subscribe to editor events. |
destroy() |
Tear down the editor and free DOM listeners. |
Useful events emitted on the editor:
| Event | Payload |
|---|---|
json-change |
OT operations describing the latest document mutation. The full state can be read back via muya.getState() or serialized to Markdown via muya.getMarkdown(). |
selection-change |
New selection ({ anchor, focus, path }). |
The full set of constructor options (font size, list defaults, math/footnote toggles, front matter delimiters, Mermaid/Vega themes, etc.) is described by IMuyaOptions in packages/core/src/types.ts; defaults live in MUYA_DEFAULT_OPTIONS in packages/core/src/config/index.ts.
Plugins are floating tools/menus that you opt into with Muya.use(Plugin, options?). They live under packages/core/src/ui/ and are exported from the package root:
| Plugin | What it does |
|---|---|
InlineFormatToolbar |
Bold / italic / link / etc. toolbar that follows the selection. |
EmojiSelector |
: trigger emoji picker. |
CodeBlockLanguageSelector |
Language picker inside fenced code blocks. |
ImageToolBar, ImageResizeBar, ImageEditTool |
Image-related affordances; ImageEditTool accepts imagePathPicker and imageAction callbacks for upload flows. |
ParagraphFrontButton, ParagraphFrontMenu |
The handle and menu that appear to the left of the active block. |
ParagraphQuickInsertMenu |
The / slash-command menu for inserting blocks. |
TableColumnToolbar, TableDragBar, TableRowColumMenu |
Table editing affordances. |
PreviewToolBar |
Tools shown over previewable blocks (math, Mermaid, etc.). |
examples/src/main.ts is the canonical reference for which plugins to register for a fully-featured editor.
Muya
βββ EventCenter custom pub/sub for editor-internal + user events
βββ Editor owns runtime modules and routes DOM events
β βββ JSONState ot-json1 document, source of truth
β βββ InlineRenderer custom lexer + snabbdom virtual DOM
β βββ Selection live selection bridged to the JSON path
β βββ Search regex search + highlight overlay
β βββ Clipboard paste/copy bridging via turndown / marked
β βββ History OT-aware undo/redo stack
β βββ ScrollPage root block of the tree
βββ Ui registry of floating tools/menus (toolbars, pickers)
βββ I18n locale dispatcher
The block tree under ScrollPage is built from TreeNode β Parent β (Content | Format). Each concrete block lives in packages/core/src/block/{commonMark,gfm,extra,content}/ and is registered in src/block/index.ts::registerBlocks(). Markdown serialization is handled in src/state/{markdownToState,stateToMarkdown,markdownToHtml,htmlToMarkdown}.ts.
Inline edits are encoded as ot-text-unicode operations nested inside ot-json1 operations, so the entire document, including inline runs, is OT-ready β connect your own transport and you have collaborative editing.
For a deeper, file-level walkthrough see CLAUDE.md.
This is a pnpm + Turborepo monorepo:
.
βββ packages/
β βββ core/ @muyajs/core β the published editor library
β βββ facade/ README-only stub (no source yet)
β βββ findReplace/ README-only stub (no source yet)
βββ examples/ muya-examples β Vite vanilla-TS demo, consumes core via workspace:*
βββ docs/ logo, roadmap, JSON state reference
βββ CLAUDE.md agent-oriented architecture and conventions guide
βββ CHANGELOG.md generated by release-it (angular conventional-changelog preset)
Engines: Node β₯18 for consumers, Node β₯20.19, β₯22.13, or β₯24 for cutting releases (the changelog plugin pins ^20.19.0 || ^22.13.0 || >=24.0.0), pnpm β₯8.5 (pinned to pnpm@10.22.0). Build target is chrome70.
pnpm install
pnpm dev # boots the examples Vite dev server (turbo dev:demo)Useful local commands (Turbo fans these out across packages):
| Command | What it runs |
|---|---|
pnpm build |
tsc && vite build in packages/core β emits lib/{es,umd,cjs,types}. |
pnpm test / pnpm coverage |
Vitest, with --passWithNoTests. |
pnpm lint / pnpm lint:fix |
ESLint (antfu config) over packages/. |
pnpm lint:types |
tsc --noEmit per package. |
pnpm lint:css |
Stylelint over all CSS. |
pnpm check-circular |
madge --circular against the public entry β CI enforces this. |
Commit messages must follow Conventional Commits (build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test); husky and commitlint enforce this. Pre-commit, lint-staged auto-fixes ESLint and Stylelint findings on touched files.
pnpm buildVite produces three formats (lib/es/index.js, lib/umd/index.js, lib/cjs/index.js) and vite-plugin-dts emits declarations to lib/types/. The publishConfig.exports map in packages/core/package.json is what npm consumers see after publish; the dev-time exports map points at src/index.ts so workspace consumers like examples/ can import the TypeScript source directly.
Releases of @muyajs/core are driven by release-it with @release-it-plugins/workspaces and @release-it/conventional-changelog (angular preset). From a clean master on Node β₯20.19 / β₯22.13 / β₯24 (matching the changelog plugin's ^20.19.0 || ^22.13.0 || >=24.0.0 engines pin β older 20.x / early 22.x will fail at preset load), with npm whoami showing an account that has write access to the @muyajs scope:
# 1. Quality gates (also runs in CI)
pnpm lint
pnpm lint:types
pnpm test
pnpm check-circular
# 2. Clean rebuild (lib/ is gitignored; never publish a stale build)
rm -rf packages/core/lib
pnpm build
# 3. Cut the release (bumps versions, writes CHANGELOG, commits/tags/pushes, publishes to npm)
pnpm release <version> # e.g. 0.1.0pnpm release will prompt for an npm 2FA one-time password by opening a browser auth flow during the pnpm publish step. If publishing breaks mid-flight (network/OTP/timeout) after the git tag is already pushed, retry just the upload β the version is already bumped:
pnpm --filter @muyajs/core publish --tag latest --access public --no-git-checksGitHub releases are created separately with the gh CLI (release-it's GitHub integration is disabled in .release-it.json to avoid needing a GITHUB_TOKEN env var):
VERSION=0.1.0
awk -v v="$VERSION" '$0 ~ "^# \\["v"\\]"{flag=1; next} /^## \[/{flag=0} flag' \
CHANGELOG.md > /tmp/release-notes.md
gh release create "v$VERSION" --title "v$VERSION" --notes-file /tmp/release-notes.mdSee docs/ROADMAP.md for the long-running roadmap (TypeScript migration, browser-compatibility goals, docs site, CI/CD, tests). Short-term release notes live in CHANGELOG.md.
What is the relationship between MarkText and Muya?
Muya is derived from MarkText. The team's goal is for Muya to live outside the desktop app so it can power web editors as well; the Electron-specific assumptions have been progressively peeled away.
Does the Muya version track MarkText's version?
No, the two version numbers are independent.
