Compare commits

...

111 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
2cf30bd695 Merge branch 'release/1.0.0' into develop 2017-09-12 17:05:16 +01:00
80 changed files with 4123 additions and 544 deletions

14
.gitignore vendored
View file

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

157
README.md
View file

@ -1,162 +1,41 @@
![One wiki to rule them all](https://www.weft.scot/images/smeagol.png)
# 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>
## Status
Smeagol is now a fully working small Wiki engine, and meets my own immediate needs.
## Using Smeagol
Read the [[User Documentation]] for an introduction to all Smeagol's features.
## 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.
### 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.
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 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}
:adam {:password "secret" :email "adam@localhost"}}
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.
## 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
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)
## Advertisement
If you like what you see here, I am available for work on open source Clojure projects.
## 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 about [[Configuring Smeagol]] and [[Deploying Smeagol]].
### Phoning home
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
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>
## Developing Smeagol
Smeagol is an open source project; you're entitled to make changes yourself. Read more about [[Developing Smeagol]].
## 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
parts of Smeagol into another open source project which uses a less restrictive
license, please contact me; I'm open to dual licensing it.
## Prerequisites
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed.
## 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).
You will need [node](https://nodejs.org/en/) and [bower](https://bower.io/) installed.
## 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.
## Advertisement
If you like what you see here, I am available for work on open source Clojure projects.

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,12 +1,14 @@
(defproject smeagol "1.0.0"
(defproject smeagol "1.0.4a"
:description "A simple Git-backed Wiki inspired by Gollum"
:url "https://github.com/simon-brooke/smeagol"
:license {:name "GNU General Public License,version 2.0 or (at your option) any later version"
:url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"}
:dependencies [[clj-jgit "0.8.10"]
[clj-yaml "0.4.0"]
[clojure.java-time "0.3.2"]
[com.cemerick/url "0.1.1"]
[com.fzakaria/slf4j-timbre "0.3.7"]
[com.stuartsierra/component "0.4.0"]
[com.taoensso/encore "2.92.0"]
[com.taoensso/timbre "4.10.0"]
[com.taoensso/tower "3.0.2" :exclusions [com.taoensso/encore]]
@ -14,18 +16,23 @@
[environ "1.1.0"]
[hiccup "1.0.5"]
[im.chit/cronj "1.4.4"]
[image-resizer "0.1.10"]
[instaparse "1.4.10"]
[lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]]
[markdown-clj "0.9.99" :exclusions [com.keminglabs/cljx]]
[me.raynes/fs "1.4.6"]
[noir-exception "0.2.5"]
[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/data.json "0.2.6"]
[org.clojure/tools.logging "0.4.0"]
[org.clojure/tools.trace "0.7.10"]
[org.slf4j/slf4j-api "1.7.25"]
[org.slf4j/log4j-over-slf4j "1.7.25"]
[org.slf4j/jul-to-slf4j "1.7.25"]
[org.slf4j/jcl-over-slf4j "1.7.25"]
[prismatic/schema "1.1.9"]
[prone "1.1.4"]
[ring/ring-anti-forgery "1.1.0"]
[ring-server "0.4.0"]
@ -36,18 +43,21 @@
:jvm-opts ["-server"]
:plugins [[lein-ancient "0.5.5" :exclusions [org.clojure/clojure org.clojure/data.xml]]
[lein-bower "0.5.1"]
[lein-codox "0.10.3"]
[io.sarnowski/lein-docker "1.0.0"]
[lein-environ "1.0.0"]
[lein-marginalia "0.7.1" :exclusions [org.clojure/clojure]]
[lein-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"]
;; [vega-embed "3.0.0-beta.20"] ;; vega-embed currently not loaded from Bower because of
;; dependency conflict which will hopefully be resolved soon.
[vega-lite "2.0.0-beta.11"]
[mermaid "6.0.0"]]
:npm {:dependencies [[mermaid "8.13.8"]
[photoswipe "4.1.3"]
[simplemde "1.11.2"]
[tablesort "5.3.0"]
[vega "5.21.0"]
[vega-embed "6.20.5"]
[vega-lite "5.2.0"]]
:root "resources/public/vendor"}
:docker {:image-name "simonbrooke/smeagol"
:dockerfile "Dockerfile"}
@ -56,13 +66,19 @@
:init smeagol.handler/init
: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"]
["clean"]
["codox"]
["change" "version" "leiningen.release/bump-version" "release"]
["vcs" "commit"]
;; ["vcs" "tag"] -- not working, problems with secret key
["clean"]
["bower" "install"]
["ring" "uberjar"]
["deploy"]
["docker" "build"]
["docker" "push"]
["change" "version" "leiningen.release/bump-version"]

View file

@ -30,14 +30,48 @@
:content-dir "resources/public/content"
;; where content is served from.
: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}
: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
;; 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
:chpass-success "Your password was changed"
;; 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
:chpass-title-prefix "Change password for"
;; prefix for title of change password page
@ -83,6 +83,8 @@
:file-upload-title "Upload a file" ;; title for the file upload page
:is-admin-prompt "Is administrator?"
:here "here" ;; used in sanity check report
:history-link "History" ;; text of the history link on the content frame
:history-title-prefix "History of" ;; prefix of the title on the history page
:home-link "Home" ;; text of the home link on the menu
:is-not-directory "is not a directory"
;; (of a file or directory) used in sanity check report
@ -90,6 +92,8 @@
;; (of a file or directory) used in sanity check report
:is-not-writable "is not writable"
;; (of a file or directory) used in sanity check report
:list-files "List uploaded files"
;; title of the 'List uploaded Files' page
:login-label "Log in!" ;; text of the login widget on the login page
:login-link "Log in" ;; text of the login link on the menu
:login-prompt "To edit this wiki"
@ -98,8 +102,7 @@
:logout-link "Log out" ;; text of the logout link on the menu
:logged-in-as "You are logged in as"
;; text of the 'logged in as' label on the menu
:history-link "History" ;; text of the history link on the content frame
:history-title-prefix "History of" ;; prefix of the title on the history page
:matching "matching" ;; 'matching' in e.g. 'list files matching fred'
:new-pass-prompt "New password" ;; text of the new password widget prompt on the change
;; password and edit user pages
:no-admin-users "There are no users in the 'passwd' file with administrative privileges"
@ -128,6 +131,8 @@
"Smeagol has been unable to find some of the resources on which it depends,
possibly because of misconfiguration or missing environment variables."
;; used in sanity check report
:sortable "You can sort this table by selecting column headers"
;; used for sortable tables
:user-lacks-field "User record in the passwd file lacks a field"
;; used in sanity check report
:username-prompt "Username" ;; text of the username widget prompt on edit user page

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"}, :simon {:email "simon@journeyman.cc", :admin true, :password "$s0$f0801$sqhbxtzK6nx9RnVUhwtQlg==$dMIUbof8esjsGyiB+zb3gMH21L/WSCR+wD3vIag4EVc="}}
{: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,33 +0,0 @@
Smeagol reads a configuration file, whose content should be formatted as a clojure map.
The default content is as follows:
```
{
:site-title "Smeagol" ;; overall title of the site, used in page headings
:default-locale "en-GB" ;; default language used for messages
:content-dir "/usr/local/etc/content"
;; where content is served from
:passwd "/usr/local/etc/passwd"
;; where the password file is stored
:log-level :info ;; the minimum logging level; one of
;; :trace :debug :info :warn :error :fatal
:formatters {"vega" smeagol.formatting/process-vega
"vis" smeagol.formatting/process-vega
"mermaid" smeagol.formatting/process-mermaid
"backticks" smeagol.formatting/process-backticks}
}
```
The values should be:
* `:content-dir` The directory in which your editable content is stored;
* `:default-locale` A string comprising a lower-case [ISO 639](https://en.wikipedia.org/wiki/ISO_639) code specifying a language, optionally followed by a hyphen and an upper-case [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166) specifying a country.
* `:formatters` A map of formatters used in [[Extensible Markup]], q.v.
* `:log-level` The minimum level of log messages to be logged; one of `:trace :debug :info :warn :error :fatal`
* `:passwd` The path to your `passwd` file - see [[Security and authentication]];
* `:site-title` The title for your wiki.
The default file is at `resources/config.edn`; this default can be overridden by providing an environment variable, `SMEAGOL_CONFIG`, whose value is the full or relative pathname of a suitable file.
Note that all the values in the configuration can be overridden with [[Environment Variables]].

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

@ -50,25 +50,6 @@ Smeagol will run as a web-app with the default configuration perfectly satisfact
## 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 [node](https://nodejs.org/en/) and [bower](https://bower.io/) installed.
You will need [node](https://nodejs.org/en/) installed.
## Running in development
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.
## 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.
## 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
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
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

@ -3,12 +3,12 @@ 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.
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:
```
@ -24,7 +24,7 @@ This works for play purposes. However, it means that any edits made to either th
## 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/).
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:
@ -41,7 +41,7 @@ This works, and uses the default values of the environment variables which are s
## 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 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.
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

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
Inspired by [visdown](http://visdown.amitkaps.com/) and [vega-lite](https://vega.github.io/vega-lite/docs/), the Vega formatter allows you to embed vega data visualisations into Smeagol pages. The graph description should start with a line comprising three back-ticks and then the word '`vega`', and end with a line comprising just three backticks.
Inspired by [visdown](https://visdown.com/) and [vega-lite](https://vega.github.io/vega-lite/docs/), the Vega formatter allows you to embed vega data visualisations into Smeagol pages. The graph description should start with a line comprising three back-ticks and then the word '`vega`', and end with a line comprising just three backticks.
Here's an example cribbed in its entirety from [here](http://visdown.amitkaps.com/london):
@ -34,9 +34,9 @@ Data files can be uploaded in the same way as images, by using the **upload a fi
## The Mermaid formatter
Graphs can now be embedded in a page using the [Mermaid](http://knsv.github.io/mermaid/index.html) graph description language. The graph description should start with a line comprising three back-ticks and then the word `mermaid`, and end with a line comprising just three backticks.
Graphs can now be embedded in a page using the [Mermaid](https://mermaid-js.github.io/mermaid/#/) graph description language. The graph description should start with a line comprising three back-ticks and then the word `mermaid`, and end with a line comprising just three backticks.
Here's an example culled from the Mermaid documentation.
Here's an example culled from the Mermaid documentation. Edit this page to see the specification.
### GANTT Chart
@ -58,6 +58,19 @@ gantt
Add to mermaid :1d
```
Mermaid graph specifications can also be loaded from URLs. Here's another example; again, edit this page to see how the trick is done.
### Class Diagram
```mermaid
data/classes.mermaid
```
## Photoswipe galleries
Not so much a formatter, this is an extension to allow you to embed image galleries in your markdown. To specify a gallery, use three backticks followed by `pswp`, followed on the following lines by a [Photoswipe](https://photoswipe.com/documentation/getting-started.html) specification in [JSON](https://www.json.org/json-en.html)
followed by three backticks on a line by themselves. There is an [[Example gallery]] with the full PhotoSwipe configuration, and a [[Simplified example gallery]] using a much simpler syntax, so that you can see how this works.
## Writing your own custom formatters
A custom formatter is simply a Clojure function which takes a string and an integer as arguments and produces a string as output. The string is the text the user has typed into their markdown; the integer is simply a number you can use to keep track of which addition to the page this is, in order, for example, to fix up some JavaScript to render it.

View file

@ -1,35 +1,33 @@
![One wiki to rule them all](https://www.weft.scot/images/smeagol.png)
# 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.
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.
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.
## Using Smeagol
Read the [[User Documentation]] for an introduction to all Smeagol's features.
## 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
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
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
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
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
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
parts of Smeagol into another open source project which uses a less restrictive
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
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, 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.
@ -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.
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.

View file

@ -10,4 +10,6 @@
+ \*\***bold**\*\*
+ \__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]]
* [[Change log]]
* [[User Documentation]]
* [[Configuring Smeagol]]
* [[Deploying Smeagol]]
* [[Developing Smeagol]]

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>

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>
<h1>{% i18n site-title %}: {{title}}</h1>
{{header|safe}}
<div id="header">
{{header|safe}}
</div>
{% if message %}
<div id="message">
<p class="message">{{message}}</p>

View file

@ -1,10 +1,18 @@
{% extends "templates/base.html" %}
{% block extra-headers %}
{% script "/vendor/node_modules/tablesort/dist/tablesort.min.js" %}
{% endblock %}
{% block content %}
<div id="content">
<table>
<tr>
<th/><th>{% i18n edit-col-hdr %}</th><th>{% i18n del-col-hdr %}</th>
<p>
{% i18n sortable %}
</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>
{% for user in users %}
<tr>
@ -13,11 +21,12 @@
<td><a href="delete-user?target={{user}}">{% i18n del-col-hdr %} {{user}}</a></td>
</tr>
{% endfor %}
<tr>
<td><a href="edit-user">{% i18n add-user-label %}</a></td>
<td></td>
<td></td>
<tr data-sort-method='none'>
<td colspan="3"><a href="edit-user">{% i18n add-user-label %}</a></td>
</tr>
</table>
</div>
<script>
new Tablesort(document.getElementById('userstable'));
</script>
{% endblock %}

View file

@ -1,7 +1,12 @@
{% extends "templates/base.html" %}
{% block extra-headers %}
{% style "/vendor/simplemde/dist/simplemde.min.css" %}
{% script "/vendor/simplemde/dist/simplemde.min.js" %}
{% ifequal js-from ":cdnjs" %}
<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 %}
{% block content %}
@ -13,7 +18,7 @@
<p class="widget">
<label for="summary">{% i18n what-changed-prompt %}</label>
<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 class="widget">
<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" %}
{% block content %}
<div id="content" class="auth">
{% if uploaded %}
{% if is-image %}
<p>
<img id="uploaded-image" alt="Uploaded image" src="content/uploads/{{uploaded}}"/>
{% if uploaded|not-empty %}
{% for upload in uploaded %}
{% if upload.is-image %}
<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>
</p>
{% else %}
<p>
{% i18n file-upload-link-text %}:
<code>![{{upload.filename}}]({{upload.resource}})</code>
</p>
{% else %}
<p>
{% i18n file-upload-link-text %}:
<code>[Uploaded file](uploads/{{uploaded}})</code>
</p>
{% endif %}
<code>[{{upload.filename}}]({{upload.resource}})</code>
</p>
{% endif %}
<br clear="right"/>
<hr/>
{% endfor %}
{% else %}
<form action="{{servlet-context}}/upload" enctype="multipart/form-data" method="POST">
{% csrf-field %}
@ -34,5 +39,8 @@
</p>
</form>
{% endif %}
<p>
Your <a href="list-uploads">uploaded files are listed here</a>.
</p>
</div>
{% endblock %}

View file

@ -1,15 +1,10 @@
{% extends "templates/base.html" %}
{% block extra-headers %}
{% style "vendor/mermaid/dist/mermaid.css" %}
<!-- there's at the time of writing (20170731) a problem with the dependencies of the Bower
package for vega-embed, so we're currently not installing either it or Vega locally.
TODO: fix -->
<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" %}
{% for script in scripts %}
<script src="{{script}}"></script>{% endfor %}
{% for style in styles %}
<link href="{{style}}" rel="stylesheet" type="text/css" />{% endfor %}
{% endblock %}
{% 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

@ -5,7 +5,7 @@
[environ.core :refer [env]]
[noir.io :as io]
[smeagol.configuration :refer [config]]
[taoensso.timbre :as timbre]))
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -37,6 +37,8 @@
;; the relative path to the password file.
(def password-file-path
"Path to the password file."
;; TODO: portability; elegance. Not very happy with this.
(or
(:passwd config)
(str (io/resource-path) "../passwd")))
@ -52,7 +54,7 @@
"Return `true` if this `username`/`password` pair match, `false` otherwise"
[username password]
(let [user ((keyword username) (get-users))]
(timbre/info (str "Authenticating " username " against " password-file-path))
(log/info (str "Authenticating " username " against " password-file-path))
(and user
(:password user)
(or
@ -92,7 +94,7 @@
Return `true` if password was successfully changed. Subsequent to user change, their
password will be encrypted."
[username oldpass newpass]
(timbre/info (format "Changing password for user %s" username))
(log/info (format "Changing password for user %s" username))
(let [users (get-users)
keywd (keyword username)
user (keywd users)
@ -110,10 +112,10 @@
{keywd
(merge user
{:password (password/encrypt newpass)})})))
(timbre/info (str "Successfully changed password for user " username))
(log/info (str "Successfully changed password for user " username))
true))
(catch Exception any
(timbre/error any
(log/error any
(format "Changing password failed for user %s failed: %s (%s)"
username (.getName (.getClass any)) (.getMessage any)))
false))))
@ -138,7 +140,7 @@
`email` address and `admin` flag; *or*, modify an existing user. Return true
if user is successfully stored, false otherwise."
[username newpass email admin]
(timbre/info "Trying to add user " username)
(log/info "Trying to add user " username)
(cond
(not (string? username)) (throw (Exception. "Username must be a string."))
(zero? (count username)) (throw (Exception. "Username cannot be zero length"))
@ -160,10 +162,10 @@
(locking password-file-path
(spit password-file-path
(assoc users (keyword username) (merge user full-details)))
(timbre/info "Successfully added user " username)
(log/info "Successfully added user " username)
true)
(catch Exception any
(timbre/error any
(log/error any
(format "Adding user %s failed: %s (%s)"
username (.getName (.getClass any)) (.getMessage any)))
false)))))
@ -177,10 +179,10 @@
(locking password-file-path
(spit password-file-path
(dissoc users (keyword username)))
(timbre/info (str "Successfully deleted user " username))
(log/info (str "Successfully deleted user " username))
true)
(catch Exception any
(timbre/error any
(log/error any
(format "Deleting user %s failed: %s (%s)"
username (.getName (.getClass any)) (.getMessage any)))
false))))

