tp/tp-style.el
Kinneyzhang 905d3523ac feat(tp): add unified convenience APIs
Expose one-shot string and buffer styling plus reactive range watches over the same schema, mutation, binding, and retained surface core.

Verification: 712 ERT tests; 92 doctests; shuffled seed 20260806; compile-all WERROR=t.
2026-08-06 03:31:50 +08:00

927 lines
39 KiB
EmacsLisp

;;; tp-style.el --- Schema-driven text property cascade -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Geekinney
;; Author: Geekinney (kinneyzhang666@gmail.com)
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3 of
;; the License, or (at your option) any later version.
;;; Commentary:
;; Pure property schema, structured selector, and cascade computation for TP.
;; This module owns no buffers, markers, mounts, or reactive subscriptions.
;;; Code:
(require 'cl-lib)
(require 'seq)
(require 'tp-core)
(define-error 'tp-style-error "TP style error")
(define-error 'tp-invalid-property-schema "Invalid TP property schema"
'tp-style-error)
(define-error 'tp-invalid-style "Invalid TP style declaration"
'tp-style-error)
(define-error 'tp-invalid-selector "Invalid TP selector" 'tp-style-error)
(cl-defstruct (tp-property-schema
(:constructor tp--make-property-schema))
"Schema governing one namespaced cascade property."
id initial inherits normalizer validator equality merge projector shorthand)
(cl-defstruct (tp-subject (:constructor tp--make-subject))
"Generic selector subject independent of any rendering consumer."
type id classes attributes state parent children)
(cl-defstruct (tp-computed-style (:constructor tp--make-computed-style))
"Computed values, active properties, custom properties, and provenance."
values custom-properties active-properties provenance)
(cl-defstruct (tp--computed-source (:constructor tp--make-computed-source))
function)
(cl-defstruct (tp--wide (:constructor tp--make-wide)) kind)
(cl-defstruct (tp--important (:constructor tp--make-important)) value)
(cl-defstruct (tp--var-ref (:constructor tp--make-var-ref))
name fallback-present-p fallback)
(cl-defstruct (tp--style-rule (:constructor tp--make-style-rule))
selector declarations origin layer scope specificity source-order)
(cl-defstruct (tp--candidate (:constructor tp--make-candidate))
property value origin important layer specificity scope-distance
source-order declaration-order selector)
(defconst tp--style-origin-order
'(default theme package author inline runtime user)
"Cascade origins ordered from weakest to strongest.")
(defconst tp--wide-kinds '(initial inherit unset revert revert-layer)
"Supported CSS-wide value kinds.")
(defconst tp--style-all-rules (make-symbol "tp-all-style-rules"))
(defconst tp--style-invalid (make-symbol "tp-invalid-style-value"))
(defconst tp--style-absent (make-symbol "tp-absent-style-value"))
(defvar tp--property-schemas (make-hash-table :test #'eq))
(defvar tp--property-schema-order nil)
(defvar tp--named-styles (make-hash-table :test #'eq))
(defvar tp--stylesheet-rules nil)
(defvar tp--cascade-layers nil)
(defvar tp--style-source-order 0)
(defconst tp--style-runtime-properties
'(tp-name tp-layers tp-meta tp-hidden tp-text)
"Runtime-only properties excluded from declarative text styles.")
(defun tp--canonical-property-id-p (id)
"Return non-nil when ID is a namespaced property symbol."
(and (symbolp id)
(let ((name (symbol-name id)))
(and (string-match-p "/" name)
(not (string-prefix-p "/" name))
(not (string-suffix-p "/" name))))))
(defun tp--custom-property-p (property)
"Return non-nil when PROPERTY names a custom cascade variable."
(and (symbolp property)
(string-prefix-p "--" (symbol-name property))))
(defun tp--callable-option-p (value)
"Return non-nil when VALUE is nil or callable."
(or (null value) (functionp value)))
(defun tp--validate-schema-functions (options)
"Validate callable fields in schema OPTIONS."
(dolist (key '(:normalizer :validator :equality :merge
:projector :shorthand))
(unless (tp--callable-option-p (plist-get options key))
(signal 'tp-invalid-property-schema
(list key (plist-get options key))))))
(defun tp--schema-option (options key default)
"Return KEY from OPTIONS when present, otherwise DEFAULT."
(if (plist-member options key) (plist-get options key) default))
(defun tp--build-property-schema (id options)
"Build and validate a property schema for ID from OPTIONS."
(unless (tp--canonical-property-id-p id)
(signal 'tp-invalid-property-schema (list :property id)))
(tp--validate-schema-functions options)
(tp--make-property-schema
:id id
:initial (plist-get options :initial)
:inherits (and (plist-get options :inherits) t)
:normalizer (tp--schema-option options :normalizer #'identity)
:validator (tp--schema-option options :validator (lambda (_value) t))
:equality (tp--schema-option options :equality #'equal)
:merge (tp--schema-option options :merge (lambda (_old new) new))
:projector (plist-get options :projector)
:shorthand (plist-get options :shorthand)))
;;;###autoload
(defun tp-define-property (id &rest options)
"Register namespaced property ID using schema OPTIONS.
OPTIONS support :initial, :inherits, :normalizer, :validator, :equality,
:merge, :projector, and :shorthand. Registration is atomic."
(let ((schema (tp--build-property-schema id options)))
(unless (gethash id tp--property-schemas)
(setq tp--property-schema-order
(append tp--property-schema-order (list id))))
(puthash id schema tp--property-schemas)
schema))
(defun tp-property-schema (id)
"Return the registered property schema for ID, or nil."
(gethash id tp--property-schemas))
(defun tp-text-property-id (property)
"Return the canonical `text/' schema id for Emacs PROPERTY."
(unless (symbolp property)
(signal 'wrong-type-argument (list 'symbolp property)))
(intern (format "text/%s" property)))
(defun tp--text-property-inherits-p (property)
"Return non-nil when PROPERTY inherits in TP's text domain."
(memq property '(face font-lock-face)))
(defun tp--text-property-merge-function (property)
"Return the schema merge function for Emacs PROPERTY."
(if (memq property tp-face-properties)
#'tp--merge-face-values
(lambda (_old new) new)))
(defun tp-register-text-property (property)
"Register and return a canonical schema for Emacs PROPERTY."
(let ((id (tp-text-property-id property)))
(or (tp-property-schema id)
(tp-define-property
id :initial nil :inherits (tp--text-property-inherits-p property)
:equality #'equal :merge (tp--text-property-merge-function property)
:projector (lambda (value) (list property value))))))
(defun tp--register-default-text-properties ()
"Register schemas for TP's known native Emacs properties."
(dolist (property tp--builtin-text-properties)
(tp-register-text-property property)))
(defun tp-text-declarations (properties)
"Convert raw Emacs PROPERTIES to canonical text declarations."
(unless (tp--declaration-list-p properties)
(signal 'tp-invalid-style (list :text-properties properties)))
(cl-loop for (property value) on properties by #'cddr
unless (memq property tp--style-runtime-properties)
append (list (tp-property-schema-id
(tp-register-text-property property))
(copy-tree value))))
(defun tp-style-reset-rules ()
"Clear stylesheet rules and their cascade-layer order."
(setq tp--stylesheet-rules nil
tp--cascade-layers nil
tp--style-source-order 0))
(defun tp-style-reset ()
"Clear all TP style schemas, definitions, rules, and ordering state."
(clrhash tp--property-schemas)
(clrhash tp--named-styles)
(setq tp--property-schema-order nil)
(tp-style-reset-rules)
(tp--register-default-text-properties))
(defun tp-undefine-style (name)
"Remove named style NAME and return nil."
(remhash name tp--named-styles)
nil)
;;;###autoload
(defun tp-computed (function)
"Return an explicit computed value source wrapping FUNCTION."
(unless (functionp function)
(signal 'wrong-type-argument (list 'functionp function)))
(tp--make-computed-source :function function))
;;;###autoload
(defun tp-wide-value (kind)
"Return a tagged CSS-wide value of KIND."
(unless (memq kind tp--wide-kinds)
(signal 'tp-invalid-style (list :wide-value kind)))
(tp--make-wide :kind kind))
;;;###autoload
(defun tp-important (value)
"Return VALUE tagged as an important declaration."
(tp--make-important :value value))
;;;###autoload
(defun tp-var (name &rest fallback)
"Return a custom-property reference to NAME with optional FALLBACK."
(unless (tp--custom-property-p name)
(signal 'tp-invalid-style (list :custom-property name)))
(when (> (length fallback) 1)
(signal 'wrong-number-of-arguments (list 'tp-var (+ 1 (length fallback)))))
(tp--make-var-ref :name name
:fallback-present-p (and fallback t)
:fallback (car fallback)))
(cl-defun tp-subject-create (&key type id classes attributes state parent children)
"Create a generic cascade subject from TYPE, ID, and metadata.
CLASSES and STATE are symbol lists. ATTRIBUTES is an alist. PARENT and
CHILDREN must be TP subjects when present."
(when (and parent (not (tp-subject-p parent)))
(signal 'wrong-type-argument (list 'tp-subject-p parent)))
(let ((subject (tp--make-subject
:type type :id id :classes (copy-sequence classes)
:attributes (copy-tree attributes)
:state (copy-sequence state) :parent parent)))
(tp-subject-set-children subject children)))
(defun tp-subject-set-children (subject children)
"Replace SUBJECT's CHILDREN and establish their parent links."
(unless (tp-subject-p subject)
(signal 'wrong-type-argument (list 'tp-subject-p subject)))
(dolist (child children)
(unless (tp-subject-p child)
(signal 'wrong-type-argument (list 'tp-subject-p child))))
(dolist (old-child (tp-subject-children subject))
(when (eq (tp-subject-parent old-child) subject)
(setf (tp-subject-parent old-child) nil)))
(setf (tp-subject-children subject) (copy-sequence children))
(dolist (child children)
(setf (tp-subject-parent child) subject))
subject)
(defun tp--subject-attribute-cell (subject name)
"Return SUBJECT's attribute cell for NAME."
(assq name (tp-subject-attributes subject)))
(defun tp--subject-previous-siblings (subject)
"Return SUBJECT's preceding siblings in document order."
(when-let ((parent (tp-subject-parent subject)))
(let ((siblings (tp-subject-children parent)) result)
(while (and siblings (not (eq (car siblings) subject)))
(push (pop siblings) result))
(nreverse result))))
(defun tp--selector-form-p (selector kind arity)
"Return non-nil when SELECTOR is KIND with ARITY arguments."
(and (consp selector) (eq (car selector) kind)
(= (length (cdr selector)) arity)))
(defun tp--validate-selector-list (selectors)
"Validate every selector in SELECTORS."
(and selectors (cl-every #'tp--selector-valid-p selectors)))
(defun tp--selector-valid-p (selector)
"Return non-nil when SELECTOR is a valid structured selector."
(pcase (and (consp selector) (car selector))
((or :type :id :class :state)
(tp--selector-form-p selector (car selector) 1))
(:attr (memq (length (cdr selector)) '(1 2)))
((or :and :is :where :not)
(tp--validate-selector-list (cdr selector)))
((or :descendant :child :adjacent :sibling)
(and (tp--selector-form-p selector (car selector) 2)
(tp--validate-selector-list (cdr selector))))
(:universal (null (cdr selector)))
(_ nil)))
(defun tp--validate-selector (selector)
"Signal an error unless SELECTOR is structurally valid."
(unless (tp--selector-valid-p selector)
(signal 'tp-invalid-selector (list selector)))
selector)
(defun tp--selector-match-attribute (selector subject)
"Return whether attribute SELECTOR matches SUBJECT."
(let ((cell (tp--subject-attribute-cell subject (nth 1 selector))))
(and cell
(or (= (length selector) 2)
(equal (cdr cell) (nth 2 selector))))))
(defun tp--selector-match-descendant (selector subject)
"Return whether descendant SELECTOR matches SUBJECT."
(and (tp-selector-match-p (nth 2 selector) subject)
(cl-loop for parent = (tp-subject-parent subject)
then (tp-subject-parent parent)
while parent
thereis (tp-selector-match-p (nth 1 selector) parent))))
(defun tp--selector-match-adjacent (selector subject)
"Return whether adjacent SELECTOR matches SUBJECT."
(let ((siblings (tp--subject-previous-siblings subject)))
(and siblings
(tp-selector-match-p (nth 1 selector) (car (last siblings)))
(tp-selector-match-p (nth 2 selector) subject))))
(defun tp--selector-match-sibling (selector subject)
"Return whether sibling SELECTOR matches SUBJECT."
(and (tp-selector-match-p (nth 2 selector) subject)
(cl-some (lambda (sibling)
(tp-selector-match-p (nth 1 selector) sibling))
(tp--subject-previous-siblings subject))))
(defun tp--selector-match-valid (selector subject)
"Match already validated SELECTOR against SUBJECT."
(pcase (car selector)
(:universal t)
(:type (equal (nth 1 selector) (tp-subject-type subject)))
(:id (equal (nth 1 selector) (tp-subject-id subject)))
(:class (memq (nth 1 selector) (tp-subject-classes subject)))
(:state (memq (nth 1 selector) (tp-subject-state subject)))
(:attr (tp--selector-match-attribute selector subject))
(:and (cl-every (lambda (item) (tp-selector-match-p item subject))
(cdr selector)))
(:is (cl-some (lambda (item) (tp-selector-match-p item subject))
(cdr selector)))
(:where (cl-some (lambda (item) (tp-selector-match-p item subject))
(cdr selector)))
(:not (not (cl-some (lambda (item) (tp-selector-match-p item subject))
(cdr selector))))
(:descendant (tp--selector-match-descendant selector subject))
(:child (and (tp-selector-match-p (nth 2 selector) subject)
(when-let ((parent (tp-subject-parent subject)))
(tp-selector-match-p (nth 1 selector) parent))))
(:adjacent (tp--selector-match-adjacent selector subject))
(:sibling (tp--selector-match-sibling selector subject))))
;;;###autoload
(defun tp-selector-match-p (selector subject)
"Return non-nil when structured SELECTOR matches SUBJECT."
(unless (tp-subject-p subject)
(signal 'wrong-type-argument (list 'tp-subject-p subject)))
(tp--validate-selector selector)
(tp--selector-match-valid selector subject))
(defun tp--specificity-add (left right)
"Add specificity triples LEFT and RIGHT."
(cl-mapcar #'+ left right))
(defun tp--specificity-max (values)
"Return the lexicographically greatest specificity in VALUES."
(cl-reduce (lambda (left right)
(if (tp--specificity-greater-p left right) left right))
values :initial-value '(0 0 0)))
(defun tp--specificity-list-sum (selectors)
"Return the combined specificity of SELECTORS."
(cl-reduce #'tp--specificity-add selectors
:key #'tp-selector-specificity
:initial-value '(0 0 0)))
(defun tp-selector-specificity (selector)
"Return SELECTOR specificity as an (ID CLASS TYPE) list."
(tp--validate-selector selector)
(pcase (car selector)
(:id '(1 0 0))
((or :class :attr :state) '(0 1 0))
(:type '(0 0 1))
((or :universal :where) '(0 0 0))
(:and (tp--specificity-list-sum (cdr selector)))
((or :is :not)
(tp--specificity-max (mapcar #'tp-selector-specificity
(cdr selector))))
((or :descendant :child :adjacent :sibling)
(tp--specificity-list-sum (cdr selector)))))
(defun tp--declaration-list-p (declarations)
"Return non-nil when DECLARATIONS is an even property/value list."
(and (listp declarations) (zerop (% (length declarations) 2))))
(defun tp--validate-declaration-property (property)
"Return PROPERTY when it names a registered or custom property."
(unless (or (tp--custom-property-p property)
(gethash property tp--property-schemas))
(signal 'tp-invalid-style (list :unknown-property property)))
property)
(defun tp--unwrap-important (value)
"Return VALUE and whether it carries an important tag."
(if (tp--important-p value)
(cons (tp--important-value value) t)
(cons value nil)))
(defun tp--tag-expanded-important (declarations important)
"Tag expanded DECLARATIONS as IMPORTANT when requested."
(if (not important)
declarations
(cl-loop for (property value) on declarations by #'cddr
append (list property (tp-important value)))))
(defun tp--expand-declaration (property value)
"Expand one PROPERTY VALUE declaration into canonical longhands."
(tp--validate-declaration-property property)
(let ((schema (gethash property tp--property-schemas)))
(if-let ((expander (and schema (tp-property-schema-shorthand schema))))
(pcase-let* ((`(,raw . ,important) (tp--unwrap-important value))
(expanded (funcall expander raw)))
(unless (tp--declaration-list-p expanded)
(signal 'tp-invalid-style (list :shorthand property expanded)))
(cl-loop for (longhand _value) on expanded by #'cddr
do (tp--validate-declaration-property longhand)
when (tp-property-schema-shorthand
(gethash longhand tp--property-schemas))
do (signal 'tp-invalid-style
(list :nested-shorthand property longhand)))
(tp--tag-expanded-important expanded important))
(list property value))))
(defun tp--expand-declarations (declarations)
"Validate and expand DECLARATIONS into canonical longhands."
(unless (tp--declaration-list-p declarations)
(signal 'tp-invalid-style (list :declarations declarations)))
(cl-loop for (property value) on declarations by #'cddr
append (tp--expand-declaration property value)))
;;;###autoload
(defun tp-define-style (name declarations)
"Define named style NAME from DECLARATIONS and return NAME."
(unless (symbolp name)
(signal 'tp-invalid-style (list :style-name name)))
(let ((expanded (tp--expand-declarations declarations)))
(puthash name (copy-tree expanded) tp--named-styles)
name))
(defun tp-style-declarations (name)
"Return a defensive copy of named style NAME declarations."
(when-let ((declarations (gethash name tp--named-styles)))
(copy-tree declarations)))
(defun tp--validate-rule-origin (origin)
"Return ORIGIN when it is a registered cascade origin."
(unless (memq origin tp--style-origin-order)
(signal 'tp-invalid-style (list :origin origin)))
origin)
(defun tp--register-cascade-layer (layer)
"Register LAYER in first-seen order when non-nil."
(when (and layer (not (memq layer tp--cascade-layers)))
(setq tp--cascade-layers (append tp--cascade-layers (list layer)))))
;;;###autoload
(cl-defun tp-stylesheet-add-rule
(selector declarations &key (origin 'author) layer scope)
"Add a structured SELECTOR rule with DECLARATIONS.
ORIGIN defaults to `author'. LAYER is ordered by first appearance. SCOPE,
when non-nil, is a selector that must match the subject or an ancestor."
(tp--validate-selector selector)
(when scope (tp--validate-selector scope))
(tp--validate-rule-origin origin)
(let ((expanded (tp--expand-declarations declarations)))
(tp--register-cascade-layer layer)
(let ((rule (tp--make-style-rule
:selector (copy-tree selector)
:declarations (copy-tree expanded)
:origin origin :layer layer :scope (copy-tree scope)
:specificity (tp-selector-specificity selector)
:source-order (cl-incf tp--style-source-order))))
(setq tp--stylesheet-rules
(append tp--stylesheet-rules (list rule)))
rule)))
(defun tp--scope-distance (scope subject)
"Return distance from SUBJECT to matching SCOPE, or nil."
(if (null scope)
most-positive-fixnum
(cl-loop for current = subject then (tp-subject-parent current)
for distance from 0
while current
when (tp-selector-match-p scope current) return distance)))
(defun tp--rule-matches-p (rule subject)
"Return non-nil if RULE matches SUBJECT."
(and (tp-selector-match-p (tp--style-rule-selector rule) subject)
(numberp (tp--scope-distance (tp--style-rule-scope rule) subject))))
(defun tp--candidate-from-entry (rule property value subject declaration-order)
"Create a candidate from RULE PROPERTY VALUE for SUBJECT.
DECLARATION-ORDER is the property's position within RULE."
(pcase-let ((`(,raw . ,important) (tp--unwrap-important value)))
(tp--make-candidate
:property property :value raw
:origin (tp--style-rule-origin rule) :important important
:layer (tp--style-rule-layer rule)
:specificity (tp--style-rule-specificity rule)
:scope-distance (tp--scope-distance (tp--style-rule-scope rule) subject)
:source-order (tp--style-rule-source-order rule)
:declaration-order declaration-order
:selector (tp--style-rule-selector rule))))
(defun tp--rule-candidates (rule subject)
"Return all property candidates from matching RULE for SUBJECT."
(when (tp--rule-matches-p rule subject)
(cl-loop for (property value) on (tp--style-rule-declarations rule)
by #'cddr
for declaration-order from 0
collect (tp--candidate-from-entry
rule property value subject declaration-order))))
(defun tp--inline-candidates (declarations)
"Return inline candidates for DECLARATIONS."
(when declarations
(cl-loop for (property value) on (tp--expand-declarations declarations)
by #'cddr
for declaration-order from 0
collect
(pcase-let ((`(,raw . ,important) (tp--unwrap-important value)))
(tp--make-candidate
:property property :value raw :origin 'inline
:important important :layer nil :specificity '(1 0 0)
:scope-distance most-positive-fixnum
:source-order (1+ tp--style-source-order)
:declaration-order declaration-order
:selector :inline)))))
(defun tp--collect-candidates (subject declarations rules)
"Collect matching candidates for SUBJECT, DECLARATIONS, and RULES."
(append
(cl-loop for rule in rules append (tp--rule-candidates rule subject))
(tp--inline-candidates declarations)))
(defun tp--specificity-greater-p (left right)
"Return non-nil when specificity LEFT is greater than RIGHT."
(catch 'result
(cl-mapc (lambda (a b)
(cond ((> a b) (throw 'result t))
((< a b) (throw 'result nil))))
left right)
nil))
(defun tp--origin-rank (origin)
"Return precedence rank for ORIGIN."
(or (cl-position origin tp--style-origin-order) -1))
(defun tp--layer-rank (candidate)
"Return cascade layer rank for CANDIDATE."
(let ((layer (tp--candidate-layer candidate))
(important (tp--candidate-important candidate)))
(cond
((and important (null layer)) -1000000)
((null layer) 1000000)
(important (- (or (cl-position layer tp--cascade-layers) 0)))
(t (or (cl-position layer tp--cascade-layers) 0)))))
(defun tp--compare-number (left right)
"Compare LEFT and RIGHT, returning 1, -1, or 0."
(cond ((> left right) 1) ((< left right) -1) (t 0)))
(defun tp--candidate-ranks (candidate)
"Return ordered scalar ranks for CANDIDATE."
(list (if (tp--candidate-important candidate) 1 0)
(tp--origin-rank (tp--candidate-origin candidate))
(tp--layer-rank candidate)))
(defun tp--rank-list-comparison (left right)
"Compare numeric rank lists LEFT and RIGHT."
(catch 'comparison
(cl-mapc (lambda (a b)
(let ((value (tp--compare-number a b)))
(unless (zerop value) (throw 'comparison value))))
left right)
0))
(defun tp--candidate-higher-p (left right)
"Return non-nil when candidate LEFT outranks RIGHT."
(let ((rank (tp--rank-list-comparison
(tp--candidate-ranks left) (tp--candidate-ranks right))))
(cond
((not (zerop rank)) (> rank 0))
((not (equal (tp--candidate-specificity left)
(tp--candidate-specificity right)))
(tp--specificity-greater-p (tp--candidate-specificity left)
(tp--candidate-specificity right)))
((/= (tp--candidate-scope-distance left)
(tp--candidate-scope-distance right))
(< (tp--candidate-scope-distance left)
(tp--candidate-scope-distance right)))
((/= (tp--candidate-source-order left)
(tp--candidate-source-order right))
(> (tp--candidate-source-order left)
(tp--candidate-source-order right)))
(t (> (tp--candidate-declaration-order left)
(tp--candidate-declaration-order right))))))
(defun tp--group-candidates (candidates)
"Group CANDIDATES by property in a hash table."
(let ((table (make-hash-table :test #'eq)))
(dolist (candidate candidates)
(let ((property (tp--candidate-property candidate)))
(puthash property (cons candidate (gethash property table)) table)))
(maphash (lambda (property values)
(puthash property
(sort values #'tp--candidate-higher-p) table))
table)
table))
(defun tp--skip-reverted-origin (candidates winner)
"Remove WINNER's origin and importance group from CANDIDATES."
(seq-remove
(lambda (candidate)
(and (eq (tp--candidate-origin candidate)
(tp--candidate-origin winner))
(eq (tp--candidate-important candidate)
(tp--candidate-important winner))))
candidates))
(defun tp--skip-reverted-layer (candidates winner)
"Remove WINNER's layer group from CANDIDATES."
(seq-remove
(lambda (candidate)
(and (eq (tp--candidate-origin candidate)
(tp--candidate-origin winner))
(eq (tp--candidate-important candidate)
(tp--candidate-important winner))
(eq (tp--candidate-layer candidate)
(tp--candidate-layer winner))))
candidates))
(defun tp--evaluate-computed-source (value)
"Evaluate VALUE only when it is an explicit computed source."
(if (tp--computed-source-p value)
(funcall (tp--computed-source-function value))
value))
(defun tp--parent-values (parent-style)
"Return computed values plist from PARENT-STYLE."
(cond ((tp-computed-style-p parent-style)
(tp-computed-style-values parent-style))
((listp parent-style) parent-style)
(t nil)))
(defun tp--parent-custom-properties (parent-style)
"Return custom property plist from PARENT-STYLE."
(when (tp-computed-style-p parent-style)
(tp-computed-style-custom-properties parent-style)))
(defun tp--parent-property-active-p (parent-style property)
"Return non-nil when PARENT-STYLE actively contributes PROPERTY."
(cond
((tp-computed-style-p parent-style)
(memq property (tp-computed-style-active-properties parent-style)))
((listp parent-style) (and (plist-member parent-style property) t))))
(defun tp--property-default-value (schema parent-style)
"Return SCHEMA's inherited or initial value using PARENT-STYLE."
(let* ((property (tp-property-schema-id schema))
(parent-values (tp--parent-values parent-style)))
(if (and (tp-property-schema-inherits schema)
(plist-member parent-values property))
(plist-get parent-values property)
(tp-property-schema-initial schema))))
(defun tp--wide-default-value (wide schema parent-style)
"Resolve non-revert WIDE value for SCHEMA using PARENT-STYLE."
(pcase (tp--wide-kind wide)
('initial (tp-property-schema-initial schema))
('inherit
(let ((values (tp--parent-values parent-style))
(property (tp-property-schema-id schema)))
(if (plist-member values property)
(plist-get values property)
(tp-property-schema-initial schema))))
('unset (if (tp-property-schema-inherits schema)
(tp--wide-default-value (tp-wide-value 'inherit)
schema parent-style)
(tp-property-schema-initial schema)))))
(defun tp--custom-raw-table (candidate-table parent-style)
"Build raw custom properties from CANDIDATE-TABLE and PARENT-STYLE."
(let ((table (make-hash-table :test #'eq)))
(cl-loop for (property value) on (tp--parent-custom-properties parent-style)
by #'cddr do (puthash property value table))
(maphash
(lambda (property candidates)
(when (tp--custom-property-p property)
(let ((selected (tp--select-custom-candidate candidates table)))
(if (eq selected tp--style-absent)
(remhash property table)
(puthash property selected table)))))
candidate-table)
table))
(defun tp--select-custom-candidate (candidates inherited-table)
"Select raw custom value from CANDIDATES and INHERITED-TABLE."
(let ((remaining candidates) selected done)
(while (and remaining (not done))
(let* ((candidate (pop remaining))
(value (tp--evaluate-computed-source
(tp--candidate-value candidate))))
(if (not (tp--wide-p value))
(setq selected value done t)
(pcase (tp--wide-kind value)
('revert (setq remaining
(tp--skip-reverted-origin remaining candidate)))
('revert-layer (setq remaining
(tp--skip-reverted-layer remaining candidate)))
((or 'inherit 'unset)
(let ((old (gethash (tp--candidate-property candidate)
inherited-table tp--style-absent)))
(setq selected old done t)))
('initial (setq selected tp--style-absent done t))))))
(if done selected
(gethash (tp--candidate-property (car candidates))
inherited-table tp--style-absent))))
(defun tp--resolve-var-fallback (reference raw resolved stack)
"Resolve REFERENCE fallback using RAW, RESOLVED, and STACK."
(if (tp--var-ref-fallback-present-p reference)
(tp--resolve-variable-value (tp--var-ref-fallback reference)
raw resolved stack)
tp--style-invalid))
(defun tp--resolve-custom-property (name raw resolved stack)
"Resolve custom property NAME using RAW, RESOLVED, and STACK."
(let ((memo (gethash name resolved tp--style-absent)))
(cond
((not (eq memo tp--style-absent)) memo)
((memq name stack) tp--style-invalid)
(t
(let ((value (gethash name raw tp--style-absent)))
(if (eq value tp--style-absent)
tp--style-invalid
(let ((answer (tp--resolve-variable-value
value raw resolved (cons name stack))))
(puthash name answer resolved)
answer)))))))
(defun tp--resolve-variable-value (value raw resolved stack)
"Resolve custom references in VALUE using RAW, RESOLVED, and STACK."
(if (not (tp--var-ref-p value))
value
(let ((answer (tp--resolve-custom-property
(tp--var-ref-name value) raw resolved stack)))
(if (eq answer tp--style-invalid)
(tp--resolve-var-fallback value raw resolved stack)
answer))))
(defun tp--resolved-custom-properties (raw)
"Return resolved custom properties plist from RAW table."
(let ((resolved (make-hash-table :test #'eq)) names result)
(maphash
(lambda (name _value) (push name names)) raw)
(dolist (name (sort names
(lambda (left right)
(string< (symbol-name left) (symbol-name right)))))
(let ((value (tp--resolve-custom-property name raw resolved nil)))
(unless (eq value tp--style-invalid)
(setq result (plist-put result name value)))))
result))
(defun tp--resolve-property-value (value schema parent-style custom)
"Resolve VALUE for SCHEMA using PARENT-STYLE and CUSTOM properties."
(setq value (tp--evaluate-computed-source value))
(cond
((and (tp--wide-p value)
(memq (tp--wide-kind value) '(initial inherit unset)))
(tp--wide-default-value value schema parent-style))
((tp--var-ref-p value)
(let ((raw (make-hash-table :test #'eq))
(resolved (make-hash-table :test #'eq)))
(cl-loop for (name item) on custom by #'cddr
do (puthash name item raw))
(tp--resolve-variable-value value raw resolved nil)))
(t value)))
(defun tp--normalize-property-value (schema value)
"Normalize and validate VALUE for SCHEMA, or return invalid sentinel."
(if (eq value tp--style-invalid)
value
(let ((normalized (funcall (tp-property-schema-normalizer schema) value)))
(if (funcall (tp-property-schema-validator schema) normalized)
normalized
tp--style-invalid))))
(defun tp--property-candidate-value (candidate schema parent-style custom)
"Resolve CANDIDATE for SCHEMA using PARENT-STYLE and CUSTOM."
(tp--normalize-property-value
schema
(tp--resolve-property-value (tp--candidate-value candidate)
schema parent-style custom)))
(defun tp--candidate-provenance (candidate)
"Return public provenance plist for CANDIDATE."
(if (null candidate)
'(:selector :initial :origin default)
(list :selector (copy-tree (tp--candidate-selector candidate))
:origin (tp--candidate-origin candidate)
:important (and (tp--candidate-important candidate) t)
:layer (tp--candidate-layer candidate)
:specificity (copy-sequence (tp--candidate-specificity candidate))
:scope-distance (tp--candidate-scope-distance candidate)
:source-order (tp--candidate-source-order candidate)
:declaration-order (tp--candidate-declaration-order candidate))))
(defun tp--resolve-property-candidates (schema candidates parent-style custom)
"Resolve SCHEMA from ordered CANDIDATES, PARENT-STYLE, and CUSTOM."
(let ((remaining candidates) winner value)
(while (and remaining (null winner))
(let* ((candidate (pop remaining))
(raw (tp--evaluate-computed-source
(tp--candidate-value candidate))))
(cond
((and (tp--wide-p raw) (eq (tp--wide-kind raw) 'revert))
(setq remaining (tp--skip-reverted-origin remaining candidate)))
((and (tp--wide-p raw) (eq (tp--wide-kind raw) 'revert-layer))
(setq remaining (tp--skip-reverted-layer remaining candidate)))
(t
(setf (tp--candidate-value candidate) raw)
(setq winner candidate
value (tp--property-candidate-value
candidate schema parent-style custom))))))
(unless winner
(setq value (tp--normalize-property-value
schema (tp--property-default-value schema parent-style))))
(when (eq value tp--style-invalid)
(setq value (tp--normalize-property-value
schema (tp--property-default-value schema parent-style))))
(cons value winner)))
(defun tp--compute-property-values (candidate-table parent-style custom provenance-p)
"Compute values from CANDIDATE-TABLE, PARENT-STYLE, and CUSTOM.
When PROVENANCE-P is non-nil, also retain winning declaration facts."
(let (values active provenance)
(dolist (property tp--property-schema-order)
(let ((schema (gethash property tp--property-schemas)))
(unless (tp-property-schema-shorthand schema)
(pcase-let ((`(,value . ,winner)
(tp--resolve-property-candidates
schema (gethash property candidate-table)
parent-style custom)))
(setq values (plist-put values property value))
(when (or winner value
(and (tp-property-schema-inherits schema)
(tp--parent-property-active-p
parent-style property)))
(push property active))
(when provenance-p
(setq provenance
(plist-put provenance property
(tp--candidate-provenance winner))))))))
(list values (nreverse active) provenance)))
;;;###autoload
(cl-defun tp-compute-style
(subject &key declarations (rules tp--style-all-rules)
parent-style provenance)
"Compute a deterministic style for SUBJECT.
DECLARATIONS are inline values. RULES defaults to the registered stylesheet;
explicit nil disables stylesheet rules. PARENT-STYLE may be a computed style
or values plist. When PROVENANCE is non-nil, winner metadata is retained."
(unless (tp-subject-p subject)
(signal 'wrong-type-argument (list 'tp-subject-p subject)))
(let* ((active-rules (if (eq rules tp--style-all-rules)
tp--stylesheet-rules rules))
(candidate-table
(tp--group-candidates
(tp--collect-candidates subject declarations active-rules)))
(raw-custom (tp--custom-raw-table candidate-table parent-style))
(custom (tp--resolved-custom-properties raw-custom)))
(pcase-let ((`(,values ,active ,facts)
(tp--compute-property-values
candidate-table parent-style custom provenance)))
(tp--make-computed-style
:values values :custom-properties custom
:active-properties active :provenance facts))))
(defun tp--projected-value (schema values active)
"Project SCHEMA from computed VALUES when its PROPERTY is ACTIVE."
(when-let ((projector (tp-property-schema-projector schema)))
(let* ((property (tp-property-schema-id schema))
(value (plist-get values property)))
(when (or value (memq property active))
(funcall projector value)))))
;;;###autoload
(defun tp-project-style (style)
"Project computed STYLE into final direct Emacs text properties."
(unless (tp-computed-style-p style)
(signal 'wrong-type-argument (list 'tp-computed-style-p style)))
(let ((values (tp-computed-style-values style))
(active (tp-computed-style-active-properties style))
result)
(dolist (property tp--property-schema-order)
(let* ((schema (gethash property tp--property-schemas))
(projected (and (not (tp-property-schema-shorthand schema))
(tp--projected-value schema values active))))
(when projected
(unless (tp--declaration-list-p projected)
(signal 'tp-invalid-style (list :projection property projected)))
(setq result (tp--deep-merge-plist result projected)))))
result))
(defun tp--project-text-declarations (declarations)
"Project native text property DECLARATIONS through the style core."
(tp-project-style
(tp-compute-style
(tp-subject-create :type 'text)
:declarations (tp-text-declarations declarations)
:rules nil)))
(tp--register-default-text-properties)
(provide 'tp-style)
;;; tp-style.el ends here