D0009: Preview and HTML

Notist 的 HTML 能力分为三个层次:notist-html 将语义文档渲染为 HTML fragment,notist build 将整个 vault 构建为静态站点,notist preview 在本地持续构建并提供浏览器预览。

三者共享同一条语言管线:

.not source
  -> notist-syntax
  -> notist-eval lowering
  -> structuring
  -> StructuredDocument
  -> notist-html

HTML 层不重新解释源码。语法、函数、scope 和 reference 的语义只在 syntax、eval 与 analysis 中定义一次,preview 和 build 只是这些结果的不同消费者。

产品模型

Notist 的基本单位是 vault/module,而不是孤立文件。因此 HTML 输出也以整个 Workspace 为单位:

  • 每个 Module 都有稳定页面。

  • Wiki Reference 变成模块之间的导航链接。

  • 没有 README.not 的虚拟目录模块也有索引页面。

  • 页面共享 vault 导航和静态资源。

  • build 与 preview 使用相同的页面结构和 URL 规则。

build 负责可部署的静态结果,preview 负责编辑期间的快速反馈。二者不应发展出两套渲染语义。

notist-html

notist-html 是纯渲染 crate。它接收 StructuredDocument,输出可嵌入页面的 semantic HTML fragment。

它负责:

  • 渲染 Block、Content 和 Element。

  • 转义文本、属性和 Raw 内容。

  • 输出 source byte range metadata。

  • 根据调用方提供的上下文生成或解析 Reference 链接。

  • 为 Custom 和 UnresolvedCall 提供安全且可见的降级结果。

它不负责:

  • 读取文件或扫描 Workspace。

  • 执行 parser、lowering 或 Function。

  • 判断某个目标 Module 是否真实存在。

  • 生成完整页面、导航栏和站点主题。

  • 启动 HTTP 服务或监听文件变化。

保持 fragment renderer 的边界,可以让静态构建、preview、测试以及未来的编辑器 Hover 复用同一层。

HTML 映射

核心映射为:

  • Block::Paragraph -> p

  • Block::List -> ulli

  • Text -> escaped text span

  • Strong -> strong

  • Heading -> h1h6

  • Quote -> blockquote

  • inline Raw -> code

  • block Raw -> pre > code

  • Reference -> a.notist-reference

  • unresolved Reference -> 不可点击的 span

  • Custom -> 带 data-notist-namespandiv

  • UnresolvedCall -> 保留函数名与 body 的降级容器

Raw Element 的文本和 Custom 内容都不是可信 HTML。插件渲染 HTML 时必须经过显式的 trusted renderer 边界,不能让 Raw 内容绕过转义。

Source Range

语义元素输出其原始 byte range:

data-notist-start="12" data-notist-end="28"

这些 metadata 为未来的点击预览跳转源码、Hover fragment 和选择同步保留稳定接口。

Annotation 可能跨越多个 Element 或 block,不能总是直接映射为一个合法嵌套的 DOM 节点。Element range 可以直接进入 metadata;需要可视化 Annotation 时,通过 range events 切分 Content,再生成正确嵌套的 annotated spans。

Static Build

静态导出使用 workspace 级命令:

notist build docs -o dist

构建器遍历整个 Workspace,并将 ModulePath 映射为 clean URL:

  • vault -> index.html

  • vault::grammar -> grammar/index.html

  • vault::designs::type system -> designs/type system/index.html

URL 中的路径 segment 必须单独 percent encode。页面之间使用相对链接,使构建结果可以直接从任意静态文件服务器或子路径部署。

Wiki Reference 在构建时解析:目标存在时输出链接;目标不存在时保留可见文本和 unresolved 样式。语法、analysis 或 evaluation diagnostics 不阻止生成其他可用页面,但 notist build 应输出 diagnostics 并返回非零退出码。

站点级职责属于 CLI build 层,包括:

  • 完整 HTML page shell。

  • vault 名称、当前 Module 标识和模块导航。

  • _notist/style.css 等共享资源。

  • 虚拟目录模块页面。

  • ModulePath 到输出目录与相对 URL 的映射。

默认构建不清空整个输出目录,避免误删用户文件。它只创建或覆盖本次 Workspace 对应的页面和 _notist 资源;删除旧产物必须由显式 --clean 请求触发。

Local Preview

本地预览复用 static build,不维护独立的动态渲染 API:

notist preview .
notist preview . --port 3000 --no-open
notist preview . --host 0.0.0.0 --port 3000

默认行为:

  • 通过 vault::designs::D0012-daemon-and-client-interfaces 的 daemon 或 embedded service 打开 disk View。

  • 打开根模块 vault

  • 观察 disk View 的 snapshot publication 并 debounce。

  • 重新构建带 live reload 资源的完整站点。

  • 使用系统分配的空闲端口并自动打开浏览器。

  • analysis 和 evaluation diagnostics 输出到 CLI。

  • 默认只监听 127.0.0.1

Preview 结构为:

filesystem event or explicit rebuild
  -> VaultEngine / Analyzer View
  -> publish WorkspaceSnapshot
  -> debounce snapshot generation
  -> build complete staging site
  -> replace served site
  -> publish preview generation over SSE
  -> browser reloads current clean URL

HTTP 服务只需要两个表面:

  • GET / 和各 Module clean URL:由静态目录服务返回页面。

  • GET /_notist/events:发送当前 revision 和后续 revision 更新。

页面中的 _notist/reload.js 使用 EventSource 订阅 revision。连接建立和重连时先收到当前值,因此浏览器可以发现断线期间错过的构建。完整页面 reload 不需要 WebSocket、DOM diff 或自定义文档协议。

Atomic Rebuild

文件变化后先在 staging 目录生成完整站点,成功后再替换当前服务目录。文件读取或构建过程出现致命错误时,旧站点继续可见,并在 CLI 输出 rebuild failed。

可恢复的 syntax、analysis 和 evaluation diagnostics 不属于构建基础设施失败。页面仍可使用降级语义生成,同时 diagnostics 在终端报告。

目录替换必须保证浏览器不会读到只写了一半的页面集合。需要更严格的跨平台原子切换时,服务状态持有不可变 generation 目录,而不是让请求观察原地删除后 rename 的中间状态。

Network Boundary

Preview 会暴露用户正在编辑的文档内容,因此默认只能监听 loopback。显式使用非 loopback 地址时必须显示警告。

源码读取、编辑操作或远程共享需要随机访问 token、Origin 检查和独立授权边界。只读静态页面与 revision stream 不应被自然扩展成无认证的编辑 API。

Workspace Consistency

Parse 的 byte range、源码文本和分析结果必须来自同一个 Workspace snapshot。不能在 analysis 完成后,再由 renderer 重新读取可能已经变化的文件。

Snapshot 的构造、revision、identity 与发布规则见 vault::designs::D0011-analyzer-and-workspace-snapshot;本文只规定 build 和 preview 如何消费捕获后的 snapshot。

Analysis 提供稳定 snapshot:

  • Module 保存 Arc<str> 或等价的不可变 source。

  • 提供按 ModulePath 查询 Module 的公开接口。

  • source、Parse、resolved references 和 diagnostics 属于同一 revision。

  • reload 完整构造新 snapshot,完成后整体替换旧 snapshot。

Preview、diagnostics、Hover 和 definition 共享同一个文档版本,而不是分别读取磁盘。