Skip to content

Commit

Permalink
Improve epub final file. (#66)
Browse files Browse the repository at this point in the history
Improvements include better table of contents, less epubcheck errors, CSS  localization, and more.
  • Loading branch information
fnando authored Jan 24, 2024
1 parent f91f2a0 commit 5630a87
Show file tree
Hide file tree
Showing 31 changed files with 321 additions and 124 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- Add `before_markdown_render` and `after_markdown_render` hooks, allowing
content manipulation.
- Add link anchor on each title.
- Improve epub final file (better table of contents, less epubcheck errors, CSS
localization, and more).

## v3.1.0

Expand Down
19 changes: 5 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,29 +147,20 @@ following:

## This the chapter title

<% note do %>
Make sure you try .md.erb files!
<% end %>
<%= image_tag "myimage.png" %>

The above content must be placed in a `.md.erb` file. The generated content will
be something like this:

```html
<div class="note info">
<p>Make sure you try .md.erb files!</p>
</div>
<img src="images/myimage.png" />
```

The `note` helper is built-in and can accept a different note type.

```erb
<% note :warning do %>
Make sure you write valid ERB code.
<% end %>
```
You book's helpers can be added to `config/helper.rb`, as this file is loaded
automatically by kitabu.

You can see available helpers on
<https://github.com/fnando/kitabu/blob/main/lib/kitabu/markdown.rb>.
<https://github.com/fnando/kitabu/blob/main/lib/kitabu/helpers.rb>.

### Syntax Highlighting

Expand Down
1 change: 1 addition & 0 deletions lib/kitabu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module Kitabu

require "kitabu/extensions/string"
require "kitabu/extensions/rouge"
require "kitabu/extensions/eeepub"
require "kitabu/errors"
require "kitabu/version"
require "kitabu/generator"
Expand Down
11 changes: 5 additions & 6 deletions lib/kitabu/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ def new(path)
end

desc "export [OPTIONS]", "Export e-book"
method_option :only, type: :string,
desc: "Can be one of: #{FORMATS.join(', ')}"
method_option :open, type: :boolean,
desc: "Automatically open PDF (Preview.app for " \
"Mac OS X and xdg-open for Linux)"

option :only, type: :string,
desc: "Can be one of: #{FORMATS.join(', ')}"
option :open, type: :boolean,
desc: "Automatically open PDF (Preview.app for " \
"Mac OS X and xdg-open for Linux)"
def export
if options[:only] && !FORMATS.include?(options[:only])
raise Error,
Expand Down
10 changes: 4 additions & 6 deletions lib/kitabu/exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ def export!
exported << Epub.export(root_dir) if export_epub
exported << Mobi.export(root_dir) if export_mobi && Dependency.calibre?

time = Time.now.strftime("%Y-%m-%dT%H:%M:%S")

if exported.all?
color = :green
message = options[:auto] ? "exported!" : "=> e-book has been exported"
message = "[#{time}] e-book has been exported"

if options[:open] && export_pdf
filepath = root_dir.join("output/#{File.basename(root_dir)}.pdf")
Expand All @@ -51,11 +53,7 @@ def export!
end
else
color = :red
message = if options[:auto]
"could not be exported!"
else
"=> e-book couldn't be exported"
end
message = "[#{time}] => e-book couldn't be exported"
end

ui.say message, color
Expand Down
14 changes: 14 additions & 0 deletions lib/kitabu/exporter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class Base
attr_accessor :source

def self.export(root_dir)
I18n.backend.eager_load!
I18n.load_path += Dir[
root_dir.join("config/locales/**/*.{yml,rb}").to_s
]

new(root_dir).export
end

Expand Down Expand Up @@ -67,6 +72,15 @@ def handle_error(error)
ui.say error.backtrace.join("\n"), :white
end

def copy_files(source, target)
target = root_dir.join(target)
FileUtils.mkdir_p root_dir.join(target)

Dir[root_dir.join(source)].each do |path|
FileUtils.cp path, target
end
end

def copy_directory(source, target)
return unless root_dir.join(source).directory?

Expand Down
79 changes: 68 additions & 11 deletions lib/kitabu/exporter/epub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ class Epub < Base
def sections
@sections ||=
html.css("div.chapter").each_with_index.map do |chapter, index|
html = Nokogiri::HTML(chapter.inner_html)

OpenStruct.new(
index:,
filename: "section_#{index}.html",
filepath: tmp_dir.join("section_#{index}.html").to_s,
html: Nokogiri::HTML(chapter.inner_html)
html:
)
end
end
Expand All @@ -25,13 +27,20 @@ def html

def export
super

copy_styles!
copy_images!
File.open(root_dir.join("output/epub/cover.html"), "w") do |io|
io << render_template(
root_dir.join("templates/epub/cover.erb"),
config
)
end
set_metadata!
write_sections!
write_toc!

epub.files sections.map(&:filepath) + assets
epub.files sections.map(&:filepath) + assets
epub.nav navigation
epub.toc_page toc_path

Expand All @@ -44,7 +53,8 @@ def export
end

def copy_styles!
copy_directory("output/styles", "output/epub/styles")
copy_files("output/styles/epub.css", "output/epub/styles")
copy_files("output/styles/files/*.css", "output/epub/styles")
end

def copy_images!
Expand All @@ -60,6 +70,10 @@ def set_metadata!
epub.uid config[:uid]
epub.identifier config[:identifier][:id],
scheme: config[:identifier][:type]

# epubchecker complains when assigning an image directly,
# but if we don't, then Apple Books doesn't render the cover.
# Need to investigate some more.
epub.cover_page cover_image if cover_image && File.exist?(cover_image)
end

Expand Down Expand Up @@ -91,6 +105,22 @@ def write_sections!
link.set_attribute("href", links.fetch(href, href))
end

# Normalize <ol>
#
section.html.css("ol[start]").each do |node|
node.remove_attribute("start")
end

# Remove anchors (only useful for html exports).
#
section.html.css("a.anchor").each(&:remove)

# Remove tabindex (only useful for html exports).
#
section.html.css("[tabindex]").each do |node|
node.remove_attribute("tabindex")
end

# Replace all srcs.
#
section.html.css("[src]").each do |element|
Expand Down Expand Up @@ -120,26 +150,53 @@ def render_chapter(content)

def assets
@assets ||= begin
assets = Dir[root_dir.join("templates/epub/*.css")]
assets = Dir[root_dir.join("output/epub/styles/**/*.css")]
assets += Dir[root_dir.join("images/**/*.{jpg,png,gif}")]
assets
end
end

def cover_image
path =
Dir[root_dir.join("templates/epub/cover.{jpg,png,gif}").to_s].first
path = Dir[root_dir.join("output/epub/images/cover.{jpg,png,gif}").to_s]
.first

path if path && File.exist?(path)
end

def navigation
sections.map do |section|
{
label: section.html.css(":first-child").text,
content: section.filename
}
klass = Struct.new(:level, :data, :parent, keyword_init: true)

root = klass.new(level: 1, data: {nav: []})
current = root

sections.each do |section|
section.html.css("h2, h3, h4, h5, h6").each do |node|
label = CGI.escape_html(node.text.strip)
level = node.name[1].to_i

data = {
label:,
content: "#{section.filename}##{node.attributes['id']}",
nav: []
}

if level > current.level
current = klass.new(level:, data:, parent: current)
elsif level == current.level
current = klass.new(level:, data:, parent: current.parent)
else
while current.parent && current.parent.level >= level
current = current.parent
end

current = klass.new(level:, data:, parent: current.parent)
end

current.parent.data[:nav] << data
end
end

root.data[:nav]
end

def template_path
Expand Down
4 changes: 2 additions & 2 deletions lib/kitabu/exporter/pdf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ def apply_footnotes!
end

def create_html_file(target, html, class_name)
html.css("html").first.set_attribute "class", class_name
html.css("body").first.set_attribute "class", class_name
html
.css("link[name=stylesheet]")
.css("link[rel=stylesheet]")
.first
.set_attribute "href", "styles/#{class_name}.css"

Expand Down
7 changes: 7 additions & 0 deletions lib/kitabu/extensions/eeepub.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

EeePub::NCX.class_eval do
def uid
@uid[:id]
end
end
4 changes: 4 additions & 0 deletions lib/kitabu/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def copy_config_file
template "config.erb", "config/kitabu.yml"
end

def copy_i18n_file
copy_file "en.yml", "config/locales/en.yml"
end

def copy_helper_file
copy_file "helper.rb", "config/helper.rb"
end
Expand Down
34 changes: 33 additions & 1 deletion lib/kitabu/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

module Kitabu
module Helpers
def css_translations
backend = I18n.backend.translations

translations =
backend.each_with_object([]) do |(lang, dict), buffer|
buffer << ":root[lang='#{lang}'] {"

dict.each do |key, value|
next unless value.is_a?(String) && value.lines.count == 1

buffer << "--#{key.to_s.tr('_', '-')}-text: #{value.inspect};"
end

buffer << "}"
end

translations.join("\n")
end

def highlight_theme(name = theme)
html = '<style type="text/css">'
html << Rouge::Theme.find(name).render(scope: ".highlight")
Expand All @@ -19,7 +38,20 @@ def escape_html(content)

def note(class_name = :info, &block)
content = block_content(block)
output << ('<div class="note %s">' % escape_html(class_name)) # rubocop:disable Style/FormatString
type = class_name.to_s
title = I18n.t(
type,
scope: :notes,
default: type.titleize
)
output << format(
'<div class="note %{type}">',
type: escape_html(class_name)
)
output << format(
'<p class="note--title">%{title}</p>',
title: escape_html(title)
)
output << markdown(content)
output << "</div>"
end
Expand Down
32 changes: 23 additions & 9 deletions lib/kitabu/toc/epub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,30 @@ def initialize(navigation)
end

def to_html
data = OpenStruct.new(navigation:).instance_eval { binding }
data = OpenStruct
.new(navigation:, helpers: self)
.instance_eval { binding }
ERB.new(template).result(data)
end

def render_navigation(nav)
return if nav.empty?

html = []
html << "<ol>"

nav.each do |item|
html << "<li>"
html << %[<a href="#{item[:content]}">#{item[:label]}</a>]
html << render_navigation(item[:nav]) if item[:nav].any?
html << "</li>"
end

html << "</ol>"

html.join
end

def template
(+<<-HTML).strip_heredoc.force_encoding("utf-8")
<?xml version="1.0" encoding="utf-8" ?>
Expand All @@ -23,17 +43,11 @@ def template
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="epub.css"/>
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
<title>Table of Contents</title>
<title><%= I18n.t(:table_of_contents, default: "Table of Contents") %></title>
</head>
<body>
<div id="toc">
<ul>
<% navigation.each do |nav| %>
<li>
<a href="<%= nav[:content] %>"><%= nav[:label] %></a>
</li>
<% end %>
</ul>
<%= helpers.render_navigation(navigation) %>
</div>
</body>
</html>
Expand Down
Loading

0 comments on commit 5630a87

Please sign in to comment.