/ 高级 / 多样化的插件

MoPress 提供三种互不排斥的方式来扩展内容的处理与展现方式,分别对应三种不同的扩展环境:独立于 MoPress 进程之外的外部程序、浏览器运行时标准、以及 MoonBit 生态内的代码库。选择哪一种,取决于想在什么层面、用什么语言解决问题。

形式扩展环境适用场景
外部 stdin、stdout 程序任意语言的独立进程用熟悉的语言编写文本、语法树处理逻辑,且需要跨项目复用,请阅读 外部插件协议
Web Components浏览器运行时需要在页面上呈现交互式、富媒体内容,且逻辑运行在客户端,请阅读 Web Components
MoonBit 模块MoonBit 生态,仅 Site 模式将一组可复用的 step 组合打包为库,在多个 Site 模式项目间共享,请阅读 MoonBit 模块插件

这是与 mdBook 的 preprocessor 机制最为接近的扩展方式:编写一个独立的可执行程序或脚本,MoPress 在构建过程中把内容通过标准输入传给它,读取它的标准输出作为处理结果。

与 mdBook 不同的是,MoPress 把这一机制拆分为两个阶段。对应的调用函数签名:

pub async fn run_markdown_preprocessors(
  config : @config.BookConfig,
  data : String,
  target: String
) -> String raise

pub async fn run_markdown_transformers(
  config : @config.BookConfig,
  data : Array[@markdown.Block],
  target: String,
) -> Array[@markdown.Block] raise

预处理器(preprocessor)操作原始 Markdown 文本,在解析为语法树之前运行,对应 run_markdown_preprocessors转换器(transformer)操作已经解析好的 Markdown 语法树,在渲染为 HTML 之前运行,对应 run_markdown_transformers。这让可以根据实际需求,选择在文本层面还是结构层面介入处理流程。两者都通过扩展配置中的 preprocessorstransformers 字段声明,具体的通信协议请阅读 外部插件协议

失败时统一抛出以下错误类型 ProcessorError


///|
/// Error type describing why an external preprocessor/transformer command
/// failed while running through `run_markdown_preprocessors` or
/// `run_markdown_transformers`.
pub suberror ProcessorError {
  NonZeroExit(command~ : String, exit_code~ : Int, stderr~ : String)
  StderrReported(command~ : String, message~ : String)
  InvalidStdout(command~ : String, raw~ : String, reason~ : String)
} derive(Debug, Eq)

试图想象一个平面直角坐标系,处理阶段与处理源分别为正交的 X、Y 轴。处理阶段即预处理器、转换器,描述的是“处理的何物”、“何时进行”。处理源即动作的发生者或者说预处理器、转换器的逻辑存在形式,分为内部与外部。在 组合 Steps 中提到的 use_preprocessoruse_transformer Step 即是运行内部的预处理器、转换器,换而言之如果一个 Step 类型为 fn (Item[String]) -> Item[String]fn (Item[@markdown.Block]) -> Item[@markdown.Block] 则分别认为是内部的预处理器、转换器。而上文的 run_markdown_preprocessorsrun_markdown_transformers 函数则是对接外部的预处理器、转换器,具体实现是语言无关的,通过外部插件协议通信。

如果需要的不是“改写内容”,而是“让某段内容在页面上具备交互能力或特殊呈现效果”,Web Components 通常是比外部程序更合适的选择。MoPress 的 Markdown 解析器在遇到无法识别的 HTML 标签时,会将其原样保留在语法树中,而不会报错或丢弃:


///|
/// A single block-level Markdown element.
pub(all) enum Block {
  /// A heading, with its level (1–6) and inline content.
  Heading(Int, Array[Inline])
  /// A paragraph of inline content.
  Paragraph(Array[Inline])
  /// A thematic break (horizontal rule).
  ThematicBreak
  /// A fenced or indented code block, with an optional language/info
  /// string and the raw code text.
  CodeBlock(String?, String)
  /// A block quote, containing its own nested sequence of blocks.
  BlockQuote(Array[Block])
  /// An ordered or unordered list.
  List(ListBlock)
  /// A raw block of HTML.
  HtmlBlock(String)
  /// A table.
  Table(TableBlock)
} derive(Eq, ToJson, Debug, FromJson)

其中 HtmlBlock(String) 就是原始 HTML 块被保留下来的形态;行内的原始 HTML 则对应 Inline 枚举中的 Html(String)。这意味着可以直接在 Markdown 正文中写自定义元素标签,只要配合注入脚本引入对应的自定义元素定义脚本,浏览器就会在渲染时原生地接管这部分内容,不需要 MoPress 在构建期做任何额外处理。详情请阅读 Web Components

如果扩展逻辑是用 MoonBit 编写的、只在 Site 模式的构建脚本内运行,最自然的做法是把它写成一组可复用的 step 函数,打包发布为一个独立的 MoonBit 包,供其他构建脚本通过包管理器引入。这一形式仅适用于 Site 模式,Book 模式的配置文件无法引用 MoonBit 代码。详情请阅读 MoonBit 模块插件

三种形式的取舍可以归结为两个问题:这段逻辑应该跑在哪里、以及希望用什么语言编写它。

  • 需要独立于 MoPress 主进程运行、且可能用非 MoonBit 语言编写,选择外部程序。

  • 需要运行在浏览器端、为读者提供交互,选择 Web Components。

  • 需要运行在构建期、且愿意用 MoonBit 编写、追求进程内调用的性能与可复用性,选择 MoonBit 模块。

这三者并不互斥,一个站点完全可以同时使用多种形式,用外部程序处理某种自定义 Markdown 语法,同时用 Web Components 渲染交互式图表。