Give independent consumers their own rules, cascade layer ordering, and source-order counters so packages such as Ebox cannot pollute TP's default stylesheet or each other. Match generic class and state tokens by value and document caller-owned stylesheet lifecycle. Verified: make clean; make test (728/728); make compile WERROR=t; checkdoc tp-style.el; git diff --check; Ebox make test against ../tp.
21 KiB
tp API 语义规范
本文档是 tp 核心 API 的当前行为契约。代码、测试、README 与 docstring 若与本文冲突,应以经过测试的代码为准并同步修正文档。
tp 当前定位是 Emacs 文本属性的高层操作工具箱,以及一套受管理的命名层、层栈和响应式渲染模型。它尚不是全部原生文本/字符属性语义的等价替代品;完整范围与路线见 REPOSITORY-AUDIT.md。
TP 1.0 迁移的第一层纯计算合同已经落地在 tp-style.el:namespaced property schema、结构化 selector、确定性 cascade、逐属性继承、custom property、显式 tp-computed 和 Emacs 属性投影已经是当前 public behavior;retained object/surface/mount/transaction 尚未切换,仍以目标架构文档描述为未来合同。
Stage 2 canonical façade 已完成为内部模型:tp--native-range、tp--presence、tp--request、tp--match、tp--result 是模块间传递的规范记录。公开入口和历史返回值保持兼容,不因内部模型收敛而改变。
Stage 3 text-only 原生语义 façade 已完成:direct/effective/source-aware lookup、property change/any/not-all 与三种 mutation policy 组合均有明确公开边界。
Stage 4 managed lifecycle 已完成:managed metadata、attach/detach、diagnostics、transaction、参数化 mounted layer args/version refresh 与 theme generation diagnostics 均有公开边界。
Stage 5 overlay-aware 字符属性查询已完成:tp-lookup :char / :char-source 报告 Emacs 选出的字符属性值、overlay 来源和获胜 overlay 身份。overlay 创建、移动、删除、priority 管理和生命周期不属于 tp 契约。
1. 对象、范围与坐标
- 所有范围均采用半开区间
[START, END)。 - 字符串使用 Emacs 原生的 0-based 坐标,合法边界为
0到(length STRING)。 - 缓冲区使用 Emacs 原生位置,通常从
point-min开始;显式 BUFFER 和 nil(当前缓冲区)具有相同语义。 - 范围结果原则上使用目标对象的原生坐标。当前公开兼容例外是
tp-intervals/tp-intervals-map:缓冲区默认返回相对 START 的 offset,传入ABSOLUTE才返回可直接回传给写 API 的原生坐标。内部 canonical range 使用原生坐标,但该 relative 默认作为历史兼容例外保留。
2. 修改策略
| 调用族 | 字符串 | 缓冲区 |
|---|---|---|
tp-set / tp-reset / tp-add / tp-remove 整串形式 |
复制后返回新字符串 | 不适用 |
| 上述函数的 START/END 形式 | 原地修改显式字符串 OBJECT | 原地修改 |
tp-match-* / tp-regexp-* |
返回新字符串 | 原地修改,返回匹配范围 |
| 层栈修改函数 | 原地修改字符串 | 原地修改 |
tp-forward-do / tp-backward-do / tp-search-map |
原地修改;替换文本必须等长 | 原地修改;替换文本可增减长度 |
普通缓冲区写入使用 Emacs 的属性修改原语,因此遵循 Emacs 的 modified、undo 和 read-only 行为;tp 不把所有修改默认包装为 silent modification。响应式重渲染和 tp-text 文本替换为了恢复受管理输出会在内部允许修改 read-only 文本,这是当前实现边界,不代表公开 API 已提供统一的 read-only 策略。相同值刷新会尽量避免无意义地翻转 buffer-modified 状态。
3. presence、nil 与 wildcard
以下三种状态必须区分:
- 属性键不存在;
- 属性键存在,值为 nil;
- 属性键存在,值为非 nil。
tp-member 用于判断直接属性键是否存在;tp-at / plist-get 单独使用时不能区分前两种状态。删除最后一个子属性会移除空的父属性键,不会偶然留下 (PROPERTY nil)。
搜索 API 的规则是:
- 省略 VALUE:匹配该直接属性的任意已存在值;
- 显式传入 nil:只匹配“键存在且值为 nil”;
- 需要继续提供 OBJECT、次数或范围等后续位置参数时,传入公共唯一哨兵
tp-any-value表示任意值; - 自定义 PREDICATE 总是优先执行,并接收请求 VALUE(可能是
tp-any-value)和实际属性值。
属性缺失的范围不属于搜索结果,即使搜索值是 nil。
4. 搜索结果
tp-search对字符串和缓冲区范围都返回(START END VALUE)列表。tp-forward/tp-backward的字符串路径返回前/后 N 个(START END VALUE);缓冲区路径移动 point,并返回第 N 次搜索的prop-match。tp-forward-do/tp-backward-do只在第 N 个匹配上调用函数;匹配不足 N 个时不调用函数,返回实际找到的数量。tp-search-map处理全部匹配并返回处理数量。
tp-forward / tp-backward 的对象相关结果差异是现存公开兼容契约。内部搜索路径已收敛到 canonical match/result 记录;公开返回结构暂不改变。
5. 原生文本查询与修改策略
Stage 3/5 查询 façade 已完成:text modes 保持 text-only,char modes 委托 Emacs 的 overlay-aware 字符属性查询。
tp-lookup 返回 tp-lookup-result 记录,字段为 property、value、present-p、source、mode、object、position、overlay。text modes 中 overlay 始终为 nil;:char / :char-source 中 overlay 为 Emacs 选出的获胜 overlay,若文本 fallback 获胜则为 nil。
| MODE | 语义 |
|---|---|
:text-direct |
只读取直接文本属性;显式 nil 与缺失通过 present-p/source 区分 |
:text-effective |
值使用 get-text-property;source 使用 text-only 来源解释 |
:text-source |
返回 direct/category/alias/default/absent 来源和值,不查看 overlay |
:char |
使用 get-char-property-and-overlay 的值,按 Emacs overlay/text 优先级解析 |
:char-source |
同 :char,并在 overlay 获胜时把 source 设为 :overlay、overlay 设为获胜 overlay |
source 取值为 :text-direct、:category、:alias、:default、:overlay 或 :absent。direct 显式 nil 的结果是 present-p 为 t、value 为 nil、source 为 :text-direct;alias nil 与 Emacs 原生语义一致,会继续寻找后续 alias/default;缺失属性的结果是 present-p 为 nil、value 为 nil、source 为 :absent。
tp-property-change 是 Emacs property change 原语的显式封装::direction :next / :previous 选择 next/previous,传入 :property 时使用 single-property change,省略时使用 all-property change。
tp-property-any / tp-property-not-all 是 text-property-any / text-property-not-all 的薄封装,保留 Emacs 对显式 nil、边界和对象的行为。
tp-with-mutation-policy 只接受三种有效组合:
| POLICY | 语义 |
|---|---|
(:modified :ordinary :read-only :respect) |
普通修改,尊重 read-only |
(:modified :ordinary :read-only :inhibit) |
绑定 inhibit-read-only,普通 modified/undo 行为 |
(:modified :silent :read-only :inhibit) |
绑定 inhibit-read-only 并使用 with-silent-modifications |
(:modified :silent :read-only :respect) 明确拒绝,因为 silent modification 与尊重 read-only 不能同时满足。
insert、copy、yank、stickiness、narrowing 和 indirect buffer 行为直接委托 Emacs;tp 不为这些原生操作提供 wrapper。
6. 直接模板展开与 managed mount
层名有两种不同用途:
6.1 直接模板展开
把非响应式层名传给 tp-set、tp-reset、tp-add、tp-match-* 或 tp-regexp-* 时,层定义展开为普通属性,通常不保留 tp-name。结果不能依赖层名进行后续移动、隐藏、按名删除或静态重定义刷新。
匿名响应式属性和响应式层需要保留 tp-name 才能登记与刷新,这是直接路径中的受管理例外。
6.2 managed mount
tp-push-layer / tp-put-layer 明确保留 tp-name 和必要的 tp-layers 状态。mounted layer 可以被查询、移动、隐藏、显示、删除和响应式刷新。
非参数化层重新定义后,已挂载区域按 old/new 所有权协调:
- 新定义写入其拥有的键;
- 旧定义拥有、但新定义不再拥有的键,仅在当前值仍等于旧值时移除;
- 外部已经改写的值不会被当作旧层残留删除。
所有 managed mount 都携带 lifecycle metadata。即使只有单个 managed layer,只要存在 tp-meta,权威存储也使用 tp-layers;直接渲染属性和 tp-layer-stack-at 等 public stack query 不暴露 tp-meta。
参数化层的已挂载 entry 保存调用实参、形参表和 definition version;重新定义参数化层后,既有 managed entry 会按保存的 args 刷新。历史无 metadata 的 entry 按 legacy entry 保守处理。
tp-attach-managed-layers 扫描已经进入缓冲区的 managed storage,补齐/规范化 metadata,登记发现的层,并返回层名列表。tp-detach-managed-layers 移除 managed storage;KEEP-RENDERED 非 nil 时保留当前可见渲染属性为普通文本属性。
tp-managed-layer-diagnostics、tp-managed-buffer-diagnostics 和 tp-managed-diagnostics 是只读诊断入口,报告 entries、args、registry、errors 与 theme diagnostics。tp-managed-diagnostics 的 theme 部分报告 generation、last hook source、refresh mode、refreshed ranges 和 errors;这是 lifecycle 诊断,不是性能基准。
tp-layer-transaction 在给定范围内执行 managed stack 修改。成功返回结构化 plist,其中 :status 为 ok、:ok 为 t,:result 保留 FUNCTION 的返回值;失败时恢复事务前文本/属性快照并默认发出 tp-layer-transaction-error,NOERROR 非 nil 时返回结构化失败 plist。
7. tp-text 替换
- 初次应用和响应式更新都按内嵌字符串的真实属性 interval 处理,不从位置 0 采样后扩散到整段。
- 调用者显式属性覆盖内嵌属性;显式 nil 也是有效覆盖值。
- 未被调用者覆盖的内嵌属性按各自 interval 保留。
- 每个 interval 算出待写属性后,
tp-set只覆盖这些键并保留其他目标属性,tp-reset替换目标的完整属性集合,tp-add则对目标已有的 face-family 与嵌套 plist 继续合并;文本内容相同和发生替换时遵守同一规则。 - 字符串和缓冲区路径遵守相同的 per-interval 属性计算;对象的复制/原地策略仍按第 2 节执行。
8. 错误边界
- 未定义或无法解析的层使用
tp-unresolved-layer表达。 - 栈 API 的 NOERROR 只抑制
tp-unresolved-layer;参数化层 body、计算、属性结构和其他内部错误必须传播。 - 隐藏层存储发生所有权冲突时使用
tp-layer-conflict。 - transform 失败或返回非字符串、compute 失败都属于业务输出失败,必须传播。
- watcher 属于 observer:单个 watcher 失败不会阻断 managed update;失败会记录到
tp-reactive-observer-errors(newest first)并输出消息。 - 公开边界不应把内部失败转换为默认值后继续写入。
9. 隐藏层与外部直接修改
存在隐藏层时,tp-layers 保存完整 managed stack,其他直接属性是第一个可见层的渲染缓存。两类调用采用不同但明确的策略:
- definition/reactive refresh 知道正在刷新哪个定义,也知道直接属性对应哪个可见 entry;它先把原生直接编辑协调进该可见 entry,再执行 old/new 所有权刷新,因此外部改写值不会被当作旧定义残留删除;
- 普通 stack decode/write 缺少这次 definition refresh 的所有权上下文,缓存不一致时会在写入前发出
tp-layer-conflict,且不修改层栈; - 所有层都隐藏时没有可接收直接属性的可见 entry,此时出现直接属性始终发出
tp-layer-conflict。
tp 不会把外部属性收编成新的匿名层,也不会静默覆盖它。
10. 返回值现状
核心写 API 的返回值仍保留历史差异:
- buffer/region
tp-set/tp-reset/tp-add:(START . END); - 整串复制式写入:新字符串;
- buffer
tp-remove/tp-clear:nil; - stack mutator:修改的 property-run 数量;
tp-put-layer/tp-push-layer:显式 OBJECT 或(START . END)。
这些返回值是当前兼容契约。property-run 数量会受无关 interval 边界影响,不应被当成稳定业务标识。内部 request/result 模型已建立,但不会改变这些历史 public returns。
11. 明确不在当前完整契约内
以下能力仍需设计或补齐,不能据现有 API 推断:
- overlay lifecycle(创建、移动、删除、priority 管理);
- insert/copy/yank/stickiness 的 tp wrapper 或 managed workflow;
- mutation policy 三种组合之外的统一 read-only、silent modification、undo 策略;
- 字符串/缓冲区完全一致的搜索结果结构;
- observer 错误的清理、重试与汇总策略。
12. Schema-driven cascade
tp-define-property只接受带 namespace 的 symbol id,例如text/face或ebox/width;schema replacement 在完整验证后一次写入,失败不会破坏旧 definition。- structured selector 原生支持 type/id/class/attribute/state、compound、descendant/child/adjacent/general sibling,以及
:is、:where和:not;tp-selector-specificity与 rule matching 使用同一 AST。 - cascade 顺序固定为 importance、origin、layer、specificity、scope proximity、source order;normal 与 important declaration 的 layer 顺序按 CSS 规则相反,unlayered normal 高于 layered normal。
tp-stylesheet-create建立相互隔离的 rule、layer order 与 source order domain;tp-stylesheet-add-rule :stylesheet SHEET只写入该实例,tp-compute-style :rules SHEET只读取该实例,避免不同 consumer 通过默认全局 stylesheet 相互污染。- 独立 stylesheet 的生命周期归创建者所有;
tp-style-reset只清理 TP 的全局 schema、named style 与 default stylesheet,调用方必须用tp-style-reset-rules SHEET显式清理自己的实例。 initial、inherit、unset、revert和revert-layer必须由tp-wide-value显式构造,普通同名 Elisp symbol 保持 literal。- 普通 function value 永远不执行;只有
tp-computed包装的 function 在计算时执行一次,其返回值不二次调用。 --namecustom property 默认继承;tp-var支持 fallback 和 cycle invalidation。属性 schema 的 normalizer/validator 在变量与 wide value 求值后执行。tp-compute-style返回tp-computed-style,保存 canonical values、resolved custom properties 和可选 provenance;tp-project-style是把 schema projector 汇总为最终 Emacs text properties 的唯一纯投影入口。- 静态
define-tp和静态define-tps生成层会同步编译为同名 canonical style;参数化或旧$var响应式定义不会冻结当前值为 style,待新的 signal/binding runtime 接管其动态 source。
13. Signals、bindings 与 transaction
tp-signal-create创建 global 或 buffer-scoped source;tp-signal-read仅在 binding compute context 中登记依赖,tp-signal-peek永不登记依赖。buffer kill 会 dispose scoped signal;global signal 可用tp-signal-dispose显式结束 lifecycle,两者都会移除 subscriptions。tp-bind的 identity 是 owner object identity 加 caller-namespaced key;重复安装复用同一 binding,compute definition 变化才使它 dirty。tp-binding-read读取 memoized value 并建立 binding→binding edge。- 每次成功 compute 以本次实际读取的依赖替换旧依赖;条件分支切换后旧 signal 不再触发。binding value 经其 equality comparator 判等,相等结果不 invalidates downstream。
- signal write 先写入 transaction-local candidate state。最外层
tp-with-transaction只遍历 exact dirty closure,去重并按 binding dependency 拓扑惰性求值;compute 内嵌 write 排队稳定,不递归执行。 tp-transaction-participate KEY PUBLISH ROLLBACK只允许在 outer transaction 内登记。所有 surface candidate 发布并切换 client state 后,participant 按登记顺序执行PUBLISH;任一后续步骤失败时,已经进入 publication 的 participant 按逆序执行ROLLBACK。KEY 在同一 transaction 内唯一,两个函数均不得接收参数,rollback 必须能够撤销 publish 已经开始后的部分副作用。- 任一 compute 或 cycle 失败会恢复 committed signal values、last successful binding values、dependencies、dirty state、owner registry 和 scheduler counters。cycle condition 携带 namespaced binding-key path。
tp-variable-signal是 global/buffer-local Elisp variable(包括后续$varcompiler)的 source adapter;它只转发精确 scope 的 write,不调用 legacy layer renderer。tp-reactive-counters只公开 invalidated、recomputed、skipped、subscription-added、subscription-removed 五个工作量计数,不暴露 internal hash shape。
14. Retained surface、range ownership 与 publication
tp-surface-plan-create只接受 key/kind/text/props/children/tags/capability 纯数据;text 与 children 互斥,同一 parent 的显式 key 不可重复,constructor 与每次 prepare 都做 defensive copy。普通 function property value 保持 literal identity,plan 不携带 marker、position、binding 或 producer closure。- producer 在 active prepare context 中先用
tp-object-ensure取得 candidate handle,再返回 plan 或tp-surface-result-create。成功 publication 才使新 handle live;失败、materialize 返回或 orphan/cross-surface validation 失败都会释放 candidate bindings、anchors 与 subscriptions。tp-object-resolve只读 live key path,不创建 identity。 tp-object-retain显式声明一个没有可见字符也应随 candidate 晋升的 logical object;未进入 plan、未 retain、也未 attachment 的 touched object 仍以tp-orphan-object拒绝。content producer 可用tp-object-attach-fragment把同一 logical object 挂到多个 plan fragment;attachment 只存在 prepare/side state,不进入 pure plan。tp-object-mounts通过 object-keyed index 返回当前数值 start/end 与 opaque tags 的防御性快照,不暴露 live marker。contentcapability 拥有一个 disjoint text span,使用 character common-prefix/suffix 与 direct-property run diff;外部字符编辑使其 stale。propertiescapability 不能携带 text,只能通过tp-object-attach-range写 attached anchor。host edit 位于 anchor 前方时 marker 正常移动;跨入 owned range 时按 boundary policy shorten/remove/stale。tp-surface-update-scoped接受同 surface 的 live object handles 和一个完整 candidate。scope 只在当前 transaction 内有效;TP 通过 object→mount index 得到授权范围,支持一个 logical object 的多个离散 mounts,并在 prepare 阶段证明 candidate 没有改变范围外输出。默认 mismatch 发出tp-scope-mismatch且零发布;只有显式(:on-mismatch root)才允许 full-root fallback。后续普通 reactive recompute 或tp-surface-update不继承这次 scope。- properties ledger 保存 baseline、last-published value 与 contribution anchors。prepare compare-before-write;外部 property override 触发
tp-property-conflict且保持旧 revision。tp-range-rebase显式接受当前 host runs 为新 baseline;unmount 只恢复仍等于 TP last-published value 的子区间,冲突子区间保持外部值并进入 report。 - 同一 surface 的 overlapping properties contributions 按 plan order 和 native property schema merge;独立 surfaces 当前不得重叠字符 ownership。这个限制把 journal owner 保持为唯一 surface,避免两套 baseline 静默覆盖。
- 最外层 transaction 先完成所有 producer/plan/conflict validation,再按 surface id publish,随后执行 transaction participants,最后提交 source values。multi-buffer change group 负责 text rollback,TP 的精确 property journal 覆盖
with-silent-modifications;失败先逆序撤销已经进入 publication 的 participants,再恢复 buffer direct properties、markers/index、objects、plan、client state、revision、bindings 与 signal values。publish 中 killed buffer 不复活,其他 surfaces 与 sources 回滚。 tp-surface-report的字段只使用 transaction/surface/source/binding/object/text/property/conflict/observer/timing 通用词汇。observer 在成功 commit 且 publishing transaction 的动态范围退出后执行,因此 observer 中的 signal write 会开启新 transaction;observer error 只写入 report,不回滚。
15. 简单与响应式便利入口
tp-propertize STRING DECLARATIONS接受 Emacs 原生 property plist,把它转换为 canonicaltext/declarations,经 schema/cascade/projector 后应用到 STRING 的防御性副本;它不建立 object、binding、anchor 或 surface。tp-apply BUFFER START END DECLARATIONS使用同一 projection 与现有 range mutation primitive,只修改声明过的 direct properties,保留文本和未声明 property,并返回(START . END)。tp-watch BUFFER START END COMPUTE创建一个 properties-only surface。COMPUTE 是返回原生 property plist 的零参数函数;其 signal/binding dependencies 由 exact graph 收集。返回值就是可传给tp-surface-inspect、tp-surface-report和tp-surface-unmount的 opaque surface。首次 publication 失败会释放 convenience 层创建的 anchor。