etaf/etaf-component.el
2026-08-24 02:08:53 +08:00

262 lines
10 KiB
EmacsLisp

;;; etaf-component.el --- ETAF Component definitions -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; Components have one public definition boundary. A stateless Component
;; declares `:view'; a stateful Component declares `:setup' which runs once
;; per retained instance and returns a render function. Both forms produce
;; the same normalized View tree and share props, slots, styles, and Context.
;;; Code:
(require 'cl-lib)
(require 'etaf-view)
(define-error 'etaf-component-definition-error
"Invalid ETAF Component definition"
'etaf-view-error)
(cl-defstruct (etaf--slot-content (:constructor etaf--slot-content-create))
"Internal caller ownership metadata for one slot entry."
owner-component-id children)
(defvar etaf--raw-slot-read-p nil
"Set while a Component render uses public raw slot accessors.")
(defun etaf--component-definition-error (format-string &rest arguments)
"Signal a Component definition error from FORMAT-STRING and ARGUMENTS."
(signal 'etaf-component-definition-error
(list (apply #'format format-string arguments))))
(defun etaf--parse-component-props (arguments)
"Return prop names from Component argument declaration ARGUMENTS."
(unless (proper-list-p arguments)
(etaf--component-definition-error
"Component prop declaration must be a proper list: %S" arguments))
(let ((tail arguments)
prop-names)
(when tail
(unless (eq (pop tail) '&key)
(etaf--component-definition-error
"Component props must use (&key NAME ...), got %S" arguments)))
(dolist (entry tail)
(unless (symbolp entry)
(etaf--component-definition-error
"Component props must be symbols without defaults: %S" entry))
(when (or (keywordp entry) (memq entry '(nil t)))
(etaf--component-definition-error
"Component prop name must be an ordinary symbol: %S" entry))
(when (memq entry prop-names)
(etaf--component-definition-error
"Duplicate Component prop: %S" entry))
(push entry prop-names))
(nreverse prop-names)))
(defun etaf--component-prop-key (name)
"Return the public keyword used to pass prop NAME."
(if (keywordp name)
name
(intern (concat ":" (symbol-name name)))))
(defun etaf-current-prop (name)
"Return current Component prop NAME.
NAME is normally an ordinary symbol used by the `:setup' lexical shorthand.
The function is also useful to code that deliberately avoids that shorthand."
(plist-get etaf--current-component-props
(etaf--component-prop-key name)))
;;;###autoload
(defun etaf-component-set-styles (name styles)
"Replace the static style form for Component NAME with STYLES.
STYLES must use the same `(styles (SELECTOR PROPERTY VALUE ...) ...)'
contract accepted by `etaf-define-component'. This small runtime authoring
hook is useful to tools which reload a stylesheet without redefining the
Component itself, such as a source/preview playground. The next mount or
render observes the replacement; existing mounted runtimes are not flushed
implicitly."
(let ((spec (and (boundp 'etaf--view-registry)
(gethash name etaf--view-registry))))
(unless (and (symbolp name) (etaf--component-spec-p spec))
(etaf--component-definition-error
"Unknown ETAF Component: %S" name))
(etaf--validate-styles-form styles name)
(setf (etaf--component-spec-styles spec) styles)
styles))
(defun etaf-component-styles (name)
"Return the current static style form for Component NAME."
(let ((spec (and (boundp 'etaf--view-registry)
(gethash name etaf--view-registry))))
(unless (and (symbolp name) (etaf--component-spec-p spec))
(etaf--component-definition-error
"Unknown ETAF Component: %S" name))
(etaf--component-spec-styles spec)))
;;;###autoload
(defun etaf-component-redefine-run (function)
"Run FUNCTION while allowing intentional Component redefinition.
Normal duplicate Component definitions remain errors. Authoring tools may
use this narrow public boundary after disposing the old Runtime, so hot reload
does not depend on ETAF's private registry flag."
(unless (functionp function)
(signal 'wrong-type-argument (list 'functionp function)))
(let ((etaf--allow-component-redefinition t))
(funcall function)))
(defun etaf-current-slots ()
"Return the current Component's normalized slot alist."
(setq etaf--raw-slot-read-p t)
(mapcar (lambda (entry)
(cons (car entry)
(if (etaf--slot-content-p (cdr entry))
(etaf--slot-content-children (cdr entry))
(cdr entry))))
etaf--current-component-slots))
(defun etaf-current-slot (name &optional fallback)
"Return the child list for slot NAME, or FALLBACK when it is absent."
(setq etaf--raw-slot-read-p t)
(let ((entry (assq name etaf--current-component-slots)))
(if entry
(if (etaf--slot-content-p (cdr entry))
(etaf--slot-content-children (cdr entry))
(cdr entry))
fallback)))
(defun etaf--component-prop-symbol-macros (props)
"Return symbol macros that read current Component PROPS."
(mapcar (lambda (prop)
`(,prop (etaf-current-prop ',prop)))
props))
(defun etaf--validate-styles-form (form name)
"Validate static Component styles FORM for Component NAME."
(when form
(unless (and (consp form) (eq (car form) 'styles))
(etaf--component-definition-error
"Component %S :styles must be a (styles RULE ...) form" name))
(dolist (rule (cdr form))
(unless (and (consp rule) (stringp (car rule)))
(etaf--component-definition-error
"Component %S style rules need string selectors: %S" name rule))
(let ((attributes (cdr rule)))
(unless (zerop (% (length attributes) 2))
(etaf--component-definition-error
"Component %S style rule has an incomplete property pair: %S"
name rule))
(while attributes
(let ((key (pop attributes)))
(pop attributes)
(unless (keywordp key)
(etaf--component-definition-error
"Component %S style properties must be keywords: %S"
name key)))))))
form)
;;;###autoload
(defmacro etaf-define-component (name arguments &rest clauses)
"Define Component NAME from prop ARGUMENTS and CLAUSES.
The definition boundary is intentionally small:
(etaf-define-component NAME (&key PROPS)
:view VIEW
:styles (styles (SELECTOR ATTR ...)))
or:
(etaf-define-component NAME (&key PROPS)
:setup SETUP
:styles (styles (SELECTOR ATTR ...)))
`:view' is rendered for every update. `:setup' runs once per retained
Component instance and must return a zero-argument render function. View
forms do not use quote; ordinary Elisp belongs in `expr :value'."
(declare (indent 2) (debug defun))
(unless (symbolp name)
(etaf--component-definition-error
"Component name must be a symbol: %S" name))
(let ((docstring (when (stringp (car clauses)) (pop clauses)))
view-form
setup-form
styles-form
saw-view
saw-setup
saw-styles)
(while clauses
(let ((keyword (pop clauses)))
(unless (keywordp keyword)
(etaf--component-definition-error
"Expected a Component definition keyword, got %S" keyword))
(unless clauses
(etaf--component-definition-error
"Component %S keyword %S has no value" name keyword))
(pcase keyword
(:view
(when saw-view
(etaf--component-definition-error
"Component %S has duplicate :view" name))
(setq view-form (pop clauses) saw-view t))
(:setup
(when saw-setup
(etaf--component-definition-error
"Component %S has duplicate :setup" name))
(setq setup-form (pop clauses) saw-setup t))
(:styles
(when saw-styles
(etaf--component-definition-error
"Component %S has duplicate :styles" name))
(setq styles-form (pop clauses) saw-styles t))
(_
(etaf--component-definition-error
"Unknown Component definition keyword %S" keyword)))))
(when (and saw-view saw-setup)
(etaf--component-definition-error
"Component %S must choose :view or :setup, not both" name))
(unless (or saw-view saw-setup)
(etaf--component-definition-error
"Component %S requires exactly one of :view or :setup" name))
(let* ((props (etaf--parse-component-props arguments))
(styles-form (etaf--validate-styles-form styles-form name))
(definition-symbol
(intern (format "%s--etaf-component-definition" name)))
(render-lambda
(when saw-view
`(lambda (etaf--component-props etaf--component-slots)
(let ((etaf--current-component-props etaf--component-props)
(etaf--current-component-slots etaf--component-slots)
(etaf--current-component-instance
etaf--current-component-instance))
(cl-symbol-macrolet
,(etaf--component-prop-symbol-macros props)
,(etaf--compile-view-form view-form :projection))))))
(setup-lambda
(when saw-setup
`(lambda (etaf--component-props etaf--component-slots)
(let ((etaf--current-component-props etaf--component-props)
(etaf--current-component-slots etaf--component-slots))
(cl-symbol-macrolet
,(etaf--component-prop-symbol-macros props)
,setup-form))))))
`(progn
(defconst ,definition-symbol
(etaf--component-spec-create
:name ',name
:props ',props
:render ,render-lambda
:setup ,setup-lambda
:styles ',styles-form)
,(or docstring
(format "Definition of ETAF Component `%s'." name)))
(etaf--register-component ',name ,definition-symbol)
',name))))
(provide 'etaf-component)
;;; etaf-component.el ends here