diff --git a/project.clj b/project.clj
index b270587..b753cd1 100644
--- a/project.clj
+++ b/project.clj
@@ -16,6 +16,8 @@
[environ "1.1.0"]
[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"]
@@ -52,6 +54,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 5ceccd7..059dfeb 100644
--- a/resources/config.edn
+++ b/resources/config.edn
@@ -28,18 +28,27 @@
;; ; ; ; ; ; ; ; ; ;
{
: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.formatting/process-mermaid
- "backticks" smeagol.formatting/process-backticks}
+ "mermaid" smeagol.extensions.mermaid/process-mermaid
+ "backticks" smeagol.formatting/process-backticks
+ "pswp" smeagol.extensions.photoswipe/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
+ :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
+ }}
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/Example gallery.md b/resources/public/content/Example gallery.md
new file mode 100644
index 0000000..a2fcd00
--- /dev/null
+++ b/resources/public/content/Example gallery.md
@@ -0,0 +1,60 @@
+## 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.
+
+## 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
+}
+
+```
+
diff --git a/resources/public/content/Extensible Markup.md b/resources/public/content/Extensible Markup.md
index 129a375..41c9873 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,19 @@ 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
+```
+
+## 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](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
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/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/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/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/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/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 %} {% else %} link {% endif %}
+ {% if entry.is-image %} {% else %} link {% endif %}
|
diff --git a/resources/templates/upload.html b/resources/templates/upload.html
index 7c0c373..3e63f17 100644
--- a/resources/templates/upload.html
+++ b/resources/templates/upload.html
@@ -1,22 +1,27 @@
{% extends "templates/base.html" %}
{% block content %}
- {% if uploaded %}
- {% if is-image %}
-
-
+ {% if uploaded|not-empty %}
+ {% for upload in uploaded %}
+ {% if upload.is-image %}
+
+
- {% i18n file-upload-link-text %}:
+
+ This is the {{upload.size|name}} file. {% 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 %}
"))
+
+
+(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))
+ (do
+ (log/warn "Failed to fetch dimensions of image " url)
+ 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
new file mode 100644
index 0000000..c73d086
--- /dev/null
+++ b/src/smeagol/extensions/utils.clj
@@ -0,0 +1,85 @@
+(ns ^{:doc "Utility functions useful to extension processors."
+ :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]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; 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
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(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
+ 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."
+ (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/extensions/vega.clj b/src/smeagol/extensions/vega.clj
new file mode 100644
index 0000000..1b9e2de
--- /dev/null
+++ b/src/smeagol/extensions/vega.clj
@@ -0,0 +1,91 @@
+(ns ^{:doc "Format Semagol's extended markdown format."
+ :author "Simon Brooke"}
+ smeagol.extensions.vega
+ (:require [clojure.data.json :as json]
+ [clj-yaml.core :as yaml]
+ [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 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
+ 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 src-resource-or-url)
+ vega-src (:data data)]
+ (log/info "Retrieved vega-src from " (:from data) " `" ((:from data) data) "`")
+ (str
+ "\n"
+ "")))
diff --git a/src/smeagol/formatting.clj b/src/smeagol/formatting.clj
index 8fe3697..fa4f2f6 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,9 @@
[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]]
+ [smeagol.extensions.photoswipe :refer [process-photoswipe]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@@ -85,14 +87,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
"Effectively, escape the backticks surrounding this `text`, by protecting them
from the `md->html` filter."
diff --git a/src/smeagol/routes/wiki.clj b/src/smeagol/routes/wiki.clj
index a8e13ce..e33cbdc 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)})))
+;; TODO: Get this working! it MUST work!
+;; (if-not
+;; (empty? uploaded)
+;; (do
+;; (map
+;; #(git/git-add git-repo (str :resource %))
+;; (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)
{: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))))}))))
+ :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 7cadaea..34f43c3 100644
--- a/src/smeagol/uploads.clj
+++ b/src/smeagol/uploads.clj
@@ -1,10 +1,20 @@
(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]
+ [noir.io :as nio]
+ [smeagol.configuration :refer [config]]
+ [smeagol.util :as util]
+ [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 +39,65 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; 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}}]
+ (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
+ 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.")))
+ ([^String path ^String filename size ^RenderedImage image]
+ (let [s (-> config :thumbnails size)
+ d (dimensions image)
+ p (io/file path (name size) filename)]
+ (if (and (integer? s) (some #(> % s) d))
+ (do
+ (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
"Store an upload both to the file system and to the database.
@@ -56,17 +110,25 @@
(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)))
+ (let [p (io/file path filename)]
+ (.renameTo tmp-file p)
+ (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
- (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?")))))
diff --git a/src/smeagol/util.clj b/src/smeagol/util.clj
index c3ce6d4..1ef44df 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,14 @@
(: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")))))
+(def upload-dir
+ (str (cjio/file content-dir "uploads")))
(defn standard-params
"Return a map of standard parameters to pass to the template renderer."