A Multiple Books Generator Inspired by GitBook and Made of Jekyll

I don’t think I can express my thought with a blog entirely. A blog can be used to record some fragments conveniently. However, I can’t publish my novels and books, which contain multiple articles of one topic, well with it.

I have searched a lot of ways to publish novels in my old blog powered by WordPress. I made a book module in the theme Dysis. The chapters were written in txt files and rendered by AJAX.

I migrated from WordPress to static site generator then. There is a better container for my novels and books which called GitBook. I used the GitBook to generate static book pages, and then write a static home page. Then I found some inconveniences:

  • If I want to publish a new book, I have to edit the home page first.
  • It’s hard to be extended. What if I want a blog?

Gitbook can be extended by plugins, but there are limitations. It is also hard to be further developed for I’m not familiar with nodejs. So I made a website powered by both of Gitbook and Jekyll.

I have used Jekyll before Gitbook. It’s convenient to write a plugin. And I’m a Ruby programmer. I write a shell to combine the functions of both two generators mechanically. However, it is a waste of the disk memory and inconvenient to manage the files. I finally decided to copy the functions of Gitbook into a Jekyll plugin.

Principles: The construction of the source files should be in consonance with those in Gitbook so that my site can be migrated from Gitbook to Jekyll perfectly.

The construction of source files in Gitbook

  • README.md is the index page of the book, which should be generated to index.html.
  • SUMMARY.md is the catalog and contains the order and hierarchies of chapters.
  • Others are chapters written in markdown files and should be parsed normally.

It could be hard to parse the SUMMARY.md. I can open the file and read for each line to match with titles and links by RegExp. But it’s not easy to get the hierarchies and there may be other exceptions. I thought is for a while and found that I can convert the markdown file to an HTML file by the converter powered by Jekyll. Then I can extract what I want with XPath.

def parse_summary(summary)
  require "nokogiri"

  # Convert the markdown file to HTML by kramdown.
  html = Kramdown::Document.new(summary).to_html

  # Parse it in xpath with nokogiri.
  parsed_html = Nokogiri::HTML(html)

  chapters = []

  # Get the lists.
  list = parsed_html.xpath("//body/ul/li")

  list.each do |li|
    chapter = Hash.new

    # Get the links and titles.
    chapter["link"] = li.xpath("a/@href").to_s
    chapter["title"] = li.xpath("a/text()").to_s
    chapter["level"] = 1
    chapters.push(chapter)

    # At most two nested list here.
    li.xpath("ul/li").each do |sub_li|
      sub_chapter = Hash.new

      sub_chapter["link"] = sub_li.xpath("a/@href").to_s
      sub_chapter["title"] = sub_li.xpath("a/text()").to_s
      sub_chapter["level"] = 2

      chapters.push(sub_chapter)
    end

  end
  # Return a Hash containing the titles, links, and hierarchies.
  return chapters
end

Generator

A BookGenerator should contain three types of pages(including home index, book index, and chapters ) which are subclasses of Page. Here is the construction of the plugin:

module Jekyll
  class IndexPage < Page
  end

  class BookPage < Page
  end

  class ChapterPage < Page
  end

  class BookGenerator < Generator
  end
end

The original files should be stored in the directory _books.

First, iterate the directories in _books and create a instance of BookPage with each directory’s name.

dir = "_books"
Dir.foreach(dir) do |book_dir|
  book_path = File.join(dir, book_dir)
  if File.directory?(book_path) and book_dir.chars.first != "."
    book = BookPage.new(site, site.source, book_dir)
  end
end

Second, parse the SUMMARY.md and get the chapters’ orders and hierarchies.

summary = File.read(File.join(book_path, "SUMMARY.md"))
parts = self.parse_summary(summary)

Iterate the returned chapters and create instances of ChapterPage.

chapters = []
book.data["parts"] = []
current = nil

# Create instances of chapters.
parts.each do |part|
  chapter = ChapterPage.new(site, site.source, book_dir, part["link"], book, part)

  # Keep the original hierarchies.
  if part["level"] == 1
    book.data["parts"].push(chapter)
    current = chapter
    chapter.data["parts"] = []
  else
    current.data["parts"].push(chapter)
  end
end
chapters.push(chapter)

Then, iterate the instances of ChapterPage to assign a next page and a prev page to each chapter. (There may be better way to do that without iteration. But I got none.)

chapters.each_with_index do |chapter, index|
  if index > 0
    chapter.data["prev"] = chapters[index - 1]
  else
    chapter.data["prev"] = book
  end
  if index < chapters.size - 1
    chapter.data["next"] = chapters[index + 1]
  end
  # Push the instances of ChapterPage to the pages generating queue.
  site.pages << chapter
end

# The book index should be the prev page of the first chapter.
book.data["next"] = chapters.first

Push the instance of BookPage to the queue.

site.pages << book

Finally, generate the home index.

book_index = IndexPage.new(site, site.source, "", books)
book_index.render(site.layouts, site.site_payload)
book_index.write(site.dest)
site.pages << book_index

There is nothing important to be mentioned in the constructor method of the subclasses of Page. Just assign the layout, the file names, the names of the directories, and whatever you want. Here is an example of BookPage:

class BookPage < Page
  def initialize(site, base, dir)
    @site = site
    @base = base
    @dir = dir.gsub(/^_/, "").downcase
    @name = "index.md"

    self.process(@name)

    read_yaml(File.join(@base, "_books", @dir), @name)

    self.data["layout"] = 'book'

    if ( self.data["start"] == self.data["end"] ) or ( !self.data["end"] )
      self.data["date"] = self.data["start"].to_s
    else
      self.data["date"] = "#{self.data["start"]}-#{self.data["end"]}"
    end

    self.data["link"] = @dir

    self.data["slug"] = @dir
  end
end

Complete Plugin File: jekyll-book-generator.rb