Pārlūkot izejas kodu

Merge pull request #8 from redline6561/content-types

Extensible Content types.
Brit Butler 12 gadi atpakaļ
vecāks
revīzija
ba4e4d9496
15 mainītis faili ar 256 papildinājumiem un 130 dzēšanām
  1. 30 0
      NEWS.md
  2. 8 2
      TODO
  3. 4 2
      coleslaw.asd
  4. 58 15
      docs/coleslaw.html
  5. 8 8
      plugins/mathjax.lisp
  6. 2 2
      src/coleslaw.lisp
  7. 84 0
      src/content.lisp
  8. 1 1
      src/feeds.lisp
  9. 26 30
      src/indices.lisp
  10. 8 3
      src/packages.lisp
  11. 23 63
      src/posts.lisp
  12. 1 1
      themes/atom.tmpl
  13. 1 1
      themes/hyde/index.tmpl
  14. 1 1
      themes/hyde/post.tmpl
  15. 1 1
      themes/rss.tmpl

+ 30 - 0
NEWS.md

@@ -0,0 +1,30 @@
1
+## Changes for 0.8 (2013-01-06):
2
+
3
+* Add support for new [content types](http://blog.redlinernotes.com/posts/Lessons-from-Coleslaw.html).
4
+* Support for [Multi-site Publishing](http://blub.co.za/posts/Adding-multi-site-support-to-Coleslaw.html).
5
+* CCL and Atom feed bugfixes.
6
+* Major code refactor and docs update.
7
+
8
+## Changes for 0.7 (2012-09-20):
9
+
10
+* Add commenting support via Disqus plugin.
11
+* Add formal plugin API with per-page predicate support. (aka "injections")
12
+* Note jsmpereira's [coleslaw heroku package](https://github.com/jsmpereira/coleslaw-heroku) in README.
13
+* Support for RSS feeds of arbitrary tags, e.g. "lisp" posts.
14
+
15
+## Changes for 0.6.5 (2012-09-12):
16
+
17
+* Add support for ATOM feeds.
18
+* Add support for a sitenav in coleslawrc configs.
19
+* Template and rendering cleanup.
20
+* Miscellaneous deployment improvements.
21
+
22
+## Changes for 0.6 (2012-08-29):
23
+
24
+* Support Markdown in core rather than as a plugin.
25
+* Improve documentation + README.
26
+* Copious bugfixes and code cleanups.
27
+
28
+## Changes for 0.5 (2012-08-22):
29
+
30
+* Initial release.

+ 8 - 2
TODO

@@ -4,8 +4,14 @@ BUGS:
4 4
 ; Slugs aren't unicode safe. See [reddit discussion](http://www.reddit.com/r/lisp/comments/yvh6g/coleslaw_jekylllike_static_blogware_in_500_lines/) and [mozilla code](https://github.com/mozilla/unicode-slugify/blob/master/slugify/__init__.py).
5 5
 
6 6
 TODO:
7
-; remove need for ordering in header. improve date check/error reporting. -> 0.8
8
-; doc themes and plugins, s3+hunchentoot. -> 0.8
7
+0.9
8
+; Add SHOUT content type.
9
+;; needs: shout template/render function. Twitter\Disqus integration with shouts?
10
+;; Indices fundamentally don't know about content-types. Is that a problem?
11
+;; Rename index.posts to something else?
12
+Coleslaw.next
13
+; improve date check/error reporting. -> 0.9
14
+; doc themes and plugins, s3+hunchentoot. -> 0.9
9 15
 ; unit tests -> 0.9
10 16
 ; Incremental compilation: only "touched" posts+tags+months and by-n. -> 1.0
11 17
 ;; possible plugins: analytics, logging/monitoring, crossposting

+ 4 - 2
coleslaw.asd

@@ -1,7 +1,7 @@
1 1
 (defsystem #:coleslaw
2 2
   :name "coleslaw-core"
3 3
   :description "Flexible Lisp Blogware"
4
-  :version "0.7"
4
+  :version "0.8"
5 5
   :license "BSD"
6 6
   :author "Brit Butler <redline6561@gmail.com>"
7 7
   :pathname "src/"
@@ -12,12 +12,14 @@
12 12
                :local-time
13 13
                :inferior-shell
14 14
                :cl-fad
15
-               :cl-ppcre)
15
+               :cl-ppcre
16
+               :closer-mop)
16 17
   :serial t
17 18
   :components ((:file "packages")
18 19
                (:file "util")
19 20
                (:file "config")
20 21
                (:file "themes")
22
+               (:file "content")
21 23
                (:file "posts")
22 24
                (:file "indices")
23 25
                (:file "feeds")

+ 58 - 15
docs/coleslaw.html

@@ -58,43 +58,86 @@ else
58 58
 <pre>Homepage: <a href="http://github.com/redline6561/coleslaw">Github</a></pre></div>
59 59
 <div class="frame">
60 60
 <div class="labeltitle">
61
+<span class="expander" onclick="expand(this, 'classes');">-</span>Classes</div>
62
+<div id="classes">
63
+<div class="symboldecl">
64
+<div class="definition">
65
+<a class="symbolname" name="blog_class" href="#blog_class">blog</a>
66
+<span class="lambdalist">(standard-object)</span>
67
+<span class="symboltype">class</span></div>
68
+<div class="documentation">
69
+<pre></pre></div></div>
70
+<div class="symboldecl">
71
+<div class="definition">
72
+<a class="symbolname" name="content_class" href="#content_class">content</a>
73
+<span class="lambdalist">(standard-object)</span>
74
+<span class="symboltype">class</span></div>
75
+<div class="documentation">
76
+<pre></pre></div></div>
77
+<div class="symboldecl">
78
+<div class="definition">
79
+<a class="symbolname" name="index_class" href="#index_class">index</a>
80
+<span class="lambdalist">(standard-object)</span>
81
+<span class="symboltype">class</span></div>
82
+<div class="documentation">
83
+<pre></pre></div></div>
84
+<div class="symboldecl">
85
+<div class="definition">
86
+<a class="symbolname" name="post_class" href="#post_class">post</a>
87
+<span class="lambdalist">(content)</span>
88
+<span class="symboltype">class</span></div>
89
+<div class="documentation">
90
+<pre></pre></div></div></div></div>
91
+<div class="frame">
92
+<div class="labeltitle">
61 93
 <span class="expander" onclick="expand(this, 'functions');">-</span>Functions</div>
62 94
 <div id="functions">
63 95
 <div class="symboldecl">
64 96
 <div class="definition">
65 97
 <a class="symbolname" name="add-injection_func" href="#add-injection_func">add-injection</a>
66
-<span class="lambdalist">str location</span>
67
-<span class="symboltype">standard-generic-function</span></div>
98
+<span class="lambdalist">injection location</span>
99
+<span class="symboltype">function</span></div>
68 100
 <div class="documentation">
69
-<pre>Add STR to the list of elements injected in LOCATION.</pre></div></div>
101
+<pre>Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be
102
+a string which will always be added or a (string . lambda). In the latter case,
103
+the lambda takes a single argument, a content object, i.e. a POST or INDEX, and
104
+any return value other than nil indicates the injection should be added.</pre></div></div>
70 105
 <div class="symboldecl">
71 106
 <div class="definition">
72
-<a class="symbolname" name="deploy_func" href="#deploy_func">deploy</a>
73
-<span class="lambdalist">staging</span>
107
+<a class="symbolname" name="discover_func" href="#discover_func">discover</a>
108
+<span class="lambdalist">content-type</span>
74 109
 <span class="symboltype">standard-generic-function</span></div>
75 110
 <div class="documentation">
76
-<pre>Deploy the STAGING dir, updating the .prev and .curr symlinks.</pre></div></div>
111
+<pre>Load all content of the given CONTENT-TYPE from disk.</pre></div></div>
112
+<div class="symboldecl">
113
+<div class="definition">
114
+<a class="symbolname" name="main_func" href="#main_func">main</a>
115
+<span class="lambdalist">config-key</span>
116
+<span class="symboltype">function</span></div>
117
+<div class="documentation">
118
+<pre>Load the user's config section corresponding to CONFIG-KEY, then
119
+compile and deploy the blog.</pre></div></div>
77 120
 <div class="symboldecl">
78 121
 <div class="definition">
79
-<a class="symbolname" name="(setf deploy)_func" href="#(setf deploy)_func">(setf deploy)</a>
80
-<span class="lambdalist">new-value object</span>
122
+<a class="symbolname" name="page-path_func" href="#page-path_func">page-path</a>
123
+<span class="lambdalist">object</span>
81 124
 <span class="symboltype">standard-generic-function</span></div>
82 125
 <div class="documentation">
83
-<pre>:undocumented</pre></div></div>
126
+<pre>The path to store OBJECT at once rendered.</pre></div></div>
84 127
 <div class="symboldecl">
85 128
 <div class="definition">
86
-<a class="symbolname" name="main_func" href="#main_func">main</a>
87
-<span class="lambdalist"></span>
88
-<span class="symboltype">function</span></div>
129
+<a class="symbolname" name="publish_func" href="#publish_func">publish</a>
130
+<span class="lambdalist">content-type</span>
131
+<span class="symboltype">standard-generic-function</span></div>
89 132
 <div class="documentation">
90
-<pre>Load the user's config, then compile and deploy the blog.</pre></div></div>
133
+<pre>Write pages to disk for all content of the given CONTENT-TYPE.</pre></div></div>
91 134
 <div class="symboldecl">
92 135
 <div class="definition">
93 136
 <a class="symbolname" name="render_func" href="#render_func">render</a>
94
-<span class="lambdalist">content &key next prev &allow-other-keys</span>
137
+<span class="lambdalist">object &key next prev &allow-other-keys</span>
95 138
 <span class="symboltype">standard-generic-function</span></div>
96 139
 <div class="documentation">
97
-<pre>Render the given CONTENT to HTML.</pre></div></div>
140
+<pre>Render the given OBJECT to HTML.</pre></div></div>
98 141
 <div class="symboldecl">
99 142
 <div class="definition">
100 143
 <a class="symbolname" name="render-content_func" href="#render-content_func">render-content</a>

+ 8 - 8
plugins/mathjax.lisp

@@ -2,9 +2,9 @@
2 2
   (:use :cl)
3 3
   (:export #:enable)
4 4
   (:import-from :coleslaw #:add-injection
5
-                          #:post
5
+                          #:content
6 6
                           #:index
7
-                          #:post-tags
7
+                          #:content-tags
8 8
                           #:index-posts))
9 9
 
10 10
 (in-package :coleslaw-mathjax)
@@ -21,10 +21,10 @@ src=\"http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLor
21 21
 </script>")
22 22
 
23 23
 (defun enable ()
24
-  (labels ((math-post-p (post)
25
-             (member "math" (post-tags post) :test #'string=))
26
-           (mathjax-p (content)
27
-             (etypecase content
28
-               (post (math-post-p content))
29
-               (index (some #'math-post-p (index-posts content))))))
24
+  (labels ((math-post-p (obj)
25
+             (member "math" (content-tags obj) :test #'string=))
26
+           (mathjax-p (obj)
27
+             (etypecase obj
28
+               (content (math-post-p obj))
29
+               (index (some #'math-post-p (index-posts obj))))))
30 30
     (add-injection (list *mathjax-header* #'mathjax-p) :head)))

+ 2 - 2
src/coleslaw.lisp

@@ -49,7 +49,7 @@ Additional args to render CONTENT can be passed via RENDER-ARGS."
49 49
                        (merge-pathnames "static" (repo *config*))))
50 50
       (when (probe-file dir)
51 51
         (run-program "cp -R ~a ." dir)))
52
-    (render-posts)
52
+    (do-ctypes (publish ctype))
53 53
     (render-indices)
54 54
     (render-feeds (feeds *config*))))
55 55
 
@@ -76,7 +76,7 @@ Additional args to render CONTENT can be passed via RENDER-ARGS."
76 76
   "Load the user's config section corresponding to CONFIG-KEY, then
77 77
 compile and deploy the blog."
78 78
   (load-config config-key)
79
-  (load-posts)
79
+  (load-content)
80 80
   (compile-theme (theme *config*))
81 81
   (compile-blog (staging *config*))
82 82
   (deploy (staging *config*)))

+ 84 - 0
src/content.lisp

@@ -0,0 +1,84 @@
1
+(in-package :coleslaw)
2
+
3
+(defparameter *content* (make-hash-table :test #'equal)
4
+  "A hash table to store all the site content and metadata.")
5
+
6
+(defclass content ()
7
+  ((tags :initform nil :initarg :tags :accessor content-tags)
8
+   (slug :initform nil :initarg :slug :accessor content-slug)
9
+   (date :initform nil :initarg :date :accessor content-date)
10
+   (text :initform nil :initarg :text :accessor content-text)))
11
+
12
+(defun construct (content-type args)
13
+  "Create an instance of CONTENT-TYPE with the given ARGS."
14
+  (apply 'make-instance content-type args))
15
+
16
+(defgeneric discover (content-type)
17
+  (:documentation "Load all content of the given CONTENT-TYPE from disk."))
18
+
19
+(defgeneric publish (content-type)
20
+  (:documentation "Write pages to disk for all content of the given CONTENT-TYPE."))
21
+
22
+(defun read-content (file &optional plist-p)
23
+  "Returns two values, a list of metadata from FILE, and the content as a string.
24
+If PLIST-P is non-nil, a single plist is returned with :content holding the text."
25
+  (flet ((slurp-remainder (stream)
26
+           (let ((seq (make-string (- (file-length stream)
27
+                                      (file-position stream)))))
28
+             (read-sequence seq stream)
29
+             (remove #\Nul seq)))
30
+         (parse-field (str)
31
+           (nth-value 1 (cl-ppcre:scan-to-strings "[a-zA-Z]+: (.*)" str)))
32
+         (field-name (line)
33
+           (make-keyword (string-upcase (subseq line 0 (position #\: line)))))
34
+         (read-delimited (str &optional (delimiter ", "))
35
+           (mapcar #'string-downcase (cl-ppcre:split delimiter str))))
36
+    (with-open-file (in file)
37
+      (unless (string= (read-line in) ";;;;;")
38
+        (error "The provided file lacks the expected header."))
39
+      (let ((meta (loop for line = (read-line in nil)
40
+                     until (string= line ";;;;;")
41
+                     appending (list (field-name line)
42
+                                     (aref (parse-field line) 0))))
43
+            (content (slurp-remainder in)))
44
+        (setf (getf meta :tags) (read-delimited (getf meta :tags)))
45
+        (if plist-p
46
+            (append meta (list :text content))
47
+            (values meta content))))))
48
+
49
+(defun find-all (content-type)
50
+  "Return a list of all instances of a given CONTENT-TYPE."
51
+  (loop for val being the hash-values in *content*
52
+     when (eql content-type (type-of val)) collect val))
53
+
54
+(defun purge-all (content-type)
55
+  "Remove all instances of CONTENT-TYPE from *content*."
56
+  (dolist (obj (find-all content-type))
57
+    (remhash (content-slug obj) *content*)))
58
+
59
+(defmacro do-ctypes (&body body)
60
+  "Iterate over the subclasses of CONTENT performing BODY with ctype lexically
61
+bound to the current subclass."
62
+  (alexandria:with-gensyms (ctypes)
63
+    `(let ((,ctypes (closer-mop:class-direct-subclasses (find-class 'content))))
64
+       (loop for ctype in (mapcar (compose 'make-keyword 'class-name) ,ctypes)
65
+          do ,@body))))
66
+
67
+(defun load-content ()
68
+  "Load all content stored in the blog's repo."
69
+  (do-ctypes (discover ctype)))
70
+
71
+(defun by-date (content)
72
+  "Sort CONTENT in reverse chronological order."
73
+  (sort content #'string> :key #'content-date))
74
+
75
+(defun slug-char-p (char)
76
+  "Determine if CHAR is a valid slug (i.e. URL) character."
77
+  (or (char<= #\0 char #\9)
78
+      (char<= #\a char #\z)
79
+      (char<= #\A char #\Z)
80
+      (member char '(#\_ #\- #\.))))
81
+
82
+(defun slugify (string)
83
+  "Return a version of STRING suitable for use as a URL."
84
+  (remove-if-not #'slug-char-p (substitute #\- #\Space string)))

+ 1 - 1
src/feeds.lisp

@@ -11,7 +11,7 @@
11 11
   "Render and write the given FEEDS for the site."
12 12
   (flet ((first-10 (list)
13 13
            (subseq list 0 (min (length list) 10))))
14
-    (let* ((by-date (by-date (hash-table-values *posts*)))
14
+    (let* ((by-date (by-date (find-all 'post)))
15 15
            (posts (first-10 by-date))
16 16
            (rss (make-instance 'index :id "rss.xml" :posts posts))
17 17
            (atom (make-instance 'index :id "feed.atom" :posts posts)))

+ 26 - 30
src/indices.lisp

@@ -27,62 +27,58 @@
27 27
   (rel-path (staging *config*) "~d" (index-id object)))
28 28
 
29 29
 (defun all-months ()
30
-  "Retrieve a list of all months with published posts."
31
-  (sort (remove-duplicates (mapcar (lambda (x) (get-month (post-date x)))
32
-                                   (hash-table-values *posts*)) :test #'string=)
30
+  "Retrieve a list of all months with published content."
31
+  (sort (remove-duplicates (mapcar (lambda (x) (get-month (content-date x)))
32
+                                   (hash-table-values *content*)) :test #'string=)
33 33
         #'string>))
34 34
 
35 35
 (defun all-tags ()
36
-  "Retrieve a list of all tags used in posts."
37
-  (sort (remove-duplicates (mappend 'post-tags (hash-table-values *posts*))
36
+  "Retrieve a list of all tags used in content."
37
+  (sort (remove-duplicates (mappend 'content-tags (hash-table-values *content*))
38 38
                            :test #'string=) #'string<))
39 39
 
40 40
 (defun get-month (timestamp)
41 41
   "Extract the YYYY-MM portion of TIMESTAMP."
42 42
   (subseq timestamp 0 7))
43 43
 
44
-(defun by-date (posts)
45
-  "Sort POSTS in reverse chronological order."
46
-  (sort posts #'string> :key #'post-date))
47
-
48
-(defun index-by-tag (tag posts)
49
-  "Return an index of all POSTS matching the given TAG."
50
-  (let ((content (remove-if-not (lambda (post) (member tag (post-tags post)
51
-                                                       :test #'string=)) posts)))
44
+(defun index-by-tag (tag content)
45
+  "Return an index of all CONTENT matching the given TAG."
46
+  (let ((results (remove-if-not (lambda (obj) (member tag (content-tags obj)
47
+                                                      :test #'string=)) content)))
52 48
     (make-instance 'tag-index :id tag
53
-                              :posts content
49
+                              :posts results
54 50
                               :title (format nil "Posts tagged ~a" tag))))
55 51
 
56
-(defun index-by-month (month posts)
57
-  "Return an index of all POSTS matching the given MONTH."
58
-  (let ((content (remove-if-not (lambda (post) (search month (post-date post)))
59
-                                posts)))
52
+(defun index-by-month (month content)
53
+  "Return an index of all CONTENT matching the given MONTH."
54
+  (let ((results (remove-if-not (lambda (obj) (search month (content-date obj)))
55
+                                content)))
60 56
     (make-instance 'date-index :id month
61
-                               :posts content
57
+                               :posts results
62 58
                                :title (format nil "Posts from ~a" month))))
63 59
 
64
-(defun index-by-n (i posts &optional (step 10))
65
-  "Return the index for the Ith page of POSTS in reverse chronological order."
60
+(defun index-by-n (i content &optional (step 10))
61
+  "Return the index for the Ith page of CONTENT in reverse chronological order."
66 62
   (make-instance 'int-index :id (1+ i)
67 63
                             :posts (let ((index (* step i)))
68
-                                     (subseq posts index (min (length posts)
69
-                                                              (+ index step))))
64
+                                     (subseq content index (min (length content)
65
+                                                                (+ index step))))
70 66
                             :title "Recent Posts"))
71 67
 
72 68
 (defun render-indices ()
73
-  "Render the indices to view posts in groups of size N, by month, and by tag."
74
-  (let ((posts (by-date (hash-table-values *posts*))))
69
+  "Render the indices to view content in groups of size N, by month, and by tag."
70
+  (let ((results (by-date (hash-table-values *content*))))
75 71
     (dolist (tag (all-tags))
76
-      (let ((index (index-by-tag tag posts)))
72
+      (let ((index (index-by-tag tag results)))
77 73
         (write-page (page-path index) (render-page index))))
78 74
     (dolist (month (all-months))
79
-      (let ((index (index-by-month month posts)))
75
+      (let ((index (index-by-month month results)))
80 76
         (write-page (page-path index) (render-page index))))
81
-    (dotimes (i (ceiling (length posts) 10))
82
-      (let ((index (index-by-n i posts)))
77
+    (dotimes (i (ceiling (length results) 10))
78
+      (let ((index (index-by-n i results)))
83 79
         (write-page (page-path index)
84 80
                     (render-page index nil
85 81
                                  :prev (and (plusp i) i)
86
-                                 :next (and (< (* (1+ i) 10) (length posts))
82
+                                 :next (and (< (* (1+ i) 10) (length results))
87 83
                                             (+ 2 i)))))))
88 84
   (update-symlink "index.html" "1.html"))

+ 8 - 3
src/packages.lisp

@@ -3,12 +3,17 @@
3 3
   (:use :cl)
4 4
   (:import-from :alexandria #:hash-table-values
5 5
                             #:make-keyword
6
-                            #:mappend)
6
+                            #:mappend
7
+                            #:compose)
7 8
   (:import-from :closure-template #:compile-template)
8 9
   (:export #:main
9 10
            #:blog
11
+           #:content
10 12
            #:post
11 13
            #:index
12
-           #:add-injection
14
+           #:page-path
15
+           #:discover
16
+           #:publish
17
+           #:render
13 18
            #:render-content
14
-           #:deploy))
19
+           #:add-injection))

+ 23 - 63
src/posts.lisp

@@ -1,15 +1,8 @@
1 1
 (in-package :coleslaw)
2 2
 
3
-(defparameter *posts* (make-hash-table :test #'equal)
4
-  "A hash table to store all the posts and their metadata.")
5
-
6
-(defclass post ()
7
-  ((slug :initform nil :initarg :slug :accessor post-slug)
8
-   (title :initform nil :initarg :title :accessor post-title)
9
-   (tags :initform nil :initarg :tags :accessor post-tags)
10
-   (date :initform nil :initarg :date :accessor post-date)
11
-   (format :initform nil :initarg :format :accessor post-format)
12
-   (content :initform nil :initarg :content :accessor post-content)))
3
+(defclass post (content)
4
+  ((title :initform nil :initarg :title :accessor post-title)
5
+   (format :initform nil :initarg :format :accessor post-format)))
13 6
 
14 7
 (defmethod render ((object post) &key prev next)
15 8
   (funcall (theme-fn 'post) (list :config *config*
@@ -18,59 +11,26 @@
18 11
                                   :next next)))
19 12
 
20 13
 (defmethod page-path ((object post))
21
-  (rel-path (staging *config*) "posts/~a" (post-slug object)))
22
-
23
-(defun read-post (in)
24
-  "Make a POST instance based on the data from the stream IN."
25
-  (flet ((check-header ()
26
-           (unless (string= (read-line in) ";;;;;")
27
-             (error "The provided file lacks the expected header.")))
28
-         (parse-field (str)
29
-           (nth-value 1 (cl-ppcre:scan-to-strings "[a-zA-Z]+: (.*)" str)))
30
-         (field-name (line)
31
-           (subseq line 0 (position #\: line)))
32
-         (read-tags (str)
33
-           (mapcar #'string-downcase (cl-ppcre:split ", " str)))
34
-         (slurp-remainder ()
35
-           (let ((seq (make-string (- (file-length in) (file-position in)))))
36
-             (read-sequence seq in)
37
-             (remove #\Nul seq))))
38
-    (check-header)
39
-    (let ((args (loop for line = (read-line in nil) until (string= line ";;;;;")
40
-                   appending (list (make-keyword (string-upcase (field-name line)))
41
-                                   (aref (parse-field line) 0)))))
42
-      (setf (getf args :tags) (read-tags (getf args :tags))
43
-            (getf args :format) (make-keyword (string-upcase (getf args :format))))
44
-      (apply 'make-instance 'post
45
-             (append args (list :content (render-content (slurp-remainder)
46
-                                                         (getf args :format))
47
-                                :slug (slugify (getf args :title))))))))
48
-
49
-(defun load-posts ()
50
-  "Read the stored .post files from the repo."
51
-  (clrhash *posts*)
14
+  (rel-path (staging *config*) "posts/~a" (content-slug object)))
15
+
16
+(defmethod initialize-instance :after ((object post) &key)
17
+  (with-accessors ((title post-title)
18
+                   (format post-format)
19
+                   (text content-text)) object
20
+      (setf (content-slug object) (slugify title)
21
+            format (make-keyword (string-upcase format))
22
+            text (render-content text format))))
23
+
24
+(defmethod discover ((content-type (eql :post)))
25
+  (purge-all 'post)
52 26
   (do-files (file (repo *config*) "post")
53
-    (with-open-file (in file)
54
-      (let ((post (read-post in)))
55
-        (if (gethash (post-slug post) *posts*)
56
-            (error "There is already an existing post with the slug ~a."
57
-                   (post-slug post))
58
-            (setf (gethash (post-slug post) *posts*) post))))))
59
-
60
-(defun render-posts ()
61
-  "Iterate through the files in the repo to render+write the posts out to disk."
62
-  (loop for (prev post next) on (append '(nil) (sort (hash-table-values *posts*)
63
-                                                     #'string< :key #'post-date))
27
+    (let ((post (construct 'post (read-content file t))))
28
+      (if (gethash (content-slug post) *content*)
29
+          (error "There is already an existing post with the slug ~a."
30
+                 (content-slug post))
31
+          (setf (gethash (content-slug post) *content*) post)))))
32
+
33
+(defmethod publish ((content-type (eql :post)))
34
+  (loop for (next post prev) on (append '(nil) (by-date (find-all 'post)))
64 35
      while post do (write-page (page-path post)
65 36
                                (render-page post nil :prev prev :next next))))
66
-
67
-(defun slug-char-p (char)
68
-  "Determine if CHAR is a valid slug (i.e. URL) character."
69
-  (or (char<= #\0 char #\9)
70
-      (char<= #\a char #\z)
71
-      (char<= #\A char #\Z)
72
-      (member char '(#\_ #\- #\.))))
73
-
74
-(defun slugify (string)
75
-  "Return a version of STRING suitable for use as a URL."
76
-  (remove-if-not #'slug-char-p (substitute #\- #\Space string)))

+ 1 - 1
themes/atom.tmpl

@@ -22,7 +22,7 @@
22 22
       <name>{$config.author}</name>
23 23
       <uri>{$config.domain}</uri>
24 24
     </author>
25
-    <content type="html">{$post.content |noAutoescape}</content>
25
+    <content type="html">{$post.text |noAutoescape}</content>
26 26
   </entry>
27 27
   {/foreach}
28 28
 

+ 1 - 1
themes/hyde/index.tmpl

@@ -6,7 +6,7 @@
6 6
   <div class="article-meta">
7 7
     <a class="article-title" href="{$config.domain}/posts/{$post.slug}.html">{$post.title}</a>
8 8
     <div class="date"> posted on {$post.date}</div>
9
-    <div class="article">{$post.content |noAutoescape}</div>
9
+    <div class="article">{$post.text |noAutoescape}</div>
10 10
   </div>
11 11
 {/foreach}
12 12
 <div id="relative-nav">

+ 1 - 1
themes/hyde/post.tmpl

@@ -14,7 +14,7 @@
14 14
   </div>{\n}
15 15
 </div>{\n}
16 16
 <div class="article-content">{\n}
17
-  {$post.content |noAutoescape}
17
+  {$post.text |noAutoescape}
18 18
 </div>{\n}
19 19
 <div class="relative-nav">{\n}
20 20
   {if $prev} <a href="{$config.domain}/posts/{$prev.slug}.html">Previous</a><br> {/if}{\n}

+ 1 - 1
themes/rss.tmpl

@@ -20,7 +20,7 @@
20 20
       {foreach $tag in $post.tags}
21 21
         <category><![CDATA[ {$tag} ]]></category>
22 22
       {/foreach}
23
-      <description><![CDATA[ {$post.content |noAutoescape} ]]></description>
23
+      <description><![CDATA[ {$post.text |noAutoescape} ]]></description>
24 24
     </item>
25 25
     {/foreach}
26 26