Преглед изворни кода

Merge pull request #51 from redline6561/experimental

Release: 0.9.5!
Brit Butler пре 11 година
родитељ
комит
4896f31596
12 измењених фајлова са 259 додато и 52 уклоњено
  1. 7 3
      NEWS.md
  2. 9 6
      README.md
  3. 1 1
      coleslaw.asd
  4. 8 2
      docs/hacking.md
  5. 89 30
      docs/plugin-use.md
  6. 17 0
      examples/dump-db.lisp
  7. 9 0
      examples/dump_db.sh
  8. 85 0
      plugins/incremental.lisp
  9. 16 0
      plugins/parallel.lisp
  10. 4 0
      src/documents.lisp
  11. 4 3
      src/packages.lisp
  12. 10 7
      src/util.lisp

+ 7 - 3
NEWS.md

@@ -1,10 +1,14 @@
1
-## Changes for 0.9.5-dev (20xx):
1
+## Changes for 0.9.5 (2014-06-04):
2 2
 
3
-* A Twitter plugin to tweet your new posts. Thanks to @PuercoPop!
3
+* A plugin for Incremental builds, cutting runtime for generating
4
+  medium to large sites roughly in half!
5
+* A Twitter plugin to tweet about your new posts. Thanks to @PuercoPop!
4 6
 * Coleslaw now exports a `get-updated-files` function which can be
5 7
   used to get a list of file-status/file-name pairs that were changed
6 8
   in the last git push. There is also an exported `find-content-by-path`
7
-  function to retrieve content objects from the above file-name.
9
+  function to retrieve content objects from the above file-name. These
10
+  were used by both the Twitter and Incremental plugins.
11
+* The usual bugfixes, performance improvements, and documentation tweaks.
8 12
 
9 13
 ## Changes for 0.9.4 (2014-05-05):
10 14
 

+ 9 - 6
README.md

@@ -7,26 +7,29 @@
7 7
 > drinking coffee, reading, writing, eating chips and salsa. I remember a gentleness
8 8
 > behind the enormous bushy eyebrows and that we called him Coleslaw. - anon
9 9
 
10
-Coleslaw aims to be flexible blog software suitable for replacing a single-user static site compiler such as Jekyll.
10
+Coleslaw aims to be flexible blog software suitable for replacing a single-user static site generator such as [Jekyll](http://jekyllrb.com/).
11 11
 
12 12
 ## Features
13 13
 * Git for storage
14
-* RSS and Atom feeds!
15
-* Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize).
14
+* RSS and Atom feeds
15
+* Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize)
16 16
   * Currently supports: Common Lisp, Emacs Lisp, Scheme, C, C++, Java, Python, Erlang, Haskell, Obj-C, Diff.
17 17
 
18 18
 * 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...
19 19
   * Static Pages
20
+  * Sitemap generation
21
+  * Incremental builds
20 22
   * Analytics via Google
