Browse Source

Massive indexes rewrite.

Brit Butler 11 years ago
parent
commit
7af3462d99
7 changed files with 175 additions and 114 deletions
  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
 // TODO: Write something about class-names as file-extension/eql-specializers!
42
 // TODO: Write something about class-names as file-extension/eql-specializers!
43
 ### Current Content Types & Indexes
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
 NUMERIC-INDEX, FEED, and TAG-FEED. Respectively, they support
46
 NUMERIC-INDEX, FEED, and TAG-FEED. Respectively, they support
47
 grouping content by tags, publishing date, and reverse chronological
47
 grouping content by tags, publishing date, and reverse chronological
48
 order. Feeds exist to special case RSS and ATOM generation.
48
 order. Feeds exist to special case RSS and ATOM generation.
71
 and return rendered HTML.  **Coleslaw** defines a helper called
71
 and return rendered HTML.  **Coleslaw** defines a helper called
72
 `theme-fn` for easy access to the template functions.
72
 `theme-fn` for easy access to the template functions.
73
 
73
 
74
+// TODO: Update for changes to compile-blog, indexes refactor, etc.
74
 ### The Lifecycle of a Page
75
 ### The Lifecycle of a Page
75
 
76
 
76
 - `(load-content)`
77
 - `(load-content)`
84
 
85
 
85
 - `(compile-blog dir)`
86
 - `(compile-blog dir)`
86
 
87
 
88
+
87
 Compilation starts by ensuring the staging directory (`/tmp/coleslaw/`
89
 Compilation starts by ensuring the staging directory (`/tmp/coleslaw/`
88
 by default) exists, cd'ing there, and copying over any necessary theme
90
 by default) exists, cd'ing there, and copying over any necessary theme
89
 assets. Then *coleslaw* iterates over the content types, calling the
91
 assets. Then *coleslaw* iterates over the content types, calling the

+ 4 - 5
plugins/mathjax.lisp

4
   (:import-from :coleslaw #:add-injection
4
   (:import-from :coleslaw #:add-injection
5
                           #:content
5
                           #:content
6
                           #:index
6
                           #:index
7
-                          #:content-tags
8
-                          #:index-posts
9
-                          #:make-tag
10
-                          #:tag-slug=))
7
+                          #:tag-p
8
+                          #:index-posts))
11
 
9
 
12
 (in-package :coleslaw-mathjax)
10
 (in-package :coleslaw-mathjax)
13
 
11
 
19
 (defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML")
17
 (defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML")
20
                  (location "http://cdn.mathjax.org/mathjax/latest/MathJax.js"))
18
                  (location "http://cdn.mathjax.org/mathjax/latest/MathJax.js"))
21
   (labels ((math-post-p (obj)
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
            (mathjax-p (obj)
22
            (mathjax-p (obj)
24
              (or force
23
              (or force
25
                  (etypecase obj
24
                  (etypecase obj

+ 50 - 42
src/coleslaw.lisp

1
 (in-package :coleslaw)
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
 (defun compile-blog (staging)
19
 (defun compile-blog (staging)
37
   "Compile the blog to a STAGING directory as specified in .coleslawrc."
20
   "Compile the blog to a STAGING directory as specified in .coleslawrc."
44
       (when (probe-file dir)
27
       (when (probe-file dir)
45
         (run-program "rsync --delete -raz ~a ." dir)))
28
         (run-program "rsync --delete -raz ~a ." dir)))
46
     (do-subclasses (ctype content)
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
     (update-symlink "index.html" "1.html")))
33
     (update-symlink "index.html" "1.html")))
50
 
34
 
51
 (defgeneric deploy (staging)
35
 (defgeneric deploy (staging)
63
         (update-symlink prev (truename curr)))
47
         (update-symlink prev (truename curr)))
64
       (update-symlink curr new-build))))
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
 (defun preview (path &optional (content-type 'post))
50
 (defun preview (path &optional (content-type 'post))
76
   "Render the content at PATH under user's configured repo and save it to
51
   "Render the content at PATH under user's configured repo and save it to
77
 ~/tmp.html. Load the user's config and theme if necessary."
52
 ~/tmp.html. Load the user's config and theme if necessary."
82
     (let* ((file (rel-path (repo *config*) path))
57
     (let* ((file (rel-path (repo *config*) path))
83
            (content (construct content-type (read-content file))))
58
            (content (construct content-type (read-content file))))
84
       (write-page "tmp.html" (render-page content)))))
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
 
21
 
22
 (defun tag-p (tag obj)
22
 (defun tag-p (tag obj)
23
   "Test if OBJ is tagged with TAG."
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
 (defun month-p (month obj)
27
 (defun month-p (month obj)
27
   "Test if OBJ was written in MONTH."
28
   "Test if OBJ was written in MONTH."
51
         (setf (getf meta :tags) (read-tags (getf meta :tags)))
52
         (setf (getf meta :tags) (read-tags (getf meta :tags)))
52
         (append meta (list :text content))))))
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
 (defun by-date (content)
55
 (defun by-date (content)
60
   "Sort CONTENT in reverse chronological order."
56
   "Sort CONTENT in reverse chronological order."
61
   (sort content #'string> :key #'content-date))
57
   (sort content #'string> :key #'content-date))

+ 9 - 5
src/documents.lisp

7
 (defvar *site* (make-hash-table :test #'equal)
7
 (defvar *site* (make-hash-table :test #'equal)
8
   "An in-memory database to hold all site documents, keyed on page-url.")
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
 ;; Class Methods
17
 ;; Class Methods
11
 
18
 
12
 (defun find-all (doc-type)
19
 (defun find-all (doc-type)
25
 (defgeneric discover (doc-type)
32
 (defgeneric discover (doc-type)
26
   (:documentation "Load all documents of the given DOC-TYPE into memory.")
33
   (:documentation "Load all documents of the given DOC-TYPE into memory.")
27
   (:method (doc-type)
34
   (:method (doc-type)
28
-    (purge-all doc-type)
29
     (let* ((class-name (class-name doc-type))
35
     (let* ((class-name (class-name doc-type))
30
            (file-type (string-downcase (symbol-name class-name))))
36
            (file-type (string-downcase (symbol-name class-name))))
37
+      (purge-all class-name)
31
       (do-files (file (repo *config*) file-type)
38
       (do-files (file (repo *config*) file-type)
32
         (let ((obj (construct class-name (read-content file))))
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
 ;; Instance Methods
42
 ;; Instance Methods
39
 
43
 

+ 106 - 54
src/indexes.lisp

5
    (posts :initform nil :initarg :posts :accessor index-posts)
5
    (posts :initform nil :initarg :posts :accessor index-posts)
6
    (title :initform nil :initarg :title :accessor index-title)))
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
 (defmethod render ((object index) &key prev next)
8
 (defmethod render ((object index) &key prev next)
30
   (funcall (theme-fn 'index) (list :tags (all-tags)
9
   (funcall (theme-fn 'index) (list :tags (all-tags)
31
                                    :months (all-months)
10
                                    :months (all-months)
34
                                    :prev prev
13
                                    :prev prev
35
                                    :next next)))
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
 (defun index-by-tag (tag content)
29
 (defun index-by-tag (tag content)
50
   "Return an index of all CONTENT matching the given TAG."
30
   "Return an index of all CONTENT matching the given TAG."
52
                  :posts (remove-if-not (lambda (x) (tag-p tag x)) content)
32
                  :posts (remove-if-not (lambda (x) (tag-p tag x)) content)
53
                  :title (format nil "Posts tagged ~a" (tag-name tag))))
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
 (defun index-by-month (month content)
52
 (defun index-by-month (month content)
56
   "Return an index of all CONTENT matching the given MONTH."
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
                  :posts (remove-if-not (lambda (x) (month-p month x)) content)
55
                  :posts (remove-if-not (lambda (x) (month-p month x)) content)
59
                  :title (format nil "Posts from ~a" month)))
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
 (defun index-by-n (i content)
75
 (defun index-by-n (i content)
62
   "Return the index for the Ith page of CONTENT in reverse chronological order."
76
   "Return the index for the Ith page of CONTENT in reverse chronological order."
63
   (let ((content (subseq content (* 10 i))))
77
   (let ((content (subseq content (* 10 i))))
65
                    :posts (take-up-to 10 content)
79
                    :posts (take-up-to 10 content)
66
                    :title "Recent Posts")))
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
 (defun render-feed (feed)
142
 (defun render-feed (feed)
69
   "Render the given FEED to both RSS and ATOM."
143
   "Render the given FEED to both RSS and ATOM."
70
   (let ((theme-fn (theme-fn (feed-format feed) "feeds")))
144
   (let ((theme-fn (theme-fn (feed-format feed) "feeds")))
73
 (defun render-index (index &rest render-args)
147
 (defun render-index (index &rest render-args)
74
   "Render the given INDEX using RENDER-ARGS if provided."
148
   "Render the given INDEX using RENDER-ARGS if provided."
75
   (write-page (page-path index) (apply #'render-page index nil render-args)))
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
 (defmethod page-url ((object post))
24
 (defmethod page-url ((object post))
25
   (format nil "~a/~a" (posts-dir *config*) (content-slug object)))
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
   (loop for (next post prev) on (append '(nil) (by-date (find-all 'post)))
28
   (loop for (next post prev) on (append '(nil) (by-date (find-all 'post)))
29
      while post do (write-page (page-path post)
29
      while post do (write-page (page-path post)
30
                                (render-page post nil :prev prev :next next))))
30
                                (render-page post nil :prev prev :next next))))