refactor: derive Ebox styles from immutable package schemas

This commit is contained in:
Kinneyzhang 2026-08-28 03:13:56 +08:00
parent 8b28f278bd
commit 31046ad8c9
10 changed files with 293 additions and 167 deletions

View File

@ -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.

View File

@ -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 schemasurface schema 只组合一次node 构造和更新不重复注册或复制整份 schema。
- `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch`
- Buffer 坐标属于生成它的 generation变更后必须重新获取。
- Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)))

View File

@ -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."

View File

@ -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)))

View File

@ -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."

View File

@ -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")