/ Site 模式 / 组合 Steps

核心库提供了一系列基础 step,可以借助 MoonBit 的管道操作符 |> 将它们自由拼接成一条完整的处理管线。本节介绍常用 step 的分类与用法,以及贯穿整条管线的模板变量机制。

pub fn separate_frontmatter(item : Item[String]) -> Item[(Map[String, String], String)]

pub fn[T : Show] extract_markdown(item : Item[T]) -> Item[(Map[String, String], String)]

pub fn[T : Show] parse_markdown(item : Item[T]) -> Item[Array[@markdown.Block]]

separate_frontmatter 将文本拆分为 YAML 元数据(Map[String, String])与正文文本,输入要求是 Item[String]extract_markdown 的作用与之相同,区别在于它接受任意实现了 Show 的类型作为输入,而不局限于 Stringparse_markdown 将文本解析为 Markdown 语法树

pub fn[T : Show] render_markdown(item : Item[T]) -> Item[String]

pub fn[T : Show] render_markdown_and_frontmatter(item : Item[T]) -> Item[String]

pub fn render_markdown_from_ast(item : Item[Array[@markdown.Block]]) -> Item[String]

pub fn render_markdown_and_frontmatter_from_ast(
  item : Item[Array[@markdown.Block]],
  frontmatter : Map[String, String],
) -> Item[String]

render_markdown 直接将文本解析并渲染为 HTML,不处理元数据,如果输入本身带有元数据块,元数据会被当作正文的一部分一并渲染,通常不是想要的效果。render_markdown_and_frontmatter 在渲染前先调用 separate_frontmatter 分离元数据,避免这个问题。render_markdown_from_astrender_markdown_and_frontmatter_from_ast 是以上两者基于已经解析好的语法树的对应版本,适用于在解析和渲染之间插入了自定义处理逻辑(如转换器)的场景,其中 render_markdown_and_frontmatter_from_ast 需要额外显式传入元数据,因为语法树本身不携带元数据信息。

pub fn apply_template(item : Item[String], template : Array[@template.TemplateNode]) -> Item[String]

pub fn apply_template_strict(item : Item[String], template : Array[@template.TemplateNode]) -> Item[String] raise

pub fn load_and_apply_template(item : Item[String], path : String, strict? : Bool) -> Item[String] raise

pub fn use_css(item : Item[String], code : Array[String]) -> Item[String]

pub fn use_js(item : Item[String], code : Array[String]) -> Item[String]

pub fn import_css(item : Item[String], paths : Array[String]) -> Item[String]

pub fn import_js(item : Item[String], paths : Array[String]) -> Item[String]

pub fn inject_head(item : Item[String], html : String) -> Item[String]

pub fn inject_body(item : Item[String], html : String) -> Item[String]

apply_template 是宽松模式的渲染,遇到问题会跳过对应节点、以空内容代替;apply_template_strict 是严格模式,遇到问题会直接 raise 具体的错误,两者都要求预先解析好的模板节点数组。load_and_apply_template 把“读取模板文件、解析、渲染”三步合成一步,strict 参数决定内部走哪种渲染模式,默认宽松。

use_cssuse_js 接受的是代码本体,数组中的每一项会各自生成一个独立的 <style><script> 标签直接内联到页面中,use_css 注入在 </head> 之前,use_js 注入在 </body> 之前。import_cssimport_js 接受的是路径或 URL,以 <link><script src> 的形式引用外部资源,不内联代码本体。inject_headinject_body 接受一段原始 HTML 片段,分别注入到页面的头部、尾部。这几个 step 的注入内容最终都是通过写入 Item.vars 中的对应变量实现的,只有在 item 之后被套用了模板,这些注入内容才会真正体现在渲染输出中。

pub fn[T] set_extension(item : Item[T], extension : String) -> Item[T]

pub fn[T : Thingable] unify(item : Item[T]) -> Item[Thing]

set_extension 修改 item 的目标扩展名,传入的字符串需要带上前导点号,如 .html 而非 htmlunify 把最终数据归一为 Thing,是管线中最常见的收尾步骤,要求数据类型实现 Thingable

pub trait Thingable {
  fn to_thing(Self) -> Thing
}

pub impl Thingable for String
pub impl Thingable for Bytes
pub impl Thingable for Json

