Replace the legacy managed layer renderer with one independent retained/reactive text runtime. TP now owns exact dependencies, stable objects, marker-backed mounts, property contribution composition, atomic publication, rollback, and direct text-property facades without ECSS or Ebox dependencies.\n\nBREAKING CHANGE: remove tp-render, tp-stack, scan-driven managed layers, inline runtime metadata, TP-owned CSS cascade APIs, and dollar-variable declarations.\n\nVerified: 290/290 ERT, shuffled 290/290 (seed 20260806), 8/8 doctests, WERROR compile-all, checkdoc, package-lint, diff-check, and isolated TP-only load.
202 lines
10 KiB
Markdown
202 lines
10 KiB
Markdown
# TP
|
||
|
||
TP 1.0 是一个可独立使用的 Emacs retained/reactive text runtime。它把声明式属性、响应式数据和稳定文本对象投影到 string 与 buffer,并拥有文本属性 contribution 合成、精确依赖追踪、保留式身份、marker-backed mount、diff、事务、回滚和最终 Buffer publication。
|
||
|
||
TP 不依赖 Ebox 或 ECSS,也不实现 CSS selector、stylesheet、specificity、cascade winner、Box、Flex、Grid、测量或布局。需要 CSS 的调用者可以先用 ECSS 算出最终 declarations,再交给 TP 发布;TP 自身只理解 Emacs 文本属性和通用 retained text surface。
|
||
|
||
英文文档:[README.md](README.md)。
|
||
|
||
## 运行要求
|
||
|
||
- Emacs 28.1 或更高版本。
|
||
- 没有第三方运行时依赖。
|
||
|
||
```elisp
|
||
(add-to-list 'load-path "/path/to/tp")
|
||
(require 'tp)
|
||
```
|
||
|
||
## 选择最小的公共入口
|
||
|
||
| 需求 | API | 是否建立 live runtime |
|
||
| --- | --- | --- |
|
||
| 返回带属性的字符串 | `tp-propertize` | 否 |
|
||
| 一次性装饰已有 buffer 范围 | `tp-apply` | 否 |
|
||
| 响应式装饰 host-owned 文本 | `tp-watch` | 是,`properties` capability |
|
||
| 管理 retained text content | `tp-surface-mount` / `tp-surface-update` | 是,`content` capability |
|
||
| 检查或卸载 retained publication | `tp-surface-report` / `tp-surface-inspect` / `tp-surface-unmount` | 是 |
|
||
|
||
一次性 API 和 retained API 使用相同的 property policy 与 projection 语义。一次性调用不会创建 object、binding、marker、subscription 或 surface state。
|
||
|
||
## 静态文本属性
|
||
|
||
```elisp
|
||
(let* ((callback (lambda (_window _object _position) "Open"))
|
||
(text
|
||
(tp-propertize
|
||
"Hello"
|
||
(list 'face '(:foreground "white" :background "navy")
|
||
'help-echo callback
|
||
'keymap nil))))
|
||
text)
|
||
```
|
||
|
||
显式 `nil` 和属性不存在是两种状态。上例中的 `keymap` 存在且值为 `nil`。函数对象始终是 literal data,因此 TP 会保留 `callback`,不会调用它。
|
||
|
||
一次性修改已有范围而不改变文字:
|
||
|
||
```elisp
|
||
(with-current-buffer (get-buffer-create "*tp-demo*")
|
||
(erase-buffer)
|
||
(insert "abcdef")
|
||
(tp-apply (current-buffer) 2 5 '(face italic)))
|
||
```
|
||
|
||
原有的 `tp-set`、`tp-reset`、`tp-add`、`tp-remove`、`tp-clear`、`tp-get`、`tp-at`、`tp-member`、match、regexp、search、navigation、interval 和 native query API 继续作为普通文本属性操作的直接 façade。
|
||
|
||
## 可复用声明配方
|
||
|
||
`define-tp` 和 `define-tps` 定义可以展开为直接 Emacs 属性的复用配方。它们只是 definition-time convenience,不是 live mounted layer,也不会把 identity 或 provenance 写入显示文本。
|
||
|
||
```elisp
|
||
(define-tp link-style (foreground)
|
||
`(face (:foreground ,foreground :weight bold)
|
||
mouse-face highlight
|
||
help-echo "Open item"))
|
||
|
||
(tp-set "Project" '(link-style "#58a6ff"))
|
||
```
|
||
|
||
属性值需要求值时使用 `tp-computed`;任何普通函数对象仍然按 literal 保存。
|
||
|
||
```elisp
|
||
(defvar my-height 1.2)
|
||
|
||
(define-tp sized-label ()
|
||
`(face ,(tp-computed (lambda () (list :height my-height)))))
|
||
```
|
||
|
||
computed function 在当前 binding/prepare context 中执行,因此其中的 `tp-signal-read` 和 `tp-binding-read` 会建立精确依赖。返回值只 normalize/project 一次,然后作为 literal data 使用。
|
||
|
||
## 响应式已有文本
|
||
|
||
`tp-watch` 装饰 marker-backed host range,但不拥有或替换其中的文字:
|
||
|
||
```elisp
|
||
(let ((online (tp-signal-create nil)))
|
||
(with-current-buffer (get-buffer-create "*tp-status*")
|
||
(erase-buffer)
|
||
(insert "offline")
|
||
(tp-watch
|
||
(current-buffer) 1 8
|
||
(lambda ()
|
||
(list 'face
|
||
(list :foreground
|
||
(if (tp-signal-read online) "green" "red")))))))
|
||
```
|
||
|
||
signal 在被读取时记录真正依赖它的 binding。修改一个 signal 只会使它的订阅者失效;条件分支改变后,不再读取的 source 会自动取消订阅。相等写入是 no-op,`tp-with-transaction` 内的重复写入会让每个受影响 binding 最多重算一次。
|
||
|
||
`tp-watch` 返回底层 surface handle,可以交给 `tp-surface-inspect`、`tp-surface-report` 或 `tp-surface-unmount`。
|
||
|
||
## Retained content
|
||
|
||
retained producer 接收 prepare context,在生成输出前取得稳定 object、安装 object-local binding,并返回纯 surface plan:
|
||
|
||
```elisp
|
||
(let* ((status (tp-signal-create "ready"))
|
||
(producer
|
||
(lambda (context)
|
||
(let* ((object (tp-object-ensure context nil 'status 'text))
|
||
(binding
|
||
(tp-bind object '(demo . status)
|
||
(lambda () (tp-signal-read status)))))
|
||
(tp-surface-plan-create
|
||
:key 'status
|
||
:kind 'text
|
||
:text (tp-binding-read binding)
|
||
:props '(face bold)
|
||
:capability 'content))))
|
||
(buffer (get-buffer-create "*tp-retained*"))
|
||
(surface
|
||
(tp-surface-mount buffer producer '(:capability content))))
|
||
(tp-signal-set status "done")
|
||
(tp-surface-report surface))
|
||
```
|
||
|
||
plan 字段只有 `key`、`kind`、`text`、`props`、`children`、`tags` 和 `capability`,不包含 marker、buffer position、patch operation、producer closure 或 client continuation。constructor 会防御性复制调用者持有的 string 和 property data。
|
||
|
||
稳定身份只在一个 surface 内有效。`tp-object-ensure` 按 parent、sibling key 和 kind reconcile;`tp-object-resolve` 按 key path 返回 live opaque handle。`tp-surface-update-scoped` 接收完整 candidate,但只授权一个或多个 retained object 当前 mount 范围内的输出变化;除非调用者显式选择 root fallback,否则越界变化会直接失败。
|
||
|
||
`tp-surface-materialize-string` 使用相同 producer/plan 语义生成一次性 string,但不建立 live surface。返回前会释放 ephemeral object、binding、subscription 和 anchor。
|
||
|
||
## Host range 与属性所有权
|
||
|
||
`properties` capability 使用 opaque range anchor。producer 通过 `tp-range-anchor-create` 创建 anchor,再用 `tp-object-attach-range` 挂到 object 上。marker 跟随 host edit,而 plan 仍然不携带位置。
|
||
|
||
TP 为每个 property interval 保存 host baseline 和各个 TP contribution。重叠 contribution 通过已注册的 property policy 合成。如果外部代码在 TP publication 后修改同名属性,下一次更新会报告 `tp-property-conflict`,不会覆盖外部值。`tp-range-rebase` 显式接受当前 host 值作为新 baseline。unmount 只恢复仍由 TP 拥有的值,并保留冲突的 host edit。
|
||
|
||
## 事务与失败语义
|
||
|
||
`tp-with-transaction` 批量处理 signal writes 和所有实际受影响的 surfaces。TP 先 prepare 所有 candidate,再按稳定顺序 publish。compute、validation、buffer write、marker/index publication 或 transaction participant 任一步失败,signal values、binding values/dependencies、文本、属性、marker、index、plan、client state、revision 和 previous report 都会一起恢复。
|
||
|
||
observer 只在成功提交之后运行,observer failure 只记录,不回滚已完成的 transaction。publication 过程中被 kill 的 buffer 保持死亡,rollback 不会把它重新创建。
|
||
|
||
## Property policy
|
||
|
||
`tp-define-property-policy` 为一个最终 Emacs text property 注册通用语义:
|
||
|
||
- normalization 与 validation;
|
||
- no-op 判断使用的 equality;
|
||
- contribution merge;
|
||
- 到最终 property value 的 projection;
|
||
- 包括 present `nil` 在内的显式 presence。
|
||
|
||
`tp-register-text-property` 为原生 property 建立默认 policy。`face` contribution 使用 merge 语义,其他属性使用各自注册的 policy。`tp-merge-declarations`、`tp-define-style` 和 `tp-style-declarations` 只处理 direct declarations,不实现 CSS cascade。
|
||
|
||
## 公共 runtime API 家族
|
||
|
||
| 家族 | 主要公共 API |
|
||
| --- | --- |
|
||
| Property policy 与 declaration | `tp-define-property-policy`, `tp-register-text-property`, `tp-text-declarations`, `tp-computed`, `tp-resolve-value`, `tp-merge-declarations`, `tp-define-style`, `tp-style-declarations` |
|
||
| Signal 与 binding | `tp-signal-create`, `tp-signal-read`, `tp-signal-peek`, `tp-signal-set`, `tp-bind`, `tp-binding-read`, `tp-with-transaction`, `tp-reactive-counters` |
|
||
| Object 与 plan | `tp-object-ensure`, `tp-object-retain`, `tp-object-attach-fragment`, `tp-object-resolve`, `tp-object-mounts`, `tp-surface-plan-create`, `tp-surface-result-create` |
|
||
| Host range | `tp-range-anchor-create`, `tp-object-attach-range`, `tp-range-rebase` |
|
||
| Surface | `tp-surface-materialize-string`, `tp-surface-mount`, `tp-surface-update`, `tp-surface-update-scoped`, `tp-surface-unmount`, `tp-surface-at-point`, `tp-surface-report`, `tp-surface-inspect` |
|
||
| Direct façade | `tp-propertize`, `tp-apply`, `tp-watch`, `tp-set`, `tp-reset`, `tp-add`, `tp-remove`, `tp-clear`, `tp-get`, `tp-at`, `tp-member` |
|
||
|
||
ownership、生命周期、错误和返回值合同见 [API semantics](docs/API-SEMANTICS.md),模块边界和完整事务流程见 [architecture](docs/ARCHITECTURE.md)。
|
||
|
||
## TP 1.0 迁移
|
||
|
||
TP 1.0 直接删除 0.3 的 managed stack/renderer runtime,不使用隐藏兼容分支。删除的行为包括 `tp-render.el`、`tp-stack.el`、stack mutation APIs、`tp-text`、`$variable` declarations、layer-to-buffer registry、scan-driven refresh、managed attach/detach/diagnostics,以及以 `tp-name`/`tp-layers`/`tp-meta` 作为权威 runtime storage 的机制。
|
||
|
||
可复用静态声明使用 direct recipe;已有文本的响应式属性使用 `tp-watch`;响应式文字或结构化 UI 使用 retained content surface。TP 不会自动扫描历史 propertized text 来重建 runtime identity。
|
||
|
||
## 示例
|
||
|
||
- [静态属性](examples/static-properties.el)
|
||
- [响应式状态](examples/reactive-status.el)
|
||
- [Retained dashboard](examples/retained-dashboard.el)
|
||
- [诊断装饰](examples/diagnostic-decoration.el)
|
||
|
||
这些示例只调用 TP public API,并在没有 Ebox 或 ECSS load-path 的环境中测试。
|
||
|
||
## 验证
|
||
|
||
```sh
|
||
make test
|
||
make test-shuffled SHUFFLE_SEED=20260806
|
||
make doctest
|
||
make compile-all WERROR=t
|
||
make checkdoc
|
||
make package-lint
|
||
make diff-check
|
||
```
|
||
|
||
结构测试还会证明:正常 signal/surface update 不扫描 `buffer-list`,不从显示文本搜索 identity;相等结果不会发布新 revision;unmount/kill cleanup 会释放 subscriptions 和 marker-backed runtime state。
|
||
|
||
## 许可证
|
||
|
||
GPL-3.0-or-later,详见 [LICENSE](LICENSE)。
|