From 119529701165c226699778a62763939966f806a9 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Thu, 6 Aug 2026 13:59:08 +0800 Subject: [PATCH] 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. --- README.md | 6 +- README_CN.md | 6 +- docs/API-SEMANTICS.md | 2 + docs/ARCHITECTURE.md | 4 +- tests/tp-style-tests.el | 45 +++++++++++++++ tp-style.el | 119 ++++++++++++++++++++++++++++------------ 6 files changed, 139 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 68c345f..1208d97 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README_CN.md b/README_CN.md index dde1cbf..35702ba 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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 的精确依赖 runtime:signal 只 invalidates 真实订阅的 binding,binding 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 diff --git a/docs/API-SEMANTICS.md b/docs/API-SEMANTICS.md index e56f1a8..8e99b81 100644 --- a/docs/API-SEMANTICS.md +++ b/docs/API-SEMANTICS.md @@ -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 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 在计算时执行一次,其返回值不二次调用。 - `--name` custom property 默认继承;`tp-var` 支持 fallback 和 cycle invalidation。属性 schema 的 normalizer/validator 在变量与 wide value 求值后执行。 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f1caa80..468cf97 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 graph;normal 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 publication;global registry 仅 weak-reference | `tp-surface-unmount`;buffer kill authoritative teardown | | tp-reactive | `tp-reactive-deps` | 变量 → 依赖层 注册表 | `tp-reactive-reset` | diff --git a/tests/tp-style-tests.el b/tests/tp-style-tests.el index 347115d..107c253 100644 --- a/tests/tp-style-tests.el +++ b/tests/tp-style-tests.el @@ -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 diff --git a/tp-style.el b/tp-style.el index b2db707..cf13c43 100644 --- a/tp-style.el +++ b/tp-style.el @@ -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)))