feat(tp): isolate stylesheet rule domains

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.
This commit is contained in:
Kinneyzhang 2026-08-06 13:59:08 +08:00
parent 07b84b0685
commit 1195297011
6 changed files with 139 additions and 43 deletions

View File

@ -174,7 +174,7 @@
Since 0.2.0 the library is organized as a family of layered modules (`tp-core`, `tp-style`, `tp-reactive`, `tp-layer`, `tp-ops`, `tp-search`, `tp-render`, `tp-stack`, `tp-query`, `tp-palette`, `tp-builtins`) behind the umbrella file `tp.el``(require 'tp)` still loads everything, so nothing changes for users. See [Installation](#installation) for the module map.
The TP 1.0 migration has begun with a pure schema-driven cascade kernel. `tp-style.el` adds namespaced property schemas, structured selectors, origin/importance/layer/specificity/scope/source-order precedence, property-specific inheritance, tagged CSS-wide values, inherited custom properties, explicit `tp-computed` sources, projection to Emacs properties, and read-only winner provenance. It deliberately owns no buffers, markers, mounts, or reactive subscriptions; retained surfaces will build on this kernel in later migration phases.
The TP 1.0 migration has begun with a pure schema-driven cascade kernel. `tp-style.el` adds namespaced property schemas, structured selectors, origin/importance/layer/specificity/scope/source-order precedence, property-specific inheritance, tagged CSS-wide values, inherited custom properties, explicit `tp-computed` sources, projection to Emacs properties, and read-only winner provenance. `tp-stylesheet-create` gives each independent consumer its own rule, layer, and source-order domain instead of forcing unrelated packages through one process-global stylesheet. It deliberately owns no buffers, markers, mounts, or reactive subscriptions; retained surfaces will build on this kernel in later migration phases.
`tp-reactive.el` now also provides the TP 1.0 exact dependency runtime: signals invalidate only their subscribed bindings, binding reads form memoized binding→binding edges, conditional computations replace obsolete dependencies, and the outermost `tp-with-transaction` flushes each dirty binding once. Candidate signal writes and binding values/dependencies commit together; compute, cycle, publication, or transaction-participant failures roll them back. `tp-transaction-participate` lets a client promote opaque side state after surfaces publish while supplying its inverse for the same rollback boundary. Global and buffer-local variable adapters reuse this graph without making text properties or buffer scans the runtime database.
@ -516,11 +516,11 @@ A complete overview of all tp.el functions organized by category:
| `tp-subject-create` / `tp-subject-set-children` | Build consumer-independent selector subjects and relations |
| `tp-selector-match-p` / `tp-selector-specificity` | Match the structured selector AST and compute its specificity |
| `tp-define-style` / `tp-style-declarations` / `tp-undefine-style` | Register, inspect, and remove defensive named declarations |
| `tp-stylesheet-add-rule` | Add an origin/layer/scope-aware structured rule |
| `tp-stylesheet-create` / `tp-stylesheet-add-rule` | Create an isolated rule/layer domain and add origin/layer/scope-aware structured rules |
| `tp-wide-value` / `tp-important` / `tp-var` | Construct unambiguous cascade values without reserving ordinary Elisp symbols |
| `tp-computed` | Mark the only function values TP should execute |
| `tp-compute-style` / `tp-project-style` | Compute canonical values/provenance and project final Emacs text properties |
| `tp-style-reset-rules` / `tp-style-reset` | Reset stylesheet-only state or the complete style registry |
| `tp-style-reset-rules` / `tp-style-reset` | Reset one isolated/default stylesheet, or TP's global schemas, named styles, and default stylesheet |
#### Signals and Bindings

View File

@ -173,7 +173,7 @@
自 0.2.0 起,本库被组织为一组分层模块(`tp-core`、`tp-style`、`tp-reactive`、`tp-layer`、`tp-ops`、`tp-search`、`tp-render`、`tp-stack`、`tp-query`、`tp-palette`、`tp-builtins`),由伞形文件 `tp.el` 统一加载 — `(require 'tp)` 仍会加载全部模块,对用户没有任何变化。模块一览见[安装](#安装)。
TP 1.0 迁移已经从纯 schema-driven cascade kernel 开始。`tp-style.el` 新增 namespaced property schema、结构化 selector、origin/importance/layer/specificity/scope/source-order 优先级、逐属性继承、带 tag 的 CSS-wide 值、可继承 custom property、显式 `tp-computed` source、Emacs 属性投影和只读 winner provenance。这个模块刻意不拥有 buffer、marker、mount 或响应式订阅;后续 retained surface 会建立在同一个 kernel 上。
TP 1.0 迁移已经从纯 schema-driven cascade kernel 开始。`tp-style.el` 新增 namespaced property schema、结构化 selector、origin/importance/layer/specificity/scope/source-order 优先级、逐属性继承、带 tag 的 CSS-wide 值、可继承 custom property、显式 `tp-computed` source、Emacs 属性投影和只读 winner provenance。`tp-stylesheet-create` 让每个独立 consumer 拥有自己的 rule、layer 与 source-order domain不必让无关包共享一份进程全局 stylesheet。这个模块刻意不拥有 buffer、marker、mount 或响应式订阅;后续 retained surface 会建立在同一个 kernel 上。
`tp-reactive.el` 现在也提供 TP 1.0 的精确依赖 runtimesignal 只 invalidates 真实订阅的 bindingbinding read 建立 memoized binding→binding edge条件计算会替换已经失效的旧依赖最外层 `tp-with-transaction` 对每个 dirty binding 最多 flush 一次。candidate signal write 与 binding value/dependencies 一起提交compute、cycle、publication 或 transaction participant 失败时一起回滚。`tp-transaction-participate` 允许 client 在 surface 发布后晋升 opaque side state同时为同一 rollback boundary 提供逆操作。global 和 buffer-local variable adapter 复用同一 graph不再把文本属性或 buffer scan 当作新 runtime database。
@ -511,11 +511,11 @@ tp.el 所有函数按类别组织的完整概览:
| `tp-subject-create` / `tp-subject-set-children` | 构造与具体 consumer 无关的 selector subject 和关系 |
| `tp-selector-match-p` / `tp-selector-specificity` | 匹配结构化 selector AST 并计算 specificity |
| `tp-define-style` / `tp-style-declarations` / `tp-undefine-style` | 注册、查询并移除防御性复制的 named declarations |
| `tp-stylesheet-add-rule` | 添加支持 origin/layer/scope 的结构化 rule |
| `tp-stylesheet-create` / `tp-stylesheet-add-rule` | 创建隔离的 rule/layer domain添加支持 origin/layer/scope 的结构化 rule |
| `tp-wide-value` / `tp-important` / `tp-var` | 构造无歧义 cascade 值,不占用普通 Elisp symbol |
| `tp-computed` | 标记 TP 唯一应该执行的 function value |
| `tp-compute-style` / `tp-project-style` | 计算 canonical values/provenance 并投影最终 Emacs text properties |
| `tp-style-reset-rules` / `tp-style-reset` | 仅重置 stylesheet state或重置完整 style registry |
| `tp-style-reset-rules` / `tp-style-reset` | 重置某个隔离/default stylesheet或重置 TP 的全局 schema、named style 与 default stylesheet |
#### Signals 与 Bindings

View File

@ -177,6 +177,8 @@ tp 不会把外部属性收编成新的匿名层,也不会静默覆盖它。
- `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 ordernormal 与 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 在计算时执行一次,其返回值不二次调用。
- `--name` custom property 默认继承;`tp-var` 支持 fallback 和 cycle invalidation。属性 schema 的 normalizer/validator 在变量与 wide value 求值后执行。

View File

@ -169,7 +169,7 @@ Stage 2 canonical façade 是内部模型,不改变公开入口和历史返回
### tp-style.el纯 schema 与 cascade kernel
只依赖 tp-core不接触 buffer、marker、mount 或订阅。它拥有 namespaced property schema、structured subject/selector、origin/importance/layer/specificity/scope/source-order cascade、逐属性继承、tagged CSS-wide values、custom property/`tp-var`、显式 `tp-computed`、named style、provenance 和最终 Emacs property projection。
只依赖 tp-core不接触 buffer、marker、mount 或订阅。它拥有 namespaced property schema、structured subject/selector、origin/importance/layer/specificity/scope/source-order cascade、逐属性继承、tagged CSS-wide values、custom property/`tp-var`、显式 `tp-computed`、named style、provenance 和最终 Emacs property projection。默认 stylesheet 只服务便利入口;独立 consumer 使用 `tp-stylesheet-create` 持有隔离的 rule、layer 与 source-order domain。
普通 function value 保持 literal只有 `tp-computed` 才会求值一次。schema/rule 注册先完整校验再原子替换。shorthand 在进入候选集前展开一次,计算结果随后经过 variable/wide-value resolution、normalizer 和 validator。该模块提供后续 signal/binding 与 retained surface 共用的唯一 style 语义,不建立第二套 renderer。
@ -582,7 +582,7 @@ insert/copy/yank/stickiness/narrowing/indirect buffer 均不在 tp-query 中封
| 模块 | 状态 | 描述 | 清理 |
|------|------|------|------|
| tp-core | —— | **无可变状态**(仅 `tp-debug-mode`/`tp-debug-echo` 两个用户选项;调试日志写入 *tp-debug* 缓冲区,由 `tp-debug-clear` 清除) | - |
| tp-style | schema、named style、stylesheet rule、cascade layer/source order registries | 纯 style definition 和 rule 状态;不保存 object、buffer 或 mount | `tp-style-reset` / `tp-style-reset-rules` |
| tp-style | schema、named style、default stylesheet 与调用方持有的独立 stylesheet instances | 纯 style definition 和 rule/layer/source-order 状态;不保存 object、buffer 或 mount独立实例不被全局 reset 暗中清理 | `tp-style-reset` / `tp-style-reset-rules` |
| tp-reactive | signals、owner bindings、dependency subscriber sets、variable adapters、transaction-local scheduler state、public counters | TP 1.0 exact reactive graphnormal update 不扫描 buffer | `tp-reactive-reset`buffer-scoped signal 随 buffer kill |
| tp-surface | buffer-local surfaces、object/mount/index、range anchors、property ledgers、root/scoped publication、plan/client-state/revision/report | TP 1.0 retained publicationglobal registry 仅 weak-reference | `tp-surface-unmount`buffer kill authoritative teardown |
| tp-reactive | `tp-reactive-deps` | 变量 → 依赖层 注册表 | `tp-reactive-reset` |

View File

@ -168,6 +168,51 @@
(should (tp-selector-match-p
'(:where (:type button) (:class missing)) second)))))
(ert-deftest tp-style-test-subject-class-and-state-tokens-use-value-equality ()
"Selectors match generic string tokens without requiring symbol identity."
(tp-style-test--isolated
(let ((subject (tp-subject-create
:type 'button
:classes (list (copy-sequence "primary"))
:state (list (copy-sequence "active")))))
(should (tp-selector-match-p '(:class "primary") subject))
(should (tp-selector-match-p '(:state "active") subject)))))
(ert-deftest tp-style-test-stylesheet-instances-isolate-rules-and-layers ()
"Independent stylesheets cannot leak rules or layer order into each other."
(tp-style-test--isolated
(tp-style-test--color-schema)
(let ((left (tp-stylesheet-create))
(right (tp-stylesheet-create))
(subject (tp-subject-create :type 'button)))
(tp-stylesheet-add-rule '(:type button) '(demo/color "blue")
:layer 'base :stylesheet left)
(tp-stylesheet-add-rule '(:type button) '(demo/color "red")
:layer 'components :stylesheet left)
(tp-stylesheet-add-rule '(:type button) '(demo/color "green")
:layer 'components :stylesheet right)
(tp-stylesheet-add-rule '(:type button) '(demo/color "purple")
:layer 'base :stylesheet right)
(should (equal (plist-get
(tp-style-test--values subject :rules left)
'demo/color)
"red"))
(should (equal (plist-get
(tp-style-test--values subject :rules right)
'demo/color)
"purple"))
(should (equal (plist-get (tp-style-test--values subject) 'demo/color)
"black"))
(tp-style-reset-rules left)
(should (equal (plist-get
(tp-style-test--values subject :rules left)
'demo/color)
"black"))
(should (equal (plist-get
(tp-style-test--values subject :rules right)
'demo/color)
"purple")))))
(ert-deftest tp-style-test-origin-importance-and-specificity-are-ordered ()
"Importance, origin, and selector specificity decide the winner in order."
(tp-style-test--isolated

View File

@ -40,6 +40,12 @@
"Computed values, active properties, custom properties, and provenance."
values custom-properties active-properties provenance)
(cl-defstruct (tp-stylesheet
(:constructor tp-stylesheet-create ())
(:conc-name tp--stylesheet-))
"Independent ordered rule and cascade-layer collection."
rules layers (source-order 0))
(cl-defstruct (tp--computed-source (:constructor tp--make-computed-source))
function)
@ -49,10 +55,10 @@
name fallback-present-p fallback)
(cl-defstruct (tp--style-rule (:constructor tp--make-style-rule))
selector declarations origin layer scope specificity source-order)
selector declarations origin layer layer-rank scope specificity source-order)
(cl-defstruct (tp--candidate (:constructor tp--make-candidate))
property value origin important layer specificity scope-distance
property value origin important layer layer-rank specificity scope-distance
source-order declaration-order selector)
(defconst tp--style-origin-order
@ -178,14 +184,24 @@ OPTIONS support :initial, :inherits, :normalizer, :validator, :equality,
(tp-register-text-property property))
(copy-tree value))))
(defun tp-style-reset-rules ()
"Clear stylesheet rules and their cascade-layer order."
(setq tp--stylesheet-rules nil
tp--cascade-layers nil
tp--style-source-order 0))
(defun tp-style-reset-rules (&optional stylesheet)
"Clear rules and cascade-layer order in optional STYLESHEET.
With nil STYLESHEET, reset TP's process-wide default stylesheet."
(if stylesheet
(progn
(unless (tp-stylesheet-p stylesheet)
(signal 'wrong-type-argument
(list 'tp-stylesheet-p stylesheet)))
(setf (tp--stylesheet-rules stylesheet) nil
(tp--stylesheet-layers stylesheet) nil
(tp--stylesheet-source-order stylesheet) 0))
(setq tp--stylesheet-rules nil
tp--cascade-layers nil
tp--style-source-order 0)))
(defun tp-style-reset ()
"Clear all TP style schemas, definitions, rules, and ordering state."
"Clear TP's schemas, named styles, and default stylesheet state.
Caller-owned stylesheet instances retain their rules until reset explicitly."
(clrhash tp--property-schemas)
(clrhash tp--named-styles)
(setq tp--property-schema-order nil)
@ -229,8 +245,8 @@ OPTIONS support :initial, :inherits, :normalizer, :validator, :equality,
(cl-defun tp-subject-create (&key type id classes attributes state parent children)
"Create a generic cascade subject from TYPE, ID, and metadata.
CLASSES and STATE are symbol lists. ATTRIBUTES is an alist. PARENT and
CHILDREN must be TP subjects when present."
CLASSES and STATE are token lists compared with `equal'. ATTRIBUTES is an
alist. PARENT and CHILDREN must be TP subjects when present."
(when (and parent (not (tp-subject-p parent)))
(signal 'wrong-type-argument (list 'tp-subject-p parent)))
(let ((subject (tp--make-subject
@ -330,8 +346,8 @@ CHILDREN must be TP subjects when present."
(:universal t)
(:type (equal (nth 1 selector) (tp-subject-type subject)))
(:id (equal (nth 1 selector) (tp-subject-id subject)))
(:class (memq (nth 1 selector) (tp-subject-classes subject)))
(:state (memq (nth 1 selector) (tp-subject-state subject)))
(:class (member (nth 1 selector) (tp-subject-classes subject)))
(:state (member (nth 1 selector) (tp-subject-state subject)))
(:attr (tp--selector-match-attribute selector subject))
(:and (cl-every (lambda (item) (tp-selector-match-p item subject))
(cdr selector)))
@ -456,31 +472,57 @@ CHILDREN must be TP subjects when present."
(signal 'tp-invalid-style (list :origin origin)))
origin)
(defun tp--register-cascade-layer (layer)
"Register LAYER in first-seen order when non-nil."
(when (and layer (not (memq layer tp--cascade-layers)))
(setq tp--cascade-layers (append tp--cascade-layers (list layer)))))
(defun tp--register-cascade-layer (layer stylesheet)
"Register LAYER in first-seen order for optional STYLESHEET.
Return LAYER's zero-based rank, or nil for an unlayered rule."
(when layer
(let ((layers (if stylesheet
(tp--stylesheet-layers stylesheet)
tp--cascade-layers)))
(unless (memq layer layers)
(setq layers (append layers (list layer)))
(if stylesheet
(setf (tp--stylesheet-layers stylesheet) layers)
(setq tp--cascade-layers layers)))
(cl-position layer layers))))
(defun tp--next-style-source-order (stylesheet)
"Increment and return source order for optional STYLESHEET."
(if stylesheet
(cl-incf (tp--stylesheet-source-order stylesheet))
(cl-incf tp--style-source-order)))
(defun tp--append-stylesheet-rule (rule stylesheet)
"Append RULE to optional STYLESHEET and return RULE."
(if stylesheet
(setf (tp--stylesheet-rules stylesheet)
(append (tp--stylesheet-rules stylesheet) (list rule)))
(setq tp--stylesheet-rules
(append tp--stylesheet-rules (list rule))))
rule)
;;;###autoload
(cl-defun tp-stylesheet-add-rule
(selector declarations &key (origin 'author) layer scope)
(selector declarations &key (origin 'author) layer scope stylesheet)
"Add a structured SELECTOR rule with DECLARATIONS.
ORIGIN defaults to `author'. LAYER is ordered by first appearance. SCOPE,
when non-nil, is a selector that must match the subject or an ancestor."
when non-nil, is a selector that must match the subject or an ancestor.
STYLESHEET isolates rules and layer order from TP's default stylesheet."
(tp--validate-selector selector)
(when scope (tp--validate-selector scope))
(tp--validate-rule-origin origin)
(let ((expanded (tp--expand-declarations declarations)))
(tp--register-cascade-layer layer)
(let ((rule (tp--make-style-rule
:selector (copy-tree selector)
:declarations (copy-tree expanded)
:origin origin :layer layer :scope (copy-tree scope)
:specificity (tp-selector-specificity selector)
:source-order (cl-incf tp--style-source-order))))
(setq tp--stylesheet-rules
(append tp--stylesheet-rules (list rule)))
rule)))
(when (and stylesheet (not (tp-stylesheet-p stylesheet)))
(signal 'wrong-type-argument (list 'tp-stylesheet-p stylesheet)))
(let* ((expanded (tp--expand-declarations declarations))
(layer-rank (tp--register-cascade-layer layer stylesheet))
(rule (tp--make-style-rule
:selector (copy-tree selector)
:declarations (copy-tree expanded)
:origin origin :layer layer :layer-rank layer-rank
:scope (copy-tree scope)
:specificity (tp-selector-specificity selector)
:source-order (tp--next-style-source-order stylesheet))))
(tp--append-stylesheet-rule rule stylesheet)))
(defun tp--scope-distance (scope subject)
"Return distance from SUBJECT to matching SCOPE, or nil."
@ -504,6 +546,7 @@ DECLARATION-ORDER is the property's position within RULE."
:property property :value raw
:origin (tp--style-rule-origin rule) :important important
:layer (tp--style-rule-layer rule)
:layer-rank (tp--style-rule-layer-rank rule)
:specificity (tp--style-rule-specificity rule)
:scope-distance (tp--scope-distance (tp--style-rule-scope rule) subject)
:source-order (tp--style-rule-source-order rule)
@ -529,7 +572,8 @@ DECLARATION-ORDER is the property's position within RULE."
(pcase-let ((`(,raw . ,important) (tp--unwrap-important value)))
(tp--make-candidate
:property property :value raw :origin 'inline
:important important :layer nil :specificity '(1 0 0)
:important important :layer nil :layer-rank nil
:specificity '(1 0 0)
:scope-distance most-positive-fixnum
:source-order (1+ tp--style-source-order)
:declaration-order declaration-order
@ -557,12 +601,13 @@ DECLARATION-ORDER is the property's position within RULE."
(defun tp--layer-rank (candidate)
"Return cascade layer rank for CANDIDATE."
(let ((layer (tp--candidate-layer candidate))
(rank (tp--candidate-layer-rank candidate))
(important (tp--candidate-important candidate)))
(cond
((and important (null layer)) -1000000)
((null layer) 1000000)
(important (- (or (cl-position layer tp--cascade-layers) 0)))
(t (or (cl-position layer tp--cascade-layers) 0)))))
(important (- (or rank 0)))
(t (or rank 0)))))
(defun tp--compare-number (left right)
"Compare LEFT and RIGHT, returning 1, -1, or 0."
@ -868,12 +913,16 @@ When PROVENANCE-P is non-nil, also retain winning declaration facts."
parent-style provenance)
"Compute a deterministic style for SUBJECT.
DECLARATIONS are inline values. RULES defaults to the registered stylesheet;
explicit nil disables stylesheet rules. PARENT-STYLE may be a computed style
or values plist. When PROVENANCE is non-nil, winner metadata is retained."
an isolated stylesheet instance selects only its rules, and explicit nil
disables stylesheet rules. PARENT-STYLE may be a computed style or values
plist. When PROVENANCE is non-nil, winner metadata is retained."
(unless (tp-subject-p subject)
(signal 'wrong-type-argument (list 'tp-subject-p subject)))
(let* ((active-rules (if (eq rules tp--style-all-rules)
tp--stylesheet-rules rules))
tp--stylesheet-rules
(if (tp-stylesheet-p rules)
(tp--stylesheet-rules rules)
rules)))
(candidate-table
(tp--group-candidates
(tp--collect-candidates subject declarations active-rules)))