Brit Butler лет назад: 11
Родитель
Сommit
7af3462d99
7 измененных файлов с 175 добавлено и 114 удалено
  1. 3 1
      docs/hacking.md
  2. 4 5
      plugins/mathjax.lisp
  3. 50 42
      src/coleslaw.lisp
  4. 2 6
      src/content.lisp
  5. 9 5
      src/documents.lisp
  6. 106 54
      src/indexes.lisp
  7. 1 1
      src/posts.lisp

+ 3 - 1
docs/hacking.md

@@ -42,7 +42,7 @@ template, and their inclusion in an INDEX is presently ad-hoc.
42 42
 // TODO: Write something about class-names as file-extension/eql-specializers!
43 43
 ### Current Content Types & Indexes
44 44
 
45
-There are 5 INDEX subclasses at present: TAG-INDEX, DATE-INDEX,
45
+There are 5 INDEX subclasses at present: TAG-INDEX, MONTH-INDEX,
46 46
 NUMERIC-INDEX, FEED, and TAG-FEED. Respectively, they support
47 47
 grouping content by tags, publishing date, and reverse chronological
48 48
 order. Feeds exist to special case RSS and ATOM generation.
@@ -71,6 +71,7 @@ loaded. These functions take a property list (or plist) as an argument
71 71
 and return rendered HTML.  **Coleslaw** defines a helper called
72 72
 `theme-fn` for easy access to the template functions.
73 73
 
74
+// TODO: Update for changes to compile-blog, indexes refactor, etc.
74 75
 ### The Lifecycle of a Page
75 76
 
76 77
 - `(load-content)`
@@ -84,6 +85,7 @@ from each matching file and inserted into the `*content*` hash-table.
84 85
 
85 86
 - `(compile-blog dir)`
86 87
 
88
+
87 89
 Compilation starts by ensuring the staging directory (`/tmp/coleslaw/`
88 90
 by default) exists, cd'ing there, and copying over any necessary theme
89 91
 assets. Then *coleslaw* iterates over the content types, calling the

+ 4 - 5
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-posts))
11 9
 
12 10
 (in-package :coleslaw-mathjax)
13 11
 
@@ -19,7 +17,8 @@
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

+ 50 - 42
src/coleslaw.lisp

@@ -1,37 +1,20 @@
1 1
 (in-package :coleslaw)
2 2
 
3
-(defgeneric render-content (text format)
4
-  (:documentation "Compile TEXT from the given FORMAT to HTML for display.")
5
-  (:method (text (format (eql :html)))
6
-    text)
7
-  (:method (text (format (eql :md)))
8
-    (let ((3bmd-code-blocks:*code-blocks* t))
9
-      (with-output-to-string (str)
10
-        (3bmd:parse-string-and-print-to-stream text str)))))
11
-
12
-(defun page-path (object)
13
-  "The path to store OBJECT at once rendered."
14
-  (rel-path (staging-dir *config*) (namestring (page-url object))))
15
-
16
-(defun render-page (content &optional theme-fn &rest render-args)
17
-  "Render the given CONTENT to disk using THEME-FN if supplied.
18
-Additional args to render CONTENT can be passed via RENDER-ARGS."
19
-  (funcall (or theme-fn (theme-fn 'base))
20
-           (list :config *config*
21
-                 :content content
22
-                 :raw (apply 'render content render-args)
23
-                 :pubdate (make-pubdate)
24
-                 :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)))
25 11
 
26
-(defun write-page (filepath page)
27
-  "Write the given PAGE to FILEPATH."
28
-  (ensure-directories-exist filepath)
29
-  (with-open-file (out filepath
30
-                   :direction :output
31
-                   :if-exists :supersede
32
-                   :if-does-not-exist :create
33
-                   :external-format '(:utf-8))
34
-    (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)))
35 18
 
36 19
 (defun compile-blog (staging)
37 20
   "Compile the blog to a STAGING directory as specified in .coleslawrc."
@@ -44,8 +27,9 @@ Additional args to render CONTENT can be passed via RENDER-ARGS."
44 27
       (when (probe-file dir)
45 28
         (run-program "rsync --delete -raz ~a ." dir)))
46 29
     (do-subclasses (ctype content)
47
-      (publish (make-keyword ctype)))
48
-    (render-indexes (feeds *config*))
30
+      (publish ctype))
31
+    (do-subclasses (itype index)
32
+      (publish itype))
49 33
     (update-symlink "index.html" "1.html")))
50 34
 
51 35
 (defgeneric deploy (staging)
@@ -63,15 +47,6 @@ Additional args to render CONTENT can be passed via RENDER-ARGS."
63 47
         (update-symlink prev (truename curr)))
64 48
       (update-symlink curr new-build))))
