EMACS-DOCUMENT

=============>集思广益

使用org-mode在leanpub上发布电子书

我最近一直在倒腾org-mode,同时还在leanpub上写书。 然后我想,为什么不用org-mode来写书呢?于是就有了这篇文章。 目前已经有一个很棒的exporter可以将org-mode导出成leanpub所需的格式。你需要下载它,因为整篇博文都是建立在它之上的。

在我们开始讨论org-mode之前,让我们先看看leanpub是怎么工作的吧.

Leanpub是遵循着KISS(keep It Simple Stupid)原则来写书的. 你可以选择用Github或者Dropbox来同步你的书籍. 在本文中,我们以Dropbox作为例子. 若你选择以Dropbox同步的方式来创建书籍, "Leanpub Bookbot" 会往你的Dropbox账户发送一份共享申请. 该申请会创建两个文件夹: manuscriptconvert_html. 其中, manuscript 目录用于存放markdown格式 (或markua)格式)的章节文件. 该文件夹中还有一个 Book.txt 的文件,用于指明成书中包含了哪些章节, 还有 Sample.txt 用于指明哪些章节组成了书的样品. 要了解更多内容,请点击这里.

1 为什么用org-mode来写书?

一开始,是因为我觉得在单一文件中编辑和移动各章节的这种编辑方式用起来很顺手. org-mode很适应这种编辑方式,只需要将每个章节看成是org文件中的最高层标题(top level heading)就行了. 不仅如此,我还可以为每个章节添加元数据作为备注. 我常常需要追踪一些无需发布的东西. 例如,我会使用org drawer来存储书籍最后一次发布的时间, 我还会记录写作每个章节所花费的时间. 这些事情,org-mode都会帮你无缝完成. 我甚至将写书时的整个工作日志都存储到org文件中了. 更棒的是,我可以使用git对书本(其实也就是一个org文件)作版本控制. Org-mode还能够添加代码块,并且该代码块还能被执行,其执行的结果就放在同一文件中该代码块的下面(然而我在写作时并未用到这项功能). 实在是有太多的理由来使用org-mode进行写作了. 最后,你还可以将org文件导出成各种格式. 甚至还存在一个插件能够将org导出成基于twitter bootstrap的HTML页面!

2 如何用org-mode来写书

其背后的思想是将org文件中的最高层标题看成是Leanpub book中的章(chapter). 若你想导出成书时排除掉某个特定的章/节(对应org-mode中的子树),只需要为对应的标题加上“noexport”标签就行了.

Org可以为各个org文件设置自己的属性,比如是否自动缩进,是否自动展开文本内容, 是否自动生成目录. 例如,你可以添加下面这段代码到文件的首部:

#+STARTUP: indent showeverything

限制org文件中能设置的标签很有用,方法是设置 TAGS 属性.

#+TAGS: noexport sample

由于Leanpub会自动生成目录,因此我不希望org-mode重复产生目录. 通过下面的配置让org-mode不再生成目录.

#+OPTIONS: toc:nil

你可以通过下面的属性来设置每个标题的工作流状态:

#+TODO: TODO(t) DRAFT(f@/!) IN-THE-BOOK(i!) | DONE(d!) CANCELED(c)

在"|"左边的状态表示某种正在进行的状态,而"|"右边的状态表示某种已经完结的状态. ‘!’ 表示当切换到这种状态时,同时会记录下当时的时间戳. ‘@’则表示当切换到这种状态时,不仅仅记录下当时的时间戳,同时还提示用于输入备注信息. 然而默认情况下,这些元数据也会随着书籍的内容一起被导出.

举个例子,下面是我的org文件中某一章的样子:

* DRAFT Routing and controllers :sample: 
- State "DRAFT" from "30%" [2016-05-30 Mon 21:08]
- State "30%" from "TODO" [2016-05-26 Thu 17:05]
Routing is responsible for matching a URL path with a custom content or functionality in your site.

为了防止元数据也被导出,我需要添加另一个名为 logdrawer 的属性,

#+STARTUP: indent showeverything logdrawer

这样一来,状态改变的日志会被收入到一个名为 LOGBOOK 的属性drawer中.

