首页
/ MarkText 中 @muyajs/core 包全景解析:TS 版 muya 引擎的架构、命令与合规基线

MarkText 中 @muyajs/core 包全景解析:TS 版 muya 引擎的架构、命令与合规基线

2026-09-04 22:03:49作者:仰钰奇

本文以 packages/muya/CLAUDE.md 为骨架,系统介绍 MarkText monorepo 中 TypeScript 版 muya 编辑器引擎(@muyajs/core)的目录布局、完整命令清单、分层架构(插件系统、Editor 调度、块树、状态往返、内联渲染、UI 层)、工具链强制的代码约定以及构建管线细节,帮助读者掌握该引擎的接入方式与底层实现原理。

包定位:从 JS 到 TypeScript 的迁移背景

packages/muya 是 muya 编辑器引擎的 TypeScript 重写版本,已迁入 MarkText monorepo,发布名为 @muyajs/core。从 package.json 可以确认:包名 @muyajs/core,当前版本 0.2.0,依赖中包含 ot-json1ot-text-unicodemarkedsnabbdomkatexmermaidvega 等核心库,engines 要求 Node ≥ 20.19。

关键的架构事实是:桌面端渲染进程现在消费 @muyajs/core 作为编辑器引擎;遗留的 JS 引擎 packages/muyajs@marktext/muyajsmuya/ 别名)正在退役,仓库中只剩极少数调用点仍引用它。

另一个重要边界:MarkText 根目录的 ESLint 配置显式忽略了 packages/muya/**,因此该包拥有完全独立的工具链(ESLint/antfu 风格、stylelint、madge、vitest),应被视为一个自包含的包,遵循自己的规范而非根目录规范。上游 muya monorepo 中的桩包(packages/facadepackages/findReplace)因没有源码而未迁移。

目录布局

packages/muya 内部布局如下(源自 CLAUDE.md,已逐一在仓库中核对):

  • src/@muyajs/core 的 TypeScript 源码,公开 API 入口为 src/index.ts
  • test/spec/ — CommonMark / GFM 规范合规测试套件,通过 test:spec 命令运行,使用独立的 vitest 配置 vitest.spec.config.ts
  • examples/muya-examples,一个基于 Vite 的原生 TS 演示工程,通过 workspace:* 消费 @muyajs/core,在仓库根 pnpm-workspace.yaml 中列为独立 workspace;
  • e2e/muya-e2e,Playwright 真浏览器 E2E 套件,自带 e2e/host/ 宿主页面,配套 e2e/README.mde2e/BACKLOG.md
  • eslint.config.mjs.stylelintrc.madgerc — 包级工具链配置。

src/ 的实际目录结构看,源码组织与文档描述完全一致:block/(base、commonMark、gfm、extra、content、mixins、scrollPage)、state/inlineRenderer/ui/selection/history/clipboard/event/i18n/locales/ 等模块齐备。

命令清单(在 MarkText 仓库根执行)

以下命令均须从 MarkText 仓库根目录运行(对应 package.jsonscripts 字段):

# 启动 examples 的 Vite dev server(上游的 pnpm dev / Turbo dev:demo 未在此接线,直接跑 vite)
pnpm -C packages/muya/examples dev:demo

# 构建:tsc && vite build,产出 lib/{es,umd,cjs} 与 lib/types
pnpm -C packages/muya build

# Vitest 单元测试(测试与源码同置于 src/**/__tests__/ 下)
pnpm -C packages/muya test
pnpm -C packages/muya coverage

# 单测单文件
pnpm -C packages/muya exec vitest run path/to/file.test.ts

# CommonMark 0.31 + GFM 0.29-gfm 规范合规套件
pnpm -C packages/muya test:spec
pnpm -C packages/muya test:spec:commonmark
pnpm -C packages/muya test:spec:gfm

# Lint 系列
pnpm -C packages/muya lint          # ESLint,覆盖 src test(antfu 配置)
pnpm -C packages/muya lint:fix
pnpm -C packages/muya lint:types    # tsc --noEmit
pnpm -C packages/muya lint:css      # Stylelint,覆盖 src/**/*.css

# 循环依赖检查(CI 强制执行)
pnpm -C packages/muya check-circular   # madge --circular src/index.ts

# Playwright E2E(chromium/firefox/webkit)
pnpm -C packages/muya/e2e e2e         # e2e:install 为一次性浏览器安装

环境与构建目标约束:Node ≥ 20.19(与 MarkText 根一致),构建目标为 chrome70——这一点在 vite.config.tsbuild.target 中可以直接核实。

