소스 검색

Merge pull request #46 from redline6561/better-indexes

Release: 0.9.3!
Brit Butler 11 년 전
부모
커밋
5705e3a7dc
25개의 변경된 파일492개의 추가작업 그리고 338개의 파일을 삭제
  1. 13 0
      NEWS.md
  2. 1 1
      README.md
  3. 0 2
      TODO
  4. 3 3
      coleslaw.asd
  5. 115 54
      docs/hacking.md
  6. 6 4
      docs/themes.md
  7. 5 6
      plugins/mathjax.lisp
  8. 3 3
      plugins/sitemap.lisp
  9. 52 59
      src/coleslaw.lisp
  10. 7 7
      src/config.lisp
  11. 23 58
      src/content.lisp
  12. 56 0
      src/documents.lisp
  13. 0 27
      src/feeds.lisp
  14. 145 0
      src/indexes.lisp
  15. 0 76
      src/indices.lisp
  16. 8 6
      src/packages.lisp
  17. 2 2
      src/posts.lisp
  18. 23 0
      src/util.lisp
  19. 3 3
      themes/atom.tmpl
  20. 9 9
      themes/hyde/index.tmpl
  21. 3 3
      themes/hyde/post.tmpl
  22. 7 7
      themes/readable/index.tmpl
  23. 3 3
      themes/readable/post.tmpl
  24. 4 4
      themes/rss.tmpl
  25. 1 1
      themes/sitemap.tmpl

+ 13 - 0
NEWS.md

