From 9f2cdfb29907f38c41fddbbb0e01692f4a35b46a Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Wed, 26 Aug 2026 16:10:40 +0800 Subject: [PATCH] feat: add canonical Box layout axes --- ebox-dsl.el | 65 +++++++++++++++++++++++++++++++++++++---- tests/ebox-dsl-tests.el | 45 ++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/ebox-dsl.el b/ebox-dsl.el index 39f3be9..72fd60d 100644 --- a/ebox-dsl.el +++ b/ebox-dsl.el @@ -14,6 +14,12 @@ (declare-function ebox-create "ebox" (&rest plist)) +(defconst ebox-dsl--outer-values '(inline block) + "Valid outer participation values for the canonical Box DSL.") + +(defconst ebox-dsl--layout-values '(normal row column flex grid) + "Valid child layout values for the canonical Box DSL.") + (defun ebox--dsl-unquote-value (value) "Unquote one static DSL property VALUE when it is quoted data." (if (and (consp value) @@ -38,6 +44,22 @@ keyword properties are normalized." (push item result)))) (nreverse result))) +(defun ebox-dsl--axis-value (props property default allowed) + "Return PROPS PROPERTY or DEFAULT after validating it against ALLOWED." + (let ((value (if (plist-member props property) + (plist-get props property) + default))) + (unless (memq value allowed) + (error "ebox-build: invalid %S value %S" property value)) + value)) + +(defun ebox-dsl--with-display (node outer layout) + "Return a shallow copy of NODE with canonical OUTER and LAYOUT display." + (let ((copy (copy-sequence node))) + (plist-put copy :display + (list outer (if (eq layout 'normal) 'flow layout))) + copy)) + (defun ebox--build-children-layout (children) "Build a vertical default layout from CHILDREN." (setq children (delq nil children)) @@ -134,6 +156,13 @@ The first value is string content. The second value is a lazy child node." (ebox-dsl-build (car children)) props))) +(defun ebox--build-grid (items) + "Build a grid node from DSL ITEMS." + (let* ((split (ebox-grid--split-attrs items)) + (props (car split)) + (children (mapcar #'ebox-dsl-build (delq nil (cdr split))))) + (apply #'ebox-grid (append props children)))) + (defun ebox--build-spacer (items) "Build a spacer node from DSL ITEMS." (let* ((split (ebox--build-split-attrs items)) @@ -145,6 +174,33 @@ The first value is string content. The second value is a lazy child node." (error "ebox-build: spacer cannot set :content")) (apply #'ebox-spacer props))) +(defun ebox--build-canonical-box (items) + "Build canonical Box ITEMS with orthogonal outer and layout axes." + (let* ((split (ebox--build-split-attrs items)) + (props (car split)) + (children (cdr split)) + (outer (ebox-dsl--axis-value + props :outer 'block ebox-dsl--outer-values)) + (layout (ebox-dsl--axis-value + props :layout 'normal ebox-dsl--layout-values)) + (box-props (ebox--plist-remove-keys props '(:outer :layout))) + node) + (setq node + (pcase layout + ('normal + (ebox--build-box (append box-props children))) + ('row + (ebox--build-layout + 'box #'ebox-row (append box-props children))) + ('column + (ebox--build-layout + 'box #'ebox-column (append box-props children))) + ('flex + (ebox--build-flex (append box-props children))) + ('grid + (ebox--build-grid (append box-props children))))) + (ebox-dsl--with-display node outer layout))) + (defun ebox-dsl-build (dsl) "Build an Ebox node from an ETML-style list DSL. @@ -171,18 +227,15 @@ the child layout in a box using the rendered child layout as content." (let ((tag (car dsl)) (items (ebox--dsl-unquote-properties (cdr dsl)))) (pcase tag - ((or 'box 'ebox) (ebox--build-box items)) + ('box (ebox--build-canonical-box items)) + ('ebox (ebox--build-box items)) ('row (ebox--build-layout tag #'ebox-row items)) ('column (ebox--build-layout tag #'ebox-column items)) ('flex (ebox--build-flex items)) ('item (ebox--build-flex-item items)) ('grid-item (ebox--build-grid-item items)) ('spacer (ebox--build-spacer items)) - ('grid - (let* ((split (ebox-grid--split-attrs items)) - (props (car split)) - (children (mapcar #'ebox-dsl-build (delq nil (cdr split))))) - (apply #'ebox-grid (append props children)))) + ('grid (ebox--build-grid items)) (_ (error "ebox-build: unknown DSL tag %S" tag))))))) (provide 'ebox-dsl) diff --git a/tests/ebox-dsl-tests.el b/tests/ebox-dsl-tests.el index d10edd4..9738cc9 100644 --- a/tests/ebox-dsl-tests.el +++ b/tests/ebox-dsl-tests.el @@ -358,6 +358,51 @@ (should (equal (ebox--computed-display column-node) '(block column))) (should (equal (ebox--computed-display flex-node) '(block flex))))) +(ert-deftest ebox-build-box-uses-orthogonal-outer-and-layout-axes () + "Canonical Box DSL should select outer participation and child layout." + (let ((normal (ebox-build '(box :outer inline :layout normal "A"))) + (row (ebox-build + '(box :outer inline :layout row + (box "A") + (box "B")))) + (column (ebox-build + '(box :outer block :layout column + (box "A") + (box "B"))))) + (should (equal (ebox--computed-display normal) '(inline flow))) + (should (equal (ebox--computed-display row) '(inline row))) + (should (equal (ebox--computed-display column) '(block column))) + (should (string= (ebox-dsl-test--plain row) "AB")) + (should (string= (ebox-dsl-test--plain column) "A\nB")))) + +(ert-deftest ebox-build-box-rejects-invalid-axis-values () + "Canonical Box DSL should reject unknown outer and layout values." + (should-error (ebox-build '(box :outer floating "A")) :type 'error) + (should-error (ebox-build '(box :layout masonry "A")) :type 'error)) + +(ert-deftest ebox-build-box-layout-consumes-direct-flex-item-properties () + "Canonical Flex Box should consume participation props from child Boxes." + (let* ((node (ebox-build + '(box :layout flex :width (300) + (box :flex-grow 1 :flex-basis (80) "A") + (box :flex-grow 2 :flex-basis (80) "B")))) + (children (plist-get node :children))) + (should (equal (ebox--computed-display node) '(block flex))) + (should (= (plist-get (car children) :flex-grow) 1)) + (should (= (plist-get (cadr children) :flex-grow) 2)) + (should (string-match-p "A" (ebox-dsl-test--plain node))) + (should (string-match-p "B" (ebox-dsl-test--plain node))))) + +(ert-deftest ebox-build-box-layout-consumes-direct-grid-item-properties () + "Canonical Grid Box should consume placement props from child Boxes." + (let* ((node (ebox-build + '(box :layout grid :grid-template-columns ((20) (20)) + (box :grid-column (1 :span 2) "Header")))) + (child (car (plist-get node :children)))) + (should (equal (ebox--computed-display node) '(block grid))) + (should (equal (plist-get child :grid-column) '(1 :span 2))) + (should (string-match-p "Header" (ebox-dsl-test--plain node))))) + (ert-deftest ebox-build-wrapper-child-layout-rerenders-on-runtime-viewport-change () "A DSL wrapper box should rerender child layout content on viewport resize." (let ((node (let ((ebox-viewport-width 96))