Skip to content

Commit 6a9b53d

Browse files
committed
Implement #181 - web app manifest support
This feature will allow sites built with Perun to be installed as an app on systems that support it. When paired with #180, this will be a slick offline experience.
1 parent 62f409a commit 6a9b53d

File tree

4 files changed

+209
-69
lines changed

4 files changed

+209
-69
lines changed

src/io/perun.clj

Lines changed: 144 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
[clojure.java.io :as io]
88
[clojure.set :as set]
99
[clojure.string :as string]
10-
[clojure.edn :as edn]
1110
[io.perun.core :as perun]
1211
[io.perun.meta :as pm]))
1312

@@ -122,6 +121,8 @@
122121
- width
123122
- height"
124123
[]
124+
;; This prevents a Java icon appearing in the dock on a Mac, and stealing program focus
125+
(System/setProperty "java.awt.headless" "true")
125126
(boot/with-pre-wrap fileset
126127
(let [pod (create-pod images-dimensions-deps)
127128
metas (trace :io.perun/images-dimensions
@@ -130,31 +131,12 @@
130131
(io.perun.contrib.images-dimensions/images-dimensions ~metas {}))]
131132
(pm/set-meta fileset updated-metas))))
132133

133-
(def ^:private ^:deps images-resize-deps
134-
'[[image-resizer "0.1.8"]])
135-
136-
(def ^:private +images-resize-defaults+
137-
{:out-dir "public"
138-
:resolutions #{3840 2560 1920 1280 1024 640}})
139-
140-
(deftask images-resize
141-
"Resize images to the provided resolutions.
142-
Each image file would have resolution appended to it's name:
143-
e.x. san-francisco.jpg would become san-francisco_3840.jpg"
144-
[o out-dir OUTDIR str "the output directory"
145-
r resolutions RESOLUTIONS #{int} "resoulitions to which images should be resized"]
146-
(boot/with-pre-wrap fileset
147-
(let [options (merge +images-resize-defaults+ *opts*)
148-
tmp (boot/tmp-dir!)
149-
pod (create-pod images-resize-deps)
150-
metas (trace :io.perun/images-resize
151-
(meta-by-ext fileset [".png" ".jpeg" ".jpg"]))
152-
updated-metas (pod/with-call-in @pod
153-
(io.perun.contrib.images-resize/images-resize ~(.getPath tmp) ~metas ~options))]
154-
(perun/report-debug "images-resize" "new resized images" updated-metas)
155-
(-> fileset
156-
(commit tmp)
157-
(pm/set-meta updated-metas)))))
134+
(defn apply-out-dir
135+
[path old-out-dir new-out-dir]
136+
(let [path-args (if (= old-out-dir new-out-dir)
137+
[path]
138+
[new-out-dir path])]
139+
(apply perun/create-filepath path-args)))
158140

159141
(defn render-in-pod
160142
"Renders paths in `inputs`, using `render-form-fn` in `pod`
@@ -231,9 +213,9 @@
231213
`passthru-fn` to handle setting changed metadata on files copied from the
232214
previous fileset. If input files should be removed from the fileset, set
233215
`rm-originals` to `true`."
234-
[{:keys [task-name render-form-fn paths-fn passthru-fn tracer pod rm-originals]}]
235-
(let [tmp (boot/tmp-dir!)
236-
prev (atom {})
216+
[{:keys [task-name render-form-fn paths-fn passthru-fn tracer pod tmp rm-originals]}]
217+
(let [prev (atom {})
218+
tmp (or tmp (boot/tmp-dir!))
237219
pod (or pod (create-pod content-deps))]
238220
(fn [next-task]
239221
(fn [fileset]
@@ -299,11 +281,10 @@
299281
(let [global-meta (pm/get-global-meta fileset)]
300282
(reduce (fn [result {:keys [path] :as entry}]
301283
(let [ext-pattern (re-pattern (str "(" (string/join "|" extensions) ")$"))
302-
new-path (if out-ext
303-
(->> out-ext
304-
(string/replace path ext-pattern)
305-
(perun/create-filepath out-dir))
306-
(perun/create-filepath out-dir path))
284+
ext-path (if out-ext
285+
(string/replace path ext-pattern out-ext)
286+
path)
287+
new-path (apply-out-dir ext-path (:out-dir entry) out-dir)
307288
path-meta (pm/path-meta path
308289
global-meta
309290
(boot/tmp-file (boot/tmp-get fileset path)))]
@@ -334,6 +315,69 @@
334315
{:filterer identity
335316
:extensions []})
336317

318+
(defn resize-paths
319+
"Returns a map of path -> input for images-resize"
320+
[fileset {:keys [out-dir parent-path meta resolutions] :as options} tmp-dir]
321+
(let [global-meta (pm/get-global-meta fileset)
322+
files (boot/ls fileset)]
323+
(reduce
324+
(fn [result {:keys [slug path extension] :as entry}]
325+
(reduce
326+
(fn [result* resolution]
327+
(let [new-filename (str slug "_" resolution "." extension)
328+
new-path (-> (perun/create-filepath parent-path new-filename)
329+
(apply-out-dir (:out-dir entry) out-dir))
330+
input-file (first (boot/by-path [path] files))
331+
img-meta (assoc (pm/path-meta new-path global-meta)
332+
:resolution resolution
333+
:input-paths #{path}
334+
:input-meta (merge (pm/meta-from-file fileset input-file)
335+
(select-keys input-file [:hash]))
336+
:tmp-dir tmp-dir)]
337+
(assoc result*
338+
new-path (merge entry
339+
meta
340+
(when out-dir
341+
{:out-dir out-dir})
342+
img-meta))))
343+
result
344+
resolutions))
345+
{}
346+
(filter-meta-by-ext fileset options))))
347+
348+
(def ^:private ^:deps images-resize-deps
349+
'[[org.clojure/tools.namespace "0.3.0-alpha3"]
350+
[image-resizer "0.1.8"]])
351+
352+
(def ^:private +images-resize-defaults+
353+
{:out-dir "public"
354+
:resolutions #{3840 2560 1920 1280 1024 640}
355+
:filterer identity
356+
:extensions [".png" ".jpeg" ".jpg"]})
357+
358+
(deftask images-resize
359+
"Resize images to the provided resolutions.
360+
Each image file would have resolution appended to it's name:
361+
e.x. san-francisco.jpg would become san-francisco_3840.jpg"
362+
[o out-dir OUTDIR str "the output directory"
363+
r resolutions RESOLUTIONS #{int} "resolutions to which images should be resized"
364+
_ filterer FILTER code "predicate to use for selecting entries (default: `identity`)"
365+
e extensions EXTENSIONS [str] "extensions of files to include (default: `[]`, aka, all extensions)"
366+
m meta META edn "metadata to set on each entry"]
367+
;; This prevents a Java icon appearing in the dock on a Mac, and stealing program focus
368+
(System/setProperty "java.awt.headless" "true")
369+
(let [pod (create-pod images-resize-deps)
370+
tmp (boot/tmp-dir!)
371+
options (merge +images-resize-defaults+ *opts*)]
372+
(content-task
373+
{:render-form-fn (fn [data] `(io.perun.contrib.images-resize/image-resize ~data))
374+
:paths-fn #(resize-paths % options (.getPath tmp))
375+
:passthru-fn content-passthru
376+
:task-name "images-resize"
377+
:tracer :io.perun/images-resize
378+
:pod pod
379+
:tmp tmp})))
380+
337381
(deftask yaml-metadata
338382
"Parse YAML metadata at the beginning of files
339383
@@ -724,20 +768,17 @@
724768
e extensions EXTENSIONS [str] "extensions of files to include"
725769
r renderer RENDERER sym "page renderer (fully qualified symbol which resolves to a function)"
726770
m meta META edn "metadata to set on each entry"]
727-
(let [{:keys [renderer] :as options} (merge +render-defaults+ *opts*)]
771+
(let [{:keys [renderer out-dir] :as options} (merge +render-defaults+ *opts*)]
728772
(letfn [(render-paths [fileset]
729773
(let [entries (filter-meta-by-ext fileset options)]
730774
(reduce
731-
(fn [result {:keys [path out-dir] :as entry}]
775+
(fn [result {:keys [path] :as entry}]
732776
(let [content (slurp (boot/tmp-file (boot/tmp-get fileset path)))
733-
path-args (if (= out-dir (:out-dir options))
734-
[path]
735-
[(:out-dir options) path])
736-
new-path (apply perun/create-filepath path-args)
777+
new-path (apply-out-dir path (:out-dir entry) out-dir)
737778
new-entry (merge entry
738779
meta
739780
{:content content
740-
:out-dir (:out-dir options)})]
781+
:out-dir out-dir})]
741782
(assoc result new-path {:meta (pm/get-global-meta fileset)
742783
:entries entries
743784
:entry new-entry
@@ -770,7 +811,9 @@
770811
path (perun/create-filepath out-dir page)
771812
static-path (fn [fileset]
772813
{path {:meta (pm/get-global-meta fileset)
773-
:entry (assoc meta :path path)}})]
814+
:entry (assoc meta
815+
:path path
816+
:out-dir out-dir)}})]
774817
(render-task {:task-name "static"
775818
:paths-fn static-path
776819
:renderer renderer
@@ -791,7 +834,7 @@
791834
(boot/tmp-get fileset)
792835
boot/tmp-file
793836
slurp))))
794-
new-path (perun/create-filepath out-dir path)
837+
new-path (apply-out-dir path (:out-dir entry) out-dir)
795838
new-entry (merge entry
796839
{:out-dir out-dir}
797840
(pm/path-meta path global-meta))]
@@ -1167,3 +1210,61 @@
11671210
:passthru-fn content-passthru
11681211
:task-name "inject-scripts"
11691212
:tracer :io.perun/inject-scripts}))))
1213+
1214+
(def ^:private ^:deps manifest-deps
1215+
'[[org.clojure/tools.namespace "0.3.0-alpha3"]
1216+
[cheshire "5.7.0"]])
1217+
1218+
(def +manifest-defaults+
1219+
{:out-dir "public"
1220+
:icon-path "icon.png"
1221+
:resolutions #{192 512}
1222+
:theme-color "#ffffff"
1223+
:display "standalone"
1224+
:scope "/"})
1225+
1226+
(deftask manifest*
1227+
[o out-dir OUTDIR str "the output directory"
1228+
t site-title TITLE str "name for the installable web application"
1229+
c theme-color COLOR str "background color theme for icon (default \"#ffffff\")"
1230+
d display DISPLAY str "display mode for browser (default \"standalone\")"
1231+
s scope SCOPE str "the scope to which the manifest applies (default \"/\")"]
1232+
(let [{:keys [site-title] :as opts} (merge +manifest-defaults+ *opts*)
1233+
pod (create-pod manifest-deps)]
1234+
(letfn [(manifest-path [fileset]
1235+
(let [icon-metas (filter-meta-by-ext fileset {:filterer :manifest-icon})
1236+
path (perun/create-filepath out-dir "manifest.json")
1237+
global-meta (pm/get-global-meta fileset)
1238+
args (merge opts
1239+
{:icons icon-metas
1240+
:input-paths (into #{} (map :path icon-metas))
1241+
:site-title (or site-title (:site-title global-meta))})]
1242+
{path args}))]
1243+
(content-task
1244+
{:render-form-fn (fn [data] `(io.perun.manifest/manifest ~data))
1245+
:paths-fn manifest-path
1246+
:task-name "manifest"
1247+
:tracer :io.perun/manifest
1248+
:pod pod}))))
1249+
1250+
(deftask manifest
1251+
"Creates a manifest.json for Android (currently)"
1252+
[o out-dir OUTDIR str "the output directory"
1253+
i icon-path PATH str "The input icon to be resized (default \"icon.png\""
1254+
r resolutions RESOLUTIONS #{int} "resolutions to which images should be resized (default #{192 512})"
1255+
t site-title TITLE str "name for the installable web application"
1256+
c theme-color COLOR str "background color theme for icon (default \"#ffffff\")"
1257+
d display DISPLAY str "display mode for browser (default \"standalone\")"
1258+
s scope SCOPE str "the scope to which the manifest applies (default \"/\")"]
1259+
(let [{:keys [out-dir icon-path resolutions site-title theme-color display scope]}
1260+
(merge +manifest-defaults+ *opts*)]
1261+
(comp (images-resize :out-dir out-dir
1262+
:resolutions resolutions
1263+
:filterer #(= (:path %) icon-path)
1264+
:meta {:manifest-icon true})
1265+
(mime-type :filterer :manifest-icon)
1266+
(manifest* :out-dir out-dir
1267+
:site-title site-title
1268+
:theme-color theme-color
1269+
:display display
1270+
:scope scope))))

src/io/perun/contrib/images_resize.clj

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,29 @@
99
[java.awt.image BufferedImage]
1010
[javax.imageio ImageIO ImageWriter]))
1111

12-
(defn write-file [options tmp file ^BufferedImage buffered-file resolution]
13-
(let [{:keys [slug extension parent-path]} file
14-
new-filename (str slug "_" resolution "." extension)
15-
new-path (perun/create-filepath (:out-dir options) parent-path new-filename)
16-
new-file (io/file tmp new-path)]
17-
(io/make-parents new-file)
18-
(ImageIO/write buffered-file extension new-file)
19-
{:path new-path}))
20-
21-
(defn resize-to [tgt-path file options resolution]
22-
(let [io-file (-> file :full-path io/file)
23-
buffered-image (iu/buffered-image io-file)
24-
resized-buffered-image (resize/resize-to-width buffered-image resolution)
25-
new-dimensions (iu/dimensions resized-buffered-image)
26-
new-meta (write-file options tgt-path file resized-buffered-image resolution)
27-
dimensions {:width (first new-dimensions) :height (second new-dimensions)}]
28-
(merge file new-meta dimensions (select-keys options [:out-dir]))))
12+
(def img-cache (atom {}))
2913

30-
(defn process-image [tgt-path file options]
31-
(perun/report-debug "image-resize" "resizing" (:path file))
32-
(pmap #(resize-to tgt-path file options %) (:resolutions options)))
14+
(defn get-input-img
15+
[{:keys [input-meta]}]
16+
(let [input-path (:full-path input-meta)
17+
key (str input-path "-" (:hash input-meta))]
18+
(if-let [buffered-image (get @img-cache key)]
19+
@buffered-image
20+
(let [buffered-image (future (-> input-path
21+
io/file
22+
iu/buffered-image))]
23+
(swap! img-cache assoc key buffered-image)
24+
@buffered-image))))
3325

34-
(defn images-resize [tgt-path files options]
35-
(let [updated-files (doall (mapcat #(process-image tgt-path % options) files))]
36-
(perun/report-info "image-resize" "processed %s image files" (count files))
37-
updated-files))
26+
(defn image-resize
27+
[{:keys [path resolution extension tmp-dir] :as data}]
28+
(perun/report-debug "image-resize" "resizing" path)
29+
(let [buffered-image (get-input-img data)
30+
resized-buffered-image (resize/resize-to-width buffered-image resolution)
31+
new-file (io/file tmp-dir path)]
32+
(io/make-parents new-file)
33+
(ImageIO/write resized-buffered-image extension new-file)
34+
(merge (dissoc data :input-meta :tmp-dir)
35+
(into {} (map vector
36+
[:width :height]
37+
(iu/dimensions resized-buffered-image))))))

src/io/perun/manifest.clj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
(ns io.perun.manifest
2+
(:require [cheshire.core :refer [generate-string]]))
3+
4+
(defn manifest
5+
[{:keys [icons site-title theme-color display scope input-paths] :as data}]
6+
(let [manifest {:name site-title
7+
:icons (for [{:keys [permalink width height mime-type]} icons]
8+
{:src permalink
9+
:sizes (str width "x" height)
10+
:type mime-type})
11+
:theme_color theme-color
12+
:display display
13+
:scope scope}]
14+
{:rendered (generate-string manifest)
15+
:input-paths input-paths}))

test/io/perun_test.clj

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,17 @@ This --- be ___markdown___.")
378378
(testing "draft"
379379
(file-exists? :path (perun/url-to-path "public/test/index.html")
380380
:negate? true
381-
:msg "`draft` should remove files"))))
381+
:msg "`draft` should remove files"))
382+
383+
(add-image :path "icon.png" :type "PNG" :width 10 :height 10)
384+
(p/manifest)
385+
(testing "manifest"
386+
(file-exists? :path (perun/url-to-path "public/manifest.json")
387+
:msg "`manifest` should write manifest.json")
388+
(file-exists? :path (perun/url-to-path "public/icon_192.png")
389+
:msg "`manifest` should write icon resized to 192px")
390+
(file-exists? :path (perun/url-to-path "public/icon_512.png")
391+
:msg "`manifest` should write icon resized to 512px"))))
382392

383393
(deftesttask with-arguments-test []
384394
(comp (boot/with-pre-wrap fileset
@@ -671,7 +681,21 @@ This --- be ___markdown___.")
671681
(content-check :path "baz.htm"
672682
:content (str "<script>" js-content "</script>")
673683
:negate? true
674-
:msg "`inject-scripts` should not alter the contents of a removed file")))))
684+
:msg "`inject-scripts` should not alter the contents of a removed file")))
685+
686+
(add-image :path "an-icon.png" :type "PNG" :width 10 :height 10)
687+
(p/manifest :out-dir "foop"
688+
:icon-path "an-icon.png"
689+
:resolutions #{20}
690+
:site-title "Blarg"
691+
:theme-color "#f0987d"
692+
:display "fullscreen"
693+
:scope "/blarp")
694+
(testing "manifest"
695+
(file-exists? :path (perun/url-to-path "foop/manifest.json")
696+
:msg "`manifest` should write manifest.json")
697+
(file-exists? :path (perun/url-to-path "foop/an-icon_20.png")
698+
:msg "`manifest` should write icon resized to 20px"))))
675699

676700
(deftesttask content-tests []
677701
(comp (testing "Collection works without input files" ;; #77

0 commit comments

Comments
 (0)