* DRAFT Routing and controllers :sample:
:LOGBOOK:
- State "DRAFT" from "30%" [2016-05-30 Mon 21:08]
- State "30%" from "TODO" [2016-05-26 Thu 17:05]
:END:
Routing is responsible for matching a URL path with a custom content or functionality in your site.

可以指定某个标题导出到某个特定的文件中,方法是通过 EXPORT_FILE_NAME 属性来指定文件名:

* Drupal permissions and users
:PROPERTIES:
:EXPORT_FILE_NAME: permissions-and-users.txt
:END:

可以給那些要被收入书籍样品的章加上一个"sample"标签.

下面的俄函数,可以将orgbuffer导出成一个Leanpub book.

(defun leanpub-export ()
  "Export buffer to a Leanpub book."
  (interactive)
  (if (file-exists-p "./Book.txt")
  (delete-file "./Book.txt"))
  (if (file-exists-p "./Sample.txt")
  (delete-file "./Sample.txt"))
  (org-map-entries
   (lambda ()
     (let* ((level (nth 1 (org-heading-components)))
            (tags (org-get-tags))
           (title (or (nth 4 (org-heading-components)) ""))
           (book-slug (org-entry-get (point) "TITLE"))
           (filename
            (or (org-entry-get (point) "EXPORT_FILE_NAME") (concat (replace-regexp-in-string " " "-" (downcase title)) ".md"))))
       (when (= level 1) ;; export only first level entries
         ;; add to Sample book if "sample" tag is found.
         (when (or (member "sample" tags) (string-prefix-p "frontmatter" filename) (string-prefix-p "mainmatter" filename))
           (append-to-file (concat filename "\n\n") nil "./Sample.txt"))
         (append-to-file (concat filename "\n\n") nil "./Book.txt")
         ;; set filename only if the property is missing
         (or (org-entry-get (point) "EXPORT_FILE_NAME")  (org-entry-put (point) "EXPORT_FILE_NAME" filename))
         (org-leanpub-export-to-markdown nil 1 nil)))) "-noexport") (org-save-all-org-buffers)
   nil nil)

注意 运行该函数需要预先安装好 org-leanpub exporter.

让我们稍微解释一下这个函数. 这里最主要的API是org-map-entries, 该函数对buffer中的每个标题都调用一次指定的函数. 这个函数首先检查当前的标题是否为最高层的标题, 若是,则调用 org-leanpub exporter 导出标题下的子树内容. org-map-entries 还接受一个可选参数 match. 在我们这个案例中, 我只希望将该函数应用于那些没有 "noexport" 标签的标题, 因此 match 的参数值为 -noexport.

Leanpub还需要一些特殊意义的文件({mainmatter},{frontmatter}和{backmatter})来标示出书籍中各部分(例如附录等)的内容. 这些特殊意义的文件由下面这些org-mode headline所标示. 你可以把下面这些内容放到你org文件中的合适位置.

* Frontmatter
:PROPERTIES:
:EXPORT_FILE_NAME: frontmatter.md
:END:
{frontmatter}

* Mainmatter
:PROPERTIES:
:EXPORT_FILE_NAME: mainmatter.md
:END:
{mainmatter}

* Backmatter
:PROPERTIES:
:EXPORT_FILE_NAME: backmatter.md
:END:
{backmatter}

3 Bonus — 通过Emacs生成书籍预览

Leanpub提供了一个API来为所编写的书籍生成预览, 即,你可以发起一个POST调用給Leanpub来触发为书籍生成预览的动作. 要在Emacs中完成这一步骤,你需要:

  • 生成一个API key. 这在Leanpub网站上有详细的指引.
  • 在Emacs上安装request 库来发起调用API的请求.

下面是生成预览的函数代码:

(defun leanpub-preview ()
  "Generate a preview of your book @ Leanpub."
  (interactive)
  (request
   "https://leanpub.com/<YOUR-BOOK-SLUG>/preview.json" ;; or better yet, get the book slug from the buffer
   :type "POST"                                        ;; and construct the URL
   :data '(("api_key" . "53cr3t"))
   :parser 'json-read
   :success (function*
             (lambda (&key data &allow-other-keys)
               (message "Preview generation queued at leanpub.com."))))
  )

希望你能用org-mode完成下一部书籍的写作!