commit
1c587f8616
38 changed files with 555 additions and 291 deletions
44
NEWS.md
44
NEWS.md
|
@ -1,8 +1,45 @@
|
||||||
|
Legend:
|
||||||
|
* Site-Breaking Change:
|
||||||
|
A change that will break most config files or coleslaw installations.
|
||||||
|
It is expected to effect all users but should require only minor
|
||||||
|
user effort to resolve.
|
||||||
|
* Incompatible Change:
|
||||||
|
A change to Coleslaw's exported interface. Plugins or Themes that have
|
||||||
|
not been upstreamed are effected and may require minor effort to fix.
|
||||||
|
|
||||||
|
## Changes for 0.9.6 (2014-09-27):
|
||||||
|
|
||||||
|
* **SITE-BREAKING CHANGE**: Coleslaw now defaults to a "basic" deploy
|
||||||
|
instead of the previous symlinked, timestamped deploy strategy.
|
||||||
|
To retain the previous behavior, add `(versioned)` to your config's
|
||||||
|
`:plugins` list.
|
||||||
|
* **Incompatible Change**: Custom themes will be broken by a change
|
||||||
|
to URL handling. Previously, we were hand-constructing URLs in the
|
||||||
|
templates. All site objects now store their URL in an instance slot.
|
||||||
|
In general, hrefs should be of the form `<a href="{$config.domain}/{$obj.url}"> ...</a>`.
|
||||||
|
* **Incompatible Change**: The interface of the `add-injection` function
|
||||||
|
has changed. If you have written a plugin which uses `add-injection`
|
||||||
|
you should update it to conform to the [new interface][plg-api].
|
||||||
|
* **New Plugin**: Support for [twitter summary cards][ts-cards] on blog
|
||||||
|
posts has been added thanks to @PuercoPop.
|
||||||
|
* **Docs**: Improved README and Theming docs. New Config File docs.
|
||||||
|
* Changes to `:routing` would previously break links in the templates
|
||||||
|
but now work seamlessly due to the updated URL handling.
|
||||||
|
* Loading content is more robust when empty lines or metadata are passed.
|
||||||
|
Thanks to @PuercoPop for the bug report and preliminary fix.
|
||||||
|
* The config `:repo` option is now deprecated as its value has become
|
||||||
|
a required argument to `coleslaw:main`. The value passed to `main`
|
||||||
|
will override the config value going forward.
|
||||||
|
* Improved handling of directories and error-reporting when they
|
||||||
|
don't exist is available thanks to @PuercoPop.
|
||||||
|
* The templates are now HTML5 valid thanks to @Ferada.
|
||||||
|
* Fixed a bug where RSS/Atom tag feeds were being published multiple times.
|
||||||
|
|
||||||
## Changes for 0.9.5 (2014-06-13):
|
## Changes for 0.9.5 (2014-06-13):
|
||||||
|
|
||||||
* A plugin for Incremental builds, cutting runtime for generating
|
* **New Plugin**: Incremental builds, cutting runtime for generating
|
||||||
medium to large sites roughly in half!
|
medium to large sites roughly in half!
|
||||||
* A Twitter plugin to tweet about your new posts. Thanks to @PuercoPop!
|
* **New Plugin**: A Twitter plugin to tweet about your new posts. Thanks to @PuercoPop!
|
||||||
* Config options for the HTML lang and charset attributes. Thanks to @ryumei!
|
* Config options for the HTML lang and charset attributes. Thanks to @ryumei!
|
||||||
* Coleslaw now exports a `get-updated-files` function which can be
|
* Coleslaw now exports a `get-updated-files` function which can be
|
||||||
used to get a list of file-status/file-name pairs that were changed
|
used to get a list of file-status/file-name pairs that were changed
|
||||||
|
@ -104,3 +141,6 @@
|
||||||
[hacking_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
|
[hacking_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
|
||||||
[theming_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/themes.md
|
[theming_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/themes.md
|
||||||
[example.rc]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
|
[example.rc]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
|
||||||
|
[plg-use]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-use.md
|
||||||
|
[plg-api]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md#extension-points
|
||||||
|
[ts-cards]: https://dev.twitter.com/cards/types/summary
|
||||||
|
|
88
README.md
88
README.md
|
@ -10,6 +10,7 @@
|
||||||
Coleslaw is Flexible Lisp Blogware similar to [Frog](https://github.com/greghendershott/frog), [Jekyll](http://jekyllrb.com/), or [Hakyll](http://jaspervdj.be/hakyll/).
|
Coleslaw is Flexible Lisp Blogware similar to [Frog](https://github.com/greghendershott/frog), [Jekyll](http://jekyllrb.com/), or [Hakyll](http://jaspervdj.be/hakyll/).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Git for storage
|
* Git for storage
|
||||||
* RSS and Atom feeds
|
* RSS and Atom feeds
|
||||||
* Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize)
|
* Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize)
|
||||||
|
@ -21,41 +22,73 @@ Coleslaw is Flexible Lisp Blogware similar to [Frog](https://github.com/greghend
|
||||||
* Incremental builds
|
* Incremental builds
|
||||||
* Analytics via Google
|
* Analytics via Google
|
||||||
* Comments via [Disqus](http://disqus.com/)
|
* Comments via [Disqus](http://disqus.com/)
|
||||||
* Hosting via [Github Pages](https://pages.github.com/), [Heroku](http://heroku.com/), or [Amazon S3](http://aws.amazon.com/s3/)
|
* Hosting via [Github Pages](https://pages.github.com/) or [Amazon S3](http://aws.amazon.com/s3/)
|
||||||
* [Tweeting](http://twitter.com/) about new posts
|
* [Tweeting](http://twitter.com/) about new posts
|
||||||
* Using LaTeX via [Mathjax](http://mathjax.org/)
|
* Using LaTeX via [Mathjax](http://mathjax.org/)
|
||||||
* Writing posts in ReStructured Text
|
* Writing posts in ReStructured Text
|
||||||
* Importing posts from [Wordpress](http://wordpress.org/)
|
* Importing posts from [Wordpress](http://wordpress.org/)
|
||||||
|
|
||||||
* There is also a [Heroku buildpack](https://github.com/jsmpereira/coleslaw-heroku) maintained by Jose Pereira.
|
|
||||||
|
|
||||||
## Example Sites
|
## Example Sites
|
||||||
* [redlinernotes](http://redlinernotes.com/blog/)
|
|
||||||
* [chip the glasses](http://chiptheglasses.com)
|
See the [wiki](https://github.com/redline6561/coleslaw/wiki/Blogroll) for a list of coleslaw-powered blogs.
|
||||||
* [kenan-bolukbasi.log](http://kenanb.com/)
|
|
||||||
* [Nothing Really Matters](http://ironhead.xs4all.nl/)
|
|
||||||
* [A year and a smile](http://blog.sjas.de)
|
|
||||||
|
|
||||||
## Hacking
|
## Hacking
|
||||||
|
|
||||||
A core goal of *coleslaw* is to be both pleasant to read and easy to hack on and extend. If you want to understand the internals and bend *coleslaw* to do new and interesting things, I strongly encourage you to read the [Hacker's Guide to Coleslaw](https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md).
|
A core goal of *coleslaw* is to be both pleasant to read and easy to
|
||||||
|
hack on and extend. If you want to understand the internals and bend
|
||||||
|
*coleslaw* to do new and interesting things, I strongly encourage you
|
||||||
|
to read the [Hacker's Guide to Coleslaw][hackers]. You'll find some
|
||||||
|
current **TODO** items towards the bottom.
|
||||||
|
|
||||||
|
[hackers]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
This software should be portable to any conforming Common Lisp implementation but testing is primarily done on [SBCL](http://www.sbcl.org/) and [CCL](http://ccl.clozure.com/).
|
|
||||||
Server side setup:
|
|
||||||
|
|
||||||
1. Setup git and create a bare repo as shown [here](http://git-scm.com/book/en/Git-on-the-Server-Setting-Up-the-Server).
|
Coleslaw should run on any conforming Common Lisp implementation but
|
||||||
2. Install Lisp (we recommend SBCL) and [Quicklisp](http://quicklisp.org/).
|
testing is primarily done on [SBCL](http://www.sbcl.org/) and
|
||||||
3. ```wget -c https://raw.github.com/redline6561/coleslaw/master/examples/example.coleslawrc -O ~/.coleslawrc``` # and edit as necessary
|
[CCL](http://ccl.clozure.com/).
|
||||||
4. ```wget -c https://raw.github.com/redline6561/coleslaw/master/examples/example.post-receive -O your-blog.git/hooks/post-receive``` # and edit as necessary
|
|
||||||
5. ```chmod +x your-blog/.git/hooks/post-receive```
|
|
||||||
6. Create or clone your blog repo locally. Add your server as a remote with ```git remote add prod git@my-host.com:path/to/repo.git```
|
|
||||||
7. Point the web server of your choice at the symlink /path/to/deploy-dir/.curr/
|
|
||||||
|
|
||||||
Now whenever you push a new commit to the server, coleslaw will update your blog automatically! You may need to `git push -u prod master` the first time.
|
Coleslaw can either be run **manually** on a local machine or
|
||||||
|
triggered **automatically** on git push to a server. If you want a
|
||||||
|
server install, run these commands on your server _after_ setting up a
|
||||||
|
[git bare repo](http://git-scm.com/book/en/Git-on-the-Server-Setting-Up-the-Server).
|
||||||
|
Otherwise, run the commands on your local machine.
|
||||||
|
|
||||||
|
1. Install a Common Lisp implementation (we recommend SBCL) and
|
||||||
|
[Quicklisp](http://quicklisp.org/).
|
||||||
|
2. Place a config file for coleslaw in your `$HOME` directory. If you
|
||||||
|
want to run multiple blogs with coleslaw, you can keep each blog's
|
||||||
|
config file in that blog's repo. Feel free to copy and edit the
|
||||||
|
[example config][ex_config] or consult the [config docs][conf_docs]
|
||||||
|
to create one from scratch.
|
||||||
|
3. This step depends on whether you're setting up a local or server install.
|
||||||
|
* Server Install: Copy and `chmod +x` the
|
||||||
|
[example post-receive hook][post_hook] to your blog's bare repo.
|
||||||
|
* Local Install: Just run the following commands in the
|
||||||
|
REPL whenever you're ready to regenerate your blog:
|
||||||
|
```
|
||||||
|
(ql:quickload :coleslaw)
|
||||||
|
(coleslaw:main "/path/to/my/blog/")
|
||||||
|
```
|
||||||
|
4. Optionally, point the web server of your liking at your config-specified
|
||||||
|
`:deploy-dir`. Or "deploy-dir/.curr" if the `versioned` plugin is enabled.
|
||||||
|
|
||||||
|
Now just write posts, git commit and build by hand or by push.
|
||||||
|
|
||||||
|
[ex_config]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
|
||||||
|
[conf_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/config.md
|
||||||
|
[post_hook]: https://github.com/redline6561/coleslaw/blob/master/examples/example.post-receive
|
||||||
|
|
||||||
|
## The Content Format
|
||||||
|
|
||||||
|
Coleslaw expects content to have a file extension matching the class
|
||||||
|
of the content. (I.e. `.post` for blog posts, `.page` for static
|
||||||
|
pages, etc.)
|
||||||
|
|
||||||
|
There should also be a metadata header on all files
|
||||||
|
starting and ending with the config-specified `:separator`, ";;;;;" by
|
||||||
|
default. Example:
|
||||||
|
|
||||||
## The Post Format
|
|
||||||
Coleslaw expects post files to be formatted as follows:
|
|
||||||
```
|
```
|
||||||
;;;;;
|
;;;;;
|
||||||
title: foo
|
title: foo
|
||||||
|
@ -66,5 +99,16 @@ format: html (for raw html) or md (for markdown)
|
||||||
your post
|
your post
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Posts require the `title:` and `format:` fields.
|
||||||
|
Pages require the `title:` and `url:` fields.
|
||||||
|
|
||||||
|
To omit a field, simply do not have the line present, empty lines and
|
||||||
|
fields (e.g. "tags:" followed by whitespace) will be ignored.
|
||||||
|
|
||||||
## Theming
|
## Theming
|
||||||
Two themes are provided: hyde and readable (based on [bootswatch readable](http://bootswatch.com/readable/)). Hyde is the default. A guide to creating themes for coleslaw lives [here](https://github.com/redline6561/coleslaw/blob/master/docs/themes.md).
|
|
||||||
|
Two themes are provided: hyde, the default, and readable (based on
|
||||||
|
[bootswatch readable](http://bootswatch.com/readable/)).
|
||||||
|
|
||||||
|
A guide to creating themes for coleslaw lives
|
||||||
|
[here](https://github.com/redline6561/coleslaw/blob/master/docs/themes.md).
|
||||||
|
|
5
TODO
5
TODO
|
@ -1,5 +0,0 @@
|
||||||
TODO:
|
|
||||||
Coleslaw.next
|
|
||||||
; See if there are any good ideas we can steal from [Frog](https://github.com/greghendershott/frog)
|
|
||||||
;; needs: shout template/render function. Twitter\Disqus integration with shouts?
|
|
||||||
; Incremental compilation: only "touched" posts+tags+months and by-n. -> 1.0
|
|
|
@ -1,7 +1,7 @@
|
||||||
(defsystem #:coleslaw
|
(defsystem #:coleslaw
|
||||||
:name "coleslaw"
|
:name "coleslaw"
|
||||||
:description "Flexible Lisp Blogware"
|
:description "Flexible Lisp Blogware"
|
||||||
:version "0.9.5"
|
:version "0.9.6"
|
||||||
:license "BSD"
|
:license "BSD"
|
||||||
:author "Brit Butler <redline6561@gmail.com>"
|
:author "Brit Butler <redline6561@gmail.com>"
|
||||||
:pathname "src/"
|
:pathname "src/"
|
||||||
|
@ -31,11 +31,10 @@
|
||||||
(intern "COLESLAW-TESTS" :coleslaw-tests))))
|
(intern "COLESLAW-TESTS" :coleslaw-tests))))
|
||||||
|
|
||||||
(defsystem #:coleslaw-tests
|
(defsystem #:coleslaw-tests
|
||||||
:depends-on (coleslaw fiveam)
|
:depends-on (coleslaw stefil)
|
||||||
:pathname "tests/"
|
:pathname "tests/"
|
||||||
:serial t
|
:serial t
|
||||||
:components ((:file "packages")
|
:components ())
|
||||||
(:file "tests")))
|
|
||||||
|
|
||||||
(defmethod operation-done-p ((op test-op)
|
(defmethod operation-done-p ((op test-op)
|
||||||
(c (eql (find-system :coleslaw))))
|
(c (eql (find-system :coleslaw))))
|
||||||
|
|
35
docs/config.md
Normal file
35
docs/config.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
## Where
|
||||||
|
|
||||||
|
Coleslaw needs a `.coleslawrc` file to operate properly. That file is usually located at
|
||||||
|
$HOME/.coleslawrc but may also be placed in the blog repo itself.
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
The only *required* information in the config is:
|
||||||
|
* `:author` => to be placed on post pages and in the copyright/CC-BY-SA notice
|
||||||
|
* `:deploy-dir` => for Coleslaw's generated HTML to go in
|
||||||
|
* `:domain` => to generate absolute links to the site content
|
||||||
|
* `:routing` => to determine the URL scheme of content on the site
|
||||||
|
* `:title` => to provide a site title
|
||||||
|
* `:theme` => to select one of the themes in "coleslaw/themes/"
|
||||||
|
|
||||||
|
It is usually recommend to start from the [example config][ex_config] and pare down from there.
|
||||||
|
|
||||||
|
[ex_config]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
|
||||||
|
|
||||||
|
## Extras
|
||||||
|
|
||||||
|
There are also many *optional* config parameters such as:
|
||||||
|
* `:charset` => to set HTML attributes for international characters, default: "UTF-8"
|
||||||
|
* `:feeds` => to generate RSS and Atom feeds for certain tagged content
|
||||||
|
* `:lang` => to set HTML attributes indicating the site language, default: "en"
|
||||||
|
* `:license` => to override the displayed content license, the default is CC-BY-SA
|
||||||
|
* `:page-ext` => to set the suffix of generated files, default: "html"
|
||||||
|
* `:plugins` => to configure and enable coleslaw's [various plugins][plugin-use]
|
||||||
|
* `:separator` => to set the separator for content metadata, default: ";;;;;"
|
||||||
|
* `:sitenav` => to provide relevant links and ease navigation
|
||||||
|
* `:staging-dir` => for Coleslaw to do intermediate work, default: "/tmp/coleslaw"
|
||||||
|
|
||||||
|
[plugin-use]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-use.md
|
155
docs/hacking.md
155
docs/hacking.md
|
@ -38,21 +38,6 @@ reduced runtime to 1.36 seconds, almost cutting it in half.
|
||||||
|
|
||||||
## Core Concepts
|
## Core Concepts
|
||||||
|
|
||||||
### Data and Deployment
|
|
||||||
|
|
||||||
**Coleslaw** is pretty fundamentally tied to the idea of git as both a
|
|
||||||
backing data store and a deployment method (via `git push`). The
|
|
||||||
consequence is that you need a bare repo somewhere with a post-recieve
|
|
||||||
hook. That post-recieve hook ([example][post_receive_hook])
|
|
||||||
will checkout the repo to a **$TMPDIR** and call `(coleslaw:main $TMPDIR)`.
|
|
||||||
|
|
||||||
It is then coleslaw's job to load all of your content, your config and
|
|
||||||
templates, and render the content to disk. Deployment is done by
|
|
||||||
moving the files to a location specified in the config and updating a
|
|
||||||
symlink. It is assumed a web server is set up to serve from that
|
|
||||||
symlink. However, there are plugins for deploying to Heroku, S3, and
|
|
||||||
Github Pages.
|
|
||||||
|
|
||||||
### Plugins
|
### Plugins
|
||||||
|
|
||||||
**Coleslaw** strongly encourages extending functionality via plugins.
|
**Coleslaw** strongly encourages extending functionality via plugins.
|
||||||
|
@ -79,8 +64,22 @@ and return rendered HTML. **Coleslaw** defines a helper called
|
||||||
there are RSS, ATOM, and sitemap templates *coleslaw* uses automatically.
|
there are RSS, ATOM, and sitemap templates *coleslaw* uses automatically.
|
||||||
No need for individual themes to reimplement a standard, after all!
|
No need for individual themes to reimplement a standard, after all!
|
||||||
|
|
||||||
|
Unfortunately, it is not very pleasant to debug broken templates.
|
||||||
|
Efforts to remedy this are being pursued for the next release.
|
||||||
|
Two particular issues to note are transposed Closure commands,
|
||||||
|
e.g. "${foo}" instead of "{$foo}", and trying to use nonexistent
|
||||||
|
keys or slots which fails silently instead of producing an error.
|
||||||
|
|
||||||
### The Lifecycle of a Page
|
### The Lifecycle of a Page
|
||||||
|
|
||||||
|
- `(progn
|
||||||
|
(load-config "/my/blog/repo/path")
|
||||||
|
(compile-theme (theme *config*)))`
|
||||||
|
|
||||||
|
Coleslaw first needs the config loaded and theme compiled,
|
||||||
|
as neither the blog location, the theme to use, and other
|
||||||
|
crucial information are not yet known.
|
||||||
|
|
||||||
- `(load-content)`
|
- `(load-content)`
|
||||||
|
|
||||||
A page starts, obviously, with a file. When *coleslaw* loads your
|
A page starts, obviously, with a file. When *coleslaw* loads your
|
||||||
|
@ -102,10 +101,10 @@ reverse-chronological index.
|
||||||
|
|
||||||
- `(deploy dir)`
|
- `(deploy dir)`
|
||||||
|
|
||||||
Finally, we move the staging directory to a timestamped path under the
|
Finally, we move the staging directory to a path under the config's
|
||||||
the config's `:deploy-dir`, delete the directory pointed to by the old
|
`:deploy-dir`. If the versioned plugin is enabled, it is a timestamped
|
||||||
'.prev' symlink, point '.curr' at '.prev', and point '.curr' at our
|
path and we delete the directory pointed to by the old '.prev' symlink,
|
||||||
freshly built site.
|
point '.curr' at '.prev', and point '.curr' at our freshly built site.
|
||||||
|
|
||||||
### Blogs vs Sites
|
### Blogs vs Sites
|
||||||
|
|
||||||
|
@ -116,13 +115,12 @@ INDEXes. Roughly speaking, a POST is a blog entry and an INDEX is a
|
||||||
collection of POSTs or other content. An INDEX really only serves to
|
collection of POSTs or other content. An INDEX really only serves to
|
||||||
group a set of content objects on a page, it isn't content itself.
|
group a set of content objects on a page, it isn't content itself.
|
||||||
|
|
||||||
This isn't ideal if you're looking for a full-on static site
|
Content Types were added in 0.8 as a step towards making *coleslaw*
|
||||||
generator. Thankfully, Content Types were added in 0.8 as a step
|
suitable for more use cases. Any subclass of CONTENT that implements
|
||||||
towards making *coleslaw* suitable for more use cases. Any subclass of
|
the *document protocol* counts as a content type. However, only POSTs
|
||||||
CONTENT that implements the *document protocol* counts as a content
|
are currently included in the bundled INDEXes since there isn't yet a
|
||||||
type. However, only POSTs are currently included in the basic INDEXes
|
formal relationship to determine which content types should be
|
||||||
since there isn't yet a formal relationship to determine which content
|
included on which indexes. It is straightforward for users to implement
|
||||||
types should be included on which indexes. Users may easily implement
|
|
||||||
their own dedicated INDEX for new Content Types.
|
their own dedicated INDEX for new Content Types.
|
||||||
|
|
||||||
### The Document Protocol
|
### The Document Protocol
|
||||||
|
@ -169,16 +167,14 @@ eql-specializing on the class, e.g.
|
||||||
|
|
||||||
**Instance Methods**:
|
**Instance Methods**:
|
||||||
|
|
||||||
- `page-url`: Generate a relative path for the object on the site,
|
- `page-url`: Retrieve the relative path for the object on the site.
|
||||||
usually sans file extension. If there is no extension, an :around
|
The implementation of `page-url` is not fully specified. For most
|
||||||
method adds "html" later. The `slug` slot on the instance is
|
content types, we compute and store the path on the instance at
|
||||||
conventionally used to hold a portion of the path that corresponds
|
initialization time making `page-url` just a reader method.
|
||||||
to a unique Primary Key or Object ID.
|
|
||||||
|
|
||||||
- `render`: A method that calls the appropriate template with `theme-fn`,
|
- `render`: A method that calls the appropriate template with `theme-fn`,
|
||||||
passing it any needed arguments and returning rendered HTML.
|
passing it any needed arguments and returning rendered HTML.
|
||||||
|
|
||||||
|
|
||||||
**Invariants**:
|
**Invariants**:
|
||||||
|
|
||||||
- Any Content Types (subclasses of CONTENT) are expected to be stored in
|
- Any Content Types (subclasses of CONTENT) are expected to be stored in
|
||||||
|
@ -217,7 +213,77 @@ order. Feeds exist to special case RSS and ATOM generation.
|
||||||
Currently, there is only 1 content type: POST, for blog entries.
|
Currently, there is only 1 content type: POST, for blog entries.
|
||||||
PAGE, a content type for static page support, is available as a plugin.
|
PAGE, a content type for static page support, is available as a plugin.
|
||||||
|
|
||||||
## Areas for Improvement
|
## Areas for Improvement (i.e. The Roadmap)
|
||||||
|
|
||||||
|
### TODO for 0.9.7
|
||||||
|
|
||||||
|
* Test suite improvements:
|
||||||
|
* `load-content`/`read-content`/parsing
|
||||||
|
* Content Discovery
|
||||||
|
* Theme Compilation
|
||||||
|
* Content Publishing
|
||||||
|
* Common Plugins including Injections
|
||||||
|
* Add proper errors to read-content/load-content? Not just ignoring bad data. Line info, etc.
|
||||||
|
* Improved template debugging? "${" instead of "{$", static checks for valid slots, etc.
|
||||||
|
At least a serious investigation into how such things might be provided.
|
||||||
|
* Some minor scripting conveniences with cl-launch? (Scaffold a post/page, Enable incremental, Build, etc).
|
||||||
|
|
||||||
|
### Assorted Cleanups
|
||||||
|
|
||||||
|
* Try to get tag-index urls out of the tags. Post templates use them.
|
||||||
|
* Profile/memoize find-all calls in **INDEX** `render` method.
|
||||||
|
|
||||||
|
### Real Error Handling
|
||||||
|
|
||||||
|
One reason Coleslaw's code base is so small is probably the
|
||||||
|
omission of any serious error handling. Trying to debug
|
||||||
|
coleslaw if there's a problem during a build is unpleasant
|
||||||
|
at best, especially for anyone not coming from the lisp world.
|
||||||
|
|
||||||
|
We need to start handling errors and reporting errors in ways
|
||||||
|
that are useful to the user. Example errors users have encountered:
|
||||||
|
|
||||||
|
1. Loading of Content. If `read-content` fails to parse a file, we
|
||||||
|
should tell the user what file failed and why. We also should
|
||||||
|
probably enforce more constraints about metadata. E.g. Empty
|
||||||
|
metadata is not allowed/meaningful. Trailing space after separator, etc.
|
||||||
|
2. Trying to load content from the bare repo instead of the clone.
|
||||||
|
i.e. Specifying the `:repo` in .coleslawrc as the bare repo.
|
||||||
|
The README should clarify this point and the need for posts to be
|
||||||
|
".post" files.
|
||||||
|
3. Custom themes that try to access non-existent properties of content
|
||||||
|
do not currently error. They just wind up returning whitespace.
|
||||||
|
When the theme compiles, we should alert the user to any obvious
|
||||||
|
issues with it.
|
||||||
|
4. Dear Lord it was miserable even debugging a transposed character error
|
||||||
|
in one of the templates. "${foo}" instead of "{$foo}". But fuck supporting
|
||||||
|
multiple templating backends I have enough problems. What can we do?
|
||||||
|
|
||||||
|
### Scripting Conveniences
|
||||||
|
|
||||||
|
It would be convenient to add command-line tools/scripts to run coleslaw,
|
||||||
|
set up the db for incremental builds, scaffold a new post, etc. for new users.
|
||||||
|
Xach's buildapp or Fare's cl-launch would be useful here. frog and hakyll are
|
||||||
|
reasonable points of inspiration for commands to offer.
|
||||||
|
|
||||||
|
### Plugin Constraints
|
||||||
|
|
||||||
|
There is no system for determining what plugins work together or
|
||||||
|
enforcing the requirements or constraints of any particular
|
||||||
|
plugin. That is to say, the plugins are not actually modular. They are
|
||||||
|
closer to controlled monkey-patching.
|
||||||
|
|
||||||
|
While adding a [real module system to common lisp][asdf3] is probably
|
||||||
|
out of scope, we might be able to add some kind of [contract library][qpq]
|
||||||
|
for implementing this functionality. At the very least, a way to check
|
||||||
|
some assertions and error out at plugin load time if they fail should be
|
||||||
|
doable. I might not be able to [make illegal states unrepresentable][misu],
|
||||||
|
but I can sure as hell make them harder to construct than they are now.
|
||||||
|
|
||||||
|
@PuercoPop has suggested looking into how [wookie does plugins][wookie].
|
||||||
|
It's much more heavyweight but might be worth looking into. If we go that
|
||||||
|
route, the plugin support code will be almost half the coleslaw core.
|
||||||
|
Weigh the tradeoffs carefully.
|
||||||
|
|
||||||
### New Content Type: Shouts!
|
### New Content Type: Shouts!
|
||||||
|
|
||||||
|
@ -249,28 +315,13 @@ Unfortunately, this does not solve:
|
||||||
Content Types it includes or the CONTENT which indexes it appears
|
Content Types it includes or the CONTENT which indexes it appears
|
||||||
on is not yet clear.
|
on is not yet clear.
|
||||||
|
|
||||||
### Incremental Compilation
|
|
||||||
|
|
||||||
Incremental compilation is doable, even straightforward if you ignore
|
|
||||||
indexes. It is also preferable to building the site in parallel as
|
|
||||||
avoiding work is better than using more workers. Moreover, being
|
|
||||||
able to determine (and expose) what files just changed enables new
|
|
||||||
functionality such as plugins that cross-post to tumblr.
|
|
||||||
|
|
||||||
This is a cool project and the effects are far reaching. Among other
|
|
||||||
things the existing deployment model would not work as it involves
|
|
||||||
rebuilding the entire site. In all likelihood we would want to update
|
|
||||||
the site 'in-place'. How to replace the compilation and deployment
|
|
||||||
model via a plugin has not yet been explored. Atomicity of filesystem
|
|
||||||
operations would be a reasonable concern. Also, every numbered INDEX
|
|
||||||
would have to be regenerated along with any tag or month indexes
|
|
||||||
matching the modified files. If incremental compilation is a goal,
|
|
||||||
simply disabling the indexes may be appropriate for certain users.
|
|
||||||
|
|
||||||
[post_receive_hook]: https://github.com/redline6561/coleslaw/blob/master/examples/example.post-receive
|
|
||||||
[closure_template]: https://github.com/archimag/cl-closure-template
|
[closure_template]: https://github.com/archimag/cl-closure-template
|
||||||
[api_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md
|
[api_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md
|
||||||
[clmd]: https://github.com/gwkkwg/cl-markdown
|
[clmd]: https://github.com/gwkkwg/cl-markdown
|
||||||
[clrz]: https://github.com/redline6561/colorize
|
[clrz]: https://github.com/redline6561/colorize
|
||||||
[pyg]: http://pygments.org/
|
[pyg]: http://pygments.org/
|
||||||
[incf]: https://github.com/redline6561/coleslaw/blob/master/plugins/incremental.lisp
|
[incf]: https://github.com/redline6561/coleslaw/blob/master/plugins/incremental.lisp
|
||||||
|
[asdf3]: https://github.com/fare/asdf3-2013
|
||||||
|
[qpq]: https://github.com/sellout/quid-pro-quo
|
||||||
|
[misu]: https://blogs.janestreet.com/effective-ml-revisited/
|
||||||
|
[wookie]: https://github.com/orthecreedence/wookie/blob/master/plugin.lisp#L181
|
||||||
|
|
|
@ -15,14 +15,12 @@
|
||||||
|
|
||||||
# Extension Points
|
# Extension Points
|
||||||
|
|
||||||
* **New functionality via JS**, for example the Disqus and Mathjax
|
* **New functionality via JS**, for example the Disqus and Mathjax plugins.
|
||||||
plugins. In this case, the plugin's `enable` function should call
|
In this case, the plugin's `enable` function should call
|
||||||
[`add-injection`](http://redlinernotes.com/docs/coleslaw.html#add-injection_func)
|
[`add-injection`](http://redlinernotes.com/docs/coleslaw.html#add-injection_func)
|
||||||
with an injection and a keyword. The injection itself is a list of
|
with an injection and a keyword. The injection is a function that takes a
|
||||||
the string to insert and a lambda or function that can be called on
|
*Document* and returns a string to insert in the page or nil.
|
||||||
a content instance to determine whether the injection should be
|
The keyword specifies whether the injected text goes in the HEAD or BODY element. The
|
||||||
included on the page. The keyword specifies whether the injected
|
|
||||||
text goes in the HEAD or BODY element. The
|
|
||||||
[Disqus plugin](http://github.com/redline6561/coleslaw/blob/master/plugins/disqus.lisp)
|
[Disqus plugin](http://github.com/redline6561/coleslaw/blob/master/plugins/disqus.lisp)
|
||||||
is a good example of this.
|
is a good example of this.
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# General Use
|
# General Use
|
||||||
|
|
||||||
* Add a list with the plugin name and settings to the ```:plugins```
|
* To enable a plugin, add its name and settings to your
|
||||||
section of your [.coleslawrc][config_file]. Plugin settings are
|
[.coleslawrc][config_file]. Plugin settings are described
|
||||||
described below.
|
below. Note that some plugins require additional setup.
|
||||||
|
|
||||||
* Available plugins are listed below with usage descriptions and
|
* Available plugins are listed below with usage descriptions and
|
||||||
config examples.
|
config examples.
|
||||||
|
@ -41,9 +41,9 @@
|
||||||
|
|
||||||
**Example**: `(incremental)`
|
**Example**: `(incremental)`
|
||||||
|
|
||||||
**Setup**:
|
**Setup**: You must run the `examples/dump_db.sh` script to
|
||||||
- You must run the `examples/dump_db.sh` script to generate a database dump
|
generate a database dump for your site before enabling the
|
||||||
for your site before enabling the incremental plugin.
|
incremental plugin.
|
||||||
|
|
||||||
## LaTeX via Mathjax
|
## LaTeX via Mathjax
|
||||||
|
|
||||||
|
@ -144,6 +144,23 @@ CL-USER> (chirp:complete-authentication "4173325")
|
||||||
#<CHIRP-OBJECTS:USER PuercoPop #18405433>
|
#<CHIRP-OBJECTS:USER PuercoPop #18405433>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Twitter Summary Cards
|
||||||
|
|
||||||
|
**Description**: Add Summary Card metadata to blog posts
|
||||||
|
to enhance twitter links to that content.
|
||||||
|
|
||||||
|
**Example**: `(twitter-summary-card :twitter-handle "@redline6561")
|
||||||
|
|
||||||
|
## Versioned Deploys
|
||||||
|
|
||||||
|
**Description**: Originally, this was Coleslaw's only deploy behavior.
|
||||||
|
Instead of deploying directly to `:deploy-dir`, creates `.curr` and
|
||||||
|
`.prev` symlinks in the *deploy-dir*, which point to timestamped
|
||||||
|
directories of the last two deploys of the site. Deploys prior to the
|
||||||
|
last two are automatically cleaned up.
|
||||||
|
|
||||||
|
**Example**: `(versioned)`
|
||||||
|
|
||||||
## Wordpress Importer
|
## Wordpress Importer
|
||||||
|
|
||||||
**NOTE**: This plugin really should be rewritten to act as a
|
**NOTE**: This plugin really should be rewritten to act as a
|
||||||
|
|
|
@ -32,7 +32,7 @@ Every page other than those in the `posts/` directory is an `index`.
|
||||||
|
|
||||||
**Every** page uses the `base.tmpl` and fills in the content using
|
**Every** page uses the `base.tmpl` and fills in the content using
|
||||||
either the `post` or `index` templates. No important logic should be
|
either the `post` or `index` templates. No important logic should be
|
||||||
in *any* template, they are only used to give provide consistent layout.
|
in *any* template, they are only used to provide a consistent layout.
|
||||||
|
|
||||||
* `base.tmpl` This template generates the outer shell of the HTML.
|
* `base.tmpl` This template generates the outer shell of the HTML.
|
||||||
It keeps a consistent look and feel for all pages in the blog. The
|
It keeps a consistent look and feel for all pages in the blog. The
|
||||||
|
@ -64,7 +64,7 @@ simplest to either modify the existing default theme, `hyde`, or copy
|
||||||
it in entirety and then tweak only the CSS of your new theme. A large
|
it in entirety and then tweak only the CSS of your new theme. A large
|
||||||
amount of visual difference can be had with a minimum of (or no)
|
amount of visual difference can be had with a minimum of (or no)
|
||||||
template hacking. There is plenty of advice on CSS styling on the web.
|
template hacking. There is plenty of advice on CSS styling on the web.
|
||||||
I'm no expert but feel free to send pull requests modifying theme's
|
I'm no expert but feel free to send pull requests modifying a theme's
|
||||||
CSS or improving this section, perhaps by recommending a CSS resource.
|
CSS or improving this section, perhaps by recommending a CSS resource.
|
||||||
|
|
||||||
## Creating a Theme from Scratch (with code)
|
## Creating a Theme from Scratch (with code)
|
||||||
|
@ -114,10 +114,15 @@ The templating language is documented [elsewhere][clt].
|
||||||
However as a short primer:
|
However as a short primer:
|
||||||
|
|
||||||
* Everything is output literally, except template commands.
|
* Everything is output literally, except template commands.
|
||||||
* Template commands are enclosed in `{` and `}`
|
* Template commands are enclosed in `{` and `}`.
|
||||||
* Variables, which are provided by coleslaw, can be referenced
|
* Variables, which are provided by coleslaw, can be referenced
|
||||||
inside a template command. So to use a variable you have to say
|
inside a template command. So to use a variable you have to say
|
||||||
`{$variable}` or `{$variable.key}`.
|
`{$variable}` or `{$variable.key}`.
|
||||||
|
**WARNING**: At present, cl-closure-template does not have great debugging.
|
||||||
|
If you typo this, e.g. `${variable}`, you will receive an *uninformative*
|
||||||
|
and apparently unrelated error. Also, attempted access of non-existent keys
|
||||||
|
fails silently. We are exploring options for making debugging easier in a
|
||||||
|
future release.
|
||||||
* If statements are written as `{if ...} ... {else} ... {/if}`.
|
* If statements are written as `{if ...} ... {else} ... {/if}`.
|
||||||
Typical examples are: `{if $injections.body} ... {/if}` or
|
Typical examples are: `{if $injections.body} ... {/if}` or
|
||||||
`{if not isLast($link)} ... {/if}`.
|
`{if not isLast($link)} ... {/if}`.
|
||||||
|
@ -139,19 +144,16 @@ The variable that should be available to all templates is:
|
||||||
#### Index Template Variables
|
#### Index Template Variables
|
||||||
|
|
||||||
- **tags** A list containing all the tags, each with keys
|
- **tags** A list containing all the tags, each with keys
|
||||||
`.name` and `.slug`.
|
`name` and `url`.
|
||||||
- **months** A list of all months with posts as `yyyy-mm` strings.
|
- **months** A list of all the content months, each with keys
|
||||||
|
`name` and `url`.
|
||||||
- **index** This is the meat of the content. This variable has
|
- **index** This is the meat of the content. This variable has
|
||||||
the following keys:
|
the following keys:
|
||||||
- `id`, the name of the page that will be rendered
|
|
||||||
- `content`, a list of content (see below)
|
- `content`, a list of content (see below)
|
||||||
- `title`, a string title to display to the user
|
- `name`, a name to use in links or href tags
|
||||||
- **prev** If this index file is part of a chain, the `id`
|
- `title`, a title to use in H1 or header tags
|
||||||
of the previous index html in the chain.
|
- **prev** Nil or the previous index with keys: `url` and `title`.
|
||||||
If this is the first file, the value will be empty.
|
- **next** Nil or the next index with keys: `url` and `title`.
|
||||||
- **next** If this index file is part of a chain, the `id`
|
|
||||||
of the next index html in the chain.
|
|
||||||
If this is the last file, the value will be empty.
|
|
||||||
|
|
||||||
#### Post Template Variable
|
#### Post Template Variable
|
||||||
|
|
||||||
|
@ -160,8 +162,8 @@ The variable that should be available to all templates is:
|
||||||
- **post** All these variables are post objects. **prev** and
|
- **post** All these variables are post objects. **prev** and
|
||||||
**next** are the adjacent posts when put in
|
**next** are the adjacent posts when put in
|
||||||
chronological order. Each post has the following keys:
|
chronological order. Each post has the following keys:
|
||||||
- `tags`, a list of tags (each with keys `name` and `slug`)
|
- `url`, the relative url of the post
|
||||||
- `slug`, the slug of the post
|
- `tags`, a list of tags (each with keys `name` and `url`)
|
||||||
- `date`, the date of posting
|
- `date`, the date of posting
|
||||||
- `text`, the HTML of the post's body
|
- `text`, the HTML of the post's body
|
||||||
- `title`, the title of the post
|
- `title`, the title of the post
|
||||||
|
@ -190,8 +192,8 @@ A simple `index.tmpl` looks like this:
|
||||||
{namespace coleslaw.theme.trivial}
|
{namespace coleslaw.theme.trivial}
|
||||||
{template index}
|
{template index}
|
||||||
{foreach $obj in $index.content}
|
{foreach $obj in $index.content}
|
||||||
<h1>{$obj.title}</h1>
|
<h1>{$object.title}</h1>
|
||||||
{$obj.text |noAutoescape}
|
{$object.text |noAutoescape}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
{/template}
|
{/template}
|
||||||
```
|
```
|
||||||
|
@ -209,14 +211,7 @@ And a simple `post.tmpl` is similarly:
|
||||||
|
|
||||||
All of the files are now populated with content. There are still no links
|
All of the files are now populated with content. There are still no links
|
||||||
between the pages so navigation is cumbersome but adding links is simple.
|
between the pages so navigation is cumbersome but adding links is simple.
|
||||||
Good luck!
|
Just do: `<a href="{$config.domain}/{$object.url}">{$object.name}</a>`.
|
||||||
|
|
||||||
## Note on adding links
|
|
||||||
|
|
||||||
As mentioned earlier, most files have a file name which is a slug of
|
|
||||||
some sort. So if you want to create a link to a tag file you should
|
|
||||||
do something like this:
|
|
||||||
`<a href="${config.domain}/tags/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>`.
|
|
||||||
|
|
||||||
[clt]: https://developers.google.com/closure/templates/
|
[clt]: https://developers.google.com/closure/templates/
|
||||||
[ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md
|
[ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
:deploy-dir "/home/git/blog/"
|
:deploy-dir "/home/git/blog/"
|
||||||
:domain "http://blog.redlinernotes.com"
|
:domain "http://blog.redlinernotes.com"
|
||||||
:feeds ("lisp")
|
:feeds ("lisp")
|
||||||
:plugins ((mathjax)
|
:plugins ((analytics :tracking-code "foo")
|
||||||
|
(disqus :shortname "my-site-name")
|
||||||
|
; (incremental) ;; *Remove comment to enable incremental builds.
|
||||||
|
(mathjax)
|
||||||
(sitemap)
|
(sitemap)
|
||||||
(static-pages)
|
(static-pages)
|
||||||
(disqus :shortname "my-site-name")
|
; (versioned) ;; *Remove comment to enable symlinked, timestamped deploys.
|
||||||
(analytics :tracking-code "foo"))
|
)
|
||||||
:repo "/home/git/tmp/improvedmeans/"
|
|
||||||
:routing ((:post "posts/~a")
|
:routing ((:post "posts/~a")
|
||||||
(:tag-index "tag/~a")
|
(:tag-index "tag/~a")
|
||||||
(:month-index "date/~a")
|
(:month-index "date/~a")
|
||||||
|
@ -22,3 +24,5 @@
|
||||||
:staging-dir "/tmp/coleslaw/"
|
:staging-dir "/tmp/coleslaw/"
|
||||||
:title "Improved Means for Achieving Deteriorated Ends"
|
:title "Improved Means for Achieving Deteriorated Ends"
|
||||||
:theme "hyde")
|
:theme "hyde")
|
||||||
|
|
||||||
|
;; * Prerequisites described in plugin docs.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
########## CONFIGURATION VALUES ##########
|
########## CONFIGURATION VALUES ##########
|
||||||
|
|
||||||
# TMP_GIT_CLONE _must_ match the :repo argument in your .coleslawrc!
|
|
||||||
TMP_GIT_CLONE=$HOME/tmp/improvedmeans/
|
TMP_GIT_CLONE=$HOME/tmp/improvedmeans/
|
||||||
|
|
||||||
# Set LISP to your preferred implementation. The following
|
# Set LISP to your preferred implementation. The following
|
||||||
|
@ -26,10 +25,11 @@ while read oldrev newrev refname; do
|
||||||
if [ $LISP = sbcl ]; then
|
if [ $LISP = sbcl ]; then
|
||||||
sbcl --eval "(ql:quickload 'coleslaw)" \
|
sbcl --eval "(ql:quickload 'coleslaw)" \
|
||||||
--eval "(coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\")" \
|
--eval "(coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\")" \
|
||||||
--eval "(coleslaw::exit)"
|
--eval "(uiop:quit)"
|
||||||
elif [ $LISP = ccl ]; then
|
elif [ $LISP = ccl ]; then
|
||||||
ccl -e "(ql:quickload 'coleslaw) (coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\") (coleslaw::exit)"
|
ccl -e "(ql:quickload 'coleslaw) (coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\") (uiop:quit)"
|
||||||
else
|
else
|
||||||
|
echo -e "$LISP is not a supported lisp dialect at this time. Exiting.\n"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -21,4 +21,5 @@
|
||||||
</script>")
|
</script>")
|
||||||
|
|
||||||
(defun enable (&key tracking-code)
|
(defun enable (&key tracking-code)
|
||||||
(add-injection (format nil *analytics-js* tracking-code) :head))
|
(let ((snippet (format nil *analytics-js* tracking-code)))
|
||||||
|
(add-injection (constantly snippet) :head)))
|
||||||
|
|
|
@ -24,5 +24,7 @@
|
||||||
<a href=\"http://disqus.com\" class=\"dsq-brlink\">comments powered by <span class=\"logo-disqus\">Disqus</span></a>")
|
<a href=\"http://disqus.com\" class=\"dsq-brlink\">comments powered by <span class=\"logo-disqus\">Disqus</span></a>")
|
||||||
|
|
||||||
(defun enable (&key shortname)
|
(defun enable (&key shortname)
|
||||||
(add-injection (list (format nil *disqus-header* shortname)
|
(flet ((inject-p (x)
|
||||||
(lambda (x) (typep x 'post))) :body))
|
(when (typep x 'post)
|
||||||
|
(format nil *disqus-header* shortname))))
|
||||||
|
(add-injection #'inject-p :body)))
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
|
|
||||||
(defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML")
|
(defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML")
|
||||||
(location "http://cdn.mathjax.org/mathjax/latest/MathJax.js"))
|
(location "http://cdn.mathjax.org/mathjax/latest/MathJax.js"))
|
||||||
(flet ((plugin-p (x) (or force (mathjax-p x))))
|
(flet ((inject-p (x)
|
||||||
(let ((mathjax-header (format nil *mathjax-header* config location preset)))
|
(when (or force (mathjax-p x))
|
||||||
(add-injection (list mathjax-header #'plugin-p) :head))))
|
(format nil *mathjax-header* config location preset))))
|
||||||
|
(add-injection #'inject-p :head)))
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
(eval-when (:compile-toplevel :load-toplevel)
|
|
||||||
(ql:quickload 'lparallel))
|
|
||||||
|
|
||||||
(defpackage :coleslaw-parallel
|
|
||||||
(:use :cl)
|
|
||||||
(:export #:enable))
|
|
||||||
|
|
||||||
(in-package :coleslaw-parallel)
|
|
||||||
|
|
||||||
;; TODO: The bulk of the speedup here should come from parallelizing discover.
|
|
||||||
;; Publish will also benefit. Whether it's better to spin off threads for each
|
|
||||||
;; content type/index type or the operations *within* discover/publish is not
|
|
||||||
;; known, the higher granularity of doing it at the iterating over types level
|
|
||||||
;; is certainly easier to prototype though.
|
|
||||||
|
|
||||||
(defun enable ())
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
(in-package :coleslaw-sitemap)
|
(in-package :coleslaw-sitemap)
|
||||||
|
|
||||||
(defclass sitemap (index)
|
(defclass sitemap ()
|
||||||
((urls :initarg :urls :reader urls)))
|
((urls :initarg :urls :reader urls)))
|
||||||
|
|
||||||
(defmethod page-url ((object sitemap)) "sitemap.xml")
|
(defmethod page-url ((object sitemap)) "sitemap.xml")
|
||||||
|
|
|
@ -20,8 +20,9 @@
|
||||||
|
|
||||||
(defmethod initialize-instance :after ((object page) &key)
|
(defmethod initialize-instance :after ((object page) &key)
|
||||||
;; Expect all static-pages to be written in Markdown for now.
|
;; Expect all static-pages to be written in Markdown for now.
|
||||||
(with-accessors ((text content-text)) object
|
(with-slots (url text) object
|
||||||
(setf text (render-text text :md))))
|
(setf url (make-pathname :defaults url)
|
||||||
|
text (render-text text :md))))
|
||||||
|
|
||||||
(defmethod render ((object page) &key next prev)
|
(defmethod render ((object page) &key next prev)
|
||||||
;; For the time being, we'll re-use the normal post theme.
|
;; For the time being, we'll re-use the normal post theme.
|
||||||
|
|
25
plugins/twitter-summary-card.lisp
Normal file
25
plugins/twitter-summary-card.lisp
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
(defpackage :coleslaw-twitter-summary-card
|
||||||
|
(:use :cl :coleslaw)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-twitter-summary-card)
|
||||||
|
|
||||||
|
(defun summary-card (post twitter-handle)
|
||||||
|
"TODO: Figure if and how to include twitter:url meta property."
|
||||||
|
(format nil "<meta property=\"twitter:card\" content=\"summary\" />
|
||||||
|
~@[<meta property=\"twitter:author\" content=\"~A\" />~]
|
||||||
|
<meta property=\"twitter:title\" content=\"~A\" />
|
||||||
|
<meta property=\"twitter:description\" content=\"~A\" />"
|
||||||
|
twitter-handle
|
||||||
|
(title-of post)
|
||||||
|
(let ((text (content-text post)))
|
||||||
|
(if (< 200 (length text))
|
||||||
|
(subseq text 0 199)
|
||||||
|
text))))
|
||||||
|
|
||||||
|
(defun enable (&key twitter-handle)
|
||||||
|
(add-injection
|
||||||
|
(lambda (x)
|
||||||
|
(when (typep x 'post)
|
||||||
|
(summary-card x twitter-handle)))
|
||||||
|
:head))
|
24
plugins/versioned.lisp
Normal file
24
plugins/versioned.lisp
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
(defpackage :coleslaw-versioned
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw #:*config*
|
||||||
|
#:deploy-dir
|
||||||
|
#:rel-path
|
||||||
|
#:run-program
|
||||||
|
#:update-symlink))
|
||||||
|
|
||||||
|
(in-package :coleslaw-versioned)
|
||||||
|
|
||||||
|
(defmethod coleslaw:deploy (staging)
|
||||||
|
(let* ((dest (deploy-dir *config*))
|
||||||
|
(new-build (rel-path dest "generated/~a" (get-universal-time)))
|
||||||
|
(prev (rel-path dest ".prev"))
|
||||||
|
(curr (rel-path dest ".curr")))
|
||||||
|
(ensure-directories-exist new-build)
|
||||||
|
(run-program "mv ~a ~a" staging new-build)
|
||||||
|
(when (and (probe-file prev) (truename prev))
|
||||||
|
(run-program "rm -r ~a" (truename prev)))
|
||||||
|
(when (probe-file curr)
|
||||||
|
(update-symlink prev (truename curr)))
|
||||||
|
(update-symlink curr new-build)))
|
||||||
|
|
||||||
|
(defun enable ())
|
|
@ -3,12 +3,11 @@
|
||||||
(defvar *last-revision* nil
|
(defvar *last-revision* nil
|
||||||
"The git revision prior to the last push. For use with GET-UPDATED-FILES.")
|
"The git revision prior to the last push. For use with GET-UPDATED-FILES.")
|
||||||
|
|
||||||
(defun main (&optional (repo-dir "") oldrev)
|
(defun main (repo-dir &optional oldrev)
|
||||||
"Load the user's config file, then compile and deploy the site. Optionally,
|
"Load the user's config file, then compile and deploy the blog stored
|
||||||
REPO-DIR is the location of the blog repo and OLDREV is the revision prior to
|
in REPO-DIR. Optionally, OLDREV is the revision prior to the last push."
|
||||||
the last push."
|
|
||||||
(setf *last-revision* oldrev)
|
|
||||||
(load-config repo-dir)
|
(load-config repo-dir)
|
||||||
|
(setf *last-revision* oldrev)
|
||||||
(load-content)
|
(load-content)
|
||||||
(compile-theme (theme *config*))
|
(compile-theme (theme *config*))
|
||||||
(let ((dir (staging-dir *config*)))
|
(let ((dir (staging-dir *config*)))
|
||||||
|
@ -40,19 +39,9 @@ the last push."
|
||||||
(update-symlink "index.html" "1.html")))
|
(update-symlink "index.html" "1.html")))
|
||||||
|
|
||||||
(defgeneric deploy (staging)
|
(defgeneric deploy (staging)
|
||||||
(:documentation "Deploy the STAGING dir, updating the .prev and .curr symlinks.")
|
(:documentation "Deploy the STAGING build to the directory specified in the config.")
|
||||||
(:method (staging)
|
(:method (staging)
|
||||||
(let* ((dest (deploy-dir *config*))
|
(run-program "rsync --delete -avz ~a ~a" staging (deploy-dir *config*))))
|
||||||
(new-build (rel-path dest "generated/~a" (get-universal-time)))
|
|
||||||
(prev (rel-path dest ".prev"))
|
|
||||||
(curr (rel-path dest ".curr")))
|
|
||||||
(ensure-directories-exist new-build)
|
|
||||||
(run-program "mv ~a ~a" staging new-build)
|
|
||||||
(when (and (probe-file prev) (truename prev))
|
|
||||||
(run-program "rm -r ~a" (truename prev)))
|
|
||||||
(when (probe-file curr)
|
|
||||||
(update-symlink prev (truename curr)))
|
|
||||||
(update-symlink curr new-build))))
|
|
||||||
|
|
||||||
(defun update-symlink (path target)
|
(defun update-symlink (path target)
|
||||||
"Update the symlink at PATH to point to TARGET."
|
"Update the symlink at PATH to point to TARGET."
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
(license :initarg :license :reader license)
|
(license :initarg :license :reader license)
|
||||||
(page-ext :initarg :page-ext :reader page-ext)
|
(page-ext :initarg :page-ext :reader page-ext)
|
||||||
(plugins :initarg :plugins :reader plugins)
|
(plugins :initarg :plugins :reader plugins)
|
||||||
(repo :initarg :repo :reader repo)
|
(repo :initarg :repo :accessor repo)
|
||||||
(routing :initarg :routing :reader routing)
|
(routing :initarg :routing :reader routing)
|
||||||
(separator :initarg :separator :reader separator)
|
(separator :initarg :separator :reader separator)
|
||||||
(sitenav :initarg :sitenav :reader sitenav)
|
(sitenav :initarg :sitenav :reader sitenav)
|
||||||
|
@ -18,6 +18,10 @@
|
||||||
(theme :initarg :theme :reader theme)
|
(theme :initarg :theme :reader theme)
|
||||||
(title :initarg :title :reader title))
|
(title :initarg :title :reader title))
|
||||||
(:default-initargs
|
(:default-initargs
|
||||||
|
:feeds nil
|
||||||
|
:license nil
|
||||||
|
:plugins nil
|
||||||
|
:sitenav nil
|
||||||
:charset "UTF-8"
|
:charset "UTF-8"
|
||||||
:lang "en"
|
:lang "en"
|
||||||
:page-ext "html"
|
:page-ext "html"
|
||||||
|
@ -61,10 +65,11 @@ doesn't exist, use the .coleslawrc in the home directory."
|
||||||
repo-config
|
repo-config
|
||||||
(rel-path (user-homedir-pathname) ".coleslawrc"))))
|
(rel-path (user-homedir-pathname) ".coleslawrc"))))
|
||||||
|
|
||||||
(defun load-config (&optional repo-dir)
|
(defun load-config (&optional (repo-dir ""))
|
||||||
"Find and load the coleslaw configuration from .coleslawrc. REPO-DIR will be
|
"Find and load the coleslaw configuration from .coleslawrc. REPO-DIR will be
|
||||||
preferred over the home directory if provided."
|
preferred over the home directory if provided."
|
||||||
(with-open-file (in (discover-config-path repo-dir) :external-format '(:utf-8))
|
(with-open-file (in (discover-config-path repo-dir) :external-format '(:utf-8))
|
||||||
(let ((config-form (read in)))
|
(let ((config-form (read in)))
|
||||||
(setf *config* (construct 'blog config-form))))
|
(setf *config* (construct 'blog config-form)
|
||||||
|
(repo *config*) repo-dir)))
|
||||||
(load-plugins (plugins *config*)))
|
(load-plugins (plugins *config*)))
|
||||||
|
|
|
@ -4,7 +4,12 @@
|
||||||
|
|
||||||
(defclass tag ()
|
(defclass tag ()
|
||||||
((name :initarg :name :reader tag-name)
|
((name :initarg :name :reader tag-name)
|
||||||
(slug :initarg :slug :reader tag-slug)))
|
(slug :initarg :slug :reader tag-slug)
|
||||||
|
(url :initarg :url)))
|
||||||
|
|
||||||
|
(defmethod initialize-instance :after ((tag tag) &key)
|
||||||
|
(with-slots (url slug) tag
|
||||||
|
(setf url (compute-url nil slug 'tag-index))))
|
||||||
|
|
||||||
(defun make-tag (str)
|
(defun make-tag (str)
|
||||||
"Takes a string and returns a TAG instance with a name and slug."
|
"Takes a string and returns a TAG instance with a name and slug."
|
||||||
|
@ -31,39 +36,47 @@
|
||||||
;; Content Types
|
;; Content Types
|
||||||
|
|
||||||
(defclass content ()
|
(defclass content ()
|
||||||
((file :initarg :file :reader content-file)
|
((url :initarg :url :reader page-url)
|
||||||
(date :initarg :date :reader content-date)
|
(date :initarg :date :reader content-date)
|
||||||
(tags :initarg :tags :accessor content-tags)
|
(file :initarg :file :reader content-file)
|
||||||
(slug :initarg :slug :accessor content-slug)
|
(tags :initarg :tags :reader content-tags)
|
||||||
(text :initarg :text :accessor content-text))
|
(text :initarg :text :reader content-text))
|
||||||
(:default-initargs :tags nil :date nil :slug nil))
|
(:default-initargs :tags nil :date nil))
|
||||||
|
|
||||||
(defmethod initialize-instance :after ((object content) &key)
|
(defmethod initialize-instance :after ((object content) &key)
|
||||||
(with-accessors ((tags content-tags)) object
|
(with-slots (tags) object
|
||||||
(when (stringp tags)
|
(when (stringp tags)
|
||||||
(setf tags (mapcar #'make-tag (cl-ppcre:split "," tags))))))
|
(setf tags (mapcar #'make-tag (cl-ppcre:split "," tags))))))
|
||||||
|
|
||||||
|
(defun parse-initarg (line)
|
||||||
|
"Given a metadata header, LINE, parse an initarg name/value pair from it."
|
||||||
|
(let ((name (string-upcase (subseq line 0 (position #\: line))))
|
||||||
|
(match (nth-value 1 (scan-to-strings "[a-zA-Z]+:\\s+(.*)" line))))
|
||||||
|
(when match
|
||||||
|
(list (make-keyword name) (aref match 0)))))
|
||||||
|
|
||||||
|
(defun parse-metadata (stream)
|
||||||
|
"Given a STREAM, parse metadata from it or signal an appropriate condition."
|
||||||
|
(flet ((get-next-line (input)
|
||||||
|
(string-trim '(#\Space #\Newline #\Tab) (read-line input nil))))
|
||||||
|
(unless (string= (get-next-line stream) (separator *config*))
|
||||||
|
(error "The file lacks the expected header: ~a" (separator *config*)))
|
||||||
|
(loop for line = (get-next-line stream)
|
||||||
|
until (string= line (separator *config*))
|
||||||
|
appending (parse-initarg line))))
|
||||||
|
|
||||||
(defun read-content (file)
|
(defun read-content (file)
|
||||||
"Returns a plist of metadata from FILE with :text holding the content as a string."
|
"Returns a plist of metadata from FILE with :text holding the content."
|
||||||
(flet ((slurp-remainder (stream)
|
(flet ((slurp-remainder (stream)
|
||||||
(let ((seq (make-string (- (file-length stream)
|
(let ((seq (make-string (- (file-length stream)
|
||||||
(file-position stream)))))
|
(file-position stream)))))
|
||||||
(read-sequence seq stream)
|
(read-sequence seq stream)
|
||||||
(remove #\Nul seq)))
|
(remove #\Nul seq))))
|
||||||
(parse-field (str)
|
|
||||||
(nth-value 1 (cl-ppcre:scan-to-strings "[a-zA-Z]+:\\s+(.*)" str)))
|
|
||||||
(field-name (line)
|
|
||||||
(make-keyword (string-upcase (subseq line 0 (position #\: line))))))
|
|
||||||
(with-open-file (in file :external-format '(:utf-8))
|
(with-open-file (in file :external-format '(:utf-8))
|
||||||
(unless (string= (read-line in) (separator *config*))
|
(let ((metadata (parse-metadata in))
|
||||||
(error "The provided file lacks the expected header."))
|
(content (slurp-remainder in))
|
||||||
(let ((meta (loop for line = (read-line in nil)
|
(filepath (enough-namestring file (repo *config*))))
|
||||||
until (string= line (separator *config*))
|
(append metadata (list :text content :file filepath))))))
|
||||||
appending (list (field-name line)
|
|
||||||
(aref (parse-field line) 0))))
|
|
||||||
(filepath (enough-namestring file (repo *config*)))
|
|
||||||
(content (slurp-remainder in)))
|
|
||||||
(append meta (list :text content :file filepath))))))
|
|
||||||
|
|
||||||
;; Helper Functions
|
;; Helper Functions
|
||||||
|
|
||||||
|
|
|
@ -26,24 +26,28 @@
|
||||||
;; Instance Methods
|
;; Instance Methods
|
||||||
|
|
||||||
(defgeneric page-url (document)
|
(defgeneric page-url (document)
|
||||||
(:documentation "The url to the DOCUMENT without the domain.")
|
(:documentation "The relative URL to the DOCUMENT."))
|
||||||
(:method (document)
|
|
||||||
(let* ((class-name (class-name (class-of document)))
|
|
||||||
(route (assoc (make-keyword class-name) (routing *config*))))
|
|
||||||
(if route
|
|
||||||
(format nil (second route) (slot-value document 'slug))
|
|
||||||
(error "No routing method found for: ~A" class-name)))))
|
|
||||||
|
|
||||||
(defmethod page-url :around ((document t))
|
|
||||||
(let* ((result (call-next-method))
|
|
||||||
(type (or (pathname-type result) "html")))
|
|
||||||
(make-pathname :type type :defaults result)))
|
|
||||||
|
|
||||||
(defgeneric render (document &key &allow-other-keys)
|
(defgeneric render (document &key &allow-other-keys)
|
||||||
(:documentation "Render the given DOCUMENT to HTML."))
|
(:documentation "Render the given DOCUMENT to HTML."))
|
||||||
|
|
||||||
;; Helper Functions
|
;; Helper Functions
|
||||||
|
|
||||||
|
(defun compute-url (document unique-id &optional class)
|
||||||
|
"Compute the relative URL for a DOCUMENT based on its UNIQUE-ID. If CLASS
|
||||||
|
is provided, it overrides the route used."
|
||||||
|
(let* ((class-name (or class (class-name (class-of document))))
|
||||||
|
(route (get-route class-name)))
|
||||||
|
(unless route
|
||||||
|
(error "No routing method found for: ~A" class-name))
|
||||||
|
(let* ((result (format nil route unique-id))
|
||||||
|
(type (or (pathname-type result) (page-ext *config*))))
|
||||||
|
(make-pathname :type type :defaults result))))
|
||||||
|
|
||||||
|
(defun get-route (doc-type)
|
||||||
|
"Return the route format string for DOC-TYPE."
|
||||||
|
(second (assoc (make-keyword doc-type) (routing *config*))))
|
||||||
|
|
||||||
(defun add-document (document)
|
(defun add-document (document)
|
||||||
"Add DOCUMENT to the in-memory database. Error if a matching entry is present."
|
"Add DOCUMENT to the in-memory database. Error if a matching entry is present."
|
||||||
(let ((url (page-url document)))
|
(let ((url (page-url document)))
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
;;; Atom and RSS Feeds
|
;;; Atom and RSS Feeds
|
||||||
|
|
||||||
(defclass feed (index)
|
(defclass base-feed () ((format :initarg :format :reader feed-format)))
|
||||||
((format :initform nil :initarg :format :accessor feed-format)))
|
|
||||||
|
(defclass feed (index base-feed) ())
|
||||||
|
|
||||||
(defmethod discover ((doc-type (eql (find-class 'feed))))
|
(defmethod discover ((doc-type (eql (find-class 'feed))))
|
||||||
(let ((content (by-date (find-all 'post))))
|
(let ((content (by-date (find-all 'post))))
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
|
|
||||||
;;; Tag Feeds
|
;;; Tag Feeds
|
||||||
|
|
||||||
(defclass tag-feed (feed) ())
|
(defclass tag-feed (index base-feed) ())
|
||||||
|
|
||||||
(defmethod discover ((doc-type (eql (find-class 'tag-feed))))
|
(defmethod discover ((doc-type (eql (find-class 'tag-feed))))
|
||||||
(let ((content (by-date (find-all 'post))))
|
(let ((content (by-date (find-all 'post))))
|
||||||
|
|
|
@ -6,13 +6,18 @@
|
||||||
"The list of tags which content has been tagged with.")
|
"The list of tags which content has been tagged with.")
|
||||||
|
|
||||||
(defclass index ()
|
(defclass index ()
|
||||||
((slug :initarg :slug :reader index-slug)
|
((url :initarg :url :reader page-url)
|
||||||
(title :initarg :title :reader title-of)
|
(name :initarg :name :reader index-name)
|
||||||
|
(title :initarg :title :reader title-of)
|
||||||
(content :initarg :content :reader index-content)))
|
(content :initarg :content :reader index-content)))
|
||||||
|
|
||||||
|
(defmethod initialize-instance :after ((object index) &key slug)
|
||||||
|
(with-slots (url) object
|
||||||
|
(setf url (compute-url object slug))))
|
||||||
|
|
||||||
(defmethod render ((object index) &key prev next)
|
(defmethod render ((object index) &key prev next)
|
||||||
(funcall (theme-fn 'index) (list :tags *all-tags*
|
(funcall (theme-fn 'index) (list :tags (find-all 'tag-index)
|
||||||
:months *all-months*
|
:months (find-all 'month-index)
|
||||||
:config *config*
|
:config *config*
|
||||||
:index object
|
:index object
|
||||||
:prev prev
|
:prev prev
|
||||||
|
@ -24,12 +29,12 @@
|
||||||
|
|
||||||
(defmethod discover ((doc-type (eql (find-class 'tag-index))))
|
(defmethod discover ((doc-type (eql (find-class 'tag-index))))
|
||||||
(let ((content (by-date (find-all 'post))))
|
(let ((content (by-date (find-all 'post))))
|
||||||
(dolist (tag (all-tags))
|
(dolist (tag *all-tags*)
|
||||||
(add-document (index-by-tag tag content)))))
|
(add-document (index-by-tag tag content)))))
|
||||||
|
|
||||||
(defun index-by-tag (tag content)
|
(defun index-by-tag (tag content)
|
||||||
"Return an index of all CONTENT matching the given TAG."
|
"Return an index of all CONTENT matching the given TAG."
|
||||||
(make-instance 'tag-index :slug (tag-slug tag)
|
(make-instance 'tag-index :slug (tag-slug tag) :name (tag-name tag)
|
||||||
:content (remove-if-not (lambda (x) (tag-p tag x)) content)
|
:content (remove-if-not (lambda (x) (tag-p tag x)) content)
|
||||||
:title (format nil "Content tagged ~a" (tag-name tag))))
|
:title (format nil "Content tagged ~a" (tag-name tag))))
|
||||||
|
|
||||||
|
@ -48,7 +53,7 @@
|
||||||
|
|
||||||
(defun index-by-month (month content)
|
(defun index-by-month (month content)
|
||||||
"Return an index of all CONTENT matching the given MONTH."
|
"Return an index of all CONTENT matching the given MONTH."
|
||||||
(make-instance 'month-index :slug month
|
(make-instance 'month-index :slug month :name month
|
||||||
:content (remove-if-not (lambda (x) (month-p month x)) content)
|
:content (remove-if-not (lambda (x) (month-p month x)) content)
|
||||||
:title (format nil "Content from ~a" month)))
|
:title (format nil "Content from ~a" month)))
|
||||||
|
|
||||||
|
@ -68,18 +73,14 @@
|
||||||
(defun index-by-n (i content)
|
(defun index-by-n (i content)
|
||||||
"Return the index for the Ith page of CONTENT in reverse chronological order."
|
"Return the index for the Ith page of CONTENT in reverse chronological order."
|
||||||
(let ((content (subseq content (* 10 i))))
|
(let ((content (subseq content (* 10 i))))
|
||||||
(make-instance 'numeric-index :slug (1+ i)
|
(make-instance 'numeric-index :slug (1+ i) :name (1+ i)
|
||||||
:content (take-up-to 10 content)
|
:content (take-up-to 10 content)
|
||||||
:title "Recent Content")))
|
:title "Recent Content")))
|
||||||
|
|
||||||
(defmethod publish ((doc-type (eql (find-class 'numeric-index))))
|
(defmethod publish ((doc-type (eql (find-class 'numeric-index))))
|
||||||
(let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-slug)))
|
(let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-name)))
|
||||||
(dolist (index indexes)
|
(loop for (next index prev) on (append '(nil) indexes)
|
||||||
(let ((prev (1- (index-slug index)))
|
while index do (write-document index nil :prev prev :next next))))
|
||||||
(next (1+ (index-slug index))))
|
|
||||||
(write-document index nil
|
|
||||||
:prev (when (plusp prev) prev)
|
|
||||||
:next (when (<= next (length indexes)) next))))))
|
|
||||||
|
|
||||||
;;; Helper Functions
|
;;; Helper Functions
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#:make-keyword
|
#:make-keyword
|
||||||
#:mappend)
|
#:mappend)
|
||||||
(:import-from :cl-fad #:file-exists-p)
|
(:import-from :cl-fad #:file-exists-p)
|
||||||
|
(:import-from :cl-ppcre #:scan-to-strings)
|
||||||
(:import-from :closure-template #:compile-template)
|
(:import-from :closure-template #:compile-template)
|
||||||
(:import-from :local-time #:format-rfc1123-timestring)
|
(:import-from :local-time #:format-rfc1123-timestring)
|
||||||
(:import-from :uiop #:getcwd
|
(:import-from :uiop #:getcwd
|
||||||
|
@ -22,12 +23,13 @@
|
||||||
#:title-of
|
#:title-of
|
||||||
#:author-of
|
#:author-of
|
||||||
#:find-content-by-path
|
#:find-content-by-path
|
||||||
;; Plugin API + Theming
|
;; Theming + Plugin API
|
||||||
|
#:theme-fn
|
||||||
#:plugin-conf-error
|
#:plugin-conf-error
|
||||||
#:render-text
|
#:render-text
|
||||||
#:add-injection
|
#:add-injection
|
||||||
#:get-updated-files
|
#:get-updated-files
|
||||||
#:theme-fn
|
#:deploy
|
||||||
;; The Document Protocol
|
;; The Document Protocol
|
||||||
#:discover
|
#:discover
|
||||||
#:publish
|
#:publish
|
||||||
|
@ -37,4 +39,5 @@
|
||||||
#:purge-all
|
#:purge-all
|
||||||
#:add-document
|
#:add-document
|
||||||
#:delete-document
|
#:delete-document
|
||||||
#:write-document))
|
#:write-document
|
||||||
|
#:content-text))
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
(in-package :coleslaw)
|
(in-package :coleslaw)
|
||||||
|
|
||||||
(defclass post (content)
|
(defclass post (content)
|
||||||
((title :initarg :title :reader title-of)
|
((title :initarg :title :reader title-of)
|
||||||
(author :initarg :author :accessor author-of)
|
(author :initarg :author :reader author-of)
|
||||||
(format :initarg :format :accessor post-format))
|
(format :initarg :format :reader post-format))
|
||||||
(:default-initargs :author nil))
|
(:default-initargs :author nil))
|
||||||
|
|
||||||
(defmethod initialize-instance :after ((object post) &key)
|
(defmethod initialize-instance :after ((object post) &key)
|
||||||
(with-accessors ((title title-of)
|
(with-slots (url title author format text) object
|
||||||
(author author-of)
|
(setf url (compute-url object (slugify title))
|
||||||
(format post-format)
|
format (make-keyword (string-upcase format))
|
||||||
(text content-text)) object
|
text (render-text text format)
|
||||||
(setf (content-slug object) (slugify title)
|
author (or author (author *config*)))))
|
||||||
format (make-keyword (string-upcase format))
|
|
||||||
text (render-text text format)
|
|
||||||
author (or author (author *config*)))))
|
|
||||||
|
|
||||||
(defmethod render ((object post) &key prev next)
|
(defmethod render ((object post) &key prev next)
|
||||||
(funcall (theme-fn 'post) (list :config *config*
|
(funcall (theme-fn 'post) (list :config *config*
|
||||||
|
|
|
@ -4,21 +4,15 @@
|
||||||
"A list that stores pairs of (string . predicate) to inject in the page.")
|
"A list that stores pairs of (string . predicate) to inject in the page.")
|
||||||
|
|
||||||
(defun add-injection (injection location)
|
(defun add-injection (injection location)
|
||||||
"Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be
|
"Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be a
|
||||||
a string which will always be added or a (string . lambda). In the latter case,
|
function that takes a DOCUMENT and returns NIL or a STRING for template insertion."
|
||||||
the lambda takes a single argument, a content object, i.e. a POST or INDEX, and
|
(push injection (getf *injections* location)))
|
||||||
any return value other than nil indicates the injection should be added."
|
|
||||||
(let ((result (etypecase injection
|
|
||||||
(string (list injection #'identity))
|
|
||||||
(list injection))))
|
|
||||||
(push result (getf *injections* location))))
|
|
||||||
|
|
||||||
(defun find-injections (content)
|
(defun find-injections (content)
|
||||||
"Iterate over *INJECTIONS* collecting any that should be added to CONTENT."
|
"Iterate over *INJECTIONS* collecting any that should be added to CONTENT."
|
||||||
(flet ((injections-for (location)
|
(flet ((injections-for (location)
|
||||||
(loop for (injection predicate) in (getf *injections* location)
|
(loop for injection in (getf *injections* location)
|
||||||
when (funcall predicate content)
|
collecting (funcall injection content))))
|
||||||
collect injection)))
|
|
||||||
(list :head (injections-for :head)
|
(list :head (injections-for :head)
|
||||||
:body (injections-for :body))))
|
:body (injections-for :body))))
|
||||||
|
|
||||||
|
|
|
@ -54,13 +54,9 @@ an UNWIND-PROTECT, then change back to the current directory."
|
||||||
(setf (getcwd) ,old)))))
|
(setf (getcwd) ,old)))))
|
||||||
|
|
||||||
(defun exit ()
|
(defun exit ()
|
||||||
|
;; KLUDGE: Just call UIOP for now. Don't want users updating scripts.
|
||||||
"Exit the lisp system returning a 0 status code."
|
"Exit the lisp system returning a 0 status code."
|
||||||
#+sbcl (sb-ext:exit)
|
(uiop:quit))
|
||||||
#+ccl (ccl:quit)
|
|
||||||
#+ecl (si:quit)
|
|
||||||
#+cmucl (ext:quit)
|
|
||||||
#+clisp (ext:quit)
|
|
||||||
#-(or sbcl ccl ecl cmucl clisp) (error "Not implemented yet."))
|
|
||||||
|
|
||||||
(defun fmt (fmt-str args)
|
(defun fmt (fmt-str args)
|
||||||
"A convenient FORMAT interface for string building."
|
"A convenient FORMAT interface for string building."
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
(defpackage :coleslaw-tests
|
|
||||||
(:use :cl :fiveam)
|
|
||||||
(:export #:run!))
|
|
61
tests/plugins/twitter-summary-card.lisp
Normal file
61
tests/plugins/twitter-summary-card.lisp
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
(defpackage :summary-card-tests
|
||||||
|
(:use :cl :coleslaw :stefil))
|
||||||
|
|
||||||
|
(in-package :summary-card-tests)
|
||||||
|
|
||||||
|
(defsuite summary-cards)
|
||||||
|
(in-suite summary-cards)
|
||||||
|
|
||||||
|
;; TODO: Create a fixture to either load a mocked config or load set of plugins.
|
||||||
|
;; Then wrap these tests to use that fixture. Then add these to defsystem, setup
|
||||||
|
;; general test run with other packages.
|
||||||
|
|
||||||
|
(coleslaw::enable-plugin :twitter-summary-card)
|
||||||
|
|
||||||
|
(defvar *short-post*
|
||||||
|
(make-instance 'post :title "hai" :text "very greetings" :format "html"))
|
||||||
|
|
||||||
|
(defvar *long-post*
|
||||||
|
(make-instance 'post :title "What a Wonderful World"
|
||||||
|
:text "I see trees of green, red roses too. I see them
|
||||||
|
bloom, for me and you. And I think to myself, what a wonderful world.
|
||||||
|
|
||||||
|
I see skies of blue,
|
||||||
|
And clouds of white.
|
||||||
|
The bright blessed day,
|
||||||
|
The dark sacred night.
|
||||||
|
And I think to myself,
|
||||||
|
What a wonderful world.
|
||||||
|
|
||||||
|
The colors of the rainbow,
|
||||||
|
So pretty in the sky.
|
||||||
|
Are also on the faces,
|
||||||
|
Of people going by,
|
||||||
|
I see friends shaking hands.
|
||||||
|
Saying, \"How do you do?\"
|
||||||
|
They're really saying,
|
||||||
|
\"I love you\".
|
||||||
|
|
||||||
|
I hear babies cry,
|
||||||
|
I watch them grow,
|
||||||
|
They'll learn much more,
|
||||||
|
Than I'll ever know.
|
||||||
|
And I think to myself,
|
||||||
|
What a wonderful world.
|
||||||
|
|
||||||
|
Yes, I think to myself,
|
||||||
|
What a wonderful world. " :format "html"))
|
||||||
|
|
||||||
|
(deftest summary-card-sans-twitter-handle ()
|
||||||
|
(let ((summary-card (summary-card *short-post* nil)))
|
||||||
|
(is (null (cl-ppcre:scan "twitter:author" summary-card)))))
|
||||||
|
|
||||||
|
(deftest summary-card-with-twitter-handle ()
|
||||||
|
(let ((summary-card (summary-card *short-post* "@PuercoPop")))
|
||||||
|
(is (cl-ppcre:scan "twitter:author" summary-card))))
|
||||||
|
|
||||||
|
(deftest summary-card-trims-long-post ()
|
||||||
|
(let ((summary-card (summary-card *long-post* nil)))
|
||||||
|
(multiple-value-bind ())
|
||||||
|
;; (scan "twitter:description\" content=\"(.*)\"" summary-card)
|
||||||
|
summary-card))
|
|
@ -1,13 +0,0 @@
|
||||||
(in-package :coleslaw-tests)
|
|
||||||
|
|
||||||
(defmacro deftest (name docstring &body body)
|
|
||||||
`(test ,name
|
|
||||||
,docstring
|
|
||||||
,@body))
|
|
||||||
|
|
||||||
(def-suite coleslaw-tests)
|
|
||||||
(in-suite coleslaw-tests)
|
|
||||||
|
|
||||||
(deftest sanity-test
|
|
||||||
"A blog should compile and deploy correctly."
|
|
||||||
(is (zerop (coleslaw:main))))
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
{foreach $post in $content.content}
|
{foreach $post in $content.content}
|
||||||
<entry>
|
<entry>
|
||||||
<link type="text/html" rel="alternate" href="{$config.domain}/posts/{$post.slug}.{$config.pageExt}"/>
|
<link type="text/html" rel="alternate" href="{$config.domain}/{$post.url}"/>
|
||||||
<title>{$post.title}</title>
|
<title>{$post.title}</title>
|
||||||
<published>{$post.date}</published>
|
<published>{$post.date}</published>
|
||||||
<updated>{$post.date}</updated>
|
<updated>{$post.date}</updated>
|
||||||
|
|
|
@ -4,20 +4,20 @@
|
||||||
<h1 class="title">{$index.title}</h1>
|
<h1 class="title">{$index.title}</h1>
|
||||||
{foreach $obj in $index.content}
|
{foreach $obj in $index.content}
|
||||||
<div class="article-meta">
|
<div class="article-meta">
|
||||||
<a class="article-title" href="{$config.domain}/posts/{$obj.slug}.{$config.pageExt}">{$obj.title}</a>
|
<a class="article-title" href="{$config.domain}/{$obj.url}">{$obj.title}</a>
|
||||||
<div class="date"> posted on {$obj.date}</div>
|
<div class="date"> posted on {$obj.date}</div>
|
||||||
<div class="article">{$obj.text |noAutoescape}</div>
|
<div class="article">{$obj.text |noAutoescape}</div>
|
||||||
</div>
|
</div>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
<div id="relative-nav">
|
<div id="relative-nav">
|
||||||
{if $prev} <a href="{$prev}.{$config.pageExt}">Previous</a> {/if}
|
{if $prev} <a href="{$config.domain}/{$prev.url}">Previous</a> {/if}
|
||||||
{if $next} <a href="{$next}.{$config.pageExt}">Next</a> {/if}
|
{if $next} <a href="{$config.domain}/{$next.url}">Next</a> {/if}
|
||||||
</div>
|
</div>
|
||||||
{if $tags}
|
{if $tags}
|
||||||
<div id="tagsoup">
|
<div id="tagsoup">
|
||||||
<p>This blog covers
|
<p>This blog covers
|
||||||
{foreach $tag in $tags}
|
{foreach $tag in $tags}
|
||||||
<a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil}
|
<a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
|
||||||
{if not isLast($tag)},{sp}{/if}
|
{if not isLast($tag)},{sp}{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
<div id="monthsoup">
|
<div id="monthsoup">
|
||||||
<p>View content from
|
<p>View content from
|
||||||
{foreach $month in $months}
|
{foreach $month in $months}
|
||||||
<a href="{$config.domain}/date/{$month}.{$config.pageExt}">{$month}</a>{nil}
|
<a href="{$config.domain}/{$month.url}">{$month.name}</a>{nil}
|
||||||
{if not isLast($month)},{sp}{/if}
|
{if not isLast($month)},{sp}{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="tags">{\n}
|
<div class="tags">{\n}
|
||||||
{if $post.tags}
|
{if $post.tags}
|
||||||
Tagged as {foreach $tag in $post.tags}
|
Tagged as {foreach $tag in $post.tags}
|
||||||
<a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil}
|
<a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
|
||||||
{if not isLast($tag)},{sp}{/if}
|
{if not isLast($tag)},{sp}{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
{$post.text |noAutoescape}
|
{$post.text |noAutoescape}
|
||||||
</div>{\n}
|
</div>{\n}
|
||||||
<div class="relative-nav">{\n}
|
<div class="relative-nav">{\n}
|
||||||
{if $prev} <a href="{$config.domain}/posts/{$prev.slug}.{$config.pageExt}">Previous</a><br> {/if}{\n}
|
{if $prev} <a href="{$config.domain}/{$prev.url}">Previous</a><br> {/if}{\n}
|
||||||
{if $next} <a href="{$config.domain}/posts/{$next.slug}.{$config.pageExt}">Next</a><br> {/if}{\n}
|
{if $next} <a href="{$config.domain}/{$next.url}">Next</a><br> {/if}{\n}
|
||||||
</div>{\n}
|
</div>{\n}
|
||||||
{/template}
|
{/template}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<h1 class="page-header">{$index.title}</h1>
|
<h1 class="page-header">{$index.title}</h1>
|
||||||
{foreach $obj in $index.content}
|
{foreach $obj in $index.content}
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<h1><a href="{$config.domain}/posts/{$obj.slug}.{$config.pageExt}">{$obj.title}</a></h1>
|
<h1><a href="{$config.domain}/{$obj.url}">{$obj.title}</a></h1>
|
||||||
<p class="date-posted">posted on {$obj.date}</p>
|
<p class="date-posted">posted on {$obj.date}</p>
|
||||||
{$obj.text |noAutoescape}
|
{$obj.text |noAutoescape}
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<p>This blog covers
|
<p>This blog covers
|
||||||
{foreach $tag in $tags}
|
{foreach $tag in $tags}
|
||||||
<a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil}
|
<a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
|
||||||
{if not isLast($tag)},{sp}{/if}
|
{if not isLast($tag)},{sp}{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</p>
|
</p>
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<p>View content from
|
<p>View content from
|
||||||
{foreach $month in $months}
|
{foreach $month in $months}
|
||||||
<a href="{$config.domain}/date/{$month}.{$config.pageExt}">{$month}</a>{nil}
|
<a href="{$config.domain}/{$month.url}">{$month.name}</a>{nil}
|
||||||
{if not isLast($month)},{sp}{/if}
|
{if not isLast($month)},{sp}{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<p>
|
<p>
|
||||||
{if $post.tags}
|
{if $post.tags}
|
||||||
Tagged as {foreach $tag in $post.tags}
|
Tagged as {foreach $tag in $post.tags}
|
||||||
<a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil}
|
<a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
|
||||||
{if not isLast($tag)},{sp}{/if}
|
{if not isLast($tag)},{sp}{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -20,8 +20,8 @@
|
||||||
{$post.text |noAutoescape}
|
{$post.text |noAutoescape}
|
||||||
|
|
||||||
<ul class="pager">
|
<ul class="pager">
|
||||||
{if $prev}<li class="previous"><a href="{$config.domain}/posts/{$prev.slug}.{$config.pageExt}">← Previous</a></li>{/if}{\n}
|
{if $prev}<li class="previous"><a href="{$config.domain}/{$prev.url}">← Previous</a></li>{/if}{\n}
|
||||||
{if $next}<li class="next"><a href="{$config.domain}/posts/{$next.slug}.{$config.pageExt}">Next →</a></li>{/if}{\n}
|
{if $next}<li class="next"><a href="{$config.domain}/{$next.url}">Next →</a></li>{/if}{\n}
|
||||||
</ul>
|
</ul>
|
||||||
</div>{\n}
|
</div>{\n}
|
||||||
{/template}
|
{/template}
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
{foreach $post in $content.content}
|
{foreach $post in $content.content}
|
||||||
<item>
|
<item>
|
||||||
<title>{$post.title}</title>
|
<title>{$post.title}</title>
|
||||||
<link>{$config.domain}/posts/{$post.slug}.{$config.pageExt}</link>
|
<link>{$config.domain}/{$post.url}</link>
|
||||||
<pubDate>{$post.date}</pubDate>
|
<pubDate>{$post.date}</pubDate>
|
||||||
<author>{$config.author}</author>
|
<author>{$config.author}</author>
|
||||||
<guid isPermaLink="true">{$config.domain}/posts/{$post.slug}.{$config.pageExt}</guid>
|
<guid isPermaLink="true">{$config.domain}/{$post.url}</guid>
|
||||||
{foreach $tag in $post.tags}
|
{foreach $tag in $post.tags}
|
||||||
<category><![CDATA[ {$tag.name |noAutoescape} ]]></category>
|
<category><![CDATA[ {$tag.name |noAutoescape} ]]></category>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
|
|
Loading…
Add table
Reference in a new issue