From 95ee7e13eb4aaae31aae14531a7d52c67f2c0a47 Mon Sep 17 00:00:00 2001 From: jem Date: Fri, 25 Jan 2019 17:53:27 +0100 Subject: [PATCH 01/37] make error handling more user friendly --- src/smeagol/include/resolve_local_file.clj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/smeagol/include/resolve_local_file.clj b/src/smeagol/include/resolve_local_file.clj index c35e3e5..a603f8a 100644 --- a/src/smeagol/include/resolve_local_file.clj +++ b/src/smeagol/include/resolve_local_file.clj @@ -19,7 +19,11 @@ smeagol.include and not inteded for direct usage." (cond exists? (do (timbre/info (format "Including page '%s' from file '%s'" uri file-path)) - (slurp file-path))))) + (slurp file-path)) + :else + (do + (timbre/info (format "Page '%s' not found at '%s'" uri file-path)) + (str "include not found at " file-path))))) (s/defn new-resolver From 1105eacb05e64c66d1848bd95b4389bf95d793d4 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 5 Feb 2020 10:47:22 +0000 Subject: [PATCH 02/37] lein-release plugin: bumped version from 1.0.3 to 1.0.4-SNAPSHOT for next development cycle --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index b147784..a015883 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject smeagol "1.0.3" +(defproject smeagol "1.0.4-SNAPSHOT" :description "A simple Git-backed Wiki inspired by Gollum" :url "https://github.com/simon-brooke/smeagol" :license {:name "GNU General Public License,version 2.0 or (at your option) any later version" From 38e4207d4a19fe679b843f06dcc27f1ab8afc670 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 5 Feb 2020 17:45:31 +0000 Subject: [PATCH 03/37] Minor change to log message --- src/smeagol/util.clj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index 8ab537d..c290e91 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -66,8 +66,9 @@ (timbre/error any (str - "Failed to parse accept-language header " - specifier)) + "Failed to parse accept-language header '" + specifier + "'")) {}))] (merge messages From 3f667afb355f9694932fb6e5756268369ba50770 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 5 Feb 2020 18:21:02 +0000 Subject: [PATCH 04/37] #44: fixed --- src/smeagol/routes/wiki.clj | 2 +- src/smeagol/uploads.clj | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index b455d78..72df1af 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -165,7 +165,7 @@ "Render a form to allow the upload of a file." [request] (let [params (keywordize-keys (:params request)) - data-path (str util/content-dir "/content/uploads/") + data-path (str util/content-dir "/uploads/") git-repo (hist/load-or-init-repo util/content-dir) upload (:upload params) uploaded (if upload (ul/store-upload params data-path)) diff --git a/src/smeagol/uploads.clj b/src/smeagol/uploads.clj index cdac04a..7fd8c18 100644 --- a/src/smeagol/uploads.clj +++ b/src/smeagol/uploads.clj @@ -59,8 +59,12 @@ (timbre/debug (str "store-upload mv file: " tmp-file " to: " path filename)) (if tmp-file - (do - (.renameTo tmp-file - (File. (str path filename))) - filename) + (try + (do + (.renameTo tmp-file + (File. (str path filename))) + filename) + (catch Exception x + (timbre/error (str "Failed to move " tmp-file " to " path filename "; " (type x) ": " (.getMessage x))) + (throw x))) (throw (Exception. "No file found?"))))) From d6d0a5fc4050ff52b37c0574b1505bde419050a8 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 5 Feb 2020 23:15:48 +0000 Subject: [PATCH 05/37] Another bit of #44, which got missed somehow. --- resources/templates/upload.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/templates/upload.html b/resources/templates/upload.html index a91865e..46a48be 100644 --- a/resources/templates/upload.html +++ b/resources/templates/upload.html @@ -8,7 +8,7 @@ {% i18n file-upload-link-text %}: - ![Uploaded image](content/uploads/{{uploaded}}) + ![Uploaded image](uploads/{{uploaded}})

{% else %}

From a6fb7583149cd0d36cf7ca31c40f6cee72ba027c Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 5 Feb 2020 23:49:02 +0000 Subject: [PATCH 06/37] Separate menu icon onto its own line, relativise the URL --- resources/public/content/User Documentation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/public/content/User Documentation.md b/resources/public/content/User Documentation.md index 92435ff..51489af 100644 --- a/resources/public/content/User Documentation.md +++ b/resources/public/content/User Documentation.md @@ -1,6 +1,7 @@ ## If you're using a small device If you're using a small device, like a mobile phone, the top menu isn't usually displayed. Instead there will be an image like this: -![Menu icon](/img/three-lines.png) + +![Menu icon](img/three-lines.png) at the top left of the page. Touching this image will cause the top menu to be displayed, and it will have all the options described in this documentation. From dad380e0d9192b33cbc3607a7ae49b6679abf472 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Thu, 6 Feb 2020 15:23:59 +0000 Subject: [PATCH 07/37] Attempting to understand why the configuration doesn't load. --- src/smeagol/configuration.clj | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/smeagol/configuration.clj b/src/smeagol/configuration.clj index f4631f9..1f9735e 100644 --- a/src/smeagol/configuration.clj +++ b/src/smeagol/configuration.clj @@ -111,9 +111,19 @@ file is read (if it is specified and present), but that individual values can be overridden by environment variables." (try + (timbre/info (str "Reading configuration from " config-file-path)) (let [file-contents (try (read-string (slurp config-file-path)) - (catch Exception _ {})) + (catch Exception x + (timbre/error + (str + "Failed to read configuration from " + config-file-path + " because: " + (type x) + "; " + (.getMessage x))) + {})) config (merge file-contents (transform-map From 84360110fc964986f27d47b50beb820565fb8ff2 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 7 Feb 2020 14:20:32 +0000 Subject: [PATCH 08/37] Fixes to two minor bugs * Wrong URL printed in upload page; * 'New file Foo' instead of 'New file Edit Foo' as default git commit message for a new file. --- resources/templates/edit.html | 2 +- resources/templates/upload.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/templates/edit.html b/resources/templates/edit.html index 3213d34..3379dc5 100644 --- a/resources/templates/edit.html +++ b/resources/templates/edit.html @@ -13,7 +13,7 @@

+ value="{%if exists%}{%else%}New file {{page}}{%endif%}" required/>

diff --git a/resources/templates/upload.html b/resources/templates/upload.html index 46a48be..53284ec 100644 --- a/resources/templates/upload.html +++ b/resources/templates/upload.html @@ -4,7 +4,7 @@ {% if uploaded %} {% if is-image %}

- Uploaded image + Uploaded image {% i18n file-upload-link-text %}: From da3bde16d0885beafe292057259808f69714778a Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 7 Feb 2020 19:01:30 +0000 Subject: [PATCH 09/37] Added 'list uploaded files' page, only accessible if logged in. --- .gitignore | 2 + project.clj | 2 + resources/i18n/en-GB.edn | 7 +- resources/public/content/_edit-side-bar.md | 4 +- resources/templates/list-uploads.html | 34 +++++++ resources/templates/upload.html | 3 + src/smeagol/routes/wiki.clj | 102 +++++++++++++++++---- src/smeagol/uploads.clj | 6 +- 8 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 resources/templates/list-uploads.html diff --git a/.gitignore b/.gitignore index 2a76665..95c58ff 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ smeagol.log* /node_modules/ .DS_Store + +resources/public/content/uploads/ diff --git a/project.clj b/project.clj index a015883..d9b5cda 100644 --- a/project.clj +++ b/project.clj @@ -5,6 +5,7 @@ :url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"} :dependencies [[clj-jgit "0.8.10"] [clj-yaml "0.4.0"] + [clojure.java-time "0.3.2"] [com.cemerick/url "0.1.1"] [com.fzakaria/slf4j-timbre "0.3.7"] [com.stuartsierra/component "0.4.0"] @@ -17,6 +18,7 @@ [im.chit/cronj "1.4.4"] [lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]] [markdown-clj "0.9.99" :exclusions [com.keminglabs/cljx]] + [me.raynes/fs "1.4.6"] [noir-exception "0.2.5"] [org.clojars.simon_brooke/internationalisation "1.0.3"] [org.clojure/clojure "1.8.0"] diff --git a/resources/i18n/en-GB.edn b/resources/i18n/en-GB.edn index 64c164c..6f4d8b2 100644 --- a/resources/i18n/en-GB.edn +++ b/resources/i18n/en-GB.edn @@ -83,6 +83,8 @@ :file-upload-title "Upload a file" ;; title for the file upload page :is-admin-prompt "Is administrator?" :here "here" ;; used in sanity check report + :history-link "History" ;; text of the history link on the content frame + :history-title-prefix "History of" ;; prefix of the title on the history page :home-link "Home" ;; text of the home link on the menu :is-not-directory "is not a directory" ;; (of a file or directory) used in sanity check report @@ -90,6 +92,8 @@ ;; (of a file or directory) used in sanity check report :is-not-writable "is not writable" ;; (of a file or directory) used in sanity check report + :list-files "List uploaded files" + ;; title of the 'List uploaded Files' page :login-label "Log in!" ;; text of the login widget on the login page :login-link "Log in" ;; text of the login link on the menu :login-prompt "To edit this wiki" @@ -98,8 +102,7 @@ :logout-link "Log out" ;; text of the logout link on the menu :logged-in-as "You are logged in as" ;; text of the 'logged in as' label on the menu - :history-link "History" ;; text of the history link on the content frame - :history-title-prefix "History of" ;; prefix of the title on the history page + :matching "matching" ;; 'matching' in e.g. 'list files matching fred' :new-pass-prompt "New password" ;; text of the new password widget prompt on the change ;; password and edit user pages :no-admin-users "There are no users in the 'passwd' file with administrative privileges" diff --git a/resources/public/content/_edit-side-bar.md b/resources/public/content/_edit-side-bar.md index bbd61ca..ccec0c0 100644 --- a/resources/public/content/_edit-side-bar.md +++ b/resources/public/content/_edit-side-bar.md @@ -10,4 +10,6 @@ + \*\***bold**\*\* + \__italic_\_ -More documentation [here](http://daringfireball.net/projects/markdown/syntax) \ No newline at end of file +More documentation [here](http://daringfireball.net/projects/markdown/syntax) + +Your uploaded files are listed here. diff --git a/resources/templates/list-uploads.html b/resources/templates/list-uploads.html new file mode 100644 index 0000000..761e4a8 --- /dev/null +++ b/resources/templates/list-uploads.html @@ -0,0 +1,34 @@ +{% extends "templates/base.html" %} + +{% block content %} +

+
+ {% csrf-field %} +

+ + +

+
+ + + + + + + + {% for entry in files %} + + + + + + + + {% endfor %} +
NameUploadedType thisTo get this
{{entry.base-name}}{{entry.modified}} + {% if entry.is-image %} ![{{entry.name|capitalize}}](uploads/{{entry.base-name}}) {% else %} [{{entry.name|capitalize}}](uploads/{{entry.base-name}}) {% endif %} + + {% if entry.is-image %} {{entry.name|capitalize}} {% else %} link {% endif %} +
+
+{% endblock %} diff --git a/resources/templates/upload.html b/resources/templates/upload.html index 53284ec..7c0c373 100644 --- a/resources/templates/upload.html +++ b/resources/templates/upload.html @@ -34,5 +34,8 @@

{% endif %} +

+ Your uploaded files are listed here. +