合规套件的"只升不降"机制

test:spec 是理解这个包质量保障哲学的关键。合规测试针对 renderToStaticHTML(..., { sanitize: false }) 运行——注意它测量的是解析器的规范符合度,而不是 DOMPurify 净化器(后者正确地非常激进,会剥掉 raw-HTML 放行类用例)。

expected-failures.json 锁定了当前失败用例的编号:任何已列入名单的用例一旦开始通过,套件就会失败(你必须把它从名单中移除);任何未列入名单的用例一旦开始失败,套件同样失败。净效果是:合规度只能升,不能降

基线数据记录在 test/spec/conformance.md(PR-6a 时点):

套件 通过 总数 通过率
CommonMark 0.31 572 652 87.7%
GFM 0.29-gfm 580 672 86.3%

从分章节数据看,CommonMark 套件中"强调与加粗"(Emphasis and strong emphasis)达到 100%(132/132)、代码片段(Code spans)100%,而"制表符"(Tabs)仅 9.1%(1/11)、"实体与数字字符引用"29.4%,是主要失分点。

对比判定的实现细节值得一读:test/spec/runner.ts 手工实现了一个 normalizeHtml 归一化器——统一自闭合标签写法(<br><br />)、按字母序重排属性、折叠相邻标签之间的空白——目的是消除 cmark 与 marked 之间那些语义等价但书写不同的 HTML 差异,同时刻意保留标签内部的文本内容(<pre><code> 中的换行是围栏代码块用例依赖的)。这个"能大声喊话的距离"(within shouting distance of the spec)定位,是该套件自述的设计标准。

架构解析

入口与插件系统

src/muya.ts 导出 Muya 类。UI 插件通过静态方法 Muya.use(Plugin, options) 全局注册,在 muya.init() 中实例化;插件以 Plugin.pluginName 为键,存储在实例的 _uiPlugins 上。源码注释明确了插件契约:

export interface IMuyaPluginConstructor {
    pluginName: string;
    new(muya: Muya, options: Record<string, unknown>): unknown;
}

examples/src/main.ts 中的插件集合是接线工具栏、选择器、菜单的标准参考。实际示例展示了完整的接线方式:Muya.use(InlineFormatToolbar)Muya.use(ImageEditTool, { imagePathPicker, imageAction })Muya.use(LinkTools, { jumpClick }) 等十余个 UI 插件,随后 new Muya(container, { markdown, ...options })m.locale(...)m.init() 完成生命周期(见 examples/src/main.ts)。

构造函数的行为(src/muya.ts 第 149–158 行):new Muya(element, options) 先用 getContainer 把传入的 DOM 元素替换为一个 contenteditable 新 div,然后依次构造 EventCenterEditorUiI18n 四个核心组件。在 muya.init() 执行 Editor.init() 之前什么都不渲染——Editor.init() 调用 registerBlocks() 并创建根 ScrollPage

Editor:运行时调度中枢

src/editor/index.ts 中的 Editor 持有全部运行时模块:JSONStateInlineRendererSelectionSearchClipboardHistory 以及根 ScrollPage。它负责维护 activeContentBlock(当前聚焦的叶子块),并将 DOM 事件(clickinputkeydownkeyupcompositionstart/end)通过 RxJS merge 合并后路由到当前块的对应处理器(clickHandlerinputHandler 等)。任何监听块上用户输入的代码最终都经过这条分发链。

OT 应用机制是这里的精髓:Editor.updateContents(operations, selection, source)ot-json1 操作应用到活的块树上。源码中的 pick/drop 游标(第 109–124 行起的 descend/restore/pick/drop 系列函数)是手工改写自 ot-json1.apply 的版本,目的是在遍历操作路径时能实际调用块树实例方法:block.replaceWithcontainer.insertBeforeScrollPage.loadBlock(name).create(...)、以及对匹配子文档调用 otText.type.apply——由此保证块树与 JSON 状态始终同步(in lockstep)。

块树:注册制 + 类继承

所有块继承 TreeNode → Parent → (Content | Format)(位于 src/block/base/)。Parent 拥有一个 childrenLinkedList 和一个 attachments 列表(存放不参与状态的节点,如图标、复选框);Content 是持有实际文本的叶子;FormatContent 基础上增加内联格式处理。

