It's probably a consequence of my recent study of functional programming—first with Haskell, and then, to a lesser extent, with Emacs Lisp itself—that I structured most of the important bits of org-blog as data transformations.
Really, one of the fundamental insights I gained while teaching myself Haskell was that it is extremely empowering to be able to know that when you call a function, nothing should be altered—that immutability really does increase your confidence that your program is doing what you think.
Now Emacs Lisp doesn't have Haskell's immutability or purity or any of those things—in fact, I have been a little dismayed to discover how many basic operations in Emacs Lisp mutate their arguments in one way or another—but it has most of the facilities you need to be able to program in that fashion.
So, with that in mind, the first step in actually posting things to a
WordPress blog is going to be to get our post
structure into a format
that we can feed to WordPress's XML-RPC interface.
Step one in that process is to define a correspondence of some sort
between a property in a post
structure and a name in a WordPress
structure:
(defconst org-blog-wp-alist
(list (cons :category "category")
(cons :content "post_content")
(cons :date "post_date_gmt")
(cons :excerpt "post_excerpt")
(cons :id "post_id")
(cons :link "link")
(cons :name "post_name")
(cons :parent "post_parent")
(cons :status "post_status")
(cons :tags "post_tag")
(cons :title "post_title")
(cons :type "post_type")))
You will have probably realized by now that I go back and forth between
quoting things like this and constructing them with list
and cons
. I
will go back and make things consistent eventually.
So this table is used by the actual transformation function, which looks like this:
(defun org-blog-post-to-wp (post)
"Transform a post into a structure for submitting to WordPress.
This is largely about mapping tag names, though the handling of
`category' and `tags' is little more complex as the WordPress API
now groups them as `taxonomies', and requires a hierarchical
structure to differentiate them.
For convenience in testing and inspection, the resulting alist is
sorted."
(sort
(reduce
'(lambda (wp new)
(let ((k (car new))
(v (cdr new)))
(when v
(cond ((eq :category k)
(setq wp (org-blog-post-to-wp-add-taxonomy wp "category" v)))
((eq :date k)
;; Convert to GMT by adding seconds offset
(push (cons "post_date_gmt" (list :datetime
(time-add (car v)
(seconds-to-time (- (car (current-time-zone)))))))
wp))
((eq :tags k)
(setq wp (org-blog-post-to-wp-add-taxonomy wp "post_tag" v)))
((eq :title k)
(push (cons "post_title" (or v "No Title")) wp))
((assq k org-blog-wp-alist)
(push (cons (cdr (assq k org-blog-wp-alist)) v) wp))
)))
wp)
post :initial-value nil)
'(lambda (a b)
(string< (car a) (car b)))))
Conceptually, this is simple—we're running reduce
over the list of
fields in the post
structure, and "accumulating" them into the wp
parameter we declare for our lambda
.
The unfortunate complexity comes from the fact that while many fields
can simply be copied over, a few require significant munging (:date
is
the biggie, though we default our :title
as well—which should
probably happen in the post-to-blog transformation now that I think on
it), and the :category
and :tags
fields require a significant chunk
of code to handle because instead of having separate fields for each in
its XML-RPC interface, WordPress places the two fields under a
higher-level structure called taxonomies
—and we don't want to have
an empty entry if a post is lacking either or both of the fields.
Thus we have the org-blog-post-to-wp-add-taxonomy
function:
(defun org-blog-post-to-wp-add-taxonomy (wp taxonomy entries)
"Handle adding taxonomy items to a WordPress struct.
The fiddly part is making sure that the sublists are sorted, for
convenience in testing and inspection."
(let* ((terms (assoc "terms_names" wp))
(existing (cdr terms))
(struct (cons taxonomy entries)))
(if existing
(progn
(push struct existing)
(setcdr terms (sort
existing
'(lambda (a b)
(string< (car a) (car b))))))
(push (list "terms_names" struct) wp))
wp))
Simply put, if the terms_names
field already exists, we have to add
our new "taxonomy" entry to it, but if it doesn't exist, we need to
create it. This is fiddlier than I would like it to be. I actually
posted
a
question on StackOverflow to see if there was a cleaner way; the
consensus was that although there were other strategies, there wasn't
anything a whole lot cleaner.
Now this is the place I put on my mea culpa hat, because as I
re-examine these two functions, I see one thing I should be doing to
make it cleaner—attaching the transformation functions for each field
to their entries in the org-blog-wp-alist
. This would have several
beneficial effects: the transformations would be closely associated with
their related fields (thus easily kept up to date) and our reduce
becomes much less cluttered—just a function invocation per field.
Also, instead of doing all those setq
invocations, I should let the
result of push
(and other functions) simply be the result of the
cond
, which becomes the result of the lambda
, which is the same as
having wp
be the last sexp
in the lambda.
But before I make that change, I want to add a test to make sure that I don't break anything:
(ert-deftest ob-test-posts-and-wp ()
"Transfer from buffers to posts and back again"
(let ((post1-struct '((:blog . "t1b")
(:category "t1c1" "t1c2")
(:content . "\n<p>Test 1 Content\n</p>")
(:date (20738 4432 0 0))
(:excerpt . "t1e")
(:id . "1")
(:link . "http://example.com/")
(:name . "t1n")
(:parent . "0")
(:status . "publish")
(:tags "t1k1" "t1k2" "t1k3")
(:title . "Test 1 Title")
(:type . "post")))
(post1-wp-input '(("link" . "http://example.com/")
("post_content" . "\n<p>Test 1 Content\n</p>")
("post_date_gmt" :datetime (20738 18832 0 0))
("post_excerpt" . "t1e")
("post_id" . "1")
("post_name" . "t1n")
("post_parent" . "0")
("post_status" . "publish")
("post_title" . "Test 1 Title")
("post_type" . "post")
("terms_names"
("category" "t1c1" "t1c2")
("post_tag" "t1k1" "t1k2" "t1k3")))))
(should (equal (org-blog-post-to-wp post1-struct) post1-wp-input))))
And, with that done, in fact, I'm not going to make any changes right at
the moment—I really want to figure out a sensible way to unify all
of my post
-transformation tables in one place, which is a somewhat
more ambitious change. So for the moment I'll note the desire in a
FIXME
comment, and move on. Tomorrow we'll look at transforming from
the structure WordPress outputs back into a post.