Notist
On this page
  1. Extension(Experimental)
  2. 构建生命周期
  3. 阶段输入
  4. 配置
  5. JSON I/O 协议
  6. 请求
  7. 响应
  8. 最小 Python 扩展
  9. 输入校验与失败
  10. Rust 接入

Extension(Experimental)

站点扩展在 beforecompile 处理源码集合,在 beforetemplate 处理模板数据。同一次构建的两个事件由同一扩展实例处理,可以在内部保留状态。外部命令使用 [site.extensions.<name>],内置扩展使用 [site.extensions.builtins.<name>]。

构建生命周期

强调节点为扩展钩子。单文档编译的内部阶段见 扩展 Notist。

  • compile 校验输入、计算页面 URL 与输出路径、分析和求值文档、提取页面元信息。

  • render 由 SSG 解析链接与资源路径,调用 notist-html 渲染正文并生成组件资源。

  • template 加载模板,将正文与模板数据组合为完整页面。静态资源与 Sass 属于站点资源处理,不单列节点。

  • site.data 不进入 .not 求值环境,不经过 before_compile。

  • serve 每次重建创建新实例或新进程;失败时保留上一次成功预览。

阶段输入

pub struct CompileInput {
    pub documents: BTreeMap<PathBuf, String>,
}

pub struct TemplateInput<'a> {
    pub pages: &'a [Page],
    pub data: BTreeMap<String, serde_json::Value>,
}

事件

输入

可修改

before_compile

发现后的源码集合

documents

before_template

正文已渲染的 Pages、加载后的模板 data

data

  • documents 的键是内容目录下的规范相对路径,使用 /;值是完整源码,扩展名决定前端。可以添加、删除或替换文档,不写回源文件。

  • data 的键是数据文件去掉扩展名后的相对路径,例如 data/example.toml 对应 example,嵌套键使用 folder/example。

  • 每个阶段先按 Builder 注册顺序运行原生扩展,再按 extension_order 运行配置扩展;未指定时按完整扩展名排序。

  • 每个钩子修改副本,成功且校验通过后才交给下一个扩展;失败停止构建,不提交本次修改。

  • 同一实例贯穿两个事件,内部状态仅属于本次构建。错误会终止该实例,不会用失败状态重试。

Page 是页面展示模型,不包含文档树:

pub struct Page {
    pub route: Route, // source、path、output、url
    pub title: String,
    pub attrs: BTreeMap<String, serde_json::Value>,
    pub headings: Vec<Heading>, // title、id、anchor、level
    pub html: String, // 正文 HTML,不包含整页模板
    pub directory: String, // 内容相对目录,根目录为空
    pub ancestors: Vec<String>,
}

Book 在 beforecompile 读取实际输入中的 SUMMARY、解析结构并按需筛选文档,结构保存在实例内;beforetemplate 将结构与最终 Pages 匹配,一次性生成 site.data.book。SUMMARY 不作为普通页面发布,模板 data 不充当内部状态通道。

配置

[site.extensions.catalog]
command = ["python3", "scripts/catalog.py", "../Cargo.toml"]
watch = ["scripts", "../Cargo.toml", "../examples"]

字段

类型

说明

command

string[]

程序与参数,第一项非空;不经过 shell。

watch

path[]

相对包根的额外依赖;目录递归监听。

command、watch 与进程 CWD 均相对 Notist.toml 所在的包根。每个命令接收两个事件,不需要配置 stages;不处理的事件也必须响应。未知配置字段、空 command 或无效 extension_order 会导致失败。

watch 在扩展执行前登记,失败构建也会保留这些依赖。Rust 扩展通过 SiteContext.resources 读取的依赖由宿主记录。路径基准见 Notist.toml 配置。

内置 Book:

[site.extensions.builtins.book]
summary = "SUMMARY.not"

表存在即启用,builtins 是保留命名空间。Book 扩展不选择或修改主题,详见 Notist Book。顶层 extensions 配置文档扩展,site.extensions 配置站点扩展。

JSON I/O 协议

协议版本为 2。一个构建启动一个进程,通过 stdin / stdout 交换 JSON Lines:每行一个完整 JSON 对象,每个事件恰好一行响应,写入后立即 flush。stdout 只用于协议,日志写 stderr。宿主发送事件并等待响应后才继续,不广播并发修改。

两个事件复用同一进程,可以保留 SUMMARY 解析结果等内部状态。最后一个事件处理完成后,宿主关闭 stdin;扩展应在 EOF 时退出,宿主检查退出码与多余输出后才允许构建成功。任何阶段失败都会终止并回收进程,下一次构建启动新进程。不能等待 stdin EOF 才开始处理请求。

请求

每个请求都有 version、event、root、config。root 是包根绝对路径,也是进程 CWD;config 是完整 SiteConfig,字段见 Notist.toml 配置。

before_compile 只携带源码集合,不含 data 或 pages:

{
  "version": 2,
  "event": "before_compile",
  "root": "/project/website",
  "config": {"title": "My Site", "content": "content"},
  "documents": {"index.not": "= Home"}
}