具体块类分布在 src/block/{commonMark,gfm,extra,content} 下,必须registerBlocks() 中注册。从 src/block/index.ts 可以看到完整的注册清单:ScrollPage 先注册自己,然后依次注册 Paragraph、AtxHeading、SetextHeading、BlockQuote、ThematicBreak、CodeBlock、OrderList、BulletList、TaskList(含 TaskListItem/TaskListCheckbox)、Table(Table/Inner/Row/Cell/CellContent)、HTML(Block/Preview/Container)、Math(Block/Preview/Container)、Frontmatter、Diagram(Block/Container/Preview)、Footnote 等。

查找链路是:ScrollPagesrc/block/scrollPage/index.ts)维护一个静态 registeredBlocks 映射,所有加载走 ScrollPage.loadBlock(blockName).create(muya, state)新增块类型的铁律:不在此注册,loadBlock 只会告警并返回 undefined。

另外,block/mixins/{containerQueryBlock,leafQueryBlock}.ts 是应用于块类的构造器 mixin,提供 queryBlock/路径解析能力(ROADMAP 注明这是从属性 mixin 有意切换而来的)。

状态层与 Markdown 往返

src/state/ 目录是文档模型的所在地:

  • JSONStatestate/index.ts)是文档的唯一事实来源(source of truth),暴露 ot-json1invert/compose/transform 静态方法——从源码结构看,整套架构已为 OT 协同编辑做好准备,只是尚未接入任何传输层;
  • markdownToState.tsmarked 把 Markdown 解析为状态树;stateToMarkdown.ts 反向序列化;markdownToHtml.tshtmlToMarkdown.ts(使用 turndown + joplin-turndown-plugin-gfm)负责 HTML 桥接。MarkdownToHtml 从公开 API 再导出(在 src/index.ts 中可见);
  • 行内文本编辑被编码为嵌套在 json1 操作之内的 ot-text-unicode 操作(见 Editor.updateContentsd.es 分支)。

两条容易被忽略的实现细节(来自 CLAUDE.md,且与源码一致):

  1. 引用式链接/图片定义[ref]: url "title"不是状态中的一等块类型。markdownToStatecase 'def' 会把原始定义行重新放进一个 paragraph 状态节点,使其能通过 Markdown 序列化无损往返。而 InlineRenderer.collectReferenceDefinitions() 在每次渲染遍历时遍历活块树,填充一个 labels Map,供 lexer 展开 [text][ref]![alt][ref] 时查询。ILinkReferenceDefinitionState 只是兼容用的废弃桩——不要编写产生它的新代码路径。
  2. TOC 按需派生:getTOC(muya)state/getTOC.ts),公开方法为 muya.getTOC(),slug 生成沿用自 commit 9cb2cbe8 的 marktext 兼容正则(src/utils/slug.ts 导出 generateGithubSlug)。

内联渲染与 DOM

src/inlineRenderer/ 用自研的 lexer/rules 管线对行内内容分词,通过 snabbdom 渲染到虚拟 DOM(序列化则用 snabbdom-to-html)。公式、代码高亮与图表分别由 KaTeX、Prism、Mermaid、Vega/Vega-Lite、PlantUML 承担——这些依赖在 package.jsondependencies 中可以逐一核对。

UI 层

src/ui/ 下每个子目录都是一个浮动工具/菜单(行内格式工具栏、图片工具、段落前置按钮、表格工具、表情选择器等),继承自 baseFloatbaseScrollFloat,使用 @floating-ui/dom 定位。它们从 src/index.ts 导入并再导出,由宿主通过 Muya.use(...) 注册。Uisrc/ui/ui.ts)是编辑器与之对话的注册表。

公开 API 面

src/index.ts 是唯一的发布入口:package.jsonexports 映射在开发期把 . 指向 ./src/index.ts,发布后指向 ./lib/es/index.js(见 package.jsonpublishConfig.exports)。这个文件应保持为唯一的导出中枢。当前公开导出包括:Muya 类、MarkdownToHtmlrenderToStaticHTMLTState/IMuyaOptions 类型、全部 10 个 locale(enzhCNzhTWja 等)、20 个 UI 插件类,以及 escapeHTML/sanitize/unescapeHTML/wordCount/generateGithubSlug/getImageInfo 等工具函数。

外观契约(排版选项与 CSS 自定义属性)

muya 支持两种等价的方式设定自身内容的排版:传入 options,或直接覆盖 CSS 自定义属性(纯 CSS 主题化)。变量设置在编辑器根节点(.mu-editor)上,由捆绑样式表消费;每个变量都有 CSS 内置默认值,因此什么都不传就渲染独立默认样式。

选项(IMuyaOptions CSS 变量 默认值 作用范围
fontSize(number, px) --mu-font-size 16px .mu-editor 基础文本
lineHeight(number) --mu-line-height 1.6 .mu-editor 基础文本
editorFontFamily(string) --mu-font-family Open Sans 字体栈 .mu-editor 基础文本
codeFontSize(number, px) --mu-code-font-size 90% .mu-code-block
codeFontFamily(string) --mu-code-font-family DejaVu Sans Mono 字体栈 .mu-code-block
wrapCodeBlocks(boolean) —(.mu-code-wrap 根类) 关闭(pre 代码块换行

这些默认值可直接在 blockSyntax.css 中核实:font-size: var(--mu-font-size, 16px)font-family: var(--mu-font-family, 'Open Sans', ...)line-height: var(--mu-line-height, 1.6),代码块处为 var(--mu-code-font-size, 90%)

三个边界约束需要注意:

  1. 行内代码(code.mu-inline-rule刻意不受上述变量驱动——它保持相对尺寸 0.8em / 等宽字体;
  2. 编辑区宽度(--editor-area-width)与配色盘(--editor-color-*)是宿主负责的另一套既有契约;
  3. 所有运行时变更统一走 muya.setOptions({...})

此外从 muya.ts 源码可以看到一个与选项相关的实用机制:PARSE_AFFECTING_OPTIONSisGitlabCompatibilityEnabledmathfootnotefrontMattertrimUnnecessaryCodeBlockEmptyLines)这一组选项会改变块结构的分类(例如 GitLab 兼容模式下 ```math 与代码块的互换),而仅基于已解析状态的渲染重建无法反映这种变化——因此触及这些选项时必须从 Markdown 重新解析整个文档。

工具链强制的代码约定

  • ESLinteslint.config.mjs,antfu 基础)叠加:
    • complexity ≤ 20、max-lines-per-function ≤ 200(对非测试 TS 为警告级);
    • 接口名必须I[A-Z0-9] 开头(如 IMuyaOptionsIPlugin),命名约定规则会拦截不合规的接口——这解释了源码中大量 I 前缀命名的由来;
    • 私有类成员必须以下划线开头(如 _uiPlugins_activeContentBlock);
    • 风格:4 空格缩进、强制分号、禁用 React 规则、禁用 Markdown linting;
    • 禁止在审计过的边界辅助函数之外使用 value as unknown as X 双重断言——应使用类型守卫或命名辅助函数(examples/src/main.ts 中对 Intl 的 polyfill 处理就是一个被显式豁免的边界案例)。
  • Madge 循环依赖检查(pnpm -C packages/muya check-circular)在 CI 中运行——新增循环导入会直接让构建失败。
  • 测试文件(*.test.ts*.spec.ts)与 vite.config.ts 从上述严格 TS lint 规则中豁免。

上游 muya 曾接线 Conventional Commits + husky + lint-staged + release-it,但这些没有迁入 MarkText——MarkText 不用 husky/commitlint,且 @muyajs/core 不从此仓库发布。提交风格遵循 MarkText 根目录的贡献指南。

构建管线要点

vite.config.ts 揭示了两个容易踩坑的细节:

  1. vite-plugin-dts@5(经 unplugin-dts 间接依赖)使用 outDirs(复数)而非 outDir。配置中 dts({ entryRoot: 'src', outDirs: 'lib/types' }) 依赖这一点把类型声明产出到 lib/types/——这正是 package.jsonpublishConfig.exports[*].types 指向的路径。如果哪天看到 lib/index.d.ts 直接出现在 lib/ 下,说明这个选项名回归了。
  2. @laynezh/vite-plugin-lib-assets 负责静态资源路由:*.pnglib/assets/icons/,字体文件进 lib/assets/fonts/outputPath 回调中按扩展名分流)。

库模式构建(build.lib)以 src/index.ts 为唯一入口,产出 es/umd/cjs 三种格式、文件名统一为 ${format}/index.js,与 main/module/publishConfig 的声明完全对应。

小结

packages/muya/CLAUDE.md 实际上是一份高密度引擎维护手册,其核心信息可归纳为四条主线:注册制块树(新增块必须走 registerBlocks())、OT 驱动的树状态同步ot-json1 + 手工 pick/drop)、只升不降的规范合规基线expected-failures.json + 87.7%/86.3% 基线)、以及自包含的独立工具链(antfu ESLint + madge + 独立 vitest 配置)。对希望向 MarkText 贡献编辑器能力、或想把 @muyajs/core 作为独立引擎嵌入自己产品的开发者,上述架构入口(Muya.use 插件注册、muya.init() 生命周期、setOptions 运行时契约)与命令清单构成了可直接落地的操作地图。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384