1 (uiop:define-package
#:lw2.resources
2 (:use
#:cl
#:iterate
#:lw2-viewer.config
#:lw2.utils
#:lw2.sites
#:lw2.context
)
3 (:import-from
#:alexandria
#:with-gensyms
#:once-only
#:when-let
#:appendf
)
4 (:import-from
#:parse-float
#:parse-float
)
5 (:export
#:*page-resources
* #:inverted-media-query
#:with-page-resources
#:require-resource
#:generate-versioned-link
#:with-resource-bindings
#:call-with-fonts-source-resources
#:site-resources
)
6 (:recycle
#:lw2-viewer
)
7 (:unintern
#:fonts-source-resources
))
9 (in-package #:lw2.resources
)
11 (defparameter *page-resources
* nil
)
12 (defparameter *link-header
* (constantly nil
))
13 (defparameter *style-tags
* (constantly nil
))
14 (defparameter *script-tags
* (constantly nil
))
15 (defparameter *async-script-tags
* (constantly nil
))
17 (defparameter *push-option
* nil
)
19 (defun inverted-media-query ()
20 (alexandria:switch
((hunchentoot:cookie-in
"dark-mode") :test
#'string-equal
)
23 (t (if (string= (hunchentoot:cookie-in
"theme") "dark")
25 "all and (prefers-color-scheme: dark)"))))
27 (defmacro with-page-resources
(&body body
)
28 `(let* ((*link-header
* *link-header
*)
29 (*style-tags
* *style-tags
*)
30 (*script-tags
* *script-tags
*)
31 (*async-script-tags
* *async-script-tags
*)
32 (*push-option
* (when (hunchentoot:cookie-in
"push") "nopush"))
33 (*page-resources
* (site-resources *current-site
*)))
34 (dynamic-flet ((fn () ,@body
))
35 (call-with-site-resources *current-site
* #'fn
))))
37 (defmacro with-appended-functions
((&rest clauses
) &body body
)
38 `(let* ,(iter (for (old-function new-name lambda-list . inner-body
) in clauses
)
39 (let ((old-function-name (gensym)))
40 (collect `(,old-function-name
,old-function
))
41 (collect `(,new-name
(lambda ,lambda-list
(funcall ,old-function-name
,@lambda-list
) ,@inner-body
)))))
44 (defun require-resource (type &rest args
)
45 (push (list* type args
) *page-resources
*))
47 (defun generate-versioned-link (file)
48 (let* ((filename (format nil
"www~A" file
)))
49 (or (ignore-errors (format nil
"~A?v=~A" file
(universal-time-to-unix (file-write-date filename
))))
52 (defgeneric call-with-fonts-source-resources
(site fn
))
54 (defun output-link-header-element (stream uri type
)
55 (multiple-value-bind (rel type as push-option
)
57 (:preconnect
(values "preconnect"))
58 (:stylesheet
(values "preload" "text/css" "style" *push-option
*))
59 (:script
(values "preload" "text/javascript" "script" *push-option
*)))
60 (format stream
"<~A>;rel=~A~@[;type=~A~]~@[;as=~A~]~@[;~A~]" uri rel type as push-option
)))
62 (defun output-style-tag (stream uri media class
)
63 (format stream
"<link rel=\"stylesheet\" href=\"~A\"~@[ media=\"~A\"~]~@[ class=\"~A\"~]>" uri media class
))
65 (defun output-script-tag (stream uri
&key async
)
66 (format stream
"<script src=\"~A\"~:[~; async~]></script>" uri async
))
68 (eval-when (:compile-toplevel
:load-toplevel
:execute
)
69 (defun preconnect-resource-forms (stream uri
)
70 (alist :link-header
`((funcall delimit
) (output-link-header-element ,stream
,uri
:preconnect
))))
72 (defun style-resource-forms (stream uri
&optional media class
)
73 (with-gensyms (versioned-uri)
74 (alist :bindings
`((,versioned-uri
(generate-versioned-link ,uri
)))
75 :link-header
`((funcall delimit
) (output-link-header-element ,stream
,versioned-uri
:stylesheet
))
76 :style-tags
`((output-style-tag ,stream
,versioned-uri
,media
,class
)))))
78 (defun script-resource-forms (stream uri
&rest args
&key async
)
79 (with-gensyms (versioned-uri)
80 (alist :bindings
`((,versioned-uri
(generate-versioned-link ,uri
)))
81 :link-header
`((funcall delimit
) (output-link-header-element ,stream
,versioned-uri
:script
))
82 (if async
:async-script-tags
:script-tags
) `((output-script-tag ,stream
,versioned-uri
,@args
))))))
84 (defmacro with-resource-bindings
((&rest clauses
) &body body
)
85 (let ((forms (make-hash-table :test
'eq
)))
86 (iter (for (resource-type . params
) in clauses
)
87 (iter (for (form-type . form-body
) in
(apply (case resource-type
88 (:preconnect
#'preconnect-resource-forms
)
89 (:style
#'style-resource-forms
)
90 (:script
#'script-resource-forms
))
92 (appendf (gethash form-type forms
) form-body
)))
93 `(let* ,(gethash :bindings forms
)
94 ,(let (w-a-f-clauses special-names lexical-names
)
95 (iter (for (form-type special-name lexical-name extra-lambda-list
)
96 in
'((:link-header
*link-header
* link-header-fn
(delimit))
97 (:style-tags
*style-tags
* style-tags-fn nil
)
98 (:script-tags
*script-tags
* script-tags-fn nil
)
99 (:async-script-tags
*async-script-tags
* async-script-tags-fn nil
)))
100 (when-let ((form-body (gethash form-type forms
)))
101 (push `(,special-name
,lexical-name
(stream ,@extra-lambda-list
) ,@form-body
) w-a-f-clauses
)
102 (push special-name special-names
)
103 (push lexical-name lexical-names
)))
104 `(with-appended-functions ,(reverse w-a-f-clauses
)
105 ,(when body
`(declare (dynamic-extent ,@lexical-names
)))
106 (setf ,@(iter (for special in special-names
) (for lexical in lexical-names
)
107 (collect special
) (collect lexical
)))
110 (defgeneric call-with-site-resources
(site fn
)
111 (:method
((site site
) fn
)
112 (with-resource-bindings ((:script
"/head.js")
113 (:script
"/script.js" :async t
))
114 (call-with-fonts-source-resources (site-fonts-source site
) fn
))))
116 (defun file-valid-date (file)
117 (max (file-write-date file
)
118 (load-time-value (get-universal-time))))
120 (defgeneric site-resources
(site)
121 (:method-combination append
:most-specific-first
)
122 (:method append
((s site
))
123 (labels ((gen-theme (theme os
)
124 (let* ((basename (format nil
"~@[-~A~].~A.css" theme os
))
125 (filename (format nil
"www/css/style~A" basename
))
126 (version (universal-time-to-unix (file-valid-date filename
)))
127 (baseurl (format nil
"~A?v=~A" basename version
)))
128 (with-resource-bindings ((:style
(format nil
"/generated-css/style~A" baseurl
) nil
"theme")
129 (:style
(format nil
"/generated-css/colors~A" baseurl
) nil
"theme light-mode")
130 (:style
(format nil
"/generated-css/inverted~A" baseurl
) (inverted-media-query) "theme dark-mode"))))))
131 (let* ((ua (hunchentoot:header-in
* :user-agent
))
132 (theme (or (and *preview
* (nonempty-string (hunchentoot:get-parameter
"theme")))
133 (nonempty-string (hunchentoot:cookie-in
"theme"))))
134 (theme (if (or (string= theme
"default") (string= theme
"dark")) nil theme
))
135 (os (cond ((search "Windows" ua
) "windows")
136 ((search "Mac OS" ua
) "mac")
138 (handler-case (gen-theme theme os
)
139 (serious-condition () (gen-theme nil os
)))
140 *html-global-resources
*))))
142 (sb-ext:defglobal
*static-assets
* nil
)
144 (let ((new-static-assets (make-hash-table :test
'equal
)))
145 (flet ((defres (uri content-type
)
146 (vector (concatenate 'string
"www" uri
) content-type
)))
147 (loop for system in
'("mac" "windows" "linux") nconc
148 (loop for theme in
'(nil "dark" "grey" "ultramodern" "zero" "brutalist" "rts" "classic" "less")
149 do
(let ((uri (format nil
"/css/style~@[-~A~].~A.css" theme system
)))
150 (setf (gethash uri new-static-assets
) (defres uri
"text/css")))))
151 (loop for
(uri content-type
) in
152 '(("/fonts.css" "text/css")
153 ("/arbital.css" "text/css")
154 ("/head.js" "text/javascript")
155 ("/script.js" "text/javascript")
156 ("/assets/favicon.ico" "image/x-icon")
157 ("/assets/telegraph.jpg" "image/jpeg")
158 ("/assets/popup.svg" "image/svg+xml"))
159 do
(setf (gethash uri new-static-assets
) (defres uri content-type
))))
160 (setf *static-assets
* new-static-assets
))
162 (hunchentoot:define-easy-handler
163 (view-versioned-resource
165 (when-let ((asset-data (gethash (hunchentoot:script-name r
) *static-assets
*)))
166 (let ((file (svref asset-data
0))
167 (content-type (svref asset-data
1)))
168 (when (assoc "v" (hunchentoot:get-parameters r
) :test
#'string
=)
169 (setf (hunchentoot:header-out
"Cache-Control") #.
(format nil
"public, max-age=~A, immutable" (- (expt 2 31) 1))))
170 (hunchentoot:handle-static-file file content-type
))
174 (defmacro override-scan
(name in-line
)
176 `(let ((reg-matches (nth-value 1 (ppcre:scan-to-strings
,(format nil
"~A:\\s*(.*?)\\s*(?:;|\\*/|$)" name
) ,in-line
))))
177 (if (>= (length reg-matches
) 1) (aref reg-matches
0)))))
179 (defun process-css-line (in-line default-gamma default-background-gamma
)
180 (let* ((invert-override (override-scan "invert-override" in-line
))
181 (gamma-override (override-scan "gamma-override" in-line
))
182 (backgroundp (to-boolean (ppcre:scan
"background|shadow" in-line
)))
183 (gamma (cond (gamma-override (parse-float gamma-override
:type
'double-float
))
184 (backgroundp default-background-gamma
)
187 (format nil
"--theme~A-color"
188 (cond (invert-override (format nil
"-override-~A" (multiple-value-call #'lw2.colors
::safe-color-name
(lw2.colors
::decode-css-color invert-override
))))
189 (gamma-override (format nil
"-gamma-~F" gamma
))
195 (defmacro with-css-lines
((line-var stream
&rest vars
) &body body
)
196 (with-gensyms (default-gamma default-background-gamma part-line full-line
)
197 `(let ((,default-gamma
1.8d0
)
198 (,default-background-gamma
1.5d0
))
199 (loop for
,line-var
= (loop with
,full-line
= nil
200 for
,part-line
= (read-line ,stream nil
)
203 (when (not ,part-line
)
205 (when-let ((x (override-scan "default-gamma" ,part-line
)))
206 (setf ,default-gamma
(parse-float x
:type
'double-float
)))
207 (when-let ((x (override-scan "default-background-gamma" ,part-line
)))
208 (setf ,default-background-gamma
(parse-float x
:type
'double-float
)))
209 (setf ,full-line
(concatenate 'string
,full-line
(string #\Newline
) ,part-line
))
210 (when (ppcre:scan
"[;{}]\\s*$" ,part-line
)
211 (return ,full-line
))))
213 do
(multiple-value-bind ,vars
214 (process-css-line ,line-var
,default-gamma
,default-background-gamma
)
217 (defun output-tweakable-css (file out-stream
)
218 (with-open-file (in-stream file
)
219 (with-css-lines (in-line in-stream variable-prefix
)
220 (let ((out-line (ppcre:regex-replace-all
221 lw2.colors
::-css-color-scanner-
223 (lambda (target-string start end match-start match-end reg-starts reg-ends
)
224 (declare (ignore start end reg-starts reg-ends
))
225 (let ((color-string (substring target-string match-start match-end
)))
226 (format nil
"var(~A-~A)"
228 (multiple-value-call #'lw2.colors
::safe-color-name
(lw2.colors
::decode-css-color color-string
))))))))
229 (write-line out-line out-stream
)))))
231 (defun output-css-colors (file out-stream invert
)
232 (let ((used-colors (make-hash-table :test
'equal
)))
233 (with-open-file (in-stream file
)
234 (format out-stream
":root {~%")
235 (with-css-lines (in-line in-stream variable-prefix invert-override gamma
)
236 (ppcre:do-matches-as-strings
(color-string lw2.colors
::-css-color-scanner- in-line
)
237 (multiple-value-bind (r g b a
) (lw2.colors
::decode-css-color color-string
)
238 (let ((color-name (format nil
"~A-~A"
240 (lw2.colors
::safe-color-name r g b a
))))
241 (unless (equal (gethash color-name used-colors
) gamma
)
242 (setf (gethash color-name used-colors
) gamma
)
243 (format out-stream
"~A: ~A;~%"
247 (multiple-value-call #'lw2.colors
::encode-css-color
(lw2.colors
::perceptual-invert-rgba r g b a gamma
)))
248 color-string
)))))))))
249 (format out-stream
"}~%"))
251 (sb-ext:defglobal
*css-generator-lock
* (sb-thread:make-mutex
:name
"CSS generator lock"))
253 (hunchentoot:define-easy-handler
256 (let ((regs (nth-value 1 (ppcre:scan-to-strings
"^/generated-css/(style|colors|inverted)([-.][a-z.-]+)$" (hunchentoot:script-name r
)))))
257 (when (= (length regs
) 2)
258 (setf (hunchentoot:header-out
"Content-Type") "text/css")
259 (when (assoc "v" (hunchentoot:get-parameters r
) :test
#'string
=)
260 (setf (hunchentoot:header-out
"Cache-Control") #.
(format nil
"public, max-age=~A, immutable" (- (expt 2 31) 1))))
261 (let ((source-file (format nil
"www/css/style~A" (aref regs
1)))
262 (cache-file (format nil
"www/generated-css/~A~A" (aref regs
0) (aref regs
1)))
263 (out-stream (flex:make-flexi-stream
(hunchentoot:send-headers
) :external-format
:utf-8
)))
264 (flet ((cache-valid-p () (and (probe-file cache-file
) (<= (file-valid-date source-file
) (file-write-date cache-file
)))))
265 (unless (cache-valid-p)
266 (sb-thread:with-mutex
(*css-generator-lock
*)
267 (unless (cache-valid-p)
268 (ensure-directories-exist cache-file
)
269 (with-atomic-file-replacement (cache-stream cache-file
:element-type
'character
:external-format
:utf-8
)
271 ((string= (aref regs
0) "style")
272 (output-tweakable-css source-file cache-stream
))
273 ((string= (aref regs
0) "colors")
274 (output-css-colors source-file cache-stream nil
))
275 ((string= (aref regs
0) "inverted")
276 (output-css-colors source-file cache-stream t
))))))))
277 (with-open-file (cache-stream cache-file
:direction
:input
:element-type
'(unsigned-byte 8))
278 (alexandria:copy-stream cache-stream out-stream
)))