{% endblock %} diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index 72df1af..5457fa1 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -7,6 +7,8 @@ [clojure.string :as cs] [clojure.walk :refer :all] [compojure.core :refer :all] + [java-time :as jt] + [me.raynes.fs :as fs] [noir.io :as io] [noir.response :as response] [noir.util.route :as route] @@ -161,6 +163,75 @@ :page page :history (hist/find-history repo-path file-name)})))) +(def image-extns #{".gif" ".jpg" ".jpeg" ".png"}) + +(defn format-instant + "Format this `unix-time`, expected to be a Long, into something human readable. + If `template` is supplied, use that as the formatting template as specified for + java.time.Formatter. Assumes system default timezone. Returns a string." + ([^Long unix-time] + (format-instant unix-time "EEEE, dd MMMM YYYY")) + ([^Long unix-time ^String template] + (jt/format + (java-time/formatter template) + (java.time.LocalDateTime/ofInstant + (java-time/instant unix-time) + (java.time.ZoneOffset/systemDefault))))) + +(defn list-uploads-page + "Render a list of all uploaded files" + [request] + (let + [params (keywordize-keys (:params request)) + data-path (str util/content-dir "/uploads/") + files + (map + #(zipmap + [:base-name :is-image :modified :name] + [(fs/base-name %) + (if + (and (fs/extension %) + (image-extns (cs/lower-case (fs/extension %)))) + true false) + (if + (fs/mod-time %) + (format-instant (fs/mod-time %))) + (fs/name %)]) + (remove + #(or (cs/starts-with? (fs/name %) ".") + (fs/directory? %)) + (file-seq (clojure.java.io/file data-path))))] + (layout/render + "list-uploads.html" + (merge (util/standard-params request) + {:title (str + (util/get-message :list-files request) + (if + (:search params) + (str " " (util/get-message :matching request)))) + :search (:search params) + :files (if + (:search params) + (try + (let [pattern (re-pattern (:search params))] + (filter + #(re-find pattern (:base-name %)) + files)) + (catch Exception _ files)) + files) + })))) + +(map + #(zipmap + [:base-name :is-image :modified :name] + [(fs/base-name %) + (if + (and (fs/extension %) (image-extns (cs/lower-case (fs/extension %)))) + true false) + (fs/mod-time %) + (fs/name %)]) + (file-seq (clojure.java.io/file "resources/public/content/uploads"))) + (defn upload-page "Render a form to allow the upload of a file." [request] @@ -174,23 +245,17 @@ (if uploaded (do - (git/git-add git-repo uploaded) + (git/git-add git-repo (str data-path (fs/name uploaded))) (git/git-commit git-repo summary {:name user :email (auth/get-email user)}))) (layout/render "upload.html" (merge (util/standard-params request) {:title (util/get-message :file-upload-title request) - :uploaded uploaded - :is-image (and + :uploaded (if uploaded (fs/base-name uploaded)) + :is-image (if uploaded - (or - (cs/ends-with? uploaded ".gif") - (cs/ends-with? uploaded ".jpg") - (cs/ends-with? uploaded ".jpeg") - (cs/ends-with? uploaded ".png") - (cs/ends-with? uploaded ".GIF") - (cs/ends-with? uploaded ".JPG") - (cs/ends-with? uploaded ".PNG")))})))) - + (image-extns + (cs/lower-case + (fs/extension uploaded))))})))) (defn version-page "Render a specific historical version of a page" @@ -286,8 +351,10 @@ (defroutes wiki-routes - (GET "/wiki" request (wiki-page request)) (GET "/" request (wiki-page request)) + (GET "/auth" request (auth-page request)) + (POST "/auth" request (auth-page request)) + (GET "/changes" request (diff-page request)) (GET "/delete-user" request (route/restricted (admin/delete-user request))) (GET "/edit" request (route/restricted (edit-page request))) (POST "/edit" request (route/restricted (edit-page request))) @@ -297,11 +364,12 @@ (GET "/edit-user" request (route/restricted (admin/edit-user request))) (POST "/edit-user" request (route/restricted (admin/edit-user request))) (GET "/history" request (history-page request)) + (GET "/list-uploads" request (route/restricted (list-uploads-page request))) + (POST "/list-uploads" request (route/restricted (list-uploads-page request))) (GET "/version" request (version-page request)) - (GET "/changes" request (diff-page request)) - (GET "/auth" request (auth-page request)) - (POST "/auth" request (auth-page request)) (GET "/passwd" request (passwd-page request)) (POST "/passwd" request (passwd-page request)) (GET "/upload" request (route/restricted (upload-page request))) - (POST "/upload" request (route/restricted (upload-page request)))) + (POST "/upload" request (route/restricted (upload-page request))) + (GET "/wiki" request (wiki-page request)) + ) diff --git a/src/smeagol/uploads.clj b/src/smeagol/uploads.clj index 7fd8c18..7cadaea 100644 --- a/src/smeagol/uploads.clj +++ b/src/smeagol/uploads.clj @@ -49,7 +49,9 @@ "Store an upload both to the file system and to the database. The issue with storing an upload is moving it into place. If `params` are passed as a map, it is expected that this is a map from - an HTTP POST operation of a form with type `multipart/form-data`." + an HTTP POST operation of a form with type `multipart/form-data`. + + On success, returns the file object uploaded." [params path] (let [upload (:upload params) tmp-file (:tempfile upload) @@ -63,7 +65,7 @@ (do (.renameTo tmp-file (File. (str path filename))) - filename) + (File. (str path filename))) (catch Exception x (timbre/error (str "Failed to move " tmp-file " to " path filename "; " (type x) ": " (.getMessage x))) (throw x))) From 59f86925e84d10f746fa2eb2f4b23a5ab0e2a199 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 7 Feb 2020 22:47:46 +0000 Subject: [PATCH 10/37] Corrected link rot (hadn't I done this before?) --- resources/public/content/Extensible Markup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/public/content/Extensible Markup.md b/resources/public/content/Extensible Markup.md index e0b1fed..129a375 100644 --- a/resources/public/content/Extensible Markup.md +++ b/resources/public/content/Extensible Markup.md @@ -4,7 +4,7 @@ A system of pluggable, extensible formatters is supported. In normal markdown, c ## The Vega formatter -Inspired by [visdown](http://visdown.amitkaps.com/) and [vega-lite](https://vega.github.io/vega-lite/docs/), the Vega formatter allows you to embed vega data visualisations into Smeagol pages. The graph description should start with a line comprising three back-ticks and then the word '`vega`', and end with a line comprising just three backticks. +Inspired by [visdown](https://visdown.com/) and [vega-lite](https://vega.github.io/vega-lite/docs/), the Vega formatter allows you to embed vega data visualisations into Smeagol pages. The graph description should start with a line comprising three back-ticks and then the word '`vega`', and end with a line comprising just three backticks. Here's an example cribbed in its entirety from [here](http://visdown.amitkaps.com/london): @@ -34,7 +34,7 @@ Data files can be uploaded in the same way as images, by using the **upload a fi ## The Mermaid formatter -Graphs can now be embedded in a page using the [Mermaid](http://knsv.github.io/mermaid/index.html) graph description language. The graph description should start with a line comprising three back-ticks and then the word `mermaid`, and end with a line comprising just three backticks. +Graphs can now be embedded in a page using the [Mermaid](https://mermaid-js.github.io/mermaid/#/) graph description language. The graph description should start with a line comprising three back-ticks and then the word `mermaid`, and end with a line comprising just three backticks. Here's an example culled from the Mermaid documentation. From ecd9d5a2707bee6c782c4540eb677794099432de Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 7 Feb 2020 22:49:00 +0000 Subject: [PATCH 11/37] Unfinished (but non-breaking) work on after-auth redirect --- src/smeagol/routes/wiki.clj | 42 +++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index 5457fa1..a8e13ce 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -163,6 +163,9 @@ :page page :history (hist/find-history repo-path file-name)})))) +;;;; this next section is all stuff supporting the list-uploads page, and maybe +;;;; should be moved to its own file. + (def image-extns #{".gif" ".jpg" ".jpeg" ".png"}) (defn format-instant @@ -221,16 +224,7 @@ files) })))) -(map - #(zipmap - [:base-name :is-image :modified :name] - [(fs/base-name %) - (if - (and (fs/extension %) (image-extns (cs/lower-case (fs/extension %)))) - true false) - (fs/mod-time %) - (fs/name %)]) - (file-seq (clojure.java.io/file "resources/public/content/uploads"))) +;;;; end of list-uploads section ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn upload-page "Render a form to allow the upload of a file." @@ -302,12 +296,14 @@ [request] (or (show-sanity-check-error) - (let [params (keywordize-keys (:form-params request)) - username (:username params) - password (:password params) - action (:action params) + (let [params (keywordize-keys (:params request)) + form-params (keywordize-keys (:form-params request)) + username (:username form-params) + password (:password form-params) + action (:action form-params) user (session/get :user) - redirect-to (or (:redirect-to params) "/wiki")] + redirect-to (:redirect-to params)] + (if redirect-to (timbre/info (str "After auth, redirect to: " redirect-to))) (cond (= action (util/get-message :logout-label request)) (do @@ -324,8 +320,22 @@ {:title (if user (str (util/get-message :logout-link request) " " user) (util/get-message :login-link request)) - :redirect-to ((:headers request) "referer")})))))) + :redirect-to redirect-to})))))) +(defn wrap-restricted-redirect + ;; TODO: this is not idiomatic, and it's too late to write something idiomatic just now + [f request] + (route/restricted + (apply + f + (if + (-> request :params :redirect-to) ;; a redirect target has already been set + request + ;; else merge a redirect target into the params + (let + [redirect-to (if (:uri request) + (cs/join "?" [(:uri request) (:query-string request)]))] + (assoc-in request [:params :redirect-to] redirect-to)))))) (defn passwd-page "Render a page to change the user password" From e00beaf790cea5e7f3ba146d617fad63163d8317 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Sat, 8 Feb 2020 10:42:29 +0000 Subject: [PATCH 12/37] Trying to get JavaScript switchable between local and cloudflare Not working for two reasons: 1. `lein npm install` does not build packages on MacOS; 2. `{% ifequal js-from ":cloudflare" %}` breaks Selmer in wiki.html but not in edit.html - WTF? --- package-lock.json | 1339 +++++++++++++++++++++++++++++++++ project.clj | 14 +- resources/config.edn | 4 +- resources/templates/edit.html | 9 +- resources/templates/wiki.html | 19 +- src/smeagol/configuration.clj | 2 + src/smeagol/formatting.clj | 2 +- src/smeagol/util.clj | 1 + 8 files changed, 1371 insertions(+), 19 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e5aa9b7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1339 @@ +{ + "name": "smeagol", + "version": "1.0.4-SNAPSHOT", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@braintree/sanitize-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-3.1.0.tgz", + "integrity": "sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg==" + }, + "@types/clone": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz", + "integrity": "sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=" + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, + "@types/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha512-mky/O83TXmGY39P1H9YbUpjV6l6voRYlufqfFCvel8l1phuy8HRjdWc1rrPuN53ITBJlbyMSV6z3niOySO5pgQ==" + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "1.1.1", + "color-convert": "2.0.1" + } + }, + "array-flat-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz", + "integrity": "sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "requires": { + "no-case": "2.3.2", + "upper-case": "1.1.3" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "requires": { + "source-map": "0.6.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "4.2.0", + "strip-ansi": "6.0.0", + "wrap-ansi": "6.2.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "codemirror": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.51.0.tgz", + "integrity": "sha512-vyuYYRv3eXL0SCuZA4spRFlKNzQAewHcipRQCOKgRy7VNAvZxTKzbItdbCl4S5AgPZ5g3WkHp+ibWQwv9TLG7Q==" + }, + "codemirror-spell-checker": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz", + "integrity": "sha1-HGYPkIlIPMtRE7m6nKGcP0mTNx4=", + "requires": { + "typo-js": "1.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "crypto-random-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.1.0.tgz", + "integrity": "sha512-Tip3yGB+bA7B0W8E4K4mNf2rZhu5r2G5Tb89/utEl5tP1QuLjTF/S9a1b8ifDrR4ORc9Utf6tscpSEtBY3YcPQ==", + "requires": { + "type-fest": "0.8.1" + } + }, + "css-b64-images": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz", + "integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI=" + }, + "d3": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.15.0.tgz", + "integrity": "sha512-C+E80SL2nLLtmykZ6klwYj5rPqB5nlfN5LdWEAVdWPppqTD8taoJi2PxLZjPeYT8FFRR2yucXq+kBlOnnvZeLg==", + "requires": { + "d3-array": "1.2.4", + "d3-axis": "1.0.12", + "d3-brush": "1.1.5", + "d3-chord": "1.0.6", + "d3-collection": "1.0.7", + "d3-color": "1.4.0", + "d3-contour": "1.3.2", + "d3-dispatch": "1.0.6", + "d3-drag": "1.2.5", + "d3-dsv": "1.2.0", + "d3-ease": "1.0.6", + "d3-fetch": "1.1.2", + "d3-force": "1.2.1", + "d3-format": "1.4.3", + "d3-geo": "1.11.9", + "d3-hierarchy": "1.1.9", + "d3-interpolate": "1.4.0", + "d3-path": "1.0.9", + "d3-polygon": "1.0.6", + "d3-quadtree": "1.0.7", + "d3-random": "1.1.2", + "d3-scale": "2.2.2", + "d3-scale-chromatic": "1.5.0", + "d3-selection": "1.4.1", + "d3-shape": "1.3.7", + "d3-time": "1.1.0", + "d3-time-format": "2.2.3", + "d3-timer": "1.0.10", + "d3-transition": "1.3.2", + "d3-voronoi": "1.1.4", + "d3-zoom": "1.8.3" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.5.tgz", + "integrity": "sha512-rEaJ5gHlgLxXugWjIkolTA0OyMvw8UWU1imYXy1v642XyyswmI1ybKOv05Ft+ewq+TFmdliD3VuK0pRp1VT/5A==", + "requires": { + "d3-dispatch": "1.0.6", + "d3-drag": "1.2.5", + "d3-interpolate": "1.4.0", + "d3-selection": "1.4.1", + "d3-transition": "1.3.2" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1.2.4", + "d3-path": "1.0.9" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz", + "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "1.2.4" + } + }, + "d3-delaunay": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.2.1.tgz", + "integrity": "sha512-ZZdeJl6cKRyqYVFYK+/meXvWIrAvZsZTD7WSxl4OPXCmuXNgDyACAClAJHD63zL25TA+IJGURUNO7rFseNFCYw==", + "requires": { + "delaunator": "4.0.1" + } + }, + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "requires": { + "d3-dispatch": "1.0.6", + "d3-selection": "1.4.1" + } + }, + "d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "requires": { + "commander": "2.20.3", + "iconv-lite": "0.4.24", + "rw": "1.3.3" + } + }, + "d3-ease": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.6.tgz", + "integrity": "sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ==" + }, + "d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "requires": { + "d3-dsv": "1.2.0" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1.0.7", + "d3-dispatch": "1.0.6", + "d3-quadtree": "1.0.7", + "d3-timer": "1.0.10" + } + }, + "d3-format": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.3.tgz", + "integrity": "sha512-mm/nE2Y9HgGyjP+rKIekeITVgBtX97o1nrvHCWX8F/yBYyevUTvu9vb5pUnKwrcSw7o7GuwMOWjS9gFDs4O+uQ==" + }, + "d3-geo": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.9.tgz", + "integrity": "sha512-9edcH6J3s/Aa3KJITWqFJbyB/8q3mMlA9Fi7z6yy+FAYMnRaxmC7jBhUnsINxVWD14GmqX3DK8uk7nV6/Ekt4A==", + "requires": { + "d3-array": "1.2.4" + } + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1.4.0" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "1.2.4", + "d3-collection": "1.0.7", + "d3-format": "1.4.3", + "d3-interpolate": "1.4.0", + "d3-time": "1.1.0", + "d3-time-format": "2.2.3" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1.4.0", + "d3-interpolate": "1.4.0" + } + }, + "d3-selection": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.1.tgz", + "integrity": "sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1.0.9" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz", + "integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==", + "requires": { + "d3-time": "1.1.0" + } + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "requires": { + "d3-color": "1.4.0", + "d3-dispatch": "1.0.6", + "d3-ease": "1.0.6", + "d3-interpolate": "1.4.0", + "d3-selection": "1.4.1", + "d3-timer": "1.0.10" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1.0.6", + "d3-drag": "1.2.5", + "d3-interpolate": "1.4.0", + "d3-selection": "1.4.1", + "d3-transition": "1.3.2" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "2.1.8", + "lodash": "4.17.15" + } + }, + "dagre-d3": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz", + "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", + "requires": { + "d3": "5.15.0", + "dagre": "0.8.5", + "graphlib": "2.1.8", + "lodash": "4.17.15" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "delaunator": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", + "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escaper": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/escaper/-/escaper-2.5.3.tgz", + "integrity": "sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ==" + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "fast-json-patch": { + "version": "3.0.0-1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz", + "integrity": "sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "5.0.0", + "path-exists": "4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "requires": { + "lodash": "4.17.15" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "requires": { + "camel-case": "3.0.0", + "clean-css": "4.2.3", + "commander": "2.20.3", + "he": "1.2.0", + "param-case": "2.1.1", + "relateurl": "0.2.7", + "uglify-js": "3.7.7" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + }, + "json-stringify-pretty-compact": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz", + "integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "4.1.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" + }, + "marked": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.0.tgz", + "integrity": "sha512-MyUe+T/Pw4TZufHkzAfDj6HarCBWia2y27/bhuYkTaiUnfDYFnCP3KUN+9oM7Wi6JA2rymtVYbQu3spE0GCmxQ==" + }, + "mermaid": { + "version": "8.4.6", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.4.6.tgz", + "integrity": "sha512-6YQBkXfvhfjKIzRhtqbCics3pJurGrJAYEeqgyRcDZeTHQ/WCB2Bh/4wdAOho1Uffe0jXB+HjmHT5kEUOxudJw==", + "requires": { + "@braintree/sanitize-url": "3.1.0", + "crypto-random-string": "3.1.0", + "d3": "5.15.0", + "dagre": "0.8.5", + "dagre-d3": "0.6.4", + "graphlib": "2.1.8", + "he": "1.2.0", + "lodash": "4.17.15", + "minify": "4.1.3", + "moment-mini": "2.22.1", + "scope-css": "1.2.1" + } + }, + "minify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz", + "integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==", + "requires": { + "clean-css": "4.2.3", + "css-b64-images": "0.2.5", + "debug": "4.1.1", + "html-minifier": "4.0.0", + "terser": "4.6.3", + "try-catch": "2.0.1", + "try-to-catch": "1.1.1" + } + }, + "moment-mini": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.22.1.tgz", + "integrity": "sha512-OUCkHOz7ehtNMYuZjNciXUfwTuz8vmF1MTbAy59ebf+ZBYZO5/tZKuChVWCX+uDo+4idJBpGltNfV8st+HwsGw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "requires": { + "lower-case": "1.1.4" + } + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "requires": { + "p-try": "2.2.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "2.2.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "requires": { + "no-case": "2.3.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "scope-css": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/scope-css/-/scope-css-1.2.1.tgz", + "integrity": "sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q==", + "requires": { + "escaper": "2.5.3", + "slugify": "1.3.6", + "strip-css-comments": "3.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "simplemde": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/simplemde/-/simplemde-1.11.2.tgz", + "integrity": "sha1-ojo12XjSxA7wfewAjJLwcNjggOM=", + "requires": { + "codemirror": "5.51.0", + "codemirror-spell-checker": "1.1.2", + "marked": "0.8.0" + } + }, + "slugify": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.6.tgz", + "integrity": "sha512-wA9XS475ZmGNlEnYYLPReSfuz/c3VQsEMoU43mi6OnKMCdbnFXd4/Yg7J0lBv8jkPolacMpOrWEaoYxuE1+hoQ==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "requires": { + "buffer-from": "1.1.1", + "source-map": "0.6.1" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "8.0.0", + "is-fullwidth-code-point": "3.0.0", + "strip-ansi": "6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "5.0.0" + } + }, + "strip-css-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-css-comments/-/strip-css-comments-3.0.0.tgz", + "integrity": "sha1-elYl7/iisibPiUehElTaluE9rok=", + "requires": { + "is-regexp": "1.0.0" + } + }, + "tablesort": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.2.0.tgz", + "integrity": "sha512-uiG1z/yvuZPod3uesK78bNMtLisnbQzNeD+CDQSZsrZTK2AzMjCmrTa1NUPBVLwD43VxJnIUYlrTM1jYl+HZFQ==" + }, + "terser": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", + "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", + "requires": { + "commander": "2.20.3", + "source-map": "0.6.1", + "source-map-support": "0.5.16" + } + }, + "topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "requires": { + "commander": "2.20.3" + } + }, + "try-catch": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz", + "integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg==" + }, + "try-to-catch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz", + "integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA==" + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "typo-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.1.0.tgz", + "integrity": "sha512-W3kLbx+ML9PBl5Bzso/lTvVxk4BCveSNAtQeht59FEtxCdGThmn6wSHA4Xq3eQYAK24NHdisMM4JmsK0GFy/pg==" + }, + "uglify-js": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.7.tgz", + "integrity": "sha512-FeSU+hi7ULYy6mn8PKio/tXsdSXN35lm4KgV2asx00kzrLU9Pi3oAslcJT70Jdj7PHX29gGUPOT6+lXGBbemhA==", + "requires": { + "commander": "2.20.3", + "source-map": "0.6.1" + } + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" + }, + "vega": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/vega/-/vega-5.8.0.tgz", + "integrity": "sha512-9bxZ62Ra+M4ZaEnVqw9+DhN2ttGjKTLpKVjvfhuXqXAXmCqBPXfcez1/9ZNFoYHc0s2Xf6+uYzHj7aLg9G0zMw==", + "requires": { + "vega-crossfilter": "4.0.1", + "vega-dataflow": "5.4.1", + "vega-encode": "4.5.0", + "vega-event-selector": "2.0.1", + "vega-expression": "2.6.2", + "vega-force": "4.0.3", + "vega-functions": "5.5.0", + "vega-geo": "4.2.0", + "vega-hierarchy": "4.0.3", + "vega-loader": "4.1.3", + "vega-parser": "5.11.0", + "vega-projection": "1.3.0", + "vega-regression": "1.0.2", + "vega-runtime": "5.0.2", + "vega-scale": "5.0.0", + "vega-scenegraph": "4.4.0", + "vega-statistics": "1.7.0", + "vega-time": "1.0.0", + "vega-transforms": "4.5.0", + "vega-typings": "0.11.0", + "vega-util": "1.12.0", + "vega-view": "5.3.2", + "vega-view-transforms": "4.5.0", + "vega-voronoi": "4.1.1", + "vega-wordcloud": "4.0.3" + }, + "dependencies": { + "vega-event-selector": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-2.0.1.tgz", + "integrity": "sha512-FGU1PefYhW9An6zVs6TE5f/XGYsIispxFErG/p9KThxL22IC90WVZzMQXKN9M8OcARq5OyWjHg3qa9Qp/Z6OJw==" + }, + "vega-expression": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-2.6.2.tgz", + "integrity": "sha512-vh8GVkAL/KtsgcdrdKdEnysZn/InIuRrkF7U+CG1eAmupMucPY/Rpu0nCdYb4CLC/xNRHx/NMFidLztQUjZJQg==", + "requires": { + "vega-util": "1.12.0" + } + }, + "vega-typings": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-0.11.0.tgz", + "integrity": "sha512-i67WNmzm1ymswDT8t54dOQusDUO+95Lzxmum0YkNrBIXso1hwFWUT1les0swNlKzSJPjhTofN2OrPXa6C+uf6w==", + "requires": { + "vega-util": "1.12.0" + } + }, + "vega-util": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.12.0.tgz", + "integrity": "sha512-eN1PAQVDyEOcwild2Fk1gbkzkqgDHNujG2/akYRtBzkhtz2EttrVIDwBkWqV/Q+VvEINEksb7TI3Wv7qVQFR5g==" + } + } + }, + "vega-canvas": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-1.2.1.tgz", + "integrity": "sha512-k/S3EPeJ37D7fYDhv4sEg7fNWVpLheQY7flfLyAmJU7aSwCMgw8cZJi0CKHchJeculssfH+41NCqvRB1QtaJnw==" + }, + "vega-crossfilter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-4.0.1.tgz", + "integrity": "sha512-wLNS4JzKaOLj8EAzI/v8XBJjUWMRWYSu6EeQF4o9Opq/78u87Ol9Lc5I27UHsww5dNNH/tHubAV4QPIXnGOp5Q==", + "requires": { + "d3-array": "2.4.0", + "vega-dataflow": "5.4.1", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-dataflow": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-5.4.1.tgz", + "integrity": "sha512-NZASrIGel2ZD+HiJsozMPO7qNB3INLFWQez6KI+gPpKQIhsz7jWzG/TBK57A8NOLJYPc6VBLiygCmdJbr5E+sA==", + "requires": { + "vega-loader": "4.1.3", + "vega-util": "1.12.2" + } + }, + "vega-embed": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-6.2.2.tgz", + "integrity": "sha512-u9CX9jpIbu2DY2lqs0J7CLA7+VXNlR+qkzxOwaVKfS0ivB54dxQXKvE59VHV02Nwb5XhaDGTEMN0FRCAvgkdLQ==", + "requires": { + "fast-json-patch": "3.0.0-1", + "json-stringify-pretty-compact": "2.0.0", + "semver": "6.3.0", + "vega-schema-url-parser": "1.1.0", + "vega-themes": "2.6.1", + "vega-tooltip": "0.20.1" + } + }, + "vega-encode": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-4.5.0.tgz", + "integrity": "sha512-oFwsYOvqwVokyY7sVCSvJr2aqJQpKQ5rbpjia/9lRMgluZUFkfLidZ1foQ++/UHISjvC/mWVNAGq3hsTV06xCQ==", + "requires": { + "d3-array": "2.4.0", + "d3-format": "1.4.3", + "d3-interpolate": "1.4.0", + "vega-dataflow": "5.4.1", + "vega-scale": "5.0.0", + "vega-time": "1.0.0", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-event-selector": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-2.0.2.tgz", + "integrity": "sha512-Uv72vBfM0lrlI2belKHFMZuVnW2uJl2ShqWPwGSXPVe6p+PzgqoPJYC8A/i5N8B54UA4UMDzlbBeo3x7q2W9Yg==" + }, + "vega-expression": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-2.6.3.tgz", + "integrity": "sha512-sME1+45BToTGsftb1Q6Ubs2iRYEoXkD2NRGnJuKS9YJ2ITzZwPHF/jy2kHW3iLpuNjj54meaO7HMQ/hUKrciUw==", + "requires": { + "vega-util": "1.12.2" + } + }, + "vega-force": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-4.0.3.tgz", + "integrity": "sha512-4stItN4jD9H1CENaCz4jXRNS1Bi9cozMOUjX2824FeJENi2RZSiAZAaGbscgerZQ/jbNcOHD8PHpC2pWldEvGA==", + "requires": { + "d3-force": "2.0.1", + "vega-dataflow": "5.4.1", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-force": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.0.1.tgz", + "integrity": "sha512-zh73/N6+MElRojiUG7vmn+3vltaKon7iD5vB/7r9nUaBeftXMzRo5IWEG63DLBCto4/8vr9i3m9lwr1OTJNiCg==", + "requires": { + "d3-dispatch": "1.0.6", + "d3-quadtree": "1.0.7", + "d3-timer": "1.0.10" + } + } + } + }, + "vega-functions": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-5.5.0.tgz", + "integrity": "sha512-kWNrwWr8Gphm3uZn58OaNT0eUdkZvOUhVUK1ec0NjfZofeVI2V6DzFrQNRvTlvUz269l7G88v1cdsQeXOJcZmg==", + "requires": { + "d3-array": "2.4.0", + "d3-color": "1.4.0", + "d3-format": "1.4.3", + "d3-geo": "1.11.9", + "d3-time-format": "2.2.3", + "vega-dataflow": "5.4.1", + "vega-expression": "2.6.3", + "vega-scale": "5.0.0", + "vega-scenegraph": "4.4.0", + "vega-selections": "5.1.0", + "vega-statistics": "1.7.0", + "vega-time": "1.0.0", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-geo": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-4.2.0.tgz", + "integrity": "sha512-umhG1645fB1iRsKAP/ZsS0b+gcW/i8DzRUihWJ83VHnlUS+Py/oocSOtx4QqfRW8xVGy/cGzflj52NhLsVW+HQ==", + "requires": { + "d3-array": "2.4.0", + "d3-color": "1.4.0", + "d3-geo": "1.11.9", + "vega-canvas": "1.2.1", + "vega-dataflow": "5.4.1", + "vega-projection": "1.3.0", + "vega-statistics": "1.7.0", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-hierarchy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-4.0.3.tgz", + "integrity": "sha512-9wNe+KyKqZW1S4++jCC38HuAhZbqNhfY7gOvwiMLjsp65tMtRETrtvYfHkULClm3UokUIX54etAXREAGW7znbw==", + "requires": { + "d3-hierarchy": "1.1.9", + "vega-dataflow": "5.4.1", + "vega-util": "1.12.2" + } + }, + "vega-lite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-4.1.1.tgz", + "integrity": "sha512-D2seO6ZbY8aZQ8+ZQfU+5NYwot3ryIDyvdQdcVoupMSgJ/oGv4QqEwL3rmu8abdSG6NhFiac0trsI+wBb0F6vQ==", + "requires": { + "@types/clone": "0.1.30", + "@types/fast-json-stable-stringify": "2.0.0", + "array-flat-polyfill": "1.0.1", + "clone": "2.1.2", + "fast-deep-equal": "3.1.1", + "fast-json-stable-stringify": "2.1.0", + "json-stringify-pretty-compact": "2.0.0", + "tslib": "1.10.0", + "vega-event-selector": "2.0.2", + "vega-expression": "2.6.3", + "vega-typings": "0.12.3", + "vega-util": "1.12.2", + "yargs": "15.1.0" + } + }, + "vega-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-4.1.3.tgz", + "integrity": "sha512-50aetjuct4WqU7LctwnZqF/NCyya9aZ1HDQZ9unFi++62vOQgRfbXLNL/dZavqwnWX3S9i0ltCznLyFMG4ck8g==", + "requires": { + "d3-dsv": "1.2.0", + "d3-time-format": "2.2.3", + "node-fetch": "2.6.0", + "topojson-client": "3.1.0", + "vega-util": "1.12.2" + } + }, + "vega-parser": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-5.11.0.tgz", + "integrity": "sha512-5JJTHtA7DkH8aMQQCiveAaFp3qwSApGfj3BKV1dxc/i3/bUB0xqaEeXju2fcBIo9UlPrpX+dj+DAz3Qb4kMfog==", + "requires": { + "vega-dataflow": "5.4.1", + "vega-event-selector": "2.0.2", + "vega-expression": "2.6.3", + "vega-functions": "5.5.0", + "vega-scale": "5.0.0", + "vega-util": "1.12.2" + } + }, + "vega-projection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-1.3.0.tgz", + "integrity": "sha512-BFOc/XSVVW96WIAAyiUcppCeegniibiKGX0OLbGpQ5WIbeDHsbCXqnkeBpD5wsjvPXaiQRHTZ0PZ8VvCoCQV+g==", + "requires": { + "d3-geo": "1.11.9" + } + }, + "vega-regression": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-1.0.2.tgz", + "integrity": "sha512-wWGurNFzkF0kxeM4bT3bZXplJYzyvuPDS2jWPx7r08l5VHcf7qc4xBetpCnNtrdG2Pg3hXGZrUWqA3/ufrfUdA==", + "requires": { + "d3-array": "2.4.0", + "vega-dataflow": "5.4.1", + "vega-statistics": "1.7.0", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-runtime": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-5.0.2.tgz", + "integrity": "sha512-Cuv+RY6kprH+vtNERg6xP4dgcdYGD2ZnxPxJNEtGi7dmtQQTBa1s7jQ0VDXTolsO6lKJ3B7np2GzKJYwevgj1A==", + "requires": { + "vega-dataflow": "5.4.1", + "vega-util": "1.12.2" + } + }, + "vega-scale": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-5.0.0.tgz", + "integrity": "sha512-HlCDdWPK1U769JTIkrlad3W/AVrZP/9dmeBBqG13djgv5l2RLUdUgDQ8EIrkPt7lOMMNlKKp4tJn0U8UlYfpfg==", + "requires": { + "d3-array": "2.4.0", + "d3-interpolate": "1.4.0", + "d3-scale": "3.2.1", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + }, + "d3-scale": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.1.tgz", + "integrity": "sha512-huz5byJO/6MPpz6Q8d4lg7GgSpTjIZW/l+1MQkzKfu2u8P6hjaXaStOpmyrD6ymKoW87d2QVFCKvSjLwjzx/rA==", + "requires": { + "d3-array": "2.4.0", + "d3-format": "1.4.3", + "d3-interpolate": "1.4.0", + "d3-time": "1.1.0", + "d3-time-format": "2.2.3" + } + } + } + }, + "vega-scenegraph": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-4.4.0.tgz", + "integrity": "sha512-w81YLlwEfHv1i0LIj2bT+eL7jhvl9baFkymCTDW3ZB9LTOulK6/CmyB52dIswY6ploFVcnYi8dYQfpGSdWZ9wA==", + "requires": { + "d3-path": "1.0.9", + "d3-shape": "1.3.7", + "vega-canvas": "1.2.1", + "vega-loader": "4.1.3", + "vega-util": "1.12.2" + } + }, + "vega-schema-url-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-1.1.0.tgz", + "integrity": "sha512-Tc85J2ofMZZOsxiqDM9sbvfsa+Vdo3GwNLjEEsPOsCDeYqsUHKAlc1IpbbhPLZ6jusyM9Lk0e1izF64GGklFDg==" + }, + "vega-selections": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-5.1.0.tgz", + "integrity": "sha512-Gm+16RaCMkWbimqKh9kuIGMK91vutJsTbIDKBXxmq0c3pTvf+Djy6KfBoFsipEJ9wkwhXHSqpLqS1tExV93E9g==", + "requires": { + "vega-expression": "2.6.3", + "vega-util": "1.12.2" + } + }, + "vega-statistics": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-1.7.0.tgz", + "integrity": "sha512-rsgVXnd8247KYYe5lUBS3mLD0C8hCgKoFOlD4Iolv1eKedZq0FjC+xan9ejQPVV7BlLsuuoYUNuaUOkQXHudcQ==", + "requires": { + "d3-array": "2.4.0" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-themes": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-2.6.1.tgz", + "integrity": "sha512-KsV48l1eBEiEr6OOTTWUQCwnPTSPfNU/5tb1iyrLtAe2B0V3Xk7YCKgsjtlIsXhs7AAOHtM/2HBsJjvQ3HeDtQ==" + }, + "vega-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-1.0.0.tgz", + "integrity": "sha512-r0yOFr/VklJwD3ew1+fEcB7E0LBCLChYlwh0KoO6cTIWMdlC4KhIIUN3/FuBfUZ4qx4V/xp71xH2YYYZTH6izg==", + "requires": { + "d3-array": "2.4.0", + "d3-time": "1.1.0", + "d3-time-format": "2.2.3", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-tooltip": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-0.20.1.tgz", + "integrity": "sha512-kk1p2VRDAZRdoi9C6UdItOO8GCFbtVfUNT1g3XPpHCYuQ4Lrjffa0SNcT/i69luC3n6qd9VyrceFoPBGM4YvTw==", + "requires": { + "vega-util": "1.12.2" + } + }, + "vega-transforms": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-4.5.0.tgz", + "integrity": "sha512-ajgXTXYLMFZAduQL3rFsBtZcBsCHsGbYy5xJDyFATpNO31e4LkAey4b0D20dPoWwSLaSd+nps8m7HOAqm13rqg==", + "requires": { + "d3-array": "2.4.0", + "vega-dataflow": "5.4.1", + "vega-statistics": "1.7.0", + "vega-time": "1.0.0", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-typings": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-0.12.3.tgz", + "integrity": "sha512-ubOekVm60PjzP0b+FAakbti9PkoGu/as2SPX7iBo5IPp8pt+Ej5SNpQJPdCl9rSpKCzU4QNWz1ptZcXL3WSTcg==", + "requires": { + "vega-util": "1.12.2" + } + }, + "vega-util": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.12.2.tgz", + "integrity": "sha512-p02+oQ/XU/gzY9S/CTZinym2NKWEMIneLc+FYdUeJZZnDGa3DvcNgUDlVR90JlwLcYZNs5dBdfYLfdRHsKZKiw==" + }, + "vega-view": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-5.3.2.tgz", + "integrity": "sha512-W1+iQf2VB6a4U55gEI9eaWHq95jdClQS0vAksSAMrS5FjnZs0oE8L178Qrv1NxDvLjSJFlpAE8r0MlckD8+eug==", + "requires": { + "d3-array": "2.4.0", + "d3-timer": "1.0.10", + "vega-dataflow": "5.4.1", + "vega-functions": "5.5.0", + "vega-runtime": "5.0.2", + "vega-scenegraph": "4.4.0", + "vega-util": "1.12.2" + }, + "dependencies": { + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + } + } + }, + "vega-view-transforms": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-4.5.0.tgz", + "integrity": "sha512-8n52147HxNSjQ23NeHN//AWt99zZP+Ukiy4kSbkCJGPZ3dW3NYdunEYNvZWyMmOKSrHIMtgdcHUM9FmPTQpE9w==", + "requires": { + "vega-dataflow": "5.4.1", + "vega-scenegraph": "4.4.0", + "vega-util": "1.12.2" + } + }, + "vega-voronoi": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-4.1.1.tgz", + "integrity": "sha512-agLmr+UGxJs5KB9D8GeZqxgeWWGoER/eVHPcFFPgVuoNBsrqf2bdoltmIkRnpiRsQnGCibGixhFEDCc9GGNAww==", + "requires": { + "d3-delaunay": "5.2.1", + "vega-dataflow": "5.4.1", + "vega-util": "1.12.2" + } + }, + "vega-wordcloud": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-4.0.3.tgz", + "integrity": "sha512-1fVy3r+ty3ddZEidsfby5jDnLi3hNezEM86cS4xc1yxsvE1a1A379GKQKg89vTPWEunmZv15ya+2tQ4fFsU8lg==", + "requires": { + "vega-canvas": "1.2.1", + "vega-dataflow": "5.4.1", + "vega-scale": "5.0.0", + "vega-statistics": "1.7.0", + "vega-util": "1.12.2" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "4.2.1", + "string-width": "4.2.0", + "strip-ansi": "6.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", + "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", + "requires": { + "cliui": "6.0.0", + "decamelize": "1.2.0", + "find-up": "4.1.0", + "get-caller-file": "2.0.5", + "require-directory": "2.1.1", + "require-main-filename": "2.0.0", + "set-blocking": "2.0.0", + "string-width": "4.2.0", + "which-module": "2.0.0", + "y18n": "4.0.0", + "yargs-parser": "16.1.0" + } + }, + "yargs-parser": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", + "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", + "requires": { + "camelcase": "5.3.1", + "decamelize": "1.2.0" + } + } + } +} diff --git a/project.clj b/project.clj index d9b5cda..b270587 100644 --- a/project.clj +++ b/project.clj @@ -40,18 +40,20 @@ :jvm-opts ["-server"] :plugins [[lein-ancient "0.5.5" :exclusions [org.clojure/clojure org.clojure/data.xml]] - [lein-bower "0.5.1"] [lein-codox "0.10.3"] [io.sarnowski/lein-docker "1.0.0"] [lein-environ "1.0.0"] [lein-marginalia "0.7.1" :exclusions [org.clojure/clojure]] + [lein-npm "0.6.2"] [lein-ring "0.12.5" :exclusions [org.clojure/clojure]]] - :bower-dependencies [[simplemde "1.11.2"] - ;; [vega-embed "3.0.0-beta.20"] ;; vega-embed currently not loaded from Bower because of - ;; dependency conflict which will hopefully be resolved soon. - [vega-lite "2.0.0-beta.11"] - [mermaid "6.0.0"]] + :npm {:dependencies [[simplemde "1.11.2"] + [vega "5.8.0"] + [vega-embed "6.2.2"] + [vega-lite "4.1.1"] + [mermaid "8.4.6"] + [tablesort "5.2.0"]] + :root "resources/public/vendor"} :docker {:image-name "simonbrooke/smeagol" :dockerfile "Dockerfile"} diff --git a/resources/config.edn b/resources/config.edn index d5f7017..5ceccd7 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -37,7 +37,9 @@ "backticks" smeagol.formatting/process-backticks} :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal + :js-from :cloudflare ;; where to load JavaScript libraries + ;; from: options are :local, :cloudflare :passwd "resources/passwd" ;; where the password file is stored - :site-title "Smeagol"} ;; overall title of the site, used in + :site-title "Smeagol"} ;; overall title of the site, used in ;; page headings diff --git a/resources/templates/edit.html b/resources/templates/edit.html index 3379dc5..36426dc 100644 --- a/resources/templates/edit.html +++ b/resources/templates/edit.html @@ -1,7 +1,12 @@ {% extends "templates/base.html" %} {% block extra-headers %} -{% style "/vendor/simplemde/dist/simplemde.min.css" %} -{% script "/vendor/simplemde/dist/simplemde.min.js" %} + {% ifequal js-from ":cloudflare" %} + + + {% else %} + {% style "/vendor/simplemde/dist/simplemde.min.css" %} + {% script "/vendor/simplemde/dist/simplemde.min.js" %} + {% endifequal %} {% endblock %} {% block content %} diff --git a/resources/templates/wiki.html b/resources/templates/wiki.html index ca87b3d..7dfa3c8 100644 --- a/resources/templates/wiki.html +++ b/resources/templates/wiki.html @@ -1,15 +1,16 @@ {% extends "templates/base.html" %} {% block extra-headers %} - {% style "vendor/mermaid/dist/mermaid.css" %} - - - {% script "/vendor/vega-lite/build/vega-lite.js" %} - - - {% script "vendor/mermaid/dist/mermaid.js" %} + + + + + + + + {% endblock %} {% block content %} diff --git a/src/smeagol/configuration.clj b/src/smeagol/configuration.clj index 1f9735e..025292a 100644 --- a/src/smeagol/configuration.clj +++ b/src/smeagol/configuration.clj @@ -100,6 +100,7 @@ '( {:from :smeagol-content-dir :to :content-dir} {:from :smeagol-default-locale :to :default-locale} {:from :smeagol-formatters :to :formatters :transform read-string} + {:from :smeagol-js-from :to :js-from :transform to-keyword} {:from :smeagol-log-level :to :log-level :transform to-keyword} {:from :smeagol-passwd :to :passwd} {:from :smeagol-site-title :to :site-title})) @@ -131,6 +132,7 @@ :smeagol-content-dir :smeagol-default-locale :smeagol-formatters + :smeagol-js-from :smeagol-log-level :smeagol-passwd :smeagol-site-title) diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 94fd134..8fe3697 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -78,7 +78,7 @@ index " = " (yaml->json (str "$schema: https://vega.github.io/schema/vega-lite/v2.json\n" vega-src)) - ";\nvega.embed('#vis" + ";\nvegaEmbed('#vis" index "', vl" index diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index c290e91..c3ce6d4 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -50,6 +50,7 @@ (let [user (session/get :user)] {:user user :admin (auth/get-admin user) + :js-from (:js-from config) :side-bar (md->html (slurp (cjio/file content-dir "_side-bar.md"))) :header (md->html (slurp (cjio/file content-dir "_header.md"))) :version (System/getProperty "smeagol.version")})) From 54b82931b22429f716b37b654e5e21eed7075ec7 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Sun, 9 Feb 2020 01:42:31 +0000 Subject: [PATCH 13/37] Major restructuring of extension processors, not yet complete --- resources/config.edn | 2 +- resources/public/content/Extensible Markup.md | 10 ++- resources/public/data/classes.mermaid | 14 +++ resources/templates/wiki.html | 2 +- src/smeagol/extensions/mermaid.clj | 85 +++++++++++++++++++ src/smeagol/extensions/utils.clj | 72 ++++++++++++++++ src/smeagol/extensions/vega.clj | 0 src/smeagol/formatting.clj | 11 +-- 8 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 resources/public/data/classes.mermaid create mode 100644 src/smeagol/extensions/mermaid.clj create mode 100644 src/smeagol/extensions/utils.clj create mode 100644 src/smeagol/extensions/vega.clj diff --git a/resources/config.edn b/resources/config.edn index 5ceccd7..c2c9161 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -33,7 +33,7 @@ :default-locale "en-GB" ;; default language used for messages :formatters {"vega" smeagol.formatting/process-vega "vis" smeagol.formatting/process-vega - "mermaid" smeagol.formatting/process-mermaid + "mermaid" smeagol.extensions.mermaid/process-mermaid "backticks" smeagol.formatting/process-backticks} :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal diff --git a/resources/public/content/Extensible Markup.md b/resources/public/content/Extensible Markup.md index 129a375..aec9e82 100644 --- a/resources/public/content/Extensible Markup.md +++ b/resources/public/content/Extensible Markup.md @@ -36,7 +36,7 @@ Data files can be uploaded in the same way as images, by using the **upload a fi Graphs can now be embedded in a page using the [Mermaid](https://mermaid-js.github.io/mermaid/#/) graph description language. The graph description should start with a line comprising three back-ticks and then the word `mermaid`, and end with a line comprising just three backticks. -Here's an example culled from the Mermaid documentation. +Here's an example culled from the Mermaid documentation. Edit this page to see the specification. ### GANTT Chart @@ -58,6 +58,14 @@ gantt Add to mermaid :1d ``` +Mermaid graph specifications can also be loaded from URLs. Here's another example; again, edit this page to see how the trick is done. + +### Class Diagram + +```mermaid +data/classes.mermaid +``` + ## Writing your own custom formatters A custom formatter is simply a Clojure function which takes a string and an integer as arguments and produces a string as output. The string is the text the user has typed into their markdown; the integer is simply a number you can use to keep track of which addition to the page this is, in order, for example, to fix up some JavaScript to render it. diff --git a/resources/public/data/classes.mermaid b/resources/public/data/classes.mermaid new file mode 100644 index 0000000..e71885a --- /dev/null +++ b/resources/public/data/classes.mermaid @@ -0,0 +1,14 @@ +classDiagram +Class01 <|-- AveryLongClass : Cool +Class03 *-- Class04 +Class05 o-- Class06 +Class07 .. Class08 +Class09 --> C2 : Where am i? +Class09 --* C3 +Class09 --|> Class07 +Class07 : equals() +Class07 : Object[] elementData +Class01 : size() +Class01 : int chimp +Class01 : int gorilla +Class08 <--> C2: Cool label diff --git a/resources/templates/wiki.html b/resources/templates/wiki.html index 7dfa3c8..ffc9b1a 100644 --- a/resources/templates/wiki.html +++ b/resources/templates/wiki.html @@ -9,7 +9,7 @@ - {% endblock %} diff --git a/src/smeagol/extensions/mermaid.clj b/src/smeagol/extensions/mermaid.clj new file mode 100644 index 0000000..9fe271b --- /dev/null +++ b/src/smeagol/extensions/mermaid.clj @@ -0,0 +1,85 @@ +(ns ^{:doc "Format Semagol's extended markdown format." + :author "Simon Brooke"} + smeagol.extensions.mermaid + (:require [smeagol.extensions.utils :refer :all] + [taoensso.timbre :as log])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; Smeagol: a very simple Wiki engine. +;;;; +;;;; 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 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2017 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; Graphs can now be embedded in a page using the +;;;; [Mermaid](https://mermaid-js.github.io/mermaid/#/) graph description +;;;; language. The graph description should start with a line comprising three +;;;; back-ticks and then the word `mermaid`, and end with a line comprising just +;;;; three backticks. +;;;; +;;;; Here's an example culled from the Mermaid documentation. +;;;; +;;;; ### GANTT Chart +;;;; +;;;; ```mermaid +;;;; gantt +;;;; dateFormat YYYY-MM-DD +;;;; title Adding GANTT diagram functionality to mermaid +;;;; section A section +;;;; Completed task :done, des1, 2014-01-06,2014-01-08 +;;;; Active task :active, des2, 2014-01-09, 3d +;;;; Future task : des3, after des2, 5d +;;;; Future task2 : des4, after des3, 5d +;;;; section Critical tasks +;;;; Completed task in the critical line :crit, done, 2014-01-06,24h +;;;; Implement parser and jison :crit, done, after des1, 2d +;;;; Create tests for parser :crit, active, 3d +;;;; Future task in critical line :crit, 5d +;;;; Create tests for renderer :2d +;;;; Add to mermaid :1d +;;;; ``` +;;;; +;;;; Mermaid graph specifications can also be loaded from URLs. Here's another +;;;; example. +;;;; +;;;; ### Class Diagram +;;;; +;;;; ```mermaid +;;;; http://localhost:3000/data/classes.mermaid +;;;; ``` +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defn process-mermaid + "If this `url-or-graph-spec` is a valid URL, it is assumed to point to a plain + text file pointing to a valid `graph-spec`; otherwise, it is expected to BE a + valid `graph-spec`. + + Lightly mung this `graph-spec`, assumed to be a mermaid specification." + [^String url-or-graph-spec ^Integer index] + (let [data (resource-url-or-data->data url-or-graph-spec) + graph-spec (:data data)] + (log/info "Retrieved graph-spec from " (:from data) " `" ((:from data) data) "`") + (str "
\n" + graph-spec + "\n
"))) + +;; (fs/file? (str (nio/resource-path) "data/classes.mermaid")) +;; (slurp (str (nio/resource-path) "data/classes.mermaid")) diff --git a/src/smeagol/extensions/utils.clj b/src/smeagol/extensions/utils.clj new file mode 100644 index 0000000..06ed74c --- /dev/null +++ b/src/smeagol/extensions/utils.clj @@ -0,0 +1,72 @@ +(ns ^{:doc "Utility functions useful to extension processors." + :author "Simon Brooke"} + smeagol.extensions.utils + (:require [cemerick.url :refer (url url-encode url-decode)] + [clojure.string :as cs] + [me.raynes.fs :as fs] + [noir.io :as io] + [taoensso.timbre :as log])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; Smeagol: a very simple Wiki engine. +;;;; +;;;; 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 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2017 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn resource-url-or-data->data + "Interpret this `resource-url-or-data` string as data to be digested by a + `process-extension` function. It may be a URL or the pathname of a local + resource, in which case the content should be fetched; or it may just be + the data itself. + + Returns a map with a key `:from` whose value may be `:url`, `:resource` or + `:text`, and a key `:data` whose value is the data. There will be an + additional key being the value of the `:from` key, whose value will be the + source of the data." + [^String resource-url-or-data] + (let [default {:from :text + :text resource-url-or-data + :data resource-url-or-data}] + (try + (try + ;; is it a URL? + (let [url (str (url resource-url-or-data)) + result (slurp url)] + {:from :url + :url url + :data result}) + (catch java.net.MalformedURLException _ + ;; no. So is it a path to a local resource? + (let [t (cs/trim resource-url-or-data) + r (str (io/resource-path) t)] + (if + (fs/file? r) + {:from :resource + :resource t + :data (slurp r)} + default)))) + (catch Exception x + (log/error + "Could not read mermaid graph specification from `" + (cs/trim resource-url-or-data) + "` because " + (.getName (.getClass x)) + (.getMessage x) ) + default)))) diff --git a/src/smeagol/extensions/vega.clj b/src/smeagol/extensions/vega.clj new file mode 100644 index 0000000..e69de29 diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 8fe3697..eca5277 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -1,4 +1,4 @@ -(ns ^{:doc "Format Semagol's enhanced markdown format." +(ns ^{:doc "Format Semagol's extended markdown format." :author "Simon Brooke"} smeagol.formatting (:require [clojure.data.json :as json] @@ -6,7 +6,8 @@ [cemerick.url :refer (url url-encode url-decode)] [clj-yaml.core :as yaml] [markdown.core :as md] - [smeagol.configuration :refer [config]])) + [smeagol.configuration :refer [config]] + [smeagol.extensions.mermaid :refer [process-mermaid]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -85,12 +86,6 @@ ");\n//]]\n")) -(defn process-mermaid - "Lightly mung this `graph-spec`, assumed to be a mermaid specification." - [^String graph-spec ^Integer index] - (str "
\n" - graph-spec - "\n
")) (defn process-backticks From 1df78111cd7ef2028ae4accb8e2fbd3483f4a1b7 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Sun, 9 Feb 2020 09:23:56 +0000 Subject: [PATCH 14/37] Added separate Vega extension file --- src/smeagol/extensions/vega.clj | 84 +++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/smeagol/extensions/vega.clj b/src/smeagol/extensions/vega.clj index e69de29..14cf285 100644 --- a/src/smeagol/extensions/vega.clj +++ b/src/smeagol/extensions/vega.clj @@ -0,0 +1,84 @@ +(ns ^{:doc "Format Semagol's extended markdown format." + :author "Simon Brooke"} + smeagol.extensions.vega + (:require [smeagol.extensions.utils :refer :all] + [taoensso.timbre :as log])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; Smeagol: a very simple Wiki engine. +;;;; +;;;; 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 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2017 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; Inspired by [visdown](https://visdown.com/) and +;;;; [vega-lite](https://vega.github.io/vega-lite/docs/), the Vega formatter +;;;; allows you to embed vega data visualisations into Smeagol pages. The graph +;;;; description should start with a line comprising three back-ticks and then +;;;; the word '`vega`', and end with a line comprising just three backticks. +;;;; +;;;; Here's an example cribbed in its entirety from +;;;; [here](http://visdown.amitkaps.com/london): +;;;; +;;;; ### Flight punctuality at London airports +;;;; +;;;; ```vega +;;;; data: +;;;; url: "data/london.csv" +;;;; transform: +;;;; - +;;;; filter: datum.year == 2016 +;;;; mark: rect +;;;; encoding: +;;;; x: +;;;; type: nominal +;;;; field: source +;;;; y: +;;;; type: nominal +;;;; field: dest +;;;; color: +;;;; type: quantitative +;;;; field: flights +;;;; aggregate: sum +;;;; ``` +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn process-vega + "If this `src-resource-or-url` is a valid URL, it is assumed to point to a + plain text file pointing to valid `vega-src`; otherwise, it is expected to + BE a valid `vega-src`. + + Process this `vega-src` string, assumed to be in YAML format, into a + specification of a Vega chart, and add the plumbing to render it." + [^String src-resource-or-url ^Integer index] + (let [data (resource-url-or-data->data url-or-graph-spec) + vega-src (:data data)] + (log/info "Retrieved vega-src from " (:from data) " `" ((:from data) data) "`") + (str + "
\n" + ""))) From 719222195e4d4cd12d072ddf3309686797ae63e7 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 10 Feb 2020 11:53:39 +0000 Subject: [PATCH 15/37] Working, but not finished. --- project.clj | 1 + resources/config.edn | 3 +- resources/public/content/Example gallery.md | 59 +++++++++++++++++ resources/public/content/Extensible Markup.md | 6 ++ .../html-includes/photoswipe-boilerplate.html | 65 +++++++++++++++++++ resources/public/vendor/README.md | 1 - resources/templates/wiki.html | 4 ++ src/smeagol/extensions/vega.clj | 11 +++- src/smeagol/formatting.clj | 23 ++++++- 9 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 resources/public/content/Example gallery.md create mode 100644 resources/public/html-includes/photoswipe-boilerplate.html delete mode 100644 resources/public/vendor/README.md diff --git a/project.clj b/project.clj index b270587..1ab812a 100644 --- a/project.clj +++ b/project.clj @@ -52,6 +52,7 @@ [vega-embed "6.2.2"] [vega-lite "4.1.1"] [mermaid "8.4.6"] + [photoswipe "4.1.3"] [tablesort "5.2.0"]] :root "resources/public/vendor"} diff --git a/resources/config.edn b/resources/config.edn index c2c9161..4f9532d 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -34,7 +34,8 @@ :formatters {"vega" smeagol.formatting/process-vega "vis" smeagol.formatting/process-vega "mermaid" smeagol.extensions.mermaid/process-mermaid - "backticks" smeagol.formatting/process-backticks} + "backticks" smeagol.formatting/process-backticks + "pswp" smeagol.formatting/process-photoswipe} :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal :js-from :cloudflare ;; where to load JavaScript libraries diff --git a/resources/public/content/Example gallery.md b/resources/public/content/Example gallery.md new file mode 100644 index 0000000..8379206 --- /dev/null +++ b/resources/public/content/Example gallery.md @@ -0,0 +1,59 @@ +## The Gallery + +This page holds an example Photoswipe gallery. + +```pswp +{ + slides: [ + { src: 'content/uploads/g1.jpg', w: 2592, h:1944, + title: 'Frost on a gate, Laurieston' }, + { src: 'content/uploads/g2.jpg', w: 2560, h:1920, + title: 'Feathered crystals on snow surface, Taliesin' }, + { src: 'content/uploads/g3.jpg', w: 2560, h:1920, + title: 'Feathered snow on log, Taliesin' }, + { src: 'content/uploads/g4.jpg', w: 2560, h:1920, + title: 'Crystaline growth on seed head, Taliesin' }], + options: { + timeToIdle: 100 + }, + openImmediately: true +} + +``` + +## How this works + +The specification for this gallery is as follows: + +``` +{ + slides: [ + { src: 'content/uploads/g1.jpg', w: 2592, h:1944, + title: 'Frost on a gate, Laurieston' }, + { src: 'content/uploads/g2.jpg', w: 2560, h:1920, + title: 'Feathered crystals on snow surface, Taliesin' }, + { src: 'content/uploads/g3.jpg', w: 2560, h:1920, + title: 'Feathered snow on log, Taliesin' }, + { src: 'content/uploads/g4.jpg', w: 2560, h:1920, + title: 'Crystaline growth on seed head, Taliesin' }], + options: { + timeToIdle: 100 + }, + openImmediately: true +} + +``` + +The format of the specification is [JSON](https://www.json.org/json-en.html); there are (at present) three keys, as follows + +### slides + +Most be present. The value of `slides` is a list delimited by square brackets of slide objects. For more information, see the [authoritative documentation](https://photoswipe.com/documentation/getting-started.html) under the sub heading **'Creating an Array of Slide Objects'**. + +### options + +Optional. The value of `options` is a JSON object [as documented here](https://photoswipe.com/documentation/options.html). + +### openImmediately + +Optional. If the value of `openImmediately` is `true`, the gallery will open immediately, covering the whole page. If false, only a button with the label 'Open the gallery' will be shown. Selecting this button will cause the gallery to open. diff --git a/resources/public/content/Extensible Markup.md b/resources/public/content/Extensible Markup.md index aec9e82..3bc8301 100644 --- a/resources/public/content/Extensible Markup.md +++ b/resources/public/content/Extensible Markup.md @@ -66,6 +66,12 @@ Mermaid graph specifications can also be loaded from URLs. Here's another exampl data/classes.mermaid ``` +## Photoswipe galleries + +Not so much a formatter, this is an extension to allow you to embed image galleries in your markdown. To specify a gallery, use three backticks followed by `pswp`, followed on the following lines by a Photoswipe specification in [JSON](https://www.json.org/json-en.html) +followed by three backticks on a line by themselves. There is an [[Example gallery]] so that you can see how this works. + + ## Writing your own custom formatters A custom formatter is simply a Clojure function which takes a string and an integer as arguments and produces a string as output. The string is the text the user has typed into their markdown; the integer is simply a number you can use to keep track of which addition to the page this is, in order, for example, to fix up some JavaScript to render it. diff --git a/resources/public/html-includes/photoswipe-boilerplate.html b/resources/public/html-includes/photoswipe-boilerplate.html new file mode 100644 index 0000000..2dec488 --- /dev/null +++ b/resources/public/html-includes/photoswipe-boilerplate.html @@ -0,0 +1,65 @@ + + +
+ + +
+ + +
+
+
+
+
+ + +
+ +
+ + + +
+ + + + + + + + + + + +
+
+
+
+
+
+
+
+ + + + + + + +
+
+
+ +
+ +
diff --git a/resources/public/vendor/README.md b/resources/public/vendor/README.md deleted file mode 100644 index c3ab41c..0000000 --- a/resources/public/vendor/README.md +++ /dev/null @@ -1 +0,0 @@ -This folder must exist in order that the Bower package manager can deploy JavaScript packages to it. diff --git a/resources/templates/wiki.html b/resources/templates/wiki.html index ffc9b1a..9413902 100644 --- a/resources/templates/wiki.html +++ b/resources/templates/wiki.html @@ -2,6 +2,10 @@ {% block extra-headers %} + {% script "https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js" %} + {% script "https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js" %} + {% style "vendor/node_modules/photoswipe/dist/photoswipe.css" %} + {% style "vendor/node_modules/photoswipe/dist/default-skin/default-skin.css" %} diff --git a/src/smeagol/extensions/vega.clj b/src/smeagol/extensions/vega.clj index 14cf285..1b9e2de 100644 --- a/src/smeagol/extensions/vega.clj +++ b/src/smeagol/extensions/vega.clj @@ -1,7 +1,9 @@ (ns ^{:doc "Format Semagol's extended markdown format." :author "Simon Brooke"} smeagol.extensions.vega - (:require [smeagol.extensions.utils :refer :all] + (:require [clojure.data.json :as json] + [clj-yaml.core :as yaml] + [smeagol.extensions.utils :refer :all] [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -60,6 +62,11 @@ ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn yaml->json + "Rewrite this string, assumed to be in YAML format, as JSON." + [^String yaml-src] + (json/write-str (yaml/parse-string yaml-src))) + (defn process-vega "If this `src-resource-or-url` is a valid URL, it is assumed to point to a plain text file pointing to valid `vega-src`; otherwise, it is expected to @@ -68,7 +75,7 @@ Process this `vega-src` string, assumed to be in YAML format, into a specification of a Vega chart, and add the plumbing to render it." [^String src-resource-or-url ^Integer index] - (let [data (resource-url-or-data->data url-or-graph-spec) + (let [data (resource-url-or-data->data src-resource-or-url) vega-src (:data data)] (log/info "Retrieved vega-src from " (:from data) " `" ((:from data) data) "`") (str diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index eca5277..357f6aa 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -6,6 +6,7 @@ [cemerick.url :refer (url url-encode url-decode)] [clj-yaml.core :as yaml] [markdown.core :as md] + [noir.io :as io] ;; used by photoswipe, only [smeagol.configuration :refer [config]] [smeagol.extensions.mermaid :refer [process-mermaid]])) @@ -85,7 +86,27 @@ index ");\n//]]\n")) - +(defn process-photoswipe + "Process specification for a photoswipe gallery" + [^String spec ^Integer index] + (str + "
\n" + (slurp (str (io/resource-path) "html-includes/photoswipe-boilerplate.html")) + "
+ +

+ ")) (defn process-backticks From ad5e41c23a0eaee8d48398ba66d080b1eed86740 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 10 Feb 2020 17:39:24 +0000 Subject: [PATCH 16/37] Progress on thumbnailing, but not working yet. --- project.clj | 1 + resources/config.edn | 7 +++- src/smeagol/uploads.clj | 89 +++++++++++++++++++++++++++++++---------- 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/project.clj b/project.clj index 1ab812a..c92f098 100644 --- a/project.clj +++ b/project.clj @@ -16,6 +16,7 @@ [environ "1.1.0"] [hiccup "1.0.5"] [im.chit/cronj "1.4.4"] + [image-resizer "0.1.10"] [lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]] [markdown-clj "0.9.99" :exclusions [com.keminglabs/cljx]] [me.raynes/fs "1.4.6"] diff --git a/resources/config.edn b/resources/config.edn index 4f9532d..78390bb 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -42,5 +42,10 @@ ;; from: options are :local, :cloudflare :passwd "resources/passwd" ;; where the password file is stored - :site-title "Smeagol"} ;; overall title of the site, used in + :site-title "Smeagol" ;; overall title of the site, used in ;; page headings + :thumbnails {:small 64 ;; maximum dimension of thumbnails + ;; stored in the /small directory + :med 400 ;; maximum dimension of thumbnails + ;; stored in the /med directory + }} diff --git a/src/smeagol/uploads.clj b/src/smeagol/uploads.clj index 7cadaea..24f2c71 100644 --- a/src/smeagol/uploads.clj +++ b/src/smeagol/uploads.clj @@ -1,10 +1,18 @@ (ns ^{:doc "Handle file uploads." :author "Simon Brooke"} smeagol.uploads - (:import [java.io File]) (:require [clojure.string :as cs] - [noir.io :as io] - [taoensso.timbre :as timbre])) + [clojure.java.io :as io] + [image-resizer.core :refer [resize]] + [image-resizer.util :refer :all] + [me.raynes.fs :as fs] + [smeagol.configuration :refer [config]] + [taoensso.timbre :as log]) + (:import [java.io File] + [java.awt Image] + [java.awt.image RenderedImage BufferedImageOp] + [javax.imageio ImageIO ImageWriter ImageWriteParam IIOImage] + [javax.imageio.stream FileImageOutputStream])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -29,21 +37,59 @@ ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; No longer used as uploaded files now go into Git. -;; (defn avoid-name-collisions -;; "Find a filename within this `path`, based on this `file-name`, that does not -;; reference an existing file. It is assumed that `path` ends with a path separator. -;; Returns a filename hwich does not currently reference a file within the path." -;; [path file-name] -;; (if (.exists (File. (str path file-name))) -;; (let [parts (cs/split file-name #"\.") -;; prefix (cs/join "." (butlast parts)) -;; suffix (last parts)] -;; (first -;; (filter #(not (.exists (File. (str path %)))) -;; (map #(str prefix "." % "." suffix) (range))))) -;; file-name)) +(def image-file-extns + "Extensions of file types we will attempt to thumbnail. GIF is excluded + because by default the javax.imageio package can read GIF, PNG, and JPEG + images but can only write PNG and JPEG images." + #{".jpg" ".jpeg" ".png"}) +(defn read-image + "Reads a BufferedImage from source, something that can be turned into + a file with clojure.java.io/file" + [source] + (ImageIO/read (io/file source))) + +(defn write-image + "Writes img, a RenderedImage, to dest, something that can be turned into + a file with clojure.java.io/file. + Takes the following keys as options: + :format - :gif, :jpg, :png or anything supported by ImageIO + :quality - for JPEG images, a number between 0 and 100" + [^RenderedImage img dest & {:keys [format quality] :or {format :jpg}}] + (if (or (not quality) (not (contains? #{:jpg :jpeg} format))) + (ImageIO/write img (name format) (io/file dest)) + (let [fmt (rest (fs/extension (cs/lower-case dest))) + iw (doto ^ImageWriter (first + (iterator-seq + (ImageIO/getImageWritersByFormatName + "jpeg"))) + (.setOutput (FileImageOutputStream. (io/file dest)))) + iw-param (doto ^ImageWriteParam (.getDefaultWriteParam iw) + (.setCompressionMode ImageWriteParam/MODE_EXPLICIT) + (.setCompressionQuality (float (/ quality 100)))) + iio-img (IIOImage. img nil nil)] + (.write iw nil iio-img iw-param)))) + +(defn auto-thumbnail + "For each of the thumbnail sizes in the configuration, create a thumbnail + for the file with this `filename` on this `path`, provided that it is a + scalable image and is larger than the size." + ([^String path ^String filename] + (if + (image-file-extns (fs/extension (cs/lower-case filename))) + (let [original (buffered-image (.File (str path filename)))] ;; fs/file? + (map + #(auto-thumbnail path filename % original) + (keys (config :thumbnails)))) + (log/info filename " cannot be thumbnailed."))) + ([^String path ^String filename size ^RenderedImage image] + (let [s (-> config :thumbnails size) + d (dimensions image)] + (if (and (integer? s) (some #(> % s) d)) + (do + (write-image (resize image s s) (io/file path (name size) filename)) + (log/info "Created a " size " thumbnail of " filename)) + (log/info filename "is smaller than " s "x" s " and was not scaled to " size))))) (defn store-upload "Store an upload both to the file system and to the database. @@ -56,17 +102,18 @@ (let [upload (:upload params) tmp-file (:tempfile upload) filename (:filename upload)] - (timbre/info + (log/info (str "Storing upload file: " upload)) - (timbre/debug + (log/debug (str "store-upload mv file: " tmp-file " to: " path filename)) (if tmp-file (try (do (.renameTo tmp-file - (File. (str path filename))) + (File. (str path filename))) ;; TODO: fs/file + (auto-thumbnail path filename) (File. (str path filename))) (catch Exception x - (timbre/error (str "Failed to move " tmp-file " to " path filename "; " (type x) ": " (.getMessage x))) + (log/error (str "Failed to move " tmp-file " to " path filename "; " (type x) ": " (.getMessage x))) (throw x))) (throw (Exception. "No file found?"))))) From 40f4f13667d1f2a193768d118e1cd85d6c754362 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 10 Feb 2020 21:36:49 +0000 Subject: [PATCH 17/37] Tactical commit: I'm fairly sure this is close to good. --- resources/templates/list-uploads.html | 4 +- resources/templates/upload.html | 29 +++++++------ src/smeagol/routes/wiki.clj | 52 +++++++++++----------- src/smeagol/uploads.clj | 62 ++++++++++++++++----------- src/smeagol/util.clj | 10 +++-- 5 files changed, 88 insertions(+), 69 deletions(-) diff --git a/resources/templates/list-uploads.html b/resources/templates/list-uploads.html index 761e4a8..e759dac 100644 --- a/resources/templates/list-uploads.html +++ b/resources/templates/list-uploads.html @@ -21,10 +21,10 @@ {{entry.base-name}} {{entry.modified}} - {% if entry.is-image %} ![{{entry.name|capitalize}}](uploads/{{entry.base-name}}) {% else %} [{{entry.name|capitalize}}](uploads/{{entry.base-name}}) {% endif %} + {% if entry.is-image %} ![{{entry.name|capitalize}}]({{entry.resource}}) {% else %} [{{entry.name|capitalize}}](uploads/{{entry.resource}}) {% endif %} - {% if entry.is-image %} {{entry.name|capitalize}} {% else %} link {% endif %} + {% if entry.is-image %} {{entry.name|capitalize}} {% else %} link {% endif %} diff --git a/resources/templates/upload.html b/resources/templates/upload.html index 7c0c373..bc469b4 100644 --- a/resources/templates/upload.html +++ b/resources/templates/upload.html @@ -1,22 +1,25 @@ {% extends "templates/base.html" %} {% block content %}
- {% if uploaded %} - {% if is-image %} -

- Uploaded image + {% if has-uploaded %} + {% for upload in uploaded %} + {{upload.filename}} + {% if upload.is-image %) +

+ Uploaded image - {% i18n file-upload-link-text %}: + {% i18n file-upload-link-text %}: - ![Uploaded image](uploads/{{uploaded}}) -

- {% else %} -

- {% i18n file-upload-link-text %}: + ![{{upload.filename}}]({{upload.resource}}) +

+ {% else %} +

+ {% i18n file-upload-link-text %}: - [Uploaded file](uploads/{{uploaded}}) -

- {% endif %} + [{{upload.filename}}]({{upload.resource}}) +

+ {% endif %} + {% endfor %} {% else %}
{% csrf-field %} diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index a8e13ce..c21e81b 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -4,6 +4,7 @@ (:require [cemerick.url :refer (url url-encode url-decode)] [clj-jgit.porcelain :as git] [clojure.java.io :as cjio] + [clojure.pprint :refer [pprint]] [clojure.string :as cs] [clojure.walk :refer :all] [compojure.core :refer :all] @@ -22,7 +23,7 @@ [smeagol.sanity :refer [show-sanity-check-error]] [smeagol.util :as util] [smeagol.uploads :as ul] - [taoensso.timbre :as timbre] + [taoensso.timbre :as log] [com.stuartsierra.component :as component] [smeagol.include.resolve-local-file :as resolve] [smeagol.include :as include])) @@ -54,7 +55,7 @@ "Process `source-text` and save it to the specified `file-path`, committing it to Git and finally redirecting to wiki-page." [params suffix request] - (timbre/trace (format "process-source: '%s'" request)) + (log/trace (format "process-source: '%s'" request)) (let [source-text (:src params) page (:page params) file-name (str page suffix) @@ -64,7 +65,7 @@ user (session/get :user) email (auth/get-email user) summary (format "%s: %s" user (or (:summary params) "no summary"))] - (timbre/info (format "Saving %s's changes ('%s') to %s in file '%s'" user summary page file-path)) + (log/info (format "Saving %s's changes ('%s') to %s in file '%s'" user summary page file-path)) (spit file-path source-text) (git/git-add git-repo file-name) (git/git-commit git-repo summary {:name user :email email}) @@ -94,9 +95,9 @@ user (session/get :user)] (if-not exists? - (timbre/info + (log/info (format "File '%s' not found; creating a new file" file-path)) - (timbre/info (format "Opening '%s' for editing" file-path))) + (log/info (format "Opening '%s' for editing" file-path))) (cond src-text (process-source params suffix request) true (layout/render template @@ -125,7 +126,7 @@ (defn wiki-page "Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page" [request] - (timbre/trace (format "wiki-page: '%s'" request)) + (log/trace (format "wiki-page: '%s'" request)) (or (show-sanity-check-error) (let [params (keywordize-keys (:params request)) @@ -135,7 +136,7 @@ exists? (.exists (clojure.java.io/as-file file-path))] (cond exists? (do - (timbre/info (format "Showing page '%s' from file '%s'" page file-path)) + (log/info (format "Showing page '%s' from file '%s'" page file-path)) (layout/render "wiki.html" (merge (util/standard-params request) {:title page @@ -156,7 +157,7 @@ page (url-decode (or (:page params) (util/get-message :default-page-title request))) file-name (str page ".md") repo-path util/content-dir] - (timbre/info (format "Showing history of page '%s'" page)) + (log/info (format "Showing history of page '%s'" page)) (layout/render "history.html" (merge (util/standard-params request) {:title (util/get-message :history-title-prefix request) @@ -187,10 +188,11 @@ (let [params (keywordize-keys (:params request)) data-path (str util/content-dir "/uploads/") + cl (count (io/resource-path)) files (map #(zipmap - [:base-name :is-image :modified :name] + [:base-name :is-image :modified :name :resource] [(fs/base-name %) (if (and (fs/extension %) @@ -199,11 +201,13 @@ (if (fs/mod-time %) (format-instant (fs/mod-time %))) - (fs/name %)]) + (fs/name %) + (subs (str (fs/absolute %)) cl)]) (remove #(or (cs/starts-with? (fs/name %) ".") (fs/directory? %)) (file-seq (clojure.java.io/file data-path))))] + (log/info (with-out-str (pprint files))) (layout/render "list-uploads.html" (merge (util/standard-params request) @@ -236,20 +240,18 @@ uploaded (if upload (ul/store-upload params data-path)) user (session/get :user) summary (format "%s: %s" user (or (:summary params) "no summary"))] - (if - uploaded - (do - (git/git-add git-repo (str data-path (fs/name uploaded))) - (git/git-commit git-repo summary {:name user :email (auth/get-email user)}))) +;; (if +;; uploaded +;; (do +;; (map +;; #(git/git-add git-repo (str :resource %)) +;; uploaded) +;; (git/git-commit git-repo summary {:name user :email (auth/get-email user)}))) (layout/render "upload.html" (merge (util/standard-params request) {:title (util/get-message :file-upload-title request) - :uploaded (if uploaded (fs/base-name uploaded)) - :is-image (if - uploaded - (image-extns - (cs/lower-case - (fs/extension uploaded))))})))) + :has-uploaded (not (empty? uploaded)) + :uploaded uploaded})))) (defn version-page "Render a specific historical version of a page" @@ -259,7 +261,7 @@ version (:version params) file-name (str page ".md") content (hist/fetch-version util/content-dir file-name version)] - (timbre/info (format "Showing version '%s' of page '%s'" version page)) + (log/info (format "Showing version '%s' of page '%s'" version page)) (layout/render "wiki.html" (merge (util/standard-params request) {:title (str (util/get-message :vers-col-hdr request) " " version " " (util/get-message :of request) " " page) @@ -274,7 +276,7 @@ page (url-decode (or (:page params) (util/get-message :default-page-title request))) version (:version params) file-name (str page ".md")] - (timbre/info (format "Showing diff between version '%s' of page '%s' and current" version page)) + (log/info (format "Showing diff between version '%s' of page '%s' and current" version page)) (layout/render "wiki.html" (merge (util/standard-params request) {:title @@ -303,11 +305,11 @@ action (:action form-params) user (session/get :user) redirect-to (:redirect-to params)] - (if redirect-to (timbre/info (str "After auth, redirect to: " redirect-to))) + (if redirect-to (log/info (str "After auth, redirect to: " redirect-to))) (cond (= action (util/get-message :logout-label request)) (do - (timbre/info (str "User " user " logging out")) + (log/info (str "User " user " logging out")) (session/remove! :user) (response/redirect redirect-to)) (and username password (auth/authenticate username password)) diff --git a/src/smeagol/uploads.clj b/src/smeagol/uploads.clj index 24f2c71..2adb69f 100644 --- a/src/smeagol/uploads.clj +++ b/src/smeagol/uploads.clj @@ -6,6 +6,7 @@ [image-resizer.core :refer [resize]] [image-resizer.util :refer :all] [me.raynes.fs :as fs] + [noir.io :as nio] [smeagol.configuration :refer [config]] [taoensso.timbre :as log]) (:import [java.io File] @@ -53,22 +54,26 @@ "Writes img, a RenderedImage, to dest, something that can be turned into a file with clojure.java.io/file. Takes the following keys as options: - :format - :gif, :jpg, :png or anything supported by ImageIO - :quality - for JPEG images, a number between 0 and 100" + :format - :gif, :jpg, :png or anything supported by ImageIO + :quality - for JPEG images, a number between 0 and 100" [^RenderedImage img dest & {:keys [format quality] :or {format :jpg}}] - (if (or (not quality) (not (contains? #{:jpg :jpeg} format))) - (ImageIO/write img (name format) (io/file dest)) - (let [fmt (rest (fs/extension (cs/lower-case dest))) - iw (doto ^ImageWriter (first - (iterator-seq - (ImageIO/getImageWritersByFormatName - "jpeg"))) - (.setOutput (FileImageOutputStream. (io/file dest)))) - iw-param (doto ^ImageWriteParam (.getDefaultWriteParam iw) - (.setCompressionMode ImageWriteParam/MODE_EXPLICIT) - (.setCompressionQuality (float (/ quality 100)))) - iio-img (IIOImage. img nil nil)] - (.write iw nil iio-img iw-param)))) + (log/info "Writing to " dest) + (let [fmt (subs (fs/extension (cs/lower-case dest)) 1) + iw (doto ^ImageWriter (first + (iterator-seq + (ImageIO/getImageWritersByFormatName + fmt))) + (.setOutput (FileImageOutputStream. (io/file dest)))) + iw-param (doto ^ImageWriteParam (.getDefaultWriteParam iw) + (.setCompressionMode ImageWriteParam/MODE_EXPLICIT) + (.setCompressionQuality (float (/ (or quality 75) 100)))) + iio-img (IIOImage. img nil nil)] + (.write iw nil iio-img iw-param))) + +(def image? + (memoize + (fn [filename] + (image-file-extns (fs/extension (cs/lower-case (str filename))))))) (defn auto-thumbnail "For each of the thumbnail sizes in the configuration, create a thumbnail @@ -76,19 +81,21 @@ scalable image and is larger than the size." ([^String path ^String filename] (if - (image-file-extns (fs/extension (cs/lower-case filename))) - (let [original (buffered-image (.File (str path filename)))] ;; fs/file? + (image? filename) + (let [original (buffered-image (File. (str path filename)))] ;; fs/file? (map #(auto-thumbnail path filename % original) (keys (config :thumbnails)))) (log/info filename " cannot be thumbnailed."))) ([^String path ^String filename size ^RenderedImage image] (let [s (-> config :thumbnails size) - d (dimensions image)] + d (dimensions image) + p (io/file path (name size) filename)] (if (and (integer? s) (some #(> % s) d)) (do - (write-image (resize image s s) (io/file path (name size) filename)) - (log/info "Created a " size " thumbnail of " filename)) + (write-image (resize image s s) p) + (log/info "Created a " size " thumbnail of " filename) + {:size size :filename filename :location (str p) :is-image true}) (log/info filename "is smaller than " s "x" s " and was not scaled to " size))))) (defn store-upload @@ -108,11 +115,16 @@ (str "store-upload mv file: " tmp-file " to: " path filename)) (if tmp-file (try - (do - (.renameTo tmp-file - (File. (str path filename))) ;; TODO: fs/file - (auto-thumbnail path filename) - (File. (str path filename))) + (let [p (io/file path filename)] + (.renameTo tmp-file p) + (remove + nil? + (cons + {:size :original + :filename filename + :location (str p) + :is-image (and (image? filename) true)} + (remove nil? (or (auto-thumbnail path filename) '()))))) (catch Exception x (log/error (str "Failed to move " tmp-file " to " path filename "; " (type x) ": " (.getMessage x))) (throw x))) diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index c3ce6d4..002556f 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -3,6 +3,7 @@ smeagol.util (:require [clojure.java.io :as cjio] [environ.core :refer [env]] + [me.raynes.fs :as fs] [noir.io :as io] [noir.session :as session] [scot.weft.i18n.core :as i18n] @@ -39,10 +40,11 @@ (:start-page config)) (def content-dir - (or - (:content-dir config) - (cjio/file (io/resource-path) "content"))) - + (str + (fs/absolute + (or + (:content-dir config) + (cjio/file (io/resource-path) "content"))))) (defn standard-params "Return a map of standard parameters to pass to the template renderer." From ba45ea51637085301d35bac0e5d8885844c6d2bd Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 10 Feb 2020 22:51:59 +0000 Subject: [PATCH 18/37] #47, #49: Auto-thumbnailing now working and configurable Not yet documented. --- resources/templates/upload.html | 10 ++++++---- src/smeagol/routes/wiki.clj | 1 - src/smeagol/uploads.clj | 33 ++++++++++++++++++--------------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/resources/templates/upload.html b/resources/templates/upload.html index bc469b4..3e63f17 100644 --- a/resources/templates/upload.html +++ b/resources/templates/upload.html @@ -1,14 +1,14 @@ {% extends "templates/base.html" %} {% block content %}
- {% if has-uploaded %} + {% if uploaded|not-empty %} {% for upload in uploaded %} - {{upload.filename}} - {% if upload.is-image %) + {% if upload.is-image %}

Uploaded image - {% i18n file-upload-link-text %}: + + This is the {{upload.size|name}} file. {% i18n file-upload-link-text %}: ![{{upload.filename}}]({{upload.resource}})

@@ -19,6 +19,8 @@ [{{upload.filename}}]({{upload.resource}})

{% endif %} +
+
{% endfor %} {% else %} diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index c21e81b..95c1828 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -250,7 +250,6 @@ (layout/render "upload.html" (merge (util/standard-params request) {:title (util/get-message :file-upload-title request) - :has-uploaded (not (empty? uploaded)) :uploaded uploaded})))) (defn version-page diff --git a/src/smeagol/uploads.clj b/src/smeagol/uploads.clj index 2adb69f..34f43c3 100644 --- a/src/smeagol/uploads.clj +++ b/src/smeagol/uploads.clj @@ -8,6 +8,7 @@ [me.raynes.fs :as fs] [noir.io :as nio] [smeagol.configuration :refer [config]] + [smeagol.util :as util] [taoensso.timbre :as log]) (:import [java.io File] [java.awt Image] @@ -80,13 +81,13 @@ for the file with this `filename` on this `path`, provided that it is a scalable image and is larger than the size." ([^String path ^String filename] - (if - (image? filename) - (let [original (buffered-image (File. (str path filename)))] ;; fs/file? - (map - #(auto-thumbnail path filename % original) - (keys (config :thumbnails)))) - (log/info filename " cannot be thumbnailed."))) + (if + (image? filename) + (let [original (buffered-image (File. (str path filename)))] ;; fs/file? + (map + #(auto-thumbnail path filename % original) + (keys (config :thumbnails)))) + (log/info filename " cannot be thumbnailed."))) ([^String path ^String filename size ^RenderedImage image] (let [s (-> config :thumbnails size) d (dimensions image) @@ -117,14 +118,16 @@ (try (let [p (io/file path filename)] (.renameTo tmp-file p) - (remove - nil? - (cons - {:size :original - :filename filename - :location (str p) - :is-image (and (image? filename) true)} - (remove nil? (or (auto-thumbnail path filename) '()))))) + (map + #(assoc % :resource (subs (:location %) (inc (count util/content-dir)))) + (remove + nil? + (cons + {:size :original + :filename filename + :location (str p) + :is-image (and (image? filename) true)} + (remove nil? (or (auto-thumbnail path filename) '())))))) (catch Exception x (log/error (str "Failed to move " tmp-file " to " path filename "; " (type x) ": " (.getMessage x))) (throw x))) From fc4dcdb5d3c10afdd94ded5f9fa99e1dfa1f9091 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Tue, 11 Feb 2020 08:30:03 +0000 Subject: [PATCH 19/37] Mainly documenting the configuration file --- resources/config.edn | 11 +- resources/public/content/Configuration.md | 33 ------ .../public/content/Configuring Smeagol.md | 102 ++++++++++++++++++ resources/public/content/Docker Image.md | 2 +- .../public/content/User Documentation.md | 2 +- resources/public/content/_side-bar.md | 1 + resources/templates/wiki.html | 8 +- src/smeagol/routes/wiki.clj | 7 +- 8 files changed, 120 insertions(+), 46 deletions(-) delete mode 100644 resources/public/content/Configuration.md create mode 100644 resources/public/content/Configuring Smeagol.md diff --git a/resources/config.edn b/resources/config.edn index 78390bb..35a85e4 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -28,22 +28,25 @@ ;; ; ; ; ; ; ; ; ; ; { :content-dir "resources/public/content" - :start-page "Introduction" ;; where content is served from. :default-locale "en-GB" ;; default language used for messages - :formatters {"vega" smeagol.formatting/process-vega + :formatters ;; formatters for processing markdown + ;; extensions. + {"vega" smeagol.formatting/process-vega "vis" smeagol.formatting/process-vega "mermaid" smeagol.extensions.mermaid/process-mermaid "backticks" smeagol.formatting/process-backticks "pswp" smeagol.formatting/process-photoswipe} :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal - :js-from :cloudflare ;; where to load JavaScript libraries - ;; from: options are :local, :cloudflare + :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 ;; page headings + :start-page "Introduction" ;; the page shown to a visitor to the + ;; root URL. :thumbnails {:small 64 ;; maximum dimension of thumbnails ;; stored in the /small directory :med 400 ;; maximum dimension of thumbnails diff --git a/resources/public/content/Configuration.md b/resources/public/content/Configuration.md deleted file mode 100644 index 4e5d987..0000000 --- a/resources/public/content/Configuration.md +++ /dev/null @@ -1,33 +0,0 @@ -Smeagol reads a configuration file, whose content should be formatted as a clojure map. - -The default content is as follows: - -``` -{ - :site-title "Smeagol" ;; overall title of the site, used in page headings - :default-locale "en-GB" ;; default language used for messages - :content-dir "/usr/local/etc/content" - ;; where content is served from - :passwd "/usr/local/etc/passwd" - ;; where the password file is stored - :log-level :info ;; the minimum logging level; one of - ;; :trace :debug :info :warn :error :fatal - :formatters {"vega" smeagol.formatting/process-vega - "vis" smeagol.formatting/process-vega - "mermaid" smeagol.formatting/process-mermaid - "backticks" smeagol.formatting/process-backticks} -} -``` - -The values should be: - -* `:content-dir` The directory in which your editable content is stored; -* `:default-locale` A string comprising a lower-case [ISO 639](https://en.wikipedia.org/wiki/ISO_639) code specifying a language, optionally followed by a hyphen and an upper-case [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166) specifying a country. -* `:formatters` A map of formatters used in [[Extensible Markup]], q.v. -* `:log-level` The minimum level of log messages to be logged; one of `:trace :debug :info :warn :error :fatal` -* `:passwd` The path to your `passwd` file - see [[Security and authentication]]; -* `:site-title` The title for your wiki. - -The default file is at `resources/config.edn`; this default can be overridden by providing an environment variable, `SMEAGOL_CONFIG`, whose value is the full or relative pathname of a suitable file. - -Note that all the values in the configuration can be overridden with [[Environment Variables]]. diff --git a/resources/public/content/Configuring Smeagol.md b/resources/public/content/Configuring Smeagol.md new file mode 100644 index 0000000..8f6472e --- /dev/null +++ b/resources/public/content/Configuring Smeagol.md @@ -0,0 +1,102 @@ +Smeagol's core configuration comes from a configuration file, `config.edn`, which may be overridden by [[Environment Variables]]. The default file is at `resources/config.edn`; this default can be overridden by providing an environment variable, `SMEAGOL_CONFIG`, whose value is the full or relative pathname of a suitable file. + + +The default configuration file is as follows: + +``` + +{ + + :content-dir "resources/public/content" + + ;; where content is served from. + + :default-locale "en-GB" ;; default language used for messages + + :formatters ;; formatters for processing markdown + + ;; extensions. + + {"vega" smeagol.formatting/process-vega + + "vis" smeagol.formatting/process-vega + + "mermaid" smeagol.extensions.mermaid/process-mermaid + + "backticks" smeagol.formatting/process-backticks + + "pswp" smeagol.formatting/process-photoswipe} + + :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 + + ;; page headings + + :start-page "Introduction" ;; the page shown to a visitor to the + + ;; root URL. + + :thumbnails {:small 64 ;; maximum dimension of thumbnails + + ;; stored in the /small directory + + :med 400 ;; maximum dimension of thumbnails + + ;; stored in the /med directory + + }} + +``` + + +## content-dir + +The value of `content-dir` should be the full or relative path to the content to be served: the Markdown files, and the upload directories. Full paths are advised, where possible. The directory must be readable and writable by the process running Smeagol. The default is `resources/public/conten` + + +The value from the configuration file may be overridden with the value of the environment variable `SMEAGOL_CONTENT_DIR`. + + +## default-locale + +The locale which you expect the majority of your visitors will use. Content negotiation will be done of course, and the best internationalisation file available will be used, but this sets a default for users who do not have any acceptable locale known to us. The default value is `en-GB`. + + +This parameter may be overridden with the environment variable `SMEAGOL-DEFAULT-LOCALE`. + + +## formatters + +Specifications for formatters for markup extensions. The exact data stored will change before Smeagol 1.1.0. TODO: update this. + + +## log-level + +The level at which logging should operate. Each setting implies all of the settings more severe than itself so + + +1. setting `:debug` will log all of `debug, info, warn, error` and| `fatal` messages; + +2. setting `:info` will log all of `info, warn, error` and| `fatal` messages; + + +and so on, so that setting `:fatal` will show only messages which report reasons for Smeagol to fail. + + +The default setting is `:info`. + + +This parameter may be overridden with the environment variable `SMEAGOL-LOG-LEVEL`. + +## TODO: Complete this doumentation! diff --git a/resources/public/content/Docker Image.md b/resources/public/content/Docker Image.md index 9341628..2a3f766 100644 --- a/resources/public/content/Docker Image.md +++ b/resources/public/content/Docker Image.md @@ -8,7 +8,7 @@ Where 127.0.0.1 is the IP address through which you want to forward port 80 (in You can then browse to Smeagol by pointing your browser at http://localhost/. -As of version 0.99.10, the Docker image is now based on the Jetty, rather than the Tomcat, deployment of Smeagol (that is to say, it runs the executable jar file). This makes for a lighter weight Docker image. All configuration can be overridden with [[Environment Variables]], which can be passed into the Docker container when the image is invoked, or from a [[Configuration]] file. +As of version 0.99.10, the Docker image is now based on the Jetty, rather than the Tomcat, deployment of Smeagol (that is to say, it runs the executable jar file). This makes for a lighter weight Docker image. All configuration can be overridden with [[Environment Variables]], which can be passed into the Docker container when the image is invoked, or from a Configuration file, see [[Configuring Smeagol]]. The `config.edn` and `passwd` files and the `content` directory are copied into `/usr/local/etc` in the Docker image, and the appropriate environment variables are set up to point to them: ``` diff --git a/resources/public/content/User Documentation.md b/resources/public/content/User Documentation.md index 51489af..604f317 100644 --- a/resources/public/content/User Documentation.md +++ b/resources/public/content/User Documentation.md @@ -67,7 +67,7 @@ To upload a file (including an image file), select the link `Upload a file` from Selecting the link will take you to the `Upload a file` page. This will prompt you for the file you wish to upload. Select your file, and then select the green `Save!` button. -After your file has uploaded, you will be shown a link which can be copied and pasted into a Wiki page to link to that file. +After your file has uploaded, you will be shown a link which can be copied and pasted into a Wiki page to link to that file. When you upload a PNG or JPG image file, multiple copies of the file may be saved at different resolutions, and you will be shown links to each of these. The `Upload a file` form also has a link to the list of all files which have been uploaded, to help with finding the one you're looking for! You must be logged in to upload files. diff --git a/resources/public/content/_side-bar.md b/resources/public/content/_side-bar.md index ad01779..6cfc4f2 100644 --- a/resources/public/content/_side-bar.md +++ b/resources/public/content/_side-bar.md @@ -1,6 +1,7 @@ * [[Introduction]] * [[Change log]] * [[User Documentation]] +* [[Configuring Smeagol]] * [[Deploying Smeagol]] * [[Developing Smeagol]] diff --git a/resources/templates/wiki.html b/resources/templates/wiki.html index 9413902..cf0655e 100644 --- a/resources/templates/wiki.html +++ b/resources/templates/wiki.html @@ -2,10 +2,10 @@ {% block extra-headers %} - {% script "https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js" %} - {% script "https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js" %} - {% style "vendor/node_modules/photoswipe/dist/photoswipe.css" %} - {% style "vendor/node_modules/photoswipe/dist/default-skin/default-skin.css" %} + + + {% style "/vendor/node_modules/photoswipe/dist/photoswipe.css" %} + {% style "/vendor/node_modules/photoswipe/dist/default-skin/default-skin.css" %} diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index 95c1828..e33cbdc 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -240,12 +240,13 @@ uploaded (if upload (ul/store-upload params data-path)) user (session/get :user) summary (format "%s: %s" user (or (:summary params) "no summary"))] -;; (if -;; uploaded +;; TODO: Get this working! it MUST work! +;; (if-not +;; (empty? uploaded) ;; (do ;; (map ;; #(git/git-add git-repo (str :resource %)) -;; uploaded) +;; (remove nil? uploaded)) ;; (git/git-commit git-repo summary {:name user :email (auth/get-email user)}))) (layout/render "upload.html" (merge (util/standard-params request) From d2e20162ef6d36d8a41cb32c7d5e72cdfdcde22d Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Tue, 11 Feb 2020 13:14:36 +0000 Subject: [PATCH 20/37] Simplified syntax for Photoswipe galleries now works --- project.clj | 1 + resources/config.edn | 2 +- resources/public/content/Example gallery.md | 39 ++-- resources/public/content/Extensible Markup.md | 5 +- .../content/Simplified example gallery.md | 24 +++ src/smeagol/extensions/mermaid.clj | 2 +- src/smeagol/extensions/photoswipe.clj | 181 ++++++++++++++++++ src/smeagol/extensions/utils.clj | 75 +++++--- src/smeagol/formatting.clj | 26 +-- src/smeagol/util.clj | 3 + 10 files changed, 279 insertions(+), 79 deletions(-) create mode 100644 resources/public/content/Simplified example gallery.md create mode 100644 src/smeagol/extensions/photoswipe.clj diff --git a/project.clj b/project.clj index c92f098..b753cd1 100644 --- a/project.clj +++ b/project.clj @@ -17,6 +17,7 @@ [hiccup "1.0.5"] [im.chit/cronj "1.4.4"] [image-resizer "0.1.10"] + [instaparse "1.4.10"] [lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]] [markdown-clj "0.9.99" :exclusions [com.keminglabs/cljx]] [me.raynes/fs "1.4.6"] diff --git a/resources/config.edn b/resources/config.edn index 35a85e4..059dfeb 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -36,7 +36,7 @@ "vis" smeagol.formatting/process-vega "mermaid" smeagol.extensions.mermaid/process-mermaid "backticks" smeagol.formatting/process-backticks - "pswp" smeagol.formatting/process-photoswipe} + "pswp" smeagol.extensions.photoswipe/process-photoswipe} :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal :js-from :cdnjs ;; where to load JavaScript libraries diff --git a/resources/public/content/Example gallery.md b/resources/public/content/Example gallery.md index 8379206..a2fcd00 100644 --- a/resources/public/content/Example gallery.md +++ b/resources/public/content/Example gallery.md @@ -1,8 +1,8 @@ -## The Gallery +## How this works -This page holds an example Photoswipe gallery. +The specification for this gallery is as follows: -```pswp +``` { slides: [ { src: 'content/uploads/g1.jpg', w: 2592, h:1944, @@ -21,11 +21,25 @@ This page holds an example Photoswipe gallery. ``` -## How this works +The format of the specification is [JSON](https://www.json.org/json-en.html); there are (at present) three keys, as follows -The specification for this gallery is as follows: +### slides -``` +Most be present. The value of `slides` is a list delimited by square brackets of slide objects. For more information, see the [authoritative documentation](https://photoswipe.com/documentation/getting-started.html) under the sub heading **'Creating an Array of Slide Objects'**. + +### options + +Optional. The value of `options` is a JSON object [as documented here](https://photoswipe.com/documentation/options.html). + +### openImmediately + +Optional. If the value of `openImmediately` is `true`, the gallery will open immediately, covering the whole page. If false, only a button with the label 'Open the gallery' will be shown. Selecting this button will cause the gallery to open. + +## The Gallery + +This page holds an example Photoswipe gallery. + +```pswp { slides: [ { src: 'content/uploads/g1.jpg', w: 2592, h:1944, @@ -44,16 +58,3 @@ The specification for this gallery is as follows: ``` -The format of the specification is [JSON](https://www.json.org/json-en.html); there are (at present) three keys, as follows - -### slides - -Most be present. The value of `slides` is a list delimited by square brackets of slide objects. For more information, see the [authoritative documentation](https://photoswipe.com/documentation/getting-started.html) under the sub heading **'Creating an Array of Slide Objects'**. - -### options - -Optional. The value of `options` is a JSON object [as documented here](https://photoswipe.com/documentation/options.html). - -### openImmediately - -Optional. If the value of `openImmediately` is `true`, the gallery will open immediately, covering the whole page. If false, only a button with the label 'Open the gallery' will be shown. Selecting this button will cause the gallery to open. diff --git a/resources/public/content/Extensible Markup.md b/resources/public/content/Extensible Markup.md index 3bc8301..41c9873 100644 --- a/resources/public/content/Extensible Markup.md +++ b/resources/public/content/Extensible Markup.md @@ -68,9 +68,8 @@ data/classes.mermaid ## Photoswipe galleries -Not so much a formatter, this is an extension to allow you to embed image galleries in your markdown. To specify a gallery, use three backticks followed by `pswp`, followed on the following lines by a Photoswipe specification in [JSON](https://www.json.org/json-en.html) -followed by three backticks on a line by themselves. There is an [[Example gallery]] so that you can see how this works. - +Not so much a formatter, this is an extension to allow you to embed image galleries in your markdown. To specify a gallery, use three backticks followed by `pswp`, followed on the following lines by a [Photoswipe](https://photoswipe.com/documentation/getting-started.html) specification in [JSON](https://www.json.org/json-en.html) +followed by three backticks on a line by themselves. There is an [[Example gallery]] with the full PhotoSwipe configuration, and a [[Simplified example gallery]] using a much simpler syntax, so that you can see how this works. ## Writing your own custom formatters diff --git a/resources/public/content/Simplified example gallery.md b/resources/public/content/Simplified example gallery.md new file mode 100644 index 0000000..4cff2da --- /dev/null +++ b/resources/public/content/Simplified example gallery.md @@ -0,0 +1,24 @@ +## How this works + +The specification for this gallery is as follows: + +``` +![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) +``` + +That's all there is to it - a sequence of image links just as you'd write them anywhere else in the wiki. + +## The Gallery + +This page holds another example Photoswipe gallery, this time using a simpler, Markdown-based specification. Processing this specification takes more work than the full syntax used in the other [Example gallery], so the gallery may be slower to load; but it's much easier to configure. + +```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) +``` + diff --git a/src/smeagol/extensions/mermaid.clj b/src/smeagol/extensions/mermaid.clj index 9fe271b..6185b5d 100644 --- a/src/smeagol/extensions/mermaid.clj +++ b/src/smeagol/extensions/mermaid.clj @@ -1,4 +1,4 @@ -(ns ^{:doc "Format Semagol's extended markdown format." +(ns ^{:doc "Mermaid formatter for Semagol's extendsible markdown format." :author "Simon Brooke"} smeagol.extensions.mermaid (:require [smeagol.extensions.utils :refer :all] diff --git a/src/smeagol/extensions/photoswipe.clj b/src/smeagol/extensions/photoswipe.clj new file mode 100644 index 0000000..764a7a4 --- /dev/null +++ b/src/smeagol/extensions/photoswipe.clj @@ -0,0 +1,181 @@ +(ns ^{:doc "Photoswipe gallery formatter for Semagol's extendsible markdown + format." + :author "Simon Brooke"} + smeagol.extensions.photoswipe + (:require [clojure.data.json :as json] + [clojure.java.io :as cio] + [clojure.string :as cs] + [image-resizer.util :refer [buffered-image dimensions]] + [instaparse.core :as insta] + [me.raynes.fs :as fs] + [noir.io :as io] + [smeagol.configuration :refer [config]] + [smeagol.extensions.utils :refer :all] + [taoensso.timbre :as log])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; Smeagol: a very simple Wiki engine. +;;;; +;;;; 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 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2017 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn process-full-photoswipe + "Process a specification for a photoswipe gallery, using a JSON + specification based on that documented on the Photoswipe website." + [^String spec ^Integer index] + (str + "
\n" + (slurp + (str (io/resource-path) "html-includes/photoswipe-boilerplate.html")) + "
+ +

+
")) + + +(def simple-grammar + "Parser to transform a sequence of Markdown image links into something we + can build into JSON. Yes, this could all have been done with regexes, but + they are very inscrutable." + (insta/parser "SLIDE := START-CAPTION title END-CAPTION src END-SRC; + START-CAPTION := '![' ; + END-CAPTION := '](' ; + END-SRC := ')' ; + title := #'[^]]*' ; + src := #'[^)]*' ; + SPACE := #'[\\r\\n\\W]*'")) + +(defn simplify + [tree] + (if + (coll? tree) + (case (first tree) + :SLIDE (remove empty? (map simplify (rest tree))) + :title tree + :src tree + :START-CAPTION nil + :END-CAPTION nil + :END-SRC nil + (remove empty? (map simplify tree))))) + +(defn uploaded? + "Does this `url` string appear to be one that has been uploaded to our + `uploads` directory?" + [url] + (and + (cs/starts-with? (str url) "content/uploads") + (fs/exists? (cio/file upload-dir (fs/base-name url))))) + +;; (uploaded? "content/uploads/g1.jpg") + +(defn slide-merge-dimensions + "If this `slide` appears to be local, return it decorated with the + dimensions of the image it references." + [slide] + (let [url (:src slide) + dimensions (try + (if (uploaded? url) + (dimensions + (buffered-image (cio/file upload-dir (fs/base-name url))))) + (catch Exception x (.getMessage x)))] + (if dimensions + (assoc slide :w (first dimensions) :h (nth dimensions 1)) + slide))) + +;; (slide-merge-dimensions +;; {:title "Frost on a gate, Laurieston", +;; :src "content/uploads/g1.jpg"}) + +(defn process-simple-slide + [slide-spec] + (let [s (simplify (simple-grammar slide-spec)) + s'(zipmap (map first s) (map #(nth % 1) s)) + thumbsizes (:thumbnails config) + thumbsize (first + (sort + #(> (%1 thumbsizes) (%2 thumbsizes)) + (keys thumbsizes))) + url (:url s') + thumb (if + (and + (uploaded? url) + thumbsize) + (let [p (str (cio/file "uploads" (name thumbsize) (fs/base-name url))) + p' (cio/file content-dir p)] + (if + (and (fs/exists? p') (fs/readable? p')) + p)))] + (slide-merge-dimensions + (if thumb + (assoc s' :msrc thumb) + s')))) + +(def process-simple-photoswipe + "Process a simplified specification for a photoswipe gallery, comprising just + a sequence of MarkDown image links. This is REALLY expensive to do, we don't + want to do it often. Hence memoised." + (memoize + (fn + [^String spec ^Integer index] + (process-full-photoswipe + (json/write-str + {:slides (map + process-simple-slide + (re-seq #"!\[[^(]*\([^)]*\)" spec)) + ;; TODO: better to split slides in instaparse + :options { :timeToIdle 100 } + :openImmediately true}) index)))) + +;; (map +;; process-simple-slide +;; (re-seq #"!\[[^(]*\([^)]*\)" +;; "![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-simple-photoswipe +;; "![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)" +;; 1) + +(defn process-photoswipe + [^String url-or-pswp-spec ^Integer index] + (let [data (resource-url-or-data->data url-or-pswp-spec) + spec (cs/trim (:data data))] + (if + (cs/starts-with? spec "![") + (process-simple-photoswipe spec index) + (process-full-photoswipe spec index)))) diff --git a/src/smeagol/extensions/utils.clj b/src/smeagol/extensions/utils.clj index 06ed74c..c73d086 100644 --- a/src/smeagol/extensions/utils.clj +++ b/src/smeagol/extensions/utils.clj @@ -2,9 +2,11 @@ :author "Simon Brooke"} smeagol.extensions.utils (:require [cemerick.url :refer (url url-encode url-decode)] + [clojure.java.io :as cjio] [clojure.string :as cs] [me.raynes.fs :as fs] [noir.io :as io] + [smeagol.configuration :refer [config]] [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -30,7 +32,17 @@ ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn resource-url-or-data->data +(def content-dir + (str + (fs/absolute + (or + (:content-dir config) + (cjio/file (io/resource-path) "content"))))) + +(def upload-dir + (str (cjio/file content-dir "uploads"))) + +(def resource-url-or-data->data "Interpret this `resource-url-or-data` string as data to be digested by a `process-extension` function. It may be a URL or the pathname of a local resource, in which case the content should be fetched; or it may just be @@ -40,33 +52,34 @@ `:text`, and a key `:data` whose value is the data. There will be an additional key being the value of the `:from` key, whose value will be the source of the data." - [^String resource-url-or-data] - (let [default {:from :text - :text resource-url-or-data - :data resource-url-or-data}] - (try - (try - ;; is it a URL? - (let [url (str (url resource-url-or-data)) - result (slurp url)] - {:from :url - :url url - :data result}) - (catch java.net.MalformedURLException _ - ;; no. So is it a path to a local resource? - (let [t (cs/trim resource-url-or-data) - r (str (io/resource-path) t)] - (if - (fs/file? r) - {:from :resource - :resource t - :data (slurp r)} - default)))) - (catch Exception x - (log/error - "Could not read mermaid graph specification from `" - (cs/trim resource-url-or-data) - "` because " - (.getName (.getClass x)) - (.getMessage x) ) - default)))) + (memoize + (fn [^String resource-url-or-data] + (let [default {:from :text + :text resource-url-or-data + :data resource-url-or-data}] + (try + (try + ;; is it a URL? + (let [url (str (url resource-url-or-data)) + result (slurp url)] + {:from :url + :url url + :data result}) + (catch java.net.MalformedURLException _ + ;; no. So is it a path to a local resource? + (let [t (cs/trim resource-url-or-data) + r (str (io/resource-path) t)] + (if + (fs/file? r) + {:from :resource + :resource t + :data (slurp r)} + default)))) + (catch Exception x + (log/error + "Could not read mermaid graph specification from `" + (cs/trim resource-url-or-data) + "` because " + (.getName (.getClass x)) + (.getMessage x) ) + default)))))) diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 357f6aa..fa4f2f6 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -6,9 +6,9 @@ [cemerick.url :refer (url url-encode url-decode)] [clj-yaml.core :as yaml] [markdown.core :as md] - [noir.io :as io] ;; used by photoswipe, only [smeagol.configuration :refer [config]] - [smeagol.extensions.mermaid :refer [process-mermaid]])) + [smeagol.extensions.mermaid :refer [process-mermaid]] + [smeagol.extensions.photoswipe :refer [process-photoswipe]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -86,28 +86,6 @@ index ");\n//]]\n")) -(defn process-photoswipe - "Process specification for a photoswipe gallery" - [^String spec ^Integer index] - (str - "
\n" - (slurp (str (io/resource-path) "html-includes/photoswipe-boilerplate.html")) - "
- -

-
")) - (defn process-backticks "Effectively, escape the backticks surrounding this `text`, by protecting them diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index 002556f..1ef44df 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -46,6 +46,9 @@ (:content-dir config) (cjio/file (io/resource-path) "content"))))) +(def upload-dir + (str (cjio/file content-dir "uploads"))) + (defn standard-params "Return a map of standard parameters to pass to the template renderer." [request] From b191f40d05c3dd64141ff59ecb9b13dd31db7373 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Tue, 11 Feb 2020 17:01:14 +0000 Subject: [PATCH 21/37] #47: warn if dimensions cannot be established. --- src/smeagol/extensions/photoswipe.clj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/smeagol/extensions/photoswipe.clj b/src/smeagol/extensions/photoswipe.clj index 764a7a4..31c6e42 100644 --- a/src/smeagol/extensions/photoswipe.clj +++ b/src/smeagol/extensions/photoswipe.clj @@ -110,7 +110,9 @@ (catch Exception x (.getMessage x)))] (if dimensions (assoc slide :w (first dimensions) :h (nth dimensions 1)) - slide))) + (do + (log/warn "Failed to fetch dimensions of image " url) + slide)))) ;; (slide-merge-dimensions ;; {:title "Frost on a gate, Laurieston", From 0d686a9b6338f000219dfd5ef0cb1ac33f7abc71 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 12 Feb 2020 12:35:18 +0000 Subject: [PATCH 22/37] #47: Something well broken here, but I'm on the right path Fragment indices are being returned instead of fragments, and it does not seem that the extension formatters are being called at all. But... config is definitely improved in the right direction. --- project.clj | 1 + resources/config.edn | 11 ++++++----- .../content/Simplified example gallery.md | 2 +- src/smeagol/extensions/photoswipe.clj | 17 +++++++++++------ src/smeagol/formatting.clj | 15 +++++++++++++-- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/project.clj b/project.clj index b753cd1..3898e1c 100644 --- a/project.clj +++ b/project.clj @@ -27,6 +27,7 @@ [org.clojure/core.memoize "0.5.9"] [org.clojure/data.json "0.2.6"] [org.clojure/tools.logging "0.4.0"] + [org.clojure/tools.trace "0.7.10"] [org.slf4j/slf4j-api "1.7.25"] [org.slf4j/log4j-over-slf4j "1.7.25"] [org.slf4j/jul-to-slf4j "1.7.25"] diff --git a/resources/config.edn b/resources/config.edn index 059dfeb..6ff0faf 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -32,11 +32,12 @@ :default-locale "en-GB" ;; default language used for messages :formatters ;; formatters for processing markdown ;; extensions. - {"vega" smeagol.formatting/process-vega - "vis" smeagol.formatting/process-vega - "mermaid" smeagol.extensions.mermaid/process-mermaid - "backticks" smeagol.formatting/process-backticks - "pswp" smeagol.extensions.photoswipe/process-photoswipe} + {:vega {:formatter "smeagol.extensions.vega/process-vega" } + :vis {:formatter "smeagol.extensions.vega/process-vega" } + :mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid" } + :backticks {:formatter "smeagol.formatting/process-backticks" } + :pswp {:formatter "smeagol.extensions.photoswipe/process-photoswipe" } + } :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal :js-from :cdnjs ;; where to load JavaScript libraries diff --git a/resources/public/content/Simplified example gallery.md b/resources/public/content/Simplified example gallery.md index 4cff2da..eac0394 100644 --- a/resources/public/content/Simplified example gallery.md +++ b/resources/public/content/Simplified example gallery.md @@ -13,7 +13,7 @@ That's all there is to it - a sequence of image links just as you'd write them a ## The Gallery -This page holds another example Photoswipe gallery, this time using a simpler, Markdown-based specification. Processing this specification takes more work than the full syntax used in the other [Example gallery], so the gallery may be slower to load; but it's much easier to configure. +This page holds another example Photoswipe gallery, this time using a simpler, Markdown-based specification. Processing this specification takes more work than the full syntax used in the other [[Example gallery]], so the gallery may be slower to load; but it's much easier to configure. ```pswp ![Frost on a gate, Laurieston](content/uploads/g1.jpg) diff --git a/src/smeagol/extensions/photoswipe.clj b/src/smeagol/extensions/photoswipe.clj index 31c6e42..2df235e 100644 --- a/src/smeagol/extensions/photoswipe.clj +++ b/src/smeagol/extensions/photoswipe.clj @@ -174,10 +174,15 @@ ;; 1) (defn process-photoswipe - [^String url-or-pswp-spec ^Integer index] + [^String url-or-pswp-spec ^Integer index] + (log/info "process-photoswipe called with arg1 `" + url-or-pswp-spec "`; arg2 `" index "`.") (let [data (resource-url-or-data->data url-or-pswp-spec) - spec (cs/trim (:data data))] - (if - (cs/starts-with? spec "![") - (process-simple-photoswipe spec index) - (process-full-photoswipe spec index)))) + spec (cs/trim (:data data)) + result + (if + (cs/starts-with? spec "![") + (process-simple-photoswipe spec index) + (process-full-photoswipe spec index))] + (log/info "process-photoswipe returning `" result "`.") + )) diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index fa4f2f6..87d8f45 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -8,7 +8,8 @@ [markdown.core :as md] [smeagol.configuration :refer [config]] [smeagol.extensions.mermaid :refer [process-mermaid]] - [smeagol.extensions.photoswipe :refer [process-photoswipe]])) + [smeagol.extensions.photoswipe :refer [process-photoswipe]] + [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -149,7 +150,17 @@ ;; I need to put the backticks back in. remarked (if (odd? index) (str "```" fragment "\n```") fragment) first-token (get-first-token fragment) - formatter (eval ((:formatters config) first-token))] + formatter (if-not + (empty? first-token) + (try + (let [kw (keyword first-token)] + (read-string (-> config :formatters kw :formatter))) + (catch Exception _ + (do + (log/info "No formatter found for extension `" first-token "`") + ;; no extension registered - there sometimes won't be, + ;; and it doesn't matter + nil))))] (cond (empty? fragments) (assoc result :text From 2f22b733c1800ee6c07a23e866355c9932602982 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 12 Feb 2020 21:13:36 +0000 Subject: [PATCH 23/37] Tidy up alphordered includes, standardised on use 'log' as alias for timbre. --- src/smeagol/authenticate.clj | 20 ++++++++++---------- src/smeagol/configuration.clj | 12 ++++++------ src/smeagol/formatting.clj | 30 +++++++++++++++++++++++++----- src/smeagol/handler.clj | 12 ++++++------ src/smeagol/history.clj | 9 +++++---- src/smeagol/layout.clj | 3 +-- src/smeagol/middleware.clj | 14 +++++++------- src/smeagol/util.clj | 4 ++-- 8 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/smeagol/authenticate.clj b/src/smeagol/authenticate.clj index e4219e9..fec7e7a 100644 --- a/src/smeagol/authenticate.clj +++ b/src/smeagol/authenticate.clj @@ -5,7 +5,7 @@ [environ.core :refer [env]] [noir.io :as io] [smeagol.configuration :refer [config]] - [taoensso.timbre :as timbre])) + [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -52,7 +52,7 @@ "Return `true` if this `username`/`password` pair match, `false` otherwise" [username password] (let [user ((keyword username) (get-users))] - (timbre/info (str "Authenticating " username " against " password-file-path)) + (log/info (str "Authenticating " username " against " password-file-path)) (and user (:password user) (or @@ -92,7 +92,7 @@ Return `true` if password was successfully changed. Subsequent to user change, their password will be encrypted." [username oldpass newpass] - (timbre/info (format "Changing password for user %s" username)) + (log/info (format "Changing password for user %s" username)) (let [users (get-users) keywd (keyword username) user (keywd users) @@ -110,10 +110,10 @@ {keywd (merge user {:password (password/encrypt newpass)})}))) - (timbre/info (str "Successfully changed password for user " username)) + (log/info (str "Successfully changed password for user " username)) true)) (catch Exception any - (timbre/error any + (log/error any (format "Changing password failed for user %s failed: %s (%s)" username (.getName (.getClass any)) (.getMessage any))) false)))) @@ -138,7 +138,7 @@ `email` address and `admin` flag; *or*, modify an existing user. Return true if user is successfully stored, false otherwise." [username newpass email admin] - (timbre/info "Trying to add user " username) + (log/info "Trying to add user " username) (cond (not (string? username)) (throw (Exception. "Username must be a string.")) (zero? (count username)) (throw (Exception. "Username cannot be zero length")) @@ -160,10 +160,10 @@ (locking password-file-path (spit password-file-path (assoc users (keyword username) (merge user full-details))) - (timbre/info "Successfully added user " username) + (log/info "Successfully added user " username) true) (catch Exception any - (timbre/error any + (log/error any (format "Adding user %s failed: %s (%s)" username (.getName (.getClass any)) (.getMessage any))) false))))) @@ -177,10 +177,10 @@ (locking password-file-path (spit password-file-path (dissoc users (keyword username))) - (timbre/info (str "Successfully deleted user " username)) + (log/info (str "Successfully deleted user " username)) true) (catch Exception any - (timbre/error any + (log/error any (format "Deleting user %s failed: %s (%s)" username (.getName (.getClass any)) (.getMessage any))) false)))) diff --git a/src/smeagol/configuration.clj b/src/smeagol/configuration.clj index 025292a..24d1e55 100644 --- a/src/smeagol/configuration.clj +++ b/src/smeagol/configuration.clj @@ -5,7 +5,7 @@ [clojure.string :as s] [environ.core :refer [env]] [noir.io :as io] - [taoensso.timbre :as timbre])) + [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -73,7 +73,7 @@ and optionally a key :transform, whose value is a function of one argument to be used to transform the value of that key." [m tuples] - (timbre/debug + (log/debug "transform-map:\n" (with-out-str (clojure.pprint/pprint m))) (reduce @@ -112,11 +112,11 @@ file is read (if it is specified and present), but that individual values can be overridden by environment variables." (try - (timbre/info (str "Reading configuration from " config-file-path)) + (log/info (str "Reading configuration from " config-file-path)) (let [file-contents (try (read-string (slurp config-file-path)) (catch Exception x - (timbre/error + (log/error (str "Failed to read configuration from " config-file-path @@ -138,12 +138,12 @@ :smeagol-site-title) config-env-transforms))] (if (env :dev) - (timbre/debug + (log/debug "Loaded configuration\n" (with-out-str (clojure.pprint/pprint config)))) config) (catch Exception any - (timbre/error any "Could not load configuration") + (log/error any "Could not load configuration") {}))) (def config (build-config)) diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 87d8f45..8eff3ec 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -44,6 +44,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Error to show if text to be rendered is nil. +;; TODO: this should go through i18n (def no-text-error "No text: does the file exist?") @@ -150,28 +151,43 @@ ;; I need to put the backticks back in. remarked (if (odd? index) (str "```" fragment "\n```") fragment) first-token (get-first-token fragment) + kw (if-not (empty? first-token) (keyword first-token)) formatter (if-not (empty? first-token) (try - (let [kw (keyword first-token)] - (read-string (-> config :formatters kw :formatter))) + (read-string (-> config :formatters kw :formatter)) (catch Exception _ (do - (log/info "No formatter found for extension `" first-token "`") + (log/info "No formatter found for extension `" kw "`") ;; no extension registered - there sometimes won't be, ;; and it doesn't matter nil))))] (cond (empty? fragments) + ;; We've come to the end of the list of fragments. Reassemble them into + ;; a single HTML text and pass it back. (assoc result :text (local-links (md/md-to-html-string (cs/join "\n\n" (reverse processed)) :heading-anchors true))) formatter - (apply-formatter index result fragments processed fragment first-token formatter) + ;; We've found a formatter to apply to the current fragment, and recurse + ;; on down the list + (let [result (apply-formatter + index + result + fragments + processed + fragment + first-token + formatter)] + (assoc result :extensions (cons kw (:extensions result)))) true - (process-markdown-fragment index result remarked (rest fragments) processed))))) + ;; Otherwise process the current fragment as markdown and recurse on + ;; down the list + (process-markdown-fragment + index result remarked (rest fragments) processed))))) (defn reintegrate-inclusions @@ -182,6 +198,10 @@ ([inclusions text] (let [ks (keys inclusions)] (if (empty? (keys inclusions)) + ;; TODO: this is one opportunity to add scripts at the end of the + ;; constructed text. I've a feeling that that would be a mistake and + ;; that instead we should hand back a map comprising the text and the + ;; keys of the extensions text (let [kw (first ks)] (reintegrate-inclusions diff --git a/src/smeagol/handler.clj b/src/smeagol/handler.clj index 9cbaca5..db580e6 100644 --- a/src/smeagol/handler.clj +++ b/src/smeagol/handler.clj @@ -16,7 +16,7 @@ [smeagol.routes.wiki :refer [wiki-routes]] [smeagol.middleware :refer [load-middleware]] [smeagol.session-manager :as session-manager] - [taoensso.timbre :as timbre] + [taoensso.timbre :as log] [taoensso.timbre.appenders.3rd-party.rotor :as rotor])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -55,9 +55,9 @@ "destroy will be called when your application shuts down, put any clean up code here" [] - (timbre/info "smeagol is shutting down...") + (log/info "smeagol is shutting down...") (cronj/shutdown! session-manager/cleanup-job) - (timbre/info "shutdown complete!")) + (log/info "shutdown complete!")) (defn init @@ -67,7 +67,7 @@ put any initialization code here" [] (try - (timbre/merge-config! + (log/merge-config! {:appenders {:rotor (rotor/rotor-appender {:path "smeagol.log" @@ -80,10 +80,10 @@ (cronj/start! session-manager/cleanup-job) (if (env :dev) (parser/cache-off!)) ;;start the expired session cleanup job - (timbre/info "\n-=[ smeagol started successfully" + (log/info "\n-=[ smeagol started successfully" (when (env :dev) "using the development profile") "]=-") (catch Exception any - (timbre/error any "Failure during startup") + (log/error any "Failure during startup") (destroy)))) ;; timeout sessions after 30 minutes diff --git a/src/smeagol/history.clj b/src/smeagol/history.clj index aca6dbe..e567db3 100644 --- a/src/smeagol/history.clj +++ b/src/smeagol/history.clj @@ -1,10 +1,10 @@ (ns ^{:doc "Explore the history of a page." :author "Simon Brooke"} smeagol.history - (:require [taoensso.timbre :as timbre] - [clj-jgit.porcelain :as git] + (:require [clj-jgit.porcelain :as git] [clj-jgit.internal :as i] - [clj-jgit.querying :as q]) + [clj-jgit.querying :as q] + [taoensso.timbre :as log]) (:import [org.eclipse.jgit.api Git] [org.eclipse.jgit.lib Repository ObjectId] [org.eclipse.jgit.revwalk RevCommit RevTree RevWalk] @@ -39,7 +39,7 @@ "If this `log-entry` contains a reference to this `file-path`, return the entry; else nil." [^String log-entry ^String file-path] - (timbre/info (format "searching '%s' for '%s'" log-entry file-path)) + (log/info (format "searching '%s' for '%s'" log-entry file-path)) (cond (seq (filter (fn* [p1__341301#] (= (first p1__341301#) file-path)) (:changed_files log-entry))) log-entry)) @@ -54,6 +54,7 @@ (try (git/load-repo git-directory-path) (catch java.io.FileNotFoundException fnf + (log/info "Initialising Git repository at" git-directory-path) (git/git-init git-directory-path) (let [repo (git/load-repo git-directory-path)] (git/git-add-and-commit repo "Initial commit") diff --git a/src/smeagol/layout.clj b/src/smeagol/layout.clj index 34a7d0a..920a9b1 100644 --- a/src/smeagol/layout.clj +++ b/src/smeagol/layout.clj @@ -12,8 +12,7 @@ [selmer.parser :as parser] [smeagol.configuration :refer [config]] [smeagol.sanity :refer :all] - [smeagol.util :as util] - [taoensso.timbre :as timbre])) + [smeagol.util :as util])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; diff --git a/src/smeagol/middleware.clj b/src/smeagol/middleware.clj index 92cfac0..4ca288d 100644 --- a/src/smeagol/middleware.clj +++ b/src/smeagol/middleware.clj @@ -1,17 +1,17 @@ (ns ^{:doc "In truth, boilerplate provided by LuminusWeb." :author "Simon Brooke"} smeagol.middleware - (:require [taoensso.timbre :as timbre] - [environ.core :refer [env]] - [selmer.middleware :refer [wrap-error-page]] + (:require [environ.core :refer [env]] + [noir-exception.core :refer [wrap-internal-error]] [prone.middleware :refer [wrap-exceptions]] [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] [ring.middleware.file :refer [wrap-file]] [ring.middleware.resource :refer [wrap-resource]] [ring.middleware.content-type :refer [wrap-content-type]] [ring.middleware.not-modified :refer [wrap-not-modified]] - [noir-exception.core :refer [wrap-internal-error]] - [smeagol.util :as util])) + [selmer.middleware :refer [wrap-error-page]] + [smeagol.util :as util] + [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -39,7 +39,7 @@ (defn log-request [handler] (fn [req] - (timbre/debug req) + (log/debug req) (handler req))) @@ -49,7 +49,7 @@ (def production-middleware - [#(wrap-internal-error % :log (fn [e] (timbre/error e))) + [#(wrap-internal-error % :log (fn [e] (log/error e))) #(wrap-resource % "public") #(wrap-file % util/content-dir {:index-files? false :prefer-handler? true}) diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index 1ef44df..0a5863a 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -10,7 +10,7 @@ [smeagol.authenticate :as auth] [smeagol.configuration :refer [config]] [smeagol.formatting :refer [md->html]] - [taoensso.timbre :as timbre])) + [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -69,7 +69,7 @@ messages (try (i18n/get-messages specifier "i18n" "en-GB") (catch Exception any - (timbre/error + (log/error any (str "Failed to parse accept-language header '" From 40ab296d1a4c7130323e48f0363cea8e424d9b4d Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Thu, 13 Feb 2020 14:44:52 +0000 Subject: [PATCH 24/37] #45: OK, it doesn't work, but it's close. Still getting fragment index instead of fragment text. --- resources/config.edn | 28 ++++++++++++++--- resources/templates/wiki.html | 20 ++++-------- src/smeagol/formatting.clj | 14 +++++++-- src/smeagol/routes/wiki.clj | 59 +++++++++++++++++++++++++++-------- src/smeagol/util.clj | 4 +-- 5 files changed, 88 insertions(+), 37 deletions(-) diff --git a/resources/config.edn b/resources/config.edn index 6ff0faf..7c462b3 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -32,11 +32,29 @@ :default-locale "en-GB" ;; default language used for messages :formatters ;; formatters for processing markdown ;; extensions. - {:vega {:formatter "smeagol.extensions.vega/process-vega" } - :vis {:formatter "smeagol.extensions.vega/process-vega" } - :mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid" } - :backticks {:formatter "smeagol.formatting/process-backticks" } - :pswp {:formatter "smeagol.extensions.photoswipe/process-photoswipe" } + {: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 {}}} + :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 {}}} + :mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid" + :scripts {:core {:local "vendor/mermaid/dist/mermaid.js"}} + :styles {}} + :backticks {:formatter "smeagol.formatting/process-backticks" + :scripts {} + :styles {}} + :pswp {:formatter "smeagol.extensions.photoswipe/process-photoswipe" + :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" + :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"}}} } :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal diff --git a/resources/templates/wiki.html b/resources/templates/wiki.html index cf0655e..7342854 100644 --- a/resources/templates/wiki.html +++ b/resources/templates/wiki.html @@ -1,20 +1,12 @@ {% extends "templates/base.html" %} {% block extra-headers %} - - - - {% style "/vendor/node_modules/photoswipe/dist/photoswipe.css" %} - {% style "/vendor/node_modules/photoswipe/dist/default-skin/default-skin.css" %} - - - - - - - - + {% for script in scripts %} + + {% endfor %} + {% for style in styles %} + + {% endfor %} {% endblock %} {% block content %} diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 8eff3ec..115a2ca 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -182,7 +182,7 @@ fragment first-token formatter)] - (assoc result :extensions (cons kw (:extensions result)))) + (assoc-in result [:extensions kw] (-> config :formatters kw))) true ;; Otherwise process the current fragment as markdown and recurse on ;; down the list @@ -194,7 +194,12 @@ "Given a map of the form produced by `process-text`, return a string of HTML text with the inclusions (if any) reintegrated." ([processed-text] - (reintegrate-inclusions (:inclusions processed-text) (:text processed-text))) + (assoc + processed-text + :content + (reintegrate-inclusions + (:inclusions processed-text) + (:text processed-text)))) ([inclusions text] (let [ks (keys inclusions)] (if (empty? (keys inclusions)) @@ -213,7 +218,10 @@ (defn md->html - "Take this markdown source, and return HTML." + "Take this `md-src` markdown source, and return a map in which: + 1. the key `:content` is bound to the equivalent HTML source; + 2. the key `:extensions`. is bound to details of the extensions + used." [md-src] (reintegrate-inclusions (process-text md-src))) diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index e33cbdc..e4ca7ba 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -25,6 +25,7 @@ [smeagol.uploads :as ul] [taoensso.timbre :as log] [com.stuartsierra.component :as component] + [smeagol.configuration :refer [config]] [smeagol.include.resolve-local-file :as resolve] [smeagol.include :as include])) @@ -123,6 +124,34 @@ (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 + AND the value of `:remote` is not nil, then the value of `:remote` will + 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))) + +(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))))))))) + (defn wiki-page "Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page" [request] @@ -134,19 +163,23 @@ file-name (str page ".md") file-path (cjio/file util/content-dir file-name) exists? (.exists (clojure.java.io/as-file file-path))] - (cond exists? - (do - (log/info (format "Showing page '%s' from file '%s'" page file-path)) - (layout/render "wiki.html" - (merge (util/standard-params request) - {:title page - :page page - :content (md->html - (include/expand-include-md - (:includer md-include-system) - (slurp file-path))) - :editable true}))) - true (response/redirect (str "/edit?page=" page)))))) + (if exists? + (do + (log/info (format "Showing page '%s' from file '%s'" page file-path)) + (let [processed-text (md->html + (include/expand-include-md + (:includer md-include-system) + (slurp file-path)))] + (layout/render "wiki.html" + (merge (util/standard-params request) + processed-text + {:title page + :scripts (collect-preferred processed-text :scripts) + :styles (collect-preferred processed-text :styles) + :page page + :editable true})))) + ;else + (response/redirect (str "/edit?page=" page)))))) (defn history-page diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index 0a5863a..972a0a7 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -56,8 +56,8 @@ {:user user :admin (auth/get-admin user) :js-from (:js-from config) - :side-bar (md->html (slurp (cjio/file content-dir "_side-bar.md"))) - :header (md->html (slurp (cjio/file content-dir "_header.md"))) + :side-bar (:content (md->html (slurp (cjio/file content-dir "_side-bar.md")))) + :header (:content (md->html (slurp (cjio/file content-dir "_header.md")))) :version (System/getProperty "smeagol.version")})) From 8032ad60af1de27bb8a273c38ce3a4c5f00aacfe Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Thu, 13 Feb 2020 19:46:58 +0000 Subject: [PATCH 25/37] No actual progress. --- src/smeagol/extensions/photoswipe.clj | 5 ++-- src/smeagol/formatting.clj | 43 ++++++++------------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/smeagol/extensions/photoswipe.clj b/src/smeagol/extensions/photoswipe.clj index 2df235e..ee80792 100644 --- a/src/smeagol/extensions/photoswipe.clj +++ b/src/smeagol/extensions/photoswipe.clj @@ -175,8 +175,6 @@ (defn process-photoswipe [^String url-or-pswp-spec ^Integer index] - (log/info "process-photoswipe called with arg1 `" - url-or-pswp-spec "`; arg2 `" index "`.") (let [data (resource-url-or-data->data url-or-pswp-spec) spec (cs/trim (:data data)) result @@ -184,5 +182,6 @@ (cs/starts-with? spec "![") (process-simple-photoswipe spec index) (process-full-photoswipe spec index))] - (log/info "process-photoswipe returning `" result "`.") +;; (log/info "process-photoswipe returning `" result "`.") + result )) diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 115a2ca..3bb0d63 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -63,32 +63,9 @@ no-text-error)) -(defn yaml->json - "Rewrite this string, assumed to be in YAML format, as JSON." - [^String yaml-src] - (json/write-str (yaml/parse-string yaml-src))) - - (declare process-text) -(defn process-vega - "Process this `vega-src` string, assumed to be in YAML format, into a specification - of a Vega chart, and add the plumbing to render it." - [^String vega-src ^Integer index] - (str - "
\n" - "")) - - (defn process-backticks "Effectively, escape the backticks surrounding this `text`, by protecting them from the `md->html` filter." @@ -117,7 +94,7 @@ (cons fragment processed))) -(defn- apply-formatter +(defn apply-formatter "Within the context of `process-text`, process a fragment for which an explicit §formatter has been identified. @@ -128,11 +105,14 @@ [index result fragments processed fragment token formatter] (let [kw (keyword (str "inclusion-" index))] - (process-text - (inc index) - (assoc-in result [:inclusions kw] (apply formatter (list (subs fragment (count token)) index))) - (rest fragments) - (cons kw processed)))) + (assoc-in + (process-text + (inc index) + result + (rest fragments) + (cons kw processed)) + [:inclusions kw] + (apply formatter (list (subs fragment (count token)) index))))) (defn process-text @@ -182,8 +162,11 @@ fragment first-token formatter)] + ;; TODO: consistency: either these things are `extensions`, or + ;; they're `formatters`. I incline to the view that they're + ;; `:extensions` (assoc-in result [:extensions kw] (-> config :formatters kw))) - true + :else ;; Otherwise process the current fragment as markdown and recurse on ;; down the list (process-markdown-fragment From 37d850d30afaef02ecc440ba799e4874fb05e9a9 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Thu, 13 Feb 2020 21:07:36 +0000 Subject: [PATCH 26/37] Still no actual progress. --- resources/config.edn | 27 +++++----- src/smeagol/extensions/photoswipe.clj | 8 +-- src/smeagol/extensions/test.clj | 10 ++++ src/smeagol/extensions/vega.clj | 2 +- src/smeagol/formatting.clj | 72 ++++++++++++++++++--------- src/smeagol/util.clj | 36 +++++++------- test/smeagol/test/formatting.clj | 10 +++- 7 files changed, 102 insertions(+), 63 deletions(-) create mode 100644 src/smeagol/extensions/test.clj diff --git a/resources/config.edn b/resources/config.edn index 7c462b3..87d1361 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -32,22 +32,12 @@ :default-locale "en-GB" ;; default language used for messages :formatters ;; formatters for processing markdown ;; extensions. - {: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 {}}} - :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 {}}} + {:backticks {:formatter "smeagol.formatting/process-backticks" + :scripts {} + :styles {}} :mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid" :scripts {:core {:local "vendor/mermaid/dist/mermaid.js"}} :styles {}} - :backticks {:formatter "smeagol.formatting/process-backticks" - :scripts {} - :styles {}} :pswp {:formatter "smeagol.extensions.photoswipe/process-photoswipe" :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"} @@ -55,6 +45,17 @@ :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"}}} + :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 {}}} + :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 {}}} } :log-level :info ;; the minimum logging level; one of ;; :trace :debug :info :warn :error :fatal diff --git a/src/smeagol/extensions/photoswipe.clj b/src/smeagol/extensions/photoswipe.clj index ee80792..7b16a09 100644 --- a/src/smeagol/extensions/photoswipe.clj +++ b/src/smeagol/extensions/photoswipe.clj @@ -176,12 +176,8 @@ (defn process-photoswipe [^String url-or-pswp-spec ^Integer index] (let [data (resource-url-or-data->data url-or-pswp-spec) - spec (cs/trim (:data data)) - result + spec (cs/trim (:data data))] (if (cs/starts-with? spec "![") (process-simple-photoswipe spec index) - (process-full-photoswipe spec index))] -;; (log/info "process-photoswipe returning `" result "`.") - result - )) + (process-full-photoswipe spec index)))) diff --git a/src/smeagol/extensions/test.clj b/src/smeagol/extensions/test.clj new file mode 100644 index 0000000..ffe5c1a --- /dev/null +++ b/src/smeagol/extensions/test.clj @@ -0,0 +1,10 @@ +(ns ^{:doc "Very simple extension for testing the extension processing flow." + :author "Simon Brooke"} + smeagol.extensions.test) + + +(def process-test-return-value "") + +(defn process-test + [^String fragment ^Integer index] + process-test-return-value) diff --git a/src/smeagol/extensions/vega.clj b/src/smeagol/extensions/vega.clj index 1b9e2de..49edea0 100644 --- a/src/smeagol/extensions/vega.clj +++ b/src/smeagol/extensions/vega.clj @@ -1,4 +1,4 @@ -(ns ^{:doc "Format Semagol's extended markdown format." +(ns ^{:doc "Format vega/vis extensions to Semagol's extended markdown format." :author "Simon Brooke"} smeagol.extensions.vega (:require [clojure.data.json :as json] diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 3bb0d63..bbcaaa7 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -93,10 +93,20 @@ 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] + (if (and (map? v1) (map? v2)) + (merge-with deep-merge v1 v2) + v2))] + (if (some identity vs) + (reduce #(rec-merge %1 %2) v vs) + (last vs)))) + (defn apply-formatter "Within the context of `process-text`, process a fragment for which an explicit - §formatter has been identified. + `formatter` has been identified. As with `process-text`, this function returns a map with two top-level keys: `:inclusions`, a map of constructed keywords to inclusion specifications, @@ -104,16 +114,28 @@ corresponding inclusion should be inserted." [index result fragments processed fragment token formatter] (let - [kw (keyword (str "inclusion-" index))] - (assoc-in - (process-text - (inc index) + [ident (keyword (str "inclusion-" index))] + (process-text + (inc index) + (deep-merge result - (rest fragments) - (cons kw processed)) - [:inclusions kw] - (apply formatter (list (subs fragment (count token)) index))))) - + {:inclusions {ident (apply formatter (list (subs fragment (count token)) index))} + :extensions (cons (keyword token) (:extensions result))}) + 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) (defn process-text "Process this `text`, assumed to be markdown potentially containing both local links @@ -124,7 +146,7 @@ inclusion specifications, and `:text`, an HTML text string with the keywords present where the corresponding inclusion should be inserted." ([^String text] - (process-text 0 {:inclusions {}} (cs/split (or text "") #"```") '())) + (process-text 0 {} (cs/split (or text "") #"```") '())) ([index result fragments processed] (let [fragment (first fragments) ;; if I didn't find a formatter for a back-tick marked fragment, @@ -154,24 +176,28 @@ formatter ;; We've found a formatter to apply to the current fragment, and recurse ;; on down the list - (let [result (apply-formatter - index - result - fragments - processed - fragment - first-token - formatter)] - ;; TODO: consistency: either these things are `extensions`, or - ;; they're `formatters`. I incline to the view that they're - ;; `:extensions` - (assoc-in result [:extensions kw] (-> config :formatters kw))) + (let + [ident (keyword (str "inclusion-" index))] + (deep-merge + (process-text + (inc index) + result + (rest fragments) + (cons ident processed)) + {:inclusions {ident (apply formatter (list (subs fragment (count first-token)) index))} + :extensions (cons kw (:extensions result))})) :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)" ) (defn reintegrate-inclusions "Given a map of the form produced by `process-text`, return a string of HTML text diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index 972a0a7..9e0a6ba 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -61,33 +61,31 @@ :version (System/getProperty "smeagol.version")})) -(defn- raw-get-messages +(def get-messages "Return the most acceptable messages collection we have given the `Accept-Language` header in this `request`." - [request] - (let [specifier ((:headers request) "accept-language") - messages (try - (i18n/get-messages specifier "i18n" "en-GB") - (catch Exception any - (log/error - any - (str - "Failed to parse accept-language header '" - specifier - "'")) - {}))] + (memoize + (fn [request] + (let [specifier ((:headers request) "accept-language") + messages (try + (i18n/get-messages specifier "i18n" "en-GB") + (catch Exception any + (log/error + any + (str + "Failed to parse accept-language header '" + specifier + "'")) + {}))] (merge messages - config))) - - -(def get-messages (memoize raw-get-messages)) + config))))) (defn get-message "Return the message with this `message-key` from this `request`. - if not found, return this `default`, if provided; else return the - `message-key`." + if not found, return this `default`, if provided; else return the + `message-key`." ([message-key request] (get-message message-key message-key request)) ([message-key default request] diff --git a/test/smeagol/test/formatting.clj b/test/smeagol/test/formatting.clj index 2887047..9cb4956 100644 --- a/test/smeagol/test/formatting.clj +++ b/test/smeagol/test/formatting.clj @@ -1,6 +1,7 @@ (ns smeagol.test.formatting (:require [clojure.test :refer :all] - [smeagol.formatting :refer [local-links no-text-error]])) + [smeagol.formatting :refer [local-links no-text-error]] + [smeagol.extensions.test :refer :all])) (deftest test-local-links (testing "Rewriting of local links" @@ -10,3 +11,10 @@ (let [text (str "# This is a heading" "[This is a foreign link](http://to.somewhere)")] (is (= (local-links text) text) "Foreign links should be unchanged")))) + +(deftest test-process-text + (testing "The process-text flow" + (let [expected process-test-return-value + actual (process-text "```test + ```")] + (is (= actual expected))))) From 0417fda9107234ff9aa04e3394403a0b0612638b Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 14 Feb 2020 08:51:14 +0000 Subject: [PATCH 27/37] Added TODO note, only. --- src/smeagol/formatting.clj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index fa4f2f6..750cf02 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -43,6 +43,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Error to show if text to be rendered is nil. +;; TODO: should go through i18n (def no-text-error "No text: does the file exist?") @@ -187,3 +188,4 @@ (reintegrate-inclusions (process-text md-src))) + From 2e106256f85d605a6360f45c179f9a9ae936ddb1 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 14 Feb 2020 11:43:07 +0000 Subject: [PATCH 28/37] More Ginny stylesheet --- resources/public/content/stylesheet.css | 101 ++++++++++++++++-------- 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/resources/public/content/stylesheet.css b/resources/public/content/stylesheet.css index 81a661c..62690e0 100644 --- a/resources/public/content/stylesheet.css +++ b/resources/public/content/stylesheet.css @@ -24,6 +24,16 @@ ## html elements generally in alphabetic order */ +a { + color: darkgray; + font-weight: bold; +} + +a:hover { + color: darkgray; + background:rgba(200,200,200,0.8); +} + body { margin: 0; padding: 0; @@ -52,14 +62,15 @@ dd { /* footer of the page - not-editable, provided by Smeagol */ footer { - border-top: thin solid gray; + border-top: thin solid silver; + color: gray; + background:rgba(200,200,200,0.8); clear: both; font-size: smaller; text-align: center; - color: gray; - background: rgba(224,224,224,0.95); width: 100%; margin: 0; + min-height: 4px; padding: 0.25em 0; bottom:0; position:fixed; @@ -71,9 +82,14 @@ footer { } footer div { + display: none; padding: 0.1em; } +footer:hover div { + display: block; +} + form { border: thin solid silver; } @@ -83,8 +99,6 @@ header { margin-top: 0; width:100%; max-width: 100%; - background-color: gray; - color: white; } header h1 { @@ -93,7 +107,6 @@ header h1 { header a { font-weight: bold; - color: white; } header a:hover { @@ -131,12 +144,12 @@ ins { label { width: 20%; min-width: 20em; - border-right: thin solid gray; + border-right: thin solid silver; display: inline-block; } table { - border: 2px solid black; + border: thin solid silver; border-collapse: collapse; } @@ -148,7 +161,7 @@ th, td { text-align: left; vertical-align: top; padding: 0.15em 1.5em; - border: 1px solid gray; + border: 1px solid silver; } th { @@ -166,6 +179,7 @@ th { /* left bar for all pages in the Wiki - editable, provided by users. Within main-container */ #side-bar { + display: none; width: 17%; height: 100%; float: left; @@ -173,10 +187,10 @@ th { /* cookies information box, fixed, in right margin, just above footer */ #cookies { - width: 30%; + width: 20%; float: right; position: fixed; - bottom: 3.5em; + bottom: 8px; right: 0; z-index: 175; background: transparent; @@ -190,8 +204,8 @@ th { text-align: right; padding: 0.25em 2em; border-radius: 0.25em; - color: white; - background:rgba(40,40,40,0.8); + color: gray; + background:rgba(200,200,200,0.8); } /* more-about-cookies box, normally hidden */ @@ -199,9 +213,9 @@ th { display: none; padding: 0.5em 2em; border-radius: 0.5em; - color: white; - background:rgba(40,40,40,0.8); - border-bottom: thin solid white; + color: gray; + background:rgba(200,200,200,0.8); + border-bottom: thin solid gray; } /* but magically appears on mouseover */ @@ -242,8 +256,8 @@ th { right: 0; padding: 0.25em 2em; border-radius: 0.25em; - color: white; - background:rgba(40,40,40,0.8); + color: gray; + background:rgba(200,200,200,0.8); font-size: 66%; } @@ -254,7 +268,11 @@ th { .minor-controls a { float: right; padding: 0.25em 2em; - color: white; + color: gray; +} + +.minor-controls a:hover { + color: darkgray; } .pseudo-input { @@ -303,8 +321,7 @@ th { /* content of the current page in the Wiki - editable, provided by users. Within main-container */ #content { border: thin solid silver; - width: 80%; - float: right; + width: 100%; padding-bottom: 5em; } @@ -312,16 +329,29 @@ th { display: none; } + #header { + font-size: smaller; + } + /* top-of-page navigation, not editable, provided by Smeagol */ #nav{ margin: 0; padding: 0; top: 0; - width: 100%; + min-height: 4px; _position: absolute; _top: expression(document.documentElement.scrollTop); z-index: 149; - background:rgba(40,40,40,0.8); + color: gray; + background:rgba(200,200,200,0.8); + } + + #nav #nav-menu { + display: none; + } + + #nav:hover #nav-menu { + display: block; } /* only needed for fly-out menu effect on tablet and phone stylesheets */ @@ -341,14 +371,14 @@ th { } #nav menu li a { - color: white; + color: gray; text-decoration: none; font-weight: bold; padding: 0.1em 0.75em; margin: 0; } - #nav menu li.active a { background: gray;} + #nav menu li.active a { background: gray; color: white;} li.nav-item a:hover { background: rgb( 240, 240, 240) } li.nav-item a:active { background: gray; color: white; } @@ -379,17 +409,15 @@ th { padding: 0; position: fixed; z-index: 149; - color: silver; - background:rgba(40,40,40,0.9); + color: black; + background:rgba(200,200,200,0.9); } #nav a { - color: white; - text-decoration: none; font-weight: bold; } - #nav:hover #nav-menu { + #nav:hover #nav-menu, #nav:hover #phone-side-bar { display: block; list-style-type: none; width: 100%; @@ -455,18 +483,21 @@ th { display: none; } + #header { + display: none; + } + #nav{ margin: 0; padding: 0; position: fixed; z-index: 149; - color: silver; - background:rgba(40,40,40,0.9); + color: black; + background:rgba(200,200,200,0.9); } #nav a { - color: white; - text-decoration: none; + color: black; font-weight: bold; } @@ -491,6 +522,8 @@ th { } #nav menu li a { + color: black; + font-weight: bold; } #nav ul li.active a { background: silver;} From c06fce3007166b790e83ff775a8526004857bef7 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 14 Feb 2020 11:43:50 +0000 Subject: [PATCH 29/37] Bugfix: looking in the wrong directory for uploads. --- resources/templates/base.html | 4 +++- src/smeagol/routes/wiki.clj | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/resources/templates/base.html b/resources/templates/base.html index 7a31f07..01152af 100644 --- a/resources/templates/base.html +++ b/resources/templates/base.html @@ -46,7 +46,9 @@

{% i18n site-title %}: {{title}}

- {{header|safe}} + {% if message %}

{{message}}

diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index e33cbdc..c87dbef 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -15,6 +15,7 @@ [noir.util.route :as route] [noir.session :as session] [smeagol.authenticate :as auth] + [smeagol.configuration :refer [config]] [smeagol.diff2html :as d2h] [smeagol.formatting :refer [md->html]] [smeagol.history :as hist] @@ -187,7 +188,6 @@ [request] (let [params (keywordize-keys (:params request)) - data-path (str util/content-dir "/uploads/") cl (count (io/resource-path)) files (map @@ -202,11 +202,19 @@ (fs/mod-time %) (format-instant (fs/mod-time %))) (fs/name %) - (subs (str (fs/absolute %)) cl)]) + (try + (subs (str (fs/absolute %)) cl) + (catch StringIndexOutOfBoundsException x + (log/error "Could not resolve relative path for" % + ";\n resource-path is:" (io/resource-path) + ";\n absolute path is:" (fs/absolute %) + ";\n data-path is:" util/upload-dir + ";\n content path is:" (:content-dir config)) + %))]) (remove #(or (cs/starts-with? (fs/name %) ".") (fs/directory? %)) - (file-seq (clojure.java.io/file data-path))))] + (file-seq (clojure.java.io/file util/upload-dir))))] (log/info (with-out-str (pprint files))) (layout/render "list-uploads.html" From 392a5f82ecbc6f824e4efee9ff4eeb5fca3540f2 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 14 Feb 2020 16:08:17 +0000 Subject: [PATCH 30/37] These are the genuine improvements out of today 1. Table sorting 2. Fixed edit page title bugette 3. SimpleMDE working again. --- resources/i18n/en-GB.edn | 2 ++ resources/templates/edit-users.html | 23 ++++++++---- resources/templates/edit.html | 6 ++-- resources/templates/list-uploads.html | 19 +++++++--- src/smeagol/routes/wiki.clj | 51 ++++++++++++--------------- src/smeagol/util.clj | 50 ++++++++++++++++++++++++-- 6 files changed, 107 insertions(+), 44 deletions(-) diff --git a/resources/i18n/en-GB.edn b/resources/i18n/en-GB.edn index 6f4d8b2..442f097 100644 --- a/resources/i18n/en-GB.edn +++ b/resources/i18n/en-GB.edn @@ -131,6 +131,8 @@ "Smeagol has been unable to find some of the resources on which it depends, possibly because of misconfiguration or missing environment variables." ;; used in sanity check report + :sortable "You can sort this table by selecting column headers" + ;; used for sortable tables :user-lacks-field "User record in the passwd file lacks a field" ;; used in sanity check report :username-prompt "Username" ;; text of the username widget prompt on edit user page diff --git a/resources/templates/edit-users.html b/resources/templates/edit-users.html index 2621576..06d0d2e 100644 --- a/resources/templates/edit-users.html +++ b/resources/templates/edit-users.html @@ -1,10 +1,18 @@ {% extends "templates/base.html" %} +{% block extra-headers %} + {% script "/vendor/node_modules/tablesort/dist/tablesort.min.js" %} +{% endblock %} {% block content %}
- - - +

+ {% i18n sortable %} +

+
{% i18n edit-col-hdr %}{% i18n del-col-hdr %}
+ + + + {% for user in users %} @@ -13,11 +21,12 @@ {% endfor %} - - - - + +
{% i18n user-title-prefix %}{% i18n edit-col-hdr %}{% i18n del-col-hdr %}
{% i18n del-col-hdr %} {{user}}
{% i18n add-user-label %}
{% i18n add-user-label %}
+ {% endblock %} diff --git a/resources/templates/edit.html b/resources/templates/edit.html index 36426dc..44ee54a 100644 --- a/resources/templates/edit.html +++ b/resources/templates/edit.html @@ -1,11 +1,11 @@ {% extends "templates/base.html" %} {% block extra-headers %} - {% ifequal js-from ":cloudflare" %} + {% ifequal js-from ":cdnjs" %} {% else %} - {% style "/vendor/simplemde/dist/simplemde.min.css" %} - {% script "/vendor/simplemde/dist/simplemde.min.js" %} + {% style "vendor/simplemde/dist/simplemde.min.css" %} + {% script "vendor/simplemde/dist/simplemde.min.js" %} {% endifequal %} {% endblock %} diff --git a/resources/templates/list-uploads.html b/resources/templates/list-uploads.html index e759dac..4289485 100644 --- a/resources/templates/list-uploads.html +++ b/resources/templates/list-uploads.html @@ -1,4 +1,10 @@ {% extends "templates/base.html" %} +{% block extra-headers %} + {% script "/vendor/node_modules/tablesort/dist/tablesort.min.js" %} + {% script "/vendor/node_modules/tablesort/dist/sorts/tablesort.number.min.js" %} + {% script "/vendor/node_modules/tablesort/dist/sorts/tablesort.date.min.js" %} + {% script "/vendor/node_modules/tablesort/dist/sorts/tablesort.monthname.min.js" %} +{% endblock %} {% block content %}
@@ -9,12 +15,15 @@

- - +

+ {% i18n sortable %} +

+
+ - + {% for entry in files %} @@ -26,9 +35,11 @@ - {% endfor %}
Name Uploaded Type thisTo get thisTo get this
{% if entry.is-image %} {{entry.name|capitalize}} {% else %} link {% endif %}
+ {% endblock %} diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index c87dbef..104b6e0 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -161,7 +161,8 @@ (log/info (format "Showing history of page '%s'" page)) (layout/render "history.html" (merge (util/standard-params request) - {:title (util/get-message :history-title-prefix request) + {:title (str (util/get-message :history-title-prefix request) + " " page) :page page :history (hist/find-history repo-path file-name)})))) @@ -175,7 +176,7 @@ If `template` is supplied, use that as the formatting template as specified for java.time.Formatter. Assumes system default timezone. Returns a string." ([^Long unix-time] - (format-instant unix-time "EEEE, dd MMMM YYYY")) + (format-instant unix-time "dd MMMM YYYY")) ([^Long unix-time ^String template] (jt/format (java-time/formatter template) @@ -188,33 +189,26 @@ [request] (let [params (keywordize-keys (:params request)) - cl (count (io/resource-path)) files - (map - #(zipmap - [:base-name :is-image :modified :name :resource] - [(fs/base-name %) - (if - (and (fs/extension %) - (image-extns (cs/lower-case (fs/extension %)))) - true false) - (if - (fs/mod-time %) - (format-instant (fs/mod-time %))) - (fs/name %) - (try - (subs (str (fs/absolute %)) cl) - (catch StringIndexOutOfBoundsException x - (log/error "Could not resolve relative path for" % - ";\n resource-path is:" (io/resource-path) - ";\n absolute path is:" (fs/absolute %) - ";\n data-path is:" util/upload-dir - ";\n content path is:" (:content-dir config)) - %))]) - (remove - #(or (cs/starts-with? (fs/name %) ".") - (fs/directory? %)) - (file-seq (clojure.java.io/file util/upload-dir))))] + (sort-by + (juxt :name (fn [x] (- 0 (count (:resource x))))) + (map + #(zipmap + [:base-name :is-image :modified :name :resource] + [(fs/base-name %) + (if + (and (fs/extension %) + (image-extns (cs/lower-case (fs/extension %)))) + true false) + (if + (fs/mod-time %) + (format-instant (fs/mod-time %))) + (fs/name %) + (util/local-url %)]) + (remove + #(or (cs/starts-with? (fs/name %) ".") + (fs/directory? %)) + (file-seq (clojure.java.io/file util/upload-dir)))))] (log/info (with-out-str (pprint files))) (layout/render "list-uploads.html" @@ -236,6 +230,7 @@ files) })))) + ;;;; end of list-uploads section ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn upload-page diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index 1ef44df..d33ee32 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -2,6 +2,7 @@ :author "Simon Brooke"} smeagol.util (:require [clojure.java.io :as cjio] + [clojure.string :as cs] [environ.core :refer [env]] [me.raynes.fs :as fs] [noir.io :as io] @@ -10,7 +11,7 @@ [smeagol.authenticate :as auth] [smeagol.configuration :refer [config]] [smeagol.formatting :refer [md->html]] - [taoensso.timbre :as timbre])) + [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -49,6 +50,51 @@ (def upload-dir (str (cjio/file content-dir "uploads"))) +(def local-url-base + (let [a (str (fs/absolute content-dir))] + (subs a 0 (- (count a) (count "content"))))) + +(defn not-servable-reason + "As a string, the reason this `file-path` cannot safely be served, or `nil` + 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)] + (cond + (cs/includes? file-path "..") + (cs/join " " file-path + "Attempts to ascend the file hierarchy are disallowed.") + (not (cs/starts-with? path local-url-base)) + (cs/join " " [path "is not servable"]) + (not (fs/exists? path)) + (cs/join " " [path "does not exist"]) + (not (fs/readable? path)) + (cs/join " " [path "is not readable"])))) + +(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))) + +(defn local-url + "Return a local URL for this `file-path`, or a deliberate 404 if none + can be safely served." + [file-path] + (try + (let [path (fs/absolute file-path) + problem (not-servable-reason path)] + (if + (empty? problem) + (subs (str path) (count local-url-base)) + (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)))) + (defn standard-params "Return a map of standard parameters to pass to the template renderer." [request] @@ -69,7 +115,7 @@ messages (try (i18n/get-messages specifier "i18n" "en-GB") (catch Exception any - (timbre/error + (log/error any (str "Failed to parse accept-language header '" From 151987e598b5ffc6c2ee997110d0f5259511f73b Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Sun, 16 Feb 2020 13:51:41 +0000 Subject: [PATCH 31/37] #45,#46: done --- resources/config.edn | 30 +++++++++------- resources/templates/wiki.html | 6 ++-- src/smeagol/formatting.clj | 42 ++++++++++++---------- src/smeagol/routes/wiki.clj | 68 +++++++++++++++++++++++++---------- src/smeagol/util.clj | 48 ++++++++++++++++++++----- 5 files changed, 131 insertions(+), 63 deletions(-) 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." From 0f0f2ecc48beda4b898adafc29b04e4169ae1cae Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 19 Feb 2020 12:50:52 +0000 Subject: [PATCH 32/37] Re-separated apply-formatter. Still doesn't work. (but doesn't blow up) --- src/smeagol/formatting.clj | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 5891eeb..fb94602 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -115,15 +115,12 @@ corresponding inclusion should be inserted." [index result fragments processed fragment token formatter] (let - [ident (keyword (str "inclusion-" index))] + [kw (keyword (str "inclusion-" index))] (process-text (inc index) - (deep-merge - result - {:inclusions {ident (apply formatter (list (subs fragment (count token)) index))} - :extensions (cons (keyword token) (:extensions result))}) - fragments - (cons ident processed)))) + (assoc-in result [:inclusions kw] (apply formatter (list (subs fragment (count token)) index))) + (rest fragments) + (cons kw processed)))) ;; (apply-formatter ;; 3 @@ -177,23 +174,9 @@ (cs/join "\n\n" (reverse processed)) :heading-anchors true))) formatter - ;; We've found a formatter to apply to the current fragment, and recurse - ;; on down the list - (let - [ident (keyword (str "inclusion-" index))] - (deep-merge - (process-text - (inc index) - result - (rest fragments) - (cons ident processed)) - {:inclusions {ident (apply formatter (list (subs fragment (count first-token)) index))} - :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))))) + (apply-formatter index result fragments processed fragment first-token formatter) + true + (process-markdown-fragment index result remarked (rest fragments) processed))))) ;; (process-text ;; "pswp From 0649ecf1958010f17a39fd31c46c95ea66814c7b Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 19 Feb 2020 15:01:09 +0000 Subject: [PATCH 33/37] Thank fuck, it works. Now to remove all the crud. --- src/smeagol/formatting.clj | 163 +++++++++++++++++++------------ src/smeagol/routes/wiki.clj | 19 ++-- src/smeagol/util.clj | 12 ++- test/smeagol/test/formatting.clj | 12 +-- 4 files changed, 125 insertions(+), 81 deletions(-) diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index fb94602..cacc9ca 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -9,6 +9,7 @@ [smeagol.configuration :refer [config]] [smeagol.extensions.mermaid :refer [process-mermaid]] [smeagol.extensions.photoswipe :refer [process-photoswipe]] + [smeagol.extensions.vega :refer [process-vega]] [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -68,7 +69,7 @@ (defn process-backticks "Effectively, escape the backticks surrounding this `text`, by protecting them - from the `md->html` filter." + from the `process-text` filter." [^String text ^Integer index] (str "
```" (.trim text) "\n```
")) @@ -76,7 +77,9 @@ (defn get-first-token "Return the first space-separated token of this `string`." [^String string] - (if string (first (cs/split string #"[^a-zA-Z0-9]+")))) + (try + (if string (first (cs/split (first (cs/split-lines string)) #"[^a-zA-Z0-9]+"))) + (catch NullPointerException _ nil))) (defn- process-markdown-fragment @@ -86,7 +89,7 @@ `: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." - [index result fragment fragments processed] + [^Integer index ^clojure.lang.Associative result ^String fragment fragments processed] (process-text (inc index) result @@ -113,14 +116,29 @@ `: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." - [index result fragments processed fragment token formatter] + [^Integer index + ^clojure.lang.Associative result + fragments + processed + ^String fragment + ^String token + formatter] + (log/info "index:" index "(type result):" (type result) "(type fragments):" (type fragments) "fragment:" fragment "token:" token ":formatter" formatter) (let - [kw (keyword (str "inclusion-" index))] + [inky (keyword (str "inclusion-" index)) + fkey (keyword token)] (process-text (inc index) - (assoc-in result [:inclusions kw] (apply formatter (list (subs fragment (count token)) index))) - (rest fragments) - (cons kw processed)))) + (deep-merge + result + {:inclusions {inky (eval (list formatter (subs fragment (count token)) index))} + :extensions {fkey (-> config :formatters fkey)}}) +;; (assoc-in +;; (assoc-in result [:inclusions inky] (eval (list formatter (subs fragment (count token)) index))) +;; [:extensions fkey] (-> config :formatters fkey)) + (rest fragments) + (cons inky processed)))) + ;; (apply-formatter ;; 3 @@ -135,55 +153,16 @@ ;; "pswp" ;; smeagol.extensions.photoswipe/process-photoswipe) -(defn process-text - "Process this `text`, assumed to be markdown potentially containing both local links - and YAML visualisation specifications, and return a map comprising JSON visualisation - specification, and HTML text with markers for where those should be reinserted. - - 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] - (let [fragment (first fragments) - ;; if I didn't find a formatter for a back-tick marked fragment, - ;; I need to put the backticks back in. - remarked (if (odd? index) (str "```" fragment "\n```") fragment) - first-token (get-first-token fragment) - kw (if-not (empty? first-token) (keyword first-token)) - formatter (if-not - (empty? first-token) - (try - (read-string (-> config :formatters kw :formatter)) - (catch Exception _ - (do - (log/info "No formatter found for extension `" kw "`") - ;; no extension registered - there sometimes won't be, - ;; and it doesn't matter - nil))))] - (cond - (empty? fragments) - ;; We've come to the end of the list of fragments. Reassemble them into - ;; a single HTML text and pass it back. - (assoc result :text - (local-links - (md/md-to-html-string - (cs/join "\n\n" (reverse processed)) - :heading-anchors true))) - formatter - (apply-formatter index result fragments processed fragment first-token formatter) - true - (process-markdown-fragment index result remarked (rest fragments) processed))))) +(defn reassemble-text + "Reassemble these processed strings into a complete text, and process it as + Markdown." + [result processed] + (assoc result :text + (local-links + (md/md-to-html-string + (cs/join "\n\n" (reverse processed)) + :heading-anchors true)))) -;; (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 @@ -212,13 +191,73 @@ (cs/replace (kw inclusions) "\\/" "/")))))))) +(defn process-text + [^Integer index ^clojure.lang.Associative result fragments processed] + (let [fragment (first fragments) + ;; if I didn't find a formatter for a back-tick marked fragment, + ;; I need to put the backticks back in. + remarked (if (odd? index) (str "```" fragment "\n```") fragment) + first-token (get-first-token fragment) + kw (if-not (empty? first-token) (keyword first-token)) + formatter (if + kw + (try + (read-string (-> config :formatters kw :formatter)) + (catch Exception _ + (do + (log/info "No formatter found for extension `" kw "`") + ;; no extension registered - there sometimes won't be, + ;; and it doesn't matter + nil))))] + (cond + (empty? fragments) + ;; We've come to the end of the list of fragments. Reassemble them into + ;; a single HTML text and pass it back. + (reassemble-text result processed) + formatter + (apply-formatter index result fragments processed fragment first-token formatter) + true + (process-markdown-fragment index result remarked (rest fragments) processed)))) + (defn md->html - "Take this `md-src` markdown source, and return a map in which: - 1. the key `:content` is bound to the equivalent HTML source; - 2. the key `:extensions`. is bound to details of the extensions - used." - [md-src] - (reintegrate-inclusions (process-text md-src))) + "Process this `text`, assumed to be markdown potentially containing both local links + and YAML visualisation specifications, and return a map comprising JSON visualisation + specification, and HTML text with markers for where those should be reinserted. + + 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." + [^clojure.lang.Associative context] + (reintegrate-inclusions + (process-text + 0 + (assoc context :extensions #{}) + (cs/split (or (:source context) "") #"```") + '()))) + + +;; (def first-token "pswp") +;; (def kw (keyword "pswp")) +;; (def fragment "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)") +;; (def index 0) +;; (def formatter (read-string (-> config :formatters kw :formatter))) +;; formatter +;; (eval (list formatter (subs fragment (count first-token)) index)) +;; (process-photoswipe (subs fragment (count first-token)) index) + +;; (process-text +;; {:source "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 {:source (slurp (clojure.java.io/file smeagol.util/content-dir "Extensible Markup.md"))}) + diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj index a3456c1..a5d4fde 100644 --- a/src/smeagol/routes/wiki.clj +++ b/src/smeagol/routes/wiki.clj @@ -107,7 +107,7 @@ (merge (util/standard-params request) {:title (str (util/get-message :edit-title-prefix request) " " page) :page page - :side-bar (md->html (slurp (cjio/file util/content-dir side-bar))) + :side-bar (md->html (assoc request :source (slurp (cjio/file util/content-dir side-bar)))) :content (if exists? (slurp file-path) "") :exists exists?}))))))) @@ -175,13 +175,15 @@ (keys (-> processed-text :extensions extension-key resource-type)))) (keys (:extensions processed-text)))))) -(cjio/file content-dir "vendor/node_modules/photoswipe/dist/photoswipe.min.js") +;; (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" ))) +;; (def processed-text (md->html {:source (slurp "resources/public/content/Simplified example gallery.md" )})) -(preferred-source (-> processed-text :extensions :pswp :scripts :core) :pswp) +;; (preferred-source (-> processed-text :extensions :pswp :scripts :core) :pswp) -(collect-preferred processed-text :scripts) +;; (-> processed-text :extensions) + +;; (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" @@ -198,9 +200,10 @@ (do (log/info (format "Showing page '%s' from file '%s'" page file-path)) (let [processed-text (md->html - (include/expand-include-md - (:includer md-include-system) - (slurp file-path)))] + (assoc request :source + (include/expand-include-md + (:includer md-include-system) + (slurp file-path))))] (layout/render "wiki.html" (merge (util/standard-params request) processed-text diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index e92bafe..653e056 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -4,13 +4,13 @@ (:require [clojure.java.io :as cjio] [clojure.string :as cs] [environ.core :refer [env]] + [markdown.core :as md] [me.raynes.fs :as fs] [noir.io :as io] [noir.session :as session] [scot.weft.i18n.core :as i18n] [smeagol.authenticate :as auth] [smeagol.configuration :refer [config]] - [smeagol.formatting :refer [md->html]] [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -124,8 +124,8 @@ "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") +;; (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." @@ -134,8 +134,10 @@ {:user user :admin (auth/get-admin user) :js-from (:js-from config) - :side-bar (:content (md->html (slurp (cjio/file content-dir "_side-bar.md")))) - :header (:content (md->html (slurp (cjio/file content-dir "_header.md")))) + :side-bar (md/md-to-html-string + (slurp (cjio/file content-dir "_side-bar.md")):heading-anchors true) + :header (md/md-to-html-string + (slurp (cjio/file content-dir "_header.md")) :heading-anchors true) :version (System/getProperty "smeagol.version")})) diff --git a/test/smeagol/test/formatting.clj b/test/smeagol/test/formatting.clj index 9cb4956..5172403 100644 --- a/test/smeagol/test/formatting.clj +++ b/test/smeagol/test/formatting.clj @@ -12,9 +12,9 @@ "[This is a foreign link](http://to.somewhere)")] (is (= (local-links text) text) "Foreign links should be unchanged")))) -(deftest test-process-text - (testing "The process-text flow" - (let [expected process-test-return-value - actual (process-text "```test - ```")] - (is (= actual expected))))) +;; (deftest test-process-text +;; (testing "The process-text flow" +;; (let [expected process-test-return-value +;; actual (process-text "```test +;; ```")] +;; (is (= actual expected))))) From 03c63da19edff1ea66f78a8e2244dc46e7ffe95a Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 19 Feb 2020 15:24:08 +0000 Subject: [PATCH 34/37] Tactical commit --- resources/public/content/stylesheet.css | 101 ++++++++---------------- src/smeagol/formatting.clj | 37 +-------- src/smeagol/local_links.clj | 50 ++++++++++++ src/smeagol/util.clj | 9 ++- test/smeagol/test/formatting.clj | 38 +++++---- test/smeagol/test/local_links.clj | 14 ++++ 6 files changed, 127 insertions(+), 122 deletions(-) create mode 100644 src/smeagol/local_links.clj create mode 100644 test/smeagol/test/local_links.clj diff --git a/resources/public/content/stylesheet.css b/resources/public/content/stylesheet.css index 62690e0..81a661c 100644 --- a/resources/public/content/stylesheet.css +++ b/resources/public/content/stylesheet.css @@ -24,16 +24,6 @@ ## html elements generally in alphabetic order */ -a { - color: darkgray; - font-weight: bold; -} - -a:hover { - color: darkgray; - background:rgba(200,200,200,0.8); -} - body { margin: 0; padding: 0; @@ -62,15 +52,14 @@ dd { /* footer of the page - not-editable, provided by Smeagol */ footer { - border-top: thin solid silver; - color: gray; - background:rgba(200,200,200,0.8); + border-top: thin solid gray; clear: both; font-size: smaller; text-align: center; + color: gray; + background: rgba(224,224,224,0.95); width: 100%; margin: 0; - min-height: 4px; padding: 0.25em 0; bottom:0; position:fixed; @@ -82,14 +71,9 @@ footer { } footer div { - display: none; padding: 0.1em; } -footer:hover div { - display: block; -} - form { border: thin solid silver; } @@ -99,6 +83,8 @@ header { margin-top: 0; width:100%; max-width: 100%; + background-color: gray; + color: white; } header h1 { @@ -107,6 +93,7 @@ header h1 { header a { font-weight: bold; + color: white; } header a:hover { @@ -144,12 +131,12 @@ ins { label { width: 20%; min-width: 20em; - border-right: thin solid silver; + border-right: thin solid gray; display: inline-block; } table { - border: thin solid silver; + border: 2px solid black; border-collapse: collapse; } @@ -161,7 +148,7 @@ th, td { text-align: left; vertical-align: top; padding: 0.15em 1.5em; - border: 1px solid silver; + border: 1px solid gray; } th { @@ -179,7 +166,6 @@ th { /* left bar for all pages in the Wiki - editable, provided by users. Within main-container */ #side-bar { - display: none; width: 17%; height: 100%; float: left; @@ -187,10 +173,10 @@ th { /* cookies information box, fixed, in right margin, just above footer */ #cookies { - width: 20%; + width: 30%; float: right; position: fixed; - bottom: 8px; + bottom: 3.5em; right: 0; z-index: 175; background: transparent; @@ -204,8 +190,8 @@ th { text-align: right; padding: 0.25em 2em; border-radius: 0.25em; - color: gray; - background:rgba(200,200,200,0.8); + color: white; + background:rgba(40,40,40,0.8); } /* more-about-cookies box, normally hidden */ @@ -213,9 +199,9 @@ th { display: none; padding: 0.5em 2em; border-radius: 0.5em; - color: gray; - background:rgba(200,200,200,0.8); - border-bottom: thin solid gray; + color: white; + background:rgba(40,40,40,0.8); + border-bottom: thin solid white; } /* but magically appears on mouseover */ @@ -256,8 +242,8 @@ th { right: 0; padding: 0.25em 2em; border-radius: 0.25em; - color: gray; - background:rgba(200,200,200,0.8); + color: white; + background:rgba(40,40,40,0.8); font-size: 66%; } @@ -268,11 +254,7 @@ th { .minor-controls a { float: right; padding: 0.25em 2em; - color: gray; -} - -.minor-controls a:hover { - color: darkgray; + color: white; } .pseudo-input { @@ -321,7 +303,8 @@ th { /* content of the current page in the Wiki - editable, provided by users. Within main-container */ #content { border: thin solid silver; - width: 100%; + width: 80%; + float: right; padding-bottom: 5em; } @@ -329,29 +312,16 @@ th { display: none; } - #header { - font-size: smaller; - } - /* top-of-page navigation, not editable, provided by Smeagol */ #nav{ margin: 0; padding: 0; top: 0; - min-height: 4px; + width: 100%; _position: absolute; _top: expression(document.documentElement.scrollTop); z-index: 149; - color: gray; - background:rgba(200,200,200,0.8); - } - - #nav #nav-menu { - display: none; - } - - #nav:hover #nav-menu { - display: block; + background:rgba(40,40,40,0.8); } /* only needed for fly-out menu effect on tablet and phone stylesheets */ @@ -371,14 +341,14 @@ th { } #nav menu li a { - color: gray; + color: white; text-decoration: none; font-weight: bold; padding: 0.1em 0.75em; margin: 0; } - #nav menu li.active a { background: gray; color: white;} + #nav menu li.active a { background: gray;} li.nav-item a:hover { background: rgb( 240, 240, 240) } li.nav-item a:active { background: gray; color: white; } @@ -409,15 +379,17 @@ th { padding: 0; position: fixed; z-index: 149; - color: black; - background:rgba(200,200,200,0.9); + color: silver; + background:rgba(40,40,40,0.9); } #nav a { + color: white; + text-decoration: none; font-weight: bold; } - #nav:hover #nav-menu, #nav:hover #phone-side-bar { + #nav:hover #nav-menu { display: block; list-style-type: none; width: 100%; @@ -483,21 +455,18 @@ th { display: none; } - #header { - display: none; - } - #nav{ margin: 0; padding: 0; position: fixed; z-index: 149; - color: black; - background:rgba(200,200,200,0.9); + color: silver; + background:rgba(40,40,40,0.9); } #nav a { - color: black; + color: white; + text-decoration: none; font-weight: bold; } @@ -522,8 +491,6 @@ th { } #nav menu li a { - color: black; - font-weight: bold; } #nav ul li.active a { background: silver;} diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index cacc9ca..3807cb0 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -10,6 +10,7 @@ [smeagol.extensions.mermaid :refer [process-mermaid]] [smeagol.extensions.photoswipe :refer [process-photoswipe]] [smeagol.extensions.vega :refer [process-vega]] + [smeagol.local-links :refer :all] [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -44,26 +45,6 @@ ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Error to show if text to be rendered is nil. -;; TODO: this should go through i18n -(def no-text-error "No text: does the file exist?") - - -(defn local-links - "Rewrite text in `html-src` surrounded by double square brackets as a local link into this wiki." - [^String html-src] - (if html-src - (cs/replace html-src #"\[\[[^\[\]]*\]\]" - #(let [text (cs/replace %1 #"[\[\]]" "") - encoded (url-encode text) - ;; I use '\_' to represent '_' in wiki markup, because - ;; '_' is meaningful in Markdown. However, this needs to - ;; be stripped out when interpreting local links. - munged (cs/replace encoded #"%26%2395%3B" "_")] - (format "%s" munged text))) - no-text-error)) - - (declare process-text) @@ -133,26 +114,10 @@ result {:inclusions {inky (eval (list formatter (subs fragment (count token)) index))} :extensions {fkey (-> config :formatters fkey)}}) -;; (assoc-in -;; (assoc-in result [:inclusions inky] (eval (list formatter (subs fragment (count token)) index))) -;; [:extensions fkey] (-> config :formatters fkey)) (rest fragments) (cons inky 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) - (defn reassemble-text "Reassemble these processed strings into a complete text, and process it as Markdown." diff --git a/src/smeagol/local_links.clj b/src/smeagol/local_links.clj new file mode 100644 index 0000000..f1bed0b --- /dev/null +++ b/src/smeagol/local_links.clj @@ -0,0 +1,50 @@ +(ns ^{:doc "Format Semagol's local links." + :author "Simon Brooke"} + smeagol.local-links + (:require [clojure.data.json :as json] + [clojure.string :as cs] + [cemerick.url :refer (url url-encode url-decode)])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; Smeagol: a very simple Wiki engine. +;;;; +;;;; 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 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2017 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Error to show if text to be rendered is nil. +;; TODO: this should go through i18n +(def no-text-error "No text: does the file exist?") + + +(defn local-links + "Rewrite text in `html-src` surrounded by double square brackets as a local link into this wiki." + [^String html-src] + (if html-src + (cs/replace html-src #"\[\[[^\[\]]*\]\]" + #(let [text (cs/replace %1 #"[\[\]]" "") + encoded (url-encode text) + ;; I use '\_' to represent '_' in wiki markup, because + ;; '_' is meaningful in Markdown. However, this needs to + ;; be stripped out when interpreting local links. + munged (cs/replace encoded #"%26%2395%3B" "_")] + (format "%s" munged text))) + no-text-error)) + + diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj index 653e056..86fdfac 100644 --- a/src/smeagol/util.clj +++ b/src/smeagol/util.clj @@ -11,6 +11,7 @@ [scot.weft.i18n.core :as i18n] [smeagol.authenticate :as auth] [smeagol.configuration :refer [config]] + [smeagol.local-links :refer :all] [taoensso.timbre :as log])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -135,9 +136,13 @@ :admin (auth/get-admin user) :js-from (:js-from config) :side-bar (md/md-to-html-string - (slurp (cjio/file content-dir "_side-bar.md")):heading-anchors true) + (local-links + (slurp (cjio/file content-dir "_side-bar.md"))) + :heading-anchors true) :header (md/md-to-html-string - (slurp (cjio/file content-dir "_header.md")) :heading-anchors true) + (local-links + (slurp (cjio/file content-dir "_header.md"))) + :heading-anchors true) :version (System/getProperty "smeagol.version")})) diff --git a/test/smeagol/test/formatting.clj b/test/smeagol/test/formatting.clj index 5172403..52e9817 100644 --- a/test/smeagol/test/formatting.clj +++ b/test/smeagol/test/formatting.clj @@ -1,20 +1,24 @@ (ns smeagol.test.formatting (:require [clojure.test :refer :all] - [smeagol.formatting :refer [local-links no-text-error]] - [smeagol.extensions.test :refer :all])) + [smeagol.formatting :refer :all] + [smeagol.extensions.test :refer :all] + [smeagol.local-links :refer :all])) -(deftest test-local-links - (testing "Rewriting of local links" - (is (= (local-links nil) no-text-error) "Should NOT fail with a no pointer exception!") - (is (= (local-links "") "") "Empty string should pass through unchanged.") - (is (= (local-links "[[froboz]]") "froboz") "Local link should be rewritten.") - (let [text (str "# This is a heading" - "[This is a foreign link](http://to.somewhere)")] - (is (= (local-links text) text) "Foreign links should be unchanged")))) - -;; (deftest test-process-text -;; (testing "The process-text flow" -;; (let [expected process-test-return-value -;; actual (process-text "```test -;; ```")] -;; (is (= actual expected))))) +(deftest test-apply-formatter + (testing "apply-formatter" + (let [actual (-> (apply-formatter + 3 + {:inclusions {}} + '() + '() + "test + ![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)" + "test" + smeagol.extensions.test/process-test) + :inclusions + :inclusion-3) + expected ""] + (is (= actual expected))))) diff --git a/test/smeagol/test/local_links.clj b/test/smeagol/test/local_links.clj new file mode 100644 index 0000000..54d31e9 --- /dev/null +++ b/test/smeagol/test/local_links.clj @@ -0,0 +1,14 @@ +(ns smeagol.test.local-links + (:require [clojure.test :refer :all] + [smeagol.local-links :refer [local-links no-text-error]] + [smeagol.extensions.test :refer :all] + [smeagol.local-links :refer :all])) + +(deftest test-local-links + (testing "Rewriting of local links" + (is (= (local-links nil) no-text-error) "Should NOT fail with a no pointer exception!") + (is (= (local-links "") "") "Empty string should pass through unchanged.") + (is (= (local-links "[[froboz]]") "froboz") "Local link should be rewritten.") + (let [text (str "# This is a heading" + "[This is a foreign link](http://to.somewhere)")] + (is (= (local-links text) text) "Foreign links should be unchanged")))) From f82ad725c151feab74e1d1ae0e5b49bebe5d2bb6 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 19 Feb 2020 16:18:58 +0000 Subject: [PATCH 35/37] Working,, but regression in Mermaid stylesheet --- resources/test/test_extension.md | 7 +++++++ resources/test/test_local_links.md | 6 ++++++ src/smeagol/formatting.clj | 1 - test/smeagol/test/formatting.clj | 19 +++++++++++++++++++ test/smeagol/test/local_links.clj | 9 +++++++-- 5 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 resources/test/test_extension.md create mode 100644 resources/test/test_local_links.md diff --git a/resources/test/test_extension.md b/resources/test/test_extension.md new file mode 100644 index 0000000..1f43160 --- /dev/null +++ b/resources/test/test_extension.md @@ -0,0 +1,7 @@ +# This is a test + +```test +the quick brown fox jumped over the lazy dog +``` + +This concludes the test. diff --git a/resources/test/test_local_links.md b/resources/test/test_local_links.md new file mode 100644 index 0000000..659f03a --- /dev/null +++ b/resources/test/test_local_links.md @@ -0,0 +1,6 @@ +# This is a test + +[[Local link]] +[Not a local link](http://nowhere.at.al) + +This concludes the test. diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj index 3807cb0..3617514 100644 --- a/src/smeagol/formatting.clj +++ b/src/smeagol/formatting.clj @@ -104,7 +104,6 @@ ^String fragment ^String token formatter] - (log/info "index:" index "(type result):" (type result) "(type fragments):" (type fragments) "fragment:" fragment "token:" token ":formatter" formatter) (let [inky (keyword (str "inclusion-" index)) fkey (keyword token)] diff --git a/test/smeagol/test/formatting.clj b/test/smeagol/test/formatting.clj index 52e9817..18769cd 100644 --- a/test/smeagol/test/formatting.clj +++ b/test/smeagol/test/formatting.clj @@ -1,5 +1,6 @@ (ns smeagol.test.formatting (:require [clojure.test :refer :all] + [clojure.string :as cs] [smeagol.formatting :refer :all] [smeagol.extensions.test :refer :all] [smeagol.local-links :refer :all])) @@ -22,3 +23,21 @@ :inclusion-3) expected ""] (is (= actual expected))))) + +(deftest test-md->html + (let [actual (:content (md->html + {:source + (cs/join + "\n" + ["# This is a test" + "" + "```test" + "![Frost on a gate, Laurieston](content/uploads/g1.jpg)" + "```" + "" + "This concludes the test"])} )) + expected (str + "

This is a test

" + "

" + "

This concludes the test

")] + (is (= expected actual)))) diff --git a/test/smeagol/test/local_links.clj b/test/smeagol/test/local_links.clj index 54d31e9..dc3682d 100644 --- a/test/smeagol/test/local_links.clj +++ b/test/smeagol/test/local_links.clj @@ -1,5 +1,6 @@ (ns smeagol.test.local-links (:require [clojure.test :refer :all] + [clojure.string :as cs] [smeagol.local-links :refer [local-links no-text-error]] [smeagol.extensions.test :refer :all] [smeagol.local-links :refer :all])) @@ -10,5 +11,9 @@ (is (= (local-links "") "") "Empty string should pass through unchanged.") (is (= (local-links "[[froboz]]") "froboz") "Local link should be rewritten.") (let [text (str "# This is a heading" - "[This is a foreign link](http://to.somewhere)")] - (is (= (local-links text) text) "Foreign links should be unchanged")))) + "[This is a foreign link](http://to.somewhere)")] + (is (= (local-links text) text) "Foreign links should be unchanged")) + (let [text (cs/trim (slurp "resources/test/test_local_links.md")) + actual (local-links text) + expected "# This is a test\n\nLocal link\n[Not a local link](http://nowhere.at.al)\n\nThis concludes the test."] + (is (= actual expected))))) From 019f2e8276c37af3d5f6e9295ab272e4c9995d2b Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 19 Feb 2020 16:42:58 +0000 Subject: [PATCH 36/37] Regression fixed. --- resources/config.edn | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/config.edn b/resources/config.edn index ecb1ea0..96ab5ca 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -38,9 +38,8 @@ :scripts {} :styles {}} :mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid" - :scripts {:core {:local "vendor/mermaid/dist/mermaid.js" - :remote "https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.4.6/mermaid.min.js"}} - :styles {}} + :scripts {:core {:local "vendor/node_modules/mermaid/dist/mermaid.min.js" + :remote "https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.4.6/mermaid.min.js"}}} :pswp {:formatter "smeagol.extensions.photoswipe/process-photoswipe" :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"} From be3c9fc94697bc43306d3998888dd975364d5ff8 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 19 Feb 2020 16:43:43 +0000 Subject: [PATCH 37/37] lein-release plugin: preparing 1.0.4 release --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 3898e1c..f3d5602 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject smeagol "1.0.4-SNAPSHOT" +(defproject smeagol "1.0.4" :description "A simple Git-backed Wiki inspired by Gollum" :url "https://github.com/simon-brooke/smeagol" :license {:name "GNU General Public License,version 2.0 or (at your option) any later version"