Compare commits

..

126 commits

Author SHA1 Message Date
8cb51260fe Fixed the master branch to correct breakage due to non-backward-compatibility in cider.
`develop` branch is still very broken at this time.
2026-03-29 16:03:39 +01:00
d22bd72f5a Added note that Smeagol is no longer actively maintained. 2023-06-16 09:34:04 +01:00
6c1bf5f860
Corrected all JavaScript security vulnerabilities except simplemde
There doesn't (yet) seem to be a fix for the simplemde problem.
2022-01-07 11:16:47 +00:00
a775ef7b83
WHOOPS! Introduced bugs when I was supposed just to be documenting. 2020-02-19 21:18:06 +00:00
b2ee34bd8e
Mainly documentation, but some reorganisation to support documentation. 2020-02-19 21:07:40 +00:00
256cf702cf
Added files used by example galleries to repository 2020-02-19 16:52:37 +00:00
8618926318
Merge tag 'smeagol-1.0.4' 2020-02-19 16:44:20 +00:00
be3c9fc946
lein-release plugin: preparing 1.0.4 release 2020-02-19 16:43:43 +00:00
6b229e35ce
Merge branch 'feature/47' into develop 2020-02-19 16:43:17 +00:00
019f2e8276
Regression fixed. 2020-02-19 16:42:58 +00:00
f82ad725c1
Working,, but regression in Mermaid stylesheet 2020-02-19 16:18:58 +00:00
03c63da19e
Tactical commit 2020-02-19 15:27:08 +00:00
0649ecf195
Thank fuck, it works. Now to remove all the crud. 2020-02-19 15:01:09 +00:00
0f0f2ecc48
Re-separated apply-formatter. Still doesn't work.
(but doesn't blow up)
2020-02-19 12:50:52 +00:00
151987e598
#45,#46: done 2020-02-16 13:51:41 +00:00
ee46b0d545
Merge remote-tracking branch 'origin/develop' into feature/47 2020-02-14 18:14:45 +00:00
10e5d15e65 Merge branch 'feature/grcss' into develop 2020-02-14 16:09:12 +00:00
392a5f82ec These are the genuine improvements out of today
1. Table sorting
2. Fixed edit page title bugette
3. SimpleMDE working again.
2020-02-14 16:08:17 +00:00
b7a7c4cfc8 Merge branch 'develop' into feature/grcss 2020-02-14 11:44:06 +00:00
c06fce3007 Bugfix: looking in the wrong directory for uploads. 2020-02-14 11:43:50 +00:00
2e106256f8 More Ginny stylesheet 2020-02-14 11:43:07 +00:00
0417fda910 Added TODO note, only. 2020-02-14 08:51:14 +00:00
37d850d30a
Still no actual progress. 2020-02-13 21:07:36 +00:00
8032ad60af
No actual progress. 2020-02-13 19:46:58 +00:00
40ab296d1a
#45: OK, it doesn't work, but it's close.
Still getting fragment index instead of fragment text.
2020-02-13 14:45:16 +00:00
2f22b733c1
Tidy up
alphordered includes, standardised on use 'log' as alias for timbre.
2020-02-12 21:13:36 +00:00
0d686a9b63
#47: Something well broken here, but I'm on the right path
Fragment indices are being returned instead of fragments, and it does not seem that the extension formatters are being called at all. But... config is definitely improved in the
right direction.
2020-02-12 12:35:18 +00:00
970636325a Merge branch 'feature/47' into develop
#47 is not yet fully finished, but it's sufficiently advanced to be snapshotted into the `develop` branch.
2020-02-12 05:41:26 +00:00
b191f40d05
#47: warn if dimensions cannot be established. 2020-02-11 17:01:14 +00:00
d2e20162ef Simplified syntax for Photoswipe galleries now works 2020-02-11 13:14:36 +00:00
fc4dcdb5d3 Mainly documenting the configuration file 2020-02-11 08:30:03 +00:00
ba45ea5163
#47, #49: Auto-thumbnailing now working and configurable
Not yet documented.
2020-02-10 22:56:24 +00:00
40f4f13667
Tactical commit: I'm fairly sure this is close to good. 2020-02-10 21:36:49 +00:00
ad5e41c23a
Progress on thumbnailing, but not working yet. 2020-02-10 17:39:24 +00:00
719222195e Working, but not finished. 2020-02-10 11:53:39 +00:00
1df78111cd Added separate Vega extension file 2020-02-09 09:23:56 +00:00
54b82931b2 Major restructuring of extension processors, not yet complete 2020-02-09 01:42:31 +00:00
e00beaf790 Trying to get JavaScript switchable between local and cloudflare
Not working for two reasons:
1. `lein npm install` does not build packages on MacOS;
2. `{% ifequal js-from ":cloudflare" %}` breaks Selmer in  wiki.html but not in edit.html - WTF?
2020-02-08 10:45:31 +00:00
ecd9d5a270
Unfinished (but non-breaking) work on after-auth redirect 2020-02-07 22:49:00 +00:00
59f86925e8
Corrected link rot (hadn't I done this before?) 2020-02-07 22:47:46 +00:00
915b5b2a6b Merge remote-tracking branch 'origin/develop' into develop 2020-02-07 19:15:19 +00:00
7ddff8434a Merge remote-tracking branch 'origin/develop' into develop 2020-02-07 19:14:42 +00:00
24888d27fc Merge remote-tracking branch 'origin/develop' into develop 2020-02-07 19:01:47 +00:00
da3bde16d0 Added 'list uploaded files' page, only accessible if logged in. 2020-02-07 19:01:30 +00:00
84360110fc Fixes to two minor bugs
* Wrong URL printed in upload page;
* 'New file Foo' instead of 'New file Edit Foo' as default git commit message for a new file.
2020-02-07 14:20:32 +00:00
dad380e0d9
Attempting to understand why the configuration doesn't load. 2020-02-06 15:23:59 +00:00
324f26dbf7 Merge remote-tracking branch 'origin/develop' into develop 2020-02-05 23:49:09 +00:00
a6fb758314 Separate menu icon onto its own line, relativise the URL 2020-02-05 23:49:02 +00:00
Simon Brooke
2496ea8f4d
Merge pull request #41 from DomainDrivenArchitecture/make_include_more_fault_tolerant
make include error handling more user friendly
2020-02-05 23:47:45 +00:00
d6d0a5fc40 Another bit of #44, which got missed somehow. 2020-02-05 23:15:48 +00:00
b2003480fd
Merge remote-tracking branch 'origin/develop' into develop 2020-02-05 22:32:43 +00:00
3f667afb35 #44: fixed 2020-02-05 18:21:02 +00:00
ab2ec82f5e Merge branch 'develop' into origin/develop 2020-02-05 17:47:05 +00:00
38e4207d4a Minor change to log message 2020-02-05 17:45:31 +00:00
852f4a616c
Merge tag 'smeagol-1.0.3' 2020-02-05 10:47:52 +00:00
1105eacb05
lein-release plugin: bumped version from 1.0.3 to 1.0.4-SNAPSHOT for next development cycle 2020-02-05 10:47:22 +00:00
a821f8d988
lein-release plugin: preparing 1.0.3 release 2020-02-05 10:47:13 +00:00
5b01945c58
Trying to sort out the release process, which is not working 2020-02-05 10:38:12 +00:00
6e907cc85d Upversioned to '1.0.3-SNAPSHOT'; amended README. 2020-01-23 08:43:49 +00:00
4d5f1d553d Updated some obsolete dependencies
(probably more need to be updated).
2020-01-23 08:26:57 +00:00
jem
95ee7e13eb make error handling more user friendly 2019-02-13 08:15:53 +01:00
f2479b2f99 Merge branch 'release/1.0.2' 2019-01-19 17:46:44 +00:00
89b7597418 Merge branch 'release/1.0.2' into develop 2019-01-19 17:46:44 +00:00
8374432a16 Added change log 2019-01-19 17:46:33 +00:00
5b785389b4 Version 1.0.2 2019-01-19 17:20:50 +00:00
2df4931fbe Merge remote-tracking branch 'origin/develop' into develop 2019-01-19 17:05:41 +00:00
Simon Brooke
01982fdb11
Merge pull request #38 from DomainDrivenArchitecture/fix_resource_handling
fix rings & noirs resource upload & serve location
2019-01-19 16:59:17 +00:00
33c0398138 Merge remote-tracking branch 'origin/develop' into develop 2019-01-19 16:43:39 +00:00
Simon Brooke
b1eeecde1d
Merge pull request #39 from DomainDrivenArchitecture/Configurable_intro_page
make start page configurable
2019-01-19 16:41:47 +00:00
jem
4f8c4b8925 fixed test - Now we are getting a 200 response again. 2019-01-18 15:06:33 +01:00
Vlad Bokov
3542ac9146
Prefer content-dir files over classpath (like /content/stylesheet.css) 2019-01-18 07:05:19 +07:00
jem
fad1fcfea5 make start page configurable 2019-01-08 19:59:16 +01:00
jem
1136e792d4 fix rings & noirs resource upload & serve location 2019-01-08 17:42:24 +01:00
Simon Brooke
4026e23946
Merge pull request #36 from DomainDrivenArchitecture/include_doc
Include doc added to README
2018-05-28 12:48:31 +01:00
jem
77d77ed334 minor doc add in README 2018-05-25 17:21:32 +02:00
jem
7e5b3d74da spelling & format fixes for README 2018-05-25 17:19:33 +02:00
jem
7b284eb13f Added include doc 2018-05-25 17:15:46 +02:00
f3456d819c Upversion to 1.0.2-SNAPSHOT 2018-05-24 08:28:37 +01:00
a8cf3ee0ea Merge branch 'master' into develop 2018-05-24 08:27:42 +01:00
05eafe603f Minor: alphorder requirements, documentation spelling. 2018-05-24 08:26:37 +01:00
880b174766 Merge remote-tracking branch 'origin/master' 2018-05-24 08:06:06 +01:00
d1cf584a38 Merge branch 'release/1.0.1' 2018-05-24 08:05:47 +01:00
5c241abecd Merge branch 'release/1.0.1' into develop 2018-05-24 08:05:46 +01:00
Simon Brooke
e4b82f93fb
Merge pull request #35 from DomainDrivenArchitecture/include-feature
Include feature
2018-05-24 08:05:16 +01:00
40f299b43d Version 1.0.1 2018-05-24 07:26:39 +01:00
56aa8ca2f1 Merge remote-tracking branch 'origin/develop' into develop 2018-05-24 07:25:00 +01:00
778c0a84e2 i18n 2018-05-24 07:23:11 +01:00
jem
01c954fc2f fix uri resolving 2018-05-22 21:22:58 +02:00
jem
7674a4c305 stick system together 2018-05-22 18:01:22 +02:00
jem
3668b26df1 add doc to namespaces 2018-05-22 16:59:43 +02:00
jem
535465c362 implement indention 2018-05-18 20:47:15 +02:00
jem
12d4661db9 expectation for header & list indent 2018-05-18 17:17:15 +02:00
jem
07342b5ac4 implemented includes-resolving for tests 2018-05-18 17:13:33 +02:00
jem
6768d71429 add replacement 2018-05-18 14:31:46 +02:00
jem
78a534349b add the replacement parameter 2018-05-18 14:31:32 +02:00
jem
6714dc04bf refactor parse out of include ns 2018-05-18 12:33:07 +02:00
jem
4a4269d202 renamed resolver -> resolve 2018-05-18 12:32:49 +02:00
jem
7d479e95b1 add one more parsing testcase 2018-05-18 12:09:16 +02:00
jem
4025b1e29c include link parsing now works 2018-05-18 12:05:13 +02:00
jem
6918ba27e8 implemented simple parsing 2018-05-17 09:08:09 +02:00
jem
9607657cc1 added schema & use separated resolver & includer component. 2018-05-16 18:41:54 +02:00
jem
464e9af7d6 use dependency injection in order to make include resolving testable 2018-05-15 19:31:24 +02:00
jem
6be21214b0 start with some thinking about the problem ... 2018-05-15 18:50:54 +02:00
cd25d8bd76 Version 1.0.1 2017-09-23 15:18:35 +01:00
1e2e9ea49b Enabled committing during lein release with an unsigned tag. 2017-09-23 15:18:06 +01:00
45dc3017dd And one translation I'd missed... 2017-09-15 21:37:03 +01:00
fbb04e558a Minor fixes to translations 2017-09-15 21:32:28 +01:00
1cfc3876e0 Translations provided by Soukyan 2017-09-15 19:23:31 +01:00
80bb64465b Bringing the default content and the GitHub wiki into line 2017-09-12 17:31:55 +01:00
c4a0d45ade upversion to 1.0.1-SNAPSHOT 2017-09-12 17:07:31 +01:00
1f2bb0bc6d Merge branch 'release/1.0.0' 2017-09-12 17:05:16 +01:00
2cf30bd695 Merge branch 'release/1.0.0' into develop 2017-09-12 17:05:16 +01:00
948d718b3d Upversion to 1.0.0 2017-09-12 17:02:27 +01:00
a509cadd82 Version 0.99.12-SNAPSHOT 2017-09-12 16:58:04 +01:00
0194ff477f Version 0.99.11 2017-09-12 16:44:54 +01:00
009ae30a08 Making new config more robust 2017-09-12 16:29:23 +01:00
a7aca5fab2 Documentation of new configuration. 2017-09-12 12:26:34 +01:00
ee0453e395 Allow configuration to be overridden by environment variables
This is (I think) the final piece in enabling Smeagol to be run neatly from Docker and, indeed, from a pre-packaged jar file.
2017-09-12 11:29:42 +01:00
9152d1fe6d Version 0.99.11-SNAPSHOT 2017-09-12 01:14:45 +01:00
3a4d177923 Version 0.99.10 2017-09-12 01:03:18 +01:00
38bea793c5 Merge branch 'feature/33' into develop 2017-09-12 00:57:32 +01:00
2f4f7aa1c5 #33: Fix 2017-09-12 00:49:16 +01:00
f3883c6b07 Experimental changes to Dockerfile 2017-09-11 13:25:10 +01:00
2f6f9286a3 Tidy-up and robustness; no major functional change. 2017-09-11 13:04:51 +01:00
9f3c16a348 Set version to 0.99.10-SNAPSHOT 2017-09-11 08:05:20 +01:00
ccb62b9d6f Merge branch 'release/1.0.0-rc3' into develop 2017-09-11 07:59:21 +01:00
84 changed files with 4461 additions and 601 deletions

14
.gitignore vendored
View file

@ -17,4 +17,18 @@ pom.xml.asc
smeagol.log* smeagol.log*
/node_modules/ /node_modules/
.DS_Store .DS_Store
.clj-kondo/
.idea/
.calva/
.lsp
.eastwood
smeagol.iml
resources/public/content/uploads/
.eastwood
resources/public/content/Populating a game world.md
*.iml

View file

@ -1,3 +1,29 @@
FROM tomcat:alpine FROM alpine:3.6
COPY target/smeagol-*-standalone.war $CATALINA_HOME/webapps/smeagol.war
MAINTAINER Simon Brooke <simon@journeyman.cc>
ENV JAVA_HOME=/usr/lib/jvm/default-jvm
RUN apk add --no-cache openjdk8 && \
ln -sf "${JAVA_HOME}/bin/"* "/usr/bin/"
# ensure the directories I'm going to write to actually exist!
RUN mkdir -p /usr/local/bin
RUN mkdir -p /usr/local/etc
COPY target/smeagol-*-standalone.jar /usr/local/bin/smeagol.jar
COPY resources/passwd /usr/local/etc/passwd
COPY resources/config.edn /usr/local/etc/config.edn
COPY resources/public/content /usr/local/etc/content
ENV SMEAGOL_CONFIG=/usr/local/etc/config.edn
ENV SMEAGOL_CONTENT_DIR=/usr/local/etc/content
ENV SMEAGOL_PASSWD=/usr/local/etc/passwd
ENV TIMBRE_DEFAULT_STACKTRACE_FONTS="{}"
ENV TIMBRE_LEVEL=':info'
ENV PORT=80
EXPOSE 80
CMD java -jar /usr/local/bin/smeagol.jar

157
README.md
View file

@ -1,162 +1,41 @@
![One wiki to rule them all](https://www.weft.scot/images/smeagol.png) ![One wiki to rule them all](https://www.weft.scot/images/smeagol.png)
# Welcome to Smeagol! # Welcome to Smeagol!
Smeagol is a simple Wiki engine inspired by [Gollum](https://github.com/gollum/gollum/wiki). Gollum is a Wiki engine written in Ruby, which uses a number of simple text formats including [Markdown](http://daringfireball.net/projects/markdown/), and which uses [Git](http://git-scm.com/) to provide versioning and backup. I needed a new Wiki for a project and thought Gollum would be ideal - but unfortunately it doesn't provide user authentication, which I needed, and it was simpler for me to reimplement the bits I did need in Clojure than to modify Gollum. Smeagol is a hackable, extensible Wiki engine which is reasonably user-friendly. It uses [Markdown](http://daringfireball.net/projects/markdown/) as its text format, and [Git](http://git-scm.com/) to provide versioning and backup.
So at this stage Smeagol is a Wiki engine written in Clojure which uses Markdown as its text format, which does have user authentication, and which uses Git as its versioning and backup system. **NOTE** Smeagol is no longer actively maintained; I have moved on to using [Cryogen](http://cryogenweb.org/) as my preferred system for generating websites. There's a lot of good ideas in Smeagol, but doing markdown to HTML conversion every time a document is requested puts load on the server which isn't really merited. If anyone else would like to take on the project, they would be welcome to do so.
<a href="https://zenhub.com"><img src="https://raw.githubusercontent.com/ZenHubIO/support/master/zenhub-badge.png"></a> ## Using Smeagol
Read the [[User Documentation]] for an introduction to all Smeagol's features.
## Status
Smeagol is now a fully working small Wiki engine, and meets my own immediate needs.
## Markup syntax ## Markup syntax
Smeagol uses the Markdown format as provided by [markdown-clj](https://github.com/yogthos/markdown-clj), with the addition that anything enclosed in double square brackets, \[\[like this\]\], will be treated as a link into the wiki itself. Smeagol uses the Markdown format as provided by [markdown-clj](https://github.com/yogthos/markdown-clj), with the addition that anything enclosed in double square brackets, \[\[like this\]\], will be treated as a link into the wiki itself. The markdown format is extensible, and has extensions already for inclusions, for data visualisations and for picture galleries. Read more about [[Extensible Markup]].
### Pluggable extensible markup
A system of pluggable, extensible formatters is supported. In normal markdown, code blocks may be delimited by three backticks at start and end, and often the syntax of the code can be indicated by a token immediately following the opening three backticks. This has been extended to allow custom formatters to be provided for such code blocks. Two example formatters are provided:
#### 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.
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
```
Data files can be uploaded in the same way as images, by using the **upload a file** link.
Note that this visualisation will not be rendered in the GitHub wiki, as it doesn't have Smeagol's data visualisation magic. This is what it should look like:
![Example visualisation](https://github.com/simon-brooke/smeagol/blob/develop/resources/public/data/london.png?raw=true)
#### 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.
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
```
To add your own formatter, compile it into a jar file which is on the classpath - it does *not* have to be part of the Smeagol project directly, and then edit the value of the key `:formatters` in the file `config.edn`; whose standard definition is:
:formatters {"vega" smeagol.formatting/process-vega
"vis" smeagol.formatting/process-vega
"mermaid" smeagol.formatting/process-mermaid}
The added key should be the word which will follow the opening three backticks of your code block, and the value of that key should be a symbol which evaluates to a function which can format the code block as required.
## Security and authentication ## Security and authentication
Security is now greatly improved. There is a file called *passwd* in the *resources* directory, which contains a clojure map which maps usernames to maps with plain-text passwords and emails thus: Smeagol now has good security and authentication. While the initial password supplied with the system is not encrypted, when it is changed it will be; and passwords for new users added through the user administration pages are encrypted. Read more about [[Security and authentication]].
{:admin {:password "admin" :email "admin@localhost" :admin true} ## Internationalisation
:adam {:password "secret" :email "adam@localhost"}} Smeagol has built in internationalisation. Currently it has translation files for English, German, Lithuanian and Russian. We'd welcome volunteers to translate it into other languages.
that is to say, the username is a keyword and the corresponding password is a string. However, since version 0.5.0, users can now change their own passwords, and when the user changes their password their new password is encrypted using the [scrypt](http://www.tarsnap.com/scrypt.html) one-way encryption scheme. The password file is now no longer either in the *resources/public* directory so cannot be downloaded through the browser, nor in the git archive to which the Wiki content is stored, so that even if that git archive is remotely clonable an attacker cannot get the password file that way.
## Images ## Images
You can (if you're logged in) upload files, including images, using the **Upload a file** link on the top menu bar. You can link to an uploaded image, or other images already available on the web, like this: You can (if you're logged in) upload files, including images, using the **Upload a file** link on the top menu bar. You can link to an uploaded image, or to other images already available on the web, like this:
![Smeagol](http://vignette3.wikia.nocookie.net/lotr/images/e/e1/Gollum_Render.png/revision/latest?cb=20141218075509) ![Smeagol](http://vignette3.wikia.nocookie.net/lotr/images/e/e1/Gollum_Render.png/revision/latest?cb=20141218075509)
## Advertisement ## Running Smeagol
If you like what you see here, I am available for work on open source Clojure projects. You can run Smeagol from the [[Docker Image]]; alternatively you can run it from an executable jar file or as a war file in a servlet container. Read how about [[Configuring Smeagol]] and [[Deploying Smeagol]].
### Phoning home ## Developing Smeagol
Smeagol currently requests the WEFT logo in the page footer from my home site. This is mainly so I can get a feel for how many people are using the product. If you object to this, edit the file Smeagol is an open source project; you're entitled to make changes yourself. Read more about [[Developing Smeagol]].
resources/templates/base.html
and replace the line
<img height="16" width="16" alt="The Web Engineering Factory &amp; Toolworks" src="http://www.weft.scot/images/weft.logo.64.png"> Developed by <a href="http://www.weft.scot/">WEFT</a>
with the line
<img height="16" width="16" alt="The Web Engineering Factory &amp; Toolworks" src="img/weft.logo.64.png"> Developed by <a href="http://www.weft.scot/">WEFT</a>
## License ## License
Copyright © 2014-2015 Simon Brooke. Licensed under the GNU General Public License, Copyright © 2014-2020 Simon Brooke. Licensed under the GNU General Public License,
version 2.0 or (at your option) any later version. If you wish to incorporate version 2.0 or (at your option) any later version. If you wish to incorporate
parts of Smeagol into another open source project which uses a less restrictive parts of Smeagol into another open source project which uses a less restrictive
license, please contact me; I'm open to dual licensing it. license, please contact me; I'm open to dual licensing it.
## Prerequisites ## Phoning home
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed. Smeagol does currently fetch one image from my home site. Read more about [[Phoning Home]], and how to prevent it (if you want to).
You will need [node](https://nodejs.org/en/) and [bower](https://bower.io/) installed. ## Advertisement
If you like what you see here, I am available for work on open source Clojure projects.
## Running
To start a web server for the application, run:
lein bower install
lein ring server
Alternatively, if you want to deploy to a servlet container (which I would strongly recommend), the simplest thing is to run:
lein bower install
lein ring uberwar
(a command which I'm sure Smeagol would entirely appreciate) and deploy the resulting war file.
## Experimental Docker image
You can now run Smeagol as a [Docker](http://www.docker.com) image. To run my Docker image, use
docker run simonbrooke/smeagol
Smeagol will run, obviously, on the IP address of your Docker image, on port 8080. To find the IP address, start the image using the command above and then use
docker inspect --format '{{ .NetworkSettings.IPAddress }}' $(docker ps -q)
Suppose this prints '10.10.10.10', then the URL to browse to will be http://10.10.10.10:8080/smeagol/
This image is _experimental_, but it does seem to work fairly well. What it does **not** yet do, however, is push the git repository to a remote location, so when you tear the Docker image down your edits will be lost. My next objective for this image is for it to have a cammand line parameter being the git address of a repository from which it can initialise the Wiki content, and to which it will periodically push local changes to the Wiki content.
To build your own Docker image, run:
lein clean
lein bower install
lein ring uberwar
lein docker build
This will build a new Docker image locally; you can, obviously, push it to your own Docker repository if you wish.

15
doc/include.md Normal file
View file

@ -0,0 +1,15 @@
# Include Feature
## Requirements
The user can include page title, abstract or the whole content in a given page. Headings and enumerations can be indented by a given include-level.
## Thoughts & Questions
* Which include syntax should be used?
* page include can be definde alongsite of image includes - sth. like
`&[:indent-heading s/Num :indent-list s/Num](relative or absolute url s/Str)`
* Which kind of urls should we accept for page includes?
* relative local urls (we will need some care to prohibit directory traversal ...)
* absolute github / gitlab / gitblit urls without authentication.
* Which metadata can be used for title / abstract ?
* MultiMarcdown-Metadata is supported by clj-markdown :-)
* How can we test page includes?
* we will need a content resolver component for testing and at least one for production resolving.

1339
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,33 +1,38 @@
(defproject smeagol "1.0.0-rc3" (defproject smeagol "1.0.4a"
: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.taoensso/encore "2.92.0"] [com.taoensso/encore "2.92.0"]
[com.cemerick/url "0.1.1"]
[com.taoensso/timbre "4.10.0"] [com.taoensso/timbre "4.10.0"]
[com.fzakaria/slf4j-timbre "0.3.7"]
[com.taoensso/tower "3.0.2" :exclusions [com.taoensso/encore]] [com.taoensso/tower "3.0.2" :exclusions [com.taoensso/encore]]
[crypto-password "0.2.0"] [crypto-password "0.2.0"]
[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.10.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"]
[org.slf4j/jcl-over-slf4j "1.7.25"] [org.slf4j/jcl-over-slf4j "1.7.25"]
[prismatic/schema "1.1.9"]
[prone "1.1.4"] [prone "1.1.4"]
[ring/ring-anti-forgery "1.1.0"] [ring/ring-anti-forgery "1.1.0"]
[ring-server "0.4.0"] [ring-server "0.4.0"]
@ -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-ring "0.8.13" :exclusions [org.clojure/clojure]]] [lein-npm "0.6.2"]
[lein-ring "0.12.5" :exclusions [org.clojure/clojure]]]
:bower-dependencies [[simplemde "1.11.2"] :npm {:dependencies [[mermaid "8.13.8"]
;; [vega-embed "3.0.0-beta.20"] ;; vega-embed currently not loaded from Bower because of [photoswipe "4.1.3"]
;; dependency conflict which will hopefully be resolved soon. [simplemde "1.11.2"]
[vega-lite "2.0.0-beta.11"] [tablesort "5.3.0"]
[mermaid "6.0.0"]] [vega "5.21.0"]
[vega-embed "6.20.5"]
[vega-lite "5.2.0"]]
:root "resources/public/vendor"}
:docker {:image-name "simonbrooke/smeagol" :docker {:image-name "simonbrooke/smeagol"
:dockerfile "Dockerfile"} :dockerfile "Dockerfile"}
@ -58,13 +66,19 @@
:init smeagol.handler/init :init smeagol.handler/init
:destroy smeagol.handler/destroy} :destroy smeagol.handler/destroy}
;; for the time being, I'm not sure that I want to formally deploy this anywhere, and I certainly don't feel
;; it's fair to clutter clojars.org with it.
:deploy-repositories [["releases" "file:/tmp"]
["snapshots" "file:/tmp"]]
:release-tasks [["vcs" "assert-committed"] :release-tasks [["vcs" "assert-committed"]
["clean"]
["codox"]
["change" "version" "leiningen.release/bump-version" "release"] ["change" "version" "leiningen.release/bump-version" "release"]
["vcs" "commit"] ["vcs" "commit"]
;; ["vcs" "tag"] -- not working, problems with secret key
["clean"]
["bower" "install"] ["bower" "install"]
["ring" "uberwar"] ["ring" "uberjar"]
["deploy"]
["docker" "build"] ["docker" "build"]
["docker" "push"] ["docker" "push"]
["change" "version" "leiningen.release/bump-version"] ["change" "version" "leiningen.release/bump-version"]

View file

@ -22,14 +22,56 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; config.edn: a simple configuration map for Smeagol; inspired by Cryogen. ;;; config.edn: a simple configuration map for Smeagol; inspired by Cryogen.
;;; This is top-level configuration. ;;; This is top-level configuration. All values can be overridden with
;;; environment variables.
;; ; ; ; ; ; ; ; ; ; ;; ; ; ; ; ; ; ; ; ;
{ {
:site-title "Smeagol" ;; overall title of the site, used in page headings :content-dir "resources/public/content"
:default-locale "en-GB" ;; default language used for messages ;; where content is served from.
:formatters {"vega" smeagol.formatting/process-vega :default-locale "en-GB" ;; default language used for messages
"vis" smeagol.formatting/process-vega :extensions-from :local ;; where to load JavaScript libraries
"mermaid" smeagol.formatting/process-mermaid ;; from: options are :local, :remote.
"backticks" smeagol.formatting/process-backticks} :formatters ;; formatters for processing markdown
} ;; extensions.
{:backticks {:formatter "smeagol.formatting/process-backticks"
:scripts {}
:styles {}}
:mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid"
:scripts {:core {:local "vendor/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
;; :trace :debug :info :warn :error :fatal
: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
;; 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.
}
}

144
resources/i18n/de-DE.edn Normal file
View file

@ -0,0 +1,144 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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.
;;;;
;;;; German language translation contributed by and
;;;; Copyright (C) 2017 Soukyan Blackwood <priority.inc@gmail.com>
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; en-GB.edn: English-language messages.
;;; This is essentially all the text in the chrome - that which isn't editable
;;; through the wiki itself; and the test in the sanity check report.
;; ; ; ; ; ; ; ; ; ;
{:add-user-label "Neuen Benutzer zufügen" ;; label for the add user link on edit users page
:change-pass-label "Kennwort ändern!"
;; text of the change password widget itself on the
;; change password page
:change-pass-link "Kennwort ändern"
;; text of the change password link on the menu
:change-pass-prompt "Um Ihr Kennwort zu ändern"
;; text of the change password widget prompt on the
;; change password page
:change-col-hdr "Änderungen" ;; header for the changes column in history
:chpass-bad-match "Die von Ihnen vorgeschlagenen Kennwörter stimmen nicht überein"
;; error text if proposed passwords don't match
:chpass-fail "Ihr Kennwort wurde nicht geändert"
;; error text on fail other htan too short or bad match
:chpass-success "Ihr Kennwort wurde geändert"
;; confirmation text on password change
:chpass-too-short "Das von Ihnen vorgeschlagene Kennwort war zu kurz: 8 Zeichen erforderlich"
;; error text if proposed password is too short
:chpass-title-prefix "Kennwort ändern für"
;; prefix for title of change password page
:content-dir "Das Inhaltsverzeichnis"
;; used in sanity check report
:content-dir-exists "Das Inhaltsverzeichnis existiert"
;; used in sanity check report
:content-dir-is-dir "Das Inhaltsverzeichnis ist ein Verzeichnis"
;; used in sanity check report
:cookies-about "Über Cookies" ;; about cookies text
:cookies-more "Es wird von Ihrem Browser gelöscht, sobald Sie diese Seite verlassen. Diese Webseite benutzt keine Cookies von dritten Parteien, daher kann Ihr Besuch hier nicht von anderen Seiten verfolgt werden."
;; more about cookies text
:default-page-title "Einleitung" ;; title of the default page in this wiki
:del-col-hdr "Löschen" ;; header for delete column on edit users page
:del-user-fail "Könnte Benutzer nicht löschen"
;; error message on failure to delete user
:del-user-success "Benutzer wurde erfolgreich gelöscht"
;; confirmation message on deletion of user
:diff-title-prefix "Änderungen seit der Version"
;; prefix for the header of the changes page
:does-not-exist "existiert nicht"
;; (of a file or directory); used in sanity check report
:edit-col-hdr "Bearbeiten" ;; header for edit column on edit users page
:edit-page-link "Diese Seite bearbeiten"
;; text of the edit page link on the content frame
:edit-title-prefix "Bearbeiten" ;; prefix for title of edit content page
:edit-users-link "Benutzer bearbeiten" ;; text of the edit users link on the menu
:edit-users-title "Benutzer zum Bearbeiten auswählen"
;; title of edit users page
:email-prompt "E-Mail Adresse" ;; text of the email widget prompt on edit user page
:file-or-directory "Datei oder Verzeichnis"
;; used in sanity check report
:file-summary-prompt "Beschreibung/was wurde gändert"
;; prompt for the file upload summary input
:file-upload-link-text "Sie können auf diese Datei verweisen, mit Hilfe von einer Verknüpfung von der Form"
;; Text introducing the link to an uploaded file
:file-upload-prompt "Datei zum Hochladen" ;; prompt string for the file upload widget
:file-upload-title "Upload a file" ;; title for the file upload page
:is-admin-prompt "Ist Administrator?"
:here "hier" ;; used in sanity check report
:home-link "Startseite" ;; text of the home link on the menu
:is-not-directory "ist kein Verzeichnis"
;; (of a file or directory) used in sanity check report
:is-not-readable "ist nicht lesbar"
;; (of a file or directory) used in sanity check report
:is-not-writable "ist nicht schreibbar"
;; (of a file or directory) used in sanity check report
:login-label "Log in!" ;; text of the login widget on the login page
:login-link "Log in" ;; text of the login link on the menu
:login-prompt "Um dieses Wiki zu bearbeiten"
;; text of the action widget prompt on the login page
:logout-label "Abmelden!" ;; text of the logout widget on the logout page
:logout-link "Abmelden" ;; text of the logout link on the menu
:logged-in-as "Sie sind angemeldet als"
;; text of the 'logged in as' label on the menu
:history-link "Historie" ;; text of the history link on the content frame
:history-title-prefix "Historie von" ;; prefix of the title on the history page
:new-pass-prompt "Neues Kennwort" ;; text of the new password widget prompt on the change
;; password and edit user pages
:no-admin-users "In der Datei 'passwd' gibt es keine Benutzer mit Administratorrechten"
;; used in sanity check report
:old-pass-prompt "Ihr Kennwort"
;; text of the old password widget prompt on the change
;; password page, and password widget on login page
:password-file "Die Kennwort ('passwd') Datei"
;; used in sanity check report
:problems-found "Probleme gefunden"
;; used in sanity check report
:rpt-pass-prompt "und noch einmal" ;; text of the new password widget prompt on the change
;; password and edit user pages
:save-prompt "Wenn Sie mit dem Bearbeiten fertig sind"
;; text of the save widget label on edit content
;; and edit user page
:save-label "Speichern!" ;; text of the save widget itself
:save-user-fail "Benutzer konnte nicht gespeichert werden"
:save-user-success "Benutzer erfolgreich gespeichert"
:see-documentation "Für mehr Information sehen Sie bitte die Dokumentation "
;; used in sanity check report
:smeagol-not-initialised
"Smeagol ist nicht richtig initialisiert"
;; title of the sanity check report
:smeagol-misconfiguration
"Smeagol konnte einige der Ressourcen, von denen es abhängt,
nicht finden; wahrscheinlich wegen einer Fehlkonfiguration oder fehlenden Umgebungsvariablen."
;; used in sanity check report
:user-lacks-field "Das Feld für den Benutzereintrag in die passwd Datei fehlt"
;; used in sanity check report
:username-prompt "Benutzername" ;; text of the username widget prompt on edit user page
;; text of the is admin widget prompt on edit user page
:user-title-prefix "Benutzer bearbeiten" ;; prefix for title of edit user page
:vers-col-hdr "Version" ;; header for the version column in history
:what-col-hdr "Wass" ;; header for the what column in history
:what-changed-prompt "Was haben Sie verändert?"
;; text of the summary widget prompt on edit
;; content page
:when-col-hdr "Was" ;; header for the when column in history
:your-uname-prompt "Ihr Benutzername" ;; text of the username widget prompt on the login page
}

1
resources/i18n/de.edn Symbolic link
View file

@ -0,0 +1 @@
de-DE.edn

View file

@ -42,7 +42,7 @@
;; error text on fail other htan too short or bad match ;; error text on fail other htan too short or bad match
:chpass-success "Your password was changed" :chpass-success "Your password was changed"
;; confirmation text on password change ;; confirmation text on password change
:chpass-too-short "You proposed password wasn't long enough: eight characters required" :chpass-too-short "Your proposed password wasn't long enough: eight characters required"
;; error text if proposed password is too short ;; error text if proposed password is too short
:chpass-title-prefix "Change password for" :chpass-title-prefix "Change password for"
;; prefix for title of change password page ;; prefix for title of change password page
@ -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
resources/i18n/lt.edn Symbolic link
View file

@ -0,0 +1 @@
lt_LT.edn

143
resources/i18n/lt_LT.edn Normal file
View file

@ -0,0 +1,143 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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.
;;;;
;;;; Lithuanian language translation contributed by and
;;;; Copyright (C) 2017 Soukyan Blackwood <priority.inc@gmail.com>
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; en-GB.edn: English-language messages.
;;; This is essentially all the text in the chrome - that which isn't editable
;;; through the wiki itself; and the test in the sanity check report.
;; ; ; ; ; ; ; ; ; ;
{:add-user-label "Pridėti naują naudotoją" ;; label for the add user link on edit users page
:change-pass-label "Pakeiskite slaptažodį!"
;; text of the change password widget itself on the
;; change password page
:change-pass-link "Pakeiskite slaptažodį"
;; text of the change password link on the menu
:change-pass-prompt "Pakeisti slaptažodį"
;; text of the change password widget prompt on the
;; change password page
:change-col-hdr "Pakeitimai" ;; header for the changes column in history
:chpass-bad-match "Jūsų siūlomi slaptažodžiai - nesutampa"
;; error text if proposed passwords don't match
:chpass-fail "Jūsų slaptažodis nebuvo pakeistas"
;; error text on fail other htan too short or bad match
:chpass-success "Jūsų slaptažodis buvo pakeistas"
;; confirmation text on password change
:chpass-too-short "Jūsų siūlomas slaptožodis per trumpas: reikia bent aštuonių ženklų"
;; error text if proposed password is too short
:chpass-title-prefix "Pakeisti slaptažodį, dėl"
;; prefix for title of change password page
:content-dir "Turinio katalogas"
;; used in sanity check report
:content-dir-exists "Turinio katalogas egzistuoja"
;; used in sanity check report
:content-dir-is-dir "Turinio katalogas yra katalogas"
;; used in sanity check report
:cookies-about "Apie slapukus" ;; about cookies text
:cookies-more "Šis puslapis saugo jūsų sesijų informaciją jūsų naršyklėje “slapukų” forma. Tai mums leidžia jums rodyti tik tai, ką norite matyti. Slapukai neturi jokių jūsų identifikavimo duomenų ir kiti puslapiai negali jų “perskaityti”. Šie slapukai ištrinami iš karto, vos jūs išjungiate šį puslapį. Šis puslapis nenaudoja jokių trečiųjų asmenų slapukų, ir jūsų apsilankymas čia negali būti atsektas jokio kito puslapio."
;; more about cookies text
:default-page-title "Pristatymas" ;; title of the default page in this wiki
:del-col-hdr "Ištrinti" ;; header for delete column on edit users page
:del-user-fail "Naudotojas negalėjo būti ištrintas"
;; error message on failure to delete user
:del-user-success "Naudotojas sėkmingai ištrintas"
;; confirmation message on deletion of user
:diff-title-prefix "Pakeitimai nuo versijos"
;; prefix for the header of the changes page
:does-not-exist "neegzsituoja"
;; (of a file or directory); used in sanity check report
:edit-col-hdr "Keisti" ;; header for edit column on edit users page
:edit-page-link "Keisti šį puslapį"
;; text of the edit page link on the content frame
:edit-title-prefix "Keisti" ;; prefix for title of edit content page
:edit-users-link "Keisti naudotojus" ;; text of the edit users link on the menu
:edit-users-title "Pasirinkti naudotojus keitimui"
;; title of edit users page
:email-prompt "el. Paštas" ;; text of the email widget prompt on edit user page
:file-or-directory "Failas ar katalogas"
;; used in sanity check report
:file-summary-prompt "Aprašymas/kas pakeista"
;; prompt for the file upload summary input
:file-upload-link-text "Galite nukreipti į šį failą naudodami formos nuorodą"
;; Text introducing the link to an uploaded file
:file-upload-prompt "Failas įkėlimui" ;; prompt string for the file upload widget
:file-upload-title "Įkelti failą" ;; title for the file upload page
:is-admin-prompt "Administratorius?"
:here "čia" ;; used in sanity check report
:home-link "Pradinis" ;; text of the home link on the menu
:is-not-directory "ne katalogas"
;; (of a file or directory) used in sanity check report
:is-not-readable "neperskaitomas"
;; (of a file or directory) used in sanity check report
:is-not-writable "nerašomas"
;; (of a file or directory) used in sanity check report
:login-label "Prisijunkite!" ;; text of the login widget on the login page
:login-link "Prisijunkite" ;; text of the login link on the menu
:login-prompt "Pakeisti šį viki"
;; text of the action widget prompt on the login page
:logout-label "Atsijunkite!" ;; text of the logout widget on the logout page
:logout-link "Atsijunkite" ;; text of the logout link on the menu
:logged-in-as "Jūs prisijungęs, kaip"
;; text of the 'logged in as' label on the menu
:history-link "Istorija" ;; text of the history link on the content frame
:history-title-prefix "Istorija apie" ;; prefix of the title on the history page
:new-pass-prompt "Naujas slaptažodis" ;; text of the new password widget prompt on the change
;; password and edit user pages
:no-admin-users "Naudotojų passwd faile su administatoriaus privilegijomis nėra"
;; used in sanity check report
:old-pass-prompt "Jūsų slaptažodis"
;; text of the old password widget prompt on the change
;; password page, and password widget on login page
:password-file "slaptažodžio (passwd) failas"
;; used in sanity check report
:problems-found "rasta problemų"
;; used in sanity check report
:rpt-pass-prompt "Ir dar kartą" ;; text of the new password widget prompt on the change
;; password and edit user pages
:save-prompt "Kai baigsite redaguoti"
;; text of the save widget label on edit content
;; and edit user page
:save-label "Išsaugokite!" ;; text of the save widget itself
:save-user-fail "Nepavyko išsaugoti naudotojo"
:save-user-success "Naudotojas sėkmingai išsaugotas"
:see-documentation "Daugiau informacijos ieškokite dokumentacijoje "
;; used in sanity check report
:smeagol-not-initialised
"Smygolas buvo blogai paleistas"
;; title of the sanity check report
:smeagol-misconfiguration
"Smygolas nerado kai kurių jam reikalingų resursų, taip nutikti galėjo dėl neteisingų nustatymų, arba trūkstamų aplinkos kintamųjų"
;; used in sanity check report
:user-lacks-field "Naudotojo passwd failo įraše trūksta laukelio"
;; used in sanity check report
:username-prompt "Naudotojo vardas" ;; text of the username widget prompt on edit user page
;; text of the is admin widget prompt on edit user page
:user-title-prefix "Pakeisti naudotoją" ;; prefix for title of edit user page
:vers-col-hdr "Versija" ;; header for the version column in history
:what-col-hdr "Kas" ;; header for the what column in history
:what-changed-prompt "Ką pakeitėte?"
;; text of the summary widget prompt on edit
;; content page
:when-col-hdr "Kada" ;; header for the when column in history
:your-uname-prompt "Jūsų naudotojo vardas" ;; text of the username widget prompt on the login page
}

1
resources/i18n/ru.edn Symbolic link
View file

@ -0,0 +1 @@
ru_RU.edn

144
resources/i18n/ru_RU.edn Normal file
View file

@ -0,0 +1,144 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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.
;;;;
;;;; Russian language translation contributed by and
;;;; Copyright (C) 2017 Soukyan Blackwood <priority.inc@gmail.com>
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; en-GB.edn: English-language messages.
;;; This is essentially all the text in the chrome - that which isn't editable
;;; through the wiki itself; and the test in the sanity check report.
;; ; ; ; ; ; ; ; ; ;
{:add-user-label "добавить нового пользователя" ;; label for the add user link on edit users page
:change-pass-label "изменить пароль!"
;; text of the change password widget itself on the
;; change password page
:change-pass-link "изменить пароль"
;; text of the change password link on the menu
:change-pass-prompt "Чтобы изменить пароль"
;; text of the change password widget prompt on the
;; change password page
:change-col-hdr "изменения" ;; header for the changes column in history
:chpass-bad-match "Ваши предложенные пароли не совпадают"
;; error text if proposed passwords don't match
:chpass-fail "Ваш пароль не был изменён"
;; error text on fail other htan too short or bad match
:chpass-success "Ваш пароль был изменён"
;; confirmation text on password change
:chpass-too-short "Вы предложили пароль недостаточно длины: требуется восемь символов"
;; error text if proposed password is too short
:chpass-title-prefix "Изменить пароль для"
;; prefix for title of change password page
:content-dir "Каталог содержимого"
;; used in sanity check report
:content-dir-exists "Каталог содержимого существуе"
;; used in sanity check report
:content-dir-is-dir "Каталог содержимого - это каталог"
;; used in sanity check report
:cookies-about "Об кукисах" ;; about cookies text
:cookies-more "Этот сайт хранит информацию о сеансе как «cookie» в вашем браузере. Это поможет нам показать вам контент, который вы хотите увидеть. Этот файл cookie не идентифицирует вас и не может быть прочитан другими веб-сайтами. Он удаляется браузером, как только вы покидаете этот сайт. Этот веб-сайт не использует сторонние файлы cookie, поэтому ваш визит здесь не может быть отслежен другими веб-сайтами"
;; more about cookies text
:default-page-title "представление" ;; title of the default page in this wiki
:del-col-hdr "Удалить" ;; header for delete column on edit users page
:del-user-fail "Не удалось удалить пользователя"
;; error message on failure to delete user
:del-user-success "успешно удалён пользователь"
;; confirmation message on deletion of user
:diff-title-prefix "Изменения с версии"
;; prefix for the header of the changes page
:does-not-exist "не существует"
;; (of a file or directory); used in sanity check report
:edit-col-hdr "редактировать" ;; header for edit column on edit users page
:edit-page-link "Редактировать эту страницу"
;; text of the edit page link on the content frame
:edit-title-prefix "редактировать" ;; prefix for title of edit content page
:edit-users-link "Редактировать пользователей" ;; text of the edit users link on the menu
:edit-users-title "Выберите пользователя для редактирования"
;; title of edit users page
:email-prompt "Адрес электронной почты" ;; text of the email widget prompt on edit user page
:file-or-directory "Файл или каталог"
;; used in sanity check report
:file-summary-prompt "Описание / что изменилось"
;; prompt for the file upload summary input
:file-upload-link-text "Вы можете ссылать этот файл, используя ссылку формы"
;; Text introducing the link to an uploaded file
:file-upload-prompt "Файл для загрузки" ;; prompt string for the file upload widget
:file-upload-title "Загрузить файл" ;; title for the file upload page
:is-admin-prompt "Администратор?"
:here "здесь" ;; used in sanity check report
:home-link "Главная" ;; text of the home link on the menu
:is-not-directory "не является каталогом"
;; (of a file or directory) used in sanity check report
:is-not-readable "не читаемый"
;; (of a file or directory) used in sanity check report
:is-not-writable "недоступен для записи"
;; (of a file or directory) used in sanity check report
:login-label "Вход!" ;; text of the login widget on the login page
:login-link "Вход" ;; text of the login link on the menu
:login-prompt "Чтобы отредактировать эту вики"
;; text of the action widget prompt on the login page
:logout-label "Выйти!" ;; text of the logout widget on the logout page
:logout-link "Выйти" ;; text of the logout link on the menu
:logged-in-as "Вы вошли как"
;; text of the 'logged in as' label on the menu
:history-link "история" ;; text of the history link on the content frame
:history-title-prefix "История об" ;; prefix of the title on the history page
:new-pass-prompt "Новый пароль" ;; text of the new password widget prompt on the change
;; password and edit user pages
:no-admin-users "В файле 'passwd' нет пользователей с правами администратора"
;; used in sanity check report
:old-pass-prompt "Ваш пароль"
;; text of the old password widget prompt on the change
;; password page, and password widget on login page
:password-file "файл пароля ('passwd')"
;; used in sanity check report
:problems-found "проблемы были найдены"
;; used in sanity check report
:rpt-pass-prompt "И опять" ;; text of the new password widget prompt on the change
;; password and edit user pages
:save-prompt "Когда вы закончили редактирование"
;; text of the save widget label on edit content
;; and edit user page
:save-label "Сохранить!" ;; text of the save widget itself
:save-user-fail "Не удалось сохранить пользователя"
:save-user-success "Успешно сохранённый пользователь"
:see-documentation "для получения дополнительной информации смотрите документацию "
;; used in sanity check report
:smeagol-not-initialised
"Смеаголь неправильно инициализирован"
;; title of the sanity check report
:smeagol-misconfiguration
"Смеаголь не смог найти некоторые ресурсы, от которых это зависит,
возможно, из-за неправильной конфигурации или отсутствующих переменных среды."
;; used in sanity check report
:user-lacks-field "В пользовательской записи в файле passwd отсутствует поле"
;; used in sanity check report
:username-prompt "Имя пользователя" ;; text of the username widget prompt on edit user page
;; text of the is admin widget prompt on edit user page
:user-title-prefix "Изменить пользователя" ;; prefix for title of edit user page
:vers-col-hdr "Версия" ;; header for the version column in history
:what-col-hdr "Что" ;; header for the what column in history
:what-changed-prompt "Что вы изменили?"
;; text of the summary widget prompt on edit
;; content page
:when-col-hdr "когда" ;; header for the when column in history
:your-uname-prompt "Ваш логин" ;; text of the username widget prompt on the login page
}

View file

@ -1 +1 @@
{:admin {:admin true, :email "info@weft.scot", :password "admin"}, :jenny {:email "jenny@auchencairn.org", :admin false, :password "$s0$f0801$1uniQfftB37G5e5GklJANQ==$kQ0+/YcCuaz2x5iYjwhNlDlnWX/exE/8pSC+R4C0WvQ="}} {:admin {:admin true, :email "info@weft.scot", :password "admin"}}

View file

@ -0,0 +1,14 @@
# 1.0.4
* New gallery extension `pswp` using [Photoswipe](https://photoswipe.com/);
* Automatic thumbnailing of uploaded images;
* Javascript supporting extensions is only loaded for those pages on which the extensions are actually used;
* Many minor bug fixes.
# 1.0.2
Two fixes contributed by a user:
* [Configurable start page](https://github.com/journeyman-cc/smeagol/commit/b1eeecde1d0555e9b708807c63e28fac21de58c4)
* [Fixed uploads](https://github.com/journeyman-cc/smeagol/commit/1136e792d4810fd9af8b443fdea6d6be4aeda79e)
Many thanks to [M Jerger](https://github.com/jerger) for these.

View file

@ -1,20 +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
:formatters {"vega" smeagol.formatting/process-vega
"vis" smeagol.formatting/process-vega
"mermaid" smeagol.formatting/process-mermaid
"backticks" smeagol.formatting/process-backticks}
}
The three keys given above should be present. The values should be:
* **:site-title** The title for your wiki
* **: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.
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.

View file

@ -0,0 +1,112 @@
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
:extensions-from :local ;; where to load JavaScript libraries
;; from: options are :local, :remote.
:formatters ;; formatters for processing markdown
;; extensions.
{:backticks {:formatter "smeagol.formatting/process-backticks"
:scripts {}
:styles {}}
:mermaid {:formatter "smeagol.extensions.mermaid/process-mermaid"
:scripts {:core {:local "vendor/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
;; :trace :debug :info :warn :error :fatal
: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`.
## :extensions-from
Where javascript support for extensions should be loaded from. Valid values are `:local` and `:remote`; if `:remote` is chosen, they will generally be loaded from [CDNJS](https://cdnjs.com/).
For an intranet site with limited outside bandwidth, or if you are particularly concerned about security, choose `:local`; otherwise, `:remote` will result in faster loading of your pages.
This parameter may be overridden with the environment variable `SMEAGOL_JS_FROM`.
## :formatters
Specifications for formatters for markup extensions.
For each extension, a map is stored with a key `:formatter`, whose value is the fully qualified name of the Clojure function providing the extension, `:scripts` and `:styles`, each of which hava one additional key for each JavaScript (in the case of `:scripts`) or CSS (in the case of `:styles`) file required by the plugin. Each of these per-file keys points to a final further map, with keys `:local` and `:remote`, whose values are URLs - relative, in the case of the `:local` key, absolute in the case of the `:remote`, pointing to where the required resource can be fetched from.
This parameter may be overridden with the environment variable `SMEAGOL_FORMATTERS`, but you'd be pretty unwise to do so unless to disable formatters altogether. Otherwise, editing the `config.edn` file would be far more convenient.
## :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`.
## :passwd
The value of this key should be the path to the password file used to authenticate users. It should **NOT** be in the content directory! For most deployments it should be a file elsewhere in the file system, but it must be readable and writable by the account which runs the process serving Smeagol.
This parameter may be overridden with the environment variable `SMEAGOL_PASSWD`.
## :site-title
The value of this key should be the overall title of the site, which is used in page headings.
This parameter may be overridden with the environment variable `SMEAGOL_SITE_TITLE`.
## :start-page
The value of this key should be the name (without the `.md` extension) of the page to show when a user visits the base URL of your Smeagol installation.
## :thumbnails
The value of this key should be a map. Keys in this map should have values which are integers. By default, the key `:small` is bound to `64` and the key `:med` to 400. When an image file is uploaded, it is stored at the resolution you uploaded; but for every key in the `:thumbnails` map whose value is larger than the larger dimension of the uploaded file, scaled copies will also be stored in those sizes.

View file

@ -1,3 +1,8 @@
## Choosing a deployment mechanism
There are currently three ways you can deploy Smeagol: as an executable Jar file, as a Docker image, and as a web-app in a [Servlet container](https://en.wikipedia.org/wiki/Web_container). Each method has advantages and disadvantages.
The Jar file is extremely easy to deploy and to configure, but cannot currently serve [HTTPS](https://en.wikipedia.org/wiki/HTTPS), which, on the modern web, is a significant disadvantage. The Docker image is just a wrapper around the Jar file; it's particularly suitable for automated deployment. The web-app solution offloads responsibility for things like HTTPS to the Servlet container, and consequently can be much more secure; but it can really only be configured at compile time.
## Deploying as a stand-alone application ## Deploying as a stand-alone application
To deploy Smeagol as a stand-alone application, either download the jar file for the release you want to deploy, or clone the source and compile it with: To deploy Smeagol as a stand-alone application, either download the jar file for the release you want to deploy, or clone the source and compile it with:
@ -6,19 +11,26 @@ To deploy Smeagol as a stand-alone application, either download the jar file for
This will create a jar file in the `target` directory, named `smeagol-`*VERSION*`-standalone.jar`. This will create a jar file in the `target` directory, named `smeagol-`*VERSION*`-standalone.jar`.
Smeagol cannot access either its configuration or its content from the jar file, as otherwise they would not be editable. Consequently you should set up three environment variables: Smeagol cannot access either its configuration or its content from the jar file, as otherwise they would not be editable. There are three solutions to this:
1. `SMEAGOL_CONFIG` should be the full or relative pathname of a Smeagol [[Configuration]] file; ### Custom configuration file
2. `SMEAGOL_CONTENT_DIR` should be the full or relative pathname of the directory from which Smeagol should serve content (which may initially be empty, but must be writable by the process which runs Smeagol)' You can copy the standard configuration file `resources/config.edn` to somewhere outside the jar file, edit it to suit your installation, and set up a single environment variable, `SMEAGOL_CONFIG`, whose value is the path to your new configuration file.
3. `SMEAGOL_PASSWD` should be the full or relative pathname of a Smeagol Passwd file - see [[Security and authentication]]. This file must contain an entry for at least your initial user, and, if you want to administer users through the user interface, must be writable by the process which runs Smeagol.
**NOTE** that `SMEAGOL_CONTENT_DIR` must contain at least the following files: ### Environment variables
Alternatively, you can configure everything through [[Environment Variables]].
### Hybrid strategy
You can have both a configuration file and environment variables. If you do this, the environment variables override the values in the configuration file.
### Necessary content
**NOTE** that the directory at `SMEAGOL_CONTENT_DIR` must contain at least the following files:
1. `_edit-side-bar.md` - the side-bar that should be displayed when editing pages; 1. `_edit-side-bar.md` - the side-bar that should be displayed when editing pages;
2. `_header.md` - the header to be displayed on all pages; 2. `_header.md` - the header to be displayed on all pages;
3. `_side-bar.md` - the side-bar that should be displayed when not editing pages. 3. `_side-bar.md` - the side-bar that should be displayed when not editing pages.
Standard versions of these files can be found in the [source repository](https://github.com/journeyman-cc/smeagol/tree/master/resources/public/content). All these files should be in markdown format - see [[Extensible Markup]]. Standard versions of these files can be found in the [source repository](https://github.com/journeyman-cc/smeagol/tree/master/resources/public/content).
You can run the jar file with: You can run the jar file with:
@ -32,29 +44,12 @@ To deploy Smeagol within a servlet container, either download the jar file for t
This will create a war file in the `target` directory, named `smeagol-`*VERSION*`-standalone.war`. Deploy this to your servlet container in the normal way; details will depend on your container. Instructions for Tomcat are [here](https://tomcat.apache.org/tomcat-8.0-doc/deployer-howto.html). This will create a war file in the `target` directory, named `smeagol-`*VERSION*`-standalone.war`. Deploy this to your servlet container in the normal way; details will depend on your container. Instructions for Tomcat are [here](https://tomcat.apache.org/tomcat-8.0-doc/deployer-howto.html).
The problem with this is that unless the environment variables (see above) were already set up in the environment of the servlet container at the time when the servlet container were launched, Smeagol will run with its built-in defaults. This will run perfectly satisfactorily provided your servlet container is configured to unpack war files, which most are. The problem with this is that unless the environment variables (see above) were already set up in the environment of the servlet container at the time when the servlet container were launched, Smeagol will run with its built-in defaults. If you want to change the defaults, you would have to edit the `resources/config.edn` file and recompile the war file.
Smeagol will run as a web-app with the default configuration perfectly satisfactorily.
## Experimental Docker image ## Experimental Docker image
You can now run Smeagol as a [Docker](http://www.docker.com) image. Read more about [[Using the Docker Image]]. You can now run Smeagol as a [Docker](http://www.docker.com) image. Read more about using the [[Docker Image]].
To run my Docker image, use
docker run simonbrooke/smeagol
Smeagol will run, obviously, on the IP address of your Docker image, on port 8080. To find the IP address, start the image using the command above and then use
docker inspect --format '{{ .NetworkSettings.IPAddress }}' $(docker ps -q)
Suppose this prints '10.10.10.10', then the URL to browse to will be http://10.10.10.10:8080/smeagol/
This image is _experimental_, but it does seem to work fairly well. What it does **not** yet do, however, is push the git repository to a remote location, so when you tear the Docker image down your edits will be lost. My next objective for this image is for it to have a cammand line parameter being the git address of a repository from which it can initialise the Wiki content, and to which it will periodically push local changes to the Wiki content.
To build your own Docker image, run:
lein clean
lein bower install
lein ring uberwar
lein docker build
This will build a new Docker image locally; you can, obviously, push it to your own Docker repository if you wish.

View file

@ -2,23 +2,45 @@
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed. You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed.
You will need [node](https://nodejs.org/en/) and [bower](https://bower.io/) installed. You will need [node](https://nodejs.org/en/) installed.
## Running in development ## Running in development
To start a web server for the application during development, run: To start a web server for the application during development, run:
lein bower install ```
lein ring server lein npm install
lein ring server
```
This should start a development server, and open a new window or tab in your default browser with the default page of the wiki loaded into it. This should start a development server, and open a new window or tab in your default browser with the default page of the wiki loaded into it.
## Editing ## Editing
I generally use [LightTable](http://lighttable.com/) as my `Clojure` editor, but it doesn't really matter what you use; if you run Smeagol as described above, then all changes you make in the code (and save) will instantly be applied to the running system. This makes for a productive development environment. I generally use [LightTable](http://lighttable.com/) as my `Clojure` editor, but it doesn't really matter what you use; if you run Smeagol as described above, then all changes you make in the code (and save) will instantly be applied to the running system. This makes for a productive development environment.
## Building for deployment
*Important:* Always run `lein clean` before building a deployment build. Once you have deployed your deployment artifact, run `lein clean` again before continuing development.
### To build a standalone executable jar file
run:
```
lein ring uberjar
```
The resulting file may be run by invoking
```
java -jar \[path to uberjar file\]
```
## Documentation ## Documentation
It is my intention that the code should be sufficiently well documented to be easy to understand. Documentation may be generated from the code by running It is my intention that the code should be sufficiently well documented to be easy to understand. Documentation may be generated from the code by running
lein codox ```
lein codox
```
## Contributing ## Contributing
If you make changes to Smeagol which you think are useful, please contribute them in the form of a [pull request on github](https://help.github.com/articles/creating-a-pull-request/). If you make changes to Smeagol which you think are useful, please contribute them in the form of a [pull request on github](https://help.github.com/articles/creating-a-pull-request/).

View file

@ -0,0 +1,55 @@
Smeagol is available as a Docker image
To run my Docker image, use
docker run -p 127.0.0.1:80:80 simonbrooke/smeagol
Where 127.0.0.1 is the IP address through which you want to forward port 80 (in real life it wouldn't be 127.0.0.1, but that's safe for testing).
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, 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:
```
COPY resources/passwd /usr/local/etc/passwd
COPY resources/config.edn /usr/local/etc/config.edn
COPY resources/public/content /usr/local/etc/content
ENV SMEAGOL_CONFIG=/usr/local/etc/config.edn
ENV SMEAGOL_CONTENT_DIR=/usr/local/etc/content
ENV SMEAGOL_PASSWD=/usr/local/etc/passwd
```
This works for play purposes. However, it means that any edits made to either the `passwd` file or the `content` directory will be lost when the Docker image is shut down. You really need to have these resources copied to a place in a real file system which is mounted by the image. While I intend that by the 1.1.0 release of Smeagol it will be possible to configure a remote origin repository to which changes are periodically pushed, which will backup and preserve the content, this won't save the `passwd` file, as this is deliberately not stored in the git repository for security reasons.
## Mounting real file systems
It's possible to mount external file systems, and to override environment variables, with arguments to Docker's extraordinarily complex [run command](https://docs.docker.com/engine/reference/commandline/run/).
I'm currently working with a recipe:
docker run -p 127.0.0.1:80:80 -v ~/tmp/etc:/usr/local/etc simonbrooke/smeagol
Where:
1. `127.0.0.1` is the IP address on the real host on which you wish to serve;
2. `:80:80` maps port 80 on the image to port 80 on the specified IP address;
3. `~/tmp/etc` is the directory on the file system of the real host where files are stored;
4. `/usr/local/etc` is the directory within the image file system to which that will be mounted;
This works, and uses the default values of the environment variables which are set up in the Docker image. However, I'm very much prepared to believe there are better recipes.
## Status
This image is _experimental_, but it does seem to work fairly well. What it does **not** yet do, however, is push the git repository to a remote location, so unless you have mounted an external file store, when you tear the Docker image down your edits will be lost. My next objective for this image is for it to have a cammand line parameter being the git address of a repository from which it can initialise the Wiki content, and to which it will periodically push local changes to the Wiki content.
## Building the Docker image
To build your own Docker image, run:
lein clean
lein bower install
lein ring uberjar
lein docker build
This will build a new Docker image locally; you can, obviously, push it to your own Docker repository if you wish.

View file

@ -0,0 +1,23 @@
## Smeagol-specific environment variables
Smeagol can be configured entirely with environment variables. The variables are:
1. `SMEAGOL_CONFIG` (optional but advised) should be the full or relative pathname of a Smeagol [[Configuration]] file;
2. `SMEAGOL_CONTENT_DIR` should be the full or relative pathname of the directory from which Smeagol should serve content (which may initially be empty, but must be writable by the process which runs Smeagol);
3. `SMEAGOL_DEFAULT_LOCALE` which should be a locale specification in the form "en-GB", "fr-FR", or whatever to suit your users;
4. `SMEAGOL_FORMATTERS` should be an [edn](https://github.com/edn-format/edn)-formatted map of formatter directives (this would be pretty hard to do from an environment variable);
5. `SMEAGOL_LOG_LEVEL` which should be one of `TRACE DEBUG INFO WARN ERROR FATAL`
6. `SMEAGOL_PASSWD` should be the full or relative pathname of a Smeagol Passwd file - see [[Security and authentication]]. This file must contain an entry for at least your initial user, and, if you want to administer users through the user interface, must be writable by the process which runs Smeagol.
7. `SMEAGOL_SITE_TITLE` which should be the title you want shown on the header of all pages.
You can have both a configuration file and environment variables; if you do, the values of the environment variables take precedence over the values in the config file.
## Other environment variables
If Smeagol is compiled as an executable jar file, the actual web server component is [Ring server](https://github.com/weavejester/ring-server). This recognises the `PORT` environment variable, and, if this is present and its value is a positive integer, will listen on the specified port (otherwise its default is 3000, which is... unusual).
Smeagol uses the [Timbre](https://github.com/ptaoussanis/timbre) logging library. This recognises the following environment variables:
1. `TIMBRE_DEFAULT_STACKTRACE_FONTS` Timbre by default colourises stacktrace dumps using ANSI terminal codes. This can be quite useful in a console, but is a real pain in a log file. To turn colourised stacktraces off, set the value of this to an empty string;
2. `TIMBRE_LEVEL` Sets the minimum logging level; but there are two problems with this. The first is that the environment variable is only read at compile time not at run time, and the second is that the syntax is a bit odd, which is why I've implemented `SMEAGOL_LOG_LEVEL` (above);
3. `TIMBRE_NS_WHITELIST` Sets a list of [Clojure namespaces](https://clojure.org/reference/namespaces) from which messages should be logged; however this is only read at compile time so isn't much use in practice;
4. `TIMBRE_NS_BLACKLIST` As above, but sets a list of namespaces from which messages should **not** be logged.

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

View file

@ -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.

View file

@ -1,35 +1,33 @@
![One wiki to rule them all](https://www.weft.scot/images/smeagol.png) ![One wiki to rule them all](https://www.weft.scot/images/smeagol.png)
# Welcome to Smeagol! # Welcome to Smeagol!
Smeagol is a simple Wiki engine inspired by [Gollum](https://github.com/gollum/gollum/wiki). Gollum is a Wiki engine written in Ruby, which uses a number of simple text formats including [Markdown](http://daringfireball.net/projects/markdown/), and which uses [Git](http://git-scm.com/) to provide versioning and backup. I needed a new Wiki for a project and thought Gollum would be ideal - but unfortunately it doesn't provide user authentication, which I needed, and it was simpler for me to reimplement the bits I did need in Clojure than to modify Gollum. Smeagol is a hackable, extensible Wiki engine which is reasonably user-friendly. It uses [Markdown](http://daringfireball.net/projects/markdown/) as its text format, and [Git](http://git-scm.com/) to provide versioning and backup.
So at this stage Smeagol is a Wiki engine written in Clojure which uses Markdown as its text format, which does have user authentication, and which uses Git as its versioning and backup system.
## Status
Smeagol is now a fully working small Wiki engine, and meets my own immediate needs.
## Using Smeagol ## Using Smeagol
Read the [[User Documentation]] for an introduction to all Smeagol's features. Read the [[User Documentation]] for an introduction to all Smeagol's features.
## Markup syntax ## Markup syntax
Smeagol uses the Markdown format as provided by [markdown-clj](https://github.com/yogthos/markdown-clj), with the addition that anything enclosed in double square brackets, \[\[like this\]\], will be treated as a link into the wiki itself. Read more about [[Extensible Markup]]. Smeagol uses the Markdown format as provided by [markdown-clj](https://github.com/yogthos/markdown-clj), with the addition that anything enclosed in double square brackets, \[\[like this\]\], will be treated as a link into the wiki itself. The markdown format is extensible, and has extensions already for inclusions, for data visualisations and for picture galleries. Read more about [[Extensible Markup]].
## Security and authentication ## Security and authentication
Smeagol now has good security and authentication. While the initial password supplied with the system is not encrypted, when it is changed it will be; and passwords for new users added through the user administration pages are encrypted. Read more about [[Security and authentication]]. Smeagol now has good security and authentication. While the initial password supplied with the system is not encrypted, when it is changed it will be; and passwords for new users added through the user administration pages are encrypted. Read more about [[Security and authentication]].
## Internationalisation
Smeagol has built in internationalisation. Currently it has translation files for English, German, Lithuanian and Russian. We'd welcome volunteers to translate it into other languages.
## Images ## Images
You can (if you're logged in) upload files, including images, using the **Upload a file** link on the top menu bar. You can link to an uploaded image, or to other images already available on the web, like this: You can (if you're logged in) upload files, including images, using the **Upload a file** link on the top menu bar. You can link to an uploaded image, or to other images already available on the web, like this:
![Smeagol](http://vignette3.wikia.nocookie.net/lotr/images/e/e1/Gollum_Render.png/revision/latest?cb=20141218075509) ![Smeagol](/img/smeagol.png)
## Running Smeagol ## Running Smeagol
You can run Smeagol from the [[Docker Image]]; alternatively you can run it from an executable jar file or as a war file in a servlet container. Read how in [[Deploying Smeagol]]. You can run Smeagol from the [[Docker Image]]; alternatively you can run it from an executable jar file or as a war file in a servlet container. Read how about [[Configuring Smeagol]] and [[Deploying Smeagol]].
## Developing Smeagol ## Developing Smeagol
Smeagol is an open source project; you're entitled to make changes yourself. Read more about [[Developing Smeagol]]. Smeagol is an open source project; you're entitled to make changes yourself. Read more about [[Developing Smeagol]].
## License ## License
Copyright © 2014-2017 Simon Brooke. Licensed under the GNU General Public License, Copyright © 2014-2020 Simon Brooke. Licensed under the GNU General Public License,
version 2.0 or (at your option) any later version. If you wish to incorporate version 2.0 or (at your option) any later version. If you wish to incorporate
parts of Smeagol into another open source project which uses a less restrictive parts of Smeagol into another open source project which uses a less restrictive
license, please contact me; I'm open to dual licensing it. license, please contact me; I'm open to dual licensing it.
@ -37,5 +35,3 @@ license, please contact me; I'm open to dual licensing it.
## Phoning home ## Phoning home
Smeagol does currently fetch one image from my home site. Read more about [[Phoning Home]], and how to prevent it (if you want to). Smeagol does currently fetch one image from my home site. Read more about [[Phoning Home]], and how to prevent it (if you want to).
## Advertisement
If you like what you see here, I am available for work on open source Clojure projects.

View file

@ -0,0 +1,24 @@
## How this works
The specification for this gallery is as follows:
```
![Frost on a gate, Laurieston](content/uploads/g1.jpg)
![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg)
![Feathered snow on log, Taliesin](content/uploads/g3.jpg)
![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)
```
That's all there is to it - a sequence of image links just as you'd write them anywhere else in the wiki.
## The Gallery
This page holds another example Photoswipe gallery, this time using a simpler, Markdown-based specification. Processing this specification takes more work than the full syntax used in the other [[Example gallery]], so the gallery may be slower to load; but it's much easier to configure.
```pswp
![Frost on a gate, Laurieston](content/uploads/g1.jpg)
![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg)
![Feathered snow on log, Taliesin](content/uploads/g3.jpg)
![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)
```

View file

@ -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:
![Menu icon](/img/three-lines.png)
![Menu icon](img/three-lines.png)
at the top left of the page. Touching this image will cause the top menu to be displayed, and it will have all the options described in this documentation. 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.

View file

@ -10,4 +10,6 @@
+ \*\***bold**\*\* + \*\***bold**\*\*
+ \__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>.

View file

@ -1,5 +1,7 @@
* [[Introduction]] * [[Introduction]]
* [[Change log]]
* [[User Documentation]] * [[User Documentation]]
* [[Configuring Smeagol]]
* [[Deploying Smeagol]] * [[Deploying Smeagol]]
* [[Developing Smeagol]] * [[Developing Smeagol]]

View file

@ -265,6 +265,14 @@ th {
padding: 0 2em 0 0; padding: 0 2em 0 0;
} }
.sanity-cause .sanity-stacktrace {
display: none;
}
.sanity-cause:hover .sanity-stacktrace {
display: block;
}
.vega-bindings, .vega-actions { .vega-bindings, .vega-actions {
font-size: 66%; font-size: 66%;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1 +0,0 @@
This folder must exist in order that the Bower package manager can deploy JavaScript packages to it.

View file

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

View file

@ -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 %}

View file

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

View 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 %} ![{{entry.name|capitalize}}]({{entry.resource}}) {% 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 %}

View file

@ -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>![Uploaded image](content/uploads/{{uploaded}})</code> <code>![{{upload.filename}}]({{upload.resource}})</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 %}

View file

@ -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 %}

View file

@ -0,0 +1,7 @@
# This is a test
```test
the quick brown fox jumped over the lazy dog
```
This concludes the test.

View file

@ -0,0 +1,6 @@
# This is a test
[[Local link]]
[Not a local link](http://nowhere.at.al)
This concludes the test.

View file

View file

@ -4,7 +4,8 @@
(:require [crypto.password.scrypt :as password] (:require [crypto.password.scrypt :as password]
[environ.core :refer [env]] [environ.core :refer [env]]
[noir.io :as io] [noir.io :as io]
[taoensso.timbre :as timbre])) [smeagol.configuration :refer [config]]
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -36,9 +37,11 @@
;; the relative path to the password file. ;; the relative path to the password file.
(def password-file-path (def password-file-path
"Path to the password file."
;; TODO: portability; elegance. Not very happy with this.
(or (or
(env :smeagol-passwd) (:passwd config)
(str (clojure.java.io/resource "passwd")))) (str (io/resource-path) "../passwd")))
(defn- get-users (defn- get-users
@ -51,7 +54,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
@ -91,7 +94,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)
@ -109,10 +112,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 (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))))
@ -137,7 +140,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"))
@ -159,10 +162,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 (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)))))
@ -176,10 +179,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 (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))))

View file

@ -1,9 +1,11 @@
(ns ^{:doc "Read and make available configuration." (ns ^{:doc "Read and make available configuration."
:author "Simon Brooke"} :author "Simon Brooke"}
smeagol.configuration smeagol.configuration
(:require [environ.core :refer [env]] (:require [clojure.pprint :refer [pprint]]
[clojure.string :as s]
[environ.core :refer [env]]
[noir.io :as io] [noir.io :as io]
[taoensso.timbre :as timbre])) [taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -37,7 +39,6 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def config-file-path (def config-file-path
"The relative path to the config file." "The relative path to the config file."
(or (or
@ -45,10 +46,106 @@
(str (io/resource-path) "../config.edn"))) (str (io/resource-path) "../config.edn")))
(defn- from-env-vars
"Read a map from those of these environment variables which have values"
[& vars]
(reduce
#(let [v (env %2)]
(if v (assoc %1 %2 v) %1))
{}
vars))
(defn to-keyword
"Convert this argument into an idiomatic clojure keyword."
[arg]
(if (and arg (not (keyword? arg)))
(keyword
(s/lower-case
(s/replace (str arg) #"[^A-Za-z0-9]+" "-")))
arg))
(defn transform-map
"transform this map `m` by applying these `transforms`. Each transforms
is expected to comprise a map with the keys :from and :to, whose values
are respectively a key to match and a key to replace that match with,
and optionally a key :transform, whose value is a function of one
argument to be used to transform the value of that key."
[m tuples]
(log/debug
"transform-map:\n"
(with-out-str (clojure.pprint/pprint m)))
(reduce
(fn [m tuple]
(if
(and (map? tuple) (map? m) (m (:from tuple)))
(let [old-val (m (:from tuple))
t (:transform tuple)]
(assoc
(dissoc m (:from tuple))
(:to tuple)
(if-not
(nil? t)
(eval (list t old-val)) old-val)))
m))
m
tuples))
(def config-env-transforms
"Transforms to use with `transform-map` to convert environment
variable names (which need to be specific) into the shorter names
used internally"
'( {:from :smeagol-content-dir :to :content-dir}
{:from :smeagol-default-locale :to :default-locale}
{:from :smeagol-formatters :to :formatters :transform read-string}
{:from :smeagol-js-from :to :extensions-from :transform to-keyword}
{:from :smeagol-log-level :to :log-level :transform to-keyword}
{:from :smeagol-passwd :to :passwd}
{:from :smeagol-site-title :to :site-title}))
(def build-config
"The actual configuration, as a map. The idea here is that the config
file is read (if it is specified and present), but that individual
values can be overridden by environment variables."
(memoize (fn []
(try
(log/info (str "Reading configuration from " config-file-path))
(let [file-contents (try
(read-string (slurp config-file-path))
(catch Exception x
(log/error
(str
"Failed to read configuration from "
config-file-path
" because: "
(type x)
"; "
(.getMessage x)))
{}))
config (merge
file-contents
(transform-map
(from-env-vars
:smeagol-content-dir
:smeagol-default-locale
:smeagol-formatters
:smeagol-js-from
:smeagol-log-level
:smeagol-passwd
:smeagol-site-title)
config-env-transforms))]
(if (env :dev)
(log/debug
"Loaded configuration\n"
(with-out-str (clojure.pprint/pprint config))))
config)
(catch Exception any
(log/error any "Could not load configuration")
{})))))
(def config (def config
"The actual configuration, as a map." "The actual configuration, as a map."
(try (build-config))
(read-string (slurp config-file-path))
(catch Exception any
(timbre/error "Could not load configuration" any)
{})))

View 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"))

View file

@ -0,0 +1,176 @@
(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]
[smeagol.util :refer [content-dir upload-dir]]
[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 := '![' ;
END-CAPTION := '](' ;
END-SRC := ')' ;
title := #'[^]]*' ;
src := #'[^)]*' ;
SPACE := #'[\\r\\n\\W]*'"))
(defn- simplify
[tree]
(if
(coll? tree)
(case (first tree)
:SLIDE (remove empty? (map simplify (rest tree)))
:title tree
:src tree
:START-CAPTION nil
:END-CAPTION nil
:END-SRC nil
(remove empty? (map simplify tree)))))
(defn slide-merge-dimensions
"If this `slide` appears to be local, return it decorated with the
dimensions of the image it references."
[slide]
(let [url (:src slide)
dimensions (try
(if (uploaded? url)
(dimensions
(buffered-image (cio/file upload-dir (fs/base-name url)))))
(catch Exception x (.getMessage x)))]
(if dimensions
(assoc slide :w (first dimensions) :h (nth dimensions 1))
(do
(log/warn "Failed to fetch dimensions of image " url)
slide))))
;; (slide-merge-dimensions
;; {:title "Frost on a gate, Laurieston",
;; :src "content/uploads/g1.jpg"})
(defn- process-simple-slide
[slide-spec]
(let [s (simplify (simple-grammar slide-spec))
s'(zipmap (map first s) (map #(nth % 1) s))
thumbsizes (:thumbnails config)
thumbsize (first
(sort
#(> (%1 thumbsizes) (%2 thumbsizes))
(keys thumbsizes)))
url (:url s')
thumb (if
(and
(uploaded? url)
thumbsize)
(let [p (str (cio/file "uploads" (name thumbsize) (fs/base-name url)))
p' (cio/file content-dir p)]
(if
(and (fs/exists? p') (fs/readable? p'))
p)))]
(slide-merge-dimensions
(if thumb
(assoc s' :msrc thumb)
s'))))
(def process-simple-photoswipe
"Process a simplified specification for a photoswipe gallery, comprising just
a sequence of MarkDown image links. This is REALLY expensive to do, we don't
want to do it often. Hence memoised."
(memoize
(fn
[^String spec ^Integer index]
(process-full-photoswipe
(json/write-str
{:slides (map
process-simple-slide
(re-seq #"!\[[^(]*\([^)]*\)" spec))
;; TODO: better to split slides in instaparse
:options { :timeToIdle 100 }
:openImmediately true}) index))))
;; (map
;; process-simple-slide
;; (re-seq #"!\[[^(]*\([^)]*\)"
;; "![Frost on a gate, Laurieston](content/uploads/g1.jpg)
;; ![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg)
;; ![Feathered snow on log, Taliesin](content/uploads/g3.jpg)
;; ![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)"))
;; (process-simple-photoswipe
;; "![Frost on a gate, Laurieston](content/uploads/g1.jpg)
;; ![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg)
;; ![Feathered snow on log, Taliesin](content/uploads/g3.jpg)
;; ![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)"
;; 1)
(defn process-photoswipe
"Process a Photoswipe specification which may conform either to the
`full` or the `simple` syntax."
[^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))))

View 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)

View file

@ -0,0 +1,94 @@
(ns ^{:doc "Utility functions useful to extension processors."
:author "Simon Brooke"}
smeagol.extensions.utils
(:require [cemerick.url :refer (url url-encode url-decode)]
[clj-yaml.core :as yaml]
[clojure.data.json :as json]
[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.util :refer [content-dir upload-dir]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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 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))))))
(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? (cjio/file upload-dir (fs/base-name url)))))
;; (uploaded? "content/uploads/g1.jpg")
(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)))

View file

@ -0,0 +1,84 @@
(ns ^{:doc "Format vega/vis extensions to Semagol's extended markdown format."
:author "Simon Brooke"}
smeagol.extensions.vega
(:require [smeagol.extensions.utils :refer :all]
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; Smeagol: a very simple Wiki engine.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2017 Simon Brooke
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; Inspired by [visdown](https://visdown.com/) and
;;;; [vega-lite](https://vega.github.io/vega-lite/docs/), the Vega formatter
;;;; allows you to embed vega data visualisations into Smeagol pages. The graph
;;;; description should start with a line comprising three back-ticks and then
;;;; the word '`vega`', and end with a line comprising just three backticks.
;;;;
;;;; Here's an example cribbed in its entirety from
;;;; [here](http://visdown.amitkaps.com/london):
;;;;
;;;; ### Flight punctuality at London airports
;;;;
;;;; ```vega
;;;; data:
;;;; url: "data/london.csv"
;;;; transform:
;;;; -
;;;; filter: datum.year == 2016
;;;; mark: rect
;;;; encoding:
;;;; x:
;;;; type: nominal
;;;; field: source
;;;; y:
;;;; type: nominal
;;;; field: dest
;;;; color:
;;;; type: quantitative
;;;; field: flights
;;;; aggregate: sum
;;;; ```
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn process-vega
"If this `src-resource-or-url` is a valid URL, it is assumed to point to a
plain text file pointing to valid `vega-src`; otherwise, it is expected to
BE a valid `vega-src`.
Process this `vega-src` string, assumed to be in YAML format, into a
specification of a Vega chart, and add the plumbing to render it."
[^String src-resource-or-url ^Integer index]
(let [data (resource-url-or-data->data 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>")))

View file

@ -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,70 +45,26 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 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 (clojure.string/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.
**NOTE** that it is not expected that this function forms part of a stable
API."
[^String text ^Integer index] [^String text ^Integer index]
(str "<pre class=\"backticks\">```" (.trim text) "\n```</pre>")) (str "<pre class=\"backticks\">```" (.trim text) "\n```</pre>"))
(defn get-first-token (defn get-first-token
"Return the first space-separated token of this `string`." "Return the first space-separated token of the first line of this `string`,
or `nil` if there is none."
[^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
@ -112,8 +73,11 @@
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 fragment fragments processed]
**NOTE** that it is not expected that this function forms part of a stable
API."
[^Integer index ^clojure.lang.Associative result ^String fragment fragments processed]
(process-text (process-text
(inc index) (inc index)
result result
@ -121,62 +85,90 @@
(cons fragment processed))) (cons fragment processed)))
(defn deep-merge
"Cripped in its entirety from [here](https://clojuredocs.org/clojure.core/merge)."
[v & vs]
(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 (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, and then recurse back into `process-text` to
process the remainder of the fragments. Arguments are as for `process-text`, q.v.,
the addition of
* `fragment` the current fragment to be processed;
* `token` the identifier of the extension processor to be applied;
* `formatter` the actual extension processor to be applied.
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]
**NOTE** that it is not expected that this function forms part of a stable
API."
[^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.
The map has two top-level keys: `:inclusions`, a map of constructed keywords to **NOTE** that it is not expected that this function forms part of a stable
inclusion specifications, and `:text`, an HTML text string with the keywords API."
present where the corresponding inclusion should be inserted." [result processed]
([^String text] (assoc result :text
(process-text 0 {:inclusions {}} (cs/split (or text "") #"```") '())) (local-links
([index result fragments processed] (md/md-to-html-string
(let [fragment (first fragments) (cs/join "\n\n" (reverse processed))
;; if I didn't find a formatter for a back-tick marked fragment, :heading-anchors true))))
;; 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 map based on
with the inclusions (if any) reintegrated." that map with the key `:content` bound to a string of HTML text
with the inclusions (if any) generated by extension processors reintegrated.
**NOTE** that it is not expected that this function forms part of a stable
API."
([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 +179,89 @@
(cs/replace (kw inclusions) "\\/" "/")))))))) (cs/replace (kw inclusions) "\\/" "/"))))))))
(defn- process-text
"Process extension fragments in this text. Arguments are:
* `index`, the index number of the current fragment;
* `result`, a context within which the final result is being accumulated;
* `fragments`, a sequence of the fragments of the original text which have
not yet been processed;
* `processed`, a reverse sequence of the fragments of the original text
which have already been processed.
Returns a map derived from `result` enhanced with the accumulated result.
**NOTE** that it is not expected that this function forms part of a stable
API."
[^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 the source text (assumed to be the value of the key `:source` in this
[md-src] `context`, expected to be a full HTTP request map, so that extensions may in
(reintegrate-inclusions (process-text md-src))) future potentially have access to things like `accepts-*` headers.
The source is assumed to be markdown potentially containing both local links
and extension specifications, and return a map with top-level keys:
* `:content`, the HTML content of the page to serve; and
* `:extensions`, being a subset of the `:formatters` map from
`smeagol.configuration/config` covering the extensions actually used in the
generated content."
[^clojure.lang.Associative context]
(reintegrate-inclusions
(process-text
0
(assoc context :extensions #{})
(cs/split (or (:source context) "") #"```")
'())))
;; (def first-token "pswp")
;; (def kw (keyword "pswp"))
;; (def fragment "pswp
;; ![Frost on a gate, Laurieston](content/uploads/g1.jpg)
;; ![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg)
;; ![Feathered snow on log, Taliesin](content/uploads/g3.jpg)
;; ![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)")
;; (def index 0)
;; (def formatter (read-string (-> config :formatters kw :formatter)))
;; formatter
;; (eval (list formatter (subs fragment (count first-token)) index))
;; (process-photoswipe (subs fragment (count first-token)) index)
;; (process-text
;; {:source "pswp
;; ![Frost on a gate, Laurieston](content/uploads/g1.jpg)
;; ![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg)
;; ![Feathered snow on log, Taliesin](content/uploads/g3.jpg)
;; ![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)"} )
;; (process-text {:source (slurp (clojure.java.io/file smeagol.util/content-dir "Extensible Markup.md"))})

View file

@ -2,6 +2,7 @@
:author "Simon Brooke"} :author "Simon Brooke"}
smeagol.handler smeagol.handler
(:require [clojure.java.io :as cjio] (:require [clojure.java.io :as cjio]
[clojure.string :refer [lower-case]]
[compojure.core :refer [defroutes]] [compojure.core :refer [defroutes]]
[compojure.route :as route] [compojure.route :as route]
[cronj.core :as cronj] [cronj.core :as cronj]
@ -11,10 +12,11 @@
[noir.util.middleware :refer [app-handler]] [noir.util.middleware :refer [app-handler]]
[ring.middleware.defaults :refer [site-defaults]] [ring.middleware.defaults :refer [site-defaults]]
[selmer.parser :as parser] [selmer.parser :as parser]
[smeagol.configuration :refer [config]]
[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]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -43,6 +45,7 @@
(defn user-access [request] (defn user-access [request]
(session/get :user)) (session/get :user))
(defroutes base-routes (defroutes base-routes
(route/resources "/") (route/resources "/")
(route/not-found "Not Found")) (route/not-found "Not Found"))
@ -52,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
@ -64,19 +67,23 @@
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"
:max-size (* 512 1024) :max-size (* 512 1024)
:backlog 10})}}) :backlog 10})}
:level (or
(:log-level config)
(if (env :dev) :debug)
:info)})
(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 "Failure during startup" any) (log/error any "Failure during startup")
(destroy)))) (destroy))))
;; timeout sessions after 30 minutes ;; timeout sessions after 30 minutes
@ -90,6 +97,7 @@
[xss-protection?] [xss-protection?]
(-> site-defaults (-> site-defaults
(update-in [:session] merge session-defaults) (update-in [:session] merge session-defaults)
(dissoc :static)
(assoc-in [:security :anti-forgery] xss-protection?))) (assoc-in [:security :anti-forgery] xss-protection?)))

View file

@ -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")

61
src/smeagol/include.clj Normal file
View file

@ -0,0 +1,61 @@
(ns ^{:doc "Functions related to the include of markdown-paged in a given markdown."
:author "Michael Jerger"}
smeagol.include
(:require
[clojure.string :as cs]
[schema.core :as s]
[com.stuartsierra.component :as component]
[smeagol.include.parse :as parse]
[smeagol.include.resolve :as resolve]
[smeagol.include.indent :as indent]))
(s/defrecord Includer
[resolver])
(defprotocol IncludeMd
(expand-include-md
[includer md-src]
"return a markdown containing resolved includes"))
(s/defn
do-expand-one-include :- s/Str
[includer :- Includer
include :- parse/IncludeLink
md-src :- s/Str]
(let [{:keys [uri replace indent-heading indent-list]} include]
(cs/replace-first
md-src
(re-pattern (cs/escape
replace
{\[ "\\["
\] "\\]"
\( "\\("
\) "\\)"}))
(indent/do-indent-list
indent-list
(indent/do-indent-heading
indent-heading
(resolve/resolve-md (:resolver includer) uri))))))
(s/defn
do-expand-includes :- s/Str
[includer :- Includer
includes :- [parse/IncludeLink]
md-src :- s/Str]
(loop [loop-includes includes
result md-src]
(if (empty? loop-includes)
result
(recur
(rest loop-includes)
(do-expand-one-include includer (first loop-includes) result)))))
(extend-type Includer
IncludeMd
(expand-include-md [includer md-src]
(do-expand-includes includer (parse/parse-include-md md-src) md-src)))
(s/defn
new-includer
[]
(map->Includer {}))

View file

@ -0,0 +1,58 @@
(ns ^{:doc "Functions related to the include of markdown-paged - handling the
list & heading indents of includes. This namespaces is implementation detail for
smeagol.include and not inteded for direct usage."
:author "Michael Jerger"}
smeagol.include.indent
(:require
[clojure.string :as cs]
[schema.core :as s]))
(s/defn
parse-list
[md-resolved :- s/Str]
(distinct
(into
(re-seq #"((^|\R? *)([\*\+-] ))" md-resolved)
(re-seq #"((^|\R? *)([0-9]+\. ))" md-resolved))))
(s/defn
parse-heading
[md-resolved :- s/Str]
(distinct
(re-seq #"((^|\R?)(#+ ))" md-resolved)))
(s/defn
do-indent :- s/Str
[indent :- s/Num
indentor :- s/Str
elements
md-resolved :- s/Str]
(loop [result md-resolved
elements elements]
(if (empty? elements)
result
(let [element (first elements)
replace (nth element 1)
start (nth element 2)
end (nth element 3)]
(recur
(cs/replace
result
(re-pattern (cs/escape
replace
{\* "\\*"
\n "\\n"}))
(str start (apply str (repeat indent indentor)) end))
(rest elements))))))
(s/defn
do-indent-heading :- s/Str
[indent :- s/Num
md-resolved :- s/Str]
(do-indent indent "#" (parse-heading md-resolved) md-resolved))
(s/defn
do-indent-list :- s/Str
[indent :- s/Num
md-resolved :- s/Str]
(do-indent indent " " (parse-list md-resolved) md-resolved))

View file

@ -0,0 +1,50 @@
(ns ^{:doc "Functions related to the include of markdown-paged - parsing of
include links. This namespaces is implementation detail for
smeagol.include and not inteded for direct usage."
:author "Michael Jerger"}
smeagol.include.parse
(:require
[schema.core :as s]))
(def IncludeLink
{:replace s/Str
:uri s/Str
:indent-heading s/Num
:indent-list s/Num})
(s/defn
convert-indent-to-int :- s/Num
[indents :- [s/Str]]
(if (some? indents)
(Integer/valueOf (nth indents 2))
0))
(s/defn
parse-indent-list
[md-src :- s/Str]
(re-matches #".*(:indent-list (\d)).*" md-src))
(s/defn
parse-indent-heading
[md-src :- s/Str]
(re-matches #".*(:indent-heading (\d)).*" md-src))
(s/defn
parse-include-link
[md-src :- s/Str]
(re-seq #".*(&\[\w*(.*)\w*\]\((.*)\)).*" md-src))
(s/defn
parse-include-md :- [IncludeLink]
[md-src :- s/Str]
(vec
(map
(fn [parse-element]
(let [replace (nth parse-element 1)
uri (nth parse-element 3)
indents-text (nth parse-element 2)]
{:replace replace
:uri uri
:indent-heading (convert-indent-to-int (parse-indent-heading indents-text))
:indent-list (convert-indent-to-int (parse-indent-list indents-text))}))
(parse-include-link md-src))))

View file

@ -0,0 +1,46 @@
(ns ^{:doc "Functions related to the include of markdown-paged - providing
a plugable load-content componet. This namespaces is implementation detail for
smeagol.include and not inteded for direct usage."
:author "Michael Jerger"}
smeagol.include.resolve
(:require
[schema.core :as s]
[com.stuartsierra.component :as component]))
(s/defrecord Resolver
[type :- s/Keyword
local-base-dir :- s/Str])
;As schema doesn't support s/defprotocol we use the dispatcher for annotation & validation.
(s/defn dispatch-by-resolver-type :- s/Keyword
"Dispatcher for different resolver implementations."
[resolver :- Resolver
uri :- s/Str]
(:type resolver))
(defmulti do-resolve-md
"Multimethod return a markdown file content for given uri."
dispatch-by-resolver-type)
(s/defmethod do-resolve-md :default
[resolver :- Resolver
uri :- s/Str]
(throw (Exception. (str "No implementation for " resolver))))
(defprotocol ResolveMd
(resolve-md
[resolver uri]
"return a markfown file content for given uri."))
(extend-type Resolver
ResolveMd
(resolve-md [resolver uri]
(s/validate s/Str uri)
(s/validate s/Str (do-resolve-md resolver uri))))
(s/defn
new-resolver
([type :- s/Keyword]
(map->Resolver {:type type :local-base-dir nil}))
([type :- s/Keyword
local-base-dir :- s/Str]
(map->Resolver {:type type :local-base-dir local-base-dir})))

View file

@ -0,0 +1,31 @@
(ns ^{:doc "Functions related to the include of markdown-paged - providing
a plugable load-local-include-links componet. This namespaces is implementation detail for
smeagol.include and not inteded for direct usage."
:author "Michael Jerger"}
smeagol.include.resolve-local-file
(:require
[schema.core :as s]
[smeagol.include.resolve :as resolve]
[com.stuartsierra.component :as component]
[clojure.java.io :as cjio]
[taoensso.timbre :as timbre]))
(s/defmethod resolve/do-resolve-md :local-file
[resolver
uri :- s/Str]
(let [file-name uri
file-path (cjio/file (:local-base-dir resolver) file-name)
exists? (.exists (clojure.java.io/as-file file-path))]
(cond exists?
(do
(timbre/info (format "Including page '%s' from file '%s'" uri file-path))
(slurp file-path))
:else
(do
(timbre/info (format "Page '%s' not found at '%s'" uri file-path))
(str "include not found at " file-path)))))
(s/defn
new-resolver
[local-base-dir :- s/Str]
(resolve/new-resolver :local-file local-base-dir))

View file

@ -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]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -38,7 +37,11 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def template-path "templates/") (def template-path
"Path to the resource directory in which Selmer templates are stored. These
should be in a place which is not editable from the Wiki, otherwise
users may break things which they cannot subsequently fix!"
"templates/")
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field))) (parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
@ -49,10 +52,14 @@
(fn [args context-map] (fn [args context-map]
(let [messages (:i18n context-map) (let [messages (:i18n context-map)
default (or (second args) (first args))] default (or (second args) (first args))]
(if (map? messages) (or (messages (keyword (first args))) default) default)))) (if (map? messages) (or (messages (keyword (first args))) default)
default))))
(deftype RenderableTemplate [template params] (deftype RenderableTemplate
;; Boilerplate from Luminus. Load a template file into an object which may
;; be rendered.
[template params]
Renderable Renderable
(render [this request] (render [this request]
(try (try
@ -76,6 +83,8 @@
(defn render (defn render
"Boilerplate from Luminus. Render an HTML page based on this `template` and
these `params`. Returns HTML source as a string."
[template & [params]] [template & [params]]
(try (try
(RenderableTemplate. template params) (RenderableTemplate. template params)

View 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))

View file

@ -1,12 +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]]
[noir-exception.core :refer [wrap-internal-error]])) [ring.middleware.file :refer [wrap-file]]
[ring.middleware.resource :refer [wrap-resource]]
[ring.middleware.content-type :refer [wrap-content-type]]
[ring.middleware.not-modified :refer [wrap-not-modified]]
[selmer.middleware :refer [wrap-error-page]]
[smeagol.util :as util]
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -34,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)))
@ -44,7 +49,12 @@
(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-file % util/content-dir
{:index-files? false :prefer-handler? true})
#(wrap-content-type %)
#(wrap-not-modified %)])
(defn load-middleware [] (defn load-middleware []

View file

@ -33,7 +33,7 @@
(defn edit-users (defn edit-users
"Put a list of users on-screen for editing." "Render a page showing a list of users for editing."
[request] [request]
(let [params (keywordize-keys (:params request)) (let [params (keywordize-keys (:params request))
user (session/get :user)] user (session/get :user)]
@ -43,7 +43,8 @@
:users (auth/list-users)})))) :users (auth/list-users)}))))
(defn delete-user (defn delete-user
"Delete a user." "Render a form allowing a user to be deleted; and
process that form.."
[request] [request]
(let [params (keywordize-keys (:params request)) (let [params (keywordize-keys (:params request))
target (:target params) target (:target params)
@ -59,7 +60,8 @@
(defn edit-user (defn edit-user
"Put an individual user's details on screen for editing." "Render a form showing an individual user's details for editing; and
process that form."
[request] [request]
(let [params (keywordize-keys (:params request))] (let [params (keywordize-keys (:params request))]
(try (try
@ -98,7 +100,7 @@
:details details :details details
:users (auth/list-users)}))) :users (auth/list-users)})))
(catch Exception any (catch Exception any
(timbre/error (.getMessage any)) (timbre/error any)
(layout/render "edit-user.html" (layout/render "edit-user.html"
(merge (util/standard-params request) (merge (util/standard-params request)
{:title (str (:edit-title-prefix (util/get-messages request)) " " (:target params)) {:title (str (:edit-title-prefix (util/get-messages request)) " " (:target params))

View file

@ -4,23 +4,34 @@
(: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]
[markdown.core :as md]
[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]
[smeagol.layout :as layout] [smeagol.layout :as layout]
[smeagol.local-links :refer :all]
[smeagol.routes.admin :as admin] [smeagol.routes.admin :as admin]
[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]
[smeagol.configuration :refer [config]]
[smeagol.include.resolve-local-file :as resolve]
[smeagol.include :as include]
[smeagol.util :refer [content-dir local-url]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -45,27 +56,21 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-git-repo
"Get the git repository for my content, creating it if necessary"
[]
(hist/load-or-init-repo util/content-dir))
(defn process-source (defn process-source
"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]
(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)
file-path (cjio/file util/content-dir file-name) file-path (cjio/file util/content-dir file-name)
exists? (.exists (cjio/as-file file-path)) exists? (.exists (cjio/as-file file-path))
git-repo (get-git-repo) git-repo (hist/load-or-init-repo util/content-dir)
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})
@ -95,16 +100,19 @@
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/md-to-html-string
(local-links
(slurp (cjio/file content-dir side-bar)))
:heading-anchors true)
:content (if exists? (slurp file-path) "") :content (if exists? (slurp file-path) "")
:exists exists?}))))))) :exists exists?})))))))
@ -115,26 +123,106 @@
(edit-page request "stylesheet" ".css" "edit-css.html" "_edit-side-bar.md")) (edit-page request "stylesheet" ".css" "edit-css.html" "_edit-side-bar.md"))
(def md-include-system
"Allowing Markdown includes. Unfortunately the contributor who contributed
this didn't document it, and I haven't yet worked out how it works. TODO:
investigate and document."
(component/start
(component/system-map
:resolver (resolve/new-resolver util/content-dir)
:includer (component/using
(include/new-includer)
[:resolver]))))
(defn preferred-source
"Here, `component` is expected to be a map with two keys, `:local` and
`:remote`. If the value of `:extensions-from` in `config.edn` is remote
AND the value of `:remote` is not nil, then the value of `:remote` will
be returned. Otherwise, if the value of `:local` is nil and the value of
`:remote` is non-nil, the value of `:remote` will be returned. By default,
the value of `:local` will be returned."
[component 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
"Collect preferred variants of resources required by extensions used in the
page described in this `processed-text`."
([processed-text]
(concat
(collect-preferred processed-text :scripts)
(collect-preferred processed-text :styles)))
([processed-text resource-type]
(reduce
concat
(map
(fn [extension-key]
(map
(fn [requirement]
(let [r (preferred-source
(-> processed-text :extensions extension-key
resource-type requirement)
requirement)]
(if (empty? r)
(log/warn "Found no valid URL for requirement"
requirement "of extension" extension-key))
r))
(keys (-> processed-text :extensions extension-key resource-type))))
(keys (:extensions processed-text))))))
;; (cjio/file content-dir "vendor/node_modules/photoswipe/dist/photoswipe.min.js")
;; (def processed-text (md->html {: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]
(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))
page (or (:page params) (util/get-message :default-page-title "Introduction" request)) page (or (:page params) util/start-page (util/get-message :default-page-title "Introduction" request))
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)) (slurp file-path))))]
:editable true}))) (layout/render "wiki.html"
true (response/redirect (str "/edit?page=" page)))))) (merge (util/standard-params request)
processed-text
{:title page
:scripts (collect-preferred processed-text :scripts)
:styles (collect-preferred processed-text :styles)
:page page
:editable true}))))
;else
(response/redirect (str "/edit?page=" page))))))
(defn history-page (defn history-page
@ -145,42 +233,105 @@
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 (str "History of " page) {: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
"File name extensions suggesting files which can be considered to be images."
#{".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 (io/resource-path) "/content/uploads/") data-path (str util/content-dir "/uploads/")
git-repo (get-git-repo) 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
@ -191,7 +342,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)
@ -206,7 +357,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
@ -224,20 +375,22 @@
(defn auth-page (defn auth-page
"Render the auth page" "Render the authentication (login) page"
[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))
@ -250,8 +403,23 @@
{: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
;; TODO TODO: it's also not working.
[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"
@ -277,8 +445,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)))
@ -288,11 +458,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))
)

View file

@ -1,4 +1,7 @@
(ns ^{:doc "Functions related to sanity checks and error reporting in conditions where the environment may not be sane." (ns ^{:doc "Functions related to sanity checks and error reporting in conditions
where the environment may not be sane. Generally, the functions in this
file are called (via `sanity-check-installation`, which is the only
supported entry point) at first start-up."
:author "Simon Brooke"} :author "Simon Brooke"}
smeagol.sanity smeagol.sanity
(:import (java.util Locale)) (:import (java.util Locale))
@ -252,13 +255,28 @@
(as-hiccup [this dictionary] "") (as-hiccup [this dictionary] "")
clojure.lang.Keyword clojure.lang.Keyword
(as-hiccup [this dictionary] (str (or (this dictionary)(string/replace (name this) "-" " ")) " ")) (as-hiccup [this dictionary]
(str
(or
(this dictionary)
(string/replace (name this) "-" " "))
" "))
clojure.lang.PersistentList clojure.lang.PersistentList
(as-hiccup [this dictionary] (apply vector (cons :div (map #(as-hiccup % dictionary) this)))) (as-hiccup [this dictionary]
(apply
vector
(cons
:div
(map #(as-hiccup % dictionary) this))))
clojure.lang.PersistentVector clojure.lang.PersistentVector
(as-hiccup [this dictionary] (apply vector (cons :div (map #(as-hiccup % dictionary) this)))) (as-hiccup [this dictionary]
(apply
vector
(cons
:div
(map #(as-hiccup % dictionary) this))))
clojure.lang.PersistentArrayMap clojure.lang.PersistentArrayMap
(as-hiccup [this dictionary] (as-hiccup [this dictionary]
@ -296,66 +314,94 @@
vector vector
(cons (cons
:div :div
(cons (cons
{:class "sanity-exception"} {:class "sanity-exception"}
(map (map
(fn [x] (fn [x]
[:div [:div
{:class "sanity-cause"} {:class "sanity-cause"}
(.getMessage x) [:h2 (.getMessage x)]
[:div {:class "sanity-stacktrace"} [:div {:class "sanity-stacktrace"}
(apply (apply
vector vector
(cons (cons
:ol :ol
(map (map
as-hiccup as-hiccup
(.getStackTrace x) (.getStackTrace x)
dictionary)))]]) dictionary)))]])
(get-causes this)))))) (get-causes this))))))
java.lang.Object java.lang.Object
(as-hiccup [this dictionary] (str this " "))) (as-hiccup [this dictionary] (str this " ")))
(defn sanity-check-report (defn get-locale-messages
[problems] "Get messages for the server-side locale."
[]
(let [locale (Locale/getDefault) (let [locale (Locale/getDefault)
locale-specifier (str (.getLanguage locale) "-" (.getCountry locale)) locale-specifier (str (.getLanguage locale) "-" (.getCountry locale))]
messages (try (try
(i18n/get-messages locale-specifier "i18n" "en-GB") (i18n/get-messages locale-specifier "i18n" "en-GB")
(catch Exception any {}))] (catch Exception any {}))))
;; Prepackaged hiccup sub-units
(defn as-hiccup-head
[messages]
[:head
[:title (as-hiccup :smeagol-not-initialised messages)]
[:link {:href "/content/stylesheet.css" :rel "stylesheet"}]])
(defn as-hiccup-header
[messages]
[:header
[:div {:id "nav"} "&nbsp;"]
[:h1 (as-hiccup :smeagol-not-initialised messages)]
[:p "&nbsp;"]])
(defn as-hiccup-see-doc
[messages]
[:p (as-hiccup :see-documentation messages)
[:a
{:href
"https://github.com/journeyman-cc/smeagol/wiki/Deploying-Smeagol"}
(as-hiccup :here messages)] "."])
(defn as-hiccup-footer
[messages]
[:footer
[:div {:id "credits"}
[:div
[:img {:height "16" :width "16" :alt "one wiki to rule them all" :src "img/smeagol.png"}]
" One Wiki to rule them all || Smeagol wiki engine || "
[:img
{:height "16" :width "16"
:alt "The Web Engineering Factory &amp; Toolworks"
:src "http://www.weft.scot/images/weft.logo.64.png"}]
" Developed by "
[:a {:href "http://www.weft.scot/"}"WEFT"]]]])
(defn sanity-check-report
"Convert this `problem` report into a nicely formatted HTML page"
[problems]
(let [messages (get-locale-messages)]
(html (html
[:html [:html
[:head (as-hiccup-head messages)
[:title (as-hiccup :smeagol-not-initialised messages)]
[:link {:href "/content/stylesheet.css" :rel "stylesheet"}]]
[:body [:body
[:header (as-hiccup-header messages)
[:div {:id "nav"} "&nbsp;"] [:div {:id "error"}
[:h1 (as-hiccup :smeagol-not-initialised messages)] [:p {:class "error"}
[:p "&nbsp;"]] (rest (as-hiccup [(count (keys problems)) :problems-found] messages))]]
[:div {:id "error" :class "error"}
[:div {:class "error"}
(as-hiccup [(count (keys problems)) :problems-found] messages)]]
[:div {:id "main-container" :class "sanity-check-report"} [:div {:id "main-container" :class "sanity-check-report"}
[:p (as-hiccup :smeagol-misconfiguration messages)] [:p (as-hiccup :smeagol-misconfiguration messages)]
(as-hiccup problems messages) (as-hiccup problems messages)
[:p (as-hiccup :see-documentation messages) (as-hiccup-see-doc messages)]
[:a (as-hiccup-footer messages)]])))
{:href
"https://github.com/journeyman-cc/smeagol/blob/master/resources/public/content/Deploying%20Smeagol.md"}
(as-hiccup :here messages)]]]
[:footer
[:div {:id "credits"}
[:div
[:img {:height "16" :width "16" :alt "one wiki to rule them all" :src "img/smeagol.png"}]
" One Wiki to rule them all || Smeagol wiki engine || "
[:img
{:height "16" :width "16"
:alt "The Web Engineering Factory &amp; Toolworks"
:src "http://www.weft.scot/images/weft.logo.64.png"}]
" Developed by "
[:a {:href "http://www.weft.scot/"}"WEFT"]]]]]])))
(defn- raw-sanity-check-installation (defn- raw-sanity-check-installation
@ -369,7 +415,7 @@
(timbre/warn "Sanity check completed; " (count (keys result)) " problem(s) found") (timbre/warn "Sanity check completed; " (count (keys result)) " problem(s) found")
(sanity-check-report result)) (sanity-check-report result))
(do (do
(timbre/info "Sanity check completed; no problem(s) found") (timbre/info "Sanity check completed; no problems found")
nil)))) nil))))
@ -383,26 +429,26 @@
If no argument is passed, run the sanity check and if it fails return page contents; If no argument is passed, run the sanity check and if it fails return page contents;
if `error` is passed, just return page content describing the error." if `error` is passed, just return page content describing the error."
([error] ([error]
(html (let [messages (get-locale-messages)]
[:html (html
[:head [:html
[:title "Smeagol is not initialised correctly"] (as-hiccup-head messages)
[:link {:href "/content/stylesheet.css" :rel "stylesheet"}]] [:body
[:body (as-hiccup-header messages)
[:header [:div {:id "error"}
[:h1 "Smeagol is not initialised correctly"]] [:p {:class "error"} (.getMessage error)]]
[:div {:id "error"} [:div {:id "main-container" :class "sanity-check-report"}
[:p {:class "error"} (.getMessage error)]] [:p (as-hiccup :smeagol-misconfiguration messages)]
[:p "There was a problem launching Smeagol probably because of misconfiguration:"] (as-hiccup error messages)
(apply (as-hiccup-see-doc messages)]
vector (as-hiccup-footer messages)]])))
(cons :ol
(map #(vector :li (.getMessage %))
(get-causes error))))
[:p :see-documentation
[:a {:href "https://github.com/journeyman-cc/smeagol/blob/develop/resources/public/content/Deploying%20Smeagol.md"} "here"]]]]))
([] ([]
(try (try
(sanity-check-installation) (sanity-check-installation)
(catch Exception any (show-sanity-check-error any))))) (catch Exception any
(timbre/error any "Failure during sanity check")
(show-sanity-check-error any)))))
(show-sanity-check-error (Exception. "That's insane!"))

View file

@ -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,36 +39,97 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 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?
"True if the file at this `filename` appears as though it may be an 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))
(if tmp-file (log/debug
(do (str "store-upload mv file: " tmp-file " to: " path filename))
(.renameTo tmp-file (if tmp-file
(File. (str path filename))) (try
filename) (let [p (io/file path filename)]
(throw (Exception. "No file found?"))))) (.renameTo tmp-file p)
(map
#(assoc % :resource (subs (:location %) (inc (count util/content-dir))))
(remove
nil?
(cons
{:size :original
:filename filename
:location (str p)
:is-image (and (image? filename) true)}
(remove nil? (or (auto-thumbnail path filename) '()))))))
(catch Exception x
(log/error (str "Failed to move " tmp-file " to " path filename "; " (type x) ": " (.getMessage x)))
(throw x)))
(throw (Exception. "No file found?")))))

View file

@ -2,13 +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 log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -34,11 +38,99 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def content-dir (def start-page
(or "The page to load on startup, taken from configuration."
(env :smeagol-content-dir) (:start-page config))
(cjio/file (io/resource-path) "content")))
(def content-dir
"The absolute path to the directory in which Wiki content (i.e., Markdown
files) are stored."
(str
(fs/absolute
(or
(:content-dir config)
(cjio/file (io/resource-path) "content")))))
(def upload-dir
"The absolute path to the directory in which uploaded files are stored."
(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."
@ -46,30 +138,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
(merge (fn [request]
(i18n/get-messages (let [specifier ((:headers request) "accept-language")
((:headers request) "accept-language") messages (try
"i18n" (i18n/get-messages specifier "i18n" "en-GB")
"en-GB") (catch Exception any
config)) (log/error
any
(str
(def get-messages (memoize raw-get-messages)) "Failed to parse accept-language header '"
specifier
"'"))
{}))]
(merge
messages
config)))))
(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]

View file

@ -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
![Frost on a gate, Laurieston](content/uploads/g1.jpg)
![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg)
![Feathered snow on log, Taliesin](content/uploads/g3.jpg)
![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)"
"test"
smeagol.extensions.test/process-test)
:inclusions
:inclusion-3)
expected "<!-- 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"
"![Frost on a gate, Laurieston](content/uploads/g1.jpg)"
"```"
""
"This concludes the test"])} ))
expected (str
"<h1 id=\"this&#95;is&#95;a&#95;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))))

View file

@ -0,0 +1,106 @@
(ns smeagol.test.include
(:require [clojure.test :refer :all]
[schema.core :as s]
[com.stuartsierra.component :as component]
[smeagol.include.resolve :as resolve]
[smeagol.include :as sut]))
(def include-simple
"# Heading1
&[](./simple.md)")
(def include-surounding-simple
"# Heading1
Some surounding &[](./simple.md) text")
(def include-heading-0
"# Heading1
&[:indent-heading 0](./with-heading.md)")
(def include-heading-list-1
"# Heading1
&[:indent-heading 1 :indent-list 1](./with-heading-and-list.md)")
(def include-heading-list-0
"# Heading1
&[:indent-list 0 :indent-heading 0](./with-heading-and-list.md)")
(def include-invalid-indent
"# Heading1
&[ invalid input should default to indent 0 ](./simple.md)")
(def include-spaced-indent
"# Heading1
&[ :indent-heading 2 :indent-list 33 ](./with-heading-and-list.md)")
(def multi
"# Heading1
&[ :indent-heading 2 :indent-list 33 ](./with-heading-and-list.md)
some text
&[](./simple.md)
more text.")
(s/defmethod resolve/do-resolve-md :test-mock
[resolver
uri :- s/Str]
(cond
(= uri "./simple.md") "Simple content."
(= uri "./with-heading-and-list.md") "# Heading2
some text
* List
## Heading 3
more text"))
(def system-under-test
(component/start
(component/system-map
:resolver (resolve/new-resolver :test-mock)
:includer (component/using
(sut/new-includer)
[:resolver]))))
(deftest test-expand-include-md
(testing "The whole integration of include"
(is
(= "# Heading"
(sut/expand-include-md (:includer system-under-test) "# Heading")))
(is
(= "# Heading1
Simple content."
(sut/expand-include-md
(:includer system-under-test)
include-simple)))
(is
(= "# Heading1
Some surounding Simple content. text"
(sut/expand-include-md
(:includer system-under-test)
include-surounding-simple)))
(is
(= "# Heading1
# Heading2
some text
* List
## Heading 3
more text"
(sut/expand-include-md
(:includer system-under-test)
include-heading-list-0)))
(is
(= "# Heading1
### Heading2
some text
* List
#### Heading 3
more text
some text
Simple content.
more text."
(sut/expand-include-md
(:includer system-under-test)
multi)))))

View file

@ -0,0 +1,35 @@
(ns smeagol.test.include.indent
(:require [clojure.test :refer :all]
[smeagol.include.indent :as sut]))
(deftest test-parse-heading
(testing
(is (= '(["# " "# " "" "# "])
(sut/parse-heading "# h1")))
(is (= '(["\n# " "\n# " "\n" "# "])
(sut/parse-heading "\n# h1")))))
(deftest test-indent-heading
(testing
(is (= "# h1"
(sut/do-indent-heading 0 "# h1")))
(is (= "### h1"
(sut/do-indent-heading 2 "# h1")))
(is (= "\n### h1"
(sut/do-indent-heading 2 "\n# h1")))))
(deftest test-parse-list
(testing
(is (= '([" * " " * " " " "* "])
(sut/parse-list " * list")))
(is (= '(["\n * " "\n * " "\n " "* "])
(sut/parse-list "\n * list")))))
(deftest test-indent-list
(testing
(is (= " * list"
(sut/do-indent-list 0 " * list")))
(is (= " * list"
(sut/do-indent-list 2 " * list")))
(is (= "\n * list"
(sut/do-indent-list 2 "\n * list")))))

View file

@ -0,0 +1,91 @@
(ns smeagol.test.include.parse
(:require [clojure.test :refer :all]
[schema.core :as s]
[smeagol.include.parse :as sut]))
(def include-simple
"# Heading1
&[](./simple.md)")
(def include-surounding-simple
"# Heading1
Some surounding &[](./simple.md) text")
(def include-heading-0
"# Heading1
&[:indent-heading 0](./with-heading.md)")
(def include-heading-list-1
"# Heading1
&[:indent-heading 1 :indent-list 1](./with-heading-and-list.md)")
(def include-heading-list-0
"# Heading1
&[:indent-list 0 :indent-heading 0](./with-heading-and-list.md)")
(def include-invalid-indent
"# Heading1
&[ invalid input should default to indent 0 ](./simple.md)")
(def include-spaced-indent
"# Heading1
&[ :indent-heading 2 :indent-list 33 ](./with-heading-and-list.md)")
(def multi
"# Heading1
&[ :indent-heading 2 :indent-list 33 ](./with-heading-and-list.md)
some text
&[](./simple.md)
more text.")
(deftest test-parse-include-md
(testing "parse include links"
(is
(= []
(sut/parse-include-md "# Heading")))
(is
(= [{:replace "&[](./simple.md)" :uri "./simple.md", :indent-heading 0, :indent-list 0}]
(sut/parse-include-md
include-simple)))
(is
(= [{:replace "&[](./simple.md)" :uri "./simple.md", :indent-heading 0, :indent-list 0}]
(sut/parse-include-md
include-surounding-simple)))
(is
(= [{:replace "&[:indent-heading 0](./with-heading.md)" :uri "./with-heading.md", :indent-heading 0, :indent-list 0}]
(sut/parse-include-md
include-heading-0)))
(is
(= [{:replace
"&[:indent-heading 1 :indent-list 1](./with-heading-and-list.md)"
:uri "./with-heading-and-list.md", :indent-heading 1, :indent-list 1}]
(sut/parse-include-md
include-heading-list-1)))
(is
(= [{:replace
"&[:indent-list 0 :indent-heading 0](./with-heading-and-list.md)"
:uri "./with-heading-and-list.md", :indent-heading 0, :indent-list 0}]
(sut/parse-include-md
include-heading-list-0)))
(is
(= [{:replace
"&[ invalid input should default to indent 0 ](./simple.md)"
:uri "./simple.md", :indent-heading 0, :indent-list 0}]
(sut/parse-include-md
include-invalid-indent)))
(is
(= [{:replace
"&[ :indent-heading 2 :indent-list 33 ](./with-heading-and-list.md)"
:uri "./with-heading-and-list.md", :indent-heading 2, :indent-list 3}]
(sut/parse-include-md
include-spaced-indent)))
(is
(= [{:replace
"&[ :indent-heading 2 :indent-list 33 ](./with-heading-and-list.md)"
:uri "./with-heading-and-list.md",
:indent-heading 2,
:indent-list 3}
{:replace "&[](./simple.md)" :uri "./simple.md", :indent-heading 0, :indent-list 0}]
(sut/parse-include-md
multi)))))

View file

@ -0,0 +1,8 @@
(ns smeagol.test.include.resolve
(:require [clojure.test :refer :all]
[smeagol.include.resolve :as sut]))
(deftest test-local-links
(testing "Rewriting of local links"
(is (thrown? Exception
(sut/resolve-md (sut/new-resolver (:default)) "./some-uri.md")))))

View 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)))))