65 49
 
66
-(defun main (&optional config-key)
67
-  "Load the user's config file, then compile and deploy the site."
68
-  (load-config config-key)
69
-  (load-content)
70
-  (compile-theme (theme *config*))
71
-  (let ((dir (staging-dir *config*)))
72
-    (compile-blog dir)
73
-    (deploy dir)))
74
-
75 50
 (defun preview (path &optional (content-type 'post))
76 51
   "Render the content at PATH under user's configured repo and save it to
77 52
 ~/tmp.html. Load the user's config and theme if necessary."
@@ -82,3 +57,36 @@ Additional args to render CONTENT can be passed via RENDER-ARGS."
82 57
     (let* ((file (rel-path (repo *config*) path))
83 58
            (content (construct content-type (read-content file))))
84 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)))

+ 2 - 6
src/content.lisp

@@ -21,7 +21,8 @@
21 21
 
22 22
 (defun tag-p (tag obj)
23 23
   "Test if OBJ is tagged with TAG."
24
-  (member tag (content-tags obj) :test #'tag-slug=))
24
+  (let ((tag (if (typep tag 'tag) tag (make-tag tag))))
25
+    (member tag (content-tags obj) :test #'tag-slug=)))
25 26
 
26 27
 (defun month-p (month obj)
27 28
   "Test if OBJ was written in MONTH."
@@ -51,11 +52,6 @@
51 52
         (setf (getf meta :tags) (read-tags (getf meta :tags)))
52 53
         (append meta (list :text content))))))
53 54
 
54
-(defun load-content ()
55
-  "Load all content stored in the blog's repo."
56
-  (do-subclasses (ctype content)
57
-    (discover ctype)))
58
-
59 55
 (defun by-date (content)
60 56
   "Sort CONTENT in reverse chronological order."
61 57
   (sort content #'string> :key #'content-date))

+ 9 - 5
src/documents.lisp

@@ -7,6 +7,13 @@
7 7
 (defvar *site* (make-hash-table :test #'equal)
8 8
   "An in-memory database to hold all site documents, keyed on page-url.")
9 9
 
10
+(defun add-document (doc)
11
+  "Add DOC to the in-memory database. If a matching entry is present, error."
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
+
10 17
 ;; Class Methods
11 18
 
12 19
 (defun find-all (doc-type)
@@ -25,15 +32,12 @@
25 32
 (defgeneric discover (doc-type)
26 33
   (:documentation "Load all documents of the given DOC-TYPE into memory.")
27 34
   (:method (doc-type)
28
-    (purge-all doc-type)
29 35
     (let* ((class-name (class-name doc-type))
30 36
            (file-type (string-downcase (symbol-name class-name))))
37
+      (purge-all class-name)
31 38
       (do-files (file (repo *config*) file-type)
32 39
         (let ((obj (construct class-name (read-content file))))
33
-          (if (gethash (page-url obj) *site*)
34
-              (error "There is already existing content with the url ~a."
35
-                     (page-url obj))
36
-              (setf (gethash (page-url obj) *site*) obj)))))))
40
+          (add-document obj))))))
37 41
 
38 42
 ;; Instance Methods
39 43
 

+ 106 - 54
src/indexes.lisp

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

+ 1 - 1
src/posts.lisp

@@ -24,7 +24,7 @@
24 24
 (defmethod page-url ((object post))
25 25
   (format nil "~a/~a" (posts-dir *config*) (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))))