news 2026/10/3 1:57:48

mdBook 可编辑代码块(Editable Playground)完整指南:启用配置、Ace 编辑器定制与源码原理剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
mdBook 可编辑代码块(Editable Playground)完整指南:启用配置、Ace 编辑器定制与源码原理剖析
  • 开发工具
  • 文档

【免费下载链接】mdBook

Create book from markdown files. Like Gitbook but implemented in Rust

项目地址:https://gitcode.com/gh_mirrors/md/mdBook
点击查看免费下载

导读:mdBook 不仅能把 Markdown 中的 Rust 代码块渲染为可一键运行的 Playground,还能进一步把它们变成可直接在线编辑的代码编辑器,让读者在浏览器里修改并运行示例代码。本文以 guide/src/format/theme/editor.md 为主体,系统讲解如何通过book.toml启用可编辑代码块、如何用editable属性标记代码块、如何理解并替换默认的 Ace 编辑器,并结合 crates/mdbook-html 与 crates/mdbook-core 的源码剖析其底层实现,帮你写出可复制、可运行、可深度定制的交互式书籍页面。

一、什么是可编辑代码块(Editable Playground)

mdBook 的 HTML 渲染器对 Rust 代码块提供了一套"Playground"能力:渲染后的代码块带有一个运行按钮(Run),点击后会把代码提交到 Rust Playground 在线执行(参见 crates/mdbook-html/front-end/js/book.js 中的run_rust_code逻辑)。在此基础上,mdBook可选地让这些代码块变成可编辑的——即渲染为一个内嵌的文本编辑器,读者可以直接修改代码内容,修改后再运行。

可编辑能力默认是关闭的,需要两步开启:

  1. 在book.toml中启用全局开关;
  2. 在具体某个代码块上打上editable标记。

只有同时满足这两个条件,该代码块才会被渲染成编辑器;否则它仍然只是普通的 Playground(或普通代码块)。

二、第一步:在 book.toml 中启用可编辑功能

在book.toml的[output.html.playground]小节中添加editable = true:

[output.html.playground] editable = true

