天天看點

用 Markdown 寫一本自己的電子書吧(一)手動篇

不知道大家平時有沒有閱讀電子書的習慣,這裡指的并不是

.txt

的文本文檔,而是通常帶有精美封面、便捷目錄、圖文并茂的

.epub

電子書。它是怎樣實作這些效果的呢?我們能不能把自己平時用 Markdown 寫的技術筆記、部落格文章做成一本屬于自己的電子書呢?

背景

EPUB 格式是什麼

其實做 Web 開發的同學,如果把

.epub

檔案通過 zip 打開後就會發現,其實它并不神秘,反而相當開放直覺和熟悉──其内在就是一堆 xhtml 頁面、css 樣式、圖檔,以及描述這些資源關系的 xml 配置資訊,把它們一起打個 zip 包就是

.epub

電子書了。

在我們解壓出來的檔案,往往會有一個

.opf

檔案,内容開頭一般是:

<?xml version="1.0"  encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" ...>
  <!-- ... -->
</package>           

複制

我們隻要通路命名空間屬性中的這個 http://www.idpf.org/2007/opf 連結,就可以查詢到關于這個

OPF

電子書的所有規範描述了。

簡單來說,這就是一個由國際數字出版論壇和 W3C 組織一起完成的開放電子書标準,在 2007 年 9 月取代了之前的 Open eBook,被國際數字出版論壇選為新的正式标準。

既然 epub 内部就是 html 頁面,我們的 Markdown 文章也能編譯成 html,那我們寫個工具将以往的文檔處理成符合 epub 标準的檔案包,不就可以做一本自己的電子書了?

開始動手:手動篇

1. 建立電子書

1-1. 基本結構

我們先建立一個 example 目錄,其中包含 META-INF 和 EPUB 兩個子目錄。然後在現有目錄結構中建立 mimetype, META-INF/container.xml 和 EPUB/package.opf 檔案:

example
├── EPUB
│   └── package.opf
├── META-INF
│   └── container.xml
└── mimetype           

複制

檔案 mimetype 内容:

application/epub+zip           

複制

檔案 META-INF/container.xml 内容:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
  <rootfiles>
    <rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/>
  </rootfiles>
</container>           

複制

檔案 EPUB/package.opf 内容:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<package xmlns="http://www.idpf.org/2007/opf" 
         xmlns:dc="http://purl.org/dc/elements/1.1/"
         xmlns:dcterms="http://purl.org/dc/terms/"
         version="3.0"
         xml:lang="en"
         unique-identifier="pub-identifier">
  <metadata>
    <dc:identifier id="pub-identifier">kepub:20211120:000000001</dc:identifier>
    <dc:title id="pub-title">Example Book</dc:title>
    <dc:language id="pub-language">en</dc:language>
    <dc:date>2021-11-20</dc:date>
    <meta property="dcterms:modified">2021-11-20T14:50:00Z</meta>
  </metadata>
  <manifest>
    <!-- TODO -->
  </manifest>
  <spine>
    <!-- TODO -->
  </spine>
</package>           

複制

以上就是我們的 .epub 檔案中最基礎的三個檔案。

其中 package.opf 中,我們在

package > metadata

内定義了一些 .epub 必備的元資訊。以後我們向電子書添加内容時,還需要根據實際情況繼續更新其中

package > manifest

資源清單 和

package > spine

書脊 的相關資訊。

1-2. 添加頁面

接下來就是向其中添加内容了。

在之前的基礎上,我們再建立一個 EPUB/book 目錄,在其中添加一個 EPUB/book/page-1.xhtml 檔案:

example
├── EPUB
│   ├── book
│   │   └── page-1.xhtml
│   └── package.opf
├── META-INF
│   └── container.xml
└── mimetype           

複制

檔案 EPUB/book/page-1.xhtml 内容:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:epub="http://www.idpf.org/2007/ops"
      xml:lang="en"
      lang="en">
<head>
    <title>Page 1</title>
</head>
<body>
    <h1>Hello world!</h1>
</body>
</html>           

複制

然後修改 package.opf 中的資源清單和書脊:

<manifest>
  <item id="page-1" href="book/page-1.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine>
  <itemref idref="page-1"/>
</spine>           

複制

對于剛才的頁面,我們建立了

package > manifest > item

條目,标記了它的

[media-type]

類型,并且設定了一個

[id="page-1"]

屬性,将其以

itemref[idref="page-1"]

的形式在書脊内進行了引用。

此時,如果将 example 目錄的内容進行 zip 打包,生成檔案名稱改為 example.epub,就已經可以在一些 epub 閱讀器中正常打開進行閱讀了。但部分基于導航目錄進行内容索引的閱讀器(比如 微信讀書)還無法正常浏覽,需要再做一點小小的改動。

1-3. 導航目錄

我們再建立一個 EPUB/toc.xhtml,内容:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:epub="http://www.idpf.org/2007/ops"
      xml:lang="en"
      lang="en">
<head>
    <title>TOC</title>
</head>
<body>
    <nav epub:type="toc" id="toc">
        <h1>Table of Contents</h1>
        <ol>
            <li>
                <a href="book-page1.xhtml">Page 1</a>
            </li>
        </ol>
    </nav>
</body>
</html>           

複制

注意其中的

nav[epub:type="toc"]

,這是 epub3 與 epub2 的差別之一,可以将目錄頁面的部分作為書籍的導航目錄,不再需要單獨提供

.ncx

檔案。

