tp/tp-style.el
Kinneyzhang 1195297011 feat(tp): isolate stylesheet rule domains
Give independent consumers their own rules, cascade layer ordering, and source-order counters so packages such as Ebox cannot pollute TP's default stylesheet or each other. Match generic class and state tokens by value and document caller-owned stylesheet lifecycle.

Verified: make clean; make test (728/728); make compile WERROR=t; checkdoc tp-style.el; git diff --check; Ebox make test against ../tp.
2026-08-06 13:59:08 +08:00

976 lines
41 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-stylesheet
(:constructor tp-stylesheet-create ())
(:conc-name tp--stylesheet-))
"Independent ordered rule and cascade-layer collection."
rules layers (source-order 0))
(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 layer-rank scope specificity source-order)
(cl-defstruct (tp--candidate (:constructor tp--make-candidate))
property value origin important layer layer-rank 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 (&optional stylesheet)
"Clear rules and cascade-layer order in optional STYLESHEET.
With nil STYLESHEET, reset TP's process-wide default stylesheet."
(if stylesheet
(progn
(unless (tp-stylesheet-p stylesheet)
(signal 'wrong-type-argument
(list 'tp-stylesheet-p stylesheet)))
(setf (tp--stylesheet-rules stylesheet) nil
(tp--stylesheet-layers stylesheet) nil
(tp--stylesheet-source-order stylesheet) 0))
(setq tp--stylesheet-rules nil
tp--cascade-layers nil
tp--style-source-order 0)))
(defun tp-style-reset ()
"Clear TP's schemas, named styles, and default stylesheet state.
Caller-owned stylesheet instances retain their rules until reset explicitly."
(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 token lists compared with `equal'. 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 (member (nth 1 selector) (tp-subject-classes subject)))
(:state (member (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 stylesheet)
"Register LAYER in first-seen order for optional STYLESHEET.
Return LAYER's zero-based rank, or nil for an unlayered rule."
(when layer
(let ((layers (if stylesheet
(tp--stylesheet-layers stylesheet)
tp--cascade-layers)))
(unless (memq layer layers)
(setq layers (append layers (list layer)))
(if stylesheet
(setf (tp--stylesheet-layers stylesheet) layers)
(setq tp--cascade-layers layers)))
(cl-position layer layers))))
(defun tp--next-style-source-order (stylesheet)
"Increment and return source order for optional STYLESHEET."
(if stylesheet
(cl-incf (tp--stylesheet-source-order stylesheet))
(cl-incf tp--style-source-order)))
(defun tp--append-stylesheet-rule (rule stylesheet)
"Append RULE to optional STYLESHEET and return RULE."
(if stylesheet
(setf (tp--stylesheet-rules stylesheet)
(append (tp--stylesheet-rules stylesheet) (list rule)))
(setq tp--stylesheet-rules
(append tp--stylesheet-rules (list rule))))
rule)
;;;###autoload
(cl-defun tp-stylesheet-add-rule
(selector declarations &key (origin 'author) layer scope stylesheet)
"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.
STYLESHEET isolates rules and layer order from TP's default stylesheet."
(tp--validate-selector selector)
(when scope (tp--validate-selector scope))
(tp--validate-rule-origin origin)
(when (and stylesheet (not (tp-stylesheet-p stylesheet)))
(signal 'wrong-type-argument (list 'tp-stylesheet-p stylesheet)))
(let* ((expanded (tp--expand-declarations declarations))
(layer-rank (tp--register-cascade-layer layer stylesheet))
(rule (tp--make-style-rule
:selector (copy-tree selector)
:declarations (copy-tree expanded)
:origin origin :layer layer :layer-rank layer-rank
:scope (copy-tree scope)
:specificity (tp-selector-specificity selector)
:source-order (tp--next-style-source-order stylesheet))))
(tp--append-stylesheet-rule rule stylesheet)))
(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)
:layer-rank (tp--style-rule-layer-rank 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 :layer-rank 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))
(rank (tp--candidate-layer-rank candidate))
(important (tp--candidate-important candidate)))
(cond
((and important (null layer)) -1000000)
((null layer) 1000000)
(important (- (or rank 0)))
(t (or rank 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;
an isolated stylesheet instance selects only its rules, and 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
(if (tp-stylesheet-p rules)
(tp--stylesheet-rules 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