Explorar el Código

First pass at support for multiple content-types. WARNING: Indices and feeds are broken.

Brit Butler hace 12 años
padre
commit
6d47244eac
Se han modificado 5 ficheros con 112 adiciones y 72 borrados
  1. 3 1
      coleslaw.asd
  2. 2 2
      src/coleslaw.lisp
  3. 81 0
      src/content.lisp
  4. 4 8
      src/indices.lisp
  5. 22 61
      src/posts.lisp

+ 3 - 1
coleslaw.asd

@@ -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")

+ 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 (class-name 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*)))

+ 81 - 0
src/content.lisp

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

+ 4 - 8
src/indices.lisp

@@ -29,25 +29,21 @@
29 29
 (defun all-months ()
30 30
   "Retrieve a list of all months with published posts."
31 31
   (sort (remove-duplicates (mapcar (lambda (x) (get-month (post-date x)))
32
-                                   (hash-table-values *posts*)) :test #'string=)
32
+                                   (hash-table-values *content*)) :test #'string=)
33 33
         #'string>))
34 34
 
35 35
 (defun all-tags ()
36 36
   "Retrieve a list of all tags used in posts."
37
-  (sort (remove-duplicates (mappend 'post-tags (hash-table-values *posts*))
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 44
 (defun index-by-tag (tag posts)
49 45
   "Return an index of all POSTS matching the given TAG."
50
-  (let ((content (remove-if-not (lambda (post) (member tag (post-tags post)
46
+  (let ((content (remove-if-not (lambda (post) (member tag (content-tags post)
51 47
                                                        :test #'string=)) posts)))
52 48
     (make-instance 'tag-index :id tag
53 49
                               :posts content
@@ -71,7 +67,7 @@
71 67
 
72 68
 (defun render-indices ()
73 69
   "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*))))
70
+  (let ((posts (by-date (hash-table-values *content*))))
75 71
     (dolist (tag (all-tags))
76 72
       (let ((index (index-by-tag tag posts)))
77 73
         (write-page (page-path index) (render-page index))))

+ 22 - 61
src/posts.lisp

@@ -1,13 +1,7 @@
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)
3
+(defclass post (content)
4
+  ((title :initform nil :initarg :title :accessor post-title)
11 5
    (format :initform nil :initarg :format :accessor post-format)
12 6
    (content :initform nil :initarg :content :accessor post-content)))
13 7
 
@@ -18,59 +12,26 @@
18 12
                                   :next next)))
19 13
 
20 14
 (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*)
15
+  (rel-path (staging *config*) "posts/~a" (content-slug object)))
16
+
17
+(defmethod initialize-instance :after ((post post) &key)
18
+  (with-accessors ((title post-title)
19
+                   (format post-format)
20
+                   (content post-content)) post
21
+      (setf (content-slug post) (slugify title)
22
+            format (make-keyword (string-upcase format))
23
+            content (render-content content format))))
24
+
25
+(defmethod discover ((content-type (eql :post)))
26
+  (purge-all 'post)
52 27
   (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))
28
+    (let ((post (construct :post (read-content file t))))
29
+      (if (gethash (content-slug post) *content*)
30
+          (error "There is already an existing post with the slug ~a."
31
+                 (content-slug post))
32
+          (setf (gethash (content-slug post) *content*) post)))))
33
+
34
+(defmethod publish ((content-type (eql :post)))
35
+  (loop for (next post prev) on (append '(nil) (by-date (find-all 'post)))
64 36
      while post do (write-page (page-path post)
65 37
                                (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)))