同樣的,繼續修改 package.opf 的資源清單和書脊:

<manifest>
  <item id="htmltoc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav"/>
  <item id="page-1" href="book/page-1.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine>
  <itemref idref="htmltoc"/>
  <itemref idref="page-1"/>
</spine>           

複制

其中

item#htmldoc

添加了

[properties="nav"]

表示這個頁面是導航目錄。

這次,再将 example 目錄内容打包為 example.epub 後,就能在大部分閱讀器内都正常打開了。

1-4. 封面頁

我們還可以給自己的電子書添加一個好看的封面,比如:

用 Markdown 寫一本自己的電子書吧(一)手動篇

将其儲存為 EPUB/images/cover.jpg,然後建立 EPUB/cover.xhtml,内容為:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:epub="http://www.idpf.org/2007/ops"
      xml:lang="en"
      lang="en">
<head>
    <title>Cover</title>
    <style type="text/css">
        img {
            max-width: 100%;
        }
    </style>
</head>
<body>
    <figure id="cover-image">
        <img src="images/cover.jpg"
             alt="Book Cover" />
    </figure>
</body>
</html>           

複制

老規矩,繼續更新 package.opf 的資源清單和書脊:

<manifest>
  <item id="htmltoc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav"/>
  <item id="cover" href="cover.xhtml" media-type="application/xhtml+xml"/>
  <item id="cover-image" href="images/cover.jpg" media-type="image/jpeg" properties="cover-image"/>
  <item id="page-1" href="book/page-1.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine>
  <itemref idref="cover" linear="no"/>
  <itemref idref="htmltoc"/>
  <itemref idref="page-1"/>
</spine>           

複制

這次,增加的圖檔檔案也需要登記在

package > manifest

資源清單内,并且添加了

properties="cover-image"

屬性,将其标記為書籍的封面圖檔。

而建立的

cover.xhtml

檔案,是為了讓我們在打開書籍後,也能在内容内看到封面的效果。

1-5. 其它

如果我們需要在電子書内,添加更多頁面、引用更多圖檔、添加裝飾樣式、改用自定義字型,也是相同的操作:

  • 将資源添加到項目内。
  • 更新 package.opf 内的

    manifest

    資源清單。
  • 如果是頁面,再添加對應

    [id]

    引用到

    spine

    書脊内,并且更新 toc.xhtml 内的

    nav

    記錄。
    • toc.xhtml 内的

      nav

      導航目錄,支援

      ol > li > ol > li ...

      嵌套實作多級目錄。
    • 部分閱讀器不支援

      nav

      導航目錄使用

      ul

      無須清單。
    • 如果需要為導航索引型閱讀器提供頁面引用,又不想讓對應記錄出現在目錄内;或者重複引用于不同位置的相同頁面記錄,可以為對應

      ol

      /

      li

      設定

      [hidden=""]

      屬性,進行隐藏處理。

2. 自動流程準備

基于上面的原理,我們已經能夠開始手動編寫我們的電子書了。

不過這個過程中還有很多手動操作并不便捷的步驟,比如 每篇文章進行 Markdown to Html 轉化、文章中所有圖檔添加到資源清單、更新文章目錄結構 ,如果文章頁面、引用資源稍微多一些,就基本沒法手動處理過來了。

是以我們需要更高效的自動處理方案。

2-1. 自動的與手動的

剛才提到的資源清單内容,大緻可以分為兩類:

  • 文章的頁面檔案。
  • 文章内引用的圖檔資源

其中,後者可以直接在 Markdown 文檔渲染成 html 檔案後,進行 html 解析再對所有

img

标簽進行彙總即可得出配置清單。

前者可以基于

.md

檔案本身的目錄結構進行資源清單的整合,但是 對于頁面在書脊和導航目錄内的順序 無法進行很好的控制。

如果基于檔案名進行排序,相當于引入了一套不可控的潛規則,對于書籍遷移、頁面删減維護都不太友善。而且如果需要處理導航目錄内隐藏、重新引用的場景,還要引入更複雜的潛規則。

不如增加一個簡化的

.json

配置檔案,統一管理頁面在導航目錄内的順序和層級關系。

2-2. 新的電子書結構

我們重新建立一個 new-book 目錄,并在其中建立一些子目錄和檔案:

new-book
├── chapter-1
│   ├── index.md
│   └── ep-1.md
└── book.json           

複制

檔案 book.json 内容:

{
  "meta": {
    "id": "kepub:20211120:000000001",
    "title": "Example Book",
    "lang": "en",
    "date": "2021-11-20",
    "modified": "2021-11-20T14:50:00Z"
  },
  "pages": [
    {
      "title": "Chapter.01",
      "file": "chapter-1/index.md",
      "children": [
        {
          "file": "chapter-1/ep-1.md",
          "hidden": true
        }
      ]
    }
  ]
}           

複制

檔案 chapter-1/index.md 内容:

# Chapter.01

Hello world!           

複制

檔案 chapter-1/ep-1.md 内容:

# Episode.01

This is episode 1.           

複制

這樣,我們日常建立一本電子書時,真正需要自己定制的内容基本就已收錄其中。而且能友善地在其中定義和調整頁面的順序和層級關系,控制對應條目是否在導航内隐藏了。

接下來,我們隻需要再編寫一些腳本,将上面的結構自動轉化成電子書需要的

mimetype

,

container.xml

,

package.opf

和各種頁面檔案,并且彙總對應的資源清單、書脊和導航目錄。