21 23
   * Comments via [Disqus](http://disqus.com/)
22 24
   * Hosting via [Github Pages](https://pages.github.com/), [Heroku](http://heroku.com/), or [Amazon S3](http://aws.amazon.com/s3/)
25
+  * [Tweeting](http://twitter.com/) about new posts
23 26
   * Using LaTeX via [Mathjax](http://mathjax.org/)
24
-  * Using ReStructured Text
27
+  * Writing posts in ReStructured Text
25 28
   * Importing posts from [Wordpress](http://wordpress.org/)
26
-  * Sitemap generation
27 29
 
28 30
 * There is also a [Heroku buildpack](https://github.com/jsmpereira/coleslaw-heroku) maintained by Jose Pereira.
29
-* Example sites: 
31
+
32
+## Example Sites
30 33
   * [redlinernotes](http://redlinernotes.com/blog/)
31 34
   * [kenan-bolukbasi.log](http://kenanb.com/)
32 35
   * [Nothing Really Matters](http://ironhead.xs4all.nl/)

+ 1 - 1
coleslaw.asd

@@ -1,7 +1,7 @@
1 1
 (defsystem #:coleslaw
2 2
   :name "coleslaw"
3 3
   :description "Flexible Lisp Blogware"
4
-  :version "0.9.5-dev"
4
+  :version "0.9.5"
5 5
   :license "BSD"
6 6
   :author "Brit Butler <redline6561@gmail.com>"
7 7
   :pathname "src/"

+ 8 - 2
docs/hacking.md

@@ -33,7 +33,8 @@ I expect that 3bmd would be the main bottleneck on a larger site. It
33 33
 would be worthwhile to see how well [cl-markdown][clmd] performs as
34 34
 a replacement if this becomes an issue for users though we would lose
35 35
 source highlighting from [colorize][clrz] and should also investigate
36
-[pygments][pyg] as a replacement.
36
+[pygments][pyg] as a replacement. Using the new [incremental][incf] plugin
37
+reduced runtime to 1.36 seconds, almost cutting it in half.
37 38
 
38 39
 ## Core Concepts
39 40
 
@@ -134,7 +135,7 @@ be seamlessly picked up by *coleslaw* and included on the rendered site.
134 135
 All current Content Types and Indexes implement the protocol faithfully.
135 136
 It consists of 2 "class" methods, 2 instance methods, and an invariant.
136 137
 
137
-There are also 4 helper functions provided that should prove useful in
138
+There are also 5 helper functions provided that should prove useful in
138 139
 implementing new content types.
139 140
 
140 141
 
@@ -191,6 +192,10 @@ eql-specializing on the class, e.g.
191 192
   unique. Such a hash collision represents content on the site being
192 193
   shadowed/overwritten. This should be used in your `discover` method.
193 194
 
195
+- `delete-document`: Remove a document from *coleslaw*'s in-memory
196
+  database. This is currently only used by the incremental compilation
197
+  plugin.
198
+
194 199
 - `write-document`: Write the document out to disk as HTML. It takes
195 200
   an optional template name and render-args to pass to the template.
196 201
   This should be used in your `publish` method.
@@ -268,3 +273,4 @@ simply disabling the indexes may be appropriate for certain users.
268 273
 [clmd]: https://github.com/gwkkwg/cl-markdown
269 274
 [clrz]: https://github.com/redline6561/colorize
270 275
 [pyg]: http://pygments.org/
276
+[incf]: https://github.com/redline6561/coleslaw/blob/master/plugins/incremental.lisp

+ 89 - 30
docs/plugin-use.md

@@ -1,83 +1,123 @@
1 1
 # General Use
2 2
 
3 3
 * Add a list with the plugin name and settings to the ```:plugins```
4
-  section of your [.coleslawrc][config_file]. Plugin settings are described below.
4
+  section of your [.coleslawrc][config_file]. Plugin settings are
5
+  described below.
5 6
 
6
-* Available plugins are listed below with usage descriptions and config examples.
7
+* Available plugins are listed below with usage descriptions and
8
+  config examples.
7 9
 
8 10
 ## Analytics via Google
9 11
 
10
-**Description**: Provides traffic analysis through [Google Analytics](http://www.google.com/analytics/).
12
+**Description**: Provides traffic analysis through
13
+  [Google Analytics](http://www.google.com/analytics/).
11 14
 
12 15
 **Example**: `(analytics :tracking-code "google-provided-unique-id")`
13 16
 
14 17
 ## Comments via Disqus
15 18
 
16
-**Description**: Provides comment support through [Disqus](http://www.disqus.com/).
19
+**Description**: Provides comment support through
20
+  [Disqus](http://www.disqus.com/).
17 21
 
18 22
 **Example**: `(disqus :shortname "disqus-provided-unique-id")`
19 23
 
20 24
 ## Hosting via Github Pages
21 25
 
22
-**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.
26
+**Description**: Allows hosting with CNAMEs via
27
+  [github-pages](http://pages.github.com/). Parses the host from the
28
+  `:domain` section of your config by default. Pass in a string to
29
+  override.
23 30
 
24 31
 **Example**: `(gh-pages :cname t)`
25 32
 
33
+## Incremental Builds
34
+
35
+**Description**: Primarily a performance enhancement. Caches the
36
+  content database between builds with
37
+  [cl-store][http://common-lisp.net/project/cl-store/] to avoid
38
+  parsing the whole git repo every time. May become default
39
+  functionality instead of a plugin at some point. Substantially
40
+  reduces runtime for medium to large sites.
41
+
42
+**Example**: `(incremental)`
43
+
44
+**Setup**:
45
+- You must run the `examples/dump_db.sh` script to generate a database dump
46
+  for your site before enabling the incremental plugin.
47
+
26 48
 ## LaTeX via Mathjax
27 49
 
28
-**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))) $$```.
50
+**Description**: Provides LaTeX support through
51
+  [Mathjax](http://www.mathjax.org/) for posts tagged with "math" and
52
+  indexes containing such posts. Any text enclosed in $$ will be
53
+  rendered, for example, ```$$ \lambda \scriptstyle{f}. (\lambda
54
+  x. (\scriptstyle{f} (x x)) \lambda x. (\scriptstyle{f} (x x)))
55
+  $$```.
29 56
 
30 57
 **Example**: ```(mathjax)```
31 58
 
32 59
 **Options**:
33 60
 
34
-- `:force`, when non-nil, will force the inclusion of MathJax on all posts.  Default value is `nil`.
61
+- `:force`, when non-nil, will force the inclusion of MathJax on all
62
+  posts.  Default value is `nil`.
35 63
 
36
-- `: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.
64
+- `:location` specifies the location of the `MathJax.js` file.  The
65
+  default value is `"http://cdn.mathjax.org/mathjax/latest/MathJax.js"`.
66
+  This is useful if you have a local copy of MathJax and want to use that
67
+  version.
37 68
 
38
-- `:preset` allows the specification of the config parameter of `MathJax.js`.  The default value is `"TeX-AMS-MML_HTMLorMML"`.
69
+- `:preset` allows the specification of the config parameter of
70
+  `MathJax.js`.  The default value is `"TeX-AMS-MML_HTMLorMML"`.
39 71
 
40
-- `:config` is used as supplementary inline configuration to the `MathJax.Hub.Config ({ ... });`. It is unused by default.
72
+- `:config` is used as supplementary inline configuration to the
73
+  `MathJax.Hub.Config ({ ... });`. It is unused by default.
41 74
 
42 75
 ## ReStructuredText
43 76
 
44
-**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.
77
+**Description**: Some people really like
78
+  [ReStructuredText](http://docutils.sourceforge.net/rst.html). Who
79
+  knows why? But it only took one method to add, so yeah! Just create
80
+  a post with `format: rst` and the plugin will do the rest.
45 81
 
46 82
 **Example**: `(rst)`
47 83
 
48 84
 ## S3 Hosting
49 85
 
50
-**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).
86
+**Description**: Allows hosting your blog entirely via
87
+  [Amazon S3](http://aws.amazon.com/s3/). It is suggested you closely
88
+  follow the relevant
89
+  [AWS guide](http://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html)
90
+  to get the DNS setup correctly. Your `:auth-file` should match that
91
+  described in the
92
+  [ZS3 docs](http://www.xach.com/lisp/zs3/#file-credentials).
51 93
 
52
-**Example**: `(s3 :auth-file "/home/redline/.aws_creds" :bucket "blog.redlinernotes.com")`
94
+**Example**: `(s3 :auth-file "/home/redline/.aws_creds" :bucket
95
+  "blog.redlinernotes.com")`
53 96
 
54 97
 ## Sitemap generator
55 98
 
56
-**Description**: This plugin generates a sitemap.xml under the page root, which is useful if you want google to crawl your site.
99
+**Description**: This plugin generates a sitemap.xml under the page
100
+  root, which is useful if you want google to crawl your site.
57 101
 
58 102
 **Example**: `(sitemap)`
59 103
 
60 104
 ## Static Pages
61 105
 
62
-**Description**: This plugin allows you to add `.page` files to your repo, that will be rendered to static pages at a designated URL.
106
+**Description**: This plugin allows you to add `.page` files to your
107
+  repo, that will be rendered to static pages at a designated URL.
63 108
 
64 109
 **Example**: `(static-pages)`
65 110
 
66
-## Wordpress Importer
67
-
68
-**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.
69
-
70
-**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.
71
-
72
-**Example**: `(import :filepath "/home/redline/redlinernotes-export.timestamp.xml" :output "/home/redlinernotes/blog/")`
73
-
74
-[config_file]: http://github.com/redline6561/coleslaw/blob/master/examples/single-site.coleslawrc
75
-
76 111
 ## Twitter
77 112
 
78
-**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.
113
+**Description**: This plugin tweets every time a new post is added to
114
+  your repo. See Setup for an example of how to get your access token
115
+  & secret.
79 116
 
80
-**Example**: `(twitter :api-key "<api-key>" :api-secret "<api-seret" :access-token "<access-token>" :access-secret "<access-secret>")`
117
+**Example**: `(twitter :api-key "<api-key>"
118
+                       :api-secret "<api-secret>"
119
+                       :access-token "<access-token>"
120
+                       :access-secret "<access-secret>")`
81 121
 
82 122
 **Setup**:
83 123
 - Create a new [twitter app](https://apps.twitter.com/). Take note of the api key & secret.
@@ -89,8 +129,8 @@
89 129
 
90 130
 ;; Use the api key & secret to get a URL where a pin code will be handled to you.
91 131
 (chirp:initiate-authentication
92
- :api-key "D1pMCK17gI10bQ6orBPS0w"
93
- :api-secret "BfkvKNRRMoBPkEtDYAAOPW4s2G9U8Z7u3KAf0dBUA")
132
+  :api-key "D1pMCK17gI10bQ6orBPS0w"
133
+  :api-secret "BfkvKNRRMoBPkEtDYAAOPW4s2G9U8Z7u3KAf0dBUA")
94 134
 ;; => "https://api.twitter.com/oauth/authorize?oauth_token=cJIw9MJM5HEtQqZKahkj1cPn3m3kMb0BYEp6qhaRxfk"
95 135
 
96 136
 ;; Exchange the pin code for an access token and and access secret. Take note
@@ -99,7 +139,26 @@ CL-USER> (chirp:complete-authentication "4173325")
99 139
 ;; => "18403733-bXtuum6qbab1O23ltUcwIk2w9NS3RusUFiuum4D3w"
100 140
 ;;    "zDFsFSaLerRz9PEXqhfB0h0FNfUIDgbEe59NIHpRWQbWk"
101 141
 
102
-;; Finally verify the credentials 
142
+;; Finally verify the credentials
103 143
 (chirp:account/verify-credentials)
104 144
 #<CHIRP-OBJECTS:USER PuercoPop #18405433>
105 145
 ```
146
+
147
+## Wordpress Importer
148
+
149
+**NOTE**: This plugin really should be rewritten to act as a
150
+  standalone script. It is designed for one time use and using it
151
+  through a site config is pretty silly.
152
+
153
+**Description**: Import blog posts from Wordpress using their export
154
+  tool. Blog entries will be read from the XML and converted into
155
+  .post files. Afterwards the XML file will be deleted to prevent
156
+  reimporting. Optionally an `:output` argument may be supplied to the
157
+  plugin. If provided, it should be a directory in which to store the
158
+  .post files. Otherwise, the value of `:repo` in your .coleslawrc
159
+  will be used.
160
+
161
+**Example**: `(import :filepath "/home/redline/redlinernotes-export.timestamp.xml"
162
+                      :output "/home/redlinernotes/blog/")`
163
+
164
+[config_file]: http://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc

+ 17 - 0
examples/dump-db.lisp

@@ -0,0 +1,17 @@
1
+(eval-when (:compile-toplevel :load-toplevel :execute)
2
+  (ql:quickload '(coleslaw cl-store)))
3
+
4
+(in-package :coleslaw)
5
+
6
+(defun main ()
7
+  (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db")))
8
+    (format t "~%~%Coleslaw loaded. Attempting to load config file.~%")
9
+    (load-config "")
10
+    (format t "~%Config loaded. Attempting to load blog content.~%")
11
+    (load-content)
12
+    (format t "~%Content loaded. Attempting to dump content database.~%")
13
+    (cl-store:store *site* db-file)
14
+    (format t "~%Content database saved to ~s!~%~%" (namestring db-file))))
15
+
16
+(main)
17
+(exit)

+ 9 - 0
examples/dump_db.sh

@@ -0,0 +1,9 @@
1
+#!/bin/sh
2
+
3
+LISP=sbcl
4
+
5
+## Disclaimer:
6
+## I have not tested that all lisps take the "--load" flag.
7
+## This code might spontaneously combust your whole everything.
8
+
9
+$LISP --load "dump-db.lisp"

+ 85 - 0
plugins/incremental.lisp

@@ -0,0 +1,85 @@
1
+(eval-when (:compile-toplevel :load-toplevel)
2
+  (ql:quickload 'cl-store))
3
+
4
+(defpackage :coleslaw-incremental
5
+  (:use :cl)
6
+  (:import-from :alexandria #:when-let)
7
+  (:import-from :coleslaw #:*config*
8
+                          #:content
9
+                          #:index
10
+                          #:discover
11
+                          #:get-updated-files
12
+                          #:find-content-by-path
13
+                          #:add-document
14
+                          #:delete-document
15
+                          ;; Private
16
+                          #:all-subclasses
17
+                          #:do-subclasses
18
+                          #:read-content
19
+                          #:construct
20
+                          #:rel-path
21
+                          #:repo
22
+                          #:update-content-metadata)
23
+  (:export #:enable))
24
+
25
+(in-package :coleslaw-incremental)
26
+
27
+;; In contrast to the original incremental plans, full of shoving state into
28
+;; the right place by hand and avoiding writing pages to disk that hadn't
29
+;; changed, the new plan is to only avoid redundant parsing of content in
30
+;; the git repo. The rest of coleslaw's operation is "fast enough".
31
+;;
32
+;;   Prior to enabling the plugin a user must have a cl-store dump of the
33
+;;   database at ~/.coleslaw.db. There is a dump_db shell script in
34
+;;   examples to generate the database dump.
35
+;;
36
+;; We're gonna be a bit dirty here and monkey patch. The compilation model
37
+;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe
38
+;; we'll settle on an interface.
39
+
40
+(defun coleslaw::load-content ()
41
+  (let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db")))
42
+    (setf coleslaw::*site* (cl-store:restore db-file))
43
+    (loop for (status path) in (get-updated-files)
44
+       for file-path = (rel-path (repo *config*) path)
45
+       do (update-content status file-path))
46
+    (update-content-metadata)
47
+    ;; Discover's :before method will delete any possibly outdated indexes.
48
+    (do-subclasses (itype index)
49
+      (discover itype))
50
+    (cl-store:store coleslaw::*site* db-file)))
51
+
52
+(defun update-content (status path)
53
+  (cond ((string= "D" status) (process-change :deleted path))
54
+        ((string= "M" status) (process-change :modified path))
55
+        ((string= "A" status) (process-change :added path))))
56
+
57
+(defgeneric process-change (status path &key &allow-other-keys)
58
+  (:documentation "Updates the database as needed for the STATUS change to PATH.")
59
+  (:method :around (status path &key)
60
+    (let ((extension (pathname-type path))
61
+          (ctypes (all-subclasses (find-class 'content))))
62
+      ;; This feels way too clever. I wish I could think of a better option.
63
+      (flet ((class-name-p (x class)
64
+               (string-equal x (symbol-name (class-name class)))))
65
+        ;; If the updated file's extension doesn't match one of our content types,
66
+        ;; we don't need to mess with it at all. Otherwise, since the class is
67
+        ;; annoyingly tricky to determine, pass it along.
68
+        (when-let (ctype (find extension ctypes :test #'class-name-p))
69
+          (call-next-method status path :ctype ctype))))))
70
+
71
+(defmethod process-change ((status (eql :deleted)) path &key)
72
+  (let ((old (find-content-by-path path)))
73
+    (delete-document old)))
74
+
75
+(defmethod process-change ((status (eql :modified)) path &key ctype)
76
+  (let ((old (find-content-by-path path))
77
+        (new (construct ctype (read-content path))))
78
+    (delete-document old)
79
+    (add-document new)))
80
+
81
+(defmethod process-change ((status (eql :added)) path &key ctype)
82
+  (let ((new (construct ctype (read-content path))))
83
+    (add-document new)))
84
+
85
+(defun enable ())

+ 16 - 0
plugins/parallel.lisp

@@ -0,0 +1,16 @@
1
+(eval-when (:compile-toplevel :load-toplevel)
2
+  (ql:quickload 'lparallel))
3
+
4
+(defpackage :coleslaw-parallel
5
+  (:use :cl)
6
+  (:export #:enable))
7
+
8
+(in-package :coleslaw-parallel)
9
+
10
+;; TODO: The bulk of the speedup here should come from parallelizing discover.
11
+;; Publish will also benefit. Whether it's better to spin off threads for each
12
+;; content type/index type or the operations *within* discover/publish is not
13
+;; known, the higher granularity of doing it at the iterating over types level
14
+;; is certainly easier to prototype though.
15
+
16
+(defun enable ())

+ 4 - 0
src/documents.lisp

@@ -51,6 +51,10 @@
51 51
         (error "There is already an existing document with the url ~a" url)
52 52
         (setf (gethash url *site*) document))))
53 53
 
54
+(defun delete-document (document)
55
+  "Given a DOCUMENT, delete it from the in-memory database."
56
+  (remhash (page-url document) *site*))
57
+
54 58
 (defun write-document (document &optional theme-fn &rest render-args)
55 59
   "Write the given DOCUMENT to disk as HTML. If THEME-FN is present,
56 60
 use it as the template passing any RENDER-ARGS."

+ 4 - 3
src/packages.lisp

@@ -25,11 +25,12 @@
25 25
            #:get-updated-files
26 26
            #:theme-fn
27 27
            ;; The Document Protocol
28
-           #:add-document
29
-           #:find-all
30
-           #:purge-all
31 28
            #:discover
32 29
            #:publish
33 30
            #:page-url
34 31
            #:render
32
+           #:find-all
33
+           #:purge-all
34
+           #:add-document
35
+           #:delete-document
35 36
            #:write-document))

+ 10 - 7
src/util.lisp

@@ -4,16 +4,19 @@
4 4
   "Create an instance of CLASS-NAME with the given ARGS."
5 5
   (apply 'make-instance class-name args))
6 6
 
7
+;; Thanks to bknr-web for this bit of code.
8
+(defun all-subclasses (class)
9
+  "Return a list of all the subclasses of CLASS."
10
+  (let ((subclasses (closer-mop:class-direct-subclasses class)))
11
+    (append subclasses (loop for subclass in subclasses
12
+                          nconc (all-subclasses subclass)))))
13
+
7 14
 (defmacro do-subclasses ((var class) &body body)
8 15
   "Iterate over the subclasses of CLASS performing BODY with VAR
9 16
 lexically bound to the current subclass."
10
-  (alexandria:with-gensyms (klasses all-subclasses)
11
-    `(labels ((,all-subclasses (class)
12
-                (let ((subclasses (closer-mop:class-direct-subclasses class)))
13
-                  (append subclasses (loop for subclass in subclasses
14
-                                        nconc (,all-subclasses subclass))))))
15
-       (let ((,klasses (,all-subclasses (find-class ',class))))
16
-         (loop for ,var in ,klasses do ,@body)))))
17
+  (alexandria:with-gensyms (klasses)
18
+    `(let ((,klasses (all-subclasses (find-class ',class))))
19
+       (loop for ,var in ,klasses do ,@body))))
17 20
 
18 21
 (defmacro do-files ((var path &optional extension) &body body)
19 22
   "For each file under PATH, run BODY. If EXTENSION is provided, only run