用于 Web 浏览器应用程序开发的未来 Markdown 编辑器
[!IMPORTANT] This repository has been migrated into marktext/marktext.
Muya now lives inside the MarkText monorepo. To use Muya or contribute code, please head over to the marktext/marktext repository. This repo will no longer receive updates. Thank you!
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.
packages/core/test/spec/conformance.md).[^id] references and [^id]: … definitions render with an interactive footnote tool and backref anchors on export.[text][ref] / ![alt][ref] resolve from [ref]: url "title" definitions on both render and round-trip.codeBlockLineNumbers: true to render gutters alongside fenced/indented code (and previewable blocks).marked (read path) and turndown + joplin-turndown-plugin-gfm (write path). MarkdownToHtml is exposed as a standalone utility; output passes through DOMPurify (sanitizeHyperlink + isValidAttribute) for XSS-safe rendering.ot-json1 / ot-text-unicode — wire it up to your own transport for collaborative editing.@types/* install needed.npm install @muyajs/core
# or
pnpm add @muyajs/core
Muya 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).
…
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 one of the bundled exports (en, zhCN, zhTW, ja, ko, es, fr, de, pt) 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. |
getTOC() |
Snapshot the current heading outline as Array. |
on(event, fn) / off(event, fn) / once(event, fn) |
Subscribe to editor events. |
destroy() |
Tear down the editor and free DOM listeners. |
Standalone utilities exported from the package root:
| Export | Purpose |
|---|---|
MarkdownToHtml |
Server-safe Markdown → HTML class (new MarkdownToHtml(md).generate()). |
renderToStaticHTML(stateOrMarkdown, opts?) |
One-shot static HTML renderer; pass { sanitize: false } only for trusted input (parser conformance tests use this). |
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 }). |
focus / blur |
Fired when the contenteditable surface gains or loses focus. |
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. |
LinkTools |
Hover toolbar over native ``, markdown links, and reference links. Takes a jumpClick callback to control jump-out behavior. |
FootnoteTool |
Floating popover for editing footnote definitions; requires the footnote: true editor option. |
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.
…
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 [email protected]). 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 build
Vite 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 # e.g. 0.1.0
pnpm 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-checks
GitHub 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.md
v0.2.0 (in flight) — the marktext-muya backport batch. 22 PRs (#208–#230) ported the upstream marktext muya tree onto @muyajs/core end-to-end:
LinkTools, getTOC() public API, focus / blur events, code block line numbers, image small-image + inline resize-bar suppression.pnpm --filter @muyajs/core test:spec) with a locked baseline (87.7% / 86.3%) and a regression gate via expected-failures.json.stateToMarkdown serialization baseline, clipboard/paste/copy corrections, editor cursor / IME / autopair / table navigation fixes, EventCenter listener-leak + once-iteration fix.暂无开放 Issues,或尚未同步最近议题。