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. - A failed candidate leaves the previous buffer, runtime identity, and report intact.
- Keys are local to siblings; visible strings are never used as identity. - 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 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`. - `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. - 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. - 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 和报告。 - 候选失败时必须保留之前的 buffer、runtime identity 和报告。
- Key 只在兄弟节点中有效;不能用可见字符串作为 identity。 - Key 只在兄弟节点中有效;不能用可见字符串作为 identity。
- Ebox 只拥有逻辑 node-to-subject 适配与 id/class/type 候选索引ECSS 的公共结构化 matcher 是唯一 selector 真相源。subject 只暴露内建 id/key 和显式 `:selector-attributes`,绝不暴露可见 content、布局状态或 runtime 容器。 - 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` - `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch`
- Buffer 坐标属于生成它的 generation变更后必须重新获取。 - Buffer 坐标属于生成它的 generation变更后必须重新获取。
- Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。 - Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。

View File

@ -155,7 +155,7 @@ and author TAG."
(error "%s rejected properties: %s" (error "%s rejected properties: %s"
context (error-message-string err))))))) context (error-message-string err)))))))
(cl-loop for (id _value) on declarations by #'cddr (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" (error "Missing canonical Ebox property: %S"
id)) id))
unless (funcall predicate property) 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 ebox-tree--excluded-source-keys)
(memq key '(:props :raw-props (memq key '(:props :raw-props
:ebox-style-declarations)) :ebox-style-declarations))
(ebox-style-property key)) (ebox-style--property key))
(push key seen) (push key seen)
(let ((old-entry (plist-member old key)) (let ((old-entry (plist-member old key))
(new-entry (plist-member new key))) (new-entry (plist-member new key)))
@ -6683,7 +6683,7 @@ derive render changes from the computed fragment style signatures."
(ordinary-keys (ordinary-keys
(cl-remove-if (cl-remove-if
(lambda (key) (lambda (key)
(or (ebox-style-property key) (or (ebox-style--property key)
(memq key ebox-tree-metadata-source-keys))) (memq key ebox-tree-metadata-source-keys)))
source-keys)) source-keys))
(style-keys (style-keys

View File

@ -24,17 +24,12 @@
"ebox-flex" (value allowed-keywords)) "ebox-flex" (value allowed-keywords))
(declare-function ebox--nonnegative-horizontal-size-pixels (declare-function ebox--nonnegative-horizontal-size-pixels
"ebox-layout" (value &optional default)) "ebox-layout" (value &optional default))
(declare-function ecss-schema-set-create "ecss" ())
(declare-function ecss-stylesheet-create "ecss" ()) (declare-function ecss-stylesheet-create "ecss" ())
(defvar ebox--preferred-size-keywords) (defvar ebox--preferred-size-keywords)
(defvar ebox--min-size-keywords) (defvar ebox--min-size-keywords)
(defvar ebox--max-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 () (defun ebox-style--create-stylesheet ()
"Return one private ECSS stylesheet for Ebox style state." "Return one private ECSS stylesheet for Ebox style state."
(ecss-stylesheet-create)) (ecss-stylesheet-create))
@ -226,24 +221,37 @@
(:name :grid-row-span :id ebox/grid-row-span :initial nil (:name :grid-row-span :id ebox/grid-row-span :initial nil
:contexts (item) :group geometry :dirty-kind geometry :signature layout :contexts (item) :group geometry :dirty-kind geometry :signature layout
:validator positive-integer) :validator positive-integer)
(:name :padding :id ebox/padding :shorthand padding) (:name :padding :id ebox/padding :shorthand padding
(:name :padding-inline :id ebox/padding-inline :shorthand padding-inline) :impacts (geometry) :projections (layout))
(:name :padding-block :id ebox/padding-block :shorthand padding-block) (:name :padding-inline :id ebox/padding-inline :shorthand padding-inline
(:name :margin :id ebox/margin :shorthand margin) :impacts (geometry) :projections (layout))
(:name :margin-inline :id ebox/margin-inline :shorthand margin-inline) (:name :padding-block :id ebox/padding-block :shorthand padding-block
(:name :margin-block :id ebox/margin-block :shorthand margin-block) :impacts (geometry) :projections (layout))
(:name :border :id ebox/border :shorthand border) (:name :margin :id ebox/margin :shorthand margin
(:name :border-top :id ebox/border-top :shorthand border-top) :impacts (geometry) :projections (layout))
(:name :border-right :id ebox/border-right :shorthand border-right) (:name :margin-inline :id ebox/margin-inline :shorthand margin-inline
(:name :border-bottom :id ebox/border-bottom :shorthand border-bottom) :impacts (geometry) :projections (layout))
(:name :border-left :id ebox/border-left :shorthand border-left) (: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 (:name :border-top-p :id ebox/border-top-p
:shorthand border-top-present :validator boolean :shorthand border-top-present :validator boolean
:exclusive-outputs t :exclusive-outputs t
:impacts (geometry structure) :projections (layout)
:group structure :dirty-kind structure :signature layout) :group structure :dirty-kind structure :signature layout)
(:name :border-bottom-p :id ebox/border-bottom-p (:name :border-bottom-p :id ebox/border-bottom-p
:shorthand border-bottom-present :validator boolean :shorthand border-bottom-present :validator boolean
:exclusive-outputs t :exclusive-outputs t
:impacts (geometry structure) :projections (layout)
:group structure :dirty-kind structure :signature layout) :group structure :dirty-kind structure :signature layout)
(:name :border-width :id ebox/border-width :shorthand border-width (:name :border-width :id ebox/border-width :shorthand border-width
:group geometry :dirty-kind geometry :signature layout) :group geometry :dirty-kind geometry :signature layout)
@ -259,16 +267,8 @@
:group geometry :dirty-kind geometry :signature layout)) :group geometry :dirty-kind geometry :signature layout))
"Canonical Ebox property schemas and public aliases.") "Canonical Ebox property schemas and public aliases.")
(defvar ebox-style--property-table nil (defvar ebox-style-schemas)
"Hash table from canonical property names and aliases to metadata.") (defvar ebox-style--property-index)
(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.")
(defconst ebox-style--declaration-cache-max-entries 512 (defconst ebox-style--declaration-cache-max-entries 512
"Maximum canonical declaration compilations retained by Ebox.") "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) (make-hash-table :test 'equal)
"Bounded memo table for canonical Ebox declaration compilation.") "Bounded memo table for canonical Ebox declaration compilation.")
(defun ebox-style--register-property-name (table name property role) (defun ebox-style--property (name)
"Register NAME for PROPERTY ROLE in TABLE, rejecting collisions." "Return registry-owned style metadata for internal NAME lookup."
(when-let* ((existing (gethash name table))) (gethash name ebox-style--property-index))
(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) (defun ebox-style-property (name)
"Return registered style metadata for canonical property or alias NAME." "Return detached style metadata for canonical property or alias NAME."
(gethash name (ebox-style--ensure-property-table))) (ecss-schema-set-property-metadata ebox-style-schemas name))
(defun ebox-style-canonical-name (name) (defun ebox-style-canonical-name (name)
"Return canonical CSS-like longhand property name for 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))) (plist-get property :name)))
(defun ebox-style-property-accepted-p (tag name) (defun ebox-style-property-accepted-p (tag name)
"Return non-nil when author form TAG accepts property 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))) (ebox-style--form-property-allowed-p tag property)))
(defun ebox-style-schema-id (name) (defun ebox-style-schema-id (name)
"Return namespaced ECSS schema id for Ebox property 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))) (plist-get property :id)))
(defun ebox-style--put (plist key value) (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))) (setq options (plist-put options :shorthand shorthand)))
options)) 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) (defvar ebox-style--closed-computed-cache (make-hash-table :test #'equal)
"Bounded cache of selector-free, dependency-free computed styles.") "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 (cl-loop for (_property value) on declarations by #'cddr
never (tp-computed-p value))) 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) (defvar ebox-style-stylesheet (ebox-style--create-stylesheet)
"Isolated ECSS stylesheet containing Ebox layout and paint rules.") "Isolated ECSS stylesheet containing Ebox layout and paint rules.")
@ -782,6 +789,12 @@ author declaration that references the slot."
right (cddr right))) right (cddr right)))
(and equal-p (null left) (null 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) (defun ebox-style--inherited-declarations-changed-p (old new)
"Return non-nil when OLD and NEW change descendant style inputs. "Return non-nil when OLD and NEW change descendant style inputs.
The inherited property domain comes only from the canonical schema. ECSS 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." "Reject duplicate canonical properties in author PLIST."
(let ((seen (make-hash-table :test 'eq))) (let ((seen (make-hash-table :test 'eq)))
(cl-loop for (name _value) on plist by #'cddr (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)) for id = (and property (plist-get property :id))
when id when id
do (when-let* ((previous (gethash id seen))) 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) (defun ebox-style--author-property-output-ids (name value)
"Return canonical output IDs produced by author NAME and 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)) (if-let* ((shorthand-name (plist-get property :shorthand))
(expander (ebox-style--shorthand shorthand-name))) (expander (ebox-style--shorthand shorthand-name)))
(cl-loop for (id _output) on (funcall expander value) by #'cddr (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." "Reject author properties whose canonical outputs overlap in PLIST."
(let (entries) (let (entries)
(cl-loop for (name value) on plist by #'cddr (cl-loop for (name value) on plist by #'cddr
for property = (ebox-style-property name) for property = (ebox-style--property name)
when property when property
do (push (list :name name :value value :property property) do (push (list :name name :value value :property property)
entries)) entries))
@ -899,17 +912,25 @@ not affect declaration compilation and are intentionally excluded."
(defun ebox-style--validate-declaration-values (declarations) (defun ebox-style--validate-declaration-values (declarations)
"Return DECLARATIONS after static Ebox-owned value validation." "Return DECLARATIONS after static Ebox-owned value validation."
(cl-loop for (property value) on declarations by #'cddr (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)) for validator = (and schema (plist-get schema :validator))
when (ebox-style--static-invalid-declaration-p validator value) when (ebox-style--static-invalid-declaration-p validator value)
do (error "ebox: invalid value for %S: %S" do (error "ebox: invalid value for %S: %S"
(plist-get schema :name) value)) (plist-get schema :name) value))
declarations) 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) (defun ebox-style-compile-declarations (plist &optional strict)
"Compile Ebox PLIST aliases to ECSS schema declarations. "Compile Ebox PLIST aliases to ECSS schema declarations.
When STRICT is non-nil, reject properties outside the Ebox style domain." When STRICT is non-nil, reject properties outside the Ebox style domain."
(ebox-style--ensure-properties)
(unless (ebox-style--valid-plist-p plist) (unless (ebox-style--valid-plist-p plist)
(user-error "Ebox style declarations must be an even property list")) (user-error "Ebox style declarations must be an even property list"))
(ebox-style--validate-no-duplicate-properties plist) (ebox-style--validate-no-duplicate-properties plist)
@ -931,7 +952,7 @@ When STRICT is non-nil, reject properties outside the Ebox style domain."
property))) property)))
(let ((compiled (let ((compiled
(ebox-style--validate-declaration-values (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) (when (>= (hash-table-count ebox-style--declaration-cache)
ebox-style--declaration-cache-max-entries) ebox-style--declaration-cache-max-entries)
(clrhash ebox-style--declaration-cache)) (clrhash ebox-style--declaration-cache))
@ -963,7 +984,7 @@ When STRICT is non-nil, reject properties outside the Ebox style domain."
declarations)) declarations))
(let ((seen (make-hash-table :test #'eq))) (let ((seen (make-hash-table :test #'eq)))
(cl-loop for (id _value) on declarations by #'cddr (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))) unless (and property (eq id (plist-get property :id)))
do (error "Ebox declaration is not a canonical property ID: %S" do (error "Ebox declaration is not a canonical property ID: %S"
id) id)
@ -984,7 +1005,7 @@ When STRICT is non-nil, reject properties outside the Ebox style domain."
(defun ebox-style-declaration-properties (declarations predicate) (defun ebox-style-declaration-properties (declarations predicate)
"Return canonical author properties in DECLARATIONS matching PREDICATE." "Return canonical author properties in DECLARATIONS matching PREDICATE."
(cl-loop for (id value) on declarations by #'cddr (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)) (error "Unknown canonical Ebox property: %S" id))
when (funcall predicate property) when (funcall predicate property)
append (list (plist-get property :name) value))) 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) (declarations predicate)
"Return non-nil when DECLARATIONS include facts outside PREDICATE." "Return non-nil when DECLARATIONS include facts outside PREDICATE."
(cl-loop for (id _value) on declarations by #'cddr (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)) (error "Unknown canonical Ebox property: %S" id))
thereis (not (funcall predicate property)))) thereis (not (funcall predicate property))))
(defun ebox-style-merge-declarations (base overrides) (defun ebox-style-merge-declarations (base overrides)
"Merge canonical BASE and OVERRIDES through ECSS property schemas." "Merge canonical BASE and OVERRIDES through ECSS property schemas."
(ebox-style--ensure-properties)
(ecss-merge-declarations ebox-style-schemas base overrides)) (ecss-merge-declarations ebox-style-schemas base overrides))
(cl-defun ebox-style-add-rule (cl-defun ebox-style-add-rule
(selector declarations &key (origin 'author) layer scope) (selector declarations &key (origin 'author) layer scope)
"Add SELECTOR rule with Ebox DECLARATIONS to the isolated stylesheet. "Add SELECTOR rule with Ebox DECLARATIONS to the isolated stylesheet.
ORIGIN, LAYER, and SCOPE use ECSS cascade semantics." ORIGIN, LAYER, and SCOPE use ECSS cascade semantics."
(ebox-style--ensure-properties)
(ecss-stylesheet-add-rule (ecss-stylesheet-add-rule
ebox-style-stylesheet ebox-style-schemas (ebox-style--selector selector) ebox-style-stylesheet ebox-style-schemas (ebox-style--selector selector)
(ebox-style-compile-declarations declarations t) (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) (defun ebox-style-compute-subject (subject declarations &optional parent-style)
"Compute SUBJECT style from DECLARATIONS and optional PARENT-STYLE via ECSS." "Compute SUBJECT style from DECLARATIONS and optional PARENT-STYLE via ECSS."
(ebox-style--ensure-properties)
(let* ((closed-p (let* ((closed-p
(and (not (ebox-style-cascade-active-p)) (and (not (ebox-style-cascade-active-p))
(ebox-style--closed-declarations-p declarations))) (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 old 'ebox/color)
(plist-member new '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) (defun ebox-style--inherited-style-signature (style)
"Return the inherited-value fingerprint of computed STYLE, or nil. "Return the inherited-value fingerprint of computed STYLE, or nil.
The fingerprint deliberately contains only values that can flow from a 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) (defun ebox-style--remove-style-properties (plist)
"Return PLIST without Ebox style or ECSS custom properties." "Return PLIST without Ebox style or ECSS custom properties."
(cl-loop for (property value) on plist by #'cddr (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)) (ebox-style--custom-property-p property))
append (list property value))) append (list property value)))
@ -1636,7 +1648,7 @@ until their private encoding is removed."
(defun ebox-style-dirty-kind (name) (defun ebox-style-dirty-kind (name)
"Return dirty kind for canonical property or alias 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))) (plist-get property :dirty-kind)))
(defun ebox-style-signature (computed-style groups) (defun ebox-style-signature (computed-style groups)
@ -1645,7 +1657,7 @@ until their private encoding is removed."
(while computed-style (while computed-style
(let* ((property-name (pop computed-style)) (let* ((property-name (pop computed-style))
(value (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)))) (group (and property (plist-get property :signature))))
(when (and property (when (and property
(or (null groups) (or (null groups)
@ -1662,8 +1674,6 @@ until their private encoding is removed."
(list (car entry) (cdr entry))) (list (car entry) (cdr entry)))
entries)))) entries))))
(ebox-style-register-properties)
(defconst ebox-style-ebox-property-rules (defconst ebox-style-ebox-property-rules
'((:padding :expand (:padding-top :padding-right '((:padding :expand (:padding-top :padding-right
:padding-bottom :padding-left) :padding-bottom :padding-left)

View File

@ -3992,7 +3992,7 @@ cannot be changed through the unpublished batch root."
(ebox-get box target-key)) (ebox-get box target-key))
value)) value))
(and declarations-changed (and declarations-changed
(ebox-style-property key))) (ebox-style--property key)))
do (if (eq target-key :content) do (if (eq target-key :content)
(if-let* ((text (if-let* ((text
(ebox--region-update-direct-text-node box))) (ebox--region-update-direct-text-node box)))

View File

@ -508,17 +508,24 @@
:removed-node-ids :removed-node-ids
(ebox--runtime-node-ids removed-node)))))))) (ebox--runtime-node-ids removed-node))))))))
(ert-deftest ebox-style-schema-registration-is-not-per-node-copy () (ert-deftest ebox-style-schema-composition-is-not-per-node-work ()
"Repeated node construction must not copy the whole ECSS schema domain." "Repeated node construction must not rebuild the immutable schema domain."
(let ((calls 0) (let ((package-calls 0)
(original (symbol-function 'ecss-schema-set-property))) (compose-calls 0)
(cl-letf (((symbol-function 'ecss-schema-set-property) (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) (lambda (&rest arguments)
(cl-incf calls) (cl-incf package-calls)
(apply original arguments)))) (apply original-package arguments)))
((symbol-function 'ecss-schema-set-compose)
(lambda (&rest arguments)
(cl-incf compose-calls)
(apply original-compose arguments))))
(dotimes (_ 24) (dotimes (_ 24)
(ebox-test-box :content "schema-hot-path" :color "#111111"))) (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 () (ert-deftest ebox-style-declaration-compilation-is-memoized ()
"Repeated equivalent style declarations compile through ECSS once." "Repeated equivalent style declarations compile through ECSS once."

View File

@ -454,6 +454,43 @@
:font-height) :font-height)
literal)))) 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 () (ert-deftest ebox-style-context-reads-each-ecss-snapshot-once ()
"Style projection should not repeatedly copy one computed ECSS snapshot." "Style projection should not repeatedly copy one computed ECSS snapshot."
(let* ((style (let* ((style
@ -2158,7 +2195,7 @@
"A color-only change should produce paint dirty, not geometry dirty." "A color-only change should produce paint dirty, not geometry dirty."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box :content "Paint" :width 80 (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)))) (region-id (car (ebox-region-ids box))))
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(let ((old (ebox--current-layout-snapshots (current-buffer)))) (let ((old (ebox--current-layout-snapshots (current-buffer))))
@ -2225,7 +2262,7 @@
"Paint-only dirty entries should produce paint-patch ops." "Paint-only dirty entries should produce paint-patch ops."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box :content "Paint" :width 80 (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)))) (region-id (car (ebox-region-ids box))))
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(let ((full-rerenders (let ((full-rerenders
@ -4255,7 +4292,7 @@
"Update reports should show dirty and patch-set summaries." "Update reports should show dirty and patch-set summaries."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box :content "Report" :width 80 (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)))) (region-id (car (ebox-region-ids box))))
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(ebox-test--region-update region-id :border-color "blue") (ebox-test--region-update region-id :border-color "blue")
@ -4569,14 +4606,18 @@
(box :id left :content "Left" :width (120) (box :id left :content "Left" :width (120)
:padding (0 (8)) :border "#D97757") :padding (0 (8)) :border "#D97757")
(box :content "Body" :width (120) (box :content "Body" :width (120)
:padding (0 (8)) :border "#D97757" :padding (0 (8))
:border-top none)) :border-right "#D97757"
:border-bottom "#D97757"
:border-left "#D97757"))
(column (column
(box :id right :content "Right" :width (120) (box :id right :content "Right" :width (120)
:padding (0 (8)) :border "#5E7F6A") :padding (0 (8)) :border "#5E7F6A")
(box :content "Body" :width (120) (box :content "Body" :width (120)
:padding (0 (8)) :border "#5E7F6A" :padding (0 (8))
:border-top none))))) :border-right "#5E7F6A"
:border-bottom "#5E7F6A"
:border-left "#5E7F6A")))))
(left-id (plist-get (left-id (plist-get
(car (ebox-selector-query-all layout "#left")) (car (ebox-selector-query-all layout "#left"))
:region-id))) :region-id)))
@ -4606,18 +4647,26 @@
:padding (0 (12)) :border "#D97757" :bgcolor "#FFF8F2" :padding (0 (12)) :border "#D97757" :bgcolor "#FFF8F2"
(column (column
(box :id left :content "Left" :width (120) (box :id left :content "Left" :width (120)
:padding (0 (8)) :border "#D97757" :padding (0 (8))
:border-bottom none) :border-top "#D97757"
:border-right "#D97757"
:border-left "#D97757")
(box :content "Body" :width (120) (box :content "Body" :width (120)
:padding (0 (8)) :border "#D97757" :padding (0 (8))
:border-top none)) :border-right "#D97757"
:border-bottom "#D97757"
:border-left "#D97757"))
(column (column
(box :content "Right" :width (120) (box :content "Right" :width (120)
:padding (0 (8)) :border "#5E7F6A" :padding (0 (8))
:border-bottom none) :border-top "#5E7F6A"
:border-right "#5E7F6A"
:border-left "#5E7F6A")
(box :content "Body" :width (120) (box :content "Body" :width (120)
:padding (0 (8)) :border "#5E7F6A" :padding (0 (8))
:border-top none))))) :border-right "#5E7F6A"
:border-bottom "#5E7F6A"
:border-left "#5E7F6A")))))
(left-id (plist-get (left-id (plist-get
(car (ebox-selector-query-all layout "#left")) (car (ebox-selector-query-all layout "#left"))
:region-id))) :region-id)))
@ -4650,18 +4699,26 @@
:padding (0 (12)) :border "#D97757" :bgcolor "#FFF8F2" :padding (0 (12)) :border "#D97757" :bgcolor "#FFF8F2"
(column (column
(box :id left :content "Left" :width (120) (box :id left :content "Left" :width (120)
:padding (0 (8)) :border "#D97757" :padding (0 (8))
:border-bottom none) :border-top "#D97757"
:border-right "#D97757"
:border-left "#D97757")
(box :content "Body" :width (120) (box :content "Body" :width (120)
:padding (0 (8)) :border "#D97757" :padding (0 (8))
:border-top none)) :border-right "#D97757"
:border-bottom "#D97757"
:border-left "#D97757"))
(column (column
(box :content "Right" :width (120) (box :content "Right" :width (120)
:padding (0 (8)) :border "#5E7F6A" :padding (0 (8))
:border-bottom none) :border-top "#5E7F6A"
:border-right "#5E7F6A"
:border-left "#5E7F6A")
(box :content "Body" :width (120) (box :content "Body" :width (120)
:padding (0 (8)) :border "#5E7F6A" :padding (0 (8))
:border-top none))))) :border-right "#5E7F6A"
:border-bottom "#5E7F6A"
:border-left "#5E7F6A")))))
(left-id (plist-get (left-id (plist-get
(car (ebox-selector-query-all layout "#left")) (car (ebox-selector-query-all layout "#left"))
:region-id))) :region-id)))
@ -5335,11 +5392,7 @@
:height 4 :min-height 3 :max-height 6 :height 4 :min-height 3 :max-height 6
:padding-top 1 :padding-bottom 1) :padding-top 1 :padding-bottom 1)
(ebox-test--region-update region-id (ebox-test--region-update region-id
:margin (list 0 '(8) 0 '(8)) :margin-inline '((8) (8)))
:margin-inline '((8) (8))
:margin-block '(0 0)
:margin-top 0 :margin-right '(8)
:margin-bottom 0 :margin-left '(8))
(let ((report (ebox-test--buffer-update-report))) (let ((report (ebox-test--buffer-update-report)))
(should (eq (plist-get report :strategy) 'span-patch)) (should (eq (plist-get report :strategy) 'span-patch))
(should (eq (plist-get report :owner-type) 'box)) (should (eq (plist-get report :owner-type) 'box))
@ -5755,11 +5808,7 @@
(region-id (plist-get handle :region-id)) (region-id (plist-get handle :region-id))
(node-id (plist-get handle :node-id))) (node-id (plist-get handle :node-id)))
(ebox-test--region-update region-id (ebox-test--region-update region-id
:margin (list 0 '(8) 0 '(8)) :margin-inline '((8) (8)))
:margin-inline '((8) (8))
:margin-block '(0 0)
:margin-top 0 :margin-right '(8)
:margin-bottom 0 :margin-left '(8))
(let* ((report (ebox-test--buffer-update-report)) (let* ((report (ebox-test--buffer-update-report))
(snapshot (ebox--ensure-layout-snapshot-spans (snapshot (ebox--ensure-layout-snapshot-spans
(current-buffer) node-id)) (current-buffer) node-id))
@ -7109,8 +7158,11 @@
`(flex :id "flex-window-wrapper" :width (240) `(flex :id "flex-window-wrapper" :width (240)
:flex-flow (row wrap) :flex-flow (row wrap)
:row-gap 1 :column-gap 0 :row-gap 1 :column-gap 0
:padding (0 (8)) :border "#D97757" :padding (0 (8))
:border-top none :bgcolor "#FFF8F2" :border-right "#D97757"
:border-bottom "#D97757"
:border-left "#D97757"
:bgcolor "#FFF8F2"
,@(cl-loop for idx below 8 ,@(cl-loop for idx below 8
collect collect
`(box :content ,(format "wrapper %02d" idx) `(box :content ,(format "wrapper %02d" idx)
@ -7156,9 +7208,8 @@
(let* ((box (ebox-test-box :width 120 (let* ((box (ebox-test-box :width 120
:padding-left 8 :padding-left 8
:padding-right 8 :padding-right 8
:border-left 1 :border-left '(1 solid "#D97757")
:border-right 1 :border-right '(1 solid "#D97757")
:border-color "#D97757"
:bgcolor "#FFF8F2")) :bgcolor "#FFF8F2"))
(source (propertize "child" 'ebox-content 77 (source (propertize "child" 'ebox-content 77
'ebox-content-owner 77 'ebox-content-owner 77
@ -7244,8 +7295,11 @@
`(flex :id "flex-prefix-wrapper" :width (240) `(flex :id "flex-prefix-wrapper" :width (240)
:flex-flow (row wrap) :flex-flow (row wrap)
:row-gap 1 :column-gap 0 :row-gap 1 :column-gap 0
:padding (0 (8)) :border "#D97757" :padding (0 (8))
:border-top none :bgcolor "#FFF8F2" :border-right "#D97757"
:border-bottom "#D97757"
:border-left "#D97757"
:bgcolor "#FFF8F2"
,@(cl-loop for idx below 8 ,@(cl-loop for idx below 8
collect collect
`(box :content ,(format "extend %02d" idx) `(box :content ,(format "extend %02d" idx)
@ -7277,8 +7331,11 @@
(ebox-test-build (ebox-test-build
`(flex :width (240) :flex-flow (row wrap) `(flex :width (240) :flex-flow (row wrap)
:row-gap 1 :column-gap 0 :row-gap 1 :column-gap 0
:padding (0 (8)) :border "#D97757" :padding (0 (8))
:border-top none :bgcolor "#FFF8F2" :border-right "#D97757"
:border-bottom "#D97757"
:border-left "#D97757"
:bgcolor "#FFF8F2"
,@(cl-loop for idx below 16 ,@(cl-loop for idx below 16
collect collect
`(box :content ,(format "piece %02d" idx) `(box :content ,(format "piece %02d" idx)
@ -7308,8 +7365,9 @@
:row-gap 1 :column-gap 0 :row-gap 1 :column-gap 0
:padding (0 (8) 2 (8)) :padding (0 (8) 2 (8))
:margin-bottom 2 :margin-bottom 2
:border "#D97757" :border-right "#D97757"
:border-top none :border-bottom "#D97757"
:border-left "#D97757"
:bgcolor "#FFF8F2" :bgcolor "#FFF8F2"
,@(cl-loop for idx below 2 ,@(cl-loop for idx below 2
collect collect
@ -7353,8 +7411,9 @@
:flex-flow (row wrap) :flex-flow (row wrap)
:row-gap 1 :column-gap 0 :row-gap 1 :column-gap 0
:padding (0 (8)) :padding (0 (8))
:border "#D97757" :border-right "#D97757"
:border-top none :border-bottom "#D97757"
:border-left "#D97757"
:bgcolor "#FFF8F2" :bgcolor "#FFF8F2"
,@(cl-loop for idx below 4 ,@(cl-loop for idx below 4
collect collect
@ -7390,8 +7449,9 @@
:flex-flow (row wrap) :flex-flow (row wrap)
:row-gap 1 :column-gap 0 :row-gap 1 :column-gap 0
:padding (0 (8)) :padding (0 (8))
:border "#D97757" :border-right "#D97757"
:border-top none :border-bottom "#D97757"
:border-left "#D97757"
,@(cl-loop for idx below 4 ,@(cl-loop for idx below 4
collect collect
`(box :content ,(format "atomic %02d" idx) `(box :content ,(format "atomic %02d" idx)
@ -9251,8 +9311,10 @@
(flex :id "signature-flex" :width (viewport) (flex :id "signature-flex" :width (viewport)
:flex-flow (row wrap) :flex-flow (row wrap)
:row-gap 1 :column-gap (8) :row-gap 1 :column-gap (8)
:padding (0 (12)) :border "#D97757" :padding (0 (12))
:border-top none :border-right "#D97757"
:border-bottom "#D97757"
:border-left "#D97757"
,@(cl-loop for idx below 24 ,@(cl-loop for idx below 24
collect collect
`(box :content ,(format "signature %02d" idx) `(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." "A hidden box renders blank with identical text, lengths, and metadata."
(let* ((make-box (let* ((make-box
(lambda (visibility) (lambda (visibility)
(ebox-test-box :content "Hello" :width (list 90) :border t (ebox-test-box :content "Hello" :width (list 90)
:border-color "#AA3311" :bgcolor "#223344" :border '(1 solid "#AA3311") :bgcolor "#223344"
:color "#FFFFFF" :padding '(0 1) :color "#FFFFFF" :padding '(0 1)
:visibility visibility))) :visibility visibility)))
(visible (ebox-render (funcall make-box 'visible))) (visible (ebox-render (funcall make-box 'visible)))

View File

@ -95,13 +95,49 @@
'(:background-color "red" :bgcolor "blue") t) '(:background-color "red" :bgcolor "blue") t)
:type 'error)) :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 () (ert-deftest ebox-style-property-registry-rejects-alias-collisions ()
"An alias should never shadow another canonical property." "An alias should never shadow another canonical property."
(let ((ebox-style--property-table nil) (should-error
(ebox-style--property-definitions (ecss-schema-package-create
'((:name :first :id ebox/first :aliases (:second)) 'ebox
(:name :second :id ebox/second)))) (mapcar
(should-error (ebox-style--ensure-property-table) :type 'error))) #'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 () (ert-deftest ebox-style-border-present-is-a-typed-shorthand-not-an-alias ()
"The concise border boolean should expand to canonical width and style." "The concise border boolean should expand to canonical width and style."

View File

@ -428,7 +428,9 @@
:padding (0 (10)) :border "#5E7F6A" :padding (0 (10)) :border "#5E7F6A"
:text-align center "wrap") :text-align center "wrap")
(flex :id "body" :max-width (293) :padding (0 (8)) (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) :flex-wrap wrap :column-gap (8)
(box :flex-shrink 0 :width (118) "A 118") (box :flex-shrink 0 :width (118) "A 118")
(box :flex-shrink 0 :width (118) "B 118") (box :flex-shrink 0 :width (118) "B 118")
@ -459,14 +461,19 @@
(layout (layout
(ebox-build (ebox-build
'(flex :flex-flow (row wrap) :row-gap 1 :column-gap (12) '(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" :bgcolor "#FBF9FF"
(column (column
(box :max-width (440) (box :max-width (440)
:padding (0 (10)) :border "#6F6A95" :padding (0 (10)) :border "#6F6A95"
:text-align center ":flex-flow (row wrap)") :text-align center ":flex-flow (row wrap)")
(flex :max-width (440) :padding (0 (8)) (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)) :flex-flow (row wrap) :gap (1 (12))
(box :width (170) "A") (box :width (170) "A")
(box :width (170) "B") (box :width (170) "B")
@ -476,7 +483,9 @@
:padding (0 (10)) :border "#6F6A95" :padding (0 (10)) :border "#6F6A95"
:text-align center ":flex-flow (column wrap)") :text-align center ":flex-flow (column wrap)")
(flex :max-width (440) :padding (0 (8)) (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)) :flex-flow (column wrap) :gap (1 (12))
(box :width (140) "A") (box :width (140) "A")
(box :width (140) "B") (box :width (140) "B")