mirror of
https://github.com/journeyman-cc/smeagol.git
synced 2026-04-12 18:05:06 +00:00
Merge tag 'smeagol-1.0.4'
This commit is contained in:
commit
8618926318
44 changed files with 2954 additions and 331 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -18,3 +18,5 @@ smeagol.log*
|
||||||
/node_modules/
|
/node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|
||||||
|
resources/public/content/uploads/
|
||||||
|
|
|
||||||
1339
package-lock.json
generated
Normal file
1339
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
project.clj
22
project.clj
|
|
@ -1,10 +1,11 @@
|
||||||
(defproject smeagol "1.0.3"
|
(defproject smeagol "1.0.4"
|
||||||
:description "A simple Git-backed Wiki inspired by Gollum"
|
:description "A simple Git-backed Wiki inspired by Gollum"
|
||||||
:url "https://github.com/simon-brooke/smeagol"
|
:url "https://github.com/simon-brooke/smeagol"
|
||||||
:license {:name "GNU General Public License,version 2.0 or (at your option) any later version"
|
:license {:name "GNU General Public License,version 2.0 or (at your option) any later version"
|
||||||
:url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"}
|
:url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"}
|
||||||
:dependencies [[clj-jgit "0.8.10"]
|
:dependencies [[clj-jgit "0.8.10"]
|
||||||
[clj-yaml "0.4.0"]
|
[clj-yaml "0.4.0"]
|
||||||
|
[clojure.java-time "0.3.2"]
|
||||||
[com.cemerick/url "0.1.1"]
|
[com.cemerick/url "0.1.1"]
|
||||||
[com.fzakaria/slf4j-timbre "0.3.7"]
|
[com.fzakaria/slf4j-timbre "0.3.7"]
|
||||||
[com.stuartsierra/component "0.4.0"]
|
[com.stuartsierra/component "0.4.0"]
|
||||||
|
|
@ -15,14 +16,18 @@
|
||||||
[environ "1.1.0"]
|
[environ "1.1.0"]
|
||||||
[hiccup "1.0.5"]
|
[hiccup "1.0.5"]
|
||||||
[im.chit/cronj "1.4.4"]
|
[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]]
|
[lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]]
|
||||||
[markdown-clj "0.9.99" :exclusions [com.keminglabs/cljx]]
|
[markdown-clj "0.9.99" :exclusions [com.keminglabs/cljx]]
|
||||||
|
[me.raynes/fs "1.4.6"]
|
||||||
[noir-exception "0.2.5"]
|
[noir-exception "0.2.5"]
|
||||||
[org.clojars.simon_brooke/internationalisation "1.0.3"]
|
[org.clojars.simon_brooke/internationalisation "1.0.3"]
|
||||||
[org.clojure/clojure "1.8.0"]
|
[org.clojure/clojure "1.8.0"]
|
||||||
[org.clojure/core.memoize "0.5.9"]
|
[org.clojure/core.memoize "0.5.9"]
|
||||||
[org.clojure/data.json "0.2.6"]
|
[org.clojure/data.json "0.2.6"]
|
||||||
[org.clojure/tools.logging "0.4.0"]
|
[org.clojure/tools.logging "0.4.0"]
|
||||||
|
[org.clojure/tools.trace "0.7.10"]
|
||||||
[org.slf4j/slf4j-api "1.7.25"]
|
[org.slf4j/slf4j-api "1.7.25"]
|
||||||
[org.slf4j/log4j-over-slf4j "1.7.25"]
|
[org.slf4j/log4j-over-slf4j "1.7.25"]
|
||||||
[org.slf4j/jul-to-slf4j "1.7.25"]
|
[org.slf4j/jul-to-slf4j "1.7.25"]
|
||||||
|
|
@ -38,18 +43,21 @@
|
||||||
:jvm-opts ["-server"]
|
:jvm-opts ["-server"]
|
||||||
|
|
||||||
:plugins [[lein-ancient "0.5.5" :exclusions [org.clojure/clojure org.clojure/data.xml]]
|
:plugins [[lein-ancient "0.5.5" :exclusions [org.clojure/clojure org.clojure/data.xml]]
|
||||||
[lein-bower "0.5.1"]
|
|
||||||
[lein-codox "0.10.3"]
|
[lein-codox "0.10.3"]
|
||||||
[io.sarnowski/lein-docker "1.0.0"]
|
[io.sarnowski/lein-docker "1.0.0"]
|
||||||
[lein-environ "1.0.0"]
|
[lein-environ "1.0.0"]
|
||||||
[lein-marginalia "0.7.1" :exclusions [org.clojure/clojure]]
|
[lein-marginalia "0.7.1" :exclusions [org.clojure/clojure]]
|
||||||
|
[lein-npm "0.6.2"]
|
||||||
[lein-ring "0.12.5" :exclusions [org.clojure/clojure]]]
|
[lein-ring "0.12.5" :exclusions [org.clojure/clojure]]]
|
||||||
|
|
||||||
:bower-dependencies [[simplemde "1.11.2"]
|
:npm {:dependencies [[simplemde "1.11.2"]
|
||||||
;; [vega-embed "3.0.0-beta.20"] ;; vega-embed currently not loaded from Bower because of
|
[vega "5.8.0"]
|
||||||
;; dependency conflict which will hopefully be resolved soon.
|
[vega-embed "6.2.2"]
|
||||||
[vega-lite "2.0.0-beta.11"]
|
[vega-lite "4.1.1"]
|
||||||
[mermaid "6.0.0"]]
|
[mermaid "8.4.6"]
|
||||||
|
[photoswipe "4.1.3"]
|
||||||
|
[tablesort "5.2.0"]]
|
||||||
|
:root "resources/public/vendor"}
|
||||||
|
|
||||||
:docker {:image-name "simonbrooke/smeagol"
|
:docker {:image-name "simonbrooke/smeagol"
|
||||||
:dockerfile "Dockerfile"}
|
:dockerfile "Dockerfile"}
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,50 @@
|
||||||
;; ; ; ; ; ; ; ; ; ;
|
;; ; ; ; ; ; ; ; ; ;
|
||||||
{
|
{
|
||||||
:content-dir "resources/public/content"
|
:content-dir "resources/public/content"
|
||||||
:start-page "Introduction"
|
|
||||||
;; where content is served from.
|
;; where content is served from.
|
||||||
:default-locale "en-GB" ;; default language used for messages
|
:default-locale "en-GB" ;; default language used for messages
|
||||||
:formatters {"vega" smeagol.formatting/process-vega
|
:extensions-from :local ;; where to load JavaScript libraries
|
||||||
"vis" smeagol.formatting/process-vega
|
;; from: options are :local, :remote.
|
||||||
"mermaid" smeagol.formatting/process-mermaid
|
:formatters ;; formatters for processing markdown
|
||||||
"backticks" smeagol.formatting/process-backticks}
|
;; extensions.
|
||||||
|
{:backticks {:formatter "smeagol.formatting/process-backticks"
|
||||||
|
:scripts {}
|
||||||
|
:styles {}}
|
||||||
|
:mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid"
|
||||||
|
: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"}
|
||||||
|
: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"}
|
||||||
|
: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"}}}
|
||||||
|
: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"}}}}
|
||||||
:log-level :info ;; the minimum logging level; one of
|
:log-level :info ;; the minimum logging level; one of
|
||||||
;; :trace :debug :info :warn :error :fatal
|
;; :trace :debug :info :warn :error :fatal
|
||||||
:passwd "resources/passwd"
|
:passwd "resources/passwd"
|
||||||
;; where the password file is stored
|
;; 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
|
;; 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
|
||||||
|
;; 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@
|
||||||
:file-upload-title "Upload a file" ;; title for the file upload page
|
:file-upload-title "Upload a file" ;; title for the file upload page
|
||||||
:is-admin-prompt "Is administrator?"
|
:is-admin-prompt "Is administrator?"
|
||||||
:here "here" ;; used in sanity check report
|
: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
|
:home-link "Home" ;; text of the home link on the menu
|
||||||
:is-not-directory "is not a directory"
|
:is-not-directory "is not a directory"
|
||||||
;; (of a file or directory) used in sanity check report
|
;; (of a file or directory) used in sanity check report
|
||||||
|
|
@ -90,6 +92,8 @@
|
||||||
;; (of a file or directory) used in sanity check report
|
;; (of a file or directory) used in sanity check report
|
||||||
:is-not-writable "is not writable"
|
:is-not-writable "is not writable"
|
||||||
;; (of a file or directory) used in sanity check report
|
;; (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-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-link "Log in" ;; text of the login link on the menu
|
||||||
:login-prompt "To edit this wiki"
|
:login-prompt "To edit this wiki"
|
||||||
|
|
@ -98,8 +102,7 @@
|
||||||
:logout-link "Log out" ;; text of the logout link on the menu
|
:logout-link "Log out" ;; text of the logout link on the menu
|
||||||
:logged-in-as "You are logged in as"
|
:logged-in-as "You are logged in as"
|
||||||
;; text of the 'logged in as' label on the menu
|
;; text of the 'logged in as' label on the menu
|
||||||
:history-link "History" ;; text of the history link on the content frame
|
:matching "matching" ;; 'matching' in e.g. 'list files matching fred'
|
||||||
:history-title-prefix "History of" ;; prefix of the title on the history page
|
|
||||||
:new-pass-prompt "New password" ;; text of the new password widget prompt on the change
|
:new-pass-prompt "New password" ;; text of the new password widget prompt on the change
|
||||||
;; password and edit user pages
|
;; password and edit user pages
|
||||||
:no-admin-users "There are no users in the 'passwd' file with administrative privileges"
|
:no-admin-users "There are no users in the 'passwd' file with administrative privileges"
|
||||||
|
|
@ -128,6 +131,8 @@
|
||||||
"Smeagol has been unable to find some of the resources on which it depends,
|
"Smeagol has been unable to find some of the resources on which it depends,
|
||||||
possibly because of misconfiguration or missing environment variables."
|
possibly because of misconfiguration or missing environment variables."
|
||||||
;; used in sanity check report
|
;; 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"
|
:user-lacks-field "User record in the passwd file lacks a field"
|
||||||
;; used in sanity check report
|
;; used in sanity check report
|
||||||
:username-prompt "Username" ;; text of the username widget prompt on edit user page
|
:username-prompt "Username" ;; text of the username widget prompt on edit user page
|
||||||
|
|
|
||||||
|
|
@ -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]].
|
|
||||||
102
resources/public/content/Configuring Smeagol.md
Normal file
102
resources/public/content/Configuring Smeagol.md
Normal file
|
|
@ -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!
|
||||||
|
|
@ -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/.
|
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:
|
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:
|
||||||
```
|
```
|
||||||
|
|
|
||||||
60
resources/public/content/Example gallery.md
Normal file
60
resources/public/content/Example gallery.md
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -4,7 +4,7 @@ A system of pluggable, extensible formatters is supported. In normal markdown, c
|
||||||
|
|
||||||
## The Vega formatter
|
## 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):
|
Here's an example cribbed in its entirety from [here](http://visdown.amitkaps.com/london):
|
||||||
|
|
||||||
|
|
@ -34,9 +34,9 @@ Data files can be uploaded in the same way as images, by using the **upload a fi
|
||||||
|
|
||||||
## The Mermaid formatter
|
## 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.
|
Here's an example culled from the Mermaid documentation. Edit this page to see the specification.
|
||||||
|
|
||||||
### GANTT Chart
|
### GANTT Chart
|
||||||
|
|
||||||
|
|
@ -58,6 +58,19 @@ gantt
|
||||||
Add to mermaid :1d
|
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
|
## 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.
|
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.
|
||||||
|
|
|
||||||
24
resources/public/content/Simplified example gallery.md
Normal file
24
resources/public/content/Simplified example gallery.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
## How this works
|
||||||
|
|
||||||
|
The specification for this gallery is as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
## If you're using a small device
|
## 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:
|
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:
|
||||||

|
|
||||||
|

|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
@ -66,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.
|
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.
|
You must be logged in to upload files.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,5 @@
|
||||||
+ \__italic_\_
|
+ \__italic_\_
|
||||||
|
|
||||||
More documentation [here](http://daringfireball.net/projects/markdown/syntax)
|
More documentation [here](http://daringfireball.net/projects/markdown/syntax)
|
||||||
|
|
||||||
|
Your <a href="list-uploads">uploaded files are listed here</a>.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
* [[Introduction]]
|
* [[Introduction]]
|
||||||
* [[Change log]]
|
* [[Change log]]
|
||||||
* [[User Documentation]]
|
* [[User Documentation]]
|
||||||
|
* [[Configuring Smeagol]]
|
||||||
* [[Deploying Smeagol]]
|
* [[Deploying Smeagol]]
|
||||||
* [[Developing Smeagol]]
|
* [[Developing Smeagol]]
|
||||||
|
|
||||||
|
|
|
||||||
14
resources/public/data/classes.mermaid
Normal file
14
resources/public/data/classes.mermaid
Normal file
|
|
@ -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
|
||||||
65
resources/public/html-includes/photoswipe-boilerplate.html
Normal file
65
resources/public/html-includes/photoswipe-boilerplate.html
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<!-- this is the whole of the photoswipe boilerplate as documented
|
||||||
|
[here](https://photoswipe.com/documentation/getting-started.html) with the
|
||||||
|
outermost div removed; it is supplied by the function
|
||||||
|
`smeagol.extensions.photoswipe/photoswipe-processor`, q.v. -->
|
||||||
|
<!-- Background of PhotoSwipe.
|
||||||
|
It's a separate element as animating opacity is faster than rgba(). -->
|
||||||
|
<div class="pswp__bg"></div>
|
||||||
|
|
||||||
|
<!-- Slides wrapper with overflow:hidden. -->
|
||||||
|
<div class="pswp__scroll-wrap">
|
||||||
|
|
||||||
|
<!-- Container that holds slides.
|
||||||
|
PhotoSwipe keeps only 3 of them in the DOM to save memory.
|
||||||
|
Don't modify these 3 pswp__item elements, data is added later on. -->
|
||||||
|
<div class="pswp__container">
|
||||||
|
<div class="pswp__item"></div>
|
||||||
|
<div class="pswp__item"></div>
|
||||||
|
<div class="pswp__item"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
|
||||||
|
<div class="pswp__ui pswp__ui--hidden">
|
||||||
|
|
||||||
|
<div class="pswp__top-bar">
|
||||||
|
|
||||||
|
<!-- Controls are self-explanatory. Order can be changed. -->
|
||||||
|
|
||||||
|
<div class="pswp__counter"></div>
|
||||||
|
|
||||||
|
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
|
||||||
|
|
||||||
|
<button class="pswp__button pswp__button--share" title="Share"></button>
|
||||||
|
|
||||||
|
<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
|
||||||
|
|
||||||
|
<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
|
||||||
|
|
||||||
|
<!-- Preloader demo https://codepen.io/dimsemenov/pen/yyBWoR -->
|
||||||
|
<!-- element will get class pswp__preloader--active when preloader is running -->
|
||||||
|
<div class="pswp__preloader">
|
||||||
|
<div class="pswp__preloader__icn">
|
||||||
|
<div class="pswp__preloader__cut">
|
||||||
|
<div class="pswp__preloader__donut"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
||||||
|
<div class="pswp__share-tooltip"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="pswp__caption">
|
||||||
|
<div class="pswp__caption__center"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
1
resources/public/vendor/README.md
vendored
1
resources/public/vendor/README.md
vendored
|
|
@ -1 +0,0 @@
|
||||||
This folder must exist in order that the Bower package manager can deploy JavaScript packages to it.
|
|
||||||
|
|
@ -46,7 +46,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1>{% i18n site-title %}: {{title}}</h1>
|
<h1>{% i18n site-title %}: {{title}}</h1>
|
||||||
{{header|safe}}
|
<div id="header">
|
||||||
|
{{header|safe}}
|
||||||
|
</div>
|
||||||
{% if message %}
|
{% if message %}
|
||||||
<div id="message">
|
<div id="message">
|
||||||
<p class="message">{{message}}</p>
|
<p class="message">{{message}}</p>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
|
{% block extra-headers %}
|
||||||
|
{% script "/vendor/node_modules/tablesort/dist/tablesort.min.js" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<table>
|
<p>
|
||||||
<tr>
|
{% i18n sortable %}
|
||||||
<th/><th>{% i18n edit-col-hdr %}</th><th>{% i18n del-col-hdr %}</th>
|
</p>
|
||||||
|
<table id="userstable">
|
||||||
|
<tr data-sort-method='none'>
|
||||||
|
<th>{% i18n user-title-prefix %}</th>
|
||||||
|
<th data-sort-method='none'>{% i18n edit-col-hdr %}</th>
|
||||||
|
<th data-sort-method='none'>{% i18n del-col-hdr %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -13,11 +21,12 @@
|
||||||
<td><a href="delete-user?target={{user}}">{% i18n del-col-hdr %} {{user}}</a></td>
|
<td><a href="delete-user?target={{user}}">{% i18n del-col-hdr %} {{user}}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr>
|
<tr data-sort-method='none'>
|
||||||
<td><a href="edit-user">{% i18n add-user-label %}</a></td>
|
<td colspan="3"><a href="edit-user">{% i18n add-user-label %}</a></td>
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
new Tablesort(document.getElementById('userstable'));
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
{% block extra-headers %}
|
{% block extra-headers %}
|
||||||
{% style "/vendor/simplemde/dist/simplemde.min.css" %}
|
{% ifequal js-from ":cdnjs" %}
|
||||||
{% script "/vendor/simplemde/dist/simplemde.min.js" %}
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplemde/1.11.2/simplemde.min.js"></script>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/simplemde/1.11.2/simplemde.min.css" rel="stylesheet" type="text/css" />
|
||||||
|
{% else %}
|
||||||
|
{% style "vendor/simplemde/dist/simplemde.min.css" %}
|
||||||
|
{% script "vendor/simplemde/dist/simplemde.min.js" %}
|
||||||
|
{% endifequal %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
@ -13,7 +18,7 @@
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="summary">{% i18n what-changed-prompt %}</label>
|
<label for="summary">{% i18n what-changed-prompt %}</label>
|
||||||
<input name="summary" id="summary" type="text"
|
<input name="summary" id="summary" type="text"
|
||||||
value="{%if exists%}{%else%}New file {{title}}{%endif%}" required/>
|
value="{%if exists%}{%else%}New file {{page}}{%endif%}" required/>
|
||||||
</p>
|
</p>
|
||||||
<p class="widget">
|
<p class="widget">
|
||||||
<label for="submit">{% i18n save-prompt %}</label>
|
<label for="submit">{% i18n save-prompt %}</label>
|
||||||
|
|
|
||||||
45
resources/templates/list-uploads.html
Normal file
45
resources/templates/list-uploads.html
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
{% 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 %}
|
||||||
|
<div id="content" class="list-uploads">
|
||||||
|
<form action="list-uploads" method="post">
|
||||||
|
{% csrf-field %}
|
||||||
|
<p class="widget">
|
||||||
|
<label for="search">{% i18n matching %}</label>
|
||||||
|
<input name="search" id="search" type="text" value="{{search}}" required/>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
{% i18n sortable %}
|
||||||
|
</p>
|
||||||
|
<table id="uploads">
|
||||||
|
<tr data-sort-method='none'>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Uploaded</th>
|
||||||
|
<th>Type this</th>
|
||||||
|
<th data-sort-method='none'>To get this</th>
|
||||||
|
</tr>
|
||||||
|
{% for entry in files %}
|
||||||
|
<tr>
|
||||||
|
<th>{{entry.base-name}}</th>
|
||||||
|
<td>{{entry.modified}}</td>
|
||||||
|
<td>
|
||||||
|
{% if entry.is-image %}  {% else %} [{{entry.name|capitalize}}](uploads/{{entry.resource}}) {% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if entry.is-image %} <img src="{{entry.resource}}" alt="{{entry.name|capitalize}}"/> {% else %} <a href="{{entry.resource}}">link</a> {% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
new Tablesort(document.getElementById('uploads'));
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content" class="auth">
|
<div id="content" class="auth">
|
||||||
{% if uploaded %}
|
{% if uploaded|not-empty %}
|
||||||
{% if is-image %}
|
{% for upload in uploaded %}
|
||||||
<p>
|
{% if upload.is-image %}
|
||||||
<img id="uploaded-image" alt="Uploaded image" src="content/uploads/{{uploaded}}"/>
|
<p>
|
||||||
|
<img id="uploaded-image" alt="Uploaded image" src="{{upload.resource}}"/>
|
||||||
|
|
||||||
{% i18n file-upload-link-text %}:
|
<!-- TODO: i18n needed -->
|
||||||
|
This is the {{upload.size|name}} file. {% i18n file-upload-link-text %}:
|
||||||
|
|
||||||
<code></code>
|
<code></code>
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
{% i18n file-upload-link-text %}:
|
{% i18n file-upload-link-text %}:
|
||||||
|
|
||||||
<code>[Uploaded file](uploads/{{uploaded}})</code>
|
<code>[{{upload.filename}}]({{upload.resource}})</code>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<br clear="right"/>
|
||||||
|
<hr/>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="{{servlet-context}}/upload" enctype="multipart/form-data" method="POST">
|
<form action="{{servlet-context}}/upload" enctype="multipart/form-data" method="POST">
|
||||||
{% csrf-field %}
|
{% csrf-field %}
|
||||||
|
|
@ -34,5 +39,8 @@
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
Your <a href="list-uploads">uploaded files are listed here</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
{% extends "templates/base.html" %}
|
{% extends "templates/base.html" %}
|
||||||
|
|
||||||
{% block extra-headers %}
|
{% block extra-headers %}
|
||||||
{% style "vendor/mermaid/dist/mermaid.css" %}
|
{% for script in scripts %}
|
||||||
<!-- there's at the time of writing (20170731) a problem with the dependencies of the Bower
|
<script src="{{script}}"></script>{% endfor %}
|
||||||
package for vega-embed, so we're currently not installing either it or Vega locally.
|
{% for style in styles %}
|
||||||
TODO: fix -->
|
<link href="{{style}}" rel="stylesheet" type="text/css" />{% endfor %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega/3.0.0-rc2/vega.js"></script>
|
|
||||||
{% script "vendor/vega-lite/build/vega-lite.js" %}
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega-embed/3.0.0-beta.19/vega-embed.js"></script>
|
|
||||||
|
|
||||||
{% script "vendor/mermaid/dist/mermaid.js" %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
||||||
7
resources/test/test_extension.md
Normal file
7
resources/test/test_extension.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# This is a test
|
||||||
|
|
||||||
|
```test
|
||||||
|
the quick brown fox jumped over the lazy dog
|
||||||
|
```
|
||||||
|
|
||||||
|
This concludes the test.
|
||||||
6
resources/test/test_local_links.md
Normal file
6
resources/test/test_local_links.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# This is a test
|
||||||
|
|
||||||
|
[[Local link]]
|
||||||
|
[Not a local link](http://nowhere.at.al)
|
||||||
|
|
||||||
|
This concludes the test.
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
[environ.core :refer [env]]
|
[environ.core :refer [env]]
|
||||||
[noir.io :as io]
|
[noir.io :as io]
|
||||||
[smeagol.configuration :refer [config]]
|
[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"
|
"Return `true` if this `username`/`password` pair match, `false` otherwise"
|
||||||
[username password]
|
[username password]
|
||||||
(let [user ((keyword username) (get-users))]
|
(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
|
(and user
|
||||||
(:password user)
|
(:password user)
|
||||||
(or
|
(or
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
Return `true` if password was successfully changed. Subsequent to user change, their
|
Return `true` if password was successfully changed. Subsequent to user change, their
|
||||||
password will be encrypted."
|
password will be encrypted."
|
||||||
[username oldpass newpass]
|
[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)
|
(let [users (get-users)
|
||||||
keywd (keyword username)
|
keywd (keyword username)
|
||||||
user (keywd users)
|
user (keywd users)
|
||||||
|
|
@ -110,10 +110,10 @@
|
||||||
{keywd
|
{keywd
|
||||||
(merge user
|
(merge user
|
||||||
{:password (password/encrypt newpass)})})))
|
{:password (password/encrypt newpass)})})))
|
||||||
(timbre/info (str "Successfully changed password for user " username))
|
(log/info (str "Successfully changed password for user " username))
|
||||||
true))
|
true))
|
||||||
(catch Exception any
|
(catch Exception any
|
||||||
(timbre/error any
|
(log/error any
|
||||||
(format "Changing password failed for user %s failed: %s (%s)"
|
(format "Changing password failed for user %s failed: %s (%s)"
|
||||||
username (.getName (.getClass any)) (.getMessage any)))
|
username (.getName (.getClass any)) (.getMessage any)))
|
||||||
false))))
|
false))))
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
`email` address and `admin` flag; *or*, modify an existing user. Return true
|
`email` address and `admin` flag; *or*, modify an existing user. Return true
|
||||||
if user is successfully stored, false otherwise."
|
if user is successfully stored, false otherwise."
|
||||||
[username newpass email admin]
|
[username newpass email admin]
|
||||||
(timbre/info "Trying to add user " username)
|
(log/info "Trying to add user " username)
|
||||||
(cond
|
(cond
|
||||||
(not (string? username)) (throw (Exception. "Username must be a string."))
|
(not (string? username)) (throw (Exception. "Username must be a string."))
|
||||||
(zero? (count username)) (throw (Exception. "Username cannot be zero length"))
|
(zero? (count username)) (throw (Exception. "Username cannot be zero length"))
|
||||||
|
|
@ -160,10 +160,10 @@
|
||||||
(locking password-file-path
|
(locking password-file-path
|
||||||
(spit password-file-path
|
(spit password-file-path
|
||||||
(assoc users (keyword username) (merge user full-details)))
|
(assoc users (keyword username) (merge user full-details)))
|
||||||
(timbre/info "Successfully added user " username)
|
(log/info "Successfully added user " username)
|
||||||
true)
|
true)
|
||||||
(catch Exception any
|
(catch Exception any
|
||||||
(timbre/error any
|
(log/error any
|
||||||
(format "Adding user %s failed: %s (%s)"
|
(format "Adding user %s failed: %s (%s)"
|
||||||
username (.getName (.getClass any)) (.getMessage any)))
|
username (.getName (.getClass any)) (.getMessage any)))
|
||||||
false)))))
|
false)))))
|
||||||
|
|
@ -177,10 +177,10 @@
|
||||||
(locking password-file-path
|
(locking password-file-path
|
||||||
(spit password-file-path
|
(spit password-file-path
|
||||||
(dissoc users (keyword username)))
|
(dissoc users (keyword username)))
|
||||||
(timbre/info (str "Successfully deleted user " username))
|
(log/info (str "Successfully deleted user " username))
|
||||||
true)
|
true)
|
||||||
(catch Exception any
|
(catch Exception any
|
||||||
(timbre/error any
|
(log/error any
|
||||||
(format "Deleting user %s failed: %s (%s)"
|
(format "Deleting user %s failed: %s (%s)"
|
||||||
username (.getName (.getClass any)) (.getMessage any)))
|
username (.getName (.getClass any)) (.getMessage any)))
|
||||||
false))))
|
false))))
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
[clojure.string :as s]
|
[clojure.string :as s]
|
||||||
[environ.core :refer [env]]
|
[environ.core :refer [env]]
|
||||||
[noir.io :as io]
|
[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
|
and optionally a key :transform, whose value is a function of one
|
||||||
argument to be used to transform the value of that key."
|
argument to be used to transform the value of that key."
|
||||||
[m tuples]
|
[m tuples]
|
||||||
(timbre/debug
|
(log/debug
|
||||||
"transform-map:\n"
|
"transform-map:\n"
|
||||||
(with-out-str (clojure.pprint/pprint m)))
|
(with-out-str (clojure.pprint/pprint m)))
|
||||||
(reduce
|
(reduce
|
||||||
|
|
@ -100,6 +100,7 @@
|
||||||
'( {:from :smeagol-content-dir :to :content-dir}
|
'( {:from :smeagol-content-dir :to :content-dir}
|
||||||
{:from :smeagol-default-locale :to :default-locale}
|
{:from :smeagol-default-locale :to :default-locale}
|
||||||
{:from :smeagol-formatters :to :formatters :transform read-string}
|
{: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-log-level :to :log-level :transform to-keyword}
|
||||||
{:from :smeagol-passwd :to :passwd}
|
{:from :smeagol-passwd :to :passwd}
|
||||||
{:from :smeagol-site-title :to :site-title}))
|
{:from :smeagol-site-title :to :site-title}))
|
||||||
|
|
@ -111,9 +112,19 @@
|
||||||
file is read (if it is specified and present), but that individual
|
file is read (if it is specified and present), but that individual
|
||||||
values can be overridden by environment variables."
|
values can be overridden by environment variables."
|
||||||
(try
|
(try
|
||||||
|
(log/info (str "Reading configuration from " config-file-path))
|
||||||
(let [file-contents (try
|
(let [file-contents (try
|
||||||
(read-string (slurp config-file-path))
|
(read-string (slurp config-file-path))
|
||||||
(catch Exception _ {}))
|
(catch Exception x
|
||||||
|
(log/error
|
||||||
|
(str
|
||||||
|
"Failed to read configuration from "
|
||||||
|
config-file-path
|
||||||
|
" because: "
|
||||||
|
(type x)
|
||||||
|
"; "
|
||||||
|
(.getMessage x)))
|
||||||
|
{}))
|
||||||
config (merge
|
config (merge
|
||||||
file-contents
|
file-contents
|
||||||
(transform-map
|
(transform-map
|
||||||
|
|
@ -121,17 +132,18 @@
|
||||||
:smeagol-content-dir
|
:smeagol-content-dir
|
||||||
:smeagol-default-locale
|
:smeagol-default-locale
|
||||||
:smeagol-formatters
|
:smeagol-formatters
|
||||||
|
:smeagol-js-from
|
||||||
:smeagol-log-level
|
:smeagol-log-level
|
||||||
:smeagol-passwd
|
:smeagol-passwd
|
||||||
:smeagol-site-title)
|
:smeagol-site-title)
|
||||||
config-env-transforms))]
|
config-env-transforms))]
|
||||||
(if (env :dev)
|
(if (env :dev)
|
||||||
(timbre/debug
|
(log/debug
|
||||||
"Loaded configuration\n"
|
"Loaded configuration\n"
|
||||||
(with-out-str (clojure.pprint/pprint config))))
|
(with-out-str (clojure.pprint/pprint config))))
|
||||||
config)
|
config)
|
||||||
(catch Exception any
|
(catch Exception any
|
||||||
(timbre/error any "Could not load configuration")
|
(log/error any "Could not load configuration")
|
||||||
{})))
|
{})))
|
||||||
|
|
||||||
(def config (build-config))
|
(def config (build-config))
|
||||||
|
|
|
||||||
85
src/smeagol/extensions/mermaid.clj
Normal file
85
src/smeagol/extensions/mermaid.clj
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
(ns ^{:doc "Mermaid formatter for Semagol's extendsible 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 "<div class=\"mermaid data-visualisation\" id=\"mermaid" index "\">\n"
|
||||||
|
graph-spec
|
||||||
|
"\n</div>")))
|
||||||
|
|
||||||
|
;; (fs/file? (str (nio/resource-path) "data/classes.mermaid"))
|
||||||
|
;; (slurp (str (nio/resource-path) "data/classes.mermaid"))
|
||||||
183
src/smeagol/extensions/photoswipe.clj
Normal file
183
src/smeagol/extensions/photoswipe.clj
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
(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
|
||||||
|
"<div class=\"pswp\" id=\"pswp-"
|
||||||
|
index "\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n"
|
||||||
|
(slurp
|
||||||
|
(str (io/resource-path) "html-includes/photoswipe-boilerplate.html"))
|
||||||
|
"</div>
|
||||||
|
<script>
|
||||||
|
\n//<![CDATA[\n
|
||||||
|
var pswpElement = document.getElementById('pswp-" index "');
|
||||||
|
var spec" index " = "
|
||||||
|
spec
|
||||||
|
";
|
||||||
|
var gallery" index
|
||||||
|
" = new PhotoSwipe( pswpElement, PhotoSwipeUI_Default, spec"
|
||||||
|
index ".slides, spec" index ".options);
|
||||||
|
if (spec" index ".openImmediately) { gallery" index ".init(); }
|
||||||
|
\n//]]\n
|
||||||
|
</script>
|
||||||
|
<p><button onclick=\"gallery" index
|
||||||
|
".init()\">Open the gallery</button></p>
|
||||||
|
</div>"))
|
||||||
|
|
||||||
|
|
||||||
|
(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 := '' ;
|
||||||
|
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 #"!\[[^(]*\([^)]*\)"
|
||||||
|
;; "
|
||||||
|
;; 
|
||||||
|
;; 
|
||||||
|
;; "))
|
||||||
|
|
||||||
|
;; (process-simple-photoswipe
|
||||||
|
;; "
|
||||||
|
;; 
|
||||||
|
;; 
|
||||||
|
;; "
|
||||||
|
;; 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))))
|
||||||
10
src/smeagol/extensions/test.clj
Normal file
10
src/smeagol/extensions/test.clj
Normal file
|
|
@ -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 "<!-- The test extension has run and this is its output -->")
|
||||||
|
|
||||||
|
(defn process-test
|
||||||
|
[^String fragment ^Integer index]
|
||||||
|
process-test-return-value)
|
||||||
85
src/smeagol/extensions/utils.clj
Normal file
85
src/smeagol/extensions/utils.clj
Normal file
|
|
@ -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))))))
|
||||||
91
src/smeagol/extensions/vega.clj
Normal file
91
src/smeagol/extensions/vega.clj
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
(ns ^{:doc "Format vega/vis extensions to 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
|
||||||
|
"<div class='data-visualisation' id='vis" index "'></div>\n"
|
||||||
|
"<script>\n//<![CDATA[\nvar vl"
|
||||||
|
index
|
||||||
|
" = "
|
||||||
|
(yaml->json (str "$schema: https://vega.github.io/schema/vega-lite/v2.json\n" vega-src))
|
||||||
|
";\nvegaEmbed('#vis"
|
||||||
|
index
|
||||||
|
"', vl"
|
||||||
|
index
|
||||||
|
");\n//]]\n</script>")))
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(ns ^{:doc "Format Semagol's enhanced markdown format."
|
(ns ^{:doc "Format Semagol's extended markdown format."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.formatting
|
smeagol.formatting
|
||||||
(:require [clojure.data.json :as json]
|
(:require [clojure.data.json :as json]
|
||||||
|
|
@ -6,7 +6,12 @@
|
||||||
[cemerick.url :refer (url url-encode url-decode)]
|
[cemerick.url :refer (url url-encode url-decode)]
|
||||||
[clj-yaml.core :as yaml]
|
[clj-yaml.core :as yaml]
|
||||||
[markdown.core :as md]
|
[markdown.core :as md]
|
||||||
[smeagol.configuration :refer [config]]))
|
[smeagol.configuration :refer [config]]
|
||||||
|
[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]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -40,62 +45,12 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
;; Error to show if text to be rendered is nil.
|
|
||||||
(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 "<a href='wiki?page=%s'>%s</a>" munged text)))
|
|
||||||
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)
|
(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
|
|
||||||
"<div class='data-visualisation' id='vis" index "'></div>\n"
|
|
||||||
"<script>\n//<![CDATA[\nvar vl"
|
|
||||||
index
|
|
||||||
" = "
|
|
||||||
(yaml->json (str "$schema: https://vega.github.io/schema/vega-lite/v2.json\n" vega-src))
|
|
||||||
";\nvega.embed('#vis"
|
|
||||||
index
|
|
||||||
"', vl"
|
|
||||||
index
|
|
||||||
");\n//]]\n</script>"))
|
|
||||||
|
|
||||||
|
|
||||||
(defn process-mermaid
|
|
||||||
"Lightly mung this `graph-spec`, assumed to be a mermaid specification."
|
|
||||||
[^String graph-spec ^Integer index]
|
|
||||||
(str "<div class=\"mermaid data-visualisation\">\n"
|
|
||||||
graph-spec
|
|
||||||
"\n</div>"))
|
|
||||||
|
|
||||||
|
|
||||||
(defn process-backticks
|
(defn process-backticks
|
||||||
"Effectively, escape the backticks surrounding this `text`, by protecting them
|
"Effectively, escape the backticks surrounding this `text`, by protecting them
|
||||||
from the `md->html` filter."
|
from the `process-text` filter."
|
||||||
[^String text ^Integer index]
|
[^String text ^Integer index]
|
||||||
(str "<pre class=\"backticks\">```" (.trim text) "\n```</pre>"))
|
(str "<pre class=\"backticks\">```" (.trim text) "\n```</pre>"))
|
||||||
|
|
||||||
|
|
@ -103,7 +58,9 @@
|
||||||
(defn get-first-token
|
(defn get-first-token
|
||||||
"Return the first space-separated token of this `string`."
|
"Return the first space-separated token of this `string`."
|
||||||
[^String 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
|
(defn- process-markdown-fragment
|
||||||
|
|
@ -113,7 +70,7 @@
|
||||||
`:inclusions`, a map of constructed keywords to inclusion specifications,
|
`:inclusions`, a map of constructed keywords to inclusion specifications,
|
||||||
and `:text`, an HTML text string with the keywords present where the
|
and `:text`, an HTML text string with the keywords present where the
|
||||||
corresponding inclusion should be inserted."
|
corresponding inclusion should be inserted."
|
||||||
[index result fragment fragments processed]
|
[^Integer index ^clojure.lang.Associative result ^String fragment fragments processed]
|
||||||
(process-text
|
(process-text
|
||||||
(inc index)
|
(inc index)
|
||||||
result
|
result
|
||||||
|
|
@ -121,62 +78,73 @@
|
||||||
(cons fragment processed)))
|
(cons fragment processed)))
|
||||||
|
|
||||||
|
|
||||||
(defn- apply-formatter
|
(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
|
"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:
|
As with `process-text`, this function returns a map with two top-level keys:
|
||||||
`:inclusions`, a map of constructed keywords to inclusion specifications,
|
`:inclusions`, a map of constructed keywords to inclusion specifications,
|
||||||
and `:text`, an HTML text string with the keywords present where the
|
and `:text`, an HTML text string with the keywords present where the
|
||||||
corresponding inclusion should be inserted."
|
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]
|
||||||
(let
|
(let
|
||||||
[kw (keyword (str "inclusion-" index))]
|
[inky (keyword (str "inclusion-" index))
|
||||||
|
fkey (keyword token)]
|
||||||
(process-text
|
(process-text
|
||||||
(inc index)
|
(inc index)
|
||||||
(assoc-in result [:inclusions kw] (apply formatter (list (subs fragment (count token)) index)))
|
(deep-merge
|
||||||
(rest fragments)
|
result
|
||||||
(cons kw processed))))
|
{:inclusions {inky (eval (list formatter (subs fragment (count token)) index))}
|
||||||
|
:extensions {fkey (-> config :formatters fkey)}})
|
||||||
|
(rest fragments)
|
||||||
|
(cons inky processed))))
|
||||||
|
|
||||||
|
|
||||||
(defn process-text
|
(defn reassemble-text
|
||||||
"Process this `text`, assumed to be markdown potentially containing both local links
|
"Reassemble these processed strings into a complete text, and process it as
|
||||||
and YAML visualisation specifications, and return a map comprising JSON visualisation
|
Markdown."
|
||||||
specification, and HTML text with markers for where those should be reinserted.
|
[result processed]
|
||||||
|
(assoc result :text
|
||||||
The map has two top-level keys: `:inclusions`, a map of constructed keywords to
|
(local-links
|
||||||
inclusion specifications, and `:text`, an HTML text string with the keywords
|
(md/md-to-html-string
|
||||||
present where the corresponding inclusion should be inserted."
|
(cs/join "\n\n" (reverse processed))
|
||||||
([^String text]
|
:heading-anchors true))))
|
||||||
(process-text 0 {:inclusions {}} (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)
|
|
||||||
formatter (eval ((:formatters config) first-token))]
|
|
||||||
(cond
|
|
||||||
(empty? fragments)
|
|
||||||
(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 reintegrate-inclusions
|
(defn reintegrate-inclusions
|
||||||
"Given a map of the form produced by `process-text`, return a string of HTML text
|
"Given a map of the form produced by `process-text`, return a string of HTML text
|
||||||
with the inclusions (if any) reintegrated."
|
with the inclusions (if any) reintegrated."
|
||||||
([processed-text]
|
([processed-text]
|
||||||
(reintegrate-inclusions (:inclusions processed-text) (:text processed-text)))
|
(assoc
|
||||||
|
processed-text
|
||||||
|
:content
|
||||||
|
(reintegrate-inclusions
|
||||||
|
(:inclusions processed-text)
|
||||||
|
(:text processed-text))))
|
||||||
([inclusions text]
|
([inclusions text]
|
||||||
(let [ks (keys inclusions)]
|
(let [ks (keys inclusions)]
|
||||||
(if (empty? (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
|
text
|
||||||
(let [kw (first ks)]
|
(let [kw (first ks)]
|
||||||
(reintegrate-inclusions
|
(reintegrate-inclusions
|
||||||
|
|
@ -187,9 +155,74 @@
|
||||||
(cs/replace (kw inclusions) "\\/" "/"))))))))
|
(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
|
(defn md->html
|
||||||
"Take this markdown source, and return HTML."
|
"Process this `text`, assumed to be markdown potentially containing both local links
|
||||||
[md-src]
|
and YAML visualisation specifications, and return a map comprising JSON visualisation
|
||||||
(reintegrate-inclusions (process-text md-src)))
|
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
|
||||||
|
;; 
|
||||||
|
;; 
|
||||||
|
;; 
|
||||||
|
;; ")
|
||||||
|
;; (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
|
||||||
|
;; 
|
||||||
|
;; 
|
||||||
|
;; 
|
||||||
|
;; "} )
|
||||||
|
|
||||||
|
;; (process-text {:source (slurp (clojure.java.io/file smeagol.util/content-dir "Extensible Markup.md"))})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
[smeagol.routes.wiki :refer [wiki-routes]]
|
[smeagol.routes.wiki :refer [wiki-routes]]
|
||||||
[smeagol.middleware :refer [load-middleware]]
|
[smeagol.middleware :refer [load-middleware]]
|
||||||
[smeagol.session-manager :as session-manager]
|
[smeagol.session-manager :as session-manager]
|
||||||
[taoensso.timbre :as timbre]
|
[taoensso.timbre :as log]
|
||||||
[taoensso.timbre.appenders.3rd-party.rotor :as rotor]))
|
[taoensso.timbre.appenders.3rd-party.rotor :as rotor]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
@ -55,9 +55,9 @@
|
||||||
"destroy will be called when your application
|
"destroy will be called when your application
|
||||||
shuts down, put any clean up code here"
|
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)
|
(cronj/shutdown! session-manager/cleanup-job)
|
||||||
(timbre/info "shutdown complete!"))
|
(log/info "shutdown complete!"))
|
||||||
|
|
||||||
|
|
||||||
(defn init
|
(defn init
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
put any initialization code here"
|
put any initialization code here"
|
||||||
[]
|
[]
|
||||||
(try
|
(try
|
||||||
(timbre/merge-config!
|
(log/merge-config!
|
||||||
{:appenders
|
{:appenders
|
||||||
{:rotor (rotor/rotor-appender
|
{:rotor (rotor/rotor-appender
|
||||||
{:path "smeagol.log"
|
{:path "smeagol.log"
|
||||||
|
|
@ -80,10 +80,10 @@
|
||||||
(cronj/start! session-manager/cleanup-job)
|
(cronj/start! session-manager/cleanup-job)
|
||||||
(if (env :dev) (parser/cache-off!))
|
(if (env :dev) (parser/cache-off!))
|
||||||
;;start the expired session cleanup job
|
;;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") "]=-")
|
(when (env :dev) "using the development profile") "]=-")
|
||||||
(catch Exception any
|
(catch Exception any
|
||||||
(timbre/error any "Failure during startup")
|
(log/error any "Failure during startup")
|
||||||
(destroy))))
|
(destroy))))
|
||||||
|
|
||||||
;; timeout sessions after 30 minutes
|
;; timeout sessions after 30 minutes
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
(ns ^{:doc "Explore the history of a page."
|
(ns ^{:doc "Explore the history of a page."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.history
|
smeagol.history
|
||||||
(:require [taoensso.timbre :as timbre]
|
(:require [clj-jgit.porcelain :as git]
|
||||||
[clj-jgit.porcelain :as git]
|
|
||||||
[clj-jgit.internal :as i]
|
[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]
|
(:import [org.eclipse.jgit.api Git]
|
||||||
[org.eclipse.jgit.lib Repository ObjectId]
|
[org.eclipse.jgit.lib Repository ObjectId]
|
||||||
[org.eclipse.jgit.revwalk RevCommit RevTree RevWalk]
|
[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;
|
"If this `log-entry` contains a reference to this `file-path`, return the entry;
|
||||||
else nil."
|
else nil."
|
||||||
[^String log-entry ^String file-path]
|
[^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
|
(cond
|
||||||
(seq (filter (fn* [p1__341301#] (= (first p1__341301#) file-path)) (:changed_files log-entry)))
|
(seq (filter (fn* [p1__341301#] (= (first p1__341301#) file-path)) (:changed_files log-entry)))
|
||||||
log-entry))
|
log-entry))
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
(try
|
(try
|
||||||
(git/load-repo git-directory-path)
|
(git/load-repo git-directory-path)
|
||||||
(catch java.io.FileNotFoundException fnf
|
(catch java.io.FileNotFoundException fnf
|
||||||
|
(log/info "Initialising Git repository at" git-directory-path)
|
||||||
(git/git-init git-directory-path)
|
(git/git-init git-directory-path)
|
||||||
(let [repo (git/load-repo git-directory-path)]
|
(let [repo (git/load-repo git-directory-path)]
|
||||||
(git/git-add-and-commit repo "Initial commit")
|
(git/git-add-and-commit repo "Initial commit")
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,11 @@ smeagol.include and not inteded for direct usage."
|
||||||
(cond exists?
|
(cond exists?
|
||||||
(do
|
(do
|
||||||
(timbre/info (format "Including page '%s' from file '%s'" uri file-path))
|
(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
|
(s/defn
|
||||||
new-resolver
|
new-resolver
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@
|
||||||
[selmer.parser :as parser]
|
[selmer.parser :as parser]
|
||||||
[smeagol.configuration :refer [config]]
|
[smeagol.configuration :refer [config]]
|
||||||
[smeagol.sanity :refer :all]
|
[smeagol.sanity :refer :all]
|
||||||
[smeagol.util :as util]
|
[smeagol.util :as util]))
|
||||||
[taoensso.timbre :as timbre]))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
|
||||||
50
src/smeagol/local_links.clj
Normal file
50
src/smeagol/local_links.clj
Normal file
|
|
@ -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 "<a href='wiki?page=%s'>%s</a>" munged text)))
|
||||||
|
no-text-error))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
(ns ^{:doc "In truth, boilerplate provided by LuminusWeb."
|
(ns ^{:doc "In truth, boilerplate provided by LuminusWeb."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.middleware
|
smeagol.middleware
|
||||||
(:require [taoensso.timbre :as timbre]
|
(:require [environ.core :refer [env]]
|
||||||
[environ.core :refer [env]]
|
[noir-exception.core :refer [wrap-internal-error]]
|
||||||
[selmer.middleware :refer [wrap-error-page]]
|
|
||||||
[prone.middleware :refer [wrap-exceptions]]
|
[prone.middleware :refer [wrap-exceptions]]
|
||||||
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
|
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
|
||||||
[ring.middleware.file :refer [wrap-file]]
|
[ring.middleware.file :refer [wrap-file]]
|
||||||
[ring.middleware.resource :refer [wrap-resource]]
|
[ring.middleware.resource :refer [wrap-resource]]
|
||||||
[ring.middleware.content-type :refer [wrap-content-type]]
|
[ring.middleware.content-type :refer [wrap-content-type]]
|
||||||
[ring.middleware.not-modified :refer [wrap-not-modified]]
|
[ring.middleware.not-modified :refer [wrap-not-modified]]
|
||||||
[noir-exception.core :refer [wrap-internal-error]]
|
[selmer.middleware :refer [wrap-error-page]]
|
||||||
[smeagol.util :as util]))
|
[smeagol.util :as util]
|
||||||
|
[taoensso.timbre :as log]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
(defn log-request [handler]
|
(defn log-request [handler]
|
||||||
(fn [req]
|
(fn [req]
|
||||||
(timbre/debug req)
|
(log/debug req)
|
||||||
(handler req)))
|
(handler req)))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
|
|
||||||
(def production-middleware
|
(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-resource % "public")
|
||||||
#(wrap-file % util/content-dir
|
#(wrap-file % util/content-dir
|
||||||
{:index-files? false :prefer-handler? true})
|
{:index-files? false :prefer-handler? true})
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,18 @@
|
||||||
(:require [cemerick.url :refer (url url-encode url-decode)]
|
(:require [cemerick.url :refer (url url-encode url-decode)]
|
||||||
[clj-jgit.porcelain :as git]
|
[clj-jgit.porcelain :as git]
|
||||||
[clojure.java.io :as cjio]
|
[clojure.java.io :as cjio]
|
||||||
|
[clojure.pprint :refer [pprint]]
|
||||||
[clojure.string :as cs]
|
[clojure.string :as cs]
|
||||||
[clojure.walk :refer :all]
|
[clojure.walk :refer :all]
|
||||||
[compojure.core :refer :all]
|
[compojure.core :refer :all]
|
||||||
|
[java-time :as jt]
|
||||||
|
[me.raynes.fs :as fs]
|
||||||
[noir.io :as io]
|
[noir.io :as io]
|
||||||
[noir.response :as response]
|
[noir.response :as response]
|
||||||
[noir.util.route :as route]
|
[noir.util.route :as route]
|
||||||
[noir.session :as session]
|
[noir.session :as session]
|
||||||
[smeagol.authenticate :as auth]
|
[smeagol.authenticate :as auth]
|
||||||
|
[smeagol.configuration :refer [config]]
|
||||||
[smeagol.diff2html :as d2h]
|
[smeagol.diff2html :as d2h]
|
||||||
[smeagol.formatting :refer [md->html]]
|
[smeagol.formatting :refer [md->html]]
|
||||||
[smeagol.history :as hist]
|
[smeagol.history :as hist]
|
||||||
|
|
@ -20,10 +24,12 @@
|
||||||
[smeagol.sanity :refer [show-sanity-check-error]]
|
[smeagol.sanity :refer [show-sanity-check-error]]
|
||||||
[smeagol.util :as util]
|
[smeagol.util :as util]
|
||||||
[smeagol.uploads :as ul]
|
[smeagol.uploads :as ul]
|
||||||
[taoensso.timbre :as timbre]
|
[taoensso.timbre :as log]
|
||||||
[com.stuartsierra.component :as component]
|
[com.stuartsierra.component :as component]
|
||||||
|
[smeagol.configuration :refer [config]]
|
||||||
[smeagol.include.resolve-local-file :as resolve]
|
[smeagol.include.resolve-local-file :as resolve]
|
||||||
[smeagol.include :as include]))
|
[smeagol.include :as include]
|
||||||
|
[smeagol.util :refer [content-dir local-url]]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -52,7 +58,7 @@
|
||||||
"Process `source-text` and save it to the specified `file-path`, committing it
|
"Process `source-text` and save it to the specified `file-path`, committing it
|
||||||
to Git and finally redirecting to wiki-page."
|
to Git and finally redirecting to wiki-page."
|
||||||
[params suffix request]
|
[params suffix request]
|
||||||
(timbre/trace (format "process-source: '%s'" request))
|
(log/trace (format "process-source: '%s'" request))
|
||||||
(let [source-text (:src params)
|
(let [source-text (:src params)
|
||||||
page (:page params)
|
page (:page params)
|
||||||
file-name (str page suffix)
|
file-name (str page suffix)
|
||||||
|
|
@ -62,7 +68,7 @@
|
||||||
user (session/get :user)
|
user (session/get :user)
|
||||||
email (auth/get-email user)
|
email (auth/get-email user)
|
||||||
summary (format "%s: %s" user (or (:summary params) "no summary"))]
|
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)
|
(spit file-path source-text)
|
||||||
(git/git-add git-repo file-name)
|
(git/git-add git-repo file-name)
|
||||||
(git/git-commit git-repo summary {:name user :email email})
|
(git/git-commit git-repo summary {:name user :email email})
|
||||||
|
|
@ -92,16 +98,16 @@
|
||||||
user (session/get :user)]
|
user (session/get :user)]
|
||||||
(if-not
|
(if-not
|
||||||
exists?
|
exists?
|
||||||
(timbre/info
|
(log/info
|
||||||
(format "File '%s' not found; creating a new file" file-path))
|
(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)
|
(cond src-text (process-source params suffix request)
|
||||||
true
|
true
|
||||||
(layout/render template
|
(layout/render template
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title (str (util/get-message :edit-title-prefix request) " " page)
|
{:title (str (util/get-message :edit-title-prefix request) " " page)
|
||||||
:page 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) "")
|
:content (if exists? (slurp file-path) "")
|
||||||
:exists exists?})))))))
|
:exists exists?})))))))
|
||||||
|
|
||||||
|
|
@ -113,6 +119,9 @@
|
||||||
|
|
||||||
|
|
||||||
(def md-include-system
|
(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/start
|
||||||
(component/system-map
|
(component/system-map
|
||||||
:resolver (resolve/new-resolver util/content-dir)
|
:resolver (resolve/new-resolver util/content-dir)
|
||||||
|
|
@ -120,10 +129,66 @@
|
||||||
(include/new-includer)
|
(include/new-includer)
|
||||||
[:resolver]))))
|
[: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 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
|
||||||
|
([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 {:source (slurp "resources/public/content/Simplified example gallery.md" )}))
|
||||||
|
|
||||||
|
;; (preferred-source (-> processed-text :extensions :pswp :scripts :core) :pswp)
|
||||||
|
|
||||||
|
;; (-> processed-text :extensions)
|
||||||
|
|
||||||
|
;; (collect-preferred processed-text :scripts)
|
||||||
|
|
||||||
(defn wiki-page
|
(defn wiki-page
|
||||||
"Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page"
|
"Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page"
|
||||||
[request]
|
[request]
|
||||||
(timbre/trace (format "wiki-page: '%s'" request))
|
(log/trace (format "wiki-page: '%s'" request))
|
||||||
(or
|
(or
|
||||||
(show-sanity-check-error)
|
(show-sanity-check-error)
|
||||||
(let [params (keywordize-keys (:params request))
|
(let [params (keywordize-keys (:params request))
|
||||||
|
|
@ -131,19 +196,24 @@
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")
|
||||||
file-path (cjio/file util/content-dir file-name)
|
file-path (cjio/file util/content-dir file-name)
|
||||||
exists? (.exists (clojure.java.io/as-file file-path))]
|
exists? (.exists (clojure.java.io/as-file file-path))]
|
||||||
(cond exists?
|
(if exists?
|
||||||
(do
|
(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"
|
(let [processed-text (md->html
|
||||||
(merge (util/standard-params request)
|
(assoc request :source
|
||||||
{:title page
|
(include/expand-include-md
|
||||||
:page page
|
(:includer md-include-system)
|
||||||
:content (md->html
|
(slurp file-path))))]
|
||||||
(include/expand-include-md
|
(layout/render "wiki.html"
|
||||||
(:includer md-include-system)
|
(merge (util/standard-params request)
|
||||||
(slurp file-path)))
|
processed-text
|
||||||
:editable true})))
|
{:title page
|
||||||
true (response/redirect (str "/edit?page=" 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
|
(defn history-page
|
||||||
|
|
@ -154,43 +224,103 @@
|
||||||
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
|
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")
|
||||||
repo-path util/content-dir]
|
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"
|
(layout/render "history.html"
|
||||||
(merge (util/standard-params request)
|
(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
|
:page page
|
||||||
:history (hist/find-history repo-path file-name)}))))
|
: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
|
||||||
|
"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 "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))
|
||||||
|
files
|
||||||
|
(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"
|
||||||
|
(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)
|
||||||
|
}))))
|
||||||
|
|
||||||
|
|
||||||
|
;;;; end of list-uploads section ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn upload-page
|
(defn upload-page
|
||||||
"Render a form to allow the upload of a file."
|
"Render a form to allow the upload of a file."
|
||||||
[request]
|
[request]
|
||||||
(let [params (keywordize-keys (:params 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)
|
git-repo (hist/load-or-init-repo util/content-dir)
|
||||||
upload (:upload params)
|
upload (:upload params)
|
||||||
uploaded (if upload (ul/store-upload params data-path))
|
uploaded (if upload (ul/store-upload params data-path))
|
||||||
user (session/get :user)
|
user (session/get :user)
|
||||||
summary (format "%s: %s" user (or (:summary params) "no summary"))]
|
summary (format "%s: %s" user (or (:summary params) "no summary"))]
|
||||||
(if
|
;; TODO: Get this working! it MUST work!
|
||||||
uploaded
|
;; (if-not
|
||||||
(do
|
;; (empty? uploaded)
|
||||||
(git/git-add git-repo uploaded)
|
;; (do
|
||||||
(git/git-commit git-repo summary {:name user :email (auth/get-email user)})))
|
;; (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"
|
(layout/render "upload.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title (util/get-message :file-upload-title request)
|
{:title (util/get-message :file-upload-title request)
|
||||||
:uploaded uploaded
|
:uploaded uploaded}))))
|
||||||
:is-image (and
|
|
||||||
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")))}))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn version-page
|
(defn version-page
|
||||||
"Render a specific historical version of a page"
|
"Render a specific historical version of a page"
|
||||||
|
|
@ -200,7 +330,7 @@
|
||||||
version (:version params)
|
version (:version params)
|
||||||
file-name (str page ".md")
|
file-name (str page ".md")
|
||||||
content (hist/fetch-version util/content-dir file-name version)]
|
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"
|
(layout/render "wiki.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title (str (util/get-message :vers-col-hdr request) " " version " " (util/get-message :of request) " " page)
|
{:title (str (util/get-message :vers-col-hdr request) " " version " " (util/get-message :of request) " " page)
|
||||||
|
|
@ -215,7 +345,7 @@
|
||||||
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
|
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
|
||||||
version (:version params)
|
version (:version params)
|
||||||
file-name (str page ".md")]
|
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"
|
(layout/render "wiki.html"
|
||||||
(merge (util/standard-params request)
|
(merge (util/standard-params request)
|
||||||
{:title
|
{:title
|
||||||
|
|
@ -237,16 +367,18 @@
|
||||||
[request]
|
[request]
|
||||||
(or
|
(or
|
||||||
(show-sanity-check-error)
|
(show-sanity-check-error)
|
||||||
(let [params (keywordize-keys (:form-params request))
|
(let [params (keywordize-keys (:params request))
|
||||||
username (:username params)
|
form-params (keywordize-keys (:form-params request))
|
||||||
password (:password params)
|
username (:username form-params)
|
||||||
action (:action params)
|
password (:password form-params)
|
||||||
|
action (:action form-params)
|
||||||
user (session/get :user)
|
user (session/get :user)
|
||||||
redirect-to (or (:redirect-to params) "/wiki")]
|
redirect-to (:redirect-to params)]
|
||||||
|
(if redirect-to (log/info (str "After auth, redirect to: " redirect-to)))
|
||||||
(cond
|
(cond
|
||||||
(= action (util/get-message :logout-label request))
|
(= action (util/get-message :logout-label request))
|
||||||
(do
|
(do
|
||||||
(timbre/info (str "User " user " logging out"))
|
(log/info (str "User " user " logging out"))
|
||||||
(session/remove! :user)
|
(session/remove! :user)
|
||||||
(response/redirect redirect-to))
|
(response/redirect redirect-to))
|
||||||
(and username password (auth/authenticate username password))
|
(and username password (auth/authenticate username password))
|
||||||
|
|
@ -259,8 +391,22 @@
|
||||||
{:title (if user
|
{:title (if user
|
||||||
(str (util/get-message :logout-link request) " " user)
|
(str (util/get-message :logout-link request) " " user)
|
||||||
(util/get-message :login-link request))
|
(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
|
(defn passwd-page
|
||||||
"Render a page to change the user password"
|
"Render a page to change the user password"
|
||||||
|
|
@ -286,8 +432,10 @@
|
||||||
|
|
||||||
|
|
||||||
(defroutes wiki-routes
|
(defroutes wiki-routes
|
||||||
(GET "/wiki" request (wiki-page request))
|
|
||||||
(GET "/" 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 "/delete-user" request (route/restricted (admin/delete-user request)))
|
||||||
(GET "/edit" request (route/restricted (edit-page request)))
|
(GET "/edit" request (route/restricted (edit-page request)))
|
||||||
(POST "/edit" request (route/restricted (edit-page request)))
|
(POST "/edit" request (route/restricted (edit-page request)))
|
||||||
|
|
@ -297,11 +445,12 @@
|
||||||
(GET "/edit-user" request (route/restricted (admin/edit-user request)))
|
(GET "/edit-user" request (route/restricted (admin/edit-user request)))
|
||||||
(POST "/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 "/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 "/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))
|
(GET "/passwd" request (passwd-page request))
|
||||||
(POST "/passwd" request (passwd-page request))
|
(POST "/passwd" request (passwd-page request))
|
||||||
(GET "/upload" request (route/restricted (upload-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))
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
(ns ^{:doc "Handle file uploads."
|
(ns ^{:doc "Handle file uploads."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.uploads
|
smeagol.uploads
|
||||||
(:import [java.io File])
|
|
||||||
(:require [clojure.string :as cs]
|
(:require [clojure.string :as cs]
|
||||||
[noir.io :as io]
|
[clojure.java.io :as io]
|
||||||
[taoensso.timbre :as timbre]))
|
[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,38 +39,96 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
;; No longer used as uploaded files now go into Git.
|
(def image-file-extns
|
||||||
;; (defn avoid-name-collisions
|
"Extensions of file types we will attempt to thumbnail. GIF is excluded
|
||||||
;; "Find a filename within this `path`, based on this `file-name`, that does not
|
because by default the javax.imageio package can read GIF, PNG, and JPEG
|
||||||
;; reference an existing file. It is assumed that `path` ends with a path separator.
|
images but can only write PNG and JPEG images."
|
||||||
;; Returns a filename hwich does not currently reference a file within the path."
|
#{".jpg" ".jpeg" ".png"})
|
||||||
;; [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))
|
|
||||||
|
|
||||||
|
(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
|
(defn store-upload
|
||||||
"Store an upload both to the file system and to the database.
|
"Store an upload both to the file system and to the database.
|
||||||
The issue with storing an upload is moving it into place.
|
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
|
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]
|
[params path]
|
||||||
(let [upload (:upload params)
|
(let [upload (:upload params)
|
||||||
tmp-file (:tempfile upload)
|
tmp-file (:tempfile upload)
|
||||||
filename (:filename upload)]
|
filename (:filename upload)]
|
||||||
(timbre/info
|
(log/info
|
||||||
(str "Storing upload file: " upload))
|
(str "Storing upload file: " upload))
|
||||||
(timbre/debug
|
(log/debug
|
||||||
(str "store-upload mv file: " tmp-file " to: " path filename))
|
(str "store-upload mv file: " tmp-file " to: " path filename))
|
||||||
(if tmp-file
|
(if tmp-file
|
||||||
(do
|
(try
|
||||||
(.renameTo tmp-file
|
(let [p (io/file path filename)]
|
||||||
(File. (str path filename)))
|
(.renameTo tmp-file p)
|
||||||
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)))
|
||||||
(throw (Exception. "No file found?")))))
|
(throw (Exception. "No file found?")))))
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,17 @@
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
smeagol.util
|
smeagol.util
|
||||||
(:require [clojure.java.io :as cjio]
|
(:require [clojure.java.io :as cjio]
|
||||||
|
[clojure.string :as cs]
|
||||||
[environ.core :refer [env]]
|
[environ.core :refer [env]]
|
||||||
|
[markdown.core :as md]
|
||||||
|
[me.raynes.fs :as fs]
|
||||||
[noir.io :as io]
|
[noir.io :as io]
|
||||||
[noir.session :as session]
|
[noir.session :as session]
|
||||||
[scot.weft.i18n.core :as i18n]
|
[scot.weft.i18n.core :as i18n]
|
||||||
[smeagol.authenticate :as auth]
|
[smeagol.authenticate :as auth]
|
||||||
[smeagol.configuration :refer [config]]
|
[smeagol.configuration :refer [config]]
|
||||||
[smeagol.formatting :refer [md->html]]
|
[smeagol.local-links :refer :all]
|
||||||
[taoensso.timbre :as timbre]))
|
[taoensso.timbre :as log]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;
|
;;;;
|
||||||
|
|
@ -39,10 +42,91 @@
|
||||||
(:start-page config))
|
(:start-page config))
|
||||||
|
|
||||||
(def content-dir
|
(def content-dir
|
||||||
(or
|
(str
|
||||||
(:content-dir config)
|
(fs/absolute
|
||||||
(cjio/file (io/resource-path) "content")))
|
(or
|
||||||
|
(:content-dir config)
|
||||||
|
(cjio/file (io/resource-path) "content")))))
|
||||||
|
|
||||||
|
(def upload-dir
|
||||||
|
(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")))))
|
||||||
|
|
||||||
|
(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]
|
||||||
|
(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
|
||||||
|
"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"])))
|
||||||
|
(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]
|
||||||
|
(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 (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)
|
||||||
|
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))))
|
||||||
|
|
||||||
|
;; (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
|
(defn standard-params
|
||||||
"Return a map of standard parameters to pass to the template renderer."
|
"Return a map of standard parameters to pass to the template renderer."
|
||||||
|
|
@ -50,37 +134,43 @@
|
||||||
(let [user (session/get :user)]
|
(let [user (session/get :user)]
|
||||||
{:user user
|
{:user user
|
||||||
:admin (auth/get-admin user)
|
:admin (auth/get-admin user)
|
||||||
:side-bar (md->html (slurp (cjio/file content-dir "_side-bar.md")))
|
:js-from (:js-from config)
|
||||||
:header (md->html (slurp (cjio/file content-dir "_header.md")))
|
:side-bar (md/md-to-html-string
|
||||||
|
(local-links
|
||||||
|
(slurp (cjio/file content-dir "_side-bar.md")))
|
||||||
|
:heading-anchors true)
|
||||||
|
:header (md/md-to-html-string
|
||||||
|
(local-links
|
||||||
|
(slurp (cjio/file content-dir "_header.md")))
|
||||||
|
:heading-anchors true)
|
||||||
:version (System/getProperty "smeagol.version")}))
|
:version (System/getProperty "smeagol.version")}))
|
||||||
|
|
||||||
|
|
||||||
(defn- raw-get-messages
|
(def get-messages
|
||||||
"Return the most acceptable messages collection we have given the
|
"Return the most acceptable messages collection we have given the
|
||||||
`Accept-Language` header in this `request`."
|
`Accept-Language` header in this `request`."
|
||||||
[request]
|
(memoize
|
||||||
(let [specifier ((:headers request) "accept-language")
|
(fn [request]
|
||||||
messages (try
|
(let [specifier ((:headers request) "accept-language")
|
||||||
(i18n/get-messages specifier "i18n" "en-GB")
|
messages (try
|
||||||
(catch Exception any
|
(i18n/get-messages specifier "i18n" "en-GB")
|
||||||
(timbre/error
|
(catch Exception any
|
||||||
any
|
(log/error
|
||||||
(str
|
any
|
||||||
"Failed to parse accept-language header "
|
(str
|
||||||
specifier))
|
"Failed to parse accept-language header '"
|
||||||
{}))]
|
specifier
|
||||||
|
"'"))
|
||||||
|
{}))]
|
||||||
(merge
|
(merge
|
||||||
messages
|
messages
|
||||||
config)))
|
config)))))
|
||||||
|
|
||||||
|
|
||||||
(def get-messages (memoize raw-get-messages))
|
|
||||||
|
|
||||||
|
|
||||||
(defn get-message
|
(defn get-message
|
||||||
"Return the message with this `message-key` from this `request`.
|
"Return the message with this `message-key` from this `request`.
|
||||||
if not found, return this `default`, if provided; else return the
|
if not found, return this `default`, if provided; else return the
|
||||||
`message-key`."
|
`message-key`."
|
||||||
([message-key request]
|
([message-key request]
|
||||||
(get-message message-key message-key request))
|
(get-message message-key message-key request))
|
||||||
([message-key default request]
|
([message-key default request]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,43 @@
|
||||||
(ns smeagol.test.formatting
|
(ns smeagol.test.formatting
|
||||||
(:require [clojure.test :refer :all]
|
(:require [clojure.test :refer :all]
|
||||||
[smeagol.formatting :refer [local-links no-text-error]]))
|
[clojure.string :as cs]
|
||||||
|
[smeagol.formatting :refer :all]
|
||||||
|
[smeagol.extensions.test :refer :all]
|
||||||
|
[smeagol.local-links :refer :all]))
|
||||||
|
|
||||||
(deftest test-local-links
|
(deftest test-apply-formatter
|
||||||
(testing "Rewriting of local links"
|
(testing "apply-formatter"
|
||||||
(is (= (local-links nil) no-text-error) "Should NOT fail with a no pointer exception!")
|
(let [actual (-> (apply-formatter
|
||||||
(is (= (local-links "") "") "Empty string should pass through unchanged.")
|
3
|
||||||
(is (= (local-links "[[froboz]]") "<a href='wiki?page=froboz'>froboz</a>") "Local link should be rewritten.")
|
{:inclusions {}}
|
||||||
(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"))))
|
"test
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
"
|
||||||
|
"test"
|
||||||
|
smeagol.extensions.test/process-test)
|
||||||
|
:inclusions
|
||||||
|
:inclusion-3)
|
||||||
|
expected "<!-- The test extension has run and this is its output -->"]
|
||||||
|
(is (= actual expected)))))
|
||||||
|
|
||||||
|
(deftest test-md->html
|
||||||
|
(let [actual (:content (md->html
|
||||||
|
{:source
|
||||||
|
(cs/join
|
||||||
|
"\n"
|
||||||
|
["# This is a test"
|
||||||
|
""
|
||||||
|
"```test"
|
||||||
|
""
|
||||||
|
"```"
|
||||||
|
""
|
||||||
|
"This concludes the test"])} ))
|
||||||
|
expected (str
|
||||||
|
"<h1 id=\"this_is_a_test\">This is a test</h1>"
|
||||||
|
"<p><!-- The test extension has run and this is its output --></p>"
|
||||||
|
"<p>This concludes the test</p>")]
|
||||||
|
(is (= expected actual))))
|
||||||
|
|
|
||||||
19
test/smeagol/test/local_links.clj
Normal file
19
test/smeagol/test/local_links.clj
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
(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]))
|
||||||
|
|
||||||
|
(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]]") "<a href='wiki?page=froboz'>froboz</a>") "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"))
|
||||||
|
(let [text (cs/trim (slurp "resources/test/test_local_links.md"))
|
||||||
|
actual (local-links text)
|
||||||
|
expected "# This is a test\n\n<a href='wiki?page=Local%20link'>Local link</a>\n[Not a local link](http://nowhere.at.al)\n\nThis concludes the test."]
|
||||||
|
(is (= actual expected)))))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue