From 7c12a9670bfe505e288adaddc36f0a91ad7088af Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Fri, 16 May 2014 14:45:17 -0400 Subject: [PATCH 01/14] Sketch out incremental plugin. --- plugins/incremental.lisp | 47 ++++++++++++++++++++++++++++++++++++++++ plugins/parallel.lisp | 16 ++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 plugins/incremental.lisp create mode 100644 plugins/parallel.lisp diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp new file mode 100644 index 0000000..e9c94ef --- /dev/null +++ b/plugins/incremental.lisp @@ -0,0 +1,47 @@ +(eval-when (:compile-toplevel :load-toplevel) + (ql:quickload 'cl-store)) + +(defpackage :coleslaw-incremental + (:use :cl) + (:import-from :coleslaw #:get-updated-files + #:find-content-by-path + #:write-document) + (:export #:enable)) + +(in-package :coleslaw-incremental) + +;; FIXME: We currently never update the site for config changes. +;; Examples to consider include changing the theme or domain of the site. + +;; NOTE: We're gonna be a bit dirty here and monkey patch. The compilation model +;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe +;; we'll settle on an interface. + +(defvar *changed-content* nil + "A list of changed content instances to iterate over and write out to disk.") + +(defun coleslaw::load-content () + ;; TODO: What if the file doesn't exist? + (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db"))) + (setf coleslaw::*site* (cl-store:restore db-file)) + (loop for (status path) in (get-updated-files) + do (update-content status path)) + (cl-store:store coleslaw::*site* db-file))) + +(defun update-content (status path) + (cond ((string= "D" status) (process-change :deleted path)) + ((string= "M" status) (process-change :modified path)) + ((string= "A" status) (process-change :added path)))) + +(defgeneric process-change (status path) + (:documentation "Updates the database as needed for the STATUS change to PATH.")) + +(defun coleslaw::compile-blog (staging) + "lulz. Do it live. DO IT ALL LIVE." + ;; FIXME: This doesn't cover prev/next links for posts, theme-fn for feeds. + (mapcar #'write-document *changed-content*)) + +;; No-op. We'll be updating in place instead. +(defmethod coleslaw:deploy (staging)) + +(defun enable ()) diff --git a/plugins/parallel.lisp b/plugins/parallel.lisp new file mode 100644 index 0000000..18e4a13 --- /dev/null +++ b/plugins/parallel.lisp @@ -0,0 +1,16 @@ +(eval-when (:compile-toplevel :load-toplevel) + (ql:quickload 'lparallel)) + +(defpackage :coleslaw-parallel + (:use :cl) + (:export #:enable)) + +(in-package :coleslaw-parallel) + +;; TODO: The bulk of the speedup here should come from parallelizing discover. +;; Publish will also benefit. Whether it's better to spin off threads for each +;; content type/index type or the operations *within* discover/publish is not +;; known, the higher granularity of doing it at the iterating over types level +;; is certainly easier to prototype though. + +(defun enable ()) From 75c30c5844897ff70fa035971337dab693b96294 Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Sun, 18 May 2014 23:59:27 -0400 Subject: [PATCH 02/14] Push sketch slightly further up hill. --- plugins/incremental.lisp | 39 ++++++++++++++++++++++++++++++++++----- src/util.lisp | 17 ++++++++++------- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp index e9c94ef..89adab5 100644 --- a/plugins/incremental.lisp +++ b/plugins/incremental.lisp @@ -3,15 +3,25 @@ (defpackage :coleslaw-incremental (:use :cl) - (:import-from :coleslaw #:get-updated-files + (:import-from :alexandria #:when-let) + (:import-from :coleslaw #:all-subclasses + #:content + #:construct + #:get-updated-files #:find-content-by-path - #:write-document) + #:write-document + #:rel-path) (:export #:enable)) (in-package :coleslaw-incremental) ;; FIXME: We currently never update the site for config changes. ;; Examples to consider include changing the theme or domain of the site. +;; Both would require full site recompiles. Consequently, it seems reasonable +;; to expect that incremental plugin users: +;; A) have done a full build of their site +;; B) have a cl-store dump of the database at ~/.coleslaw.db +;; ^ we should provide a script or plugin just for this ;; NOTE: We're gonna be a bit dirty here and monkey patch. The compilation model ;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe @@ -21,7 +31,6 @@ "A list of changed content instances to iterate over and write out to disk.") (defun coleslaw::load-content () - ;; TODO: What if the file doesn't exist? (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db"))) (setf coleslaw::*site* (cl-store:restore db-file)) (loop for (status path) in (get-updated-files) @@ -33,8 +42,28 @@ ((string= "M" status) (process-change :modified path)) ((string= "A" status) (process-change :added path)))) -(defgeneric process-change (status path) - (:documentation "Updates the database as needed for the STATUS change to PATH.")) +(defgeneric process-change (status path &key &allow-other-keys) + (:documentation "Updates the database as needed for the STATUS change to PATH.") + (:method :around (status path &key) + (let ((extension (pathname-type path)) + (ctypes (all-subclasses (find-class 'content)))) + ;; This feels way too clever. I wish I could think of a better option. + (flet ((class-name-p (x class) + (string-equal x (symbol-name (class-name class))))) + (when-let (ctype (find extension ctypes :test #'class-name-p)) + (call-next-method status path :ctype ctype)))))) + +(defmethod process-change ((status (eql :deleted)) path &key) + (let ((obj (find-content-by-path path))) + )) + +(defmethod process-change ((status (eql :modified)) path &key) + (let ((obj (find-content-by-path path))) + )) + +(defmethod process-change ((status (eql :added)) path &key ctype) + (let ((obj (construct ctype (read-content path)))) + )) (defun coleslaw::compile-blog (staging) "lulz. Do it live. DO IT ALL LIVE." diff --git a/src/util.lisp b/src/util.lisp index f5e3a09..af25127 100644 --- a/src/util.lisp +++ b/src/util.lisp @@ -4,16 +4,19 @@ "Create an instance of CLASS-NAME with the given ARGS." (apply 'make-instance class-name args)) +;; Thanks to bknr-web for this bit of code. +(defun all-subclasses (class) + "Return a list of all the subclasses of CLASS." + (let ((subclasses (closer-mop:class-direct-subclasses class))) + (append subclasses (loop for subclass in subclasses + nconc (all-subclasses subclass))))) + (defmacro do-subclasses ((var class) &body body) "Iterate over the subclasses of CLASS performing BODY with VAR lexically bound to the current subclass." - (alexandria:with-gensyms (klasses all-subclasses) - `(labels ((,all-subclasses (class) - (let ((subclasses (closer-mop:class-direct-subclasses class))) - (append subclasses (loop for subclass in subclasses - nconc (,all-subclasses subclass)))))) - (let ((,klasses (,all-subclasses (find-class ',class)))) - (loop for ,var in ,klasses do ,@body))))) + (alexandria:with-gensyms (klasses) + `(let ((,klasses (all-subclasses (find-class ',class)))) + (loop for ,var in ,klasses do ,@body)))) (defmacro do-files ((var path &optional extension) &body body) "For each file under PATH, run BODY. If EXTENSION is provided, only run From 7d9b0aa0111177f235ef0ed508ec2a5dc1d23950 Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Mon, 19 May 2014 12:00:54 -0400 Subject: [PATCH 03/14] Flesh out the sketch a bit more and add some constraints. --- plugins/incremental.lisp | 45 +++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp index 89adab5..a4c406a 100644 --- a/plugins/incremental.lisp +++ b/plugins/incremental.lisp @@ -15,26 +15,33 @@ (in-package :coleslaw-incremental) -;; FIXME: We currently never update the site for config changes. +;; KLUDGE: We currently never update the site for config changes. ;; Examples to consider include changing the theme or domain of the site. ;; Both would require full site recompiles. Consequently, it seems reasonable ;; to expect that incremental plugin users: ;; A) have done a full build of their site ;; B) have a cl-store dump of the database at ~/.coleslaw.db ;; ^ we should provide a script or plugin just for this +;; C) move the original deployment to a location of their choice and +;; set it as staging-dir in coleslaw's config prior to enabling incremental builds +;; D) to further simplify *my* life, we assume the date of a piece of content will +;; never be changed retroactively, only its tags ;; NOTE: We're gonna be a bit dirty here and monkey patch. The compilation model ;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe ;; we'll settle on an interface. -(defvar *changed-content* nil - "A list of changed content instances to iterate over and write out to disk.") +(defvar *transients* '(coleslaw::numeric-index coleslaw::feed coleslaw::tag-feed) + "A list of document types that should be regenerated on *any* change to the blog.") (defun coleslaw::load-content () (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db"))) (setf coleslaw::*site* (cl-store:restore db-file)) (loop for (status path) in (get-updated-files) do (update-content status path)) + (coleslaw::update-content-metadata) + (dolist (doc-type *transients*) + (discover (find-class doc-type))) (cl-store:store coleslaw::*site* db-file))) (defun update-content (status path) @@ -50,23 +57,47 @@ ;; This feels way too clever. I wish I could think of a better option. (flet ((class-name-p (x class) (string-equal x (symbol-name (class-name class))))) + ;; If the updated file's extension doesn't match one of our content types, + ;; we don't need to mess with it at all. Otherwise, since the class is + ;; annoyingly tricky to determine, pass it along. (when-let (ctype (find extension ctypes :test #'class-name-p)) (call-next-method status path :ctype ctype)))))) +;; TODO: We should check to see if a *new* tag or month exists +;; and create an index appropriately. If the last content from a +;; given month or with a given tag is deleted, just drop the index. +;; (And also remove it from *all-months* / *all-tags*. Should we store those?) +;; Additionally, the tag/month lists won't be updated on tag/month index pages. + (defmethod process-change ((status (eql :deleted)) path &key) - (let ((obj (find-content-by-path path))) - )) + (let ((old (find-content-by-path path))) + ;; TODO: Remove from any tag and month indexes. + (delete-document old))) (defmethod process-change ((status (eql :modified)) path &key) - (let ((obj (find-content-by-path path))) + (let ((old (find-content-by-path path)) + (new (construct ctype (read-content path)))) + (setf (gethash (page-url old) coleslaw::*site*) new) + ;; TODO: + ;; Iterate over tags in new, setting old to new in each tag index's content. + ;; If there are new tags/date, add it to relevant indices. + ;; If tags/date are removed, remove from relevant indices. )) (defmethod process-change ((status (eql :added)) path &key ctype) - (let ((obj (construct ctype (read-content path)))) + (let ((new (construct ctype (read-content path)))) )) +(defun delete-document (document) + "Given a DOCUMENT, delete it from the staging directory and in-memory DB." + (let ((url (page-url document))) + (delete-file (rel-path (staging-dir *config*) (namestring url))) + (remhash (page-url document) coleslaw::*site*))) + (defun coleslaw::compile-blog (staging) "lulz. Do it live. DO IT ALL LIVE." + (dolist (doc-type *transients*) + (publish (find-class doc-type))) ;; FIXME: This doesn't cover prev/next links for posts, theme-fn for feeds. (mapcar #'write-document *changed-content*)) From 8e8e3231ecfbbcb778ab319a853bbbfc8469d00f Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 11:15:02 -0400 Subject: [PATCH 04/14] Finish deletion, push addition uphill a bit. --- plugins/incremental.lisp | 57 +++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp index a4c406a..60750fb 100644 --- a/plugins/incremental.lisp +++ b/plugins/incremental.lisp @@ -4,13 +4,22 @@ (defpackage :coleslaw-incremental (:use :cl) (:import-from :alexandria #:when-let) - (:import-from :coleslaw #:all-subclasses - #:content - #:construct + (:import-from :coleslaw #:content + #:discover #:get-updated-files #:find-content-by-path + #:find-all + #:add-document #:write-document - #:rel-path) + ;; Private + #:all-subclasses + #:construct + #:rel-path + #:index-content + #:content-date + #:content-tags + #:tag-slug + ) (:export #:enable)) (in-package :coleslaw-incremental) @@ -22,17 +31,17 @@ ;; A) have done a full build of their site ;; B) have a cl-store dump of the database at ~/.coleslaw.db ;; ^ we should provide a script or plugin just for this -;; C) move the original deployment to a location of their choice and -;; set it as staging-dir in coleslaw's config prior to enabling incremental builds -;; D) to further simplify *my* life, we assume the date of a piece of content will -;; never be changed retroactively, only its tags +;; C) move the original deployment to a location of their choice and set it +;; as staging-dir in coleslaw's config prior to enabling incremental builds +;; D) to further simplify *my* life, we assume the date of a piece of content +;; will never be changed retroactively, only its tags ;; NOTE: We're gonna be a bit dirty here and monkey patch. The compilation model ;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe ;; we'll settle on an interface. (defvar *transients* '(coleslaw::numeric-index coleslaw::feed coleslaw::tag-feed) - "A list of document types that should be regenerated on *any* change to the blog.") + "A list of document types that should be regenerated on any change to the blog.") (defun coleslaw::load-content () (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db"))) @@ -65,13 +74,17 @@ ;; TODO: We should check to see if a *new* tag or month exists ;; and create an index appropriately. If the last content from a -;; given month or with a given tag is deleted, just drop the index. -;; (And also remove it from *all-months* / *all-tags*. Should we store those?) -;; Additionally, the tag/month lists won't be updated on tag/month index pages. +;; given month or with a given tag is deleted, delete the index. +;; Unfortunately, the tag/month links won't be updated on all +;; tag/month indexes since we only regenerate them for new posts. (defmethod process-change ((status (eql :deleted)) path &key) - (let ((old (find-content-by-path path))) - ;; TODO: Remove from any tag and month indexes. + (let* ((old (find-content-by-path path)) + (month-index (find-month-index (content-date old)))) + (delete old (index-content month-index)) + (dolist (tag (content-tags old)) + (let ((tag-index (find-tag-index tag))) + (delete old (index-content tag-index)))) (delete-document old))) (defmethod process-change ((status (eql :modified)) path &key) @@ -86,7 +99,9 @@ (defmethod process-change ((status (eql :added)) path &key ctype) (let ((new (construct ctype (read-content path)))) - )) + (add-document new) + ;; FIXME: New posts won't have prev/next links populated. + (write-document new))) (defun delete-document (document) "Given a DOCUMENT, delete it from the staging directory and in-memory DB." @@ -97,11 +112,17 @@ (defun coleslaw::compile-blog (staging) "lulz. Do it live. DO IT ALL LIVE." (dolist (doc-type *transients*) - (publish (find-class doc-type))) - ;; FIXME: This doesn't cover prev/next links for posts, theme-fn for feeds. - (mapcar #'write-document *changed-content*)) + (publish (find-class doc-type)))) ;; No-op. We'll be updating in place instead. (defmethod coleslaw:deploy (staging)) (defun enable ()) + +;;;; Utils + +(defun find-tag-index (tag) + (find (tag-slug tag) (find-all 'tag-index) :key #'index-slug :test #'equal)) + +(defun find-month-index (date) + (find (subseq date 0 7) (find-all 'month-index) :key #'index-slug :test #'equal)) From 62c940bde9562bc5a8ceb40a753a63399328aa4b Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 14:51:24 -0400 Subject: [PATCH 05/14] Finish addition, push modification uphill a bit. --- plugins/incremental.lisp | 56 ++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp index 60750fb..b934ddd 100644 --- a/plugins/incremental.lisp +++ b/plugins/incremental.lisp @@ -35,8 +35,8 @@ ;; as staging-dir in coleslaw's config prior to enabling incremental builds ;; D) to further simplify *my* life, we assume the date of a piece of content ;; will never be changed retroactively, only its tags - -;; NOTE: We're gonna be a bit dirty here and monkey patch. The compilation model +;; +;; We're gonna be a bit dirty here and monkey patch. The compilation model ;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe ;; we'll settle on an interface. @@ -72,11 +72,9 @@ (when-let (ctype (find extension ctypes :test #'class-name-p)) (call-next-method status path :ctype ctype)))))) -;; TODO: We should check to see if a *new* tag or month exists -;; and create an index appropriately. If the last content from a -;; given month or with a given tag is deleted, delete the index. -;; Unfortunately, the tag/month links won't be updated on all -;; tag/month indexes since we only regenerate them for new posts. +;; TODO: If the last content from a given month or with a given tag +;; is deleted, delete the index. Unfortunately, the tag/month links +;; won't be updated on all indexes since we only regenerate them for new posts. (defmethod process-change ((status (eql :deleted)) path &key) (let* ((old (find-content-by-path path)) @@ -90,25 +88,25 @@ (defmethod process-change ((status (eql :modified)) path &key) (let ((old (find-content-by-path path)) (new (construct ctype (read-content path)))) - (setf (gethash (page-url old) coleslaw::*site*) new) + (delete-document old) ;; TODO: - ;; Iterate over tags in new, setting old to new in each tag index's content. - ;; If there are new tags/date, add it to relevant indices. - ;; If tags/date are removed, remove from relevant indices. - )) + ;; Iterate over tags in new, replacing old with new in each tag index's content. + ;; If there are new tags/date, add it to relevant indices (or create them). + ;; If tags/date are removed, remove from relevant indices (or delete them). + (add-document new) + (write-document new))) (defmethod process-change ((status (eql :added)) path &key ctype) - (let ((new (construct ctype (read-content path)))) + (let* ((new (construct ctype (read-content path))) + (tags (content-tags new)) + (month (subseq (content-date new) 0 7))) + (maybe-add-month-index new month) + (dolist (tag tags) + (maybe-add-tag-index new tag)) (add-document new) ;; FIXME: New posts won't have prev/next links populated. (write-document new))) -(defun delete-document (document) - "Given a DOCUMENT, delete it from the staging directory and in-memory DB." - (let ((url (page-url document))) - (delete-file (rel-path (staging-dir *config*) (namestring url))) - (remhash (page-url document) coleslaw::*site*))) - (defun coleslaw::compile-blog (staging) "lulz. Do it live. DO IT ALL LIVE." (dolist (doc-type *transients*) @@ -121,6 +119,26 @@ ;;;; Utils +(defun delete-document (document) + "Given a DOCUMENT, delete it from the staging directory and in-memory DB." + (let ((url (page-url document))) + (delete-file (rel-path (staging-dir *config*) (namestring url))) + (remhash (page-url document) coleslaw::*site*))) + +(defun maybe-add-month-index (content month) + "Add a month index for MONTH containing CONTENT if one does not exist." +(unless (find-month-index month) + (let ((month-index (coleslaw::index-by-month month (list content)))) + (add-document month-index) + (write-document month-index)))) + +(defun maybe-add-tag-index (content tag) + "Add a tag index for TAG containing CONTENT if one does not exist." + (unless (find-tag-index tag) + (let ((tag-index (coleslaw::index-by-tag tag (list content)))) + (add-document tag-index) + (write-document tag-index)))) + (defun find-tag-index (tag) (find (tag-slug tag) (find-all 'tag-index) :key #'index-slug :test #'equal)) From 346715780552f6e00b5748b89572d3d80d0511b7 Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 16:32:43 -0400 Subject: [PATCH 06/14] Add and export DELETE-DOCUMENT. --- src/documents.lisp | 4 ++++ src/packages.lisp | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/documents.lisp b/src/documents.lisp index a7aff98..3c398dd 100644 --- a/src/documents.lisp +++ b/src/documents.lisp @@ -51,6 +51,10 @@ (error "There is already an existing document with the url ~a" url) (setf (gethash url *site*) document)))) +(defun delete-document (document) + "Given a DOCUMENT, delete it from the in-memory database." + (remhash (page-url document) *site*)) + (defun write-document (document &optional theme-fn &rest render-args) "Write the given DOCUMENT to disk as HTML. If THEME-FN is present, use it as the template passing any RENDER-ARGS." diff --git a/src/packages.lisp b/src/packages.lisp index 341c2c3..c6755a5 100644 --- a/src/packages.lisp +++ b/src/packages.lisp @@ -25,11 +25,12 @@ #:get-updated-files #:theme-fn ;; The Document Protocol - #:add-document - #:find-all - #:purge-all #:discover #:publish #:page-url #:render + #:find-all + #:purge-all + #:add-document + #:delete-document #:write-document)) From bed63d21563b1f4d4c59caf0518721b3177117ae Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 16:37:56 -0400 Subject: [PATCH 07/14] New pass at "incremental" compilation. See now, isn't that better? --- docs/hacking.md | 6 ++- plugins/incremental.lisp | 104 +++++++-------------------------------- 2 files changed, 23 insertions(+), 87 deletions(-) diff --git a/docs/hacking.md b/docs/hacking.md index ef6effd..d1843b2 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -134,7 +134,7 @@ be seamlessly picked up by *coleslaw* and included on the rendered site. All current Content Types and Indexes implement the protocol faithfully. It consists of 2 "class" methods, 2 instance methods, and an invariant. -There are also 4 helper functions provided that should prove useful in +There are also 5 helper functions provided that should prove useful in implementing new content types. @@ -191,6 +191,10 @@ eql-specializing on the class, e.g. unique. Such a hash collision represents content on the site being shadowed/overwritten. This should be used in your `discover` method. +- `delete-document`: Remove a document from *coleslaw*'s in-memory + database. This is currently only used by the incremental compilation + plugin. + - `write-document`: Write the document out to disk as HTML. It takes an optional template name and render-args to pass to the template. This should be used in your `publish` method. diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp index b934ddd..9c26125 100644 --- a/plugins/incremental.lisp +++ b/plugins/incremental.lisp @@ -8,49 +8,38 @@ #:discover #:get-updated-files #:find-content-by-path - #:find-all #:add-document - #:write-document + #:delete-document ;; Private #:all-subclasses + #:do-subclasses #:construct - #:rel-path - #:index-content - #:content-date - #:content-tags - #:tag-slug - ) + #:rel-path) (:export #:enable)) (in-package :coleslaw-incremental) -;; KLUDGE: We currently never update the site for config changes. -;; Examples to consider include changing the theme or domain of the site. -;; Both would require full site recompiles. Consequently, it seems reasonable -;; to expect that incremental plugin users: -;; A) have done a full build of their site -;; B) have a cl-store dump of the database at ~/.coleslaw.db -;; ^ we should provide a script or plugin just for this -;; C) move the original deployment to a location of their choice and set it -;; as staging-dir in coleslaw's config prior to enabling incremental builds -;; D) to further simplify *my* life, we assume the date of a piece of content -;; will never be changed retroactively, only its tags +;; In contrast to the original incremental plans, full of shoving state into +;; the right place by hand and avoiding writing pages to disk that hadn't +;; changed, the new plan is to only avoid redundant parsing of content in +;; the git repo. The rest of coleslaw's operation is "fast enough". +;; +;; Prior to enabling the plugin a user must have a cl-store dump of the +;; database at ~/.coleslaw.db. We should provide a script to generate it. ;; ;; We're gonna be a bit dirty here and monkey patch. The compilation model ;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe ;; we'll settle on an interface. -(defvar *transients* '(coleslaw::numeric-index coleslaw::feed coleslaw::tag-feed) - "A list of document types that should be regenerated on any change to the blog.") - (defun coleslaw::load-content () (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db"))) (setf coleslaw::*site* (cl-store:restore db-file)) (loop for (status path) in (get-updated-files) do (update-content status path)) (coleslaw::update-content-metadata) - (dolist (doc-type *transients*) - (discover (find-class doc-type))) + (do-subclasses (itype index) + ;; Discover's before method will delete the possibly outdated indexes. + (discover itype)) (cl-store:store coleslaw::*site* db-file))) (defun update-content (status path) @@ -72,75 +61,18 @@ (when-let (ctype (find extension ctypes :test #'class-name-p)) (call-next-method status path :ctype ctype)))))) -;; TODO: If the last content from a given month or with a given tag -;; is deleted, delete the index. Unfortunately, the tag/month links -;; won't be updated on all indexes since we only regenerate them for new posts. - (defmethod process-change ((status (eql :deleted)) path &key) - (let* ((old (find-content-by-path path)) - (month-index (find-month-index (content-date old)))) - (delete old (index-content month-index)) - (dolist (tag (content-tags old)) - (let ((tag-index (find-tag-index tag))) - (delete old (index-content tag-index)))) + (let ((old (find-content-by-path path))) (delete-document old))) -(defmethod process-change ((status (eql :modified)) path &key) +(defmethod process-change ((status (eql :modified)) path &key ctype) (let ((old (find-content-by-path path)) (new (construct ctype (read-content path)))) (delete-document old) - ;; TODO: - ;; Iterate over tags in new, replacing old with new in each tag index's content. - ;; If there are new tags/date, add it to relevant indices (or create them). - ;; If tags/date are removed, remove from relevant indices (or delete them). - (add-document new) - (write-document new))) + (add-document new))) (defmethod process-change ((status (eql :added)) path &key ctype) - (let* ((new (construct ctype (read-content path))) - (tags (content-tags new)) - (month (subseq (content-date new) 0 7))) - (maybe-add-month-index new month) - (dolist (tag tags) - (maybe-add-tag-index new tag)) - (add-document new) - ;; FIXME: New posts won't have prev/next links populated. - (write-document new))) - -(defun coleslaw::compile-blog (staging) - "lulz. Do it live. DO IT ALL LIVE." - (dolist (doc-type *transients*) - (publish (find-class doc-type)))) - -;; No-op. We'll be updating in place instead. -(defmethod coleslaw:deploy (staging)) + (let ((new (construct ctype (read-content path)))) + (add-document new))) (defun enable ()) - -;;;; Utils - -(defun delete-document (document) - "Given a DOCUMENT, delete it from the staging directory and in-memory DB." - (let ((url (page-url document))) - (delete-file (rel-path (staging-dir *config*) (namestring url))) - (remhash (page-url document) coleslaw::*site*))) - -(defun maybe-add-month-index (content month) - "Add a month index for MONTH containing CONTENT if one does not exist." -(unless (find-month-index month) - (let ((month-index (coleslaw::index-by-month month (list content)))) - (add-document month-index) - (write-document month-index)))) - -(defun maybe-add-tag-index (content tag) - "Add a tag index for TAG containing CONTENT if one does not exist." - (unless (find-tag-index tag) - (let ((tag-index (coleslaw::index-by-tag tag (list content)))) - (add-document tag-index) - (write-document tag-index)))) - -(defun find-tag-index (tag) - (find (tag-slug tag) (find-all 'tag-index) :key #'index-slug :test #'equal)) - -(defun find-month-index (date) - (find (subseq date 0 7) (find-all 'month-index) :key #'index-slug :test #'equal)) From 3109a988c477a1cc99828b537700bf706f8325c1 Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 16:41:12 -0400 Subject: [PATCH 08/14] Tiny tweak to incremental plugin. --- plugins/incremental.lisp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp index 9c26125..4e30466 100644 --- a/plugins/incremental.lisp +++ b/plugins/incremental.lisp @@ -14,7 +14,8 @@ #:all-subclasses #:do-subclasses #:construct - #:rel-path) + #:rel-path + #:update-content-metadata) (:export #:enable)) (in-package :coleslaw-incremental) @@ -36,7 +37,7 @@ (setf coleslaw::*site* (cl-store:restore db-file)) (loop for (status path) in (get-updated-files) do (update-content status path)) - (coleslaw::update-content-metadata) + (update-content-metadata) (do-subclasses (itype index) ;; Discover's before method will delete the possibly outdated indexes. (discover itype)) From 1d18a324541bff67e9a4df946f3817ff24ff3fc3 Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 17:01:23 -0400 Subject: [PATCH 09/14] Add basic dump_db.sh. We fight for the user! (or hacker, sysadmin, whatever) --- examples/dump-db.lisp | 17 +++++++++++++++++ examples/dump_db.sh | 9 +++++++++ 2 files changed, 26 insertions(+) create mode 100644 examples/dump-db.lisp create mode 100755 examples/dump_db.sh diff --git a/examples/dump-db.lisp b/examples/dump-db.lisp new file mode 100644 index 0000000..686057a --- /dev/null +++ b/examples/dump-db.lisp @@ -0,0 +1,17 @@ +(eval-when (:compile-toplevel :load-toplevel :execute) + (ql:quickload '(coleslaw cl-store))) + +(in-package :coleslaw) + +(defun main () + (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db"))) + (format t "~%~%Coleslaw loaded. Attempting to load config file.~%") + (load-config "") + (format t "~%Config loaded. Attempting to load blog content.~%") + (load-content) + (format t "~%Content loaded. Attempting to dump content database.~%") + (cl-store:store *site* db-file) + (format t "~%Content database saved to ~s!~%~%" (namestring db-file)))) + +(main) +(exit) diff --git a/examples/dump_db.sh b/examples/dump_db.sh new file mode 100755 index 0000000..d0bd651 --- /dev/null +++ b/examples/dump_db.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +LISP=sbcl + +## Disclaimer: +## I have not tested that all lisps take the "--load" flag. +## This code might spontaneously combust your whole everything. + +$LISP --load "dump-db.lisp" From 6daa930366395b306d0b6a36aa062be09a8694aa Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 17:18:24 -0400 Subject: [PATCH 10/14] Fix some bugs in the incremental plugin. --- plugins/incremental.lisp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp index 4e30466..83bd1c6 100644 --- a/plugins/incremental.lisp +++ b/plugins/incremental.lisp @@ -4,7 +4,9 @@ (defpackage :coleslaw-incremental (:use :cl) (:import-from :alexandria #:when-let) - (:import-from :coleslaw #:content + (:import-from :coleslaw #:*config* + #:content + #:index #:discover #:get-updated-files #:find-content-by-path @@ -13,8 +15,10 @@ ;; Private #:all-subclasses #:do-subclasses + #:read-content #:construct #:rel-path + #:repo #:update-content-metadata) (:export #:enable)) @@ -36,7 +40,8 @@ (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db"))) (setf coleslaw::*site* (cl-store:restore db-file)) (loop for (status path) in (get-updated-files) - do (update-content status path)) + for file-path = (rel-path (repo *config*) path) + do (update-content status file-path)) (update-content-metadata) (do-subclasses (itype index) ;; Discover's before method will delete the possibly outdated indexes. From 06a1f73ad23f2afef7c9b9da683ded27db92a17f Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 17:24:17 -0400 Subject: [PATCH 11/14] Add a naive performance evaluation to the docs. --- docs/hacking.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/hacking.md b/docs/hacking.md index d1843b2..2cffba6 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -33,7 +33,8 @@ I expect that 3bmd would be the main bottleneck on a larger site. It would be worthwhile to see how well [cl-markdown][clmd] performs as a replacement if this becomes an issue for users though we would lose source highlighting from [colorize][clrz] and should also investigate -[pygments][pyg] as a replacement. +[pygments][pyg] as a replacement. Using the new [incremental][incf] plugin +reduced runtime to 1.36 seconds, almost cutting it in half. ## Core Concepts @@ -272,3 +273,4 @@ simply disabling the indexes may be appropriate for certain users. [clmd]: https://github.com/gwkkwg/cl-markdown [clrz]: https://github.com/redline6561/colorize [pyg]: http://pygments.org/ +[incf]: https://github.com/redline6561/coleslaw/blob/master/plugins/incremental.lisp From 37a1d7ad6af3e95dad0b4e4902829983fbba8f08 Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Tue, 3 Jun 2014 18:13:12 -0400 Subject: [PATCH 12/14] Comment tweaks. --- plugins/incremental.lisp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/incremental.lisp b/plugins/incremental.lisp index 83bd1c6..0a87170 100644 --- a/plugins/incremental.lisp +++ b/plugins/incremental.lisp @@ -30,7 +30,8 @@ ;; the git repo. The rest of coleslaw's operation is "fast enough". ;; ;; Prior to enabling the plugin a user must have a cl-store dump of the -;; database at ~/.coleslaw.db. We should provide a script to generate it. +;; database at ~/.coleslaw.db. There is a dump_db shell script in +;; examples to generate the database dump. ;; ;; We're gonna be a bit dirty here and monkey patch. The compilation model ;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe @@ -43,8 +44,8 @@ for file-path = (rel-path (repo *config*) path) do (update-content status file-path)) (update-content-metadata) + ;; Discover's :before method will delete any possibly outdated indexes. (do-subclasses (itype index) - ;; Discover's before method will delete the possibly outdated indexes. (discover itype)) (cl-store:store coleslaw::*site* db-file))) From 70cad7c7d1c47b91f328286030c4c06d51b5fe4b Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Wed, 4 Jun 2014 11:22:47 -0400 Subject: [PATCH 13/14] More docs, README updates. --- README.md | 15 +++--- docs/plugin-use.md | 115 +++++++++++++++++++++++++++++++++------------ 2 files changed, 94 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 10354ad..653235d 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,29 @@ > drinking coffee, reading, writing, eating chips and salsa. I remember a gentleness > behind the enormous bushy eyebrows and that we called him Coleslaw. - anon -Coleslaw aims to be flexible blog software suitable for replacing a single-user static site compiler such as Jekyll. +Coleslaw aims to be flexible blog software suitable for replacing a single-user static site generator such as [Jekyll](http://jekyllrb.com/). ## Features * Git for storage -* RSS and Atom feeds! -* Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize). +* RSS and Atom feeds +* Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize) * Currently supports: Common Lisp, Emacs Lisp, Scheme, C, C++, Java, Python, Erlang, Haskell, Obj-C, Diff. * A [Plugin API](http://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md) and [**plugins**](http://github.com/redline6561/coleslaw/blob/master/docs/plugin-use.md) for... * Static Pages + * Sitemap generation + * Incremental builds * Analytics via Google * Comments via [Disqus](http://disqus.com/) * Hosting via [Github Pages](https://pages.github.com/), [Heroku](http://heroku.com/), or [Amazon S3](http://aws.amazon.com/s3/) + * [Tweeting](http://twitter.com/) about new posts * Using LaTeX via [Mathjax](http://mathjax.org/) - * Using ReStructured Text + * Writing posts in ReStructured Text * Importing posts from [Wordpress](http://wordpress.org/) - * Sitemap generation * There is also a [Heroku buildpack](https://github.com/jsmpereira/coleslaw-heroku) maintained by Jose Pereira. -* Example sites: + +## Example Sites * [redlinernotes](http://redlinernotes.com/blog/) * [kenan-bolukbasi.log](http://kenanb.com/) * [Nothing Really Matters](http://ironhead.xs4all.nl/) diff --git a/docs/plugin-use.md b/docs/plugin-use.md index be0e7be..5d0c03e 100644 --- a/docs/plugin-use.md +++ b/docs/plugin-use.md @@ -1,83 +1,119 @@ # General Use * Add a list with the plugin name and settings to the ```:plugins``` - section of your [.coleslawrc][config_file]. Plugin settings are described below. + section of your [.coleslawrc][config_file]. Plugin settings are + described below. -* Available plugins are listed below with usage descriptions and config examples. +* Available plugins are listed below with usage descriptions and + config examples. ## Analytics via Google -**Description**: Provides traffic analysis through [Google Analytics](http://www.google.com/analytics/). +**Description**: Provides traffic analysis through + [Google Analytics](http://www.google.com/analytics/). **Example**: `(analytics :tracking-code "google-provided-unique-id")` ## Comments via Disqus -**Description**: Provides comment support through [Disqus](http://www.disqus.com/). +**Description**: Provides comment support through + [Disqus](http://www.disqus.com/). **Example**: `(disqus :shortname "disqus-provided-unique-id")` ## Hosting via Github Pages -**Description**: Allows hosting with CNAMEs via [github-pages](http://pages.github.com/). Parses the host from the `:domain` section of your config by default. Pass in a string to override. +**Description**: Allows hosting with CNAMEs via + [github-pages](http://pages.github.com/). Parses the host from the + `:domain` section of your config by default. Pass in a string to + override. **Example**: `(gh-pages :cname t)` +## Incremental Builds + +**Description**: Primarily a performance enhancement. Caches the + content database between builds with + [cl-store][http://common-lisp.net/project/cl-store/] to avoid + parsing the whole git repo every time. May become default + functionality instead of a plugin at some point. Substantially + reduces runtime for medium to large sites. + +**Example**: `(incremental)` + ## LaTeX via Mathjax -**Description**: Provides LaTeX support through [Mathjax](http://www.mathjax.org/) for posts tagged with "math" and indexes containing such posts. Any text enclosed in $$ will be rendered, for example, ```$$ \lambda \scriptstyle{f}. (\lambda x. (\scriptstyle{f} (x x)) \lambda x. (\scriptstyle{f} (x x))) $$```. +**Description**: Provides LaTeX support through + [Mathjax](http://www.mathjax.org/) for posts tagged with "math" and + indexes containing such posts. Any text enclosed in $$ will be + rendered, for example, ```$$ \lambda \scriptstyle{f}. (\lambda + x. (\scriptstyle{f} (x x)) \lambda x. (\scriptstyle{f} (x x))) + $$```. **Example**: ```(mathjax)``` **Options**: -- `:force`, when non-nil, will force the inclusion of MathJax on all posts. Default value is `nil`. +- `:force`, when non-nil, will force the inclusion of MathJax on all + posts. Default value is `nil`. -- `:location` specifies the location of the `MathJax.js` file. The default value is `"http://cdn.mathjax.org/mathjax/latest/MathJax.js"`. This is useful if you have a local copy of MathJax and want to use that version. +- `:location` specifies the location of the `MathJax.js` file. The + default value is `"http://cdn.mathjax.org/mathjax/latest/MathJax.js"`. + This is useful if you have a local copy of MathJax and want to use that + version. -- `:preset` allows the specification of the config parameter of `MathJax.js`. The default value is `"TeX-AMS-MML_HTMLorMML"`. +- `:preset` allows the specification of the config parameter of + `MathJax.js`. The default value is `"TeX-AMS-MML_HTMLorMML"`. -- `:config` is used as supplementary inline configuration to the `MathJax.Hub.Config ({ ... });`. It is unused by default. +- `:config` is used as supplementary inline configuration to the + `MathJax.Hub.Config ({ ... });`. It is unused by default. ## ReStructuredText -**Description**: Some people really like [ReStructuredText](http://docutils.sourceforge.net/rst.html). Who knows why? But it only took one method to add, so yeah! Just create a post with `format: rst` and the plugin will do the rest. +**Description**: Some people really like + [ReStructuredText](http://docutils.sourceforge.net/rst.html). Who + knows why? But it only took one method to add, so yeah! Just create + a post with `format: rst` and the plugin will do the rest. **Example**: `(rst)` ## S3 Hosting -**Description**: Allows hosting your blog entirely via [Amazon S3](http://aws.amazon.com/s3/). It is suggested you closely follow the relevant [AWS guide](http://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to get the DNS setup correctly. Your `:auth-file` should match that described in the [ZS3 docs](http://www.xach.com/lisp/zs3/#file-credentials). +**Description**: Allows hosting your blog entirely via + [Amazon S3](http://aws.amazon.com/s3/). It is suggested you closely + follow the relevant + [AWS guide](http://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) + to get the DNS setup correctly. Your `:auth-file` should match that + described in the + [ZS3 docs](http://www.xach.com/lisp/zs3/#file-credentials). -**Example**: `(s3 :auth-file "/home/redline/.aws_creds" :bucket "blog.redlinernotes.com")` +**Example**: `(s3 :auth-file "/home/redline/.aws_creds" :bucket + "blog.redlinernotes.com")` ## Sitemap generator -**Description**: This plugin generates a sitemap.xml under the page root, which is useful if you want google to crawl your site. +**Description**: This plugin generates a sitemap.xml under the page + root, which is useful if you want google to crawl your site. **Example**: `(sitemap)` ## Static Pages -**Description**: This plugin allows you to add `.page` files to your repo, that will be rendered to static pages at a designated URL. +**Description**: This plugin allows you to add `.page` files to your + repo, that will be rendered to static pages at a designated URL. **Example**: `(static-pages)` -## Wordpress Importer - -**NOTE**: This plugin really should be rewritten to act as a standalone script. It is designed for one time use and using it through a site config is pretty silly. - -**Description**: Import blog posts from Wordpress using their export tool. Blog entries will be read from the XML and converted into .post files. Afterwards the XML file will be deleted to prevent reimporting. Optionally an `:output` argument may be supplied to the plugin. If provided, it should be a directory in which to store the .post files. Otherwise, the value of `:repo` in your .coleslawrc will be used. - -**Example**: `(import :filepath "/home/redline/redlinernotes-export.timestamp.xml" :output "/home/redlinernotes/blog/")` - -[config_file]: http://github.com/redline6561/coleslaw/blob/master/examples/single-site.coleslawrc - ## Twitter -**Description**: This plugin tweets every time a new post is added to your repo. See Setup for an example of how to get your access token & secret. +**Description**: This plugin tweets every time a new post is added to + your repo. See Setup for an example of how to get your access token + & secret. -**Example**: `(twitter :api-key "" :api-secret "" :access-secret "")` +**Example**: `(twitter :api-key "" + :api-secret "" + :access-token "" + :access-secret "")` **Setup**: - Create a new [twitter app](https://apps.twitter.com/). Take note of the api key & secret. @@ -89,8 +125,8 @@ ;; Use the api key & secret to get a URL where a pin code will be handled to you. (chirp:initiate-authentication - :api-key "D1pMCK17gI10bQ6orBPS0w" - :api-secret "BfkvKNRRMoBPkEtDYAAOPW4s2G9U8Z7u3KAf0dBUA") + :api-key "D1pMCK17gI10bQ6orBPS0w" + :api-secret "BfkvKNRRMoBPkEtDYAAOPW4s2G9U8Z7u3KAf0dBUA") ;; => "https://api.twitter.com/oauth/authorize?oauth_token=cJIw9MJM5HEtQqZKahkj1cPn3m3kMb0BYEp6qhaRxfk" ;; Exchange the pin code for an access token and and access secret. Take note @@ -99,7 +135,26 @@ CL-USER> (chirp:complete-authentication "4173325") ;; => "18403733-bXtuum6qbab1O23ltUcwIk2w9NS3RusUFiuum4D3w" ;; "zDFsFSaLerRz9PEXqhfB0h0FNfUIDgbEe59NIHpRWQbWk" -;; Finally verify the credentials +;; Finally verify the credentials (chirp:account/verify-credentials) # ``` + +## Wordpress Importer + +**NOTE**: This plugin really should be rewritten to act as a + standalone script. It is designed for one time use and using it + through a site config is pretty silly. + +**Description**: Import blog posts from Wordpress using their export + tool. Blog entries will be read from the XML and converted into + .post files. Afterwards the XML file will be deleted to prevent + reimporting. Optionally an `:output` argument may be supplied to the + plugin. If provided, it should be a directory in which to store the + .post files. Otherwise, the value of `:repo` in your .coleslawrc + will be used. + +**Example**: `(import :filepath "/home/redline/redlinernotes-export.timestamp.xml" + :output "/home/redlinernotes/blog/")` + +[config_file]: http://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc From 733a8a82ad82e9a492e04e10b8e03d33edc45c60 Mon Sep 17 00:00:00 2001 From: Brit Butler Date: Wed, 4 Jun 2014 11:40:14 -0400 Subject: [PATCH 14/14] Add one more docs note, call it 0.9.5. --- NEWS.md | 10 +++++++--- coleslaw.asd | 2 +- docs/plugin-use.md | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index a662cdc..db96091 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,10 +1,14 @@ -## Changes for 0.9.5-dev (20xx): +## Changes for 0.9.5 (2014-06-04): -* A Twitter plugin to tweet your new posts. Thanks to @PuercoPop! +* A plugin for Incremental builds, cutting runtime for generating + medium to large sites roughly in half! +* A Twitter plugin to tweet about your new posts. Thanks to @PuercoPop! * Coleslaw now exports a `get-updated-files` function which can be used to get a list of file-status/file-name pairs that were changed in the last git push. There is also an exported `find-content-by-path` - function to retrieve content objects from the above file-name. + function to retrieve content objects from the above file-name. These + were used by both the Twitter and Incremental plugins. +* The usual bugfixes, performance improvements, and documentation tweaks. ## Changes for 0.9.4 (2014-05-05): diff --git a/coleslaw.asd b/coleslaw.asd index 40ab023..71552b4 100644 --- a/coleslaw.asd +++ b/coleslaw.asd @@ -1,7 +1,7 @@ (defsystem #:coleslaw :name "coleslaw" :description "Flexible Lisp Blogware" - :version "0.9.5-dev" + :version "0.9.5" :license "BSD" :author "Brit Butler " :pathname "src/" diff --git a/docs/plugin-use.md b/docs/plugin-use.md index 5d0c03e..7164c61 100644 --- a/docs/plugin-use.md +++ b/docs/plugin-use.md @@ -41,6 +41,10 @@ **Example**: `(incremental)` +**Setup**: +- You must run the `examples/dump_db.sh` script to generate a database dump + for your site before enabling the incremental plugin. + ## LaTeX via Mathjax **Description**: Provides LaTeX support through