diff --git a/resources/config.edn b/resources/config.edn index 87d1361..ecb1ea0 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -30,37 +30,35 @@ :content-dir "resources/public/content" ;; where content is served from. :default-locale "en-GB" ;; default language used for messages + :extensions-from :local ;; where to load JavaScript libraries + ;; from: options are :local, :remote. :formatters ;; formatters for processing markdown ;; extensions. {:backticks {:formatter "smeagol.formatting/process-backticks" :scripts {} :styles {}} :mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid" - :scripts {:core {:local "vendor/mermaid/dist/mermaid.js"}} + :scripts {:core {:local "vendor/mermaid/dist/mermaid.js" + :remote "https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.4.6/mermaid.min.js"}} :styles {}} :pswp {:formatter "smeagol.extensions.photoswipe/process-photoswipe" - :scripts {:core {:local "/vendor/node_modules/photoswipe/dist/photoswipe.min.js" + :scripts {:core {:local "vendor/node_modules/photoswipe/dist/photoswipe.min.js" :remote "https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js"} - :ui {:local "/vendor/node_modules/photoswipe/dist/photoswipe-ui-default.min.js" + :ui {:local "vendor/node_modules/photoswipe/dist/photoswipe-ui-default.min.js" :remote "https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js"}} - :styles {:core {:local "/vendor/node_modules/photoswipe/dist/photoswipe.css" - :remote "/vendor/node_modules/photoswipe/dist/default-skin/default-skin.css"}}} + :styles {:core {:local "vendor/node_modules/photoswipe/dist/photoswipe.css"} + :skin {:local "vendor/node_modules/photoswipe/dist/default-skin/default-skin.css"}}} :test {:formatter "smeagol.extensions.test/process-test" } :vega {:formatter "smeagol.extensions.vega/process-vega" :scripts {:core {:remote "https://cdnjs.cloudflare.com/ajax/libs/vega/5.9.1/vega.min.js"} :lite {:remote "https://cdnjs.cloudflare.com/ajax/libs/vega-lite/4.1.1/vega-lite.min.js"} - :embed {:remote "https://cdnjs.cloudflare.com/ajax/libs/vega-embed/6.2.2/vega-embed.min.js"} - :styles {}}} + :embed {:remote "https://cdnjs.cloudflare.com/ajax/libs/vega-embed/6.2.2/vega-embed.min.js"}}} :vis {:formatter "smeagol.extensions.vega/process-vega" :scripts {:core {:remote "https://cdnjs.cloudflare.com/ajax/libs/vega/5.9.1/vega.min.js"} :lite {:remote "https://cdnjs.cloudflare.com/ajax/libs/vega-lite/4.1.1/vega-lite.min.js"} - :embed {:remote "https://cdnjs.cloudflare.com/ajax/libs/vega-embed/6.2.2/vega-embed.min.js"} - :styles {}}} - } + :embed {:remote "https://cdnjs.cloudflare.com/ajax/libs/vega-embed/6.2.2/vega-embed.min.js"}}}} :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal - :js-from :cdnjs ;; where to load JavaScript libraries - ;; from: options are :local, :cdnjs :passwd "resources/passwd" ;; where the password file is stored :site-title "Smeagol" ;; overall title of the site, used in @@ -71,4 +69,10 @@ ;; stored in the /small directory :med 400 ;; maximum dimension of thumbnails ;; stored in the /med directory - }} + ;; you can add as many extra keys and values as + ;; you like here for additional sizes of images. + ;; Images will only be scaled if their maximum + ;; dimension (in pixels) is greater than the value; + ;; only JPEG and PNG images will be scaled. + } + } diff --git a/resources/templates/wiki.html b/resources/templates/wiki.html index 7342854..81634ea 100644 --- a/resources/templates/wiki.html +++ b/resources/templates/wiki.html @@ -2,11 +2,9 @@ {% block extra-headers %} {% for script in scripts %} - - {% endfor %} + {% endfor %} {% for style in styles %} - - {% endfor %} + {% endfor %} {% endblock %} {% block content %} diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 153c904..5891eeb 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -93,6 +93,7 @@ fragments (cons fragment processed))) + (defn deep-merge [v & vs] "Cripped in its entirety from https://clojuredocs.org/clojure.core/merge." (letfn [(rec-merge [v1 v2] @@ -124,18 +125,18 @@ fragments (cons ident processed)))) -(apply-formatter - 3 - {:inclusions {}} - '() - '() - "pswp - ![Frost on a gate, Laurieston](content/uploads/g1.jpg) - ![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg) - ![Feathered snow on log, Taliesin](content/uploads/g3.jpg) - ![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)" - "pswp" - smeagol.extensions.photoswipe/process-photoswipe) +;; (apply-formatter +;; 3 +;; {:inclusions {}} +;; '() +;; '() +;; "pswp +;; ![Frost on a gate, Laurieston](content/uploads/g1.jpg) +;; ![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg) +;; ![Feathered snow on log, Taliesin](content/uploads/g3.jpg) +;; ![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)" +;; "pswp" +;; smeagol.extensions.photoswipe/process-photoswipe) (defn process-text "Process this `text`, assumed to be markdown potentially containing both local links @@ -145,6 +146,8 @@ The map has two top-level keys: `:inclusions`, a map of constructed keywords to inclusion specifications, and `:text`, an HTML text string with the keywords present where the corresponding inclusion should be inserted." + + ;; TODO: the inclusion->index bug is in here somewhere. ([^String text] (process-text 0 {} (cs/split (or text "") #"```") '())) ([index result fragments processed] @@ -185,19 +188,19 @@ (rest fragments) (cons ident processed)) {:inclusions {ident (apply formatter (list (subs fragment (count first-token)) index))} - :extensions (cons kw (:extensions result))})) + :extensions (assoc (or (:extensions result) {}) kw (-> config :formatters kw))})) :else ;; Otherwise process the current fragment as markdown and recurse on ;; down the list (process-markdown-fragment index result remarked (rest fragments) processed))))) -(process-text - "pswp - ![Frost on a gate, Laurieston](content/uploads/g1.jpg) - ![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg) - ![Feathered snow on log, Taliesin](content/uploads/g3.jpg) - ![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)" ) +;; (process-text +;; "pswp +;; ![Frost on a gate, Laurieston](content/uploads/g1.jpg) +;; ![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg) +;; ![Feathered snow on log, Taliesin](content/uploads/g3.jpg) +;; ![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)" ) (defn reintegrate-inclusions "Given a map of the form produced by `process-text`, return a string of HTML text @@ -236,3 +239,4 @@ + diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index 0fa7cac..a3456c1 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -28,7 +28,8 @@ [com.stuartsierra.component :as component] [smeagol.configuration :refer [config]] [smeagol.include.resolve-local-file :as resolve] - [smeagol.include :as include])) + [smeagol.include :as include] + [smeagol.util :refer [content-dir local-url]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -118,6 +119,9 @@ (def md-include-system + "Allowing Markdown includes. Unfortunately the contributor who contributed + this didn't document it, and I haven't yet worked out how it works. TODO: + investigate and document." (component/start (component/system-map :resolver (resolve/new-resolver util/content-dir) @@ -125,6 +129,7 @@ (include/new-includer) [:resolver])))) + (defn preferred-source "Here, `component` is expected to be a map with two keys, `:local` and `:remote`. If the value of `:extensions-from` in `config.edn` is remote @@ -132,26 +137,51 @@ be returned. Otherwise, if the value of `:local` is nil and the value of `:remote` is non-nil, the value of `:remote` will be returned. By default, the value of `:local` will be returned." - [component] - (let [l (:local component) ;; TODO: look at the trick in Selmer to get relative URL - r (:remote component)] - (cond - (= (:extensions-from config) :remote) (if (empty? r) l r) - (empty? l) r - :else l))) + [component ks] + (try + (let [l (:local component) + l' (if-not (empty? l) (local-url l) l) + r (:remote component)] + (cond + (= (:extensions-from config) :remote) + (if (empty? r) l' r) + (empty? l') r + :else l')) + (catch Exception any + (log/error "Failed to find appropriate source for component" ks "because:" any) + nil))) + +;; (preferred-source {:local "vendor/node_modules/photoswipe/dist/photoswipe.min.js", +;; :remote "https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js"} :core) (defn collect-preferred - "From extensions referenced in this `processed-text`, extract the preferred - URLs for this keyword `k`, expected to be either `:scripts` or `:styles`." - [processed-text k] - (set - (remove - nil? - (map - preferred-source - (apply - concat - (map vals (map k (vals (:extensions processed-text))))))))) + ([processed-text] + (concat + (collect-preferred processed-text :scripts) + (collect-preferred processed-text :styles))) + ([processed-text resource-type] + (reduce concat + (map + (fn [extension-key] + (map + (fn [requirement] + (let [r (preferred-source + (-> processed-text :extensions extension-key resource-type requirement) + requirement)] + (if (empty? r) + (log/warn "Found no valid URL for requirement" + requirement "of extension" extension-key)) + r)) + (keys (-> processed-text :extensions extension-key resource-type)))) + (keys (:extensions processed-text)))))) + +(cjio/file content-dir "vendor/node_modules/photoswipe/dist/photoswipe.min.js") + +(def processed-text (md->html (slurp "resources/public/content/Simplified example gallery.md" ))) + +(preferred-source (-> processed-text :extensions :pswp :scripts :core) :pswp) + +(collect-preferred processed-text :scripts) (defn wiki-page "Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page" diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index 64e0a7c..e92bafe 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -51,6 +51,8 @@ (str (cjio/file content-dir "uploads"))) (def local-url-base + "Essentially, the slash-terminated absolute path of the `public` resource + directory." (let [a (str (fs/absolute content-dir))] (subs a 0 (- (count a) (count "content"))))) @@ -59,7 +61,11 @@ if it is safe to serve. This reason may be logged, but should *not* be shown to remote users, as it would allow file system probing." [file-path] - (let [path (fs/absolute file-path)] + (try + (let [path (if + (cs/starts-with? (str file-path) "/") + file-path + (cjio/file local-url-base file-path))] (cond (cs/includes? file-path "..") (cs/join " " file-path @@ -69,31 +75,57 @@ (not (fs/exists? path)) (cs/join " " [path "does not exist"]) (not (fs/readable? path)) - (cs/join " " [path "is not readable"])))) + (cs/join " " [path "is not readable"]))) + (catch Exception any (cs/join " " file-path "is not servable because" (.getMessage any))))) + + +;; (not-servable-reason "/home/simon/workspace/smeagol/resources/public/content/vendor/node_modules/photoswipe/dist/photoswipe.min.js") +;; (not-servable-reason "/root/froboz") (defn local-url? "True if this `file-path` can be served as a local URL, else false." [file-path] - (empty? (not-servable-reason file-path))) + (try + (if + (empty? (not-servable-reason file-path)) + true + (do + (log/error + "In `smeagol.util/local-url? `" file-path "` is not a servable resource.") + false)) + (catch Exception any + (log/error + "In `smeagol.util/local-url `" file-path "` is not a servable resource:" any) + false))) (defn local-url "Return a local URL for this `file-path`, or a deliberate 404 if none can be safely served." + ;; TODO: this actually returns a relative URL relative to local-url-base. + ;; That's not quite what we want because in Tomcat contexts the absolute + ;; URL may be different. We *ought* to be able to extract the offset from the + ;; servlet context, but it may be simpler to jam it in the config. [file-path] (try - (let [path (fs/absolute file-path) + (let [path (if + (cs/starts-with? file-path local-url-base) + (subs file-path (count local-url-base)) + file-path) problem (not-servable-reason path)] (if (empty? problem) - (subs (str path) (count local-url-base)) + path (do (log/error "In `smeagol.util/local-url `" file-path "` is not a servable resource.") (str "404-not-found?path=" file-path)))) (catch Exception any - (log/error - "In `smeagol.util/local-url `" file-path "` is not a servable resource:" any) - (str "404-not-found?path=" file-path)))) + (log/error + "In `smeagol.util/local-url `" file-path "` is not a servable resource:" any) + (str "404-not-found?path=" file-path)))) + +(local-url? "vendor/node_modules/photoswipe/dist/photoswipe.min.js") +(local-url? "/home/simon/workspace/smeagol/resources/public/vendor/node_modules/photoswipe/dist/photoswipe.min.js") (defn standard-params "Return a map of standard parameters to pass to the template renderer."