before_template 携带最终 pages 和前序扩展修改后的 data,不含源码集合:

{
  "version": 2,
  "event": "before_template",
  "root": "/project/website",
  "config": {"title": "My Site", "content": "content"},
  "pages": [],
  "data": {"settings": {"language": "zh"}}
}

上例省略 config 的其余字段,pages 的元素使用上述 Page 结构。Rust 命令可用 notist_ssg::command 的 Request、Event、Response 类型读取和生成协议。

响应

响应必须携带相同的 version 和 event。beforecompile 只能返回 documents;beforetemplate 只能返回 data,不能返回 pages 或 documents:

{"version":2,"event":"before_compile","documents":{"index.not":"= Edited","demo.md":"# Demo"}}
{"version":2,"event":"before_template","data":{"catalog":{"count":2}}}
  • 省略可修改字段或写 null 表示不修改;无操作响应仍需 version 与 event。

  • 返回映射表示完整替换,不是增量补丁;空映射表示清空。要保留已有键,先修改请求中的映射,再整体返回。

  • 响应拒绝未知字段、阶段不匹配、版本不匹配。beforecompile 不允许返回 data,beforetemplate 不允许返回 pages。

  • 可选 errors 是字符串数组,非空立即终止构建,即使同时返回修改也不会应用:

{"version":2,"event":"before_compile","errors":["missing Cargo.toml"]}

最小 Python 扩展

import json
import sys

generated = None
for line in sys.stdin:
    request = json.loads(line)
    response = {"version": 2, "event": request["event"]}
    try:
        if request["version"] != 2:
            raise ValueError("unsupported protocol version")
        if request["event"] == "before_compile":
            documents = request["documents"]
            documents["examples/demo.md"] = "# Demo\n\nGenerated.\n"
            generated = {"name": "demo"}
            response["documents"] = documents
        elif request["event"] == "before_template":
            data = request["data"]
            data["demo"] = generated
            response["data"] = data
        else:
            raise ValueError("unsupported event")
    except Exception as error:
        response["errors"] = [str(error)]
    print(json.dumps(response), flush=True)

输入校验与失败

  • 文档路径必须是规范相对路径:不能为空、不能为绝对路径、不能含反斜杠、. 或 .. 段、空段或隐藏段;不能占用 _notist;扩展名必须由已注册前端支持。

  • beforecompile 后检查文档路径及输出冲突;beforetemplate 后检查 data 键,键必须是规范相对路径。

  • 生成文档的源身份是 content 下的相对路径,imports、链接、嵌入与诊断以此为基准。根注解照常生效,新目录进入 site.directories。

  • 配置了扩展时 content 目录可以不存在,内容可以全部由扩展提供。每次构建重新发现输入,删除的生成页面不会残留。

  • 任一阶段失败停止构建,保留已有输出或上一次成功预览;脚本自行写文件、上传等外部副作用不会回滚。

  • 无效 JSON、缺少换行、版本不符、未知字段、非法路径、非零退出码和多余 stdout 都会使构建失败。错误信息包含扩展名与阶段。

路径与输出规则见 Notist SSG。

Rust 接入

pub trait SiteExtension: Debug + Send + Sync {
    fn name(&self) -> &str;
    fn before_compile(
        &mut self, context: &SiteContext<'_>, input: &mut CompileInput,
    ) -> Result<(), Error> { Ok(()) }
    fn before_template(
        &mut self, context: &SiteContext<'_>, input: &mut TemplateInput<'_>,
    ) -> Result<(), Error> { Ok(()) }
    fn finish(&mut self) -> Result<(), Error> { Ok(()) }
}

两个事件默认不修改输入。finish 是成功路径上的资源收尾,不是数据修改阶段;失败清理由实例的 Drop 完成。SiteContext 提供只读 config 与 resources。

use notist_ssg::{Builder, Error};
use notist_ssg::extensions::{CompileInput, SiteContext, SiteExtension, TemplateInput};

#[derive(Debug, Clone, Default)]
struct Catalog { count: usize }

impl SiteExtension for Catalog {
    fn name(&self) -> &str { "catalog" }

    fn before_compile(
        &mut self, _: &SiteContext<'_>, input: &mut CompileInput,
    ) -> Result<(), Error> {
        input.documents.insert("examples/demo.md".into(), "# Demo".into());
        self.count = input.documents.len();
        Ok(())
    }

    fn before_template(
        &mut self, _: &SiteContext<'_>, input: &mut TemplateInput<'_>,
    ) -> Result<(), Error> {
        input.data.insert("catalog".into(), serde_json::json!({"count": self.count}));
        Ok(())
    }
}

let builder = Builder::default().with_extension(Catalog::default());

Builder 注册的原生扩展需实现 Clone;每次构建从注册的原型克隆一个新实例,两个事件调用同一实例。Clone 必须隔离构建内可变状态,不应通过共享锁把中间结果带到另一构建。

build / serve 运行站点扩展,单文件 check / cst / core / json / query / html 不运行。完整示例见 examples/ssg-extensions 与 从 Cargo examples 生成站点。接口与协议处于实验阶段。