diff --git a/docs/maintainer/ebox-current-implementation-reference.en.md b/docs/maintainer/ebox-current-implementation-reference.en.md index ba573da..174762c 100644 --- a/docs/maintainer/ebox-current-implementation-reference.en.md +++ b/docs/maintainer/ebox-current-implementation-reference.en.md @@ -79,6 +79,7 @@ Caller-owned Source Tree - A failed candidate leaves the previous buffer, runtime identity, and report intact. - Keys are local to siblings; visible strings are never used as identity. - Ebox owns logical node-to-subject adaptation and id/class/type candidate indexes; ECSS's public structured matcher is the only selector truth source. Subjects expose built-in id/key plus explicit `:selector-attributes`, never visible content, layout state, or runtime containers. +- `ebox-style--property-definitions` is the single Ebox author-property source. Load time derives Ebox's read-only lookup index and one immutable ECSS package schema from it; the surface schema is composed once, and node construction or updates never re-register or copy the whole schema domain. - `owner-rerender` is broader than `span-patch`, which is broader than `paint-patch`. - Buffer coordinates belong to the generation that produced them and must be refreshed after mutation. - Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness. diff --git a/docs/maintainer/ebox-current-implementation-reference.zh.md b/docs/maintainer/ebox-current-implementation-reference.zh.md index d01f8d3..1db37a9 100644 --- a/docs/maintainer/ebox-current-implementation-reference.zh.md +++ b/docs/maintainer/ebox-current-implementation-reference.zh.md @@ -79,6 +79,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor - 候选失败时必须保留之前的 buffer、runtime identity 和报告。 - Key 只在兄弟节点中有效;不能用可见字符串作为 identity。 - Ebox 只拥有逻辑 node-to-subject 适配与 id/class/type 候选索引;ECSS 的公共结构化 matcher 是唯一 selector 真相源。subject 只暴露内建 id/key 和显式 `:selector-attributes`,绝不暴露可见 content、布局状态或 runtime 容器。 +- `ebox-style--property-definitions` 是 Ebox 作者属性的唯一来源:加载时一次生成 Ebox 自己的只读查询索引与一个不可变 ECSS package schema;surface schema 只组合一次,node 构造和更新不重复注册或复制整份 schema。 - `owner-rerender` 范围大于 `span-patch`,`span-patch` 大于 `paint-patch`。 - Buffer 坐标属于生成它的 generation,变更后必须重新获取。 - Grid 使用普通测量与渲染流水线;native reflow 可以拒绝不适合的树并回退到 Elisp,正确性不变。 diff --git a/ebox-canonical.el b/ebox-canonical.el index 2e12034..a16b170 100644 --- a/ebox-canonical.el +++ b/ebox-canonical.el @@ -155,7 +155,7 @@ and author TAG." (error "%s rejected properties: %s" context (error-message-string err))))))) (cl-loop for (id _value) on declarations by #'cddr - for property = (or (ebox-style-property id) + for property = (or (ebox-style--property id) (error "Missing canonical Ebox property: %S" id)) unless (funcall predicate property) diff --git a/ebox-incremental.el b/ebox-incremental.el index 063b3c9..70061f3 100644 --- a/ebox-incremental.el +++ b/ebox-incremental.el @@ -6401,7 +6401,7 @@ by impact classification; the declaration container itself is not geometry." (memq key ebox-tree--excluded-source-keys) (memq key '(:props :raw-props :ebox-style-declarations)) - (ebox-style-property key)) + (ebox-style--property key)) (push key seen) (let ((old-entry (plist-member old key)) (new-entry (plist-member new key))) @@ -6683,7 +6683,7 @@ derive render changes from the computed fragment style signatures." (ordinary-keys (cl-remove-if (lambda (key) - (or (ebox-style-property key) + (or (ebox-style--property key) (memq key ebox-tree-metadata-source-keys))) source-keys)) (style-keys diff --git a/ebox-style.el b/ebox-style.el index 241a093..d1f33a2 100644 --- a/ebox-style.el +++ b/ebox-style.el @@ -24,17 +24,12 @@ "ebox-flex" (value allowed-keywords)) (declare-function ebox--nonnegative-horizontal-size-pixels "ebox-layout" (value &optional default)) -(declare-function ecss-schema-set-create "ecss" ()) (declare-function ecss-stylesheet-create "ecss" ()) (defvar ebox--preferred-size-keywords) (defvar ebox--min-size-keywords) (defvar ebox--max-size-keywords) -(defun ebox-style--create-schema-set () - "Return one private ECSS schema set for Ebox style state." - (ecss-schema-set-create)) - (defun ebox-style--create-stylesheet () "Return one private ECSS stylesheet for Ebox style state." (ecss-stylesheet-create)) @@ -226,24 +221,37 @@ (:name :grid-row-span :id ebox/grid-row-span :initial nil :contexts (item) :group geometry :dirty-kind geometry :signature layout :validator positive-integer) - (:name :padding :id ebox/padding :shorthand padding) - (:name :padding-inline :id ebox/padding-inline :shorthand padding-inline) - (:name :padding-block :id ebox/padding-block :shorthand padding-block) - (:name :margin :id ebox/margin :shorthand margin) - (:name :margin-inline :id ebox/margin-inline :shorthand margin-inline) - (:name :margin-block :id ebox/margin-block :shorthand margin-block) - (:name :border :id ebox/border :shorthand border) - (:name :border-top :id ebox/border-top :shorthand border-top) - (:name :border-right :id ebox/border-right :shorthand border-right) - (:name :border-bottom :id ebox/border-bottom :shorthand border-bottom) - (:name :border-left :id ebox/border-left :shorthand border-left) + (:name :padding :id ebox/padding :shorthand padding + :impacts (geometry) :projections (layout)) + (:name :padding-inline :id ebox/padding-inline :shorthand padding-inline + :impacts (geometry) :projections (layout)) + (:name :padding-block :id ebox/padding-block :shorthand padding-block + :impacts (geometry) :projections (layout)) + (:name :margin :id ebox/margin :shorthand margin + :impacts (geometry) :projections (layout)) + (:name :margin-inline :id ebox/margin-inline :shorthand margin-inline + :impacts (geometry) :projections (layout)) + (:name :margin-block :id ebox/margin-block :shorthand margin-block + :impacts (geometry) :projections (layout)) + (:name :border :id ebox/border :shorthand border + :impacts (geometry structure paint) :projections (layout paint)) + (:name :border-top :id ebox/border-top :shorthand border-top + :impacts (geometry structure paint) :projections (layout paint)) + (:name :border-right :id ebox/border-right :shorthand border-right + :impacts (geometry structure paint) :projections (layout paint)) + (:name :border-bottom :id ebox/border-bottom :shorthand border-bottom + :impacts (geometry structure paint) :projections (layout paint)) + (:name :border-left :id ebox/border-left :shorthand border-left + :impacts (geometry structure paint) :projections (layout paint)) (:name :border-top-p :id ebox/border-top-p :shorthand border-top-present :validator boolean :exclusive-outputs t + :impacts (geometry structure) :projections (layout) :group structure :dirty-kind structure :signature layout) (:name :border-bottom-p :id ebox/border-bottom-p :shorthand border-bottom-present :validator boolean :exclusive-outputs t + :impacts (geometry structure) :projections (layout) :group structure :dirty-kind structure :signature layout) (:name :border-width :id ebox/border-width :shorthand border-width :group geometry :dirty-kind geometry :signature layout) @@ -259,16 +267,8 @@ :group geometry :dirty-kind geometry :signature layout)) "Canonical Ebox property schemas and public aliases.") -(defvar ebox-style--property-table nil - "Hash table from canonical property names and aliases to metadata.") - -(defvar ebox-style-schemas (ebox-style--create-schema-set) - "ECSS property schemas for the isolated Ebox style domain.") - -(defvar ebox-style--registered-schemas nil - "Schema-set object whose Ebox properties are already registered. -`ecss-schema-set-property' returns a detached schema copy, so probing it on -every `ebox-create' needlessly copies the entire property metadata domain.") +(defvar ebox-style-schemas) +(defvar ebox-style--property-index) (defconst ebox-style--declaration-cache-max-entries 512 "Maximum canonical declaration compilations retained by Ebox.") @@ -277,44 +277,27 @@ every `ebox-create' needlessly copies the entire property metadata domain.") (make-hash-table :test 'equal) "Bounded memo table for canonical Ebox declaration compilation.") -(defun ebox-style--register-property-name (table name property role) - "Register NAME for PROPERTY ROLE in TABLE, rejecting collisions." - (when-let* ((existing (gethash name table))) - (unless (eq existing property) - (error "Ebox property %S %s collides with canonical property %S" - name role (plist-get existing :name)))) - (puthash name property table)) - -(defun ebox-style--ensure-property-table () - "Return the canonical style property registry table." - (or ebox-style--property-table - (let ((table (make-hash-table :test 'eq))) - (dolist (property ebox-style--property-definitions) - (ebox-style--register-property-name - table (plist-get property :name) property "name") - (ebox-style--register-property-name - table (plist-get property :id) property "ID") - (dolist (alias (plist-get property :aliases)) - (ebox-style--register-property-name table alias property "alias"))) - (setq ebox-style--property-table table)))) +(defun ebox-style--property (name) + "Return registry-owned style metadata for internal NAME lookup." + (gethash name ebox-style--property-index)) (defun ebox-style-property (name) - "Return registered style metadata for canonical property or alias NAME." - (gethash name (ebox-style--ensure-property-table))) + "Return detached style metadata for canonical property or alias NAME." + (ecss-schema-set-property-metadata ebox-style-schemas name)) (defun ebox-style-canonical-name (name) "Return canonical CSS-like longhand property name for NAME." - (when-let* ((property (ebox-style-property name))) + (when-let* ((property (ebox-style--property name))) (plist-get property :name))) (defun ebox-style-property-accepted-p (tag name) "Return non-nil when author form TAG accepts property NAME." - (when-let* ((property (ebox-style-property name))) + (when-let* ((property (ebox-style--property name))) (ebox-style--form-property-allowed-p tag property))) (defun ebox-style-schema-id (name) "Return namespaced ECSS schema id for Ebox property NAME." - (when-let* ((property (ebox-style-property name))) + (when-let* ((property (ebox-style--property name))) (plist-get property :id))) (defun ebox-style--put (plist key value) @@ -694,6 +677,50 @@ generated wrappers that already contain exact line breaks." (setq options (plist-put options :shorthand shorthand))) options)) +(defun ebox-style--schema-impacts (property) + "Return immutable impact classes for PROPERTY metadata." + (or (plist-get property :impacts) + (and (eq (plist-get property :group) 'typography) + '(geometry paint)) + (list (plist-get property :dirty-kind)))) + +(defun ebox-style--schema-projections (property) + "Return immutable projection owners for PROPERTY metadata." + (or (plist-get property :projections) + (delq nil (list (plist-get property :signature))))) + +(defun ebox-style--schema-definition (property) + "Return one ECSS package definition for Ebox PROPERTY metadata." + (append + (list :id (plist-get property :id) + :aliases + (cons (plist-get property :name) + (copy-sequence (plist-get property :aliases))) + :impacts (ebox-style--schema-impacts property) + :projections (ebox-style--schema-projections property) + :metadata property) + (ebox-style--schema-options property))) + +(defconst ebox-style--schema-package + (ecss-schema-package-create + 'ebox + (mapcar #'ebox-style--schema-definition + ebox-style--property-definitions)) + "Immutable Ebox property package registered with ECSS.") + +(defconst ebox-style--property-index + (let ((table (make-hash-table :test #'eq))) + (dolist (property ebox-style--property-definitions table) + (puthash (plist-get property :id) property table) + (puthash (plist-get property :name) property table) + (dolist (alias (plist-get property :aliases)) + (puthash alias property table)))) + "Ebox-owned immutable lookup index over property definitions.") + +(defconst ebox-style-schemas + (ecss-schema-set-compose ebox-style--schema-package) + "Immutable Ebox-only surface schema set.") + (defvar ebox-style--closed-computed-cache (make-hash-table :test #'equal) "Bounded cache of selector-free, dependency-free computed styles.") @@ -711,26 +738,6 @@ generated wrappers that already contain exact line breaks." (cl-loop for (_property value) on declarations by #'cddr never (tp-computed-p value))) -(defun ebox-style-register-properties () - "Register the complete namespaced Ebox property domain with ECSS." - (dolist (property ebox-style--property-definitions) - (apply #'ecss-schema-set-define - ebox-style-schemas (plist-get property :id) - (ebox-style--schema-options property))) - (clrhash ebox-style--closed-computed-cache) - (clrhash ebox-style--closed-inheritance-cache) - (setq ebox-style--registered-schemas ebox-style-schemas)) - -(defun ebox-style--ensure-properties () - "Ensure Ebox property schemas exist in the isolated ECSS schema set." - (unless (eq ebox-style--registered-schemas ebox-style-schemas) - ;; Only the first call for a new schema-set needs to inspect/register the - ;; domain. Avoid the detached `ecss-schema-set-property' accessor on the - ;; hot `ebox-create' path; it recursively copies schema metadata. - (if (ecss-schema-set-property ebox-style-schemas 'ebox/color) - (setq ebox-style--registered-schemas ebox-style-schemas) - (ebox-style-register-properties)))) - (defvar ebox-style-stylesheet (ebox-style--create-stylesheet) "Isolated ECSS stylesheet containing Ebox layout and paint rules.") @@ -782,6 +789,12 @@ author declaration that references the slot." right (cddr right))) (and equal-p (null left) (null right)))) +(defconst ebox-style--inherited-properties + (cl-loop for property in ebox-style--property-definitions + when (plist-get property :inherits) + collect (plist-get property :id)) + "Canonical ECSS properties inherited according to the shared schema.") + (defun ebox-style--inherited-declarations-changed-p (old new) "Return non-nil when OLD and NEW change descendant style inputs. The inherited property domain comes only from the canonical schema. ECSS @@ -813,7 +826,7 @@ consume them even though Ebox does not interpret their names." "Reject duplicate canonical properties in author PLIST." (let ((seen (make-hash-table :test 'eq))) (cl-loop for (name _value) on plist by #'cddr - for property = (ebox-style-property name) + for property = (ebox-style--property name) for id = (and property (plist-get property :id)) when id do (when-let* ((previous (gethash id seen))) @@ -825,7 +838,7 @@ consume them even though Ebox does not interpret their names." (defun ebox-style--author-property-output-ids (name value) "Return canonical output IDs produced by author NAME and VALUE." - (when-let* ((property (ebox-style-property name))) + (when-let* ((property (ebox-style--property name))) (if-let* ((shorthand-name (plist-get property :shorthand)) (expander (ebox-style--shorthand shorthand-name))) (cl-loop for (id _output) on (funcall expander value) by #'cddr @@ -836,7 +849,7 @@ consume them even though Ebox does not interpret their names." "Reject author properties whose canonical outputs overlap in PLIST." (let (entries) (cl-loop for (name value) on plist by #'cddr - for property = (ebox-style-property name) + for property = (ebox-style--property name) when property do (push (list :name name :value value :property property) entries)) @@ -899,17 +912,25 @@ not affect declaration compilation and are intentionally excluded." (defun ebox-style--validate-declaration-values (declarations) "Return DECLARATIONS after static Ebox-owned value validation." (cl-loop for (property value) on declarations by #'cddr - for schema = (ebox-style-property property) + for schema = (ebox-style--property property) for validator = (and schema (plist-get schema :validator)) when (ebox-style--static-invalid-declaration-p validator value) do (error "ebox: invalid value for %S: %S" (plist-get schema :name) value)) declarations) +(defun ebox-style--merge-ordered-author-declarations (declarations) + "Expand ordered Ebox DECLARATIONS as distinct normalization groups. +Ebox currently preserves CSS shorthand/longhand declaration order, while its +own author validator rejects exact aliases and explicitly exclusive outputs." + (apply #'ecss-merge-declarations + ebox-style-schemas + (cl-loop for (property value) on declarations by #'cddr + collect (list property value)))) + (defun ebox-style-compile-declarations (plist &optional strict) "Compile Ebox PLIST aliases to ECSS schema declarations. When STRICT is non-nil, reject properties outside the Ebox style domain." - (ebox-style--ensure-properties) (unless (ebox-style--valid-plist-p plist) (user-error "Ebox style declarations must be an even property list")) (ebox-style--validate-no-duplicate-properties plist) @@ -931,7 +952,7 @@ When STRICT is non-nil, reject properties outside the Ebox style domain." property))) (let ((compiled (ebox-style--validate-declaration-values - (ecss-merge-declarations ebox-style-schemas canonical)))) + (ebox-style--merge-ordered-author-declarations canonical)))) (when (>= (hash-table-count ebox-style--declaration-cache) ebox-style--declaration-cache-max-entries) (clrhash ebox-style--declaration-cache)) @@ -963,7 +984,7 @@ When STRICT is non-nil, reject properties outside the Ebox style domain." declarations)) (let ((seen (make-hash-table :test #'eq))) (cl-loop for (id _value) on declarations by #'cddr - for property = (ebox-style-property id) + for property = (ebox-style--property id) unless (and property (eq id (plist-get property :id))) do (error "Ebox declaration is not a canonical property ID: %S" id) @@ -984,7 +1005,7 @@ When STRICT is non-nil, reject properties outside the Ebox style domain." (defun ebox-style-declaration-properties (declarations predicate) "Return canonical author properties in DECLARATIONS matching PREDICATE." (cl-loop for (id value) on declarations by #'cddr - for property = (or (ebox-style-property id) + for property = (or (ebox-style--property id) (error "Unknown canonical Ebox property: %S" id)) when (funcall predicate property) append (list (plist-get property :name) value))) @@ -993,20 +1014,18 @@ When STRICT is non-nil, reject properties outside the Ebox style domain." (declarations predicate) "Return non-nil when DECLARATIONS include facts outside PREDICATE." (cl-loop for (id _value) on declarations by #'cddr - for property = (or (ebox-style-property id) + for property = (or (ebox-style--property id) (error "Unknown canonical Ebox property: %S" id)) thereis (not (funcall predicate property)))) (defun ebox-style-merge-declarations (base overrides) "Merge canonical BASE and OVERRIDES through ECSS property schemas." - (ebox-style--ensure-properties) (ecss-merge-declarations ebox-style-schemas base overrides)) (cl-defun ebox-style-add-rule (selector declarations &key (origin 'author) layer scope) "Add SELECTOR rule with Ebox DECLARATIONS to the isolated stylesheet. ORIGIN, LAYER, and SCOPE use ECSS cascade semantics." - (ebox-style--ensure-properties) (ecss-stylesheet-add-rule ebox-style-stylesheet ebox-style-schemas (ebox-style--selector selector) (ebox-style-compile-declarations declarations t) @@ -1015,7 +1034,6 @@ ORIGIN, LAYER, and SCOPE use ECSS cascade semantics." (defun ebox-style-compute-subject (subject declarations &optional parent-style) "Compute SUBJECT style from DECLARATIONS and optional PARENT-STYLE via ECSS." - (ebox-style--ensure-properties) (let* ((closed-p (and (not (ebox-style-cascade-active-p)) (ebox-style--closed-declarations-p declarations))) @@ -1085,12 +1103,6 @@ winner is required so parent inherited geometry/paint cannot be stale." (plist-member old 'ebox/color) (plist-member new 'ebox/color))) -(defconst ebox-style--inherited-properties - (cl-loop for property in ebox-style--property-definitions - when (plist-get property :inherits) - collect (plist-get property :id)) - "Canonical ECSS properties inherited according to the shared schema.") - (defun ebox-style--inherited-style-signature (style) "Return the inherited-value fingerprint of computed STYLE, or nil. The fingerprint deliberately contains only values that can flow from a @@ -1326,7 +1338,7 @@ same canonical property id, so callers never inspect backend plist fields." (defun ebox-style--remove-style-properties (plist) "Return PLIST without Ebox style or ECSS custom properties." (cl-loop for (property value) on plist by #'cddr - unless (or (ebox-style-property property) + unless (or (ebox-style--property property) (ebox-style--custom-property-p property)) append (list property value))) @@ -1636,7 +1648,7 @@ until their private encoding is removed." (defun ebox-style-dirty-kind (name) "Return dirty kind for canonical property or alias NAME." - (when-let* ((property (ebox-style-property name))) + (when-let* ((property (ebox-style--property name))) (plist-get property :dirty-kind))) (defun ebox-style-signature (computed-style groups) @@ -1645,7 +1657,7 @@ until their private encoding is removed." (while computed-style (let* ((property-name (pop computed-style)) (value (pop computed-style)) - (property (ebox-style-property property-name)) + (property (ebox-style--property property-name)) (group (and property (plist-get property :signature)))) (when (and property (or (null groups) @@ -1662,8 +1674,6 @@ until their private encoding is removed." (list (car entry) (cdr entry))) entries)))) -(ebox-style-register-properties) - (defconst ebox-style-ebox-property-rules '((:padding :expand (:padding-top :padding-right :padding-bottom :padding-left) diff --git a/ebox.el b/ebox.el index abc059a..64c1514 100644 --- a/ebox.el +++ b/ebox.el @@ -3992,7 +3992,7 @@ cannot be changed through the unpublished batch root." (ebox-get box target-key)) value)) (and declarations-changed - (ebox-style-property key))) + (ebox-style--property key))) do (if (eq target-key :content) (if-let* ((text (ebox--region-update-direct-text-node box))) diff --git a/tests/ebox-commit-tests.el b/tests/ebox-commit-tests.el index e2efed4..e14e024 100644 --- a/tests/ebox-commit-tests.el +++ b/tests/ebox-commit-tests.el @@ -508,17 +508,24 @@ :removed-node-ids (ebox--runtime-node-ids removed-node)))))))) -(ert-deftest ebox-style-schema-registration-is-not-per-node-copy () - "Repeated node construction must not copy the whole ECSS schema domain." - (let ((calls 0) - (original (symbol-function 'ecss-schema-set-property))) - (cl-letf (((symbol-function 'ecss-schema-set-property) +(ert-deftest ebox-style-schema-composition-is-not-per-node-work () + "Repeated node construction must not rebuild the immutable schema domain." + (let ((package-calls 0) + (compose-calls 0) + (original-package (symbol-function 'ecss-schema-package-create)) + (original-compose (symbol-function 'ecss-schema-set-compose))) + (cl-letf (((symbol-function 'ecss-schema-package-create) (lambda (&rest arguments) - (cl-incf calls) - (apply original arguments)))) + (cl-incf package-calls) + (apply original-package arguments))) + ((symbol-function 'ecss-schema-set-compose) + (lambda (&rest arguments) + (cl-incf compose-calls) + (apply original-compose arguments)))) (dotimes (_ 24) (ebox-test-box :content "schema-hot-path" :color "#111111"))) - (should (= calls 0)))) + (should (= package-calls 0)) + (should (= compose-calls 0)))) (ert-deftest ebox-style-declaration-compilation-is-memoized () "Repeated equivalent style declarations compile through ECSS once." diff --git a/tests/ebox-core-render-tests.el b/tests/ebox-core-render-tests.el index 84f41b3..7b54107 100644 --- a/tests/ebox-core-render-tests.el +++ b/tests/ebox-core-render-tests.el @@ -454,6 +454,43 @@ :font-height) literal)))) +(ert-deftest ebox-ecss-computed-fact-retains-owner-impact-and-provenance () + "The Ebox package schema should remain attached to one computed fact." + (let* ((style + (ecss-compute-style + ebox-style-schemas (ecss-subject-create :type "box") + :declarations (ebox-style-compile-declarations '(:width (120)) t) + :provenance t)) + (fact (ecss-computed-style-property-fact style 'ebox/width))) + (should (eq 'ebox/width (ecss-computed-property-fact-property fact))) + (should (eq 'ebox (ecss-computed-property-fact-owner fact))) + (should (equal '(geometry) + (ecss-computed-property-fact-impacts fact))) + (should (equal '(layout) + (ecss-computed-property-fact-projections fact))) + (should (equal (ecss-computed-property-fact-value fact) + (ecss-computed-style-value style 'ebox/width))) + (should (eq 'declaration + (plist-get (ecss-computed-property-fact-provenance fact) + :source))))) + +(ert-deftest ebox-ecss-multi-impact-fact-shares-one-computed-value () + "Typography geometry and paint consumers should share one value identity." + (let* ((style + (ecss-compute-style + ebox-style-schemas (ecss-subject-create :type "box") + :declarations + (ebox-style-compile-declarations '(:font-weight bold) t))) + (fact + (ecss-computed-style-property-fact style 'ebox/font-weight)) + (geometry-value (ecss-computed-property-fact-value fact)) + (paint-value (ecss-computed-property-fact-value fact))) + (should (equal '(geometry paint) + (ecss-computed-property-fact-impacts fact))) + (should (eq geometry-value paint-value)) + (should (equal geometry-value + (ecss-computed-style-value style 'ebox/font-weight))))) + (ert-deftest ebox-style-context-reads-each-ecss-snapshot-once () "Style projection should not repeatedly copy one computed ECSS snapshot." (let* ((style @@ -2158,7 +2195,7 @@ "A color-only change should produce paint dirty, not geometry dirty." (ebox-test--reset-runtime-state) (let* ((box (ebox-test-box :content "Paint" :width 80 - :border t :border-color "red")) + :border '(1 solid "red"))) (region-id (car (ebox-region-ids box)))) (ebox-test--with-rendered-buffer box (let ((old (ebox--current-layout-snapshots (current-buffer)))) @@ -2225,7 +2262,7 @@ "Paint-only dirty entries should produce paint-patch ops." (ebox-test--reset-runtime-state) (let* ((box (ebox-test-box :content "Paint" :width 80 - :border t :border-color "red")) + :border '(1 solid "red"))) (region-id (car (ebox-region-ids box)))) (ebox-test--with-rendered-buffer box (let ((full-rerenders @@ -4255,7 +4292,7 @@ "Update reports should show dirty and patch-set summaries." (ebox-test--reset-runtime-state) (let* ((box (ebox-test-box :content "Report" :width 80 - :border t :border-color "red")) + :border '(1 solid "red"))) (region-id (car (ebox-region-ids box)))) (ebox-test--with-rendered-buffer box (ebox-test--region-update region-id :border-color "blue") @@ -4569,14 +4606,18 @@ (box :id left :content "Left" :width (120) :padding (0 (8)) :border "#D97757") (box :content "Body" :width (120) - :padding (0 (8)) :border "#D97757" - :border-top none)) + :padding (0 (8)) + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757")) (column (box :id right :content "Right" :width (120) :padding (0 (8)) :border "#5E7F6A") (box :content "Body" :width (120) - :padding (0 (8)) :border "#5E7F6A" - :border-top none))))) + :padding (0 (8)) + :border-right "#5E7F6A" + :border-bottom "#5E7F6A" + :border-left "#5E7F6A"))))) (left-id (plist-get (car (ebox-selector-query-all layout "#left")) :region-id))) @@ -4606,18 +4647,26 @@ :padding (0 (12)) :border "#D97757" :bgcolor "#FFF8F2" (column (box :id left :content "Left" :width (120) - :padding (0 (8)) :border "#D97757" - :border-bottom none) + :padding (0 (8)) + :border-top "#D97757" + :border-right "#D97757" + :border-left "#D97757") (box :content "Body" :width (120) - :padding (0 (8)) :border "#D97757" - :border-top none)) + :padding (0 (8)) + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757")) (column (box :content "Right" :width (120) - :padding (0 (8)) :border "#5E7F6A" - :border-bottom none) + :padding (0 (8)) + :border-top "#5E7F6A" + :border-right "#5E7F6A" + :border-left "#5E7F6A") (box :content "Body" :width (120) - :padding (0 (8)) :border "#5E7F6A" - :border-top none))))) + :padding (0 (8)) + :border-right "#5E7F6A" + :border-bottom "#5E7F6A" + :border-left "#5E7F6A"))))) (left-id (plist-get (car (ebox-selector-query-all layout "#left")) :region-id))) @@ -4650,18 +4699,26 @@ :padding (0 (12)) :border "#D97757" :bgcolor "#FFF8F2" (column (box :id left :content "Left" :width (120) - :padding (0 (8)) :border "#D97757" - :border-bottom none) + :padding (0 (8)) + :border-top "#D97757" + :border-right "#D97757" + :border-left "#D97757") (box :content "Body" :width (120) - :padding (0 (8)) :border "#D97757" - :border-top none)) + :padding (0 (8)) + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757")) (column (box :content "Right" :width (120) - :padding (0 (8)) :border "#5E7F6A" - :border-bottom none) + :padding (0 (8)) + :border-top "#5E7F6A" + :border-right "#5E7F6A" + :border-left "#5E7F6A") (box :content "Body" :width (120) - :padding (0 (8)) :border "#5E7F6A" - :border-top none))))) + :padding (0 (8)) + :border-right "#5E7F6A" + :border-bottom "#5E7F6A" + :border-left "#5E7F6A"))))) (left-id (plist-get (car (ebox-selector-query-all layout "#left")) :region-id))) @@ -5335,11 +5392,7 @@ :height 4 :min-height 3 :max-height 6 :padding-top 1 :padding-bottom 1) (ebox-test--region-update region-id - :margin (list 0 '(8) 0 '(8)) - :margin-inline '((8) (8)) - :margin-block '(0 0) - :margin-top 0 :margin-right '(8) - :margin-bottom 0 :margin-left '(8)) + :margin-inline '((8) (8))) (let ((report (ebox-test--buffer-update-report))) (should (eq (plist-get report :strategy) 'span-patch)) (should (eq (plist-get report :owner-type) 'box)) @@ -5755,11 +5808,7 @@ (region-id (plist-get handle :region-id)) (node-id (plist-get handle :node-id))) (ebox-test--region-update region-id - :margin (list 0 '(8) 0 '(8)) - :margin-inline '((8) (8)) - :margin-block '(0 0) - :margin-top 0 :margin-right '(8) - :margin-bottom 0 :margin-left '(8)) + :margin-inline '((8) (8))) (let* ((report (ebox-test--buffer-update-report)) (snapshot (ebox--ensure-layout-snapshot-spans (current-buffer) node-id)) @@ -7109,8 +7158,11 @@ `(flex :id "flex-window-wrapper" :width (240) :flex-flow (row wrap) :row-gap 1 :column-gap 0 - :padding (0 (8)) :border "#D97757" - :border-top none :bgcolor "#FFF8F2" + :padding (0 (8)) + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757" + :bgcolor "#FFF8F2" ,@(cl-loop for idx below 8 collect `(box :content ,(format "wrapper %02d" idx) @@ -7156,9 +7208,8 @@ (let* ((box (ebox-test-box :width 120 :padding-left 8 :padding-right 8 - :border-left 1 - :border-right 1 - :border-color "#D97757" + :border-left '(1 solid "#D97757") + :border-right '(1 solid "#D97757") :bgcolor "#FFF8F2")) (source (propertize "child" 'ebox-content 77 'ebox-content-owner 77 @@ -7244,8 +7295,11 @@ `(flex :id "flex-prefix-wrapper" :width (240) :flex-flow (row wrap) :row-gap 1 :column-gap 0 - :padding (0 (8)) :border "#D97757" - :border-top none :bgcolor "#FFF8F2" + :padding (0 (8)) + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757" + :bgcolor "#FFF8F2" ,@(cl-loop for idx below 8 collect `(box :content ,(format "extend %02d" idx) @@ -7277,8 +7331,11 @@ (ebox-test-build `(flex :width (240) :flex-flow (row wrap) :row-gap 1 :column-gap 0 - :padding (0 (8)) :border "#D97757" - :border-top none :bgcolor "#FFF8F2" + :padding (0 (8)) + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757" + :bgcolor "#FFF8F2" ,@(cl-loop for idx below 16 collect `(box :content ,(format "piece %02d" idx) @@ -7308,8 +7365,9 @@ :row-gap 1 :column-gap 0 :padding (0 (8) 2 (8)) :margin-bottom 2 - :border "#D97757" - :border-top none + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757" :bgcolor "#FFF8F2" ,@(cl-loop for idx below 2 collect @@ -7353,8 +7411,9 @@ :flex-flow (row wrap) :row-gap 1 :column-gap 0 :padding (0 (8)) - :border "#D97757" - :border-top none + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757" :bgcolor "#FFF8F2" ,@(cl-loop for idx below 4 collect @@ -7390,8 +7449,9 @@ :flex-flow (row wrap) :row-gap 1 :column-gap 0 :padding (0 (8)) - :border "#D97757" - :border-top none + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757" ,@(cl-loop for idx below 4 collect `(box :content ,(format "atomic %02d" idx) @@ -9251,8 +9311,10 @@ (flex :id "signature-flex" :width (viewport) :flex-flow (row wrap) :row-gap 1 :column-gap (8) - :padding (0 (12)) :border "#D97757" - :border-top none + :padding (0 (12)) + :border-right "#D97757" + :border-bottom "#D97757" + :border-left "#D97757" ,@(cl-loop for idx below 24 collect `(box :content ,(format "signature %02d" idx) @@ -12674,8 +12736,8 @@ explicit visible-window handoff may admit its full-content publication." "A hidden box renders blank with identical text, lengths, and metadata." (let* ((make-box (lambda (visibility) - (ebox-test-box :content "Hello" :width (list 90) :border t - :border-color "#AA3311" :bgcolor "#223344" + (ebox-test-box :content "Hello" :width (list 90) + :border '(1 solid "#AA3311") :bgcolor "#223344" :color "#FFFFFF" :padding '(0 1) :visibility visibility))) (visible (ebox-render (funcall make-box 'visible))) diff --git a/tests/ebox-dsl-tests.el b/tests/ebox-dsl-tests.el index 26ef14c..61b95e1 100644 --- a/tests/ebox-dsl-tests.el +++ b/tests/ebox-dsl-tests.el @@ -95,13 +95,49 @@ '(:background-color "red" :bgcolor "blue") t) :type 'error)) +(ert-deftest ebox-style-preserves-ordered-shorthand-longhand-normalization () + "Ebox author pairs remain ordered normalization groups before cutover." + (let ((border-then-top + (ebox-style-compile-declarations + '(:border "red" :border-top none) t)) + (top-then-border + (ebox-style-compile-declarations + '(:border-top none :border "red") t))) + (should (eq 'none + (plist-get border-then-top 'ebox/border-top-style))) + (should (equal "red" + (plist-get border-then-top 'ebox/border-right-color))) + (should (eq 'solid + (plist-get top-then-border 'ebox/border-top-style))) + (should (equal "red" + (plist-get top-then-border 'ebox/border-top-color))))) + (ert-deftest ebox-style-property-registry-rejects-alias-collisions () "An alias should never shadow another canonical property." - (let ((ebox-style--property-table nil) - (ebox-style--property-definitions - '((:name :first :id ebox/first :aliases (:second)) - (:name :second :id ebox/second)))) - (should-error (ebox-style--ensure-property-table) :type 'error))) + (should-error + (ecss-schema-package-create + 'ebox + (mapcar + #'ebox-style--schema-definition + '((:name :first :id ebox/first :aliases (:second) + :group paint :dirty-kind paint :signature paint) + (:name :second :id ebox/second + :group paint :dirty-kind paint :signature paint)))) + :type 'ecss-invalid-property-schema) + (should (eq 'ebox/background-color + (ecss-schema-set-canonical-id ebox-style-schemas :bgcolor))) + (should (eq :background-color + (plist-get + (ecss-schema-set-property-metadata + ebox-style-schemas :bgcolor) + :name))) + (let ((metadata (ebox-style-property :bgcolor))) + (setf (plist-get metadata :name) :damaged) + (setcar (plist-get metadata :contexts) 'damaged) + (should (eq :background-color + (plist-get (ebox-style-property :bgcolor) :name))) + (should (equal '(box) + (plist-get (ebox-style-property :bgcolor) :contexts))))) (ert-deftest ebox-style-border-present-is-a-typed-shorthand-not-an-alias () "The concise border boolean should expand to canonical width and style." diff --git a/tests/ebox-flex-tests.el b/tests/ebox-flex-tests.el index c11200c..52f7f53 100644 --- a/tests/ebox-flex-tests.el +++ b/tests/ebox-flex-tests.el @@ -428,7 +428,9 @@ :padding (0 (10)) :border "#5E7F6A" :text-align center "wrap") (flex :id "body" :max-width (293) :padding (0 (8)) - :border "#5E7F6A" :border-top none + :border-right "#5E7F6A" + :border-bottom "#5E7F6A" + :border-left "#5E7F6A" :flex-wrap wrap :column-gap (8) (box :flex-shrink 0 :width (118) "A 118") (box :flex-shrink 0 :width (118) "B 118") @@ -459,14 +461,19 @@ (layout (ebox-build '(flex :flex-flow (row wrap) :row-gap 1 :column-gap (12) - :padding (0 (12)) :border "#6F6A95" :border-top none + :padding (0 (12)) + :border-right "#6F6A95" + :border-bottom "#6F6A95" + :border-left "#6F6A95" :bgcolor "#FBF9FF" (column (box :max-width (440) :padding (0 (10)) :border "#6F6A95" :text-align center ":flex-flow (row wrap)") (flex :max-width (440) :padding (0 (8)) - :border "#6F6A95" :border-top none + :border-right "#6F6A95" + :border-bottom "#6F6A95" + :border-left "#6F6A95" :flex-flow (row wrap) :gap (1 (12)) (box :width (170) "A") (box :width (170) "B") @@ -476,7 +483,9 @@ :padding (0 (10)) :border "#6F6A95" :text-align center ":flex-flow (column wrap)") (flex :max-width (440) :padding (0 (8)) - :border "#6F6A95" :border-top none + :border-right "#6F6A95" + :border-bottom "#6F6A95" + :border-left "#6F6A95" :flex-flow (column wrap) :gap (1 (12)) (box :width (140) "A") (box :width (140) "B")