@@ -1,3 +1,13 @@
1
+## Changes for 0.9.3 (2013-04-16):
2
+
3
+* **INCOMPATIBLE CHANGE**: `page-path` and the `blog` config class are no longer exported.
4
+* New Docs: [A Hacker's Guide to Coleslaw](hacking_guide) and [Themes](theming_guide)!
5
+* A new theme *readable* based on bootswatch readable, courtesy of @rmoritz!
6
+* Posts may have an author to support multi-user blogs courtesy of @tychoish.
7
+* Fixes to the ReStructuredText plugin courtesy of @tychoish.
8
+* UTF-8 fixes for config files and site content courtesy of @cl-ment.
9
+* Fix timestamps in the sitemap plugin courtesy of @woudshoo.
10
+
1 11
 ## Changes for 0.9.2 (2013-05-11):
2 12
 
3 13
 * **INCOMPATIBLE CHANGE**: Renamed staging, deploy config options staging-dir, deploy-dir.
@@ -52,3 +62,6 @@
52 62
 ## Changes for 0.5 (2012-08-22):
53 63
 
54 64
 * Initial release.
65
+
66
+[hacking_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
67
+[theming_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/themes.md

+ 1 - 1
README.md

@@ -56,4 +56,4 @@ your post
56 56
 ```
57 57
 
58 58
 ## Theming
59
-A default theme, hyde, is provided. Themes are made using Google's closure-template and the source for [hyde](https://github.com/redline6561/coleslaw/tree/master/themes/hyde) should be simple and instructive until I can provide better docs.
59
+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).

+ 0 - 2
TODO

@@ -1,7 +1,5 @@
1 1
 TODO:
2 2
 Coleslaw.next
3 3
 ; See if there are any good ideas we can steal from [Frog](https://github.com/greghendershott/frog)
4
-; Add HACKING.md docs, i.e. formalize workflow+releases. No more landing broken stuff on master!
5 4
 ;; needs: shout template/render function. Twitter\Disqus integration with shouts?
6
-;; Rename index.posts to something else?
7 5
 ; Incremental compilation: only "touched" posts+tags+months and by-n. -> 1.0

+ 3 - 3
coleslaw.asd

@@ -1,7 +1,7 @@
1 1
 (defsystem #:coleslaw
2 2
   :name "coleslaw"
3 3
   :description "Flexible Lisp Blogware"
4
-  :version "0.9.2"
4
+  :version "0.9.3"
5 5
   :license "BSD"
6 6
   :author "Brit Butler <redline6561@gmail.com>"
7 7
   :pathname "src/"
@@ -19,10 +19,10 @@
19 19
                (:file "util")
20 20
                (:file "config")
21 21
                (:file "themes")
22
+               (:file "documents")
22 23
                (:file "content")
23 24
                (:file "posts")
24
-               (:file "indices")
25
-               (:file "feeds")
25
+               (:file "indexes")
26 26
                (:file "coleslaw"))
27 27
   :in-order-to ((test-op (load-op coleslaw-tests)))
28 28
   :perform (test-op :after (op c)

+ 115 - 54
docs/hacking.md

@@ -20,36 +20,84 @@ will checkout the repo to a **$TMPDIR** and call `(coleslaw:main $TMPDIR)`.
20 20
 
21 21
 It is then coleslaw's job to load all of your content, your config and
22 22
 templates, and render the content to disk. Deployment is done by
23
-updating a symlink and the default install assumes your webserver will
24
-be configured to serve from that symlink. However, there are plugins
25
-for deploying to Heroku, S3, and Github Pages.
23
+moving the files to a location specified in the config and updating a
24
+symlink.  It is assumed a web server is set up to serve from that
25
+symlink. However, there are plugins for deploying to Heroku, S3, and
26
+Github Pages.
26 27
 
27 28
 ### Blogs vs Sites
28 29
 
29 30
 **Coleslaw** is blogware. When I designed it, I only cared that it
30
-could replace my server's wordpress install. As a result, the code is
31
-still structured in terms of POSTs and INDEXes. Roughly speaking, a
32
-POST is a blog entry and an INDEX is a collection of POSTs or other
33
-content. An INDEX really only serves to group a set of content objects
34
-on a page, it isn't content itself.
31
+could replace my server's wordpress install. As a result, the code
32
+until very recently was structured in terms of POSTs and
33
+INDEXes. Roughly speaking, a POST is a blog entry and an INDEX is a
34
+collection of POSTs or other content. An INDEX really only serves to
35
+group a set of content objects on a page, it isn't content itself.
35 36
 
36 37
 This isn't ideal if you're looking for a full-on static site
37 38
 generator.  Content Types were added in 0.8 as a step towards making
38 39
 *coleslaw* suitable for more use cases but still have some
39
-limitations. Chiefly, the association between Content Types, their
40
-template, and their inclusion in an INDEX is presently ad-hoc.
40
+limitations. Any subclass of CONTENT that implements the *document
41
+protocol* counts as a content type. However, only POSTs are currently
42
+included on INDEXes since their isn't yet a formal relationship to
43
+determine what content types should be included on which indexes.
41 44
 
42
-### Current Content Types & Indices
45
+### The Document Protocol
43 46
 
44
-There are 3 INDEX subclasses at present: TAG-INDEX, DATE-INDEX, and
45
-NUMERIC-INDEX, for grouping content by tags, publishing date, and
46
-reverse chronological order, respectively. Currently, there is only 1
47
-content type: POST, for blog entries.
47
+The *document protocol* was born during a giant refactoring in 0.9.3.
48
+Any object that will be rendered to HTML should adhere to the protocol.
49
+Subclasses of CONTENT (content types) that implement the protocol will
50
+be seamlessly picked up by *coleslaw* and included on the rendered site.
51
+
52
+All current Content Types and Indexes implement the protocol faithfully.
53
+It consists of 2 "class" methods, 2 instance methods, and an invariant.
54
+
55
+
56
+* Class Methods:
57
+
58
+Since Common Lisp doesn't have explicit support for class methods, we
59
+implement them by eql-specializing on the class, e.g.
60
+```lisp
61
+(defmethod foo ((doc-type (eql (find-class 'bar))))
62
+  ... )
63
+```
64
+
65
+- `discover`: Create instances for documents of the class and put them in
66
+  in-memory database with `add-document`. If your class is a subclass of
67
+  CONTENT, there is a default method for this.
68
+- `publish`: Iterate over all objects of the class
69
+
70
+
71
+* Instance Methods:
72
+
73
+- `page-url`: Generate a unique, relative path for the object on the site
74
+  sans file extension. An :around method adds that later. The `slug` slot
75
+  on the object is generally used to hold a portion of the unique
76
+  identifier. i.e. `(format nil "posts/~a" (content-slug object))`.
77
+- `render`: A method that calls the appropriate template with `theme-fn`,
78
+  passing it any needed arguments and returning rendered HTML.
79
+
80
+
81
+* Invariants:
82
+
83
+- Any Content Types (subclasses of CONTENT) are expected to be stored in
84
+  the site's git repo with the lowercased class-name as a file extension,
85
+  i.e. (".post" for POST files).
86
+
87
+### Current Content Types & Indexes
88
+
89
+There are 5 INDEX subclasses at present: TAG-INDEX, MONTH-INDEX,
90
+NUMERIC-INDEX, FEED, and TAG-FEED. Respectively, they support
91
+grouping content by tags, publishing date, and reverse chronological
92
+order. Feeds exist to special case RSS and ATOM generation.
93
+Currently, there is only 1 content type: POST, for blog entries.
48 94
 
49 95
 I'm planning to add a content type PAGE, for static pages. It should
50 96
 be a pretty straightforward subclass of CONTENT with the necessary
51
-methods: `render`, `page-url` and `publish`, but will require a small
52
-tweak to prevent showing up in any INDEX.
97
+methods: `render`, `page-url` and `publish`. It could have a `url`
98
+slot with `page-url` as a reader to allow arbitrary layout on the site.
99
+The big question is how to handle templating and how indexes or other
100
+content should link to it.
53 101
 
54 102
 ### Templates and Theming
55 103
 
@@ -57,41 +105,42 @@ User configs are allowed to specify a theme, otherwise the default is
57 105
 used. A theme consists of a directory under "themes/" containing css,
58 106
 images, and at least 3 templates: Base, Index, and Post.
59 107
 
60
-**Coleslaw** exclusively uses
108
+**Coleslaw** uses
61 109
 [cl-closure-template](https://github.com/archimag/cl-closure-template)
62
-for templating which is a well documented CL implementation of
63
-Google's Closure Templates. Each template file should be in a
64
-namespace like `coleslaw.theme.theme-name`.
110
+exclusively for templating. **cl-closure-template** is a well
111
+documented CL implementation of Google's Closure Templates. Each
112
+template file should contain a namespace like
113
+`coleslaw.theme.theme-name`.
65 114
 
66 115
 Each template creates a lisp function in the theme's package when
67 116
 loaded. These functions take a property list (or plist) as an argument
68 117
 and return rendered HTML.  **Coleslaw** defines a helper called
69
-`theme-fn` for easy access to the template functions.
118
+`theme-fn` for easy access to the template functions. Additionally,
119
+there are RSS, ATOM, and sitemap templates *coleslaw* uses automatically.
120
+No need for individual themes to reimplement a standard, after all!
70 121
 
122
+// TODO: Update for changes to compile-blog, indexes refactor, etc.
71 123
 ### The Lifecycle of a Page
72 124
 
73 125
 - `(load-content)`
74 126
 
75
-A page starts, obviously, with a file. When
76
-*coleslaw* loads your content, it iterates over a list of content
77
-types (i.e. subclasses of CONTENT).  For each content type, it
78
-iterates over all files in the repo with a matching extension,
79
-e.g. ".post" for POSTs. Objects of the appropriate class are created
80
-from each matching file and inserted into the `*content*` hash-table.
127
+A page starts, obviously, with a file. When *coleslaw* loads your
128
+content, it iterates over a list of content types (i.e. subclasses of
129
+CONTENT).  For each content type, it iterates over all files in the
130
+repo with a matching extension, e.g. ".post" for POSTs. Objects of the
131
+appropriate class are created from each matching file and inserted
132
+into the an in-memory data store. Then the INDEXes are created by
133
+iterating over the POSTs and inserted into the data store.
81 134
 
82 135
 - `(compile-blog dir)`
83 136
 
84 137
 Compilation starts by ensuring the staging directory (`/tmp/coleslaw/`
85 138
 by default) exists, cd'ing there, and copying over any necessary theme
86
-assets. Then *coleslaw* iterates over the content types, calling the
87
-`publish` method on each one. Publish creates any non-INDEX pages for
88
-the objects of that content type by iterating over the objects in an
89
-appropriate fashion, rendering them, and passing the result to
90
-`write-page` (which should probably just be renamed to `write-file`).
91
-
92
-After this, `render-indices` and `render-feeds` are called, and an
93
-'index.html' symlink is created to point to the first reverse
94
-chronological index.
139
+assets. Then *coleslaw* iterates over the content types and index classes,
140
+calling the `publish` method on each one. Publish iterates over the
141
+class instances, rendering each one and writing the result out to disk
142
+with `write-page` (which should probably just be renamed to `write-file`).
143
+After this, an 'index.html' symlink is created to point to the first index.
95 144
 
96 145
 - `(deploy dir)`
97 146
 
@@ -102,10 +151,29 @@ freshly built site.
102 151
 
103 152
 ## Areas for Improvement
104 153
 
154
+### Render Function Cleanup
155
+
156
+There are currently 3 render-foo* functions and 3 implementations of the
157
+render method. Only the render-foo* functions call `write-page` so there
158
+should be some room for cleanup here. The render method implementations
159
+are probably necessary unless we want to start storing their arguments
160
+on the models. There may be a different way to abstract the data flow.
161
+
162
+### User-Defined Routing
163
+
164
+There is no reason *coleslaw* should be in charge of the site layout or
165
+should care. If all objects only used the *slug* slot in their `page-url`
166
+methods, there could be a :routing argument in the config containing
167
+a plist of `(:class "~{format string~}")` pairs. A default method could
168
+check the :class key under `(routing *config*)` if no specialized
169
+`page-url` was defined. This would have the additional benefit of
170
+localizing all the site routing in one place. New Content Types would
171
+probably `pushnew` a plist onto the config key in their `enable` function.
172
+
105 173
 ### Better Content Types
106 174
 
107
-Creating a new content type should be both straightforward and doable
108
-as a plugin. All that is really required is a subclass of CONTENT with
175
+Creating a new content type is both straightforward and doable as a
176
+plugin. All that is really required is a subclass of CONTENT with
109 177
 any needed slots, a template, a `render` method to call the template
110 178
 with any needed options, a `page-url` method for layout, and a
111 179
 `publish` method.
@@ -115,10 +183,12 @@ Unfortunately, this does not solve:
115 183
 1. The issue of compiling the template at load-time and making sure it
116 184
    was installed in the theme package. The plugin would need to do
117 185
    this itself or the template would need to be included in 'core'.
186
+   Thankfully, this should be easy with *cl-closure-template*.
118 187
 2. More seriously, there is no formal relationship between content
119
-   types and indices. Indices include *ALL* objects in the `*content*`
120
-   hash table. This may be undesirable and doesn't permit indices
121
-   dedicated to particular content types.
188
+   types and indexes. Consequentially, INDEXes include only POST
189
+   objects at the moment. Whether the INDEX should specify what
190
+   Content Types it includes or the CONTENT which indexes it appears
191
+   on is not yet clear.
122 192
 
123 193
 ### New Content Type: Shouts!
124 194
 
@@ -130,19 +200,10 @@ tabs or stored on twitter's servers. It would be cool to see SHOUTs as
130 200
 a plugin, probably with a dedicated SHOUT-INDEX, and some sort of
131 201
 oEmbed/embed.ly/noembed support.
132 202
 
133
-### Layouts and Paths
134
-
135
-Defining a page-url for every content-object and index seems a bit
136
-silly. It also spreads information about the site layout throughout
137
-the codebase, it might be better to have a slot in the config that
138
-defines this information with a key to go with each format string.
139
-Adding a new content-type as a plugin could then provide a default
140
-by banging on the config or specify the path in its `enable` options.
141
-
142 203
 ### Incremental Compilation
143 204
 
144 205
 Incremental compilation is doable, even straightforward if you ignore
145
-indices. It is also preferable to building the site in parallel as
206
+indexes. It is also preferable to building the site in parallel as
146 207
 avoiding work is better than using more workers. Moreover, being
147 208
 able to determine (and expose) what files just changed enables new
148 209
 functionality such as plugins that cross-post to tumblr.
@@ -158,6 +219,6 @@ things the existing deployment model would not work as it involves
158 219
 rebuilding the entire site. In all likelihood we would want to update
159 220
 the site 'in-place'. Atomicity of filesystem operations would be a
160 221
 reasonable concern. Also, every numbered INDEX would have to be
161
-regenerated along with any tag or month indices matching the
222
+regenerated along with any tag or month indexes matching the
162 223
 modified files. If incremental compilation is a goal, simply
163
-disabling the indices may be appropriate for certain users.
224
+disabling the indexes may be appropriate for certain users.

+ 6 - 4
docs/themes.md

@@ -4,9 +4,10 @@ The theming support in coleslaw is very flexible and relatively easy
4 4
 to use. However it does require some knowledge of HTML, CSS, and how
5 5
 coleslaw processes content.
6 6
 
7
-To understand how coleslaw processes a blog, a look at the [overview][ovr]
8
-documentation may prove useful. This document will focus mainly on the
9
-template engine and how you can influence the resulting HTML.
7
+To understand how coleslaw processes a blog, a look at the
8
+[overview][ovr] and [hacking][hck] documentation may prove
9
+useful. This document will focus mainly on the template engine and how
10
+you can influence the resulting HTML.
10 11
 
11 12
 **NOTE**: Themes are not able to change the generated file names or the
12 13
 generated file structure on disk. They can change the resulting HTML, nothing more.
@@ -219,7 +220,8 @@ Good luck!
219 220
 
220 221
 As mentioned earlier, most files have a file name which is a slug of
221 222
 some sort. So if you want to create a link to a tag file you should
222
-do something like this: `<a href="${config.domain}/tags/{$tag.slug}">{$tag.name}</a>`.
223
+do something like this: `<a href="${config.domain}/tags/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>`.
223 224
 
224 225
 [clt]: https://developers.google.com/closure/templates/
225 226
 [ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md
227
+[hck]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md

+ 5 - 6
plugins/mathjax.lisp

@@ -4,10 +4,8 @@
4 4
   (:import-from :coleslaw #:add-injection
5 5
                           #:content
6 6
                           #:index
7
-                          #:content-tags
8
-                          #:index-posts
9
-                          #:make-tag
10
-                          #:tag-slug=))
7
+                          #:tag-p
8
+                          #:index-content))
11 9
 
12 10
 (in-package :coleslaw-mathjax)
13 11
 
@@ -19,11 +17,12 @@
19 17
 (defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML")
20 18
                  (location "http://cdn.mathjax.org/mathjax/latest/MathJax.js"))
21 19
   (labels ((math-post-p (obj)
22
-             (member (make-tag "math") (content-tags obj) :test #'tag-slug=))
20
+             ;; Would it be better to test against latex than math, here?
21
+             (tag-p "math" obj))
23 22
            (mathjax-p (obj)
24 23
              (or force
25 24
                  (etypecase obj
26 25
                    (content (math-post-p obj))
27
-                   (index (some #'math-post-p (index-posts obj)))))))
26
+                   (index (some #'math-post-p (index-content obj)))))))
28 27
     (let ((mathjax-header (format nil *mathjax-header* config location preset)))
29 28
       (add-injection (list mathjax-header #'mathjax-p) :head))))

+ 3 - 3
plugins/sitemap.lisp

@@ -18,12 +18,12 @@
18 18
 (in-package :coleslaw-sitemap)
19 19
 
20 20
 (defmethod deploy :before (staging)
21
-  "Render sitemap.xml under document root"
21
+  "Render sitemap.xml under document root."
22 22
   (declare (ignore staging))
23 23
   (let ((urls (append '("" "sitemap.xml") ; empty string is for root url
24
-                      (mapcar #'page-url (find-all 'coleslaw:post)))))
24
+                      (mapcar #'page-url (hash-table-values coleslaw::*site*)))))
25 25
     (write-page (rel-path (staging-dir *config*) "sitemap.xml")
26
-                (funcall (theme-fn 'sitemap "feeds")
26
+                (funcall (theme-fn 'sitemap "sitemap")
27 27
                          (list :config *config*
28 28
                                :urls urls
29 29
                                :pubdate (format-timestring nil (now)))))))

+ 52 - 59
src/coleslaw.lisp

@@ -1,52 +1,20 @@
1 1
 (in-package :coleslaw)
2 2
 
3
-(defgeneric render (object &key &allow-other-keys)
4
-  (:documentation "Render the given OBJECT to HTML."))
5
-
6
-(defgeneric render-content (text format)
7
-  (:documentation "Compile TEXT from the given FORMAT to HTML for display.")
8
-  (:method (text (format (eql :html)))
9
-    text)
10
-  (:method (text (format (eql :md)))
11
-    (let ((3bmd-code-blocks:*code-blocks* t))
12
-      (with-output-to-string (str)
13
-        (3bmd:parse-string-and-print-to-stream text str)))))
14
-
15
-(defgeneric page-url (object)
16
-  (:documentation "The url to the object, without the domain."))
17
-
18
-(defmethod page-url :around ((object t))
19
-  (let ((result (call-next-method))
20
-        (extension (if (string= (page-ext *config*) "/")
21
-                       "html"
22
-                       (page-ext *config*))))
23
-    (if (pathname-type result)
24
-        result
25
-        (make-pathname :type extension :defaults result))))
26
-
27
-(defun page-path (object)
28
-  "The path to store OBJECT at once rendered."
29
-  (rel-path (staging-dir *config*) (namestring (page-url object))))
30
-
31
-(defun render-page (content &optional theme-fn &rest render-args)
32
-  "Render the given CONTENT to disk using THEME-FN if supplied.
33
-Additional args to render CONTENT can be passed via RENDER-ARGS."
34
-  (funcall (or theme-fn (theme-fn 'base))
35
-           (list :config *config*
36
-                 :content content
37
-                 :raw (apply 'render content render-args)
38
-                 :pubdate (make-pubdate)
39
-                 :injections (find-injections content))))
3
+(defun main (&optional config-key)
4
+  "Load the user's config file, then compile and deploy the site."
5
+  (load-config config-key)
6
+  (load-content)
7
+  (compile-theme (theme *config*))
8
+  (let ((dir (staging-dir *config*)))
9
+    (compile-blog dir)
10
+    (deploy dir)))
40 11
 
41
-(defun write-page (filepath page)
42
-  "Write the given PAGE to FILEPATH."
43
-  (ensure-directories-exist filepath)
44
-  (with-open-file (out filepath
45
-                   :direction :output
46
-                   :if-exists :supersede
47
-                   :if-does-not-exist :create
48
-                   :external-format '(:utf-8))
49
-    (write-line page out)))
12
+(defun load-content ()
13
+  "Load all content stored in the blog's repo."
14
+  (do-subclasses (ctype content)
15
+    (discover ctype))
16
+  (do-subclasses (itype index)
17
+    (discover itype)))
50 18
 
51 19
 (defun compile-blog (staging)
52 20
   "Compile the blog to a STAGING directory as specified in .coleslawrc."
@@ -58,10 +26,11 @@ Additional args to render CONTENT can be passed via RENDER-ARGS."
58 26
                        (merge-pathnames "static" (repo *config*))))
59 27
       (when (probe-file dir)
60 28
         (run-program "rsync --delete -raz ~a ." dir)))
61
-    (do-ctypes (publish (make-keyword ctype)))
62
-    (render-indices)
63
-    (update-symlink "index.html" "1.html")
64
-    (render-feeds (feeds *config*))))
29
+    (do-subclasses (ctype content)
30
+      (publish ctype))
31
+    (do-subclasses (itype index)
32
+      (publish itype))
33
+    (update-symlink "index.html" "1.html")))
65 34
 
66 35
 (defgeneric deploy (staging)
67 36
   (:documentation "Deploy the STAGING dir, updating the .prev and .curr symlinks.")
@@ -78,15 +47,6 @@ Additional args to render CONTENT can be passed via RENDER-ARGS."
78 47
         (update-symlink prev (truename curr)))
79 48
       (update-symlink curr new-build))))
80 49
 
81
-(defun main (&optional config-key)
82
-  "Load the user's config file, then compile and deploy the site."
83
-  (load-config config-key)
84
-  (load-content)
85
-  (compile-theme (theme *config*))
86
-  (let ((dir (staging-dir *config*)))
87
-    (compile-blog dir)
88
-    (deploy dir)))
89
-
90 50
 (defun preview (path &optional (content-type 'post))
91 51
   "Render the content at PATH under user's configured repo and save it to
92 52
 ~/tmp.html. Load the user's config and theme if necessary."
@@ -97,3 +57,36 @@ Additional args to render CONTENT can be passed via RENDER-ARGS."
97 57
     (let* ((file (rel-path (repo *config*) path))
98 58
            (content (construct content-type (read-content file))))
99 59
       (write-page "tmp.html" (render-page content)))))
60
+
61
+(defgeneric render-content (text format)
62
+  (:documentation "Compile TEXT from the given FORMAT to HTML for display.")
63
+  (:method (text (format (eql :html)))
64
+    text)
65
+  (:method (text (format (eql :md)))
66
+    (let ((3bmd-code-blocks:*code-blocks* t))
67
+      (with-output-to-string (str)
68
+        (3bmd:parse-string-and-print-to-stream text str)))))
69
+
70
+(defun page-path (object)
71
+  "The path to store OBJECT at once rendered."
72
+  (rel-path (staging-dir *config*) (namestring (page-url object))))
73
+
74
+(defun render-page (content &optional theme-fn &rest render-args)
75
+  "Render the given CONTENT to disk using THEME-FN if supplied.
76
+Additional args to render CONTENT can be passed via RENDER-ARGS."
77
+  (funcall (or theme-fn (theme-fn 'base))
78
+           (list :config *config*
79
+                 :content content
80
+                 :raw (apply 'render content render-args)
81
+                 :pubdate (make-pubdate)
82
+                 :injections (find-injections content))))
83
+
84
+(defun write-page (filepath page)
85
+  "Write the given PAGE to FILEPATH."
86
+  (ensure-directories-exist filepath)
87
+  (with-open-file (out filepath
88
+                   :direction :output
89
+                   :if-exists :supersede
90
+                   :if-does-not-exist :create
91
+                   :external-format '(:utf-8))
92
+    (write-line page out)))

+ 7 - 7
src/config.lisp

@@ -6,15 +6,15 @@
6 6
    (domain          :initarg :domain         :accessor domain)
7 7
    (feeds           :initarg :feeds          :accessor feeds)
8 8
    (license         :initarg :license        :accessor license)
9
+   (page-ext        :initarg :page-ext       :accessor page-ext       :initform "html")
9 10
    (plugins         :initarg :plugins        :accessor plugins)
10 11
    (repo            :initarg :repo           :accessor repo)
12
+   (routing         :initarg :routing        :accessor routing)
13
+   (separator       :initarg :separator      :accessor separator      :initform ";;;;;")
11 14
    (sitenav         :initarg :sitenav        :accessor sitenav)
12 15
    (staging-dir     :initarg :staging-dir    :accessor staging-dir    :initform "/tmp/coleslaw/")
13
-   (posts-dir       :initarg :posts-dir      :accessor posts-dir      :initform "posts")
14
-   (separator       :initarg :separator      :accessor separator      :initform ";;;;;")
15
-   (page-ext        :initarg :page-ext       :accessor page-ext       :initform "html")
16
-   (title           :initarg :title          :accessor title)
17
-   (theme           :initarg :theme          :accessor theme)))
16
+   (theme           :initarg :theme          :accessor theme)
17
+   (title           :initarg :title          :accessor title)))
18 18
 
19 19
 (define-condition unknown-config-section-error (error)
20 20
   ((text :initarg :text :reader text)))
@@ -55,14 +55,14 @@ if necessary. DIR is ~ by default."
55 55
     (let ((config-form (read in)))
56 56
       (if (symbolp (car config-form))
57 57
           ;; Single site config: ignore CONFIG-KEY.
58
-          (setf *config* (apply #'make-instance 'blog config-form))
58
+          (setf *config* (construct 'blog config-form))
59 59
           ;; Multi-site config: load config section for CONFIG-KEY.
60 60
           (let* ((config-key-pathname (cl-fad:pathname-as-directory config-key))
61 61
                  (section (assoc config-key-pathname config-form
62 62
                                  :key #'cl-fad:pathname-as-directory
63 63
                                  :test #'equal)))
64 64
             (if section
65
-                (setf *config* (apply #'make-instance 'blog (cdr section))
65
+                (setf *config* (construct 'blog (cdr section))
66 66
                       (repo *config*) config-key)
67 67
                 (error 'unknown-config-section-error
68 68
                        :text (format nil "In ~A: No such key: '~A'." in config-key)))))

+ 23 - 58
src/content.lisp

@@ -1,7 +1,6 @@
1 1
 (in-package :coleslaw)
2 2
 
3
-(defparameter *content* (make-hash-table :test #'equal)
4
-  "A hash table to store all the site content and metadata.")
3
+;; Tagging
5 4
 
6 5
 (defclass tag ()
7 6
   ((name :initform nil :initarg :name :accessor tag-name)
@@ -16,27 +15,27 @@
16 15
   "Test if the slugs for tag A and B are equal."
17 16
   (string= (tag-slug a) (tag-slug b)))
18 17
 
18
+;; Slugs
19
+
20
+(defun slug-char-p (char)
21
+  "Determine if CHAR is a valid slug (i.e. URL) character."
22
+  (or (char<= #\0 char #\9)
23
+      (char<= #\a char #\z)
24
+      (char<= #\A char #\Z)
25
+      (member char '(#\_ #\-))))
26
+
27
+(defun slugify (string)
28
+  "Return a version of STRING suitable for use as a URL."
29
+  (remove-if-not #'slug-char-p (substitute #\- #\Space string)))
30
+
31
+;; Content Types
32
+
19 33
 (defclass content ()
20 34
   ((tags :initform nil :initarg :tags :accessor content-tags)
21 35
    (slug :initform nil :initarg :slug :accessor content-slug)
22 36
    (date :initform nil :initarg :date :accessor content-date)
23 37
    (text :initform nil :initarg :text :accessor content-text)))
24 38
 
25
-(defun construct (content-type args)
26
-  "Create an instance of CONTENT-TYPE with the given ARGS."
27
-  (apply 'make-instance content-type args))
28
-
29
-(defun tag-p (tag obj)
30
-  "Test if OBJ is tagged with TAG."
31
-  (member tag (content-tags obj) :test #'tag-slug=))
32
-
33
-(defun month-p (month obj)
34
-  "Test if OBJ was written in MONTH."
35
-  (search month (content-date obj)))
36
-
37
-(defgeneric publish (content-type)
38
-  (:documentation "Write pages to disk for all content of the given CONTENT-TYPE."))
39
-
40 39
 (defun read-content (file)
41 40
   "Returns a plist of metadata from FILE with :text holding the content as a string."
42 41
   (flet ((slurp-remainder (stream)
@@ -61,49 +60,15 @@
61 60
         (setf (getf meta :tags) (read-tags (getf meta :tags)))
62 61
         (append meta (list :text content))))))
63 62
 
64
-(defun find-all (content-type)
65
-  "Return a list of all instances of a given CONTENT-TYPE."
66
-  (loop for val being the hash-values in *content*
67
-     when (typep val content-type) collect val))
68
-
69
-(defun purge-all (content-type)
70
-  "Remove all instances of CONTENT-TYPE from *content*."
71
-  (dolist (obj (find-all content-type))
72
-    (remhash (content-slug obj) *content*)))
73
-
74
-(defun discover (content-type)
75
-  "Load all content of the given CONTENT-TYPE from disk."
76
-  (purge-all content-type)
77
-  (let ((file-type (string-downcase (princ-to-string content-type))))
78
-    (do-files (file (repo *config*) file-type)
79
-      (let ((obj (construct content-type (read-content file))))
80
-        (if (gethash (content-slug obj) *content*)
81
-            (error "There is already existing content with the slug ~a."
82
-                   (content-slug obj))
83
-            (setf (gethash (content-slug obj) *content*) obj))))))
84
-
85
-(defmacro do-ctypes (&body body)
86
-  "Iterate over the subclasses of CONTENT performing BODY with ctype lexically
87
-bound to the current subclass."
88
-  (alexandria:with-gensyms (ctypes)
89
-    `(let ((,ctypes (closer-mop:class-direct-subclasses (find-class 'content))))
90
-       (loop for ctype in (mapcar #'class-name ,ctypes) do ,@body))))
63
+(defun tag-p (tag obj)
64
+  "Test if OBJ is tagged with TAG."
65
+  (let ((tag (if (typep tag 'tag) tag (make-tag tag))))
66
+    (member tag (content-tags obj) :test #'tag-slug=)))
91 67
 
92
-(defun load-content ()
93
-  "Load all content stored in the blog's repo."
94
-  (do-ctypes (discover ctype)))
68
+(defun month-p (month obj)
69
+  "Test if OBJ was written in MONTH."
70
+  (search month (content-date obj)))
95 71
 
96 72
 (defun by-date (content)
97 73
   "Sort CONTENT in reverse chronological order."
98 74
   (sort content #'string> :key #'content-date))
99
-
100
-(defun slug-char-p (char)
101
-  "Determine if CHAR is a valid slug (i.e. URL) character."
102
-  (or (char<= #\0 char #\9)
103
-      (char<= #\a char #\z)
104
-      (char<= #\A char #\Z)
105
-      (member char '(#\_ #\-))))
106
-
107
-(defun slugify (string)
108
-  "Return a version of STRING suitable for use as a URL."
109
-  (remove-if-not #'slug-char-p (substitute #\- #\Space string)))

+ 56 - 0
src/documents.lisp

@@ -0,0 +1,56 @@
1
+(in-package :coleslaw)
2
+
3
+;;;; The Document Protocol
4
+
5
+;; Data Storage
6
+
7
+(defvar *site* (make-hash-table :test #'equal)
8
+  "An in-memory database to hold all site documents, keyed on page-url.")
9
+
10
+(defun add-document (doc)
11
+  "Add DOC to the in-memory database. Error if a matching entry is present."
12
+  (let ((url (page-url doc)))
13
+    (if (gethash url *site*)
14
+        (error "There is already an existing document with the url ~a" url)
15
+        (setf (gethash url *site*) doc))))
16
+
17
+;; Class Methods
18
+
19
+(defun find-all (doc-type)
20
+  "Return a list of all instances of a given DOC-TYPE."
21
+  (loop for val being the hash-values in *site*
22
+     when (typep val doc-type) collect val))
23
+
24
+(defun purge-all (doc-type)
25
+  "Remove all instances of DOC-TYPE from memory."
26
+  (dolist (obj (find-all doc-type))
27
+    (remhash (page-url obj) *site*)))
28
+
29
+(defgeneric publish (doc-type)
30
+  (:documentation "Write pages to disk for all documents of the given DOC-TYPE."))
31
+
32
+(defgeneric discover (doc-type)
33
+  (:documentation "Load all documents of the given DOC-TYPE into memory.")
34
+  (:method (doc-type)
35
+    (let* ((class-name (class-name doc-type))
36
+           (file-type (string-downcase (symbol-name class-name))))
37
+      (do-files (file (repo *config*) file-type)
38
+        (let ((obj (construct class-name (read-content file))))
39
+          (add-document obj))))))
40
+
41
+(defmethod discover :before (doc-type)
42
+  (purge-all (class-name doc-type)))
43
+
44
+;; Instance Methods
45
+
46
+(defgeneric page-url (document)
47
+  (:documentation "The url to the document, without the domain."))
48
+
49
+(defmethod page-url :around ((document t))
50
+  (let ((result (call-next-method)))
51
+    (if (pathname-type result)
52
+        result
53
+        (make-pathname :type "html" :defaults result))))
54
+
55
+(defgeneric render (document &key &allow-other-keys)
56
+  (:documentation "Render the given DOCUMENT to HTML."))

+ 0 - 27
src/feeds.lisp

@@ -1,27 +0,0 @@
1
-(in-package :coleslaw)
2
-
3
-(defun make-pubdate ()
4
-  "Make a RFC1123 pubdate representing the current time."
5
-  (local-time:format-rfc1123-timestring nil (local-time:now)))
6
-
7
-(defun render-feed (posts &key path template tag)
8
-  (flet ((first-10 (list) (subseq list 0 (min (length list) 10)))
9
-         (tag-posts (list) (remove-if-not (lambda (x) (tag-p tag x)) list)))
10
-    (let ((template (theme-fn template "feeds"))
11
-          (index (if tag
12
-                     (make-instance 'tag-index :id path
13
-                                    :posts (first-10 (tag-posts posts)))
14
-                     (make-instance 'index :id path
15
-                                    :posts (first-10 posts)))))
16
-      (write-page (page-path index) (render-page index template)))))
17
-
18
-(defun render-feeds (tag-feeds)
19
-  "Render the default RSS and ATOM feeds along with any TAG-FEEDS."
20
-  (let ((posts (by-date (find-all 'post))))
21
-    (dolist (feed '((:path "rss.xml" :template :rss-feed)
22
-                    (:path "atom.xml" :template :atom-feed)))
23
-      (apply #'render-feed posts feed))
24
-    (dolist (feed tag-feeds)
25
-      (apply #'render-feed posts (list :path (format nil "~A-rss.xml" feed)
26
-                                       :tag (make-tag feed)
27
-                                       :template :rss-feed)))))

+ 145 - 0
src/indexes.lisp

@@ -0,0 +1,145 @@
1
+(in-package :coleslaw)
2
+
3
+(defclass index ()
4
+  ((slug :initform nil :initarg :slug :accessor index-slug)
5
+   (title :initform nil :initarg :title :accessor index-title)
6
+   (content :initform nil :initarg :content :accessor index-content)))
7
+
8
+(defmethod render ((object index) &key prev next)
9
+  (funcall (theme-fn 'index) (list :tags (all-tags)
10
+                                   :months (all-months)
11
+                                   :config *config*
12
+                                   :index object
13
+                                   :prev prev
14
+                                   :next next)))
15
+
16
+;;; Index by Tag
17
+
18
+(defclass tag-index (index) ())
19
+
20
+(defmethod page-url ((object tag-index))
21
+  (format nil "tag/~a" (index-slug object)))
22
+
23
+(defmethod discover ((doc-type (eql (find-class 'tag-index))))
24
+  (let ((content (by-date (find-all 'post))))
25
+    (dolist (tag (all-tags))
26
+      (add-document (index-by-tag tag content)))))
27
+
28
+(defun index-by-tag (tag content)
29
+  "Return an index of all CONTENT matching the given TAG."
30
+  (make-instance 'tag-index :slug (tag-slug tag)
31
+                 :content (remove-if-not (lambda (x) (tag-p tag x)) content)
32
+                 :title (format nil "Content tagged ~a" (tag-name tag))))
33
+
34
+(defmethod publish ((doc-type (eql (find-class 'tag-index))))
35
+  (dolist (index (find-all 'tag-index))
36
+    (render-index index)))
37
+
38
+;;; Index by Month
39
+
40
+(defclass month-index (index) ())
41
+
42
+(defmethod page-url ((object month-index))
43
+  (format nil "date/~a" (index-slug object)))
44
+
45
+(defmethod discover ((doc-type (eql (find-class 'month-index))))
46
+  (let ((content (by-date (find-all 'post))))
47
+    (dolist (month (all-months))
48
+      (add-document (index-by-month month content)))))
49
+
50
+(defun index-by-month (month content)
51
+  "Return an index of all CONTENT matching the given MONTH."
52
+  (make-instance 'month-index :slug month
53
+                 :content (remove-if-not (lambda (x) (month-p month x)) content)
54
+                 :title (format nil "Content from ~a" month)))
55
+
56
+(defmethod publish ((doc-type (eql (find-class 'month-index))))
57
+  (dolist (index (find-all 'month-index))
58
+    (render-index index)))
59
+
60
+;;; Reverse Chronological Index
61
+
62
+(defclass numeric-index (index) ())
63
+
64
+(defmethod page-url ((object numeric-index))
65
+  (format nil "~d" (index-slug object)))
66
+
67
+(defmethod discover ((doc-type (eql (find-class 'numeric-index))))
68
+  (let ((content (by-date (find-all 'post))))
69
+    (dotimes (i (ceiling (length content) 10))
70
+      (add-document (index-by-n i content)))))
71
+
72
+(defun index-by-n (i content)
73
+  "Return the index for the Ith page of CONTENT in reverse chronological order."
74
+  (let ((content (subseq content (* 10 i))))
75
+    (make-instance 'numeric-index :slug (1+ i)
76
+                   :content (take-up-to 10 content)
77
+                   :title "Recent Content")))
78
+
79
+(defmethod publish ((doc-type (eql (find-class 'numeric-index))))
80
+  (let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-slug)))
81
+    (dolist (index indexes)
82
+      (let ((prev (1- (index-slug index)))
83
+            (next (1+ (index-slug index))))
84
+        (render-index index :prev (when (plusp prev) prev)
85
+                            :next (when (<= next (length indexes)) next))))))
86
+
87
+;;; Atom and RSS Feeds
88
+
89
+(defclass feed (index)
90
+  ((format :initform nil :initarg :format :accessor feed-format)))
91
+
92
+(defmethod page-url ((object feed))
93
+  (format nil "~(~a~).xml" (feed-format object)))
94
+
95
+(defmethod discover ((doc-type (eql (find-class 'feed))))
96
+  (let ((content (take-up-to 10 (by-date (find-all 'post)))))
97
+    (dolist (format '(rss atom))
98
+      (let ((feed (make-instance 'feed :content content :format format)))
99
+        (add-document feed)))))
100
+
101
+(defmethod publish ((doc-type (eql (find-class 'feed))))
102
+  (dolist (feed (find-all 'feed))
103
+    (render-feed feed)))
104
+
105
+(defclass tag-feed (feed) ())
106
+
107
+(defmethod page-url ((object tag-feed))
108
+  (format nil "tag/~a~(~a~).xml" (index-slug object) (feed-format object)))
109
+
110
+(defmethod discover ((doc-type (eql (find-class 'tag-feed))))
111
+  (let ((content (by-date (find-all 'post))))
112
+    (dolist (tag (feeds *config*))
113
+      (let ((tagged (remove-if-not (lambda (x) (tag-p tag x)) content)))
114
+        (dolist (format '(rss atom))
115
+          (let ((feed (make-instance 'tag-feed :content (take-up-to 10 tagged)
116
+                                     :format format
117
+                                     :slug tag)))
118
+            (add-document feed)))))))
119
+
120
+(defmethod publish ((doc-type (eql (find-class 'tag-feed))))
121
+  (dolist (feed (find-all 'tag-feed))
122
+    (render-feed feed)))
123
+
124
+;;; Helper Functions
125
+
126
+(defun all-months ()
127
+  "Retrieve a list of all months with published content."
128
+  (let ((months (mapcar (lambda (x) (subseq (content-date x) 0 7))
129
+                        (find-all 'post))))
130
+    (sort (remove-duplicates months :test #'string=) #'string>)))
131
+
132
+(defun all-tags ()
133
+  "Retrieve a list of all tags used in content."
134
+  (let* ((dupes (mappend #'content-tags (find-all 'post)))
135
+         (tags (remove-duplicates dupes :test #'string= :key #'tag-slug)))
136
+    (sort tags #'string< :key #'tag-name)))
137
+
138
+(defun render-feed (feed)
139
+  "Render the given FEED to both RSS and ATOM."
140
+  (let ((theme-fn (theme-fn (feed-format feed) "feeds")))
141
+    (write-page (page-path feed) (render-page feed theme-fn))))
142
+
143
+(defun render-index (index &rest render-args)
144
+  "Render the given INDEX using RENDER-ARGS if provided."
145
+  (write-page (page-path index) (apply #'render-page index nil render-args)))

+ 0 - 76
src/indices.lisp

@@ -1,76 +0,0 @@
1
-(in-package :coleslaw)
2
-
3
-(defclass index ()
4
-  ((id :initform nil :initarg :id :accessor index-id)
5
-   (posts :initform nil :initarg :posts :accessor index-posts)
6
-   (title :initform nil :initarg :title :accessor index-title)))
7
-
8
-(defclass tag-index (index) ())
9
-(defclass date-index (index) ())
10
-(defclass numeric-index (index) ())
11
-
12
-(defmethod page-url ((object index))
13
-  (index-id object))
14
-(defmethod page-url ((object tag-index))
15
-  (format nil "tag/~a" (index-id object)))
16
-(defmethod page-url ((object date-index))
17
-  (format nil "date/~a" (index-id object)))
18
-(defmethod page-url ((object numeric-index))
19
-  (format nil "~d" (index-id object)))
20
-
21
-(defmethod render ((object index) &key prev next)
22
-  (funcall (theme-fn 'index) (list :tags (all-tags)
23
-                                   :months (all-months)
24
-                                   :config *config*
25
-                                   :index object
26
-                                   :prev prev
27
-                                   :next next)))
28
-
29
-(defun all-months ()
30
-  "Retrieve a list of all months with published content."
31
-  (let ((months (mapcar (lambda (x) (subseq (content-date x) 0 7))
32
-                        (hash-table-values *content*))))
33
-    (sort (remove-duplicates months :test #'string=) #'string>)))
34
-
35
-(defun all-tags ()
36
-  "Retrieve a list of all tags used in content."
37
-  (let* ((dupes (mappend #'content-tags (hash-table-values *content*)))
38
-         (tags (remove-duplicates dupes :test #'string= :key #'tag-slug)))
39
-    (sort tags #'string< :key #'tag-name)))
40
-
41
-(defun index-by-tag (tag content)
42
-  "Return an index of all CONTENT matching the given TAG."
43
-  (make-instance 'tag-index :id (tag-slug tag)
44
-                 :posts (remove-if-not (lambda (x) (tag-p tag x)) content)
45
-                 :title (format nil "Posts tagged ~a" (tag-name tag))))
46
-
47
-(defun index-by-month (month content)
48
-  "Return an index of all CONTENT matching the given MONTH."
49
-  (make-instance 'date-index :id month
50
-                 :posts (remove-if-not (lambda (x) (month-p month x)) content)
51
-                 :title (format nil "Posts from ~a" month)))
52
-
53
-(defun index-by-n (i content &optional (step 10))
54
-  "Return the index for the Ith page of CONTENT in reverse chronological order."
55
-  (let* ((start (* step i))
56
-         (end (min (length content) (+ start step))))
57
-    (make-instance 'numeric-index :id (1+ i)
58
-                              :posts (subseq content start end)
59
-                              :title "Recent Posts")))
60
-
61
-(defun render-index (index &rest render-args)
62
-  "Render the given INDEX using RENDER-ARGS if provided."
63
-  (write-page (page-path index) (apply #'render-page index nil render-args)))
64
-
65
-(defun render-indices ()
66
-  "Render the indices to view content in groups of size N, by month, and by tag."
67
-  (let ((results (by-date (find-all 'post))))
68
-    (dolist (tag (all-tags))
69
-      (render-index (index-by-tag tag results)))
70
-    (dolist (month (all-months))
71
-      (render-index (index-by-month month results)))
72
-    (dotimes (i (ceiling (length results) 10))
73
-      (render-index (index-by-n i results)
74
-                    :prev (and (plusp i) i)
75
-                    :next (and (< (* (1+ i) 10) (length results))
76
-                               (+ 2 i))))))

+ 8 - 6
src/packages.lisp

@@ -10,14 +10,16 @@
10 10
   (:export #:main
11 11
            #:preview
12 12
            #:*config*
13
-           #:blog
14 13
            #:content
15 14
            #:post
16 15
            #:index
17
-           #:page-path
16
+           #:render-content
17
+           #:add-injection
18
+           ;; The Document Protocol
19
+           #:add-document
20
+           #:find-all
21
+           #:purge-all
18 22
            #:discover
19 23
            #:publish
20
-           #:render
21
-           #:render-content
22
-           #:read-content
23
-           #:add-injection))
24
+           #:page-url
25
+           #:render))

+ 2 - 2
src/posts.lisp

@@ -22,9 +22,9 @@
22 22
                                   :next next)))
23 23
 
24 24
 (defmethod page-url ((object post))
25
-  (format nil "~a/~a" (posts-dir *config*) (content-slug object)))
25
+  (format nil "posts/~a" (content-slug object)))
26 26
 
27
-(defmethod publish ((content-type (eql :post)))
27
+(defmethod publish ((doc-type (eql (find-class 'post))))
28 28
   (loop for (next post prev) on (append '(nil) (by-date (find-all 'post)))
29 29
      while post do (write-page (page-path post)
30 30
                                (render-page post nil :prev prev :next next))))

+ 23 - 0
src/util.lisp

@@ -1,5 +1,20 @@
1 1
 (in-package :coleslaw)
2 2
 
3
+(defun construct (class-name args)
4
+  "Create an instance of CLASS-NAME with the given ARGS."
5
+  (apply 'make-instance class-name args))
6
+
7
+(defmacro do-subclasses ((var class) &body body)
8
+  "Iterate over the subclasses of CLASS performing BODY with VAR
9
+lexically bound to the current subclass' class-name."
10
+  (alexandria:with-gensyms (klasses all-subclasses)
11
+    `(labels ((,all-subclasses (class)
12
+                (let ((subclasses (closer-mop:class-direct-subclasses class)))
13
+                  (append subclasses (loop for subclass in subclasses
14
+                                        nconc (,all-subclasses subclass))))))
15
+       (let ((,klasses (,all-subclasses (find-class ',class))))
16
+         (loop for ,var in ,klasses do ,@body)))))
17
+
3 18
 (defun fmt (fmt-str args)
4 19
   "A convenient FORMAT interface for string building."
5 20
   (apply 'format nil fmt-str args))
@@ -69,3 +84,11 @@ an UNWIND-PROTECT, then change back to the current directory."
69 84
                          (setf (current-directory) ,path)
70 85
                          ,@body)
71 86
          (setf (current-directory) ,old)))))
87
+
88
+(defun take-up-to (n seq)
89
+  "Take elements from SEQ until all elements or N have been taken."
90
+  (subseq seq 0 (min (length seq) n)))
91
+
92
+(defun make-pubdate ()
93
+  "Make a RFC1123 pubdate representing the current time."
94
+  (local-time:format-rfc1123-timestring nil (local-time:now)))

+ 3 - 3
themes/atom.tmpl

@@ -1,6 +1,6 @@
1 1
 {namespace coleslaw.theme.feeds}
2 2
 
3
-{template atom-feed}
3
+{template atom}
4 4
 <?xml version="1.0"?>{\n}
5 5
 <feed xmlns="http://www.w3.org/2005/Atom">
6 6
 
@@ -12,9 +12,9 @@
12 12
     <name>{$config.author}</name>
13 13
   </author>
14 14
 
15
-  {foreach $post in $content.posts}
15
+  {foreach $post in $content.content}
16 16
   <entry>
17
-    <link type="text/html" rel="alternate" href="{$config.domain}/posts/{$post.slug}.html"/>
17
+    <link type="text/html" rel="alternate" href="{$config.domain}/posts/{$post.slug}.{$config.pageExt}"/>
18 18
     <title>{$post.title}</title>
19 19
     <published>{$post.date}</published>
20 20
     <updated>{$post.date}</updated>

+ 9 - 9
themes/hyde/index.tmpl

@@ -2,31 +2,31 @@
2 2
 
3 3
 {template index}
4 4
 <h1 class="title">{$index.title}</h1>
5
-{foreach $post in $index.posts}
5
+{foreach $obj in $index.content}
6 6
   <div class="article-meta">
7
-    <a class="article-title" href="{$config.domain}/posts/{$post.slug}.html">{$post.title}</a>
8
-    <div class="date"> posted on {$post.date}</div>
9
-    <div class="article">{$post.text |noAutoescape}</div>
7
+    <a class="article-title" href="{$config.domain}/posts/{$obj.slug}.{$config.pageExt}">{$obj.title}</a>
8
+    <div class="date"> posted on {$obj.date}</div>
9
+    <div class="article">{$obj.text |noAutoescape}</div>
10 10
   </div>
11 11
 {/foreach}
12 12
 <div id="relative-nav">
13
-  {if $prev} <a href="{$prev}.html">Previous</a> {/if}
14
-  {if $next} <a href="{$next}.html">Next</a> {/if}
13
+  {if $prev} <a href="{$prev}.{$config.pageExt}">Previous</a> {/if}
14
+  {if $next} <a href="{$next}.{$config.pageExt}">Next</a> {/if}
15 15
 </div>
16 16
 {if $tags}
17 17
 <div id="tagsoup">
18 18
   <p>This blog covers
19 19
     {foreach $tag in $tags}
20
-      <a href="{$config.domain}/tag/{$tag.slug}.html">{$tag.name}</a>{nil}
20
+      <a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil}
21 21
       {if not isLast($tag)},{sp}{/if}
22 22
     {/foreach}
23 23
 </div>
24 24
 {/if}
25 25
 {if $months}
26 26
 <div id="monthsoup">
27
-  <p>View posts from
27
+  <p>View content from
28 28
     {foreach $month in $months}
29
-      <a href="{$config.domain}/date/{$month}.html">{$month}</a>{nil}
29
+      <a href="{$config.domain}/date/{$month}.{$config.pageExt}">{$month}</a>{nil}
30 30
       {if not isLast($month)},{sp}{/if}
31 31
     {/foreach}
32 32
 </div>

+ 3 - 3
themes/hyde/post.tmpl

@@ -5,7 +5,7 @@
5 5
   <h1 class="title">{$post.title}</h1>{\n}
6 6
   <div class="tags">{\n}
7 7
     Tagged as {foreach $tag in $post.tags}
8
-                <a href="../tag/{$tag.slug}.html">{$tag.name}</a>{nil}
8
+                <a href="../tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil}
9 9
                     {if not isLast($tag)},{sp}{/if}
10 10
               {/foreach}
11 11
   </div>{\n}
@@ -17,7 +17,7 @@
17 17
   {$post.text |noAutoescape}
18 18
 </div>{\n}
19 19
 <div class="relative-nav">{\n}
20
-  {if $prev} <a href="{$config.domain}/posts/{$prev.slug}.html">Previous</a><br> {/if}{\n}
21
-  {if $next} <a href="{$config.domain}/posts/{$next.slug}.html">Next</a><br> {/if}{\n}
20
+  {if $prev} <a href="{$config.domain}/posts/{$prev.slug}.{$config.pageExt}">Previous</a><br> {/if}{\n}
21
+  {if $next} <a href="{$config.domain}/posts/{$next.slug}.{$config.pageExt}">Next</a><br> {/if}{\n}
22 22
 </div>{\n}
23 23
 {/template}

+ 7 - 7
themes/readable/index.tmpl

@@ -2,18 +2,18 @@
2 2
 
3 3
 {template index}
4 4
 <h1 class="page-header">{$index.title}</h1>
5
-{foreach $post in $index.posts}
5
+{foreach $obj in $index.content}
6 6
   <div class="row-fluid">
7
-    <h1><a href="{$config.domain}/posts/{$post.slug}.html">{$post.title}</a></h1>
8
-    <p class="date-posted">posted on {$post.date}</p>
9
-    {$post.text |noAutoescape}
7
+    <h1><a href="{$config.domain}/posts/{$obj.slug}.{$config.pageExt}">{$obj.title}</a></h1>
8
+    <p class="date-posted">posted on {$obj.date}</p>
9
+    {$obj.text |noAutoescape}
10 10
   </div>
11 11
 {/foreach}
12 12
 {if $tags}
13 13
 <div class="row-fluid">
14 14
   <p>This blog covers
15 15
     {foreach $tag in $tags}
16
-      <a href="{$config.domain}/tag/{$tag.slug}.html">{$tag.name}</a>{nil}
16
+      <a href="{$config.domain}/tag/{$tag.slug}.{$config.pageExt}">{$tag.name}</a>{nil}
17 17
       {if not isLast($tag)},{sp}{/if}
18 18
     {/foreach}
19 19
   </p>
@@ -21,9 +21,9 @@
21 21
 {/if}
22 22
 {if $months}
23 23
 <div class="row-fluid">
24
-  <p>View posts from
24
+  <p>View content from
25 25
     {foreach $month in $months}
26
-      <a href="{$config.domain}/date/{$month}.html">{$month}</a>{nil}
26
+      <a href="{$config.domain}/date/{$month}.{$config.pageExt}">{$month}</a>{nil}
27 27
       {if not isLast($month)},{sp}{/if}
28 28
     {/foreach}
29 29
   </p>

+ 3 - 3
themes/readable/post.tmpl

@@ -5,7 +5,7 @@
5 5
   <h1 class="page-header">{$post.title}</h1>{\n}
6 6
   <p>Tagged as 
7 7
     {foreach $tag in $post.tags}
8
-      <a href="../tag/{$tag.slug}.html">{$tag.name}</a>{nil}
8
+      <a href="../tag/{$tag.slug}{$config.pageExt}">{$tag.name}</a>{nil}
9 9
       {if not isLast($tag)},{sp}{/if}
10 10
     {/foreach}
11 11
   </p>
@@ -14,8 +14,8 @@
14 14
   {$post.text |noAutoescape}
15 15
   
16 16
   <ul class="pager">
17
-    {if $prev}<li class="previous"><a href="{$config.domain}/posts/{$prev.slug}.html">&larr; Previous</a></li>{/if}{\n}
18
-    {if $next}<li class="next"><a href="{$config.domain}/posts/{$next.slug}.html">Next &rarr;</a></li>{/if}{\n}
17
+    {if $prev}<li class="previous"><a href="{$config.domain}/posts/{$prev.slug}.{$config.pageExt}">&larr; Previous</a></li>{/if}{\n}
18
+    {if $next}<li class="next"><a href="{$config.domain}/posts/{$next.slug}.{$config.pageExt}">Next &rarr;</a></li>{/if}{\n}
19 19
   </ul>
20 20
 </div>{\n}
21 21
 {/template}

+ 4 - 4
themes/rss.tmpl

@@ -1,6 +1,6 @@
1 1
 {namespace coleslaw.theme.feeds}
2 2
 
3
-{template rss-feed}
3
+{template rss}
4 4
 <?xml version="1.0"?>{\n}
5 5
 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
6 6
   <channel>
@@ -10,13 +10,13 @@
10 10
     <language>en-us</language>
11 11
     <pubDate>{$pubdate}</pubDate>
12 12
 
13
-    {foreach $post in $content.posts}
13
+    {foreach $post in $content.content}
14 14
     <item>
15 15
       <title>{$post.title}</title>
16
-      <link>{$config.domain}/posts/{$post.slug}.html</link>
16
+      <link>{$config.domain}/posts/{$post.slug}.{$config.pageExt}</link>
17 17
       <pubDate>{$post.date}</pubDate>
18 18
       <author>{$config.author}</author>
19
-      <guid isPermaLink="true">{$config.domain}/posts/{$post.slug}.html</guid>
19
+      <guid isPermaLink="true">{$config.domain}/posts/{$post.slug}.{$config.pageExt}</guid>
20 20
       {foreach $tag in $post.tags}
21 21
         <category><![CDATA[ {$tag} ]]></category>
22 22
       {/foreach}

+ 1 - 1
themes/sitemap.tmpl

@@ -1,4 +1,4 @@
1
-{namespace coleslaw.theme.feeds}
1
+{namespace coleslaw.theme.sitemap}
2 2
 
3 3
 {template sitemap}
4 4
 <?xml version="1.0"?>{\n}