该配置对应源码中的Playground配置结构体(crates/mdbook-core/src/config.rs#L613-L641):

#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[non_exhaustive] pub struct Playground { /// Should playground snippets be editable? Default: `false`. pub editable: bool, /// Display the copy button. Default: `true`. pub copyable: bool, /// Copy JavaScript files for the editor to the output directory? /// Default: `true`. pub copy_js: bool, /// Display line numbers on playground snippets. Default: `false`. pub line_numbers: bool, /// Display the run button. Default: `true` pub runnable: bool, }

[output.html.playground]小节实际可用的全部配置项及默认值如下:

配置键含义默认值
editable是否把打了editable标记的代码块渲染为可编辑编辑器false
copyable是否显示"复制到剪贴板"按钮true
copy_js是否把编辑器所需的 JavaScript 文件复制到输出目录true
line_numbers是否在编辑器/代码块中显示行号false
runnable是否显示运行按钮(是否作为可运行 Playground 处理)true

各键之间的关系在源码中体现得很清楚:

  • editable && copy_js同时为true时,渲染阶段才会把 Ace 编辑器的整套脚本注入页面(见下文"静态资源加载"一节);
  • line_numbers通过模板变量playground_line_numbers传递给前端(crates/mdbook-html/src/html_handlebars/hbs_renderer.rs#L555-L563),由 crates/mdbook-html/front-end/playground_editor/editor.js 读取并决定是否显示行号与装订线(gutter);
  • copyable决定是否渲染复制按钮;
  • runnable决定代码块是否被视为"可运行 Playground"(源码见 crates/mdbook-html/src/html/tree.rs#L1004-L1009)。

提示:配置键采用 kebab-case(连字符风格)命名,即line-numbers与line_numbers等价(rename_all = "kebab-case");同时该结构体标注了deny_unknown_fields,写入未定义字段会导致配置解析失败(详见下文"自定义编辑器"一节)。

三、第二步:用 editable 属性标记代码块

启用全局开关后,还需要在具体代码块的围栏信息字符串中追加editable属性:

```rust,editable fn main() { let number = 5; print!("{}", number); } ```

渲染结果就是一个可编辑的 Playground:

fn main() { let number = 5; print!("{}", number); }

注意此时编辑器右上角会出现一个Undo Changes(撤销更改)按钮——这是可编辑 Playground 的标志性特征。点击后编辑器会恢复为该代码块的原始内容。

editable 标记的生效条件

从渲染源码(crates/mdbook-html/src/html/tree.rs#L997-L1043)可以看出,editable属性与全局开关是"AND"关系:

let is_editable = class_set.contains("editable"); let is_playground = class_set.contains("language-rust") && ((!class_set.contains("ignore") && !class_set.contains("noplayground") && !class_set.contains("noplaypen") && self.options.config.playground.runnable) || class_set.contains("mdbook-runnable"));

即:

  • 只有代码块 class 中同时出现language-rust(或等效的 playground 判定)与editable,才可能成为可编辑块;
  • 若全局editable未开启,或代码块未打editable标记,则代码块退化为普通 Playground 或普通代码块;
  • 代码块上的ignore、noplayground、noplaypen会禁止其成为 Playground。

仓库自带的测试用例验证了这一点:tests/testsuite/rendering/editable_rust_block/src/editable-rust.md 同时包含一个rust,editable块与一个普通rust块,并由 tests/testsuite/rendering.rs#L87-L91 的editable_rust_block测试比对渲染结果。GUI 测试 tests/gui/playground.goml#L5-L11 则断言:在开启editable = true的测试书(tests/gui/books/playground/src/chapter_1.md)中,页面包含 2 个.play-button(对应普通块与可编辑块各一个运行按钮),但只有 1 个.reset-button(即 Undo Changes 按钮只出现在editable块上)。

可编辑块与 fn main 自动包裹

另一个值得注意的细节:对于不可编辑的普通 Rust Playground,mdBook 会像 rustdoc 一样,在代码缺少fn main时自动用隐藏文本包裹一层fn main(见 crates/mdbook-html/src/html/hide_lines.rs#L108-L124 的wrap_rust_main)。但可编辑块不会做这种包裹(crates/mdbook-html/src/html/tree.rs#L1028-L1037),因为可编辑块的内容会原样交给 Ace 编辑器展示——如果用户看到的是被隐藏包裹后的代码,编辑体验会很奇怪。因此,可编辑代码块请直接书写完整可编译的代码(含fn main)。

四、默认编辑器:Ace

默认情况下,可编辑代码块使用的编辑器是Ace(https://ace.c9.io/),mdBook 将整套 Ace 运行时资源直接编译进二进制(通过include_bytes!内嵌),存放于:

  • crates/mdbook-html/front-end/playground_editor/ace.js:Ace 核心库;
  • crates/mdbook-html/front-end/playground_editor/editor.js:mdBook 自研的编辑器初始化脚本;
  • crates/mdbook-html/front-end/playground_editor/mode-rust.js:Rust 语法高亮模式;
  • crates/mdbook-html/front-end/playground_editor/theme-dawn.js 与theme-tomorrow_night.js:亮/暗两套编辑器配色。

这些资源的挂载点在 crates/mdbook-html/src/theme/playground_editor.rs(pub(crate) static JS / ACE_JS / MODE_RUST_JS / THEME_DAWN_JS / THEME_TOMORROW_NIGHT_JS)。

静态资源加载:按需注入

由于 Ace 体积很大,mdBook只在确实需要时才加载它。在 crates/mdbook-html/src/html_handlebars/static_files.rs#L98-L111:

let playground_config = &html_config.playground; // Ace is a very large dependency, so only load it when requested if playground_config.editable && playground_config.copy_js { // Load the editor this.add_builtin("editor.js", playground_editor::JS); this.add_builtin("ace.js", playground_editor::ACE_JS); this.add_builtin("mode-rust.js", playground_editor::MODE_RUST_JS); this.add_builtin("theme-dawn.js", playground_editor::THEME_DAWN_JS); this.add_builtin( "theme-tomorrow_night.js", playground_editor::THEME_TOMORROW_NIGHT_JS, ); }

只有editable = true且copy_js = true(默认值)时,这 5 个脚本才会被写入输出目录并注入页面模板。页面模板 crates/mdbook-html/front-end/templates/index.hbs#L310-L316 中的注入点如下:

{{#if playground_js}} <script src="{{ resource "ace.js" }}"></script> <script src="{{ resource "mode-rust.js" }}"></script> <script src="{{ resource "editor.js" }}"></script> <script src="{{ resource "theme-dawn.js" }}"></script> <script src="{{ resource "theme-tomorrow_night.js" }}"></script> {{/if}}

模板变量playground_js由 crates/mdbook-html/src/html_handlebars/hbs_renderer.rs#L555-L563 注入,且仅当editable && copy_js时为true;若同时开启了line_numbers,还会额外注入playground_line_numbers = true。

前端初始化逻辑

editor.js 在页面加载后扫描所有带.editableclass 的元素,逐个用ace.edit()初始化:

Array.from(document.querySelectorAll('.editable')).forEach(function(editable) { let display_line_numbers = window.playground_line_numbers || false; let editor = ace.edit(editable); editor.setOptions({ highlightActiveLine: false, showPrintMargin: false, showLineNumbers: display_line_numbers, showGutter: display_line_numbers, maxLines: Infinity, fontSize: "0.875em" // please adjust the font size of the code in general.css }); ... editor.getSession().setMode("ace/mode/rust"); editor.originalCode = editor.getValue(); editors.push(editor); });

要点:

  • 行号/装订线是否显示完全取决于playground_line_numbers全局变量(即配置中的line_numbers);
  • editor.originalCode保存了原始代码,供"Undo Changes"按钮恢复使用;
  • 初始化后的编辑器实例统一注册到window.editors数组,供其他脚本(如book.js)访问。

五、可编辑块与 Playground 的联动:book.js 的关键耦合

可编辑块不是孤立存在的编辑器,它与运行、复制、撤销等功能深度联动,这些逻辑都写在 crates/mdbook-html/front-end/js/book.js 中:

  1. 取代码:playground_text()在检测到window.ace且代码块带editable时,会优先从 Ace 编辑器实例读取当前编辑后的内容(而不是静态 HTML 文本),保证运行、复制按钮使用的是读者修改后的代码(book.js#L9-L15)。

  2. 实时联动运行按钮:为可编辑块注册change监听器,读者每次修改代码都会重新检查extern crate依赖的可用性并更新运行按钮状态;同时绑定Ctrl-Enter快捷键,在编辑器内按下即可直接运行代码(book.js#L62-L84)。

  3. Undo Changes 按钮:只有window.ace存在且代码块带editable时,才会创建reset-button(Undo Changes 按钮),点击后把编辑器内容恢复为editor.originalCode并清除选区(book.js#L304-L320):

undoChangesButton.addEventListener('click', function() { const editor = window.ace.edit(code_block); editor.setValue(editor.originalCode); editor.clearSelection(); });

GUI 测试 tests/gui/editor-keypress.goml 还专门验证了编辑器聚焦时的按键行为:在 Ace 编辑器内按s、?、方向键等不会触发 mdBook 的全局快捷键(搜索框、帮助弹窗、页面导航),确保编辑器交互与书籍全局交互互不干扰。

六、自定义编辑器

文档描述的方式

原文档给出了"更换编辑器"的配置入口——在[output.html.playground]中提供一个不同的编辑器目录:

[output.html.playground] editable = true editor = "/path/to/editor"

并强调:要让更换后的编辑器正常工作,必须同时覆盖theme目录下的book.js,因为book.js与默认 Ace 编辑器存在多处耦合(前文所述的取代码、运行联动、Undo Changes 按钮等逻辑都硬编码依赖window.ace与.editableclass)。

结合当前仓库源码的说明

需要如实说明的一点是:从当前仓库源码结构看,Playground配置结构体只定义了editable、copyable、copy_js、line_numbers、runnable五个字段(crates/mdbook-core/src/config.rs#L613-L641),并未包含editor字段;且该结构体标注了deny_unknown_fields,意味着写入未定义的键会导致配置解析报错。也就是说,在当前版本代码中,"提供一个编辑器目录"更多体现为一种替换前端资源的设计思路,其落地方式是通过主题覆盖(theme 目录)实现,而非一个可直接解析的配置键。如果你在使用的 mdBook 版本上配置editor报错,请以实际版本的配置说明为准。

从主题系统(crates/mdbook-html/src/theme/mod.rs)可以看到覆盖机制:Theme::new()会检查书籍根目录下的theme文件夹,凡是存在同名文件(如book.js、index.hbs、css/*.css等,完整清单见 theme/mod.rs#L75-L103)就用用户文件替换内置默认文件;内置编辑器资源(editor.js、ace.js等)则始终由渲染器以add_builtin方式注入(static_files.rs#L100-L111)。

因此,基于当前仓库实现,可落地的自定义路径是:

  1. 覆盖theme/book.js:以默认 book.js 为模板,把其中所有依赖 Ace 的代码(playground_text中window.ace.edit分支、change监听、Ctrl-Enter 命令、Undo Changes 按钮创建等)替换为你所选编辑器的等价实现。可先用mdbook init --theme把默认主题复制到源码目录,再修改,避免破坏既有功能(这也是主题定制的一般建议,见 guide/src/format/theme/README.md)。

  2. 覆盖theme/index.hbs(可选):如果不想加载 Ace 的 5 个脚本,可以在模板层面去掉playground_js分支里的脚本标签,改引你自己的编辑器资源;index.hbs位于可覆盖文件清单中。

  3. 保持接口契约:你自己的book.js需要继续维护"从编辑器读取当前代码"、"运行按钮联动"、"恢复原始代码"这三类行为(分别对应默认实现中的playground_text、change监听与reset-button),否则运行/复制/撤销功能会失效。

七、完整配置示例

一个同时开启可编辑、行号、复制按钮的完整book.toml示例:

[book] title = "My Interactive Book" [output.html.playground] editable = true # 启用可编辑代码块(默认 false) line_numbers = true # 编辑器与代码块显示行号(默认 false) copyable = true # 显示复制按钮(默认 true) runnable = true # 显示运行按钮(默认 true) copy_js = true # 将编辑器脚本复制到输出目录(默认 true)

配合 Markdown 中的:

```rust,editable fn main() { let number = 5; print!("{}", number); } ```

mdbook build后,book目录输出的页面即包含一个带Undo Changes按钮、可编辑、可运行、可复制的 Rust 示例块。

仓库自带的 GUI 测试书 tests/gui/books/editor/book.toml 与 tests/gui/books/editor/src/chapter_1.md 就是这一配置的最小可运行样例,可以直接参考。

八、小结与注意事项

  • 两步开启:book.toml中[output.html.playground] editable = true,代码块围栏信息里写rust,editable,缺一不可。
  • Undo Changes 按钮是可编辑块的标志,逻辑见 book.js#L304-L320;恢复的是editor.originalCode(原始代码)。
  • Ace 按需加载:仅editable && copy_js时才注入 5 个编辑器脚本(static_files.rs#L98-L111),未启用时页面不会白白引入大体积 JS。
  • 可编辑块不自动包裹fn main(tree.rs#L1028-L1037),请书写完整可编译代码。
  • 自定义编辑器:原文档建议的editor = "/path/to/editor"配置项在当前仓库的Playground结构体中未定义(且deny_unknown_fields),实际自定义需通过覆盖theme/book.js(必要时连同index.hbs)实现,且必须保留与 Ace 耦合的取代码、运行联动、撤销等逻辑(book.js 是核心挂载点)。
  • 测试验证:渲染正确性可参考 tests/testsuite/rendering/editable_rust_block 与 tests/testsuite/rendering.rs#L87-L91;UI 行为可参考 tests/gui/playground.goml 与 tests/gui/editor-keypress.goml。

掌握了以上内容,你就可以为自己的 mdBook 构建"打开即改、改完即跑"的交互式示例页面,并在需要时把默认的 Ace 编辑器替换成自研方案——这正是 guide/src/format/theme/editor.md 所描述能力的完整落地路径。

  • 开发工具
  • 文档

【免费下载链接】mdBook

Create book from markdown files. Like Gitbook but implemented in Rust

项目地址:https://gitcode.com/gh_mirrors/md/mdBook
点击查看免费下载

相关推荐

上一篇:NapCatQQ完全指南:从零开始打造现代化QQ机器人
下一篇:Harpoon单元测试框架:插件自身测试体系的设计与实现

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 1:56:19

DRV8818+PIC18F86J50工业步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华