Last time, we looked at what was involved in extracting the information
we wanted from our buffer and putting it into a post
structure. Today
we'll look at the complimentary operation of merging the data from a
post
into a buffer.
This is a very important part of the process—there are certain things
that we must record, like the id
that the post is assigned by the
blogging software, if we're going to be able to really maintain the blog
from within org-blog. While it would be very simple to skip this part,
imagine if, once you had posted an entry for the first time, you had to
log into your site in order to edit it? That would be a failing user
experience. So, merge we must.
Really, though, the process is fairly straightforward. First, though, we
want to establish a mapping between the property used within our post
structure, and the property name (in the org-mode
sense) that is used
within the buffer:
(defconst mapping (list (cons :blog "POST_BLOG")
(cons :category "POST_CATEGORY")
(cons :date "DATE")
(cons :excerpt "DESCRIPTION")
(cons :id "POST_ID")
(cons :link "POST_LINK")
(cons :name "POST_NAME")
(cons :parent "POST_PARENT")
(cons :status "POST_STATUS")
(cons :tags "KEYWORDS")
(cons :title "TITLE")
(cons :type "POST_TYPE")))
Looking at this again with fresh eyes makes me realize that this data structure is going to get refactored before too long. As an example of why, let's look back at a piece of yesterday's code:
'(("POST_BLOG" :blog)
("POST_CATEGORY" :category)
("POST_ID" :id)
("POST_LINK" :link)
("POST_NAME" :name)
("POST_PARENT" :parent)
("POST_STATUS" :status)
("POST_TYPE" :type))
The fact that these two bits of structure are largely redundant should be pretty obvious. And I do actually have a plan for refactoring this (and a few other things) in a way that I think will clean up a lot of code. But I want to get the first version out before I worry about that too much—what I have is working, and I'd rather have people using it.
Anyway, that mapping
structure is used in the function that actually
update the buffer. The idea is pretty simple, really—pull out the
current post
values in the buffer, then iterate over the new
values, making sure they're formatted correctly, then examining the
current value. If it's nil
, insert the new value at the head of the
buffer, otherwise compare with the exiting value, and When they differ,
find the pertinent property definition and update it.
To make sure that stuff goes in with a semblance of order, we sort a copy of the list before we start iterating—though, in truth, this doesn't work as well as I'd like because we're sorting on the property name, but inserting the property string, which may have "POST_" included. But that's for another refactoring.
(defun org-blog-buffer-merge-post (merge)
"Merge a post into a buffer.
Given a post structure (presumably returned from the server),
update the buffer to reflect the values it contains."
(save-excursion
(save-restriction
;; Get the current values
(let ((current (org-blog-buffer-extract-post))) (ref:extract)
(mapc (ref:iterate)
(lambda (item)
(let ((k (car item))
(v (cdr item))
val existing)
(when (cdr (assq k mapping))
(setq val (cond ((eq v nil) (ref:format)
(print "setting val to nil")
nil)
((eq k :date)
(format-time-string "[%Y-%m-%d %a %H:%M]" (car v)))
((listp v)
(mapconcat 'identity v ", "))
((stringp v)
v)
(t
"default")))
(goto-char (point-min))
;; (print (format "Comparison for %s is %s against %s" k v (cdr (assq k current))))
(cond
;; Inserting a new keyword
((eq (cdr (assq k current)) nil) (ref:new)
(when val
(insert (concat "#+" (cdr (assq k mapping)) ": " val "\n"))))
;; Updating an existing keyword
((not (equal (cdr (assq k current)) val)) (ref:update)
(let ((re (org-make-options-regexp (list (cdr (assq k mapping))) nil))
(case-fold-search t))
(re-search-forward re nil t)
(replace-match (concat "#+" (cdr (assq k mapping)) ": " val) t t)))))))
;; Reverse sort fields to insert alphabetically
(sort (ref:sort)
(copy-alist merge)
'(lambda (a b)
(string< (car b) (car a)))))))))
When you come down to it, the process really is simple enough. The
refactoring I envision is to create a table of all our post properties
and the processes that need to be run to convert from post
to buffer
and back again, so that this routine becomes much more
straightforward—map over each item, apply its formatter, see if it's
new and/or matches and behave appropriately. This could also be used in
the extraction routine.
Anyway, I'm only going to show the last test, where we actually
round-trip our structure. We create a temporary buffer, merge in a
post
structure, then extract a post
from the resulting buffer,
compare it against what we expect, then merge it back in a second time,
and make sure that it matches again.
(ert-deftest ob-test-merge-round-trip ()
"Try merging a full post into a full buffer, and make sure
you get the same thing out."
(with-temp-buffer
(let ((post-string "#+POST_BLOG: t2b
#+POST_CATEGORY: t2c1, t2c2
#+DATE: [2013-01-25 Fri 00:00]
#+DESCRIPTION: t2e
#+POST_ID: 1
#+POST_LINK: http://example.com/
#+POST_NAME: t2n
#+POST_PARENT: 0
#+POST_STATUS: publish
#+KEYWORDS: t2k1, t2k2, t2k3
#+TITLE: Test 2 Title
#+POST_TYPE: post
")
(post-struct '((:blog . "t2b")
(:category "t2c1" "t2c2")
(:content . "\n")
(:date (20738 4432))
(:excerpt . "t2e")
(:id . "1")
(:link . "http://example.com/")
(:name . "t2n")
(:parent . "0")
(:status . "publish")
(:tags "t2k1" "t2k2" "t2k3")
(:title . "Test 2 Title")
(:type . "post"))))
(org-blog-buffer-merge-post post-struct)
(should (equal (buffer-string) post-string))
(should (equal (org-blog-buffer-extract-post) post-struct))
(org-blog-buffer-merge-post post-struct)
(should (equal (buffer-string) post-string)))))