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

发布于 2025-01-01 23:34:27 字数 6573 浏览 12 评论 0

我最近一直在倒腾 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 用于指明哪些章节组成了书的样品。要了解更多内容,请点击 这里 .

为什么用 org-mode 来写书?

一开始,是因为我觉得在单一文件中编辑和移动各章节的这种编辑方式用起来很顺手。org-mode 很适应这种编辑方式,只需要将每个章节看成是 org 文件中的最高层标题(top level heading) 就行了。不仅如此,我还可以为每个章节添加元数据作为备注。我常常需要追踪一些无需发布的东西。

例如,我会使用 org drawer 来存储书籍最后一次发布的时间, 我还会记录写作每个章节所花费的时间。这些事情,org-mode 都会帮你无缝完成。我甚至将写书时的整个工作日志都存储到 org 文件中了。更棒的是,我可以使用 git 对书本(其实也就是一个 org 文件) 作版本控制。

Org-mode 还能够添加代码块,并且该代码块还能被执行,其执行的结果就放在同一文件中该代码块的下面(然而我在写作时并未用到这项功能)。实在是有太多的理由来使用 org-mode 进行写作了。最后,你还可以将 org 文件导出成各种格式。甚至还存在一个插件能够将 org 导出成基于 twitter bootstrap 的 HTML 页面!

如何用 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}

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 完成下一部书籍的写作!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

寂寞清仓

暂无简介

文章
评论
28 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文