ebox/ebox-style.el
Kinneyzhang 4a25d573c2
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
Add retained layer layout: position/left/top/z-index/layer/anchor properties, new ebox-layer.el and ebox-composite.el, update docs and Makefile
2026-09-10 01:58:23 +08:00

2616 lines
119 KiB
EmacsLisp

;;; ebox-style.el --- CSS-like style model for Ebox -*- lexical-binding: t; -*-
;;; Commentary:
;; Owns style property registration, shorthand expansion, computed style, and
;; dirty-effect classification. It does not own Emacs buffer text properties.
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(require 'ecss)
(require 'tp-style)
(require 'ebox-source)
(require 'ebox-layout-config)
(require 'ebox-size)
(defvar ebox--longhand)
(declare-function ebox-selector-parse "ebox-selector" (selector))
(declare-function ebox-node-factory--create-from-properties
"ebox-node-factory" (&rest plist))
(declare-function ebox--flex-normalize-container-props
"ebox-flex" (props))
(declare-function ebox-grid--normalize-config-props
"ebox-grid" (props))
(declare-function ecss-stylesheet-create "ecss" ())
(defun ebox-style--create-stylesheet ()
"Return one private ECSS stylesheet for Ebox style state."
(ecss-stylesheet-create))
(defconst ebox-style--border-styles
'(none solid)
"Valid CSS border-style keywords accepted by Ebox style parsing.")
(defconst ebox-style--default-foreground 'ebox/default-foreground
"Internal paint consequence that blocks an inherited foreground.")
(defconst ebox-style--property-definitions
'((:name :color :id ebox/color :initial nil :inherits t
:contexts (text box) :group paint :dirty-kind paint :signature paint
:normalizer color :validator color
:engine-projection (:to :color :conv color))
(:name :background-color :id ebox/background-color :aliases (:bgcolor)
:initial nil :contexts (text box) :group paint :dirty-kind paint
:signature paint :normalizer color :validator color
:engine-projection (:to :bgcolor :conv color))
(:name :font-family :id ebox/font-family :initial nil :inherits t
:contexts (text box) :group typography :dirty-kind geometry :signature layout
:validator font-family)
(:name :font-size :id ebox/font-size
:initial nil :inherits t :contexts (text box) :group typography
:dirty-kind geometry :signature layout :validator font-size)
(:name :font-weight :id ebox/font-weight :initial normal :inherits t
:contexts (text box) :group typography :dirty-kind geometry :signature layout
:validator font-weight)
(:name :font-style :id ebox/font-style :aliases (:font-slant)
:initial normal :inherits t
:contexts (text box) :group typography :dirty-kind geometry :signature layout
:validator font-style)
(:name :text-decoration-line :id ebox/text-decoration-line :initial none
:contexts (text box) :group paint :dirty-kind paint :signature paint
:validator text-decoration-line)
(:name :text-decoration-color :id ebox/text-decoration-color
:initial currentColor :contexts (text box) :group paint :dirty-kind paint
:signature paint :normalizer color :validator color
:engine-projection (:to :text-decoration-color :conv color))
(:name :text-decoration-style :id ebox/text-decoration-style :initial solid
:contexts (text box) :group paint :dirty-kind paint :signature paint
:validator text-decoration-style)
(:name :box-sizing :id ebox/box-sizing :initial border-box
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout)
(:name :position :id ebox/position :initial static
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout :validator position
:runtime-default t)
(:name :left :id ebox/left :initial (px 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis inline :validator signed-size :runtime-default t)
(:name :top :id ebox/top :initial (lh 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis block :validator signed-size :runtime-default t)
(:name :z-index :id ebox/z-index :initial 0
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout :validator integer
:runtime-default t)
(:name :layer :id ebox/layer :initial local
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout :validator layer
:runtime-default t)
(:name :anchor :id ebox/anchor :initial nil
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout :validator anchor
:runtime-default t)
(:name :placement :id ebox/placement :initial bottom-start
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout :validator placement
:runtime-default t)
(:name :width :id ebox/width :initial auto
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis inline :validator horizontal-size
:engine-projection (:to :width :conv preferred-size))
(:name :min-width :id ebox/min-width :initial auto
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis inline :validator min-horizontal-size
:engine-projection (:to :min-width :conv min-size))
(:name :max-width :id ebox/max-width :initial none
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis inline :validator max-horizontal-size
:engine-projection (:to :max-width :conv max-size))
(:name :height :id ebox/height :initial auto
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis block :validator vertical-size)
(:name :min-height :id ebox/min-height :initial auto
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis block :validator min-vertical-size)
(:name :max-height :id ebox/max-height :initial none
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis block :validator max-vertical-size)
(:name :padding-top :id ebox/padding-top :initial (lh 0)
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size
:engine-projection (:to :padding-top-height :conv line))
(:name :padding-right :id ebox/padding-right :initial (px 0)
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis inline :validator nonnegative-size
:engine-projection (:to :padding-right-pixel :conv pixel))
(:name :padding-bottom :id ebox/padding-bottom :initial (lh 0)
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size
:engine-projection (:to :padding-bottom-height :conv line))
(:name :padding-left :id ebox/padding-left :initial (px 0)
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis inline :validator nonnegative-size
:engine-projection (:to :padding-left-pixel :conv pixel))
(:name :padding-block-start :id ebox/padding-block-start :initial (lh 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size
:engine-projection (:to :padding-top-height :conv line))
(:name :padding-inline-end :id ebox/padding-inline-end :initial (px 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis inline :validator nonnegative-size
:engine-projection (:to :padding-right-pixel :conv pixel))
(:name :padding-block-end :id ebox/padding-block-end :initial (lh 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size
:engine-projection (:to :padding-bottom-height :conv line))
(:name :padding-inline-start :id ebox/padding-inline-start :initial (px 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis inline :validator nonnegative-size
:engine-projection (:to :padding-left-pixel :conv pixel))
(:name :margin-top :id ebox/margin-top :initial (lh 0)
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size
:engine-projection (:to :margin-top-height :conv line))
(:name :margin-right :id ebox/margin-right :initial (px 0)
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis inline :validator nonnegative-size
:engine-projection (:to :margin-right-pixel :conv pixel))
(:name :margin-bottom :id ebox/margin-bottom :initial (lh 0)
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size
:engine-projection (:to :margin-bottom-height :conv line))
(:name :margin-left :id ebox/margin-left :initial (px 0)
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis inline :validator nonnegative-size
:engine-projection (:to :margin-left-pixel :conv pixel))
(:name :margin-block-start :id ebox/margin-block-start :initial (lh 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size
:engine-projection (:to :margin-top-height :conv line))
(:name :margin-inline-end :id ebox/margin-inline-end :initial (px 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis inline :validator nonnegative-size
:engine-projection (:to :margin-right-pixel :conv pixel))
(:name :margin-block-end :id ebox/margin-block-end :initial (lh 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size
:engine-projection (:to :margin-bottom-height :conv line))
(:name :margin-inline-start :id ebox/margin-inline-start :initial (px 0)
:contexts (box row column flex grid) :group geometry
:dirty-kind geometry :signature layout
:size-axis inline :validator nonnegative-size
:engine-projection (:to :margin-left-pixel :conv pixel))
(:name :border-top-width :id ebox/border-top-width
:initial (px 0) :contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis stroke :validator border-width
:engine-projection (:to :border-top-pixel :conv border-pixel))
(:name :border-right-width :id ebox/border-right-width
:initial (px 0) :contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis stroke :validator border-width
:engine-projection (:to :border-right-pixel :conv border-pixel))
(:name :border-bottom-width :id ebox/border-bottom-width
:initial (px 0) :contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis stroke :validator border-width
:engine-projection (:to :border-bottom-pixel :conv border-pixel))
(:name :border-left-width :id ebox/border-left-width
:initial (px 0) :contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:size-axis stroke :validator border-width
:engine-projection (:to :border-left-pixel :conv border-pixel))
(:name :border-top-style :id ebox/border-top-style :initial none
:contexts (box row column flex grid) :group structure
:dirty-kind structure :signature layout :validator border-style)
(:name :border-right-style :id ebox/border-right-style :initial none
:contexts (box row column flex grid) :group structure
:dirty-kind structure :signature layout :validator border-style)
(:name :border-bottom-style :id ebox/border-bottom-style :initial none
:contexts (box row column flex grid) :group structure
:dirty-kind structure :signature layout :validator border-style)
(:name :border-left-style :id ebox/border-left-style :initial none
:contexts (box row column flex grid) :group structure
:dirty-kind structure :signature layout :validator border-style)
(:name :border-top-color :id ebox/border-top-color :initial currentColor
:contexts (box row column flex grid) :group paint
:dirty-kind paint :signature paint
:normalizer color :validator color
:engine-projection (:to :border-top-color :conv color))
(:name :border-right-color :id ebox/border-right-color :initial currentColor
:contexts (box row column flex grid) :group paint
:dirty-kind paint :signature paint
:normalizer color :validator color
:engine-projection (:to :border-right-color :conv color))
(:name :border-bottom-color :id ebox/border-bottom-color
:initial currentColor :contexts (box row column flex grid)
:group paint :dirty-kind paint :signature paint
:normalizer color :validator color
:engine-projection (:to :border-bottom-color :conv color))
(:name :border-left-color :id ebox/border-left-color :initial currentColor
:contexts (box row column flex grid) :group paint
:dirty-kind paint :signature paint
:normalizer color :validator color
:engine-projection (:to :border-left-color :conv color))
(:name :text-align :id ebox/text-align :initial left :inherits t
:contexts (box)
:author-contexts (box)
:group geometry :dirty-kind geometry :signature layout)
(:name :overflow :id ebox/overflow :initial scroll
:contexts (box row column flex grid) :group structure
:dirty-kind geometry :signature layout
:impacts (geometry paint) :projections (layout paint))
(:name :wrap-mode :id ebox/wrap-mode :initial word :inherits t
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:validator wrap-mode)
(:name :visibility :id ebox/visibility :initial visible :inherits t
:contexts (box row column flex grid)
:group paint :dirty-kind paint :signature paint)
(:name :outer :id ebox/outer :initial block
:contexts (box row column flex grid)
:group structure :dirty-kind structure :signature structure
:validator outer :impacts (structure geometry)
:projections (structure layout)
:engine-projection (:to nil))
(:name :item-gap :id ebox/item-gap :initial (px 0) :contexts (row column)
:group geometry :dirty-kind geometry :signature layout
:size-axis main :validator nonnegative-size)
(:name :cross-align :id ebox/cross-align :initial stretch
:contexts (row column) :group geometry :dirty-kind geometry
:signature layout :validator cross-align)
(:name :flex-direction :id ebox/flex-direction :initial row
:contexts (flex) :group geometry :dirty-kind geometry :signature layout
:validator flex-direction)
(:name :flex-wrap :id ebox/flex-wrap :initial nowrap :contexts (flex)
:group geometry :dirty-kind geometry :signature layout
:validator flex-wrap)
(:name :justify-content :id ebox/justify-content :initial flex-start
:contexts (flex grid) :group geometry :dirty-kind geometry
:signature layout :validator justify-content)
(:name :align-items :id ebox/align-items :initial stretch
:contexts (flex grid) :group geometry :dirty-kind geometry
:signature layout :validator align-items)
(:name :align-content :id ebox/align-content :initial stretch
:contexts (flex grid) :group geometry :dirty-kind geometry
:signature layout :validator align-content)
(:name :justify-items :id ebox/justify-items :initial stretch
:contexts (grid) :group geometry :dirty-kind geometry :signature layout
:validator justify-items)
(:name :row-gap :id ebox/row-gap :initial (lh 0)
:contexts (flex grid) :group geometry :dirty-kind geometry :signature layout
:size-axis block :validator nonnegative-size)
(:name :column-gap :id ebox/column-gap
:initial (px 0) :contexts (flex grid) :group geometry :dirty-kind geometry
:signature layout :size-axis inline :validator nonnegative-size)
(:name :order :id ebox/order :initial 0 :contexts (item)
:group geometry :dirty-kind geometry :signature layout :validator order)
(:name :flex-grow :id ebox/flex-grow :initial 0 :contexts (item)
:group geometry :dirty-kind geometry :signature layout
:validator flex-factor)
(:name :flex-shrink :id ebox/flex-shrink :initial 1 :contexts (item)
:group geometry :dirty-kind geometry :signature layout
:validator flex-factor)
(:name :flex-basis :id ebox/flex-basis :initial auto :contexts (item)
:group geometry :dirty-kind geometry :signature layout
:size-axis length :validator preferred-size)
(:name :align-self :id ebox/align-self :initial auto :contexts (item)
:group geometry :dirty-kind geometry :signature layout
:validator align-self)
(:name :justify-self :id ebox/justify-self :initial auto :contexts (item)
:group geometry :dirty-kind geometry :signature layout
:validator justify-self)
(:name :grid-template-columns :id ebox/grid-template-columns :initial nil
:contexts (grid) :group geometry :dirty-kind geometry :signature layout
:size-grammar tracks :size-axis inline :validator grid-track-list)
(:name :grid-template-rows :id ebox/grid-template-rows :initial nil
:contexts (grid) :group geometry :dirty-kind geometry :signature layout
:size-grammar tracks :size-axis block :validator grid-track-list)
(:name :grid-auto-columns :id ebox/grid-auto-columns :initial auto
:contexts (grid) :group geometry :dirty-kind geometry :signature layout
:size-grammar tracks :size-axis inline :validator grid-track)
(:name :grid-auto-rows :id ebox/grid-auto-rows :initial auto
:contexts (grid) :group geometry :dirty-kind geometry :signature layout
:size-grammar tracks :size-axis block :validator grid-track)
(:name :grid-auto-flow :id ebox/grid-auto-flow :initial row
:contexts (grid) :group structure :dirty-kind structure :signature layout
:validator grid-auto-flow)
(:name :grid-column :id ebox/grid-column :initial auto :contexts (item)
:group geometry :dirty-kind geometry :signature layout
:validator grid-placement)
(:name :grid-row :id ebox/grid-row :initial auto :contexts (item)
:group geometry :dirty-kind geometry :signature layout
:validator grid-placement)
(:name :grid-column-span :id ebox/grid-column-span
:shorthand grid-column-span :validator positive-integer
:contexts (item) :group geometry :dirty-kind geometry :signature layout)
(:name :grid-row-span :id ebox/grid-row-span
:shorthand grid-row-span :validator positive-integer
:contexts (item) :group geometry :dirty-kind geometry :signature layout)
(:name :padding :id ebox/padding :shorthand padding
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:engine-projection
(:expand (:padding-top :padding-right :padding-bottom :padding-left)
:mode trbl))
(:name :padding-inline :id ebox/padding-inline :shorthand padding-inline
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:engine-projection (:expand (:padding-left :padding-right) :mode pair))
(:name :padding-block :id ebox/padding-block :shorthand padding-block
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:engine-projection (:expand (:padding-top :padding-bottom) :mode pair))
(:name :margin :id ebox/margin :shorthand margin
:contexts (box row column flex grid)
:impacts (geometry) :projections (layout)
:engine-projection
(:expand (:margin-top :margin-right :margin-bottom :margin-left)
:mode trbl))
(:name :margin-inline :id ebox/margin-inline :shorthand margin-inline
:contexts (box row column flex grid)
:impacts (geometry) :projections (layout)
:engine-projection (:expand (:margin-left :margin-right) :mode pair))
(:name :margin-block :id ebox/margin-block :shorthand margin-block
:contexts (box row column flex grid)
:impacts (geometry) :projections (layout)
:engine-projection (:expand (:margin-top :margin-bottom) :mode pair))
(:name :border :id ebox/border :shorthand border
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:engine-projection
(:expand (:border-top :border-right :border-bottom :border-left)
:mode same))
(:name :border-top :id ebox/border-top :shorthand border-top
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:engine-projection
(:expand (:border-top-width :border-top-style :border-top-color)
:mode wsc))
(:name :border-right :id ebox/border-right :shorthand border-right
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:engine-projection
(:expand (:border-right-width :border-right-style :border-right-color)
:mode wsc))
(:name :border-bottom :id ebox/border-bottom :shorthand border-bottom
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:engine-projection
(:expand (:border-bottom-width :border-bottom-style :border-bottom-color)
:mode wsc))
(:name :border-left :id ebox/border-left :shorthand border-left
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:engine-projection
(:expand (:border-left-width :border-left-style :border-left-color)
:mode wsc))
(:name :border-top-p :id ebox/border-top-p
:shorthand border-top-present :validator boolean
:contexts (box row column flex grid)
:impacts (geometry paint) :projections (layout paint)
:group structure :dirty-kind structure :signature layout)
(:name :border-width :id ebox/border-width :shorthand border-width
:contexts (box row column flex grid)
:group geometry :dirty-kind geometry :signature layout
:engine-projection
(:expand (:border-top-width :border-right-width
:border-bottom-width :border-left-width)
:mode trbl))
(:name :border-style :id ebox/border-style :shorthand border-style
:contexts (box row column flex grid)
:group structure :dirty-kind structure :signature layout
:engine-projection
(:expand (:border-top-style :border-right-style
:border-bottom-style :border-left-style)
:mode trbl))
(:name :border-color :id ebox/border-color :shorthand border-color
:contexts (box row column flex grid)
:group paint :dirty-kind paint :signature paint
:engine-projection
(:expand (:border-top-color :border-right-color
:border-bottom-color :border-left-color)
:mode trbl))
(:name :gap :id ebox/gap :shorthand gap
:contexts (flex grid)
:group geometry :dirty-kind geometry :signature layout)
(:name :flex-flow :id ebox/flex-flow :shorthand flex-flow
:contexts (flex)
:group geometry :dirty-kind geometry :signature layout)
(:name :flex :id ebox/flex :shorthand flex
:contexts (item)
:group geometry :dirty-kind geometry :signature layout))
"Canonical Ebox property schemas and public aliases.")
(defconst ebox-style--internal-engine-projection-rules
'((:display :to nil))
"Projection rules for engine-only fields outside the public schema.")
(defconst ebox-style--engine-projection-modes '(trbl pair wsc same)
"Closed set of shorthand split modes used by engine projection.")
(defconst ebox-style--engine-projection-converters
'(pixel size-pixel border-pixel preferred-size min-size max-size line color)
"Closed set of value converters used by engine projection.")
(define-error 'ebox-style-schema-error "Invalid Ebox style schema")
(defun ebox-style--definition-for-name (name &optional definitions)
"Return NAME's canonical entry from DEFINITIONS or the package definitions."
(cl-find-if
(lambda (property)
(or (eq name (plist-get property :name))
(eq name (plist-get property :id))
(memq name (plist-get property :aliases))))
(or definitions ebox-style--property-definitions)))
(defun ebox-style--definition-engine-targets
(property &optional definitions stack)
"Return private engine targets derived from PROPERTY in DEFINITIONS."
(let* ((definitions (or definitions ebox-style--property-definitions))
(name (plist-get property :name))
(rule (plist-get property :engine-projection)))
(when (memq name stack)
(signal 'ebox-style-schema-error
(list :engine-projection-cycle (reverse (cons name stack)))))
(cond
((null rule) (list name))
((plist-get rule :expand)
(delete-dups
(cl-mapcan
(lambda (target)
(let ((definition
(ebox-style--definition-for-name target definitions)))
(unless definition
(signal 'ebox-style-schema-error
(list :unknown-engine-target target :property name)))
(ebox-style--definition-engine-targets
definition definitions (cons name stack))))
(plist-get rule :expand))))
((plist-member rule :to)
(when-let* ((target (plist-get rule :to))) (list target)))
(t (list name)))))
(defun ebox-style--validate-engine-projection-definitions (definitions)
"Validate engine projection metadata in DEFINITIONS and return t."
(let ((lookup-keys (make-hash-table :test #'eq)))
(dolist (property definitions)
(let ((name (plist-get property :name))
(id (plist-get property :id))
(rule (plist-get property :engine-projection)))
(unless (and (keywordp name) (symbolp id))
(signal 'ebox-style-schema-error
(list :malformed-property-identity property)))
(dolist (entry
(append (list (cons name :name) (cons id :id))
(mapcar (lambda (alias) (cons alias :alias))
(plist-get property :aliases))))
(unless (symbolp (car entry))
(signal 'ebox-style-schema-error
(list :malformed-lookup-key entry :property name)))
(when-let* ((existing (gethash (car entry) lookup-keys)))
(signal 'ebox-style-schema-error
(list :lookup-key-collision (car entry)
:existing existing
:incoming (list :property name :role (cdr entry)))))
(puthash (car entry)
(list :property name :role (cdr entry))
lookup-keys))
(when rule
(unless (proper-list-p rule)
(signal 'ebox-style-schema-error
(list :malformed-engine-rule name rule)))
(let ((expand (plist-get rule :expand))
(to-present (plist-member rule :to))
(converter (plist-get rule :conv)))
(when (eq (not (null expand)) (not (null to-present)))
(signal 'ebox-style-schema-error
(list :ambiguous-engine-rule name rule)))
(when expand
(unless (and (proper-list-p expand) expand
(memq (plist-get rule :mode)
ebox-style--engine-projection-modes))
(signal 'ebox-style-schema-error
(list :malformed-engine-expansion name rule))))
(when (and to-present
(plist-get rule :to)
(not (keywordp (plist-get rule :to))))
(signal 'ebox-style-schema-error
(list :malformed-engine-target name rule)))
(when (and converter
(not (memq converter
ebox-style--engine-projection-converters)))
(signal 'ebox-style-schema-error
(list :unknown-engine-converter name converter))))))
(ebox-style--definition-engine-targets property definitions))
(dolist (entry ebox-style--internal-engine-projection-rules)
(when (ebox-style--definition-for-name (car entry) definitions)
(signal 'ebox-style-schema-error
(list :public-rule-in-internal-table (car entry)))))
t))
(defconst ebox-style--engine-projection-schema-valid-p
(ebox-style--validate-engine-projection-definitions
ebox-style--property-definitions)
"Non-nil after the package engine projection schema validates.")
(defvar ebox-style-schemas)
(defvar ebox-style--property-index)
(defconst ebox-style--declaration-cache-max-entries 512
"Maximum canonical declaration compilations retained by Ebox.")
(defvar ebox-style--declaration-cache
(make-hash-table :test 'equal)
"Bounded memo table for canonical Ebox declaration compilation.")
(defun ebox-style--property (name)
"Return registry-owned style metadata for internal NAME lookup."
(gethash name ebox-style--property-index))
(defun ebox-style-property (name)
"Return detached style metadata for canonical property or alias NAME."
(ecss-schema-set-property-metadata ebox-style-schemas name))
(defun ebox-style-canonical-name (name)
"Return canonical CSS-like longhand property name for NAME."
(when-let* ((property (ebox-style--property name)))
(plist-get property :name)))
(defun ebox-style-property-accepted-p (tag name)
"Return non-nil when author form TAG accepts property NAME."
(when-let* ((property (ebox-style--property name)))
(ebox-style--form-property-allowed-p tag property)))
(defun ebox-style-schema-id (name)
"Return namespaced ECSS schema id for Ebox property NAME."
(when-let* ((property (ebox-style--property name)))
(plist-get property :id)))
(defun ebox-style--put (plist key value)
"Return PLIST with KEY set to VALUE."
(plist-put plist key value))
(defun ebox-style--atomic-size-form-p (value)
"Return non-nil when VALUE denotes one size expression before validation.
Keep malformed unit and function forms intact so their target property reports
the invalid value and its allowed units instead of a shorthand arity error."
(and (consp value)
(memq (car value) '(px ch vw lh vh % calc min max clamp))))
(defun ebox-style--split-trbl (value)
"Split VALUE using CSS TRBL rules into top, right, bottom, left."
(cond
((null value) (list nil nil nil nil))
((ebox-style--atomic-size-form-p value) (make-list 4 value))
((atom value) (list value value value value))
((and (listp value) (keywordp (car value)))
(list (plist-get value :top) (plist-get value :right)
(plist-get value :bottom) (plist-get value :left)))
((listp value)
(pcase (length value)
(1 (list (nth 0 value) (nth 0 value) (nth 0 value) (nth 0 value)))
(2 (list (nth 0 value) (nth 1 value) (nth 0 value) (nth 1 value)))
(3 (list (nth 0 value) (nth 1 value) (nth 2 value) (nth 1 value)))
(4 (list (nth 0 value) (nth 1 value) (nth 2 value) (nth 3 value)))
(_ (error "Ebox four-side shorthand requires one to four values: %S"
value))))
(t (list value value value value))))
(defun ebox-style--split-pair (value)
"Split VALUE into a start/end pair."
(cond
((null value) (list nil nil))
((ebox-style--atomic-size-form-p value) (list value value))
((atom value) (list value value))
((listp value)
(pcase (length value)
(1 (list (nth 0 value) (nth 0 value)))
(2 (list (nth 0 value) (nth 1 value)))
(_ (error "Ebox pair shorthand requires one or two values: %S" value))))
(t (list value value))))
(defun ebox-style--wsc-classify (item)
"Classify border shorthand ITEM as width, style, or color."
(cond
((ebox-style--atomic-size-form-p item) (cons 'width item))
((memq item ebox-style--border-styles) (cons 'style item))
((or (stringp item) (tp-paint-slot-p item)) (cons 'color item))
(t (error "Invalid Ebox border component: %S" item))))
(defun ebox-style--split-wsc (value)
"Split VALUE into border width, style, and color."
(cond
((null value) (list nil nil nil))
((ebox-style--atomic-size-form-p value) (list value 'solid nil))
((atom value)
(let ((classified (ebox-style--wsc-classify value)))
(pcase (car classified)
('width (list (cdr classified) nil nil))
('style (list '(px 1) (cdr classified) nil))
('color (list '(px 1) 'solid (cdr classified))))))
((proper-list-p value)
(let (width style color width-p style-p color-p)
(dolist (item value)
(let ((classified (ebox-style--wsc-classify item)))
(pcase (car classified)
('width
(when width-p (error "Duplicate Ebox border width in %S" value))
(setq width-p t width (cdr classified)))
('style
(when style-p (error "Duplicate Ebox border style in %S" value))
(setq style-p t style (cdr classified)))
('color
(when color-p (error "Duplicate Ebox border color in %S" value))
(setq color-p t color (cdr classified))))))
(list (or width '(px 1)) (or style 'solid) color)))
(t (error "Ebox border must be a color or proper component list: %S"
value))))
(defun ebox-style--atom-consp (list)
"Return non-nil when LIST is a strict dotted pair of atoms."
(and (consp list)
(atom (car list))
(cdr list)
(atom (cdr list))))
(defun ebox-style--parse-color (color)
"Return a detached canonical COLOR value without consulting a frame."
(if (stringp color) (copy-sequence color) color))
(defun ebox-style--trbl-declarations (properties value)
"Expand VALUE across four namespaced PROPERTIES."
(cl-loop for property in properties
for part in (ebox-style--split-trbl value)
append (list property part)))
(defun ebox-style--pair-declarations (properties value)
"Expand VALUE across two namespaced PROPERTIES."
(cl-loop for property in properties
for part in (ebox-style--split-pair value)
append (list property part)))
(defun ebox-style--padding-shorthand (value)
"Expand padding VALUE to namespaced logical longhands."
(ebox-style--trbl-declarations
'(ebox/padding-block-start ebox/padding-inline-end
ebox/padding-block-end ebox/padding-inline-start)
value))
(defun ebox-style--padding-inline-shorthand (value)
"Expand inline padding VALUE to namespaced longhands."
(ebox-style--pair-declarations
'(ebox/padding-inline-start ebox/padding-inline-end) value))
(defun ebox-style--padding-block-shorthand (value)
"Expand block padding VALUE to namespaced longhands."
(ebox-style--pair-declarations
'(ebox/padding-block-start ebox/padding-block-end) value))
(defun ebox-style--margin-shorthand (value)
"Expand margin VALUE to namespaced logical longhands."
(ebox-style--trbl-declarations
'(ebox/margin-block-start ebox/margin-inline-end
ebox/margin-block-end ebox/margin-inline-start)
value))
(defun ebox-style--margin-inline-shorthand (value)
"Expand inline margin VALUE to namespaced longhands."
(ebox-style--pair-declarations
'(ebox/margin-inline-start ebox/margin-inline-end) value))
(defun ebox-style--margin-block-shorthand (value)
"Expand block margin VALUE to namespaced longhands."
(ebox-style--pair-declarations
'(ebox/margin-block-start ebox/margin-block-end) value))
(defconst ebox-style--border-sides '(top right bottom left)
"Physical border sides in CSS shorthand order.")
(defun ebox-style--border-side-declarations (side value)
"Expand border VALUE for SIDE to three namespaced longhands."
(pcase-let ((`(,width ,style ,color) (ebox-style--split-wsc value)))
(list (intern (format "ebox/border-%s-width" side)) width
(intern (format "ebox/border-%s-style" side)) style
(intern (format "ebox/border-%s-color" side)) color)))
(defun ebox-style--border-shorthand (value)
"Expand border VALUE directly to all namespaced side longhands."
(cl-mapcan (lambda (side)
(ebox-style--border-side-declarations side value))
ebox-style--border-sides))
(defun ebox-style--border-top-shorthand (value)
"Expand top border VALUE to namespaced longhands."
(ebox-style--border-side-declarations 'top value))
(defun ebox-style--border-right-shorthand (value)
"Expand right border VALUE to namespaced longhands."
(ebox-style--border-side-declarations 'right value))
(defun ebox-style--border-bottom-shorthand (value)
"Expand bottom border VALUE to namespaced longhands."
(ebox-style--border-side-declarations 'bottom value))
(defun ebox-style--border-left-shorthand (value)
"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))
(list 'px (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-component-shorthand (component value)
"Expand border COMPONENT VALUE across all physical sides."
(ebox-style--trbl-declarations
(mapcar (lambda (side)
(intern (format "ebox/border-%s-%s" side component)))
ebox-style--border-sides)
value))
(defun ebox-style--border-width-shorthand (value)
"Expand border width VALUE across all physical sides."
(ebox-style--border-component-shorthand 'width value))
(defun ebox-style--border-style-shorthand (value)
"Expand border style VALUE across all physical sides."
(ebox-style--border-component-shorthand 'style value))
(defun ebox-style--border-color-shorthand (value)
"Expand border color VALUE across all physical sides."
(ebox-style--border-component-shorthand 'color value))
(defun ebox-style--gap-shorthand (value)
"Expand gap VALUE to row and column gaps."
(ebox-style--pair-declarations '(ebox/row-gap ebox/column-gap) value))
(defun ebox-style--grid-column-span-shorthand (value)
"Expand Grid column span VALUE into canonical auto placement."
(unless (and (integerp value) (> value 0))
(error "Ebox grid-column-span must be a positive integer: %S" value))
(list 'ebox/grid-column (list :span value)))
(defun ebox-style--grid-row-span-shorthand (value)
"Expand Grid row span VALUE into canonical auto placement."
(unless (and (integerp value) (> value 0))
(error "Ebox grid-row-span must be a positive integer: %S" value))
(list 'ebox/grid-row (list :span value)))
(defun ebox-style--flex-flow-shorthand (value)
"Expand flex-flow VALUE to direction and wrapping longhands."
(unless (and (proper-list-p value) (= (length value) 2))
(error "Ebox flex-flow requires one direction and one wrap value: %S"
value))
(let ((values value) direction wrap)
(setq direction
(cl-find-if (lambda (item)
(memq item '(row row-reverse column column-reverse)))
values)
wrap (cl-find-if (lambda (item)
(memq item '(nowrap wrap wrap-reverse)))
values))
(unless (and direction wrap)
(error "Ebox flex-flow requires one direction and one wrap value: %S"
value))
(list 'ebox/flex-direction direction 'ebox/flex-wrap wrap)))
(defun ebox-style--flex-shorthand (value)
"Expand flex item VALUE to grow, shrink, and basis longhands."
(pcase value
((and (pred numberp) (guard (>= value 0)))
(list 'ebox/flex-grow value 'ebox/flex-shrink 1
'ebox/flex-basis '(% 0)))
('none '(ebox/flex-grow 0 ebox/flex-shrink 0 ebox/flex-basis auto))
('auto '(ebox/flex-grow 1 ebox/flex-shrink 1 ebox/flex-basis auto))
('initial '(ebox/flex-grow 0 ebox/flex-shrink 1 ebox/flex-basis auto))
((and (pred proper-list-p) (guard (= (length value) 3)))
(list 'ebox/flex-grow (nth 0 value)
'ebox/flex-shrink (nth 1 value)
'ebox/flex-basis (nth 2 value)))
(_ (error "Ebox flex requires none, auto, initial, a factor, or (grow shrink basis): %S"
value))))
(defun ebox-style--normalizer (name)
"Return schema normalizer named NAME."
(pcase name
('color #'ebox-style--parse-color)
(_ #'identity)))
(defun ebox-style--negative-numeric-size-p (value)
"Return non-nil when VALUE is or starts with a negative number."
(or (and (numberp value) (< value 0))
(and (consp value)
(numberp (car value))
(< (car value) 0))))
(defun ebox-style--nonnegative-size-p (value)
"Return non-nil when VALUE is a canonical nonnegative length-percentage."
(ebox-size-value-p value nil t))
(defconst ebox-style--preferred-size-keywords
'(auto min-content max-content fit-content stretch)
"CSS preferred and minimum size keywords in the supported subset.")
(defconst ebox-style--maximum-size-keywords
'(none min-content max-content fit-content stretch)
"CSS maximum size keywords in the supported subset.")
(defun ebox-style--horizontal-size-p (value)
"Return non-nil when VALUE is a canonical horizontal size."
(or (memq value ebox-style--preferred-size-keywords)
(ebox-size-value-for-axis-p value 'inline)))
(defun ebox-style--preferred-size-p (value)
"Return non-nil when VALUE is a size whose parent axis is not known yet."
(or (memq value ebox-style--preferred-size-keywords)
(ebox-style--nonnegative-size-p value)))
(defun ebox-style--min-horizontal-size-p (value)
"Return non-nil when VALUE is a canonical minimum horizontal size."
(ebox-style--horizontal-size-p value))
(defun ebox-style--max-horizontal-size-p (value)
"Return non-nil when VALUE is a canonical maximum horizontal size."
(or (memq value ebox-style--maximum-size-keywords)
(ebox-size-value-for-axis-p value 'inline)))
(defun ebox-style--vertical-size-p (value)
"Return non-nil when VALUE is a canonical vertical size."
(or (memq value ebox-style--preferred-size-keywords)
(ebox-size-value-for-axis-p value 'block)))
(defun ebox-style--min-vertical-size-p (value)
"Return non-nil when VALUE is a canonical minimum vertical size."
(ebox-style--vertical-size-p value))
(defun ebox-style--max-vertical-size-p (value)
"Return non-nil when VALUE is a canonical maximum vertical size."
(or (memq value ebox-style--maximum-size-keywords)
(ebox-size-value-for-axis-p value 'block)))
(defun ebox-style--nonnegative-number-p (value)
"Return non-nil when VALUE is nil or a non-negative number."
(or (null value) (and (numberp value) (>= value 0))))
(defun ebox-style--positive-integer-p (value)
"Return non-nil when VALUE is nil or a positive integer."
(or (null value) (and (integerp value) (> value 0))))
(defun ebox-style--grid-placement-p (value)
"Return non-nil when VALUE is a valid public Grid placement."
(or (null value)
(eq value 'auto)
(and (integerp value) (> value 0))
(and (proper-list-p value)
(pcase value
(`(,start)
(and (integerp start) (> start 0)))
(`(,start :span ,span)
(and (integerp start) (> start 0)
(integerp span) (> span 0)))
(`(:span ,span)
(and (integerp span) (> span 0)))
(`(,start ,end)
(and (integerp start) (> start 0)
(integerp end) (> end start)))))))
(defun ebox-style--outer-p (value)
"Return non-nil when VALUE is an Ebox outer participation value."
(memq value '(inline block)))
(defun ebox-style--signed-size-p (value &optional axis)
"Return non-nil when VALUE is a signed typed length in size AXIS.
Leave fractional and contextual lengths unresolved until used placement."
(and (ebox-size-value-p value t)
(let ((units (cdr (assq (or axis 'length) ebox-size-axis-units))))
(cl-labels ((valid-p (expression)
(or (atom expression)
(if (memq (car expression) '(px ch vw lh vh %))
(memq (car expression) units)
(cl-every #'valid-p (cdr expression))))))
(valid-p value)))))
(defun ebox-style--position-p (value)
"Return non-nil when VALUE is a supported positioning mode."
(memq value '(static relative absolute)))
(defun ebox-style--layer-p (value)
"Return non-nil when VALUE is a supported layer paint target."
(memq value '(local root)))
(defun ebox-style--anchor-p (value)
"Return non-nil when VALUE is nil or a semantic symbol or string id."
(or (symbolp value) (stringp value)))
(defun ebox-style--placement-p (value)
"Return non-nil when VALUE is a supported anchor placement."
(memq value '(bottom-start bottom-end top-start top-end)))
(defun ebox-style--font-family-p (value)
"Return non-nil when VALUE is an Ebox font family."
(or (null value)
(and (stringp value) (not (string-empty-p value)))
(symbolp value)))
(defun ebox-style--color-p (value)
"Return non-nil when VALUE is a canonical or deferred color fact."
(or (null value) (stringp value) (eq value 'currentColor)
(tp-paint-slot-p value)))
(defun ebox-style--font-size-p (value)
"Return non-nil when VALUE is a positive CSS reference-pixel size."
(or (null value) (and (numberp value) (> value 0))))
(defun ebox-style--font-weight-p (value)
"Return non-nil when VALUE is a canonical CSS font weight."
(or (null value)
(memq value '(normal bold bolder lighter
100 200 300 400 500 600 700 800 900))))
(defun ebox-style--font-style-p (value)
"Return non-nil when VALUE is a canonical CSS font style."
(or (null value) (memq value '(normal italic oblique))))
(defun ebox-style--border-width-p (value)
"Return non-nil when VALUE is a nonnegative length without percentages."
(ebox-size-value-p value nil nil))
(defun ebox-style--border-style-p (value)
"Return non-nil when VALUE is a supported canonical border style."
(memq value '(none solid)))
(defun ebox-style--text-decoration-line-p (value)
"Return non-nil when VALUE is a canonical text-decoration line set."
(let ((values (if (listp value) value (list value))))
(or (equal values '(none))
(and values
(cl-every (lambda (item)
(memq item '(underline overline line-through)))
values)
(= (length values)
(length (delete-dups (copy-sequence values))))))))
(defun ebox-style--text-decoration-style-p (value)
"Return non-nil when VALUE is a canonical text-decoration style."
(memq value '(solid double dotted dashed wavy)))
(defun ebox-style--nonnegative-integer-p (value)
"Return non-nil when VALUE is a nonnegative integer."
(and (integerp value) (>= value 0)))
(defun ebox-style--cross-align-p (value)
"Return non-nil when VALUE is a Row/Column cross alignment."
(memq value '(start center end stretch)))
(defun ebox-style--flex-direction-p (value)
"Return non-nil when VALUE is a canonical Flex direction."
(memq value '(row row-reverse column column-reverse)))
(defun ebox-style--flex-wrap-p (value)
"Return non-nil when VALUE is a canonical Flex wrapping mode."
(memq value '(nowrap wrap wrap-reverse)))
(defun ebox-style--justify-content-p (value)
"Return non-nil when VALUE is a canonical content justification."
(memq value '(normal start end flex-start flex-end center stretch
space-between space-around space-evenly)))
(defun ebox-style--align-items-p (value)
"Return non-nil when VALUE is a canonical item alignment."
(memq value '(normal start end stretch flex-start flex-end center baseline)))
(defun ebox-style--align-content-p (value)
"Return non-nil when VALUE is a canonical multi-line alignment."
(memq value '(normal start end stretch flex-start flex-end center
space-between space-around space-evenly)))
(defun ebox-style--justify-items-p (value)
"Return non-nil when VALUE is a canonical Grid item justification."
(memq value '(stretch start end center)))
(defun ebox-style--align-self-p (value)
"Return non-nil when VALUE is a canonical parent-relative alignment."
(memq value '(auto normal start end stretch flex-start flex-end
center baseline)))
(defun ebox-style--grid-auto-flow-p (value)
"Return non-nil when VALUE is a canonical Grid auto-flow axis."
(memq value '(row column)))
(defun ebox-style--grid-fixed-track-p (value)
"Return non-nil when VALUE is a nonnegative Grid track size expression."
(ebox-style--nonnegative-size-p value))
(defun ebox-style--grid-fr-track-p (value)
"Return non-nil when VALUE is one positive fractional Grid track."
(and (proper-list-p value) (= (length value) 2)
(eq (car value) 'fr)
(numberp (cadr value)) (> (cadr value) 0)))
(defun ebox-style--grid-track-p (value &optional allow-repeat)
"Return non-nil when VALUE is one Grid track.
ALLOW-REPEAT permits a top-level repeat expression."
(cond
((memq value '(auto min-content max-content)) t)
((ebox-style--grid-fixed-track-p value) t)
((ebox-style--grid-fr-track-p value) t)
((and (proper-list-p value) (= (length value) 3)
(eq (car value) 'minmax))
(let ((minimum (cadr value)) (maximum (caddr value)))
(and (not (and (consp minimum)
(memq (car minimum) '(minmax repeat))))
(not (and (consp maximum)
(memq (car maximum) '(minmax repeat))))
(ebox-style--grid-track-p minimum nil)
(not (ebox-style--grid-fr-track-p minimum))
(ebox-style--grid-track-p maximum nil))))
((and allow-repeat (proper-list-p value) (= (length value) 3)
(eq (car value) 'repeat)
(integerp (cadr value)) (> (cadr value) 0))
(let ((body (caddr value)))
(or (ebox-style--grid-track-p body nil)
(and (proper-list-p body) body
(cl-every (lambda (track)
(ebox-style--grid-track-p track nil))
body)))))
(t nil)))
(defun ebox-style--grid-track-list-p (value)
"Return non-nil when VALUE is a valid Grid track list."
(or (null value)
(and (proper-list-p value) value
(cl-every (lambda (track)
(ebox-style--grid-track-p track t))
value))))
(defun ebox-style--grid-auto-track-p (value)
"Return non-nil when VALUE denotes at most one implicit Grid track."
(or (null value)
(ebox-style--grid-track-p value nil)
(and (proper-list-p value) (= (length value) 1)
(ebox-style--grid-track-p (car value) nil))))
(defun ebox-style--order-p (value)
"Return non-nil when VALUE is an integer parent ordering key."
(integerp value))
(defun ebox-style--justify-self-p (value)
"Return non-nil when VALUE is a canonical Grid self alignment."
(memq value '(auto stretch start end center)))
(defun ebox-style--wrap-mode-p (value)
"Return non-nil when VALUE is a public Box wrapping mode."
(memq value '(word char kp none)))
(defun ebox-style-no-soft-wrap-p (value)
"Return non-nil when wrap VALUE disables soft line breaking.
`none' is the canonical author value; nil is retained only for private
generated wrappers that already contain exact line breaks."
(memq value '(nil none)))
(defun ebox-style-soft-wrap-p (value)
"Return non-nil when wrap VALUE performs soft line breaking."
(memq value '(word char kp)))
(defun ebox-style--validator (name)
"Return schema validator named NAME."
(pcase name
('preferred-size #'ebox-style--preferred-size-p)
('horizontal-size #'ebox-style--horizontal-size-p)
('min-horizontal-size #'ebox-style--min-horizontal-size-p)
('max-horizontal-size #'ebox-style--max-horizontal-size-p)
('vertical-size #'ebox-style--vertical-size-p)
('min-vertical-size #'ebox-style--min-vertical-size-p)
('max-vertical-size #'ebox-style--max-vertical-size-p)
('nonnegative-size #'ebox-style--nonnegative-size-p)
('nonnegative-number #'ebox-style--nonnegative-number-p)
('positive-integer #'ebox-style--positive-integer-p)
('grid-placement #'ebox-style--grid-placement-p)
('outer #'ebox-style--outer-p)
('signed-size #'ebox-style--signed-size-p)
('position #'ebox-style--position-p)
('integer #'integerp)
('layer #'ebox-style--layer-p)
('anchor #'ebox-style--anchor-p)
('placement #'ebox-style--placement-p)
('font-family #'ebox-style--font-family-p)
('color #'ebox-style--color-p)
('font-size #'ebox-style--font-size-p)
('font-weight #'ebox-style--font-weight-p)
('font-style #'ebox-style--font-style-p)
('border-width #'ebox-style--border-width-p)
('border-style #'ebox-style--border-style-p)
('text-decoration-line #'ebox-style--text-decoration-line-p)
('text-decoration-style #'ebox-style--text-decoration-style-p)
('nonnegative-integer #'ebox-style--nonnegative-integer-p)
('cross-align #'ebox-style--cross-align-p)
('flex-direction #'ebox-style--flex-direction-p)
('flex-wrap #'ebox-style--flex-wrap-p)
('justify-content #'ebox-style--justify-content-p)
('align-items #'ebox-style--align-items-p)
('align-content #'ebox-style--align-content-p)
('justify-items #'ebox-style--justify-items-p)
('align-self #'ebox-style--align-self-p)
('grid-auto-flow #'ebox-style--grid-auto-flow-p)
('grid-track-list #'ebox-style--grid-track-list-p)
('grid-track #'ebox-style--grid-auto-track-p)
('order #'ebox-style--order-p)
('flex-factor #'ebox-style--nonnegative-number-p)
('justify-self #'ebox-style--justify-self-p)
('wrap-mode #'ebox-style--wrap-mode-p)
('boolean (lambda (value) (memq value '(nil t))))
(_ (lambda (_value) t))))
(defun ebox-style--shorthand (name)
"Return shorthand expander named NAME."
(alist-get
name
'((padding . ebox-style--padding-shorthand)
(padding-inline . ebox-style--padding-inline-shorthand)
(padding-block . ebox-style--padding-block-shorthand)
(margin . ebox-style--margin-shorthand)
(margin-inline . ebox-style--margin-inline-shorthand)
(margin-block . ebox-style--margin-block-shorthand)
(border . ebox-style--border-shorthand)
(border-top . ebox-style--border-top-shorthand)
(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-width . ebox-style--border-width-shorthand)
(border-style . ebox-style--border-style-shorthand)
(border-color . ebox-style--border-color-shorthand)
(gap . ebox-style--gap-shorthand)
(grid-column-span . ebox-style--grid-column-span-shorthand)
(grid-row-span . ebox-style--grid-row-span-shorthand)
(flex-flow . ebox-style--flex-flow-shorthand)
(flex . ebox-style--flex-shorthand))))
(defun ebox-style--size-axis (property &optional tag)
"Return PROPERTY's size domain for author TAG when known."
(let ((axis (plist-get property :size-axis)))
(if (eq axis 'main)
(pcase tag ('row 'inline) ('column 'block) (_ 'length))
axis)))
(defun ebox-style--size-value-valid-p (property value &optional tag)
"Return non-nil when PROPERTY's VALUE respects TAG's size domain.
Grid track wrappers are traversed after their structural validator succeeds."
(let ((axis (ebox-style--size-axis property tag)))
(or (null axis)
(if (eq (plist-get property :size-grammar) 'tracks)
(cl-labels
((valid-p (entry)
(cond
((atom entry) t)
((eq (car entry) 'fr) t)
((memq (car entry) '(minmax repeat))
(cl-every #'valid-p (cdr entry)))
((ebox-size-value-p entry)
(ebox-size-value-for-axis-p entry axis))
(t (cl-every #'valid-p entry)))))
(valid-p value))
(if (eq (plist-get property :validator) 'signed-size)
(ebox-style--signed-size-p value axis)
(or (symbolp value)
(ebox-size-value-for-axis-p value axis)))))))
(defun ebox-style--schema-validator (property)
"Return PROPERTY's grammar and unit-domain validator."
(let ((validator (ebox-style--validator (plist-get property :validator))))
(if (plist-get property :size-axis)
(lambda (value)
(and (funcall validator value)
(ebox-style--size-value-valid-p property value)))
validator)))
(defun ebox-style--schema-options (property)
"Return ECSS registration options for PROPERTY metadata."
(let ((options (list :initial (plist-get property :initial)
:inherits (plist-get property :inherits)
:normalizer (ebox-style--normalizer
(plist-get property :normalizer))
:validator (ebox-style--schema-validator property)
:equality #'equal)))
(when-let* ((shorthand (ebox-style--shorthand
(plist-get property :shorthand))))
(setq options (plist-put options :shorthand shorthand)))
options))
(defun ebox-style--schema-impacts (property)
"Return immutable impact classes for PROPERTY metadata."
(let ((name (plist-get property :name))
(group (plist-get property :group)))
(or (plist-get property :impacts)
(and (eq group 'typography) '(geometry paint))
(and (or (string-prefix-p ":padding" (symbol-name name))
(and (string-prefix-p ":border" (symbol-name name))
(not (string-suffix-p "-color" (symbol-name name)))
(not (eq name :border-color))))
'(geometry paint))
(list (plist-get property :dirty-kind)))))
(defun ebox-style--schema-projections (property)
"Return immutable projection owners for PROPERTY metadata."
(let ((name (plist-get property :name))
(group (plist-get property :group))
(contexts (plist-get property :contexts)))
(or (plist-get property :projections)
(cond
((eq group 'typography) '(measurement paint))
((memq 'item contexts) '(parent-participation))
((or (string-prefix-p ":padding" (symbol-name name))
(and (string-prefix-p ":border" (symbol-name name))
(not (string-suffix-p "-color" (symbol-name name)))
(not (eq name :border-color))))
'(layout paint))
((eq group 'paint) '(paint))
((eq group 'structure) '(structure layout))
(t '(layout))))))
(defun ebox-style--schema-domain-owner (property)
"Return the domain owner of PROPERTY's computed projection."
(let ((name (plist-get property :name))
(group (plist-get property :group))
(contexts (plist-get property :contexts)))
(cond
((eq group 'typography) 'font-metrics-projection)
((memq 'item contexts) 'direct-parent-layout)
((eq group 'paint) 'tp-contribution)
((or (memq name '(:gap :flex-flow))
(cl-some
(lambda (tag)
(memq name (ebox-layout-config-property-names tag)))
'(row column flex grid)))
'ebox-layout-config)
(t 'ebox-box-frame))))
(defun ebox-style--schema-metadata (property)
"Return detached, complete registry metadata for PROPERTY."
(let ((metadata (copy-tree property)))
(setq metadata
(plist-put metadata :namespace
(if (memq (plist-get property :name)
'(:wrap-mode :item-gap :cross-align
:border-top-p :grid-column-span
:grid-row-span :layer :anchor :placement))
'ebox
'css)))
(setq metadata
(plist-put metadata :owner
(ebox-style--schema-domain-owner property)))
(setq metadata
(plist-put metadata :impacts
(ebox-style--schema-impacts property)))
(setq metadata
(plist-put metadata :engine-targets
(ebox-style--definition-engine-targets property)))
(plist-put metadata :projections
(ebox-style--schema-projections property))))
(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 (ebox-style--schema-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 (source ebox-style--property-definitions table)
(let ((property (ebox-style--schema-metadata source)))
(puthash (plist-get property :id) property table)
(puthash (plist-get property :name) property table)
(dolist (alias (plist-get property :aliases))
(puthash alias property table)))))
"Ebox-owned immutable lookup index over property definitions.")
(defconst ebox-style-schemas
(ecss-schema-set-compose ebox-style--schema-package)
"Immutable Ebox-only surface schema set.")
(defvar ebox-style--closed-computed-cache (make-hash-table :test #'equal)
"Bounded cache of selector-free, dependency-free computed styles.")
(defvar ebox-style--closed-inheritance-cache
(make-hash-table :test #'eq :weakness 'key)
"Computed-style keyed canonical inheritance projections.")
(defconst ebox-style--closed-computed-cache-limit 512
"Maximum selector-free computed styles retained across projections.")
(declare-function tp-computed-p "tp-style" (value))
(defun ebox-style--closed-declarations-p (declarations)
"Return non-nil when DECLARATIONS contain no computed value source."
(cl-loop for (_property value) on declarations by #'cddr
never (tp-computed-p value)))
(defvar ebox-style-stylesheet (ebox-style--create-stylesheet)
"Isolated ECSS stylesheet containing Ebox layout and paint rules.")
(defun ebox-style-cascade-active-p ()
"Return non-nil when the Ebox stylesheet contains cascade rules.
Inline declarations are compiled into Ebox engine properties while nodes are
created, so a surface with no stylesheet rules does not need a per-node ECSS
cascade pass during its initial static projection."
(not (null (ecss-stylesheet-rules ebox-style-stylesheet))))
(defun ebox-style-reset-rules ()
"Clear all rules and cascade layers in `ebox-style-stylesheet'."
(ecss-stylesheet-clear ebox-style-stylesheet))
(defun ebox-style--selector (selector)
"Return ECSS selector AST compiled from Ebox SELECTOR."
(if (stringp selector)
(progn
(require 'ebox-selector)
(ebox-selector-parse selector))
selector))
(defun ebox-style--custom-property-p (property)
"Return non-nil when PROPERTY names an ECSS custom property."
(and (symbolp property)
(string-prefix-p "--" (symbol-name property))))
(defun ebox-style--declaration-value-equal-p (left right)
"Return non-nil when declaration values LEFT and RIGHT are equivalent.
TP paint slots are stable paint addresses. Their installation flag and
current face specification belong to TP publication state, not to the Ebox
author declaration that references the slot."
(if (and (tp-paint-slot-p left) (tp-paint-slot-p right))
(eq (tp-paint-slot-face left) (tp-paint-slot-face right))
(equal-including-properties left right)))
(defun ebox-style-declarations-equal-p (left right)
"Return non-nil when canonical declaration plists LEFT and RIGHT are equal."
(let ((left left)
(right right)
equal-p)
(setq equal-p t)
(while (and equal-p left right)
(setq equal-p
(and (eq (car left) (car right))
(ebox-style--declaration-value-equal-p
(cadr left) (cadr right)))
left (cddr left)
right (cddr right)))
(and equal-p (null left) (null right))))
(defconst ebox-style--inherited-properties
(cl-loop for property in ebox-style--property-definitions
when (plist-get property :inherits)
collect (plist-get property :id))
"Canonical ECSS properties inherited according to the shared schema.")
(defun ebox-style--inherited-declarations-changed-p (old new)
"Return non-nil when OLD and NEW change descendant style inputs.
The inherited property domain comes only from the canonical schema. ECSS
custom properties also participate because a descendant `var' expression may
consume them even though Ebox does not interpret their names."
(or
(cl-some
(lambda (property)
(let ((old-entry (plist-member old property))
(new-entry (plist-member new property)))
(not (and (eq (not (null old-entry)) (not (null new-entry)))
(ebox-style--declaration-value-equal-p
(cadr old-entry) (cadr new-entry))))))
ebox-style--inherited-properties)
(not
(ebox-style-declarations-equal-p
(cl-loop for (property value) on old by #'cddr
when (ebox-style--custom-property-p property)
append (list property value))
(cl-loop for (property value) on new by #'cddr
when (ebox-style--custom-property-p property)
append (list property value))))))
(defun ebox-style--valid-plist-p (plist)
"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--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
not affect declaration compilation and are intentionally excluded."
(let (canonical)
(cl-loop for (property value) on plist by #'cddr
for schema = (ebox-style-schema-id property)
if schema
do (setq canonical
(append canonical (list schema value)))
else if (ebox-style--custom-property-p property)
do (setq canonical
(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)
"Return non-nil when VALIDATOR rejects static VALUE."
(pcase validator
('preferred-size (not (ebox-style--preferred-size-p value)))
('nonnegative-number
(and (numberp value) (< value 0)))
('horizontal-size (not (ebox-style--horizontal-size-p value)))
('min-horizontal-size (not (ebox-style--min-horizontal-size-p value)))
('max-horizontal-size (not (ebox-style--max-horizontal-size-p value)))
('vertical-size (not (ebox-style--vertical-size-p value)))
('min-vertical-size (not (ebox-style--min-vertical-size-p value)))
('max-vertical-size (not (ebox-style--max-vertical-size-p value)))
('nonnegative-size (not (ebox-style--nonnegative-size-p value)))
('signed-size (not (ebox-style--signed-size-p value)))
('position (not (ebox-style--position-p value)))
('integer (not (integerp value)))
('layer (not (ebox-style--layer-p value)))
('anchor (not (ebox-style--anchor-p value)))
('placement (not (ebox-style--placement-p value)))
('positive-integer
(and (numberp value) (not (ebox-style--positive-integer-p value))))
('grid-placement
(and (or (numberp value) (proper-list-p value))
(not (ebox-style--grid-placement-p value))))
('font-family (not (ebox-style--font-family-p value)))
('color (not (ebox-style--color-p value)))
('font-size (not (ebox-style--font-size-p value)))
('font-weight (not (ebox-style--font-weight-p value)))
('font-style (not (ebox-style--font-style-p value)))
('border-width (not (ebox-style--border-width-p value)))
('border-style (not (ebox-style--border-style-p value)))
('text-decoration-line
(not (ebox-style--text-decoration-line-p value)))
('text-decoration-style
(not (ebox-style--text-decoration-style-p value)))
('nonnegative-integer
(not (ebox-style--nonnegative-integer-p value)))
('cross-align (not (ebox-style--cross-align-p value)))
('flex-direction (not (ebox-style--flex-direction-p value)))
('flex-wrap (not (ebox-style--flex-wrap-p value)))
('justify-content (not (ebox-style--justify-content-p value)))
('align-items (not (ebox-style--align-items-p value)))
('align-content (not (ebox-style--align-content-p value)))
('justify-items (not (ebox-style--justify-items-p value)))
('align-self (not (ebox-style--align-self-p value)))
('grid-auto-flow (not (ebox-style--grid-auto-flow-p value)))
('grid-track-list (not (ebox-style--grid-track-list-p value)))
('grid-track (not (ebox-style--grid-auto-track-p value)))
('order (not (ebox-style--order-p value)))
('flex-factor
(and (numberp value) (not (ebox-style--nonnegative-number-p value))))
('justify-self (not (ebox-style--justify-self-p value)))
('wrap-mode
(not (ebox-style--wrap-mode-p value)))))
(defun ebox-style--validate-declaration-values (declarations &optional tag)
"Return DECLARATIONS after static value and author TAG axis validation."
(cl-loop for (property value) on declarations by #'cddr
for schema = (ebox-style--property property)
for validator = (and schema (plist-get schema :validator))
for axis = (ebox-style--size-axis schema tag)
when (or (ebox-style--static-invalid-declaration-p validator value)
(not (ebox-style--size-value-valid-p schema value tag)))
do (if axis
(signal 'ebox-size-error
(list
(format "Invalid value for %S; allowed units: %S"
(plist-get schema :name)
(cdr (assq axis ebox-size-axis-units)))
value))
(error "ebox: invalid value for %S: %S"
(plist-get schema :name) value)))
declarations)
(defconst ebox-style--edge-projection-groups
'((:padding-top ebox/padding-top ebox/padding-block-start)
(:padding-right ebox/padding-right ebox/padding-inline-end)
(:padding-bottom ebox/padding-bottom ebox/padding-block-end)
(:padding-left ebox/padding-left ebox/padding-inline-start)
(:margin-top ebox/margin-top ebox/margin-block-start)
(:margin-right ebox/margin-right ebox/margin-inline-end)
(:margin-bottom ebox/margin-bottom ebox/margin-block-end)
(:margin-left ebox/margin-left ebox/margin-inline-start))
"Canonical facts sharing one fixed horizontal-tb/ltr physical edge.")
(defconst ebox-style--edge-projection-targets
(cl-loop for (target . properties) in ebox-style--edge-projection-groups
append (mapcar (lambda (property) (cons property target))
properties))
"Canonical edge property to fixed physical projection target.")
(defun ebox-style--validate-edge-projection-conflicts (declarations)
"Reject canonical DECLARATIONS that target one physical edge twice."
(let ((seen (make-hash-table :test #'eq)))
(cl-loop for (property _value) on declarations by #'cddr
for target = (alist-get property
ebox-style--edge-projection-targets)
when target
do (when-let* ((previous (gethash target seen)))
(signal 'ecss-invalid-declaration
(list :duplicate-edge-target target
:first-property previous
:second-property property)))
and do (puthash target property seen)))
declarations)
(defun ebox-style-compile-declarations (plist &optional strict)
"Compile Ebox PLIST aliases to ECSS schema declarations.
When STRICT is non-nil, reject properties outside the Ebox style domain."
(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)
(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)))
(if (not (eq cached missing))
;; Canonical declaration plists are immutable after compilation;
;; return a fresh top-level plist so callers own their list spine
;; without re-running ECSS's recursive boundary copier.
(copy-sequence cached)
(let ((canonical (cadr cache-key)))
(when strict
(cl-loop for (property _value) on plist by #'cddr
unless (or (ebox-style-schema-id property)
(ebox-style--custom-property-p property))
do (user-error "Unknown Ebox style property: %S"
property)))
(let ((compiled
(ebox-style--validate-declaration-values
(ebox-style--validate-edge-projection-conflicts
(ecss-expand-declarations ebox-style-schemas canonical)))))
(when (>= (hash-table-count ebox-style--declaration-cache)
ebox-style--declaration-cache-max-entries)
(clrhash ebox-style--declaration-cache))
(puthash cache-key compiled ebox-style--declaration-cache)
(copy-sequence compiled))))))
(defun ebox-style--form-property-allowed-p (tag property)
"Return non-nil when author TAG accepts PROPERTY metadata."
(let ((contexts (plist-get property :contexts))
(author-contexts (plist-get property :author-contexts)))
(if author-contexts
(memq tag author-contexts)
(or (memq tag contexts)
(and (not (eq tag 'text))
(or (memq 'box contexts) (memq 'item contexts)))))))
(defun ebox-style-validate-form-declarations (tag declarations)
"Validate canonical DECLARATIONS for Text/Box author TAG."
(unless (memq tag '(text box row column flex grid))
(error "Unknown Ebox author form: %S" tag))
(unless (ebox-style--valid-plist-p declarations)
(error "Ebox canonical declarations must be an even plist: %S"
declarations))
(let ((seen (make-hash-table :test #'eq)))
(cl-loop for (id _value) on declarations by #'cddr
for property = (ebox-style--property id)
unless (and property (eq id (plist-get property :id)))
do (error "Ebox declaration is not a canonical property ID: %S"
id)
do (when (gethash id seen)
(error "Duplicate canonical Ebox declaration: %S" id))
do (puthash id t seen)
unless (ebox-style--form-property-allowed-p tag property)
do (error "Ebox %S does not accept %S"
tag (plist-get property :name))))
(ebox-style--validate-declaration-values declarations tag)
declarations)
(defun ebox-style--validate-author-values (plist)
"Reject values in author PLIST outside the public property grammars.
An ECSS schema may use nil as an internal initial value, but nil is not an
author value unless the public grammar is boolean or an optional anchor."
(unless (ebox-style--valid-plist-p plist)
(user-error "Ebox style declarations must be an even property list"))
(cl-loop for (name value) on plist by #'cddr
for property = (ebox-style--property name)
when (and property
(null value)
(not (memq (plist-get property :validator)
'(boolean anchor))))
do (error "ebox: invalid value for %S: nil"
(plist-get property :name)))
plist)
(defun ebox-style-compile-form (tag plist)
"Compile evaluated author PLIST once and validate it for TAG."
(ebox-style-validate-form-declarations
tag
(ebox-style-compile-declarations
(ebox-style--validate-author-values plist) t)))
(defun ebox-style-declaration-properties (declarations predicate)
"Return canonical author properties in DECLARATIONS matching PREDICATE."
(cl-loop for (id value) on declarations by #'cddr
for property = (or (ebox-style--property id)
(error "Unknown canonical Ebox property: %S" id))
when (funcall predicate property)
append (list (plist-get property :name) value)))
(defun ebox-style-declarations-require-projection-p
(declarations predicate)
"Return non-nil when DECLARATIONS include facts outside PREDICATE."
(cl-loop for (id _value) on declarations by #'cddr
for property = (or (ebox-style--property id)
(error "Unknown canonical Ebox property: %S" id))
thereis (not (funcall predicate property))))
(defun ebox-style-merge-declarations (base overrides)
"Merge canonical BASE and OVERRIDES through ECSS property schemas."
(ecss-merge-declarations ebox-style-schemas base overrides))
(cl-defun ebox-style-add-rule
(selector declarations &key (origin 'author) layer scope)
"Add SELECTOR rule with Ebox DECLARATIONS to the isolated stylesheet.
ORIGIN, LAYER, and SCOPE use ECSS cascade semantics."
(ecss-stylesheet-add-rule
ebox-style-stylesheet ebox-style-schemas (ebox-style--selector selector)
(ebox-style-compile-declarations declarations t)
:origin origin :layer layer
:scope (and scope (ebox-style--selector scope))))
(defun ebox-style-compute-subject (subject declarations &optional parent-style)
"Compute SUBJECT style from DECLARATIONS and optional PARENT-STYLE via ECSS."
(let* ((closed-p
(and (not (ebox-style-cascade-active-p))
(ebox-style--closed-declarations-p declarations)))
(key (and closed-p (list declarations parent-style)))
(missing (make-symbol "ebox-closed-style-cache-missing"))
(cached (and key (gethash key ebox-style--closed-computed-cache
missing))))
(if (and key (not (eq cached missing)))
cached
(let ((style
(ecss-compute-style
ebox-style-schemas subject :declarations declarations
:stylesheet ebox-style-stylesheet :parent-style parent-style
:value-resolver #'tp-resolve-value)))
(when key
(when (>= (hash-table-count ebox-style--closed-computed-cache)
ebox-style--closed-computed-cache-limit)
(clrhash ebox-style--closed-computed-cache))
(puthash key style ebox-style--closed-computed-cache))
style))))
(defconst ebox-style--theme-delta-properties
'(ebox/color ebox/background-color)
"Canonical paint properties eligible for a declaration-only Theme delta.")
(defconst ebox-style--paint-delta-properties
'(ebox/color ebox/background-color
ebox/text-decoration-line ebox/text-decoration-color
ebox/text-decoration-style ebox/visibility
ebox/border-top-color ebox/border-right-color
ebox/border-bottom-color ebox/border-left-color)
"Canonical paint properties that may change without changing geometry.")
(defun ebox-style--without-theme-delta-properties (declarations)
"Return DECLARATIONS without the canonical Theme paint properties."
(cl-loop for (property value) on declarations by #'cddr
unless (memq property ebox-style--theme-delta-properties)
append (list property value)))
(defun ebox-style--without-paint-delta-properties (declarations)
"Return DECLARATIONS without canonical paint-only properties."
(cl-loop for (property value) on declarations by #'cddr
unless (memq property ebox-style--paint-delta-properties)
append (list property value)))
(defun ebox-style--paint-declarations-equivalent-p (old new)
"Return non-nil when OLD and NEW differ only in paint declarations.
All paint properties retain presence parity; layout, structure, and typography
declarations must remain identical."
(and (equal (ebox-style--without-paint-delta-properties old)
(ebox-style--without-paint-delta-properties new))
(cl-every
(lambda (property)
(= (if (plist-member old property) 1 0)
(if (plist-member new property) 1 0)))
ebox-style--paint-delta-properties)))
(defun ebox-style--theme-declarations-equivalent-p (old new)
"Return non-nil when OLD and NEW differ only in explicit Theme paint values.
Both paint properties must retain presence parity and an explicit foreground
winner is required so parent inherited geometry/paint cannot be stale."
(and (equal (ebox-style--without-theme-delta-properties old)
(ebox-style--without-theme-delta-properties new))
(cl-every
(lambda (property)
(= (if (plist-member old property) 1 0)
(if (plist-member new property) 1 0)))
ebox-style--theme-delta-properties)
(plist-member old 'ebox/color)
(plist-member new 'ebox/color)))
(defun ebox-style--inherited-style-signature (style)
"Return the inherited-value fingerprint of computed STYLE, or nil.
The fingerprint deliberately contains only values that can flow from a
parent into a descendant, plus custom properties which can feed any
`var' expression. A Theme delta may reuse a computed style only when this
fingerprint is unchanged between the old and candidate parent."
(when (ecss-computed-style-p style)
(list
:inherited
(mapcar (lambda (property)
(cons property (ecss-computed-style-value style property)))
ebox-style--inherited-properties)
:custom-properties (ecss-computed-style-custom-properties style))))
(defun ebox-style--theme-delta-computed
(style old-declarations new-declarations
&optional old-parent-style new-parent-style)
"Return a cheap STYLE copy for a safe Theme paint-only declaration delta.
OLD-DECLARATIONS and NEW-DECLARATIONS are the canonical ECSS plists.
OLD-PARENT-STYLE and NEW-PARENT-STYLE are the previous and candidate
computed parent styles, when the subject has an inherited parent.
The cascade selector facts and every non-Theme declaration must be unchanged;
both Theme properties must keep the same explicit-presence state, and the
inherited parent fingerprint must be unchanged. In that case only the
computed values and winner metadata for those properties change. Any other
shape returns nil so the caller uses the full ECSS computation."
(when (and (ecss-computed-style-p style)
(ebox-style--theme-declarations-equivalent-p
old-declarations new-declarations))
(let ((changed-p
(cl-some
(lambda (property)
(and (plist-member old-declarations property)
(plist-member new-declarations property)
(not (equal (plist-get old-declarations property)
(plist-get new-declarations property)))))
ebox-style--theme-delta-properties)))
(when (and changed-p
(equal (ebox-style--inherited-style-signature
old-parent-style)
(ebox-style--inherited-style-signature
new-parent-style)))
(let ((values (copy-tree (ecss-computed-style-values style)))
(active (copy-sequence
(ecss-computed-style-active-properties style)))
(specified (copy-sequence
(ecss-computed-style-specified-properties style))))
(dolist (property ebox-style--theme-delta-properties)
(let ((id (ebox-style-schema-id property)))
(when (plist-member new-declarations property)
(setf values
(plist-put values id
(copy-tree (plist-get new-declarations property))))
(cl-pushnew id active)
(cl-pushnew id specified))))
(ecss-computed-style-copy-with-values
style values :active-properties active :specified-properties specified))))))
(defun ebox-style--inherited-style-signature-without-theme-color (style)
"Return STYLE's inherited fingerprint excluding the Theme color value.
The caller uses this to prove that a parent update changes only the inherited
foreground value; fonts, custom properties, and all other inherited facts must
remain identical before a computed child style may be copied."
(when (ecss-computed-style-p style)
(list
:inherited
(mapcar
(lambda (property)
(cons property (ecss-computed-style-value style property)))
(delq 'ebox/color (copy-sequence ebox-style--inherited-properties)))
:custom-properties (ecss-computed-style-custom-properties style))))
(defun ebox-style--theme-parent-delta-computed
(style declarations old-parent-style new-parent-style)
"Return a copied STYLE when a static parent changes only Theme color.
For an explicit child color, STYLE is reusable unchanged. For an inherited
child color, the copied value follows NEW-PARENT-STYLE. OLD-PARENT-STYLE and
NEW-PARENT-STYLE must have identical non-color inherited fingerprints; any
provenance or parent-shape mismatch returns nil for normal ECSS computation."
(when (and (ecss-computed-style-p style)
(proper-list-p declarations)
(ecss-computed-style-p old-parent-style)
(ecss-computed-style-p new-parent-style)
(equal
(ebox-style--inherited-style-signature-without-theme-color
old-parent-style)
(ebox-style--inherited-style-signature-without-theme-color
new-parent-style)))
(if (ecss-computed-style-specified-p style 'ebox/color)
(ecss-computed-style-copy-with-values
style (ecss-computed-style-values style))
(when (and (equal (ecss-computed-style-value
style 'ebox/color)
(ecss-computed-style-value
old-parent-style 'ebox/color))
(not (equal (ecss-computed-style-value
old-parent-style 'ebox/color)
(ecss-computed-style-value
new-parent-style 'ebox/color))))
(let ((values (copy-tree (ecss-computed-style-values style))))
(setq values
(plist-put values 'ebox/color
(copy-tree
(ecss-computed-style-value
new-parent-style 'ebox/color))))
(ecss-computed-style-copy-with-values style values))))))
(defun ebox-style--theme-inherited-delta-computed
(style declarations old-parent-style new-parent-style)
"Return a copied STYLE when only inherited Theme color changed.
This compatibility entry point requires STYLE to inherit its color; explicit
child colors are handled by `ebox-style--theme-parent-delta-computed'."
(when (and (ecss-computed-style-p style)
(not (ecss-computed-style-specified-p style 'ebox/color)))
(ebox-style--theme-parent-delta-computed
style declarations old-parent-style new-parent-style)))
(defun ebox-style--public-computed-values (style)
"Return STYLE values using canonical public Ebox property names."
(let ((values (ecss-computed-style-values style)) result)
(dolist (property ebox-style--property-definitions)
(unless (plist-get property :shorthand)
(setq result
(plist-put result (plist-get property :name)
(plist-get values (plist-get property :id))))))
result))
(defun ebox-style--common-border-value (style component)
"Return common border COMPONENT in STYLE, or nil when sides differ."
(let ((values
(mapcar (lambda (side)
(plist-get style
(intern (format ":border-%s-%s" side component))))
ebox-style--border-sides)))
(when (cl-every (lambda (value) (equal value (car values))) (cdr values))
(car values))))
(defun ebox-style--add-border-aggregates (style)
"Add uniform border shorthand facts to public computed STYLE."
(dolist (component '(width style color) style)
(when-let* ((value (ebox-style--common-border-value style component)))
(setq style
(plist-put style (intern (format ":border-%s" component)) value)))))
(defun ebox-style-compute (plist)
"Return canonical public computed style for inline PLIST through ECSS."
(let ((style
(ecss-compute-style
ebox-style-schemas (ecss-subject-create :type "box")
:declarations (ebox-style-compile-declarations plist)
:rules nil :value-resolver #'tp-resolve-value)))
(ebox-style--add-border-aggregates
(ebox-style--public-computed-values style))))
(defun ebox-style-expand-shorthands (plist)
"Expand PLIST to canonical public longhands through ECSS schemas."
(ebox-style-compute plist))
(defun ebox-style-computed-active-p (style property)
"Return non-nil when computed STYLE actively contributes PROPERTY."
(memq (ebox-style-schema-id property)
(ecss-computed-style-active-properties style)))
(defun ebox-style-computed-value (style property)
"Return computed STYLE value for public Ebox PROPERTY."
(plist-get (ecss-computed-style-values style)
(ebox-style-schema-id property)))
(defvar ebox-style--computed-snapshot-cache
(make-hash-table :test #'eq :weakness 'key)
"Immutable Ebox projection snapshots keyed by computed style identity.")
(defun ebox-style--computed-snapshot (style &optional specified-p)
"Return one internal immutable ECSS snapshot for STYLE projection.
When SPECIFIED-P is non-nil, include declaration-winner facts. Ebox
materializes each immutable computed style once per snapshot shape; callers
must treat the returned values as read-only."
(let* ((entry
(or (gethash style ebox-style--computed-snapshot-cache)
(let ((created (vector nil nil)))
(puthash style created ebox-style--computed-snapshot-cache)
created)))
(index (if specified-p 1 0))
(snapshot (aref entry index)))
(or snapshot
(let* ((other (aref entry (- 1 index)))
(values (if other (nth 0 other)
(ecss-computed-style-values style)))
(active (if other (nth 1 other)
(ecss-computed-style-active-properties style)))
(created
(list values active
(and specified-p
(ecss-computed-style-specified-properties style)))))
(aset entry index created)
created))))
(defun ebox-style--snapshot-active-p (snapshot property)
"Return non-nil when SNAPSHOT actively contributes PROPERTY."
(memq (ebox-style-schema-id property) (nth 1 snapshot)))
(defun ebox-style--context-values
(style context &optional specified-only snapshot)
"Return public STYLE values for CONTEXT.
When SPECIFIED-ONLY is non-nil, exclude initial and inherited-only facts.
SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
(let* ((snapshot
(or snapshot
(ebox-style--computed-snapshot style specified-only)))
(values (nth 0 snapshot))
(active (nth 1 snapshot))
(specified
(and specified-only
(nth 2 snapshot)))
result)
(dolist (property ebox-style--property-definitions)
(let ((id (plist-get property :id)))
(when (and (not (plist-get property :shorthand))
(memq context (plist-get property :contexts))
(memq id active)
(or (not specified-only)
(memq id specified)))
(setq result
(plist-put result (plist-get property :name)
(plist-get values id))))))
result))
(defun ebox-style--project-edge-values (style values snapshot)
"Resolve logical/physical VALUES through STYLE's cross-property winners.
SNAPSHOT supplies the already-read computed value table."
(let* ((edge-names
(mapcar (lambda (entry)
(ebox-style-canonical-name (car entry)))
ebox-style--edge-projection-targets))
(result
(cl-loop for (property value) on values by #'cddr
unless (memq property edge-names)
append (list property value))))
(dolist (group ebox-style--edge-projection-groups result)
(let* ((target (car group))
(winner
(ecss-computed-style-precedence-winner style (cdr group))))
(when winner
(setq result
(plist-put result target
(plist-get (car snapshot) winner))))))))
(defun ebox-style--specified-property-p (style property)
"Return non-nil when STYLE PROPERTY has a declaration winner."
(ecss-computed-style-specified-p style property))
(defun ebox-style--node-declarations-from-record (node record)
"Return NODE's author declarations from borrowed immutable source RECORD."
(let ((declarations
(and record (ebox-source-record-declarations record))))
(if (memq (plist-get node :ebox-type) '(flex grid))
(ebox-style-merge-declarations
declarations
(plist-get (plist-get node :box) :ebox-style-overrides))
declarations)))
(defun ebox-style-node-declarations (source-index node)
"Return NODE's immutable author declarations from candidate SOURCE-INDEX.
Private generated layout nodes have no author source and therefore contribute
no declarations of their own."
(ebox-style--node-declarations-from-record
node
(when-let* ((handle (plist-get node :ebox-source-handle)))
(ebox-source--index-record source-index handle))))
(defun ebox-style-node-specified-value
(node name &optional missing source-index)
"Return NODE's canonical specified value for author property NAME.
Return MISSING when NAME is valid but not specified. Aliases resolve to the
same canonical property id, so callers never inspect backend plist fields.
NODE may be a canonical input with one root. A mounted raw node requires its
explicit generation SOURCE-INDEX; facts are never recovered from its handle."
(when (ebox-canonical-input-p node)
(setq source-index (ebox-canonical-input--source-index node)
node (ebox-canonical-input--single-root
node "ebox-style-node-specified-value")))
(unless (ebox-source-index-p source-index)
(error "Ebox specified-value lookup requires a source index"))
(let ((id (or (ebox-style-schema-id name)
(error "Unknown Ebox property: %S" name)))
(declarations (ebox-style-node-declarations source-index node)))
(if (plist-member declarations id)
(plist-get declarations id)
missing)))
(defun ebox-style--remove-style-properties (plist)
"Return PLIST without Ebox style or ECSS custom properties."
(cl-loop for (property value) on plist by #'cddr
unless (or (ebox-style--property property)
(ebox-style--custom-property-p property))
append (list property value)))
(defvar ebox-style--box-runtime-defaults)
(defun ebox-style--reset-box-engine-style (box)
"Reset BOX engine style fields to their declared defaults."
(cl-loop for (property value) on ebox--longhand by #'cddr
unless (memq property '(:content :scroll-offset))
do (plist-put box property (copy-tree value)))
(unless (eq (plist-get box :ebox-kind) 'text)
(cl-loop for (property value) on ebox-style--box-runtime-defaults by #'cddr
do (plist-put box property (copy-tree value))))
box)
(defun ebox-style--apply-engine-values (box values)
"Project public style VALUES into BOX engine fields."
(ebox-style--reset-box-engine-style box)
(let ((expanded
(ebox-style--expand-engine-plist
(ebox-style--without-font-values values))))
(cl-loop for (property value) on expanded by #'cddr
do (plist-put box property value)))
box)
(defvar ebox-style--box-engine-longhand-cache
(make-hash-table :test #'eq :weakness 'key)
"Computed-style keyed expanded engine longhands for canonical Box nodes.")
(defvar ebox-style--text-engine-longhand-cache
(make-hash-table :test #'eq :weakness 'key)
"Computed-style keyed expanded engine longhands for canonical Text nodes.")
(defun ebox-style--cached-engine-longhands (style text-p snapshot)
"Return STYLE's expanded engine longhands for TEXT-P and SNAPSHOT."
(let* ((cache (if text-p
ebox-style--text-engine-longhand-cache
ebox-style--box-engine-longhand-cache))
(missing (make-symbol "ebox-engine-longhands-missing"))
(cached (gethash style cache missing)))
(if (not (eq cached missing))
cached
(let ((expanded
(ebox-style--expand-engine-plist
(ebox-style--without-font-values
(if text-p
(ebox-style--text-values style snapshot)
(ebox-style--box-values style snapshot))))))
(puthash style expanded cache)
expanded))))
(defun ebox-style--apply-engine-longhands (box longhands)
"Project already expanded LONGHANDS into BOX engine fields."
(ebox-style--reset-box-engine-style box)
(cl-loop for (property value) on longhands by #'cddr
do (plist-put box property value))
box)
(defun ebox-style--box-values (style &optional snapshot)
"Return computed box values ready for Ebox engine projection."
(let* ((snapshot (or snapshot (ebox-style--computed-snapshot style)))
(values
(ebox-style--project-edge-values
style (ebox-style--context-values style 'box nil snapshot)
snapshot)))
(when (and (ebox-style--snapshot-active-p snapshot :color)
(null (plist-get values :color)))
(setq values
(plist-put values :color ebox-style--default-foreground)))
values))
(defun ebox-style--text-values (style snapshot)
"Return computed STYLE values owned by canonical Text."
(let ((values (ebox-style--context-values style 'text nil snapshot)))
(when (and (ebox-style--snapshot-active-p snapshot :color)
(null (plist-get values :color)))
(setq values
(plist-put values :color ebox-style--default-foreground)))
;; Wrap policy is authored only by Box, then inherited as Text's
;; measurement input without creating a second Text author property.
(when (ebox-style--snapshot-active-p snapshot :wrap-mode)
(setq values
(plist-put values :wrap-mode
(plist-get (car snapshot) 'ebox/wrap-mode))))
values))
(defconst ebox-style--font-property-names
'(:font-family :font-size :font-weight :font-style)
"Canonical font inputs owned by one FontMetricsProjection.")
(defun ebox-style--without-font-values (values)
"Return computed VALUES without canonical font inputs."
(cl-loop for (property value) on values by #'cddr
unless (memq property ebox-style--font-property-names)
append (list property value)))
(defun ebox-style--closed-inheritance-values (style &optional snapshot)
"Return STYLE's immutable canonical inheritance projection."
(let* ((missing (make-symbol "ebox-inheritance-missing"))
(cached (gethash style ebox-style--closed-inheritance-cache missing)))
(if (not (eq cached missing))
cached
(let* ((snapshot (or snapshot (ebox-style--computed-snapshot style)))
(values (ebox-style--box-values style snapshot))
result)
(dolist (property '(:color :wrap-mode))
(when (plist-member values property)
(setq result
(plist-put result property (plist-get values property)))))
(puthash style result ebox-style--closed-inheritance-cache)
result))))
(defun ebox-style-apply-closed-inheritance (node style &optional snapshot)
"Apply only closed computed inheritance from STYLE to fresh canonical NODE."
(let ((values (ebox-style--closed-inheritance-values style snapshot)))
(plist-put node :ebox-computed-style style)
(cl-loop for (property value) on values by #'cddr
do (plist-put node property value)))
node)
(defun ebox-style--text-paint-owner-p (node)
"Return non-nil when NODE materializes inherited text paint itself.
Canonical Text owns typography and foreground paint. Canonical Box retains
those computed facts for inheritance but materializes only box paint such as
background and borders. Legacy runtime boxes keep their established behavior
until their private encoding is removed."
(not (eq (plist-get node :ebox-kind) 'box)))
(defconst ebox-style--container-wrapper-neutral-properties
'(:width :height :box-sizing :color
:font-family :font-size :font-weight :font-style)
"Container properties that do not require an internal visual wrapper.")
(defun ebox-style--container-wrapper-needed-p (style &optional snapshot)
"Return non-nil when STYLE needs a visual container wrapper."
(cl-loop for (property value)
on (ebox-style--context-values style 'box t snapshot) by #'cddr
thereis (and value
(not (memq
property
ebox-style--container-wrapper-neutral-properties)))))
(defun ebox-style--new-container-wrapper ()
"Return a stable internal box for container-owned box paint."
(let ((wrapper
(ebox-node-factory--create-from-properties
:content "" :wrap-mode nil :key 'ebox/style-wrapper)))
(plist-put wrapper :ebox-style-wrapper t)
(plist-put wrapper :ebox-style-generated-wrapper t)
wrapper))
(defun ebox-style--container-wrapper (node style &optional snapshot)
"Return NODE wrapper required by STYLE, updating NODE ownership."
(let ((wrapper (plist-get node :box))
(needed (ebox-style--container-wrapper-needed-p style snapshot)))
(cond
(needed
(unless wrapper
(setq wrapper (ebox-style--new-container-wrapper))
(plist-put node :box wrapper))
wrapper)
((and wrapper (plist-get wrapper :ebox-style-generated-wrapper))
(plist-put node :box nil)
nil)
(t wrapper))))
(defun ebox-style--apply-container-wrapper (node style &optional snapshot)
"Apply computed STYLE to NODE's internal visual wrapper."
(when-let* ((wrapper (ebox-style--container-wrapper node style snapshot)))
(ebox-style--apply-engine-values
wrapper
(ebox-style--box-values style snapshot))
(unless (ebox-style--specified-property-p style 'ebox/wrap-mode)
(plist-put wrapper :wrap-mode nil))))
(defun ebox-style--apply-container (node style &optional snapshot)
"Apply computed STYLE to Flex or Grid NODE."
(let* ((type (plist-get node :ebox-type))
(metadata
(ebox-style--remove-style-properties (plist-get node :raw-props)))
(values
(append (ebox-style--context-values style 'box nil snapshot)
(ebox-style--context-values style type nil snapshot)))
(raw (append metadata values)))
(plist-put node :raw-props raw)
(plist-put node :props
(if (eq type 'flex)
(ebox--flex-normalize-container-props raw)
raw))
(when (eq type 'grid)
(plist-put node :ebox-grid-config
(ebox-grid--normalize-config-props raw)))
(ebox-style--apply-container-wrapper node style snapshot)))
(defun ebox-style--layout-config-values (style kind snapshot)
"Return specified STYLE values owned by KIND's typed LayoutConfig."
(let ((names (ebox-layout-config-property-names kind)))
(cl-loop for (property value)
on (ebox-style--context-values style kind t snapshot)
by #'cddr
when (memq property names)
append (list property value))))
(defun ebox-style--merge-layout-config-values (base overrides)
"Return detached BASE LayoutConfig values updated by OVERRIDES."
(let ((result (copy-tree base)))
(cl-loop for (property value) on overrides by #'cddr
do (setq result (plist-put result property value)))
result))
(defun ebox-style--normalize-layout-config-overrides (kind overrides)
"Return specified KIND OVERRIDES in canonical LayoutConfig form."
(when overrides
(let* ((config (ebox-layout-config-for-form kind overrides))
(normalized (ebox-layout-config-props config))
(names (cl-loop for (name _value) on overrides by #'cddr
collect name)))
(cl-loop for name in names
append (list name (plist-get normalized name))))))
(defun ebox-style--apply-typed-layout-config (node style snapshot)
"Project computed STYLE child-layout facts into typed Box NODE."
(let* ((current (plist-get node :ebox-layout-config))
(kind (and current (ebox-layout-config-kind current)))
(current-properties
(and current
(eq kind (ebox-layout-config-kind current))
(ebox-layout-config-props current)))
(overrides
(and (memq kind '(flex grid))
(ebox-style--layout-config-values style kind snapshot)))
(properties
(ebox-style--merge-layout-config-values
current-properties
(ebox-style--normalize-layout-config-overrides kind overrides))))
(when overrides
(plist-put node :ebox-layout-config
(ebox-layout-config-with-props current properties)))
(plist-put node :display
(list (or (car-safe (plist-get node :display)) 'block)
(if (eq kind 'normal) 'flow kind)))))
(defun ebox-style--delete-node-property (node property)
"Delete PROPERTY from NODE in place while preserving NODE identity."
(when (eq (car node) property)
(error "Ebox internal node property cannot occupy plist head: %S" property))
(let ((tail node))
(while (cddr tail)
(if (eq (caddr tail) property)
(setcdr (cdr tail) (cddddr tail))
(setq tail (cddr tail)))))
node)
(defconst ebox-style--item-projection-properties
'(:order :flex-grow :flex-shrink :flex-basis :align-self
:grid-column :grid-row :justify-self)
"Canonical node fields owned by computed parent participation style.")
(defun ebox-style--transfer-node-property (old new property)
"Transfer style-owned PROPERTY presence and value from OLD to NEW."
(if (plist-member old property)
(plist-put new property (plist-get old property))
(ebox-style--delete-node-property new property)))
(defun ebox-style--computed-projection-transferable-p (old new style)
"Return non-nil when OLD's STYLE projection is exact for canonical NEW.
Display axes and typed layout config combine style with the fresh node's base
structure. They must already agree; otherwise the ordinary projection path
recomputes that combination from NEW."
(and (memq (plist-get new :ebox-kind) '(text box))
(eq (plist-get old :ebox-kind) (plist-get new :ebox-kind))
(eq (plist-get old :ebox-type) (plist-get new :ebox-type))
(eq style (plist-get old :ebox-computed-style))
(equal (plist-get old :display) (plist-get new :display))
(equal (plist-get old :ebox-layout-config)
(plist-get new :ebox-layout-config))))
(defun ebox-style--transfer-computed-projection (old new style)
"Transfer OLD's materialized STYLE projection onto fresh canonical NEW.
OLD and NEW must have the same canonical kind and source-stable computed
style. Content, children, source/runtime identity, and scroll state remain
owned by NEW; only fields owned by the unchanged computed style are shared."
(unless (ebox-style--computed-projection-transferable-p old new style)
(error "Ebox computed projection transfer requires source-stable peers"))
(plist-put new :ebox-computed-style style)
(cl-loop for (property _value) on ebox--longhand by #'cddr
unless (memq property '(:content :scroll-offset))
do (ebox-style--transfer-node-property old new property))
(when (eq (plist-get new :ebox-kind) 'box)
(cl-loop for (property _value) on ebox-style--box-runtime-defaults by #'cddr
do (ebox-style--transfer-node-property old new property)))
(dolist (property ebox-style--item-projection-properties)
(ebox-style--transfer-node-property old new property))
new)
(defun ebox-style--apply-item (node style &optional snapshot)
"Apply computed Flex/Grid item fields from STYLE to NODE."
(let ((values (ebox-style--context-values style 'item nil snapshot)))
(dolist (property ebox-style--item-projection-properties)
(ebox-style--delete-node-property node property))
(cl-loop for (property value) on values by #'cddr
do (plist-put node property value)))
node)
(defun ebox-style--apply-display-axes (node style)
"Apply computed public outer participation from STYLE to NODE."
(let ((outer-specified-p
(ebox-style--specified-property-p style 'ebox/outer)))
(if (eq (plist-get node :ebox-kind) 'text)
(progn
(when outer-specified-p
(error "Ebox Text is fixed inline and rejects display axes"))
(plist-put node :display '(inline flow)))
(when outer-specified-p
(let ((display (or (plist-get node :display) '(block flow))))
(plist-put node :display
(list (ebox-style-computed-value style :outer)
(cadr display)))))))
node)
(defun ebox-style-apply-computed (node style &optional snapshot)
"Apply ECSS computed STYLE consequences to semantic Ebox NODE."
(unless (ecss-computed-style-p style)
(signal 'wrong-type-argument (list 'ecss-computed-style-p style)))
(let* ((type (plist-get node :ebox-type))
(config (plist-get node :ebox-layout-config))
(kind (and config (ebox-layout-config-kind config)))
(snapshot (or snapshot (ebox-style--computed-snapshot style t))))
(plist-put node :ebox-computed-style style)
(ebox-style--apply-display-axes node style)
(cond
((or (memq (plist-get node :ebox-kind) '(text box))
(eq type 'box))
(ebox-style--apply-engine-longhands
node
(ebox-style--cached-engine-longhands
style (eq (plist-get node :ebox-kind) 'text) snapshot))
(when (eq (plist-get node :ebox-kind) 'box)
(ebox-style--apply-typed-layout-config node style snapshot)))
((memq (or kind type) '(flex grid))
(ebox-style--apply-container node style snapshot)))
(ebox-style--apply-item node style snapshot))
node)
(defun ebox-style-dirty-kind (name)
"Return dirty kind for canonical property or alias NAME."
(when-let* ((property (ebox-style--property name)))
(plist-get property :dirty-kind)))
(defun ebox-style-signature (computed-style groups)
"Return deterministic signature for COMPUTED-STYLE filtered by GROUPS."
(let (entries)
(while computed-style
(let* ((property-name (pop computed-style))
(value (pop computed-style))
(property (ebox-style--property property-name))
(group (and property (plist-get property :signature))))
(when (and property
(or (null groups)
(memq group groups)
(memq (plist-get property :group) groups)))
(push (cons property-name value) entries))))
(setq entries
(sort entries
(lambda (a b)
(string< (symbol-name (car a))
(symbol-name (car b))))))
(apply #'append
(mapcar (lambda (entry)
(list (car entry) (cdr entry)))
entries))))
(defun ebox-style--convert (value converter)
"Project VALUE with CONVERTER without resolving contextual size units.
Layout owns used pixel/line values. Computed style and its cached engine
projection retain expressions so viewport, container, and font changes can
resolve them again. Numeric zero remains valid for private generated edges."
(pcase converter
((or 'pixel 'size-pixel 'border-pixel 'line) (or value 0))
('color (ebox-style--parse-color value))
(_ value)))
(defconst ebox-style--nonnegative-edge-longhands
'(:padding-left-pixel :padding-right-pixel
:padding-top-height :padding-bottom-height
:margin-left-pixel :margin-right-pixel
:margin-top-height :margin-bottom-height
:border-left-pixel :border-right-pixel
:border-top-pixel :border-bottom-pixel)
"Engine edge longhands that cannot represent negative geometry.")
(defun ebox-style--validate-edge-longhands (plist)
"Return PLIST after rejecting negative normalized engine edges."
(let ((tail plist))
(while tail
(let ((property (pop tail))
(value (pop tail)))
(when (and (memq property ebox-style--nonnegative-edge-longhands)
(ebox-style--negative-numeric-size-p value))
(error "ebox: %S cannot be negative: %S" property value))))
plist))
(defun ebox-style--apply-border-used-widths (plist)
"Return PLIST with border widths resolved against their styles.
Canonical width remains in the computed style; private engine pixels are the
used geometry and become zero when the corresponding style is not `solid'."
(dolist (side '(top right bottom left) plist)
(let ((width-key (intern (format ":border-%s-pixel" side)))
(style-key (intern (format ":border-%s-style" side))))
(when (or (plist-member plist width-key)
(plist-member plist style-key))
(unless (eq (plist-get plist style-key) 'solid)
(setq plist (plist-put plist width-key 0)))))))
(defun ebox-style--split-value (value mode n)
"Split VALUE according to MODE for N targets."
(pcase mode
('trbl (ebox-style--split-trbl value))
('pair (ebox-style--split-pair value))
('wsc (ebox-style--split-wsc value))
('same (make-list n value))
(_ (make-list n value))))
(defun ebox-style--get-ebox-rule (property)
"Return Ebox private engine projection rule for PROPERTY."
(or (when-let* ((definition (ebox-style--property property)))
(plist-get definition :engine-projection))
(cdr (assq property ebox-style--internal-engine-projection-rules))))
(defun ebox-style--expand-engine-property (property value)
"Expand public Ebox PROPERTY with VALUE to engine longhands."
(let ((rule (ebox-style--get-ebox-rule property)))
(cond
((null rule) (list property value))
((plist-get rule :expand)
(let* ((targets (plist-get rule :expand))
(mode (plist-get rule :mode))
(parts (ebox-style--split-value value mode (length targets))))
(cl-mapcan #'ebox-style--expand-engine-property targets parts)))
((plist-member rule :to)
(let ((target (plist-get rule :to))
(converter (plist-get rule :conv)))
(if target
(list target (ebox-style--convert value converter))
nil)))
(t (list property value)))))
(defun ebox-style--engine-projection-targets (property)
"Return private engine keys potentially projected from PROPERTY."
(if-let* ((definition (ebox-style--property property)))
(copy-sequence (plist-get definition :engine-targets))
(let ((rule
(cdr (assq property ebox-style--internal-engine-projection-rules))))
(if (and rule (plist-member rule :to))
(when-let* ((target (plist-get rule :to))) (list target))
(list property)))))
(defun ebox-style--expand-engine-plist (plist)
"Project canonical PLIST into private Ebox runtime fields."
(let (expanded)
(while plist
(let ((property (pop plist))
(value (pop plist)))
(setq expanded
(nconc expanded
(ebox-style--expand-engine-property property value)))))
(ebox-style--apply-border-used-widths
(ebox-style--validate-edge-longhands expanded))))
(defconst ebox-style--box-runtime-defaults
(ebox-style--expand-engine-plist
(cl-loop for property in ebox-style--property-definitions
when (plist-get property :runtime-default)
append (list (plist-get property :name)
(copy-tree (plist-get property :initial)))))
"Schema-owned defaults for additional Box runtime style fields.
Reset and source-stable transfer share this registration. Canonical Text
does not materialize these box-only placement facts.")
(defun ebox-style--border-delta-value
(declarations full-declarations style name)
"Return border NAME from delta, complete declarations, STYLE, or initial."
(let* ((definition (ebox-style--property name))
(id (plist-get definition :id)))
(cond
((plist-member declarations id) (plist-get declarations id))
((plist-member full-declarations id) (plist-get full-declarations id))
((and style (ecss-computed-style-p style))
(ebox-style-computed-value style name))
(t (copy-tree (plist-get definition :initial))))))
(defun ebox-style--expand-engine-delta
(declarations full-declarations current-style)
"Project canonical DECLARATIONS against CURRENT-STYLE.
Ordinary properties remain change-local. Border width/style pairs are
completed from FULL-DECLARATIONS and CURRENT-STYLE before deriving their used
engine pixels."
(let (ordinary affected-sides)
(cl-loop for (id value) on declarations by #'cddr
for definition = (ebox-style--property id)
for name = (plist-get definition :name)
for side = (cl-loop for candidate in '(top right bottom left)
when (memq name
(list
(intern (format
":border-%s-width"
candidate))
(intern (format
":border-%s-style"
candidate))))
return candidate)
if side do (cl-pushnew side affected-sides)
else do (setq ordinary (append ordinary (list name value))))
(let ((expanded (ebox-style--expand-engine-plist ordinary)))
(dolist (side (nreverse affected-sides) expanded)
(let ((width-name (intern (format ":border-%s-width" side)))
(style-name (intern (format ":border-%s-style" side))))
(setq expanded
(append
expanded
(ebox-style--expand-engine-plist
(list width-name
(ebox-style--border-delta-value
declarations full-declarations
current-style width-name)
style-name
(ebox-style--border-delta-value
declarations full-declarations
current-style style-name))))))))))
(provide 'ebox-style)
;;; ebox-style.el ends here