diff --git a/docs/user/ebox-api-reference.en.md b/docs/user/ebox-api-reference.en.md index 668a776..881ce30 100644 --- a/docs/user/ebox-api-reference.en.md +++ b/docs/user/ebox-api-reference.en.md @@ -190,7 +190,7 @@ together. | Content/geometry | `:content`, `:box-sizing`, `:width`, `:min-width`, `:max-width`, `:height`, `:min-height`, `:max-height` | | Padding | `:padding`, `:padding-inline`, `:padding-block`; longhands `:padding-block-start`, `:padding-inline-end`, `:padding-block-end`, `:padding-inline-start`; aliases `:padding-top`, `:padding-right`, `:padding-bottom`, `:padding-left` and the pixel/height aliases `:padding-right-pixel`, `:padding-left-pixel`, `:padding-top-height`, `:padding-bottom-height` | | Margin | `:margin`, `:margin-inline`, `:margin-block`; longhands `:margin-block-start`, `:margin-inline-end`, `:margin-block-end`, `:margin-inline-start`; aliases `:margin-top`, `:margin-right`, `:margin-bottom`, `:margin-left` and the pixel/height aliases `:margin-right-pixel`, `:margin-left-pixel`, `:margin-top-height`, `:margin-bottom-height` | -| Border | `:border`, `:border-top`, `:border-right`, `:border-bottom`, `:border-left`, `:border-width`, `:border-style`, `:border-color`; side longhands `:border-*-width`, `:border-*-style`, `:border-*-color`; compatibility aliases `:border-top-p`, `:border-bottom-p`, `:border-left-pixel`, `:border-right-pixel` | +| Border | `:border`, `:border-top`, `:border-right`, `:border-bottom`, `:border-left`, `:border-width`, `:border-style`, `:border-color`; side longhands `:border-*-width`, `:border-*-style`, `:border-*-color`; Ebox boolean shorthands `:border-top-p`, `:border-bottom-p`; compatibility aliases `:border-left-pixel`, `:border-right-pixel` | | Paint | `:color`, `:background-color`, and alias `:bgcolor` | | Typography | `:font`, `:font-family`, `:font-height` and alias `:font-size`, `:font-weight`, `:font-slant` | | Text/layout | `:text-align` (`left`, `center`, `right`), `:vertical-align` (`top`, `center`/`middle`, `bottom`), `:overflow` (`scroll`, `hidden`, `visible`), `:wrap-mode` (`word`, `char`, `kp`, or nil), `:visibility` (`visible` or `hidden`) | @@ -587,7 +587,8 @@ style-rule functions immediately below are public module-level style APIs. | Function | Use | | --- | --- | -| `ebox-create`, `ebox-build` | Create a node or compile the `.ebox` list DSL. | +| `ebox-text-create`, `ebox-normal-layout-create`, `ebox-box-create` | Framework-integration port for typed TextNode, Normal LayoutConfig, and BoxNode values. | +| `ebox-create`, `ebox-build` | Create a legacy node or compile the `.ebox` list DSL during migration. | | `ebox-concat`, `ebox-stack`, `ebox-row`, `ebox-column`, `ebox-spacer` | Compose simple horizontal/vertical layouts and blank boxes. | | `ebox-flex`, `ebox-flex-item` | Build flex containers and item metadata. | | `ebox-grid`, `ebox-grid-fr`, `ebox-grid-item` | Build Grid layouts, fractional tracks, and placement metadata. | diff --git a/docs/user/ebox-api-reference.zh.md b/docs/user/ebox-api-reference.zh.md index 784c397..1bd119e 100644 --- a/docs/user/ebox-api-reference.zh.md +++ b/docs/user/ebox-api-reference.zh.md @@ -173,7 +173,7 @@ property;`ebox-region-update` 接受其目标支持的可变 content/style/scr | Content/geometry | `:content`、`:box-sizing`、`:width`、`:min-width`、`:max-width`、`:height`、`:min-height`、`:max-height` | | Padding | `:padding`、`:padding-inline`、`:padding-block`;longhand `:padding-block-start`、`:padding-inline-end`、`:padding-block-end`、`:padding-inline-start`;别名 `:padding-top`、`:padding-right`、`:padding-bottom`、`:padding-left`,以及 `:padding-right-pixel`、`:padding-left-pixel`、`:padding-top-height`、`:padding-bottom-height` | | Margin | `:margin`、`:margin-inline`、`:margin-block`;longhand `:margin-block-start`、`:margin-inline-end`、`:margin-block-end`、`:margin-inline-start`;别名 `:margin-top`、`:margin-right`、`:margin-bottom`、`:margin-left`,以及 `:margin-right-pixel`、`:margin-left-pixel`、`:margin-top-height`、`:margin-bottom-height` | -| Border | `:border`、`:border-top`、`:border-right`、`:border-bottom`、`:border-left`、`:border-width`、`:border-style`、`:border-color`;side longhand `:border-*-width`、`:border-*-style`、`:border-*-color`;兼容别名 `:border-top-p`、`:border-bottom-p`、`:border-left-pixel`、`:border-right-pixel` | +| Border | `:border`、`:border-top`、`:border-right`、`:border-bottom`、`:border-left`、`:border-width`、`:border-style`、`:border-color`;side longhand `:border-*-width`、`:border-*-style`、`:border-*-color`;Ebox boolean shorthand `:border-top-p`、`:border-bottom-p`;兼容别名 `:border-left-pixel`、`:border-right-pixel` | | Paint | `:color`、`:background-color`、别名 `:bgcolor` | | Typography | `:font`、`:font-family`、`:font-height` 和别名 `:font-size`、`:font-weight`、`:font-slant` | | Text/layout | `:text-align`(`left`、`center`、`right`)、`:vertical-align`(`top`、`center`/`middle`、`bottom`)、`:overflow`(`scroll`、`hidden`、`visible`)、`:wrap-mode`(`word`、`char`、`kp` 或 nil)、`:visibility`(`visible` 或 `hidden`) | @@ -533,7 +533,8 @@ style API。 | 函数 | 用途 | | --- | --- | -| `ebox-create`、`ebox-build` | 创建节点或编译 `.ebox` list DSL。 | +| `ebox-text-create`、`ebox-normal-layout-create`、`ebox-box-create` | 框架集成使用的 typed TextNode、Normal LayoutConfig 与 BoxNode 端口。 | +| `ebox-create`、`ebox-build` | 迁移期间创建旧节点或编译 `.ebox` list DSL。 | | `ebox-concat`、`ebox-stack`、`ebox-row`、`ebox-column`、`ebox-spacer` | 组合简单横向/纵向布局与空白 box。 | | `ebox-flex`、`ebox-flex-item` | 构建 Flex 容器与 item metadata。 | | `ebox-grid`、`ebox-grid-fr`、`ebox-grid-item` | 构建 Grid、分数轨道和 placement metadata。 | diff --git a/ebox-canonical.el b/ebox-canonical.el new file mode 100644 index 0000000..98b6f46 --- /dev/null +++ b/ebox-canonical.el @@ -0,0 +1,213 @@ +;;; ebox-canonical.el --- Typed canonical Ebox nodes -*- lexical-binding: t; -*- + +;;; Commentary: +;; Owns the typed programmatic boundary between normalized author input and +;; Ebox's current runtime representation. Canonical kind, layout, source, and +;; child facts remain explicit even when the runtime reuses an existing box +;; encoding for rendering. This module does not parse the `.ebox' DSL or +;; perform measurement, layout, paint, or buffer publication. + +;;; Code: + +(require 'cl-lib) +(require 'subr-x) +(require 'ebox-style) +(require 'ebox-node-factory) + +(cl-defstruct + (ebox-layout-config + (:constructor ebox-layout-config--create)) + "Canonical typed layout selection for one BoxNode." + kind + props) + +(defun ebox-canonical--validate-plist (plist context) + "Validate PLIST shape for error CONTEXT and return PLIST." + (unless (proper-list-p plist) + (error "%s properties must be a proper plist: %S" context plist)) + (unless (zerop (% (length plist) 2)) + (error "%s property is missing a value: %S" context (car (last plist)))) + (cl-loop for key in plist by #'cddr + unless (keywordp key) + do (error "%s property name must be a keyword: %S" context key)) + plist) + +(defun ebox-canonical--without-keys (plist keys) + "Return PLIST without entries whose keys occur in KEYS." + (cl-loop for (key value) on plist by #'cddr + unless (memq key keys) + append (list key value))) + +(defun ebox-canonical--field-values (plist field) + "Return every FIELD value from PLIST in source order." + (cl-loop for (key value) on plist by #'cddr + when (eq key field) + collect value)) + +(defun ebox-canonical--required-field (plist field context) + "Return exactly one required FIELD from PLIST for error CONTEXT." + (let ((values (ebox-canonical--field-values plist field))) + (unless (= (length values) 1) + (error "%s requires %S exactly once" context field)) + (car values))) + +(defun ebox-canonical--optional-field (plist field context) + "Return `(PRESENT . VALUE)' for optional FIELD in PLIST and CONTEXT." + (let ((values (ebox-canonical--field-values plist field))) + (when (> (length values) 1) + (error "%s accepts %S at most once" context field)) + (and values (cons t (car values))))) + +(defun ebox-canonical--compiled-property-definitions (plist context) + "Return canonical property definitions compiled from PLIST for CONTEXT." + (condition-case err + (cl-loop for (property _value) + on (ebox-style-compile-declarations plist t) by #'cddr + collect (or (ebox-style-property property) + (error "Missing canonical Ebox property: %S" + property))) + (error + (error "%s rejected properties: %s" context (error-message-string err))))) + +(defun ebox-canonical--text-measurement-property-p (property) + "Return non-nil when PROPERTY belongs to canonical Text measurement." + (eq (plist-get property :group) 'typography)) + +(defun ebox-canonical--normal-box-property-p (property) + "Return non-nil when PROPERTY belongs to Normal Box geometry/participation." + (let ((contexts (plist-get property :contexts)) + (group (plist-get property :group)) + (name (plist-get property :name))) + (or (memq 'item contexts) + (and (memq 'box contexts) + (not (memq group '(paint typography))) + (not (eq name :wrap-mode)))))) + +(defun ebox-canonical--assert-property-role (plist predicate context) + "Require every compiled PLIST property to satisfy PREDICATE for CONTEXT." + (dolist (property + (ebox-canonical--compiled-property-definitions plist context)) + (unless (funcall predicate property) + (error "%s does not accept %S" + context (plist-get property :name))))) + +;;;###autoload +(defun ebox-normal-layout-create () + "Return the canonical Normal layout config." + (ebox-layout-config--create :kind 'normal :props nil)) + +(defun ebox-node-kind (node) + "Return canonical NODE kind, or nil for a legacy runtime node." + (and (listp node) (plist-get node :ebox-kind))) + +(defun ebox-text-node-p (node) + "Return non-nil when NODE is a canonical TextNode." + (eq (ebox-node-kind node) 'text)) + +(defun ebox-box-node-p (node) + "Return non-nil when NODE is a canonical BoxNode." + (eq (ebox-node-kind node) 'box)) + +(defun ebox-node-source-handle (node) + "Return canonical NODE's opaque source handle." + (and (listp node) (plist-get node :ebox-source-handle))) + +(defun ebox-text-node-value (node) + "Return canonical TextNode NODE's string payload." + (unless (ebox-text-node-p node) + (error "Expected canonical TextNode, got %S" node)) + (plist-get node :ebox-text-value)) + +(defun ebox-box-node-layout (node) + "Return canonical BoxNode NODE's typed layout config." + (unless (ebox-box-node-p node) + (error "Expected canonical BoxNode, got %S" node)) + (plist-get node :ebox-layout-config)) + +(defun ebox-box-node-children (node) + "Return canonical BoxNode NODE's retained canonical children." + (unless (ebox-box-node-p node) + (error "Expected canonical BoxNode, got %S" node)) + (plist-get node :ebox-canonical-children)) + +;;;###autoload +(defun ebox-text-create (&rest plist) + "Create a canonical TextNode from evaluated PLIST. + +`:value' is required and must be one string. `:source-handle' is optional. + Only text measurement properties are accepted; paint is projected separately." + (ebox-canonical--validate-plist plist "ebox-text-create") + (let* ((value (ebox-canonical--required-field + plist :value "Ebox Text")) + (source-field (ebox-canonical--optional-field + plist :source-handle "Ebox Text")) + (source-handle (cdr source-field)) + (props (ebox-canonical--without-keys + plist '(:value :source-handle)))) + (unless (stringp value) + (error "Ebox Text :value must be a string: %S" value)) + (ebox-canonical--assert-property-role + props #'ebox-canonical--text-measurement-property-p "Ebox Text") + (let ((node (apply #'ebox-create (append props (list :content value))))) + (plist-put node :ebox-kind 'text) + (plist-put node :ebox-text-value value) + (plist-put node :ebox-source-handle source-handle) + node))) + +;;;###autoload +(defun ebox-box-create (&rest plist) + "Create a canonical BoxNode from evaluated PLIST. + +`:layout' must occur exactly once as an `ebox-layout-config'. +`:children' is a list of canonical nodes. This first vertical slice supports +Normal layout with zero or one child; later slices extend the same typed port +to the other Layout variants." + (ebox-canonical--validate-plist plist "ebox-box-create") + (let* ((layout (ebox-canonical--required-field + plist :layout "Ebox Box")) + (children-field (ebox-canonical--optional-field + plist :children "Ebox Box")) + (outer-field (ebox-canonical--optional-field + plist :outer "Ebox Box")) + (source-field (ebox-canonical--optional-field + plist :source-handle "Ebox Box")) + (children (if children-field (cdr children-field) nil)) + (outer (if outer-field (cdr outer-field) 'block)) + (source-handle (cdr source-field)) + (props (ebox-canonical--without-keys + plist '(:layout :children :outer :source-handle)))) + (unless (ebox-layout-config-p layout) + (error "Ebox Box :layout must be a typed layout config: %S" + layout)) + (unless (memq outer '(inline block)) + (error "Ebox Box :outer must be inline or block: %S" outer)) + (unless (proper-list-p children) + (error "Ebox Box :children must be a proper list: %S" children)) + (unless (cl-every (lambda (child) + (or (ebox-text-node-p child) + (ebox-box-node-p child))) + children) + (error "Ebox Box children must be canonical Text/Box nodes: %S" + children)) + (unless (eq (ebox-layout-config-kind layout) 'normal) + (error "Ebox Box Layout is not implemented in this slice: %S" + (ebox-layout-config-kind layout))) + (when (> (length children) 1) + (error "Ebox Box Normal layout currently accepts at most one child")) + (ebox-canonical--assert-property-role + props #'ebox-canonical--normal-box-property-p "Ebox Box") + (let ((node + (apply #'ebox-create + (append props + (when children + (list :ebox-content-node (car children))))))) + (plist-put node :ebox-kind 'box) + (plist-put node :ebox-layout-config layout) + (plist-put node :ebox-canonical-children children) + (plist-put node :ebox-source-handle source-handle) + (plist-put node :display (list outer 'flow)) + node))) + +(provide 'ebox-canonical) + +;;; ebox-canonical.el ends here diff --git a/ebox-dsl.el b/ebox-dsl.el index 72fd60d..bbfce0a 100644 --- a/ebox-dsl.el +++ b/ebox-dsl.el @@ -11,6 +11,7 @@ (require 'ebox-tree) (require 'ebox-flex) (require 'ebox-grid) +(require 'ebox-canonical) (declare-function ebox-create "ebox" (&rest plist)) @@ -97,6 +98,20 @@ The first value is string content. The second value is a lazy child node." (when child-node (list :ebox-content-node child-node))))))) +(defun ebox--build-text (items) + "Build one canonical TextNode from DSL ITEMS." + (let* ((split (ebox--build-split-attrs items)) + (props (car split)) + (payloads (delq nil (cdr split)))) + (when props + (error "Ebox build: Text author properties are not implemented in this slice")) + (unless (= (length payloads) 1) + (error "ebox-build: text requires exactly one string payload")) + (unless (stringp (car payloads)) + (error "ebox-build: text payload must be a string: %S" (car payloads))) + (apply #'ebox-text-create + (append props (list :value (car payloads)))))) + (defun ebox--build-wrap-layout (tag props layout) "Wrap LAYOUT with box PROPS for DSL TAG, or return LAYOUT unchanged." (if (null props) @@ -220,13 +235,14 @@ and `column' may also receive box properties; those properties wrap the child layout in a box using the rendered child layout as content." (cond ((ebox--node-p dsl) dsl) - ((stringp dsl) (ebox-create :content dsl)) + ((stringp dsl) (ebox-text-create :value dsl)) ((not (and (consp dsl) (symbolp (car dsl)))) (error "ebox-build: invalid DSL node %S" dsl)) (t (let ((tag (car dsl)) (items (ebox--dsl-unquote-properties (cdr dsl)))) (pcase tag + ('text (ebox--build-text items)) ('box (ebox--build-canonical-box items)) ('ebox (ebox--build-box items)) ('row (ebox--build-layout tag #'ebox-row items)) diff --git a/ebox-node-factory.el b/ebox-node-factory.el new file mode 100644 index 0000000..36b3dee --- /dev/null +++ b/ebox-node-factory.el @@ -0,0 +1,62 @@ +;;; ebox-node-factory.el --- Runtime box node factory -*- lexical-binding: t; -*- + +;;; Commentary: +;; Owns the legacy runtime box encoding used by render/layout modules during +;; the typed canonical migration. Higher programmatic constructors may lower +;; through this factory, but author DSL parsing and buffer publication do not +;; belong here. + +;;; Code: + +(require 'ebox-style) + +(defconst ebox--longhand + '(:content "" + :box-sizing border-box + :width nil :min-width 0 :max-width nil + :height nil :min-height 0 :max-height nil + :padding-left-pixel 0 :padding-right-pixel 0 + :padding-top-height 0 :padding-bottom-height 0 + :margin-left-pixel 0 :margin-right-pixel 0 + :margin-top-height 0 :margin-bottom-height 0 + :border-left-pixel 0 :border-left-color nil + :border-right-pixel 0 :border-right-color nil + :border-top-p nil :border-top-color nil + :border-bottom-p nil :border-bottom-color nil + :color nil :bgcolor nil + :font nil :font-family nil :font-height nil + :font-weight nil :font-slant nil + :text-align left :vertical-align top + :overflow scroll :visibility visible + :wrap-mode word :scroll-offset 0) + "Default longhand fields for the current runtime box encoding.") + +(defun ebox--longhand-create (&optional plist) + "Return a runtime box field plist overridden by PLIST." + (let ((box (copy-sequence ebox--longhand))) + (while plist + (let ((key (pop plist)) + (value (pop plist))) + (plist-put box key value))) + box)) + +;;;###autoload +(defun ebox-create (&rest plist) + "Create one legacy runtime box from PLIST. + +New framework integrations should use `ebox-text-create' and +`ebox-box-create'. This factory remains public during source-tree migration." + (let ((declarations (ebox-style-compile-declarations plist)) + (expanded (ebox-style-expand-ebox-plist plist)) + (controlled-scroll-offset-p (plist-member plist :scroll-offset))) + (let ((box (ebox--longhand-create expanded))) + (plist-put box :ebox-type 'box) + (plist-put box :display '(block flow)) + (plist-put box :ebox-style-declarations declarations) + (when controlled-scroll-offset-p + (plist-put box :ebox-scroll-offset-controlled-p t)) + box))) + +(provide 'ebox-node-factory) + +;;; ebox-node-factory.el ends here diff --git a/ebox-style.el b/ebox-style.el index 6e57e8d..9ef0054 100644 --- a/ebox-style.el +++ b/ebox-style.el @@ -114,14 +114,14 @@ :group geometry :dirty-kind geometry :signature layout :validator nonnegative-size) (:name :border-top-width :id ebox/border-top-width - :aliases (:border-top-p) :initial nil :contexts (box) + :initial nil :contexts (box) :group geometry :dirty-kind geometry :signature layout) (:name :border-right-width :id ebox/border-right-width :aliases (:border-right-pixel) :initial nil :contexts (box) :group geometry :dirty-kind geometry :signature layout :validator nonnegative-size) (:name :border-bottom-width :id ebox/border-bottom-width - :aliases (:border-bottom-p) :initial nil :contexts (box) + :initial nil :contexts (box) :group geometry :dirty-kind geometry :signature layout) (:name :border-left-width :id ebox/border-left-width :aliases (:border-left-pixel) :initial nil :contexts (box) @@ -231,6 +231,14 @@ (: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 :border-top-p :id ebox/border-top-p + :shorthand border-top-present :validator boolean + :exclusive-outputs t + :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 + :group structure :dirty-kind structure :signature layout) (:name :border-width :id ebox/border-width :shorthand border-width :group geometry :dirty-kind geometry :signature layout) (:name :border-style :id ebox/border-style :shorthand border-style @@ -263,15 +271,25 @@ 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) - (puthash (plist-get property :name) property table) - (puthash (plist-get property :id) property table) + (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)) - (puthash alias property table))) + (ebox-style--register-property-name table alias property "alias"))) (setq ebox-style--property-table table)))) (defun ebox-style-property (name) @@ -457,6 +475,22 @@ every `ebox-create' needlessly copies the entire property metadata domain.") "Expand left border VALUE to namespaced longhands." (ebox-style--border-side-declarations 'left value)) +(defun ebox-style--border-present-declarations (side value) + "Return canonical boolean border declarations for SIDE and VALUE." + (unless (memq value '(nil t)) + (error "Ebox border-%s-p must be boolean: %S" side value)) + (list (intern (format "ebox/border-%s-width" side)) (if value 1 0) + (intern (format "ebox/border-%s-style" side)) + (if value 'solid 'none))) + +(defun ebox-style--border-top-present-shorthand (value) + "Expand boolean top-border VALUE to canonical width and style." + (ebox-style--border-present-declarations 'top value)) + +(defun ebox-style--border-bottom-present-shorthand (value) + "Expand boolean bottom-border VALUE to canonical width and style." + (ebox-style--border-present-declarations 'bottom value)) + (defun ebox-style--border-component-shorthand (component value) "Expand border COMPONENT VALUE across all physical sides." (ebox-style--trbl-declarations @@ -593,6 +627,7 @@ every `ebox-create' needlessly copies the entire property metadata domain.") ('font-family #'ebox-style--font-family-p) ('font-height #'ebox-style--font-height-p) ('font-attribute #'ebox-style--font-attribute-p) + ('boolean (lambda (value) (memq value '(nil t)))) (_ (lambda (_value) t)))) (defun ebox-style--shorthand (name) @@ -610,6 +645,8 @@ every `ebox-create' needlessly copies the entire property metadata domain.") (border-right . ebox-style--border-right-shorthand) (border-bottom . ebox-style--border-bottom-shorthand) (border-left . ebox-style--border-left-shorthand) + (border-top-present . ebox-style--border-top-present-shorthand) + (border-bottom-present . ebox-style--border-bottom-present-shorthand) (border-width . ebox-style--border-width-shorthand) (border-style . ebox-style--border-style-shorthand) (border-color . ebox-style--border-color-shorthand) @@ -694,6 +731,58 @@ cascade pass during its initial static projection." "Return non-nil when PLIST is a proper even-length list." (and (proper-list-p plist) (zerop (% (length plist) 2)))) +(defun ebox-style--validate-no-duplicate-properties (plist) + "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 id = (and property (plist-get property :id)) + when id + do (when-let* ((previous (gethash id seen))) + (error + "Duplicate Ebox property %S via %S and %S" + (plist-get property :name) previous name)) + and do (puthash id name seen))) + plist) + +(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))) + (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 + collect id) + (list (plist-get property :id))))) + +(defun ebox-style--validate-exclusive-properties (plist) + "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) + when property + do (push (list :name name :value value :property property) + entries)) + (when (cl-some (lambda (entry) + (plist-get (plist-get entry :property) + :exclusive-outputs)) + entries) + (dolist (entry entries) + (when (plist-get (plist-get entry :property) :exclusive-outputs) + (let ((outputs + (ebox-style--author-property-output-ids + (plist-get entry :name) (plist-get entry :value)))) + (dolist (other entries) + (unless (eq entry other) + (when (cl-intersection + outputs + (ebox-style--author-property-output-ids + (plist-get other :name) (plist-get other :value)) + :test #'eq) + (error "Ebox property %S cannot be combined with %S" + (plist-get entry :name) + (plist-get other :name)))))))))) + plist) + (defun ebox-style--declaration-cache-key (plist strict) "Return a stable style-only cache key for PLIST and STRICT. Non-style node properties such as content, Host references, and callbacks do @@ -706,7 +795,12 @@ not affect declaration compilation and are intentionally excluded." (append canonical (list schema value))) else if (ebox-style--custom-property-p property) do (setq canonical - (append canonical (list property value)))) + (append canonical (list property value))) + else if strict + do (setq canonical + (append canonical + (list 'ebox-style/unknown-property + property value)))) (list strict canonical))) (defun ebox-style--static-invalid-declaration-p (validator value) @@ -738,6 +832,8 @@ 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) + (ebox-style--validate-exclusive-properties plist) (let* ((cache-key (ebox-style--declaration-cache-key plist strict)) (missing (make-symbol "ebox-style-declaration-cache-missing")) (cached (gethash cache-key ebox-style--declaration-cache missing))) diff --git a/ebox.el b/ebox.el index c84af7e..c3c0eb2 100644 --- a/ebox.el +++ b/ebox.el @@ -23,9 +23,11 @@ "Directory containing the active Ebox Lisp sources.") (defconst ebox--compile-sources - '("ebox-cache.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el" + '("ebox-cache.el" "ebox-style.el" "ebox-node-factory.el" + "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" - "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el" + "ebox-flex.el" "ebox-grid.el" "ebox-canonical.el" + "ebox-buffer-backend.el" "ebox-incremental.el" "ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el" "ebox-native-reflow.el") @@ -39,6 +41,7 @@ (require 'subr-x) (require 'ebox-cache) (require 'ebox-style) +(require 'ebox-node-factory) (require 'ebox-child-range) (require 'ebox-tree) (require 'ebox-measure) @@ -47,6 +50,7 @@ (require 'ebox-layout) (require 'ebox-flex) (require 'ebox-grid) +(require 'ebox-canonical) (require 'ebox-buffer-backend) (require 'ebox-incremental) (require 'ebox-surface) @@ -387,51 +391,6 @@ caller, such as the Playground preview, renders against a concrete window.") "Generate next unique region ID." (cl-incf ebox--region-id-counter)) -(defconst ebox--longhand - '( :content "" - :box-sizing border-box ;; content-box or border-box - - ;; 所有宽度的单位是像素,所有高度的单位是行数 - :width nil :min-width 0 :max-width nil - :height nil :min-height 0 :max-height nil - - :padding-left-pixel 0 :padding-right-pixel 0 - :padding-top-height 0 :padding-bottom-height 0 - :margin-left-pixel 0 :margin-right-pixel 0 - :margin-top-height 0 :margin-bottom-height 0 - - ;; 左右边框支持设置宽度 - :border-left-pixel 0 :border-left-color nil - :border-right-pixel 0 :border-right-color nil - ;; 上下边框由于 Emacs 限制,用 overline/underline 实现,不支持设置宽度 - :border-top-p nil :border-top-color nil - :border-bottom-p nil :border-bottom-color nil - :color nil - :bgcolor nil - :font nil :font-family nil :font-height nil - :font-weight nil :font-slant nil - :text-align left :vertical-align top - ;; supports: 'visible / 'hidden / 'scroll - :overflow scroll - ;; supports: 'visible / 'hidden — hidden suppresses ink while the - ;; layout footprint, buffer text, and metadata stay identical - :visibility visible - ;; supports: 'word / 'char / 'kp - :wrap-mode word - :scroll-offset 0 ;; 当前滚动偏移(行数) - ) - "Default plist for a box structure (longhand properties only).") - -(defun ebox--longhand-create (&optional plist) - "Create a shorthand box structure. -PLIST is a property list that overrides the defaults." - (let ((box (copy-sequence ebox--longhand))) - (while plist - (let ((key (pop plist)) - (value (pop plist))) - (plist-put box key value))) - box)) - (defsubst ebox-get (box property) "Retrieve PROPERTY from BOX." (plist-get box property)) @@ -869,21 +828,6 @@ Supports: t (default fg), nil, string, (light . dark) cons." ;;; --- 核心 API --- -;;;###autoload -(defun ebox-create (&rest plist) - "Create a box from PLIST. Supports all shorthand and longhand properties. -Properties are expanded recursively according to `ebox-property-rules'." - (let ((declarations (ebox-style-compile-declarations plist)) - (expanded (ebox--expand-plist plist)) - (controlled-scroll-offset-p (plist-member plist :scroll-offset))) - (let ((box (ebox--longhand-create expanded))) - (plist-put box :ebox-type 'box) - (plist-put box :display '(block flow)) - (plist-put box :ebox-style-declarations declarations) - (when controlled-scroll-offset-p - (plist-put box :ebox-scroll-offset-controlled-p t)) - box))) - ;;; ============================================================ ;;; Buffer Runtime State & Scroll State ;;; ============================================================ @@ -4643,6 +4587,9 @@ through dirty-set and patch-set execution before falling back to root rerender." ebox-candidate-patch-host-paint ebox-clear-cache ebox-column + ebox-normal-layout-create + ebox-text-create + ebox-box-create ebox-commit ebox-concat ebox-create @@ -4724,8 +4671,16 @@ and are documented separately in the public API reference.") ;; All other ebox-- prefixed functions are internal implementation details. ;; ;; ── Creation ────────────────────────────────────────────────── +;; ebox-text-create :value string &rest canonical-measurement-props +;; Create one typed canonical TextNode. +;; ebox-normal-layout-create +;; Create the typed Normal LayoutConfig. +;; ebox-box-create :layout config :children nodes &rest geometry-props +;; Create one typed canonical BoxNode. The current vertical slice +;; accepts Normal layout with zero or one canonical child. ;; ebox-create &rest plist -;; Create a box. Key options: :content :width :height +;; Legacy untyped runtime constructor retained during migration. +;; Key options: :content :width :height ;; :padding :margin :border :border-color :bgcolor ;; :text-align :wrap-mode :box-sizing :surface-properties :host-ref ;; ebox-build dsl diff --git a/tests/ebox-core-render-tests.el b/tests/ebox-core-render-tests.el index d29f0ce..95dfb40 100644 --- a/tests/ebox-core-render-tests.el +++ b/tests/ebox-core-render-tests.el @@ -5254,13 +5254,13 @@ :padding (0 (8)) :border "#D97757") (box :content "Body" :width (120) :padding (0 (8)) :border "#D97757" - :border-top-p nil)) + :border-top none)) (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-p nil))))) + :border-top none))))) (left-id (plist-get (car (ebox-selector-query-all layout "#left")) :region-id))) @@ -5291,17 +5291,17 @@ (column (box :id left :content "Left" :width (120) :padding (0 (8)) :border "#D97757" - :border-bottom-p nil) + :border-bottom none) (box :content "Body" :width (120) :padding (0 (8)) :border "#D97757" - :border-top-p nil)) + :border-top none)) (column (box :content "Right" :width (120) :padding (0 (8)) :border "#5E7F6A" - :border-bottom-p nil) + :border-bottom none) (box :content "Body" :width (120) :padding (0 (8)) :border "#5E7F6A" - :border-top-p nil))))) + :border-top none))))) (left-id (plist-get (car (ebox-selector-query-all layout "#left")) :region-id))) @@ -5335,17 +5335,17 @@ (column (box :id left :content "Left" :width (120) :padding (0 (8)) :border "#D97757" - :border-bottom-p nil) + :border-bottom none) (box :content "Body" :width (120) :padding (0 (8)) :border "#D97757" - :border-top-p nil)) + :border-top none)) (column (box :content "Right" :width (120) :padding (0 (8)) :border "#5E7F6A" - :border-bottom-p nil) + :border-bottom none) (box :content "Body" :width (120) :padding (0 (8)) :border "#5E7F6A" - :border-top-p nil))))) + :border-top none))))) (left-id (plist-get (car (ebox-selector-query-all layout "#left")) :region-id))) @@ -8504,7 +8504,7 @@ `(flex :width (240) :flex-flow (row wrap) :row-gap 1 :column-gap 0 :padding (0 (8)) :border "#D97757" - :border-top-p nil :bgcolor "#FFF8F2" + :border-top none :bgcolor "#FFF8F2" ,@(cl-loop for idx below 6 collect `(box :id ,(format "prefix-item-%02d" idx) @@ -8610,7 +8610,7 @@ :flex-flow (row wrap) :row-gap 1 :column-gap 0 :padding (0 (8)) :border "#D97757" - :border-top-p nil :bgcolor "#FFF8F2" + :border-top none :bgcolor "#FFF8F2" ,@(cl-loop for idx below 8 collect `(box :content ,(format "wrapper %02d" idx) @@ -8659,7 +8659,7 @@ :flex-flow (row wrap) :row-gap 1 :column-gap 0 :padding (0 (8)) :border "#D97757" - :border-top-p nil :bgcolor "#FFF8F2" + :border-top none :bgcolor "#FFF8F2" ,@(cl-loop for idx below 4 collect `(box :content ,(format "complete %02d" idx) @@ -8788,7 +8788,7 @@ :flex-flow (row wrap) :row-gap 1 :column-gap 0 :padding (0 (8)) :border "#D97757" - :border-top-p nil :bgcolor "#FFF8F2" + :border-top none :bgcolor "#FFF8F2" ,@(cl-loop for idx below 8 collect `(box :content ,(format "extend %02d" idx) @@ -8821,7 +8821,7 @@ `(flex :width (240) :flex-flow (row wrap) :row-gap 1 :column-gap 0 :padding (0 (8)) :border "#D97757" - :border-top-p nil :bgcolor "#FFF8F2" + :border-top none :bgcolor "#FFF8F2" ,@(cl-loop for idx below 16 collect `(box :content ,(format "piece %02d" idx) @@ -8852,7 +8852,7 @@ :padding (0 (8) 2 (8)) :margin-bottom 2 :border "#D97757" - :border-top-p nil + :border-top none :bgcolor "#FFF8F2" ,@(cl-loop for idx below 2 collect @@ -8897,7 +8897,7 @@ :row-gap 1 :column-gap 0 :padding (0 (8)) :border "#D97757" - :border-top-p nil + :border-top none :bgcolor "#FFF8F2" ,@(cl-loop for idx below 4 collect @@ -8934,7 +8934,7 @@ :row-gap 1 :column-gap 0 :padding (0 (8)) :border "#D97757" - :border-top-p nil + :border-top none ,@(cl-loop for idx below 4 collect `(box :content ,(format "atomic %02d" idx) @@ -11066,7 +11066,7 @@ :flex-flow (row wrap) :row-gap 1 :column-gap (8) :padding (0 (12)) :border "#D97757" - :border-top-p nil + :border-top none ,@(cl-loop for idx below 24 collect `(box :content ,(format "signature %02d" idx) diff --git a/tests/ebox-dsl-tests.el b/tests/ebox-dsl-tests.el index 6b88b66..64eb2e3 100644 --- a/tests/ebox-dsl-tests.el +++ b/tests/ebox-dsl-tests.el @@ -52,6 +52,164 @@ (ebox-style-apply-computed node style) (should (equal (ebox--computed-display node) '(inline flex))))) +(ert-deftest ebox-style-strict-cache-does-not-hide-unknown-properties () + "Strict declaration validation should remain exact after an empty cache hit." + (ebox-style-compile-declarations nil t) + (should-error + (ebox-style-compile-declarations '(:not-an-ebox-property 1) t) + :type 'error)) + +(ert-deftest ebox-style-aliases-share-one-canonical-property-id () + "A concise alias should disappear before canonical declarations." + (should + (equal (ebox-style-compile-declarations '(:bgcolor "red") t) + (ebox-style-compile-declarations '(:background-color "red") t)))) + +(ert-deftest ebox-style-rejects-canonical-and-alias-duplicates () + "One declaration source should not spell one property twice." + (should-error + (ebox-style-compile-declarations + '(:bgcolor "red" :background-color "blue") t) + :type 'error) + (should-error + (ebox-style-compile-declarations + '(:background-color "red" :bgcolor "blue") t) + :type 'error)) + +(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))) + +(ert-deftest ebox-style-border-present-is-a-typed-shorthand-not-an-alias () + "The concise border boolean should expand to canonical width and style." + (should + (equal (ebox-style-compile-declarations '(:border-top-p t) t) + '(ebox/border-top-width 1 ebox/border-top-style solid))) + (should + (equal (ebox-style-compile-declarations '(:border-top-p nil) t) + '(ebox/border-top-width 0 ebox/border-top-style none))) + (should (eq (ebox-style-canonical-name :border-top-p) :border-top-p))) + +(ert-deftest ebox-style-exclusive-check-does-not-reexpand-ordinary-shorthands () + "Conflict validation should leave the declaration cache effective." + (let ((calls 0) + (original (symbol-function 'ebox-style--padding-shorthand))) + (clrhash ebox-style--declaration-cache) + (cl-letf (((symbol-function 'ebox-style--padding-shorthand) + (lambda (value) + (cl-incf calls) + (funcall original value)))) + (ebox-style-compile-declarations '(:padding 1) t) + (ebox-style-compile-declarations '(:padding 1) t)) + (should (= calls 1)))) + +(ert-deftest ebox-style-border-present-rejects-owned-longhand-conflicts () + "One source should not mix the boolean border shorthand with its outputs." + (should-error + (ebox-style-compile-declarations + '(:border-top-p t :border-top-width 2) t) + :type 'error) + (should-error + (ebox-style-compile-declarations + '(:border-top-style dashed :border-top-p t) t) + :type 'error) + (should-error + (ebox-style-compile-declarations + '(:border-top-p t :border-top (2 dashed "red")) t) + :type 'error) + (should-error + (ebox-style-compile-declarations + '(:border-top-p t :border-width 2) t) + :type 'error) + (should-error + (ebox-style-compile-declarations + '(:border-style dashed :border-top-p t) t) + :type 'error) + (should-error + (ebox-style-compile-declarations + '(:border-bottom-p t :border (1 solid "red")) t) + :type 'error)) + +(ert-deftest ebox-canonical-text-constructor-is-renderable-and-typed () + "A canonical TextNode should retain its kind, source, and string value." + (let ((node (ebox-text-create :value "Hello" :source-handle 'source-text))) + (should (ebox-text-node-p node)) + (should-not (ebox-box-node-p node)) + (should (eq (ebox-node-source-handle node) 'source-text)) + (should (string= (ebox-text-node-value node) "Hello")) + (should (string= (ebox-dsl-test--plain node) "Hello")))) + +(ert-deftest ebox-canonical-text-constructor-rejects-non-text-shapes () + "A canonical TextNode should have exactly one string payload." + (should-error (ebox-text-create :value 7) :type 'error) + (should-error (ebox-text-create :value "A" :content "B") :type 'error) + (should-error (ebox-text-create :value "A" :children nil) :type 'error) + (should-error (ebox-text-create :value "A" :color "red") :type 'error) + (should-error + (ebox-text-create :value "A" :value "B") + :type 'error) + (should-error + (ebox-text-create :value "A" :source-handle 'a :source-handle 'b) + :type 'error)) + +(ert-deftest ebox-canonical-normal-box-renders-one-typed-text-child () + "A canonical BoxNode should retain typed Layout and child source facts." + (let* ((text (ebox-text-create :value "A" :source-handle 'source-text)) + (layout (ebox-normal-layout-create)) + (box (ebox-box-create :layout layout + :children (list text) + :source-handle 'source-box))) + (should (ebox-box-node-p box)) + (should-not (ebox-text-node-p box)) + (should (eq (ebox-node-source-handle box) 'source-box)) + (should (eq (ebox-layout-config-kind + (ebox-box-node-layout box)) + 'normal)) + (should (equal (ebox-box-node-children box) (list text))) + (should (string= (ebox-dsl-test--plain box) "A")))) + +(ert-deftest ebox-canonical-box-rejects-paint-only-properties () + "Canonical Box geometry should not duplicate paint-only state." + (should-error + (ebox-box-create :layout (ebox-normal-layout-create) + :bgcolor "red") + :type 'error)) + +(ert-deftest ebox-canonical-box-requires-unique-reserved-fields () + "Typed Box reserved fields should never be missing or duplicated." + (let ((normal (ebox-normal-layout-create))) + (should-error (ebox-box-create :children nil) :type 'error) + (should-error + (ebox-box-create :layout normal :layout normal :children nil) + :type 'error) + (should-error + (ebox-box-create :layout normal :children nil :children nil) + :type 'error) + (should-error + (ebox-box-create :layout normal :outer nil :children nil) + :type 'error))) + +(ert-deftest ebox-build-normalizes-text-author-forms () + "The Ebox DSL should normalize explicit and bare Text forms." + (let ((text (ebox-build '(text "A"))) + (bare (ebox-build "B"))) + (should (ebox-text-node-p text)) + (should (ebox-text-node-p bare)) + (should (string= (ebox-dsl-test--plain text) "A")) + (should (string= (ebox-dsl-test--plain bare) "B")))) + +(ert-deftest ebox-build-text-requires-one-string-payload () + "The Ebox Text form should reject empty, multiple, and nested payloads." + (should-error (ebox-build '(text)) :type 'error) + (should-error (ebox-build '(text "A" "B")) :type 'error) + (should-error (ebox-build '(text (text "A"))) :type 'error) + (should-error (ebox-build '(text :color "red" "A")) :type 'error) + (should-error (ebox-build '(text :wrap-mode garbage "A")) :type 'error)) + (ert-deftest ebox-dsl-module-preserves-public-build-syntax () "Extracted DSL compiler should preserve existing ebox-build syntax." (let ((node (ebox-build diff --git a/tests/ebox-flex-tests.el b/tests/ebox-flex-tests.el index 9884a05..1c48e04 100644 --- a/tests/ebox-flex-tests.el +++ b/tests/ebox-flex-tests.el @@ -416,7 +416,7 @@ :padding (0 (10)) :border "#5E7F6A" :text-align center) (flex :id "body" :max-width (293) :padding (0 (8)) - :border "#5E7F6A" :border-top-p nil + :border "#5E7F6A" :border-top none :flex-wrap wrap :column-gap (8) (item :flex-shrink 0 (box :content "A 118" :width (118))) @@ -439,14 +439,14 @@ (layout (ebox-build '(flex :flex-flow (row wrap) :row-gap 1 :column-gap (12) - :padding (0 (12)) :border "#6F6A95" :border-top-p nil + :padding (0 (12)) :border "#6F6A95" :border-top none :bgcolor "#FBF9FF" (column (box :content ":flex-flow (row wrap)" :max-width (440) :padding (0 (10)) :border "#6F6A95" :text-align center) (flex :max-width (440) :padding (0 (8)) - :border "#6F6A95" :border-top-p nil + :border "#6F6A95" :border-top none :flex-flow (row wrap) :gap (1 (12)) (item (box :content "A" :width (170))) (item (box :content "B" :width (170))) @@ -456,7 +456,7 @@ :padding (0 (10)) :border "#6F6A95" :text-align center) (flex :max-width (440) :padding (0 (8)) - :border "#6F6A95" :border-top-p nil + :border "#6F6A95" :border-top none :flex-flow (column wrap) :gap (1 (12)) (item (box :content "A" :width (140))) (item (box :content "B" :width (140))) diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index 430fb8d..8d48c46 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -11,10 +11,12 @@ (defconst ebox-test--autoload-source-files '("ebox.el" + "ebox-node-factory.el" "ebox-measure.el" "ebox-layout.el" "ebox-flex.el" "ebox-grid.el" + "ebox-canonical.el" "ebox-buffer-backend.el" "ebox-selector.el" "ebox-native-reflow.el") @@ -49,12 +51,34 @@ "The public ebox facade should load all internal foundation modules." (require 'ebox) (dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure + ebox-node-factory ebox-fragment ebox-layout ebox-flex ebox-grid + ebox-canonical ebox-buffer-backend ebox-incremental ebox-surface ebox-viewport ebox-dsl ebox-selector)) (should (featurep feature)))) +(ert-deftest ebox-canonical-module-loads-typed-port-without-facade () + "The canonical autoload boundary should not depend on loading `ebox.el'." + (let* ((emacs (expand-file-name invocation-name invocation-directory)) + (parent (file-name-directory (directory-file-name ebox-test--root))) + (ecss (expand-file-name "ecss" parent)) + (tp (expand-file-name "tp" parent))) + (with-temp-buffer + (should + (zerop + (process-file + emacs nil (current-buffer) nil + "-Q" "--batch" + "-L" ebox-test--root "-L" ecss "-L" tp + "--eval" "(setq load-prefer-newer t)" + "--eval" + (concat + "(progn (require 'ebox-canonical) " + "(unless (ebox-text-node-p (ebox-text-create :value \"A\")) " + " (error \"Typed Text autoload failed\")))"))))))) + (ert-deftest ebox-facade-exposes-native-workflow-without-loading-it () "The two native commands should be available without native load work." (require 'ebox) @@ -67,10 +91,12 @@ (should (commandp 'ebox-byte-compile)) (should (equal ebox--compile-sources - '("ebox-cache.el" "ebox-style.el" "ebox-child-range.el" + '("ebox-cache.el" "ebox-style.el" "ebox-node-factory.el" + "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" - "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el" + "ebox-flex.el" "ebox-grid.el" "ebox-canonical.el" + "ebox-buffer-backend.el" "ebox-incremental.el" "ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el" "ebox-native-reflow.el")))) @@ -143,6 +169,9 @@ ("ebox-grid-fr" . "defun") ("ebox-grid-item" . "defun") ("ebox-grid" . "defun") + ("ebox-normal-layout-create" . "defun") + ("ebox-text-create" . "defun") + ("ebox-box-create" . "defun") ("ebox-build" . "defun") ("ebox-create" . "defun") ("ebox-scroll-down" . "defun") @@ -178,6 +207,9 @@ ebox-candidate-replace-host-ref ebox-clear-cache ebox-column + ebox-normal-layout-create + ebox-text-create + ebox-box-create ebox-commit ebox-concat ebox-create