aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/build-theme.el
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/build-theme.el')
-rw-r--r--scripts/theme-studio/build-theme.el218
1 files changed, 134 insertions, 84 deletions
diff --git a/scripts/theme-studio/build-theme.el b/scripts/theme-studio/build-theme.el
index c869dea18..4432ef57c 100644
--- a/scripts/theme-studio/build-theme.el
+++ b/scripts/theme-studio/build-theme.el
@@ -6,13 +6,13 @@
;; The last link in the theme-studio pipeline: turn a theme.json exported by
;; the tool (see scripts/theme-studio/README.md and
-;; docs/design/theme-studio-package-faces-spec.org) into a single,
+;; docs/specs/theme-studio-package-faces-spec-doing.org) into a single,
;; self-contained, loadable Emacs deftheme written to themes/<name>-theme.el.
;;
;; Four tiers come out of the JSON:
-;; - default -- background from assignments.bg, foreground from .p
-;; - syntax -- assignments.<cat> -> font-lock / tree-sitter faces, with
-;; the bold / italic category sets applied
+;; - default -- background from syntax.bg, foreground from syntax.p
+;; - syntax -- syntax.<cat> -> font-lock / tree-sitter faces, with full
+;; face attrs where Emacs supports them
;; - ui -- the ui keys are already real face names; fg/bg passthrough
;; - packages -- per-package face specs with :inherit / :height / weight /
;; slant
@@ -27,10 +27,10 @@
;; emacs --batch -l build-theme.el \
;; --eval '(build-theme/convert-file "dupre-revised.json" "themes")'
;;
-;; The output is a flat generated deftheme, not the hand-authored
-;; palette/faces/theme trio that the original dupre theme ships -- a theme.json
-;; carries resolved per-face hex, not dupre's semantic-mapping layer, so a flat
-;; deftheme is the faithful output and never clobbers the curated dupre files.
+;; The output is a flat generated deftheme: a theme.json carries resolved
+;; per-face hex, so the converter emits one custom-theme-set-faces with a
+;; literal value per face. Giving generated themes a palette + assignments
+;; structure is tracked in its own spec.
;;; Code:
@@ -71,30 +71,6 @@ independently is not possible without clobbering types.")
"Non-nil when S is a \"#rrggbb\" hex color string."
(and (stringp s) (string-match-p "\\`#[0-9a-fA-F]\\{6\\}\\'" s)))
-(defun build-theme/--attrs (inherit fg bg bold italic underline strike height)
- "Build a face-attribute plist from the given fields, in canonical order.
-INHERIT is a face symbol or nil. FG and BG are hex strings or nil. BOLD,
-ITALIC, UNDERLINE, and STRIKE are booleans. HEIGHT is a float multiplier; 1.0
-(or nil) is omitted as the default. Only set attributes are written, so a
-fully-nil face yields an empty plist."
- (let (plist)
- (when (and height (numberp height) (/= height 1.0))
- (setq plist (list :height height)))
- (when strike (setq plist (append (list :strike-through t) plist)))
- (when underline (setq plist (append (list :underline t) plist)))
- (when italic (setq plist (append (list :slant 'italic) plist)))
- (when bold (setq plist (append (list :weight 'bold) plist)))
- (when bg (setq plist (append (list :background bg) plist)))
- (when fg (setq plist (append (list :foreground fg) plist)))
- (when inherit (setq plist (append (list :inherit inherit) plist)))
- plist))
-
-(defun build-theme/--face-spec (face attrs)
- "Wrap FACE and its ATTRS plist as a `custom-theme-set-faces' spec.
-Return nil when ATTRS is empty, so cleared faces emit nothing."
- (when attrs
- (list face (list (list t attrs)))))
-
(defun build-theme/--obj-get (obj key)
"Value of KEY in alist OBJ, or nil."
(cdr (assq key obj)))
@@ -106,81 +82,153 @@ Return nil when ATTRS is empty, so cleared faces emit nothing."
((stringp value) (intern value))
(t nil)))
+(defun build-theme/--box (box)
+ "Convert a box spec alist (style/color/width) to an Emacs `:box' value, or nil.
+STYLE is \"line\", \"released\", or \"pressed\"; WIDTH defaults to 1; COLOR (a hex
+string) applies to the line style and falls through to the face foreground when
+unset."
+ (when box
+ (let ((style (build-theme/--obj-get box 'style))
+ (color (build-theme/--obj-get box 'color))
+ (width (or (build-theme/--obj-get box 'width) 1)))
+ (cond ((equal style "released") (append (list :line-width width :style 'released-button)
+ (when color (list :color color))))
+ ((equal style "pressed") (append (list :line-width width :style 'pressed-button)
+ (when color (list :color color))))
+ ((equal style "line") (if color (list :line-width width :color color)
+ (list :line-width width)))
+ (t nil)))))
+
+(defun build-theme/--weight (obj)
+ "Weight symbol for OBJ: explicit `weight' string, else the legacy `bold' flag."
+ (let ((w (build-theme/--obj-get obj 'weight)))
+ (cond ((and (stringp w) (> (length w) 0)) (intern w))
+ ((build-theme/--obj-get obj 'bold) 'bold))))
+
+(defun build-theme/--slant (obj)
+ "Slant symbol for OBJ: explicit `slant' string, else the legacy `italic' flag."
+ (let ((s (build-theme/--obj-get obj 'slant)))
+ (cond ((and (stringp s) (> (length s) 0)) (intern s))
+ ((build-theme/--obj-get obj 'italic) 'italic))))
+
+(defun build-theme/--line-attr (val)
+ "Coerce an overline/strike-through VAL to an Emacs attribute value.
+nil and t pass through; a {color: C} alist becomes C, or t when COLOR is unset.
+Tolerates the legacy boolean form."
+ (cond ((null val) nil)
+ ((eq val t) t)
+ ((consp val) (or (build-theme/--obj-get val 'color) t))
+ (t t)))
+
+(defun build-theme/--underline (obj)
+ "Underline attribute value for OBJ.
+nil when unset. t is a plain line in the face color. A color or wave style
+yields a (:color C :style S) plist. Tolerates the legacy boolean form."
+ (let ((u (build-theme/--obj-get obj 'underline)))
+ (cond ((null u) nil)
+ ((eq u t) t)
+ ((consp u)
+ (let* ((color (build-theme/--obj-get u 'color))
+ (style (build-theme/--obj-get u 'style))
+ (wave (and (stringp style) (not (equal style "line")) (intern style))))
+ (cond ((and color wave) (list :color color :style wave))
+ (color (list :color color))
+ (wave (list :style wave))
+ (t t))))
+ (t t))))
+
+(defun build-theme/--attrs (obj)
+ "Build a face-attribute plist from face-spec alist OBJ, in canonical order.
+Reads the full attribute model -- inherit, family, fg/bg, distant-foreground,
+weight, slant, height, underline, overline, strike-through, box, inverse-video,
+extend -- and tolerates the older boolean bold/italic/underline/strike fields.
+Only attributes that are set appear, so a blank face yields nil."
+ (let* ((height (build-theme/--obj-get obj 'height))
+ (family (build-theme/--obj-get obj 'family))
+ (pairs
+ (list
+ (cons :inherit (build-theme/--inherit-symbol (build-theme/--obj-get obj 'inherit)))
+ (cons :family (and (stringp family) (> (length family) 0) family))
+ (cons :foreground (build-theme/--obj-get obj 'fg))
+ (cons :background (build-theme/--obj-get obj 'bg))
+ (cons :distant-foreground (build-theme/--obj-get obj 'distant-fg))
+ (cons :weight (build-theme/--weight obj))
+ (cons :slant (build-theme/--slant obj))
+ (cons :height (and (numberp height) (/= height 1.0) height))
+ (cons :underline (build-theme/--underline obj))
+ (cons :overline (build-theme/--line-attr (build-theme/--obj-get obj 'overline)))
+ (cons :strike-through (build-theme/--line-attr (build-theme/--obj-get obj 'strike)))
+ (cons :box (build-theme/--box (build-theme/--obj-get obj 'box)))
+ (cons :inverse-video (and (build-theme/--obj-get obj 'inverse) t))
+ (cons :extend (and (build-theme/--obj-get obj 'extend) t))))
+ (plist nil))
+ (dolist (pair pairs)
+ (when (cdr pair)
+ (setq plist (nconc plist (list (car pair) (cdr pair))))))
+ plist))
+
+(defun build-theme/--face-spec (face attrs)
+ "Wrap FACE and its ATTRS plist as a `custom-theme-set-faces' spec.
+Return nil when ATTRS is empty, so cleared faces emit nothing."
+ (when attrs
+ (list face (list (list t attrs)))))
+
;;; ---------------------------------------------------------------------------
;;; Tiers
-(defun build-theme/--default-spec (assignments)
- "Build the `default' face spec from ASSIGNMENTS bg / p."
- (let ((bg (build-theme/--obj-get assignments 'bg))
- (fg (build-theme/--obj-get assignments 'p)))
- (build-theme/--face-spec 'default (build-theme/--attrs nil fg bg nil nil nil nil nil))))
+(defun build-theme/--default-spec (syntax)
+ "Build the `default' face spec from SYNTAX bg / p entries."
+ (let ((bg (build-theme/--obj-get (build-theme/--obj-get syntax 'bg) 'fg))
+ (fg (build-theme/--obj-get (build-theme/--obj-get syntax 'p) 'fg)))
+ (build-theme/--face-spec 'default (build-theme/--attrs (list (cons 'fg fg) (cons 'bg bg))))))
-(defun build-theme/--syntax-face-specs (assignments bold italic)
- "Build syntax-tier face specs from ASSIGNMENTS plus the BOLD and ITALIC sets.
-BOLD and ITALIC are lists of category-key symbols. Each category fans out to
-the font-lock faces in `build-theme/--syntax-face-map'."
+(defun build-theme/--syntax-face-specs (syntax)
+ "Build syntax-tier face specs from SYNTAX.
+Each category fans out to the font-lock faces in
+`build-theme/--syntax-face-map'."
(let (specs)
(dolist (pair build-theme/--syntax-face-map)
(let* ((cat (car pair))
(faces (cdr pair))
- (hex (build-theme/--obj-get assignments cat)))
- (when hex
- (let ((attrs (build-theme/--attrs nil hex nil
- (memq cat bold) (memq cat italic) nil nil nil)))
+ (obj (build-theme/--obj-get syntax cat)))
+ (when obj
+ (let ((attrs (build-theme/--attrs obj)))
(dolist (face faces)
(when-let ((spec (build-theme/--face-spec face attrs)))
(push spec specs)))))))
(nreverse specs)))
-(defun build-theme/--ui-face-specs (ui)
- "Build UI-tier face specs from the UI alist (face -> {fg,bg,bold,italic})."
+(defun build-theme/--specs-from-entries (entries)
+ "Build face specs from ENTRIES, an alist of (face . attribute-alist).
+Empty-attr entries emit nothing (cleared faces drop out)."
(let (specs)
- (dolist (entry ui)
- (let* ((face (car entry))
- (obj (cdr entry))
- (attrs (build-theme/--attrs nil
- (build-theme/--obj-get obj 'fg)
- (build-theme/--obj-get obj 'bg)
- (build-theme/--obj-get obj 'bold)
- (build-theme/--obj-get obj 'italic)
- (build-theme/--obj-get obj 'underline)
- (build-theme/--obj-get obj 'strike)
- nil)))
- (when-let ((spec (build-theme/--face-spec face attrs)))
- (push spec specs))))
+ (dolist (entry entries)
+ (when-let ((spec (build-theme/--face-spec
+ (car entry)
+ (build-theme/--attrs (cdr entry)))))
+ (push spec specs)))
(nreverse specs)))
+(defun build-theme/--ui-face-specs (ui)
+ "Build UI-tier face specs from the UI alist (face -> attribute alist)."
+ (build-theme/--specs-from-entries ui))
+
(defun build-theme/--package-face-specs (packages)
"Build package-tier face specs from the PACKAGES alist (app -> face -> spec)."
(let (specs)
(dolist (app packages)
- (dolist (entry (cdr app))
- (let* ((face (car entry))
- (obj (cdr entry))
- (attrs (build-theme/--attrs
- (build-theme/--inherit-symbol (build-theme/--obj-get obj 'inherit))
- (build-theme/--obj-get obj 'fg)
- (build-theme/--obj-get obj 'bg)
- (build-theme/--obj-get obj 'bold)
- (build-theme/--obj-get obj 'italic)
- (build-theme/--obj-get obj 'underline)
- (build-theme/--obj-get obj 'strike)
- (build-theme/--obj-get obj 'height))))
- (when-let ((spec (build-theme/--face-spec face attrs)))
- (push spec specs)))))
- (nreverse specs)))
+ (setq specs (nconc specs (build-theme/--specs-from-entries (cdr app)))))
+ specs))
(defun build-theme/--all-specs (data)
"Build the full ordered face-spec list from parsed theme.json DATA."
- (let ((assignments (build-theme/--obj-get data 'assignments))
- (bold (mapcar #'intern (build-theme/--obj-get data 'bold)))
- (italic (mapcar #'intern (build-theme/--obj-get data 'italic)))
+ (let ((syntax (build-theme/--obj-get data 'syntax))
(ui (build-theme/--obj-get data 'ui))
(packages (build-theme/--obj-get data 'packages)))
(delq nil
(append
- (list (build-theme/--default-spec assignments))
- (build-theme/--syntax-face-specs assignments bold italic)
+ (list (build-theme/--default-spec syntax))
+ (build-theme/--syntax-face-specs syntax)
(build-theme/--ui-face-specs ui)
(build-theme/--package-face-specs packages)))))
@@ -221,11 +269,13 @@ Signal a `file-missing' error when JSON-FILE does not exist."
(defun build-theme/convert-file (json-file &optional out-dir)
"Convert JSON-FILE (a theme.json export) into a deftheme file.
-Write themes/<name>-theme.el, where <name> is the JSON name field, into
-OUT-DIR (default: the themes/ directory of this repo). Return the written
-path."
+Write themes/<name>-theme.el, where <name> is JSON-FILE's basename, into
+OUT-DIR (default: the themes/ directory of this repo). The basename names the
+theme so each export lands under its own file (sterling.json becomes
+sterling-theme.el), not the name field inside the JSON.
+Return the written path."
(let* ((data (build-theme/--parse json-file))
- (name (build-theme/--obj-get data 'name))
+ (name (file-name-base json-file))
(specs (build-theme/--all-specs data))
(dir (or out-dir
(expand-file-name