July 20, 2010

Bulk view

Using org2blog to publish Org-mode subtrees

This patch modifies punchagan’s org2blog to allow you to publish an Org subtree with M-x org2blog-post-subtree. It posts a draft by default, and publishes the post if you call it with C-u M-x org2blog-post-subtree. It gets the posting date from SCHEDULED, DEADLINE, active or inactive timestamps, or the Post Date property, and lets you use tags as categories or use a separate Categories property. It inherits tags from parent headings, too. It picks up the title from the subtree heading or uses the Title property.

Patch

diff --git a/org2blog.el b/org2blog.el
index dc88291..b95caba 100644
--- a/org2blog.el
+++ b/org2blog.el
@@ -77,6 +77,11 @@
   :group 'org2blog 
   :type 'string)
 
+(defcustom org2blog-use-tags-as-categories nil
+  "Non-nil means assign :tags: to WordPress categories instead."
+  :group 'org2blog
+  :type 'boolean)
+
 (defvar org2blog-categories-list nil 
   "List of weblog categories")
 
@@ -433,4 +438,143 @@
        (goto-char current-pos)
        (command-execute (lookup-key org-mode-map (kbd "C-c t")))))))
 
+(defun org2blog-create-categories (categories)
+  "Create unknown CATEGORIES."
+  (mapcar
+   (lambda (cat)
+     (if (and (not (member cat org2blog-categories-list))
+              (y-or-n-p (format "Create %s category? " cat)))
+         (wp-new-category org2blog-server-xmlrpc-url
+                          org2blog-server-userid
+                          (org2blog-password)
+                          org2blog-server-blogid
+                          cat)))
+   categories))
+
+(defun org2blog-password ()
+  "Get password or prompt if needed."
+  (or org2blog-server-pass
+      (setq org2blog-server-pass (read-passwd "Weblog password? "))))
+
+(defun org2blog-upload-images-insert-links (&optional beg end)
+  "Upload images and replace with links in the region specified by BEG to END."
+  (interactive "r")
+  (let ((re 
+        (concat "\\[\\[\\(.*\\)" 
+                (substring (org-image-file-name-regexp) 0 -2)
+                "\\]\\]"))
+       file-all-urls file-name file-web-url blog-pass)
+    (save-excursion
+      (save-restriction
+        (narrow-to-region (or beg (point-min))
+                          (or end (point-max)))
+        (goto-char (point-min))
+        (while (re-search-forward re nil t 1)
+          (setq file-name (concat 
+                           (match-string-no-properties 1)
+                           "."
+                           (match-string-no-properties 2)))
+          (unless (save-match-data (string-match org-link-types-re file-name))
+            (save-match-data 
+              (if (assoc file-name file-all-urls)
+                  (setq file-web-url (cdr (assoc file-name file-all-urls)))
+                (setq file-web-url
+                      (cdr (assoc "url" 
+                                  (metaweblog-upload-image org2blog-server-xmlrpc-url
+                                                           org2blog-server-userid
+                                                           (org2blog-password)
+                                                           org2blog-server-weblog-id
+                                                           (get-image-properties file-name))))
+                      file-all-urls (append file-all-urls (list (cons 
+                                                                 file-name file-web-url))))))
+            (replace-match (concat "[[" file-web-url "]]") t t nil 0)))))
+    file-all-urls))
+
+(defun org2blog-post-subtree (&optional publish)
+  "Post the current entry as a draft. Publish if PUBLISH is non-nil."
+  (interactive "P")
+  (let ((post (org2blog-parse-subtree))
+        post-id)
+    (org2blog-create-categories (cdr (assoc "categories" post)))
+    (setq post-id (cdr (assoc "post-id" post)))
+    (save-excursion 
+      (org2blog-upload-images-insert-links (org-back-to-heading) (org-end-of-subtree)))
+    (if post-id
+        (metaweblog-edit-post org2blog-server-xmlrpc-url
+                             org2blog-server-userid
+                              (org2blog-password)
+                             post-id
+                              post
+                             publish)
+      (setq post-id
+            (metaweblog-new-post
+             org2blog-server-xmlrpc-url
+             org2blog-server-userid
+             (org2blog-password)
+             org2blog-server-blogid
+             post
+             publish))
+      (org-entry-put (point) "Post ID" post-id)
+      (message (if publish
+                   "Published (%s): %s"
+                 "Draft (%s): %s")
+               post-id
+               (cdr (assoc "title" post))))))
+
+(defun org2blog-parse-subtree ()
+  "Parse the current subtree as a blog entry."
+  (let (html-text
+        (post-title (or (org-entry-get (point) "Title")
+                        (org-get-heading t)))
+        (post-id (org-entry-get (point) "Post ID"))
+        ;; Set post-date to the Post Date property or look for timestamp
+        (post-date (or (org-entry-get (point) "Post Date")
+                       (org-entry-get (point) "SCHEDULED")
+                       (org-entry-get (point) "DEADLINE")                       
+                       (org-entry-get (point) "TIMESTAMP_IA")
+                       (org-entry-get (point) "TIMESTAMP")))
+        (tags (org-get-tags-at (point) nil))
+        (categories (org-split-string (or (org-entry-get (point) "CATEGORIES") "") ":")))
+    ;; Convert post date to ISO timestamp
+    (setq post-date
+          (format-time-string "%Y%m%dT%T"
+                              (if post-date
+                                  (apply 'encode-time (org-parse-time-string post-date))
+                                (current-time))
+                              t))
+    (if org2blog-use-tags-as-categories
+        (setq categories tags
+              tags nil))
+    (save-excursion
+      (setq html-text
+            (org-export-region-as-html
+             (and (org-back-to-heading) (line-end-position))
+             (org-end-of-subtree)
+             t 'string))
+      (setq html-text
+            (with-temp-buffer
+              (insert html-text)
+              (goto-char (point-min))
+              ;; Fix newlines
+             (let (start-pos end-pos)
+                (setq start-pos (point-min))
+               (goto-char start-pos)
+                (while (re-search-forward "<\\(pre\\|blockquote\\).*?>" nil t 1)
+                  (setq end-pos (match-beginning 0))
+                  (replace-string "\n" " " nil start-pos end-pos)
+                  (re-search-forward (concat "</" (match-string-no-properties 1) ">") nil t 1)
+                  (setq start-pos (match-end 0))
+                  (goto-char start-pos))
+               (setq end-pos (point-max))
+               (replace-string "\n" " " nil start-pos end-pos))
+              ;; Copy the text
+              (buffer-substring-no-properties (point-min) (point-max)))))
+    (list
+     (cons "date" post-date)
+     (cons "title" post-title)
+     (cons "tags" tags)
+     (cons "categories" categories)
+     (cons "post-id" post-id)
+     (cons "description" html-text))))
+
 (provide 'org2blog)

I like using one big Org file for all of my notes so that I can search and categorize things easily.

Here is the sample code from my ~/.emacs:

(add-to-list 'load-path "~/elisp/org2blog")
(require 'org2blog)
(setq org2blog-server-url "http://sachachua.com/blog/xmlrpc.php"
      org2blog-server-user "admin"
      org2blog-server-weblog-id ""
      org2blog-use-tags-as-categories t)
(org2blog-login)

Then I can go to the entry and call M-x org2blog-post-subtree to post a draft or C-u M-x org2blog-post-subtree to publish it.

Note that the code uses whatever heading level you’re on, so if you’re under a sub-heading of the post you want to publish, use C-c C-u outline-up-heading to go up headings until you’re at the right level.

You can get the modified source code from http://github.com/sachac/org2blog . I’ve also sent a pull request upstream.