View file

@ -5,7 +5,7 @@
[clojure.string :as s]
[environ.core :refer [env]]
[noir.io :as io]
[taoensso.timbre :as timbre]))
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -73,7 +73,7 @@
and optionally a key :transform, whose value is a function of one
argument to be used to transform the value of that key."
[m tuples]
(timbre/debug
(log/debug
"transform-map:\n"
(with-out-str (clojure.pprint/pprint m)))
(reduce
@ -100,38 +100,52 @@
'( {: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}))
(defn build-config
[]
(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."
(try
(let [file-contents (try
(read-string (slurp config-file-path))
(catch Exception _ {}))
config (merge
file-contents
(transform-map
(from-env-vars
:smeagol-content-dir
:smeagol-default-locale
:smeagol-formatters
:smeagol-log-level
:smeagol-passwd
:smeagol-site-title)
config-env-transforms))]
(if (env :dev)
(timbre/debug
"Loaded configuration\n"
(with-out-str (clojure.pprint/pprint config))))
config)
(catch Exception any
(timbre/error any "Could not load configuration")
{})))
(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 (build-config))
(def config
"The actual configuration, as a map."
(build-config))

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"}
smeagol.formatting
(:require [clojure.data.json :as json]
@ -6,7 +6,12 @@
[cemerick.url :refer (url url-encode url-decode)]
[clj-yaml.core :as yaml]
[markdown.core :as md]
[smeagol.configuration :refer [config]]))
[smeagol.configuration :refer [config]]
[smeagol.extensions.mermaid :refer [process-mermaid]]
[smeagol.extensions.photoswipe :refer [process-photoswipe]]
[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)
(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
"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]
(str "<pre class=\"backticks\">```" (.trim text) "\n```</pre>"))
(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]
(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
@ -112,8 +73,11 @@
As with `process-text`, this function returns a map with two top-level keys:
`:inclusions`, a map of constructed keywords to inclusion specifications,
and `:text`, an HTML text string with the keywords present where the
corresponding inclusion should be inserted."
[index result fragment fragments processed]
corresponding inclusion should be inserted.
**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
(inc index)
result
@ -121,62 +85,90 @@
(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
"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:
`:inclusions`, a map of constructed keywords to inclusion specifications,
and `:text`, an HTML text string with the keywords present where the
corresponding inclusion should be inserted."
[index result fragments processed fragment token formatter]
corresponding inclusion should be inserted.
**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
[kw (keyword (str "inclusion-" index))]
[inky (keyword (str "inclusion-" index))
fkey (keyword token)]
(process-text
(inc index)
(assoc-in result [:inclusions kw] (apply formatter (list (subs fragment (count token)) index)))
(rest fragments)
(cons kw processed))))
(deep-merge
result
{:inclusions {inky (eval (list formatter (subs fragment (count token)) index))}
:extensions {fkey (-> config :formatters fkey)}})
(rest fragments)
(cons inky processed))))
(defn process-text
"Process this `text`, assumed to be markdown potentially containing both local links
and YAML visualisation specifications, and return a map comprising JSON visualisation
specification, and HTML text with markers for where those should be reinserted.
(defn- reassemble-text
"Reassemble these processed strings into a complete text, and process it as
Markdown.
The map has two top-level keys: `:inclusions`, a map of constructed keywords to
inclusion specifications, and `:text`, an HTML text string with the keywords
present where the corresponding inclusion should be inserted."
([^String text]
(process-text 0 {:inclusions {}} (cs/split (or text "") #"```") '()))
([index result fragments processed]
(let [fragment (first fragments)
;; if I didn't find a formatter for a back-tick marked fragment,
;; I need to put the backticks back in.
remarked (if (odd? index) (str "```" fragment "\n```") fragment)
first-token (get-first-token fragment)
formatter (eval ((:formatters config) first-token))]
(cond
(empty? fragments)
(assoc result :text
(local-links
(md/md-to-html-string
(cs/join "\n\n" (reverse processed))
:heading-anchors true)))
formatter
(apply-formatter index result fragments processed fragment first-token formatter)
true
(process-markdown-fragment index result remarked (rest fragments) processed)))))
**NOTE** that it is not expected that this function forms part of a stable
API."
[result processed]
(assoc result :text
(local-links
(md/md-to-html-string
(cs/join "\n\n" (reverse processed))
:heading-anchors true))))
(defn reintegrate-inclusions
"Given a map of the form produced by `process-text`, return a string of HTML text
with the inclusions (if any) reintegrated."
(defn- reintegrate-inclusions
"Given a map of the form produced by `process-text`, return a map based on
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]
(reintegrate-inclusions (:inclusions processed-text) (:text processed-text)))
(assoc
processed-text
:content
(reintegrate-inclusions
(:inclusions processed-text)
(:text processed-text))))
([inclusions text]
(let [ks (keys inclusions)]
(if (empty? (keys inclusions))
;; TODO: this is one opportunity to add scripts at the end of the
;; constructed text. I've a feeling that that would be a mistake and
;; that instead we should hand back a map comprising the text and the
;; keys of the extensions
text
(let [kw (first ks)]
(reintegrate-inclusions
@ -187,9 +179,89 @@
(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
"Take this markdown source, and return HTML."
[md-src]
(reintegrate-inclusions (process-text md-src)))
"Process the source text (assumed to be the value of the key `:source` in this
`context`, expected to be a full HTTP request map, so that extensions may in
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

@ -16,7 +16,7 @@
[smeagol.routes.wiki :refer [wiki-routes]]
[smeagol.middleware :refer [load-middleware]]
[smeagol.session-manager :as session-manager]
[taoensso.timbre :as timbre]
[taoensso.timbre :as log]
[taoensso.timbre.appenders.3rd-party.rotor :as rotor]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -55,9 +55,9 @@
"destroy will be called when your application
shuts down, put any clean up code here"
[]
(timbre/info "smeagol is shutting down...")
(log/info "smeagol is shutting down...")
(cronj/shutdown! session-manager/cleanup-job)
(timbre/info "shutdown complete!"))
(log/info "shutdown complete!"))
(defn init
@ -67,7 +67,7 @@
put any initialization code here"
[]
(try
(timbre/merge-config!
(log/merge-config!
{:appenders
{:rotor (rotor/rotor-appender
{:path "smeagol.log"
@ -80,10 +80,10 @@
(cronj/start! session-manager/cleanup-job)
(if (env :dev) (parser/cache-off!))
;;start the expired session cleanup job
(timbre/info "\n-=[ smeagol started successfully"
(log/info "\n-=[ smeagol started successfully"
(when (env :dev) "using the development profile") "]=-")
(catch Exception any
(timbre/error any "Failure during startup")
(log/error any "Failure during startup")
(destroy))))
;; timeout sessions after 30 minutes
@ -97,6 +97,7 @@
[xss-protection?]
(-> site-defaults
(update-in [:session] merge session-defaults)
(dissoc :static)
(assoc-in [:security :anti-forgery] xss-protection?)))

View file

@ -1,10 +1,10 @@
(ns ^{:doc "Explore the history of a page."
:author "Simon Brooke"}
smeagol.history
(:require [taoensso.timbre :as timbre]
[clj-jgit.porcelain :as git]
(:require [clj-jgit.porcelain :as git]
[clj-jgit.internal :as i]
[clj-jgit.querying :as q])
[clj-jgit.querying :as q]
[taoensso.timbre :as log])
(:import [org.eclipse.jgit.api Git]
[org.eclipse.jgit.lib Repository ObjectId]
[org.eclipse.jgit.revwalk RevCommit RevTree RevWalk]
@ -39,7 +39,7 @@
"If this `log-entry` contains a reference to this `file-path`, return the entry;
else nil."
[^String log-entry ^String file-path]
(timbre/info (format "searching '%s' for '%s'" log-entry file-path))
(log/info (format "searching '%s' for '%s'" log-entry file-path))
(cond
(seq (filter (fn* [p1__341301#] (= (first p1__341301#) file-path)) (:changed_files log-entry)))
log-entry))
@ -54,6 +54,7 @@
(try
(git/load-repo git-directory-path)
(catch java.io.FileNotFoundException fnf
(log/info "Initialising Git repository at" git-directory-path)
(git/git-init git-directory-path)
(let [repo (git/load-repo git-directory-path)]
(git/git-add-and-commit repo "Initial commit")

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]
[smeagol.configuration :refer [config]]
[smeagol.sanity :refer :all]
[smeagol.util :as util]
[taoensso.timbre :as timbre]))
[smeagol.util :as util]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -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)))
@ -49,10 +52,14 @@
(fn [args context-map]
(let [messages (:i18n context-map)
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
(render [this request]
(try
@ -76,6 +83,8 @@
(defn render
"Boilerplate from Luminus. Render an HTML page based on this `template` and
these `params`. Returns HTML source as a string."
[template & [params]]
(try
(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."
:author "Simon Brooke"}
smeagol.middleware
(:require [taoensso.timbre :as timbre]
[environ.core :refer [env]]
[selmer.middleware :refer [wrap-error-page]]
(:require [environ.core :refer [env]]
[noir-exception.core :refer [wrap-internal-error]]
[prone.middleware :refer [wrap-exceptions]]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[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]
(fn [req]
(timbre/debug req)
(log/debug req)
(handler req)))
@ -44,7 +49,12 @@
(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 []

View file

@ -33,7 +33,7 @@
(defn edit-users
"Put a list of users on-screen for editing."
"Render a page showing a list of users for editing."
[request]
(let [params (keywordize-keys (:params request))
user (session/get :user)]
@ -43,7 +43,8 @@
:users (auth/list-users)}))))
(defn delete-user
"Delete a user."
"Render a form allowing a user to be deleted; and
process that form.."
[request]
(let [params (keywordize-keys (:params request))
target (:target params)
@ -59,7 +60,8 @@
(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]
(let [params (keywordize-keys (:params request))]
(try

View file

@ -4,23 +4,34 @@
(:require [cemerick.url :refer (url url-encode url-decode)]
[clj-jgit.porcelain :as git]
[clojure.java.io :as cjio]
[clojure.pprint :refer [pprint]]
[clojure.string :as cs]
[clojure.walk :refer :all]
[compojure.core :refer :all]
[java-time :as jt]
[markdown.core :as md]
[me.raynes.fs :as fs]
[noir.io :as io]
[noir.response :as response]
[noir.util.route :as route]
[noir.session :as session]
[smeagol.authenticate :as auth]
[smeagol.configuration :refer [config]]
[smeagol.diff2html :as d2h]
[smeagol.formatting :refer [md->html]]
[smeagol.history :as hist]
[smeagol.layout :as layout]
[smeagol.local-links :refer :all]
[smeagol.routes.admin :as admin]
[smeagol.sanity :refer [show-sanity-check-error]]
[smeagol.util :as util]
[smeagol.uploads :as ul]
[taoensso.timbre :as timbre]))
[taoensso.timbre :as log]
[com.stuartsierra.component :as component]
[smeagol.configuration :refer [config]]
[smeagol.include.resolve-local-file :as resolve]
[smeagol.include :as include]
[smeagol.util :refer [content-dir local-url]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -49,6 +60,7 @@
"Process `source-text` and save it to the specified `file-path`, committing it
to Git and finally redirecting to wiki-page."
[params suffix request]
(log/trace (format "process-source: '%s'" request))
(let [source-text (:src params)
page (:page params)
file-name (str page suffix)
@ -58,7 +70,7 @@
user (session/get :user)
email (auth/get-email user)
summary (format "%s: %s" user (or (:summary params) "no summary"))]
(timbre/info (format "Saving %s's changes ('%s') to %s in file '%s'" user summary page file-path))
(log/info (format "Saving %s's changes ('%s') to %s in file '%s'" user summary page file-path))
(spit file-path source-text)
(git/git-add git-repo file-name)
(git/git-commit git-repo summary {:name user :email email})
@ -88,16 +100,19 @@
user (session/get :user)]
(if-not
exists?
(timbre/info
(log/info
(format "File '%s' not found; creating a new file" file-path))
(timbre/info (format "Opening '%s' for editing" file-path)))
(log/info (format "Opening '%s' for editing" file-path)))
(cond src-text (process-source params suffix request)
true
(layout/render template
(merge (util/standard-params request)
{:title (str (util/get-message :edit-title-prefix request) " " page)
:page page
:side-bar (md->html (slurp (cjio/file util/content-dir side-bar)))
:side-bar (md/md-to-html-string
(local-links
(slurp (cjio/file content-dir side-bar)))
:heading-anchors true)
:content (if exists? (slurp file-path) "")
:exists exists?})))))))
@ -108,26 +123,106 @@
(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
"Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page"
[request]
(log/trace (format "wiki-page: '%s'" request))
(or
(show-sanity-check-error)
(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-path (cjio/file util/content-dir file-name)
exists? (.exists (clojure.java.io/as-file file-path))]
(cond exists?
(do
(timbre/info (format "Showing page '%s' from file '%s'" page file-path))
(layout/render "wiki.html"
(merge (util/standard-params request)
{:title page
:page page
:content (md->html (slurp file-path))
:editable true})))
true (response/redirect (str "/edit?page=" page))))))
(if exists?
(do
(log/info (format "Showing page '%s' from file '%s'" page file-path))
(let [processed-text (md->html
(assoc request :source
(include/expand-include-md
(:includer md-include-system)
(slurp file-path))))]
(layout/render "wiki.html"
(merge (util/standard-params request)
processed-text
{: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
@ -138,42 +233,105 @@
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
file-name (str page ".md")
repo-path util/content-dir]
(timbre/info (format "Showing history of page '%s'" page))
(log/info (format "Showing history of page '%s'" page))
(layout/render "history.html"
(merge (util/standard-params request)
{:title (str "History of " page)
{:title (str (util/get-message :history-title-prefix request)
" " page)
:page page
:history (hist/find-history repo-path file-name)}))))
;;;; this next section is all stuff supporting the list-uploads page, and maybe
;;;; should be moved to its own file.
(def image-extns
"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
"Render a form to allow the upload of a file."
[request]
(let [params (keywordize-keys (:params request))
data-path (str (io/resource-path) "/content/uploads/")
data-path (str util/content-dir "/uploads/")
git-repo (hist/load-or-init-repo util/content-dir)
upload (:upload params)
uploaded (if upload (ul/store-upload params data-path))
user (session/get :user)
summary (format "%s: %s" user (or (:summary params) "no summary"))]
(if
uploaded
(do
(git/git-add git-repo uploaded)
(git/git-commit git-repo summary {:name user :email (auth/get-email user)})))
;; TODO: Get this working! it MUST work!
;; (if-not
;; (empty? uploaded)
;; (do
;; (map
;; #(git/git-add git-repo (str :resource %))
;; (remove nil? uploaded))
;; (git/git-commit git-repo summary {:name user :email (auth/get-email user)})))
(layout/render "upload.html"
(merge (util/standard-params request)
{:title (util/get-message :file-upload-title request)
:uploaded 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")))}))))
:uploaded uploaded}))))
(defn version-page
@ -184,7 +342,7 @@
version (:version params)
file-name (str page ".md")
content (hist/fetch-version util/content-dir file-name version)]
(timbre/info (format "Showing version '%s' of page '%s'" version page))
(log/info (format "Showing version '%s' of page '%s'" version page))
(layout/render "wiki.html"
(merge (util/standard-params request)
{:title (str (util/get-message :vers-col-hdr request) " " version " " (util/get-message :of request) " " page)
@ -199,7 +357,7 @@
page (url-decode (or (:page params) (util/get-message :default-page-title request)))
version (:version params)
file-name (str page ".md")]
(timbre/info (format "Showing diff between version '%s' of page '%s' and current" version page))
(log/info (format "Showing diff between version '%s' of page '%s' and current" version page))
(layout/render "wiki.html"
(merge (util/standard-params request)
{:title
@ -217,20 +375,22 @@
(defn auth-page
"Render the auth page"
"Render the authentication (login) page"
[request]
(or
(show-sanity-check-error)
(let [params (keywordize-keys (:form-params request))
username (:username params)
password (:password params)
action (:action params)
(let [params (keywordize-keys (:params request))
form-params (keywordize-keys (:form-params request))
username (:username form-params)
password (:password form-params)
action (:action form-params)
user (session/get :user)
redirect-to (or (:redirect-to params) "/wiki")]
redirect-to (:redirect-to params)]
(if redirect-to (log/info (str "After auth, redirect to: " redirect-to)))
(cond
(= action (util/get-message :logout-label request))
(do
(timbre/info (str "User " user " logging out"))
(log/info (str "User " user " logging out"))
(session/remove! :user)
(response/redirect redirect-to))
(and username password (auth/authenticate username password))
@ -243,8 +403,23 @@
{:title (if user
(str (util/get-message :logout-link request) " " user)
(util/get-message :login-link request))
:redirect-to ((:headers request) "referer")}))))))
:redirect-to redirect-to}))))))
(defn wrap-restricted-redirect
;; TODO: this is not idiomatic, and it's too late to write something idiomatic just now
;; 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
"Render a page to change the user password"
@ -270,8 +445,10 @@
(defroutes wiki-routes
(GET "/wiki" request (wiki-page request))
(GET "/" request (wiki-page request))
(GET "/auth" request (auth-page request))
(POST "/auth" request (auth-page request))
(GET "/changes" request (diff-page request))
(GET "/delete-user" request (route/restricted (admin/delete-user request)))
(GET "/edit" request (route/restricted (edit-page request)))
(POST "/edit" request (route/restricted (edit-page request)))
@ -281,11 +458,12 @@
(GET "/edit-user" request (route/restricted (admin/edit-user request)))
(POST "/edit-user" request (route/restricted (admin/edit-user request)))
(GET "/history" request (history-page request))
(GET "/list-uploads" request (route/restricted (list-uploads-page request)))
(POST "/list-uploads" request (route/restricted (list-uploads-page request)))
(GET "/version" request (version-page request))
(GET "/changes" request (diff-page request))
(GET "/auth" request (auth-page request))
(POST "/auth" request (auth-page request))
(GET "/passwd" request (passwd-page request))
(POST "/passwd" request (passwd-page request))
(GET "/upload" request (route/restricted (upload-page request)))
(POST "/upload" request (route/restricted (upload-page request))))
(POST "/upload" request (route/restricted (upload-page request)))
(GET "/wiki" request (wiki-page request))
)

View file

@ -1,5 +1,7 @@
(ns ^{:doc "Functions related to sanity checks and error reporting in conditions
where the environment may not be sane."
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"}
smeagol.sanity
(:import (java.util Locale))

View file

@ -1,10 +1,20 @@
(ns ^{:doc "Handle file uploads."
:author "Simon Brooke"}
smeagol.uploads
(:import [java.io File])
(:require [clojure.string :as cs]
[noir.io :as io]
[taoensso.timbre :as timbre]))
[clojure.java.io :as io]
[image-resizer.core :refer [resize]]
[image-resizer.util :refer :all]
[me.raynes.fs :as fs]
[noir.io :as nio]
[smeagol.configuration :refer [config]]
[smeagol.util :as util]
[taoensso.timbre :as log])
(:import [java.io File]
[java.awt Image]
[java.awt.image RenderedImage BufferedImageOp]
[javax.imageio ImageIO ImageWriter ImageWriteParam IIOImage]
[javax.imageio.stream FileImageOutputStream]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -29,36 +39,97 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; No longer used as uploaded files now go into Git.
;; (defn avoid-name-collisions
;; "Find a filename within this `path`, based on this `file-name`, that does not
;; reference an existing file. It is assumed that `path` ends with a path separator.
;; Returns a filename hwich does not currently reference a file within the path."
;; [path file-name]
;; (if (.exists (File. (str path file-name)))
;; (let [parts (cs/split file-name #"\.")
;; prefix (cs/join "." (butlast parts))
;; suffix (last parts)]
;; (first
;; (filter #(not (.exists (File. (str path %))))
;; (map #(str prefix "." % "." suffix) (range)))))
;; file-name))
(def image-file-extns
"Extensions of file types we will attempt to thumbnail. GIF is excluded
because by default the javax.imageio package can read GIF, PNG, and JPEG
images but can only write PNG and JPEG images."
#{".jpg" ".jpeg" ".png"})
(defn read-image
"Reads a BufferedImage from source, something that can be turned into
a file with clojure.java.io/file"
[source]
(ImageIO/read (io/file source)))
(defn write-image
"Writes img, a RenderedImage, to dest, something that can be turned into
a file with clojure.java.io/file.
Takes the following keys as options:
:format - :gif, :jpg, :png or anything supported by ImageIO
:quality - for JPEG images, a number between 0 and 100"
[^RenderedImage img dest & {:keys [format quality] :or {format :jpg}}]
(log/info "Writing to " dest)
(let [fmt (subs (fs/extension (cs/lower-case dest)) 1)
iw (doto ^ImageWriter (first
(iterator-seq
(ImageIO/getImageWritersByFormatName
fmt)))
(.setOutput (FileImageOutputStream. (io/file dest))))
iw-param (doto ^ImageWriteParam (.getDefaultWriteParam iw)
(.setCompressionMode ImageWriteParam/MODE_EXPLICIT)
(.setCompressionQuality (float (/ (or quality 75) 100))))
iio-img (IIOImage. img nil nil)]
(.write iw nil iio-img iw-param)))
(def image?
"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
"Store an upload both to the file system and to the database.
The issue with storing an upload is moving it into place.
If `params` are passed as a map, it is expected that this is a map from
an HTTP POST operation of a form with type `multipart/form-data`."
an HTTP POST operation of a form with type `multipart/form-data`.
On success, returns the file object uploaded."
[params path]
(let [upload (:upload params)
tmp-file (:tempfile upload)
filename (:filename upload)]
(timbre/info
(log/info
(str "Storing upload file: " upload))
(if tmp-file
(do
(.renameTo tmp-file
(File. (str path filename)))
filename)
(throw (Exception. "No file found?")))))
(log/debug
(str "store-upload mv file: " tmp-file " to: " path filename))
(if tmp-file
(try
(let [p (io/file path filename)]
(.renameTo tmp-file p)
(map
#(assoc % :resource (subs (:location %) (inc (count util/content-dir))))
(remove
nil?
(cons
{:size :original
:filename filename
:location (str p)
:is-image (and (image? filename) true)}
(remove nil? (or (auto-thumbnail path filename) '()))))))
(catch Exception x
(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,14 +2,17 @@
:author "Simon Brooke"}
smeagol.util
(:require [clojure.java.io :as cjio]
[clojure.string :as cs]
[environ.core :refer [env]]
[markdown.core :as md]
[me.raynes.fs :as fs]
[noir.io :as io]
[noir.session :as session]
[scot.weft.i18n.core :as i18n]
[smeagol.authenticate :as auth]
[smeagol.configuration :refer [config]]
[smeagol.formatting :refer [md->html]]
[taoensso.timbre :as timbre]))
[smeagol.local-links :refer :all]
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -35,11 +38,99 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def content-dir
(or
(:content-dir config)
(cjio/file (io/resource-path) "content")))
(def start-page
"The page to load on startup, taken from configuration."
(:start-page config))
(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
"Return a map of standard parameters to pass to the template renderer."
@ -47,37 +138,43 @@
(let [user (session/get :user)]
{:user user
:admin (auth/get-admin user)
:side-bar (md->html (slurp (cjio/file content-dir "_side-bar.md")))
:header (md->html (slurp (cjio/file content-dir "_header.md")))
:js-from (:js-from config)
: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")}))
(defn- raw-get-messages
(def get-messages
"Return the most acceptable messages collection we have given the
`Accept-Language` header in this `request`."
[request]
(let [specifier ((:headers request) "accept-language")
messages (try
(i18n/get-messages specifier "i18n" "en-GB")
(catch Exception any
(timbre/error
any
(str
"Failed to parse accept-language header "
specifier))
{}))]
(memoize
(fn [request]
(let [specifier ((:headers request) "accept-language")
messages (try
(i18n/get-messages specifier "i18n" "en-GB")
(catch Exception any
(log/error
any
(str
"Failed to parse accept-language header '"
specifier
"'"))
{}))]
(merge
messages
config)))
(def get-messages (memoize raw-get-messages))
config)))))
(defn get-message
"Return the message with this `message-key` from this `request`.
if not found, return this `default`, if provided; else return the
`message-key`."
if not found, return this `default`, if provided; else return the
`message-key`."
([message-key request]
(get-message message-key message-key request))
([message-key default request]

View file

@ -1,12 +1,43 @@
(ns smeagol.test.formatting
(: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
(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"))))
(deftest test-apply-formatter
(testing "apply-formatter"
(let [actual (-> (apply-formatter
3
{:inclusions {}}
'()
'()
"test
![Frost on a gate, Laurieston](content/uploads/g1.jpg)
![Feathered crystals on snow surface, Taliesin](content/uploads/g2.jpg)
![Feathered snow on log, Taliesin](content/uploads/g3.jpg)
![Crystaline growth on seed head, Taliesin](content/uploads/g4.jpg)"
"test"
smeagol.extensions.test/process-test)
:inclusions
:inclusion-3)
expected "<!-- 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)))))