;;; tumblr-mode.el --- Major mode for Tumblr ;; ;; Copyright (C) 2011, 2012 Julian Qian ;; ;; Author: Julian Qian ;; Created: Dec 25, 2011 ;; Version: 0.2 ;; ;; This file is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3, or (at your option) ;; any later version. ;; ;; This file is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, ;; Boston, MA 02111-1307, USA. ;; ;;; Commentary: ;; ;; tumblr-mode.el is a major mode for tumblr.com. ;; ;; You can write and manage your tumblr posts in Emacs with this mode. ;; It only supports to create regular(text) type in tumblr.com now, ;; because I think it's the most suitable type that can be edited in ;; Emacs. ;; ;; - `tumblr-new-post' create a buffer to post ;; - `tumblr-save-post' save buffer to tumblr.com ;; - `tumblr-list-posts' list tumblr posts in a buffer ;; ;;; Install: ;; ;; Download and put `tumblr-mode.el' into your load-path, then use it in ;; your ~/.emacs: ;; ;; (require 'tumblr-mode) ;; ;;; Code: (require 'cl) (require 'url) (require 'xml) (defgroup tumblr-mode nil "A major mode for Tumblr.com") (defcustom tumblr-hostnames '() "a list containing all your tumblr hosts, eg: \(setq tumblr-hostnames '\(\"myblog.tumblr.com\" \"myphoto.tumblr.com\"\)\)" :type '(repeat string) :group 'tumblr-mode) (defcustom tumblr-email "" "tumblr login email" :type 'string :group 'tumblr-mode) (defcustom tumblr-password "" "tumblr login password" :type 'string :group 'tumblr-mode) (defcustom tumblr-post-type "text" "tumblr post type" :type 'string :group 'tumblr-mode) (defcustom tumblr-post-format "markdown" "tumblr post format" :type 'string :group 'tumblr-mode) (defcustom tumblr-retrieve-posts-num-total 20 "the number of posts to be listed" :type 'integer :group 'tumblr-mode) (defvar tumblr-current-hostname nil) (defvar tumblr-current-tag nil) (defvar tumblr-current-state nil) (defvar tumblr-post-header-delimiters '("--" . "--") "A cons containing two flags, to indicate the header, aka. meta info section, of a post. Default are two \"--\", but you can replace them with \"\" for better markdown preview, eg: (setq tumblr-post-header-delimiters '(\"\")) ") (defvar tumblr-post-status '("published" "draft")) (defvar tumblr-post-types '("text" "quote" "photo" "link" "chat" "video" "audio")) (defvar tumblr-post-formats '("markdown" "html")) (defvar tumblr-request-filters '("text" "none")) (defvar tumblr-retrieve-posts-num-once 20) (defvar tumblr-retrieve-posts-list nil) (defvar tumblr-mode-map (let ((map (make-keymap))) (suppress-keymap map) (define-key map (kbd "n") 'next-line) (define-key map (kbd "p") 'previous-line) (define-key map (kbd "q") 'bury-buffer) (define-key map (kbd "g") 'tumblr-list-posts) map) "keymap for `tumblr-mode'.") (defvar tumblr-post-mode-map (let ((map (make-keymap))) (define-key map (kbd "C-c C-s") (lambda () (interactive) (call-interactively 'tumblr-save-post) (set-buffer-modified-p nil))) map) "keymap for `tumblr-post-mode'.") ;; utilities (defun assocref (item alist) (cdr (assoc item alist))) (defun assqref (item alist) (cdr (assq item alist))) (defun tumblr-format-timestamp (secs) (format-time-string "%F" (seconds-to-time (if (stringp secs) (string-to-number secs) secs)))) (defun tumblr-query-string (args) (mapconcat (lambda (arg) (concat (url-hexify-string (car arg)) "=" (url-hexify-string (cdr arg)))) args "&")) (defun tumblr-get-hostname () (setq tumblr-current-hostname (if (and (listp tumblr-hostnames) (car tumblr-hostnames)) (funcall (if (fboundp 'ido-completing-read) 'ido-completing-read 'completion-read) "Choose hostname: " tumblr-hostnames nil nil nil nil (car tumblr-hostnames)) (read-string "Tumblr hostname: ")))) (defun tumblr-get-tag () (setq tumblr-current-tag (read-string "Choose a tag: " nil '(tumblr-current-tag)))) (defun tumblr-get-state () (setq tumblr-current-state (funcall (if (fboundp 'ido-completing-read) 'ido-completing-read 'completion-read) "Choose state: " tumblr-post-status nil nil nil nil (car tumblr-post-status)))) (defun tumblr-get-email () (or tumblr-email (setq tumblr-email (read-string "Tumblr email: ")))) (defun tumblr-get-password () (or tumblr-password (setq tumblr-password (read-passwd "Tumblr password: ")))) ;; tumblr format (defun tumblr-list-posts-format (width content) (let ((fmt (format "%%-%d.%ds" width (decf width)))) (format fmt content))) (defun tumblr-mode-line-title-format (title) (format "*tumblr: %s*" title)) ;; http functions (defun tumblr-authenticated-read-xml-root (hostname params) (let* ((url-request-method "POST") ;; (url-http-attempt-keepalives nil) (url-mime-charset-string "utf-8;q=0.7,*;q=0.7") (url-request-extra-headers '(("Content-Type" . "application/x-www-form-urlencoded"))) (url-request-data (tumblr-query-string (append params `(("email" . ,(tumblr-get-email)) ("password" . ,(tumblr-get-password)))))) (coding-system-for-read 'utf-8) (coding-system-for-write 'utf-8) (buffer (url-retrieve-synchronously (format "http://%s/api/read" hostname))) (root (with-current-buffer buffer (set-buffer-multibyte t) ;; (write-file "/tmp/tumblr.txt" nil) ;; (decode-coding-region (point-min) (point-max) 'utf-8) (goto-char (point-min)) (if (search-forward " tumblr-retrieve-posts-num-total total) total tumblr-retrieve-posts-num-total) total)) (remaining total-retrieving) (tumblr-retrieve-posts-list nil)) ;; retrieve posts (while (> remaining 0) (let ((retrieving (if (> remaining tumblr-retrieve-posts-num-once) tumblr-retrieve-posts-num-once remaining))) (setq remaining (- remaining retrieving)) (setq tumblr-retrieve-posts-list (append tumblr-retrieve-posts-list (tumblr-list-posts-internal hostname retrieving tagged state))))) ;; list all posts (with-current-buffer (get-buffer-create "*tumblr-mode*") (goto-char (point-min)) (save-excursion (kill-region (point-min) (point-max)) (let ((title-len 48) ; keep less than 80 columns (tags-len 20) (date-len 11)) ;; header (save-excursion (insert (tumblr-list-posts-format title-len "Title")) (insert (tumblr-list-posts-format tags-len "Tags")) (insert (tumblr-list-posts-format date-len "Date")) (insert "\n")) (overlay-put (make-overlay (line-beginning-position) (line-end-position)) 'face 'header-line) (forward-line) ;; list posts (mapc (lambda (post) (insert (tumblr-list-posts-format title-len (assqref 'title post))) (insert (tumblr-list-posts-format tags-len (mapconcat (lambda (tag) (format "#%s" tag)) (assqref 'tags post) ", "))) (insert (tumblr-list-posts-format date-len (tumblr-format-timestamp (assqref 'timestamp post)))) (make-text-button (line-beginning-position) (line-end-position) 'id (assqref 'id post) 'group hostname 'action 'tumblr-get-post-edit 'face 'default) (insert "\n")) tumblr-retrieve-posts-list))) ;; skip header (forward-line) (tumblr-mode hostname) (set-window-buffer nil (current-buffer))) (message "Retrieved total %d posts on %s, tagged %s, state: %s." total-retrieving hostname tagged state))) (defun tumblr-get-post (post-id hostname) (let* ((root (tumblr-authenticated-read-xml-root hostname `(("id" . ,post-id) ("filter" . "none")))) (tumblr (car root)) (posts (car (xml-get-children tumblr 'posts))) (post (car (xml-get-children posts 'post))) (attrs (xml-node-attributes post)) (title (caddar (xml-get-children post 'regular-title))) (body (caddar (xml-get-children post 'regular-body))) ; post content (tags (mapcar (lambda (tag) (caddr tag)) (xml-get-children post 'tag))) (buffer (get-buffer-create (tumblr-mode-line-title-format title)))) ;; edit post (with-current-buffer buffer (goto-char (point-min)) (save-excursion (kill-region (point-min) (point-max)) ;; insert meta info, as octopress does (tumblr-insert-post-template title attrs tags hostname) ;; insert content (insert body)) (tumblr-prepare-post-edit)) (set-window-buffer nil buffer))) (defun tumblr-insert-post-template (title &optional attrs-alist tags-list group) (if (car tumblr-post-header-delimiters) (insert (format "%s\n" (car tumblr-post-header-delimiters)))) (let ((date (assqref 'date attrs-alist)) (id (assqref 'id attrs-alist)) (slug (assqref 'slug attrs-alist)) (state (assqref 'state attrs-alist)) (format (assqref 'format attrs-alist)) (tags (mapconcat (lambda (tag) tag) tags-list ", "))) (and id (insert (format "id: %s\n" id))) (and title (insert (format "title: %s\n" title))) (and slug (insert (format "slug: %s\n" slug))) (and group (insert (format "group: %s\n" group))) (and tags (insert (format "tags: %s\n" tags))) (and format (insert (format "format: %s\n" format))) (and state (insert (format "state: %s\n" state))) (and date (insert (format "date: %s\n" date)))) (if (cdr tumblr-post-header-delimiters) (insert (format "%s\n\n" (cdr tumblr-post-header-delimiters))) (error "cdr of `tumblr-post-header-delimiters' is a must, otherwise it cannot identify what's header of the post."))) (defun tumblr-get-post-edit (button) (tumblr-get-post (button-get button 'id) (button-get button 'group))) (defun tumblr-save-post () "Posting current buffer to tumblr.com. Below options are accepted headers for posting: - `title' Post's title, required - `post-id' Post's identity, required when editing an existed post - `group' The hostname the post will be posted to, it will override `tumblr-default-hostname', optional - `tags' Tags seperated by comma, optional - `slug' Slug for friendly url, optional - `state' published/draft/submission/queue, optional - `date' When the post is posted, optional One example of the post buffer: -- title: a post writed by tumblr-mode.el group: test.tumblr.com tags: tumblr, emacs -- blah..blah..blah " (interactive) (let* ((body-start (point-min)) ;; get meta properities (props (save-excursion ;; only search meta info in 0~10 lines (let ((bound (progn (goto-char (point-min)) (forward-line 10) (point))) beg end) (goto-char (point-min)) ; start to search ;; meta info begin point (if (and (car tumblr-post-header-delimiters) (search-forward-regexp (format "%s\r?\n" (car tumblr-post-header-delimiters)) bound t)) (setq beg (match-end 0)) (setq beg (point-min))) ;; meta info end point (if (and (cdr tumblr-post-header-delimiters) (search-forward-regexp (format "%s\r?\n" (cdr tumblr-post-header-delimiters)) bound t)) (progn (setq end (match-beginning 0)) ) (error "Can't found the delimiter of post's header and body")) ;; meanwhile, we can get body start point (if (search-forward-regexp "[^\r\n]" nil t) (setq body-start (match-beginning 0)) (setq body-start (point))) ;; found meta info (if (and (< beg end) (< end body-start)) (let* ((lines-text (buffer-substring-no-properties beg end)) (lines (split-string lines-text "\r?\n" t)) prop) (mapcar (lambda (line) (when (string-match "\\s-*\\([^:]+\\)\\s-*:\\s-*\\(.+\\)" line) `(,(match-string 1 line) . ,((lambda (str) ; strip tail white space (if (string-match "\\s-+$" str) (replace-match "" t t str) str)) (match-string 2 line))))) lines)) (error "Failed to parse the post, maybe format is wrong."))))) ;; get body content (body (buffer-substring-no-properties body-start (point-max))) (id (assocref "id" props)) (title (assocref "title" props)) (tags (assocref "tags" props)) (date (assocref "date" props)) (group (assocref "group" props)) (state (assocref "state" props)) (slug (assocref "slug" props))) (when (y-or-n-p (format "%s post [ %s ]?%s" (if id "Save" "Create") title (if tags (format " tags: %s." tags) ""))) (tumblr-write-post (if (or (null group) (string= "" group)) (tumblr-get-hostname) group) `(("post-id" . ,id) ; WTF..api/read is "id", but api/write is "post-id" ("title" . ,title) ("body" . ,body) ("tags" . ,tags) ("date" . ,date) ("slug" . ,slug) ("state" . ,state))) (set-buffer-modified-p nil)))) (defun tumblr-prepare-post-edit () (if (fboundp 'markdown-mode) (markdown-mode) (message "Recommand to apply markdown-mode for tumblr post writing.")) (tumblr-post-mode 1) (set-buffer-modified-p nil)) (defun tumblr-new-post (title) (interactive "sCreate post title: \n") (switch-to-buffer (tumblr-mode-line-title-format title)) (tumblr-insert-post-template title '((slug . " ") (state . "published")) nil (or tumblr-current-hostname (tumblr-get-hostname))) (tumblr-prepare-post-edit)) ;; (put 'tumblr-mode 'mode-class 'special) (defun tumblr-mode (&optional hostname) (kill-all-local-variables) (buffer-disable-undo) (setq major-mode 'tumblr-mode mode-name "tumblr-mode" mode-line-buffer-identification (append (default-value 'mode-line-buffer-identification) `(,(format " [%s] " hostname)))) (make-local-variable 'tumblr-retrieve-posts-list) (use-local-map tumblr-mode-map) (run-mode-hooks 'tumblr-mode-hook)) (define-minor-mode tumblr-post-mode "A minor mode for tumblr post, see \\[tumblr-new-post] and \\[tumblr-save-post]" :init-value nil :lighter " Tumblr" :keymap tumblr-post-mode-map :group 'tumblr-mode) (provide 'tumblr-mode) ;;; tumblr-mode.el ends here