diff --git a/NEWS.md b/NEWS.md index aa0b152..ac55843 100644 --- a/NEWS.md +++ b/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 ` ...`. +* **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): -* A plugin for Incremental builds, cutting runtime for generating +* **New Plugin**: Incremental builds, cutting runtime for generating 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! * 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 @@ -104,3 +141,6 @@ [hacking_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.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 +[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 diff --git a/README.md b/README.md index 3265222..72bfb4b 100644 --- a/README.md +++ b/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/). ## Features + * Git for storage * RSS and Atom feeds * 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 * Analytics via Google * 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 * Using LaTeX via [Mathjax](http://mathjax.org/) * Writing posts in ReStructured Text * 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 - * [redlinernotes](http://redlinernotes.com/blog/) - * [chip the glasses](http://chiptheglasses.com) - * [kenan-bolukbasi.log](http://kenanb.com/) - * [Nothing Really Matters](http://ironhead.xs4all.nl/) - * [A year and a smile](http://blog.sjas.de) + +See the [wiki](https://github.com/redline6561/coleslaw/wiki/Blogroll) for a list of coleslaw-powered blogs. ## 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 -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). -2. Install Lisp (we recommend SBCL) and [Quicklisp](http://quicklisp.org/). -3. ```wget -c https://raw.github.com/redline6561/coleslaw/master/examples/example.coleslawrc -O ~/.coleslawrc``` # and edit as necessary -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/ +Coleslaw should run on any conforming Common Lisp implementation but +testing is primarily done on [SBCL](http://www.sbcl.org/) and +[CCL](http://ccl.clozure.com/). -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 @@ -66,5 +99,16 @@ format: html (for raw html) or md (for markdown) 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 -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). diff --git a/TODO b/TODO deleted file mode 100644 index 95ca4af..0000000 --- a/TODO +++ /dev/null @@ -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 diff --git a/coleslaw.asd b/coleslaw.asd index 71552b4..5257163 100644 --- a/coleslaw.asd +++ b/coleslaw.asd @@ -1,7 +1,7 @@ (defsystem #:coleslaw :name "coleslaw" :description "Flexible Lisp Blogware" - :version "0.9.5" + :version "0.9.6" :license "BSD" :author "Brit Butler " :pathname "src/" @@ -31,11 +31,10 @@ (intern "COLESLAW-TESTS" :coleslaw-tests)))) (defsystem #:coleslaw-tests - :depends-on (coleslaw fiveam) + :depends-on (coleslaw stefil) :pathname "tests/" :serial t - :components ((:file "packages") - (:file "tests"))) + :components ()) (defmethod operation-done-p ((op test-op) (c (eql (find-system :coleslaw)))) diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..5814c57 --- /dev/null +++ b/docs/config.md @@ -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 diff --git a/docs/hacking.md b/docs/hacking.md index 2cffba6..6cf7cc1 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -38,21 +38,6 @@ reduced runtime to 1.36 seconds, almost cutting it in half. ## 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 **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. 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 +- `(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)` A page starts, obviously, with a file. When *coleslaw* loads your @@ -102,10 +101,10 @@ reverse-chronological index. - `(deploy dir)` -Finally, we move the staging directory to a timestamped path under the -the config's `:deploy-dir`, delete the directory pointed to by the old -'.prev' symlink, point '.curr' at '.prev', and point '.curr' at our -freshly built site. +Finally, we move the staging directory to a path under the config's +`:deploy-dir`. If the versioned plugin is enabled, it is a timestamped +path and we delete the directory pointed to by the old '.prev' symlink, +point '.curr' at '.prev', and point '.curr' at our freshly built site. ### 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 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 -generator. Thankfully, Content Types were added in 0.8 as a step -towards making *coleslaw* suitable for more use cases. Any subclass of -CONTENT that implements the *document protocol* counts as a content -type. However, only POSTs are currently included in the basic INDEXes -since there isn't yet a formal relationship to determine which content -types should be included on which indexes. Users may easily implement +Content Types were added in 0.8 as a step towards making *coleslaw* +suitable for more use cases. Any subclass of CONTENT that implements +the *document protocol* counts as a content type. However, only POSTs +are currently included in the bundled INDEXes since there isn't yet a +formal relationship to determine which content types should be +included on which indexes. It is straightforward for users to implement their own dedicated INDEX for new Content Types. ### The Document Protocol @@ -169,16 +167,14 @@ eql-specializing on the class, e.g. **Instance Methods**: -- `page-url`: Generate a relative path for the object on the site, - usually sans file extension. If there is no extension, an :around - method adds "html" later. The `slug` slot on the instance is - conventionally used to hold a portion of the path that corresponds - to a unique Primary Key or Object ID. +- `page-url`: Retrieve the relative path for the object on the site. + The implementation of `page-url` is not fully specified. For most + content types, we compute and store the path on the instance at + initialization time making `page-url` just a reader method. - `render`: A method that calls the appropriate template with `theme-fn`, passing it any needed arguments and returning rendered HTML. - **Invariants**: - 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. 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! @@ -249,28 +315,13 @@ Unfortunately, this does not solve: Content Types it includes or the CONTENT which indexes it appears 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 [api_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md [clmd]: https://github.com/gwkkwg/cl-markdown [clrz]: https://github.com/redline6561/colorize [pyg]: http://pygments.org/ [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 diff --git a/docs/plugin-api.md b/docs/plugin-api.md index f2108aa..c7979b2 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -15,14 +15,12 @@ # Extension Points -* **New functionality via JS**, for example the Disqus and Mathjax - plugins. In this case, the plugin's `enable` function should call +* **New functionality via JS**, for example the Disqus and Mathjax plugins. + In this case, the plugin's `enable` function should call [`add-injection`](http://redlinernotes.com/docs/coleslaw.html#add-injection_func) - with an injection and a keyword. The injection itself is a list of - the string to insert and a lambda or function that can be called on - a content instance to determine whether the injection should be - included on the page. The keyword specifies whether the injected - text goes in the HEAD or BODY element. The + with an injection and a keyword. The injection is a function that takes a + *Document* and returns a string to insert in the page or nil. + 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) is a good example of this. diff --git a/docs/plugin-use.md b/docs/plugin-use.md index 7164c61..1387ef5 100644 --- a/docs/plugin-use.md +++ b/docs/plugin-use.md @@ -1,8 +1,8 @@ # General Use -* Add a list with the plugin name and settings to the ```:plugins``` - section of your [.coleslawrc][config_file]. Plugin settings are - described below. +* To enable a plugin, add its name and settings to your + [.coleslawrc][config_file]. Plugin settings are described + below. Note that some plugins require additional setup. * Available plugins are listed below with usage descriptions and config examples. @@ -41,9 +41,9 @@ **Example**: `(incremental)` -**Setup**: -- You must run the `examples/dump_db.sh` script to generate a database dump - for your site before enabling the incremental plugin. +**Setup**: You must run the `examples/dump_db.sh` script to + generate a database dump for your site before enabling the + incremental plugin. ## LaTeX via Mathjax @@ -144,6 +144,23 @@ CL-USER> (chirp:complete-authentication "4173325") # ``` +## 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 **NOTE**: This plugin really should be rewritten to act as a diff --git a/docs/themes.md b/docs/themes.md index 6ab70d9..178dc93 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -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 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. 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 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. -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. ## Creating a Theme from Scratch (with code) @@ -114,10 +114,15 @@ The templating language is documented [elsewhere][clt]. However as a short primer: * 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 inside a template command. So to use a variable you have to say `{$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}`. Typical examples are: `{if $injections.body} ... {/if}` or `{if not isLast($link)} ... {/if}`. @@ -139,19 +144,16 @@ The variable that should be available to all templates is: #### Index Template Variables - **tags** A list containing all the tags, each with keys - `.name` and `.slug`. -- **months** A list of all months with posts as `yyyy-mm` strings. + `name` and `url`. +- **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 the following keys: - - `id`, the name of the page that will be rendered - `content`, a list of content (see below) - - `title`, a string title to display to the user -- **prev** If this index file is part of a chain, the `id` - of the previous index html in the chain. - If this is the first file, the value will be empty. -- **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. + - `name`, a name to use in links or href tags + - `title`, a title to use in H1 or header tags +- **prev** Nil or the previous index with keys: `url` and `title`. +- **next** Nil or the next index with keys: `url` and `title`. #### 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 **next** are the adjacent posts when put in chronological order. Each post has the following keys: - - `tags`, a list of tags (each with keys `name` and `slug`) - - `slug`, the slug of the post + - `url`, the relative url of the post + - `tags`, a list of tags (each with keys `name` and `url`) - `date`, the date of posting - `text`, the HTML of the post's body - `title`, the title of the post @@ -190,8 +192,8 @@ A simple `index.tmpl` looks like this: {namespace coleslaw.theme.trivial} {template index} {foreach $obj in $index.content} -

{$obj.title}

- {$obj.text |noAutoescape} +

{$object.title}

+ {$object.text |noAutoescape} {/foreach} {/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 between the pages so navigation is cumbersome but adding links is simple. -Good luck! - -## 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: -`{$tag.name}`. +Just do: `{$object.name}`. [clt]: https://developers.google.com/closure/templates/ [ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md diff --git a/examples/example.coleslawrc b/examples/example.coleslawrc index 73016af..25d0f3f 100644 --- a/examples/example.coleslawrc +++ b/examples/example.coleslawrc @@ -2,12 +2,14 @@ :deploy-dir "/home/git/blog/" :domain "http://blog.redlinernotes.com" :feeds ("lisp") - :plugins ((mathjax) + :plugins ((analytics :tracking-code "foo") + (disqus :shortname "my-site-name") + ; (incremental) ;; *Remove comment to enable incremental builds. + (mathjax) (sitemap) (static-pages) - (disqus :shortname "my-site-name") - (analytics :tracking-code "foo")) - :repo "/home/git/tmp/improvedmeans/" + ; (versioned) ;; *Remove comment to enable symlinked, timestamped deploys. + ) :routing ((:post "posts/~a") (:tag-index "tag/~a") (:month-index "date/~a") @@ -22,3 +24,5 @@ :staging-dir "/tmp/coleslaw/" :title "Improved Means for Achieving Deteriorated Ends" :theme "hyde") + +;; * Prerequisites described in plugin docs. diff --git a/examples/example.post-receive b/examples/example.post-receive index 32398e7..698ae72 100644 --- a/examples/example.post-receive +++ b/examples/example.post-receive @@ -1,6 +1,5 @@ ########## CONFIGURATION VALUES ########## -# TMP_GIT_CLONE _must_ match the :repo argument in your .coleslawrc! TMP_GIT_CLONE=$HOME/tmp/improvedmeans/ # Set LISP to your preferred implementation. The following @@ -26,10 +25,11 @@ while read oldrev newrev refname; do if [ $LISP = sbcl ]; then sbcl --eval "(ql:quickload 'coleslaw)" \ --eval "(coleslaw:main \"$TMP_GIT_CLONE\" \"$oldrev\")" \ - --eval "(coleslaw::exit)" + --eval "(uiop:quit)" 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 + echo -e "$LISP is not a supported lisp dialect at this time. Exiting.\n" exit 1 fi fi diff --git a/plugins/analytics.lisp b/plugins/analytics.lisp index d72e5ec..987b660 100644 --- a/plugins/analytics.lisp +++ b/plugins/analytics.lisp @@ -21,4 +21,5 @@ ") (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))) diff --git a/plugins/disqus.lisp b/plugins/disqus.lisp index 418320e..a2cab65 100644 --- a/plugins/disqus.lisp +++ b/plugins/disqus.lisp @@ -24,5 +24,7 @@ comments powered by Disqus") (defun enable (&key shortname) - (add-injection (list (format nil *disqus-header* shortname) - (lambda (x) (typep x 'post))) :body)) + (flet ((inject-p (x) + (when (typep x 'post) + (format nil *disqus-header* shortname)))) + (add-injection #'inject-p :body))) diff --git a/plugins/mathjax.lisp b/plugins/mathjax.lisp index df6bd80..31e7cc7 100644 --- a/plugins/mathjax.lisp +++ b/plugins/mathjax.lisp @@ -24,6 +24,7 @@ (defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML") (location "http://cdn.mathjax.org/mathjax/latest/MathJax.js")) - (flet ((plugin-p (x) (or force (mathjax-p x)))) - (let ((mathjax-header (format nil *mathjax-header* config location preset))) - (add-injection (list mathjax-header #'plugin-p) :head)))) + (flet ((inject-p (x) + (when (or force (mathjax-p x)) + (format nil *mathjax-header* config location preset)))) + (add-injection #'inject-p :head))) diff --git a/plugins/parallel.lisp b/plugins/parallel.lisp deleted file mode 100644 index 18e4a13..0000000 --- a/plugins/parallel.lisp +++ /dev/null @@ -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 ()) diff --git a/plugins/sitemap.lisp b/plugins/sitemap.lisp index a869c89..ea4a0ea 100644 --- a/plugins/sitemap.lisp +++ b/plugins/sitemap.lisp @@ -13,7 +13,7 @@ (in-package :coleslaw-sitemap) -(defclass sitemap (index) +(defclass sitemap () ((urls :initarg :urls :reader urls))) (defmethod page-url ((object sitemap)) "sitemap.xml") diff --git a/plugins/static-pages.lisp b/plugins/static-pages.lisp index 19fdfd1..eb38033 100644 --- a/plugins/static-pages.lisp +++ b/plugins/static-pages.lisp @@ -20,8 +20,9 @@ (defmethod initialize-instance :after ((object page) &key) ;; Expect all static-pages to be written in Markdown for now. - (with-accessors ((text content-text)) object - (setf text (render-text text :md)))) + (with-slots (url text) object + (setf url (make-pathname :defaults url) + text (render-text text :md)))) (defmethod render ((object page) &key next prev) ;; For the time being, we'll re-use the normal post theme. diff --git a/plugins/twitter-summary-card.lisp b/plugins/twitter-summary-card.lisp new file mode 100644 index 0000000..77aff6f --- /dev/null +++ b/plugins/twitter-summary-card.lisp @@ -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 " +~@[~] + +" + 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)) diff --git a/plugins/versioned.lisp b/plugins/versioned.lisp new file mode 100644 index 0000000..4818318 --- /dev/null +++ b/plugins/versioned.lisp @@ -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 ()) diff --git a/src/coleslaw.lisp b/src/coleslaw.lisp index e92616e..d3f5d98 100644 --- a/src/coleslaw.lisp +++ b/src/coleslaw.lisp @@ -3,12 +3,11 @@ (defvar *last-revision* nil "The git revision prior to the last push. For use with GET-UPDATED-FILES.") -(defun main (&optional (repo-dir "") oldrev) - "Load the user's config file, then compile and deploy the site. Optionally, -REPO-DIR is the location of the blog repo and OLDREV is the revision prior to -the last push." - (setf *last-revision* oldrev) +(defun main (repo-dir &optional oldrev) + "Load the user's config file, then compile and deploy the blog stored +in REPO-DIR. Optionally, OLDREV is the revision prior to the last push." (load-config repo-dir) + (setf *last-revision* oldrev) (load-content) (compile-theme (theme *config*)) (let ((dir (staging-dir *config*))) @@ -40,19 +39,9 @@ the last push." (update-symlink "index.html" "1.html"))) (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) - (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)))) + (run-program "rsync --delete -avz ~a ~a" staging (deploy-dir *config*)))) (defun update-symlink (path target) "Update the symlink at PATH to point to TARGET." diff --git a/src/config.lisp b/src/config.lisp index ae30c30..d73a98a 100644 --- a/src/config.lisp +++ b/src/config.lisp @@ -10,7 +10,7 @@ (license :initarg :license :reader license) (page-ext :initarg :page-ext :reader page-ext) (plugins :initarg :plugins :reader plugins) - (repo :initarg :repo :reader repo) + (repo :initarg :repo :accessor repo) (routing :initarg :routing :reader routing) (separator :initarg :separator :reader separator) (sitenav :initarg :sitenav :reader sitenav) @@ -18,6 +18,10 @@ (theme :initarg :theme :reader theme) (title :initarg :title :reader title)) (:default-initargs + :feeds nil + :license nil + :plugins nil + :sitenav nil :charset "UTF-8" :lang "en" :page-ext "html" @@ -61,10 +65,11 @@ doesn't exist, use the .coleslawrc in the home directory." repo-config (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 preferred over the home directory if provided." (with-open-file (in (discover-config-path repo-dir) :external-format '(:utf-8)) (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*))) diff --git a/src/content.lisp b/src/content.lisp index aa7cf72..0774ead 100644 --- a/src/content.lisp +++ b/src/content.lisp @@ -4,7 +4,12 @@ (defclass tag () ((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) "Takes a string and returns a TAG instance with a name and slug." @@ -31,39 +36,47 @@ ;; Content Types (defclass content () - ((file :initarg :file :reader content-file) + ((url :initarg :url :reader page-url) (date :initarg :date :reader content-date) - (tags :initarg :tags :accessor content-tags) - (slug :initarg :slug :accessor content-slug) - (text :initarg :text :accessor content-text)) - (:default-initargs :tags nil :date nil :slug nil)) + (file :initarg :file :reader content-file) + (tags :initarg :tags :reader content-tags) + (text :initarg :text :reader content-text)) + (:default-initargs :tags nil :date nil)) (defmethod initialize-instance :after ((object content) &key) - (with-accessors ((tags content-tags)) object + (with-slots (tags) object (when (stringp 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) - "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) (let ((seq (make-string (- (file-length stream) (file-position stream))))) (read-sequence seq stream) - (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)))))) + (remove #\Nul seq)))) (with-open-file (in file :external-format '(:utf-8)) - (unless (string= (read-line in) (separator *config*)) - (error "The provided file lacks the expected header.")) - (let ((meta (loop for line = (read-line in nil) - until (string= line (separator *config*)) - 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)))))) + (let ((metadata (parse-metadata in)) + (content (slurp-remainder in)) + (filepath (enough-namestring file (repo *config*)))) + (append metadata (list :text content :file filepath)))))) ;; Helper Functions diff --git a/src/documents.lisp b/src/documents.lisp index 3c398dd..0d62f48 100644 --- a/src/documents.lisp +++ b/src/documents.lisp @@ -26,24 +26,28 @@ ;; Instance Methods (defgeneric page-url (document) - (:documentation "The url to the DOCUMENT without the domain.") - (: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))) + (:documentation "The relative URL to the DOCUMENT.")) (defgeneric render (document &key &allow-other-keys) (:documentation "Render the given DOCUMENT to HTML.")) ;; 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) "Add DOCUMENT to the in-memory database. Error if a matching entry is present." (let ((url (page-url document))) diff --git a/src/feeds.lisp b/src/feeds.lisp index 8aa5e3c..4bebcb0 100644 --- a/src/feeds.lisp +++ b/src/feeds.lisp @@ -2,8 +2,9 @@ ;;; Atom and RSS Feeds -(defclass feed (index) - ((format :initform nil :initarg :format :accessor feed-format))) +(defclass base-feed () ((format :initarg :format :reader feed-format))) + +(defclass feed (index base-feed) ()) (defmethod discover ((doc-type (eql (find-class 'feed)))) (let ((content (by-date (find-all 'post)))) @@ -19,7 +20,7 @@ ;;; Tag Feeds -(defclass tag-feed (feed) ()) +(defclass tag-feed (index base-feed) ()) (defmethod discover ((doc-type (eql (find-class 'tag-feed)))) (let ((content (by-date (find-all 'post)))) diff --git a/src/indexes.lisp b/src/indexes.lisp index eaf0807..6cbf056 100644 --- a/src/indexes.lisp +++ b/src/indexes.lisp @@ -6,13 +6,18 @@ "The list of tags which content has been tagged with.") (defclass index () - ((slug :initarg :slug :reader index-slug) - (title :initarg :title :reader title-of) + ((url :initarg :url :reader page-url) + (name :initarg :name :reader index-name) + (title :initarg :title :reader title-of) (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) - (funcall (theme-fn 'index) (list :tags *all-tags* - :months *all-months* + (funcall (theme-fn 'index) (list :tags (find-all 'tag-index) + :months (find-all 'month-index) :config *config* :index object :prev prev @@ -24,12 +29,12 @@ (defmethod discover ((doc-type (eql (find-class 'tag-index)))) (let ((content (by-date (find-all 'post)))) - (dolist (tag (all-tags)) + (dolist (tag *all-tags*) (add-document (index-by-tag tag content))))) (defun index-by-tag (tag content) "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) :title (format nil "Content tagged ~a" (tag-name tag)))) @@ -48,7 +53,7 @@ (defun index-by-month (month content) "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) :title (format nil "Content from ~a" month))) @@ -68,18 +73,14 @@ (defun index-by-n (i content) "Return the index for the Ith page of CONTENT in reverse chronological order." (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) :title "Recent Content"))) (defmethod publish ((doc-type (eql (find-class 'numeric-index)))) - (let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-slug))) - (dolist (index indexes) - (let ((prev (1- (index-slug index))) - (next (1+ (index-slug index)))) - (write-document index nil - :prev (when (plusp prev) prev) - :next (when (<= next (length indexes)) next)))))) + (let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-name))) + (loop for (next index prev) on (append '(nil) indexes) + while index do (write-document index nil :prev prev :next next)))) ;;; Helper Functions diff --git a/src/packages.lisp b/src/packages.lisp index a9a3786..93eedcd 100644 --- a/src/packages.lisp +++ b/src/packages.lisp @@ -5,6 +5,7 @@ #:make-keyword #:mappend) (:import-from :cl-fad #:file-exists-p) + (:import-from :cl-ppcre #:scan-to-strings) (:import-from :closure-template #:compile-template) (:import-from :local-time #:format-rfc1123-timestring) (:import-from :uiop #:getcwd @@ -22,12 +23,13 @@ #:title-of #:author-of #:find-content-by-path - ;; Plugin API + Theming + ;; Theming + Plugin API + #:theme-fn #:plugin-conf-error #:render-text #:add-injection #:get-updated-files - #:theme-fn + #:deploy ;; The Document Protocol #:discover #:publish @@ -37,4 +39,5 @@ #:purge-all #:add-document #:delete-document - #:write-document)) + #:write-document + #:content-text)) diff --git a/src/posts.lisp b/src/posts.lisp index ebfc2c2..d8fe29d 100644 --- a/src/posts.lisp +++ b/src/posts.lisp @@ -1,20 +1,17 @@ (in-package :coleslaw) (defclass post (content) - ((title :initarg :title :reader title-of) - (author :initarg :author :accessor author-of) - (format :initarg :format :accessor post-format)) + ((title :initarg :title :reader title-of) + (author :initarg :author :reader author-of) + (format :initarg :format :reader post-format)) (:default-initargs :author nil)) (defmethod initialize-instance :after ((object post) &key) - (with-accessors ((title title-of) - (author author-of) - (format post-format) - (text content-text)) object - (setf (content-slug object) (slugify title) - format (make-keyword (string-upcase format)) - text (render-text text format) - author (or author (author *config*))))) + (with-slots (url title author format text) object + (setf url (compute-url object (slugify title)) + format (make-keyword (string-upcase format)) + text (render-text text format) + author (or author (author *config*))))) (defmethod render ((object post) &key prev next) (funcall (theme-fn 'post) (list :config *config* diff --git a/src/themes.lisp b/src/themes.lisp index 1d7947c..dce2138 100644 --- a/src/themes.lisp +++ b/src/themes.lisp @@ -4,21 +4,15 @@ "A list that stores pairs of (string . predicate) to inject in the page.") (defun add-injection (injection location) - "Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be -a string which will always be added or a (string . lambda). In the latter case, -the lambda takes a single argument, a content object, i.e. a POST or INDEX, and -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)))) + "Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be a +function that takes a DOCUMENT and returns NIL or a STRING for template insertion." + (push injection (getf *injections* location))) (defun find-injections (content) "Iterate over *INJECTIONS* collecting any that should be added to CONTENT." (flet ((injections-for (location) - (loop for (injection predicate) in (getf *injections* location) - when (funcall predicate content) - collect injection))) + (loop for injection in (getf *injections* location) + collecting (funcall injection content)))) (list :head (injections-for :head) :body (injections-for :body)))) diff --git a/src/util.lisp b/src/util.lisp index 1811bde..1862ec3 100644 --- a/src/util.lisp +++ b/src/util.lisp @@ -54,13 +54,9 @@ an UNWIND-PROTECT, then change back to the current directory." (setf (getcwd) ,old))))) (defun exit () + ;; KLUDGE: Just call UIOP for now. Don't want users updating scripts. "Exit the lisp system returning a 0 status code." - #+sbcl (sb-ext:exit) - #+ccl (ccl:quit) - #+ecl (si:quit) - #+cmucl (ext:quit) - #+clisp (ext:quit) - #-(or sbcl ccl ecl cmucl clisp) (error "Not implemented yet.")) + (uiop:quit)) (defun fmt (fmt-str args) "A convenient FORMAT interface for string building." diff --git a/tests/packages.lisp b/tests/packages.lisp deleted file mode 100644 index 6661770..0000000 --- a/tests/packages.lisp +++ /dev/null @@ -1,3 +0,0 @@ -(defpackage :coleslaw-tests - (:use :cl :fiveam) - (:export #:run!)) diff --git a/tests/plugins/twitter-summary-card.lisp b/tests/plugins/twitter-summary-card.lisp new file mode 100644 index 0000000..e5faf60 --- /dev/null +++ b/tests/plugins/twitter-summary-card.lisp @@ -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)) diff --git a/tests/tests.lisp b/tests/tests.lisp deleted file mode 100644 index 50d5071..0000000 --- a/tests/tests.lisp +++ /dev/null @@ -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)))) diff --git a/themes/atom.tmpl b/themes/atom.tmpl index 761f684..52e4ee4 100644 --- a/themes/atom.tmpl +++ b/themes/atom.tmpl @@ -14,7 +14,7 @@ {foreach $post in $content.content} - + {$post.title} {$post.date} {$post.date} diff --git a/themes/hyde/index.tmpl b/themes/hyde/index.tmpl index 9100fdf..31a4fce 100644 --- a/themes/hyde/index.tmpl +++ b/themes/hyde/index.tmpl @@ -4,20 +4,20 @@

{$index.title}

{foreach $obj in $index.content} {/foreach}
- {if $prev} Previous {/if} - {if $next} Next {/if} + {if $prev} Previous {/if} + {if $next} Next {/if}
{if $tags}

This blog covers {foreach $tag in $tags} - {$tag.name}{nil} + {$tag.name}{nil} {if not isLast($tag)},{sp}{/if} {/foreach}

@@ -26,7 +26,7 @@

View content from {foreach $month in $months} - {$month}{nil} + {$month.name}{nil} {if not isLast($month)},{sp}{/if} {/foreach}

diff --git a/themes/hyde/post.tmpl b/themes/hyde/post.tmpl index 8400fcb..f7b81bc 100644 --- a/themes/hyde/post.tmpl +++ b/themes/hyde/post.tmpl @@ -6,7 +6,7 @@
{\n} {if $post.tags} Tagged as {foreach $tag in $post.tags} - {$tag.name}{nil} + {$tag.name}{nil} {if not isLast($tag)},{sp}{/if} {/foreach} {/if} @@ -21,7 +21,7 @@ {$post.text |noAutoescape}
{\n}
{\n} - {if $prev} Previous
{/if}{\n} - {if $next} Next
{/if}{\n} + {if $prev} Previous
{/if}{\n} + {if $next} Next
{/if}{\n}
{\n} {/template} diff --git a/themes/readable/index.tmpl b/themes/readable/index.tmpl index 8abe85f..1061f93 100644 --- a/themes/readable/index.tmpl +++ b/themes/readable/index.tmpl @@ -4,7 +4,7 @@

{$index.title}

{foreach $obj in $index.content}
-

{$obj.title}

+

{$obj.title}

posted on {$obj.date}

{$obj.text |noAutoescape}
@@ -13,7 +13,7 @@

This blog covers {foreach $tag in $tags} - {$tag.name}{nil} + {$tag.name}{nil} {if not isLast($tag)},{sp}{/if} {/foreach}

@@ -23,7 +23,7 @@

View content from {foreach $month in $months} - {$month}{nil} + {$month.name}{nil} {if not isLast($month)},{sp}{/if} {/foreach}

diff --git a/themes/readable/post.tmpl b/themes/readable/post.tmpl index 15843f1..051750b 100644 --- a/themes/readable/post.tmpl +++ b/themes/readable/post.tmpl @@ -6,7 +6,7 @@

{if $post.tags} Tagged as {foreach $tag in $post.tags} - {$tag.name}{nil} + {$tag.name}{nil} {if not isLast($tag)},{sp}{/if} {/foreach} {/if} @@ -20,8 +20,8 @@ {$post.text |noAutoescape}

{\n} {/template} diff --git a/themes/rss.tmpl b/themes/rss.tmpl index a21682e..7a4ecb5 100644 --- a/themes/rss.tmpl +++ b/themes/rss.tmpl @@ -13,10 +13,10 @@ {foreach $post in $content.content} {$post.title} - {$config.domain}/posts/{$post.slug}.{$config.pageExt} + {$config.domain}/{$post.url} {$post.date} {$config.author} - {$config.domain}/posts/{$post.slug}.{$config.pageExt} + {$config.domain}/{$post.url} {foreach $tag in $post.tags} {/foreach}