String 会被转换为 Thing::DocBytes 会被转换为 Thing::AssetJson 会被转换为其自身对应的表示,具体的 Thing 定义与写入方式请阅读 输出格式

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

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

run_markdown_preprocessors 对原始文本依次执行一系列外部预处理命令,run_markdown_transformers 对已解析的语法树依次执行一系列外部转换命令。这两个函数本身不是 (Item[T]) -> Item[R] 形状的 step,而是接受、返回裸数据(StringArray[@markdown.Block])的普通函数,接入管线时通常需要配合 Item::map_with_eff 使用:

item.map_with_eff(text => @bridge.run_markdown_preprocessors(config, text))

完整的通信协议、错误类型定义,请阅读 外部插件协议

除了通过外部命令扩展处理逻辑,Site 模式还允许直接传入一组 MoonBit 函数作为处理步骤:

pub fn use_preprocessor(
  item : Item[String],
  preprocessors : Array[(Item[String]) -> Item[String] raise],
) -> Item[String] raise

pub fn use_transformer(
  item : Item[Array[@markdown.Block]],
  transformers : Array[(Item[Array[@markdown.Block]]) -> Item[Array[@markdown.Block]] raise],
) -> Item[Array[@markdown.Block]] raise

这两个 step 会按顺序依次调用数组中的每一个函数,前一个的输出作为后一个的输入,语义上与外部命令版本的预处理器、转换器完全一致,区别只是执行环境从独立进程变成了进程内的普通函数调用,因此不需要经过 JSON 序列化,性能开销更低,也更方便调试,可以直接打断点、直接抛出 MoonBit 原生的错误类型。

在 Site 模式中模板渲染的上下文变量通过 Item::add_varsItem::set_var,或者其他 step 内部隐式设置。举例来说,。

如果希望像 Book 模式那样,自动把元数据的字段暴露为模板变量,可以使用 render_markdown_and_frontmatterrender_markdown_and_frontmatter_from_ast step 或:

raw
|> @core.separate_frontmatter
|> x => {
  let (frontmatter, body) = x.data
  x.map(_ => body).add_vars(frontmatter.map(k, v => (k, @template.Value::String(v))))
}

反过来,如果场景不需要元数据出现在模板变量中,完全可以跳过这一步,vars 就不会包含任何元数据内容——这一切都在自己的掌控之中,Book 模式的自动行为只是这套机制的一种预设组合方式,而非唯一的固定行为。

Book 模式的 MarkDown 页面渲染函数 process_markdown_page 就是最好的参考示例(以最新 Api 文档为准,以下代码仅供参考)

@core.render_markdown_and_frontmatter_from_ast(
  seprated.map(_ => ast),
  seprated.data.0,
)
|> @core.set_extension(".html")
|> x => {
  let (prev, next) = summary.prev_next(x.location())
  x.add_vars(
    Map([
      (
        "section",
        summary.flatten(x.location(), config.base_url)
        |> @template.Value::from_json,
      ),
      ("site_title", config.title |> String),
      ("site_description", config.description |> String),
      ("site_keywords", config.keywords |> String),
      ("site_authors", config.authors.join(",") |> String),
      ("site_language", config.language |> String),
      ("favicon", config.favicon |> String),
      ("logo", config.logo |> String),
      ("repository", config.repository.unwrap_or("") |> String),
      ("breadcrumb", summary.breadcrumb(x.location()) |> String),
      ("version", "v1.0.0" |> String),
      ("current", x.location() |> String),
      (
        "prev",
        prev
        .map(prev => {
          @template.Object(
            Map([("title", prev.0 |> String), ("location", prev.1 |> String)]),
          )
        })
        .unwrap_or(String("")),
      ),
      (
        "next",
        next
        .map(next => {
          @template.Object(
            Map([("title", next.0 |> String), ("location", next.1 |> String)]),
          )
        })
        .unwrap_or(String("")),
      ),
    ]),
  )
}
|> @core.load_and_apply_template(config.extensions.template)
|> @core.import_css(config.extensions.import_css)
|> @core.import_js(config.extensions.import_js)
|> @core.use_css(config.extensions.use_css)
|> @core.use_js(config.extensions.use_js)
|> @core.inject_head(config.extensions.inject_head.join("\n"))
|> @core.inject_body(config.extensions.inject_body.join("\n"))
|> @core.unify