From 5630a87906fe21fc51854fbf34043f8e15524ab0 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Tue, 23 Jan 2024 19:31:29 -0800 Subject: [PATCH] Improve epub final file. (#66) Improvements include better table of contents, less epubcheck errors, CSS localization, and more. --- CHANGELOG.md | 2 + README.md | 19 ++--- lib/kitabu.rb | 1 + lib/kitabu/cli.rb | 11 ++- lib/kitabu/exporter.rb | 10 +-- lib/kitabu/exporter/base.rb | 14 ++++ lib/kitabu/exporter/epub.rb | 79 ++++++++++++++++--- lib/kitabu/exporter/pdf.rb | 4 +- lib/kitabu/extensions/eeepub.rb | 7 ++ lib/kitabu/generator.rb | 4 + lib/kitabu/helpers.rb | 34 +++++++- lib/kitabu/toc/epub.rb | 32 +++++--- lib/kitabu/toc/html.rb | 2 +- spec/kitabu/exporter/epub_spec.rb | 4 +- spec/kitabu/exporter/pdf_spec.rb | 4 +- spec/support/have_tag.rb | 2 +- spec/support/mybook/config/kitabu.yml | 7 +- spec/support/mybook/templates/html/layout.erb | 10 ++- templates/Gemfile | 1 + templates/config.erb | 9 ++- templates/en.yml | 13 +++ templates/helper.rb | 20 +++++ templates/templates/epub/cover.erb | 6 +- templates/templates/epub/page.erb | 6 +- templates/templates/html/layout.erb | 13 ++- templates/templates/styles/epub.css | 43 +++++++++- templates/templates/styles/html.css | 36 +++++---- templates/templates/styles/pdf.css | 18 ++--- templates/templates/styles/print.css | 4 +- templates/text/03_Syntax_Highlighting.md.erb | 8 +- templates/text/04_Dynamic_Content.md.erb | 22 ------ 31 files changed, 321 insertions(+), 124 deletions(-) create mode 100644 lib/kitabu/extensions/eeepub.rb create mode 100644 templates/en.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d56bf..276552f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index ed46f51..e230e2b 100644 --- a/README.md +++ b/README.md @@ -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 -
-

Make sure you try .md.erb files!

-
+ ``` -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 -. +. ### Syntax Highlighting diff --git a/lib/kitabu.rb b/lib/kitabu.rb index 72b591f..eb40384 100644 --- a/lib/kitabu.rb +++ b/lib/kitabu.rb @@ -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" diff --git a/lib/kitabu/cli.rb b/lib/kitabu/cli.rb index 2fdf242..5c1ad52 100644 --- a/lib/kitabu/cli.rb +++ b/lib/kitabu/cli.rb @@ -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, diff --git a/lib/kitabu/exporter.rb b/lib/kitabu/exporter.rb index 2d084df..a4cb9cf 100644 --- a/lib/kitabu/exporter.rb +++ b/lib/kitabu/exporter.rb @@ -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") @@ -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 diff --git a/lib/kitabu/exporter/base.rb b/lib/kitabu/exporter/base.rb index 8ea5252..a60581b 100644 --- a/lib/kitabu/exporter/base.rb +++ b/lib/kitabu/exporter/base.rb @@ -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 @@ -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? diff --git a/lib/kitabu/exporter/epub.rb b/lib/kitabu/exporter/epub.rb index 81652c7..e601c7a 100644 --- a/lib/kitabu/exporter/epub.rb +++ b/lib/kitabu/exporter/epub.rb @@ -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 @@ -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 @@ -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! @@ -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 @@ -91,6 +105,22 @@ def write_sections! link.set_attribute("href", links.fetch(href, href)) end + # Normalize
    + # + 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| @@ -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 diff --git a/lib/kitabu/exporter/pdf.rb b/lib/kitabu/exporter/pdf.rb index 9df58e5..0c584c6 100644 --- a/lib/kitabu/exporter/pdf.rb +++ b/lib/kitabu/exporter/pdf.rb @@ -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" diff --git a/lib/kitabu/extensions/eeepub.rb b/lib/kitabu/extensions/eeepub.rb new file mode 100644 index 0000000..224db32 --- /dev/null +++ b/lib/kitabu/extensions/eeepub.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +EeePub::NCX.class_eval do + def uid + @uid[:id] + end +end diff --git a/lib/kitabu/generator.rb b/lib/kitabu/generator.rb index 90cd56d..9b7c584 100644 --- a/lib/kitabu/generator.rb +++ b/lib/kitabu/generator.rb @@ -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 diff --git a/lib/kitabu/helpers.rb b/lib/kitabu/helpers.rb index 3e6bc4d..9789056 100644 --- a/lib/kitabu/helpers.rb +++ b/lib/kitabu/helpers.rb @@ -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 = '
    diff --git a/templates/Gemfile b/templates/Gemfile index 927ade5..a9501f2 100644 --- a/templates/Gemfile +++ b/templates/Gemfile @@ -1,5 +1,6 @@ # frozen_string_literal: true source "https://rubygems.org" + gem "guard-shell" gem "kitabu" diff --git a/templates/config.erb b/templates/config.erb index 83dc2ea..9705be8 100644 --- a/templates/config.erb +++ b/templates/config.erb @@ -1,3 +1,4 @@ +--- # The book's title. Will be used everywhere! title: "Kitabu" @@ -28,12 +29,12 @@ subject: "This guide will help you understand how all of the pieces fit together keywords: "kitabu, e-book, calibre, epub, mobi, pdf, prince" # Some unique identification. Works great with your domain -# like `http://yourbook.example.com`. +# like `yourbook.example.com`. uid: "<%= @uid %>" # Your book identification like ISBN or ISSN. identifier: - id: "http://yourbook.example.com" + id: "https://yourbook.example.com" type: "URL" # can be ISBN, ISSN or URL # This book authors. @@ -42,3 +43,7 @@ authors: # Syntax highlight theme. Can be any of supported by Rouge. theme: github + +# Accent color. This will be set as a CSS variable, so you can use it on all +# CSS files like `color: var(--accent-color)`. +accent_color: "#5091b1" diff --git a/templates/en.yml b/templates/en.yml new file mode 100644 index 0000000..d6d2f2b --- /dev/null +++ b/templates/en.yml @@ -0,0 +1,13 @@ +--- +en: + table_of_contents: Table of Contents + content: Content + left_blank: This page intentionally left blank + chapter: Chapter + + notes: + warning: Warning! + note: Just so you know… + tip: Here’s a tip! + caution: Caution! + important: Important! diff --git a/templates/helper.rb b/templates/helper.rb index a129a13..888bc0c 100644 --- a/templates/helper.rb +++ b/templates/helper.rb @@ -1,7 +1,27 @@ # frozen_string_literal: true module Kitabu + # If you need to process the markdown before rendering it, you can use the + # `before_markdown_render` hook. + # + # Kitabu.add_hook(:before_markdown_render) do |content| + # content + # end + + # Similarly, you can manipulate the generated HTML by using the hook + # `after_markdown_render`. + # + # Kitabu.add_hook(:after_markdown_render) do |content| + # html = Nokogiri::HTML(content) + # + # # Do something with HTML here. + # + # html.to_html + # end + module Helpers + # This method is just a helper example. + # Feel free to remove it once you get rid of the sample output. def lexers_list buffer = [%[
      ]] diff --git a/templates/templates/epub/cover.erb b/templates/templates/epub/cover.erb index e21dcc8..25b6525 100644 --- a/templates/templates/epub/cover.erb +++ b/templates/templates/epub/cover.erb @@ -2,14 +2,14 @@ - + <%= title %> - + - +

      <%= title %>

      diff --git a/templates/templates/epub/page.erb b/templates/templates/epub/page.erb index c5a23ae..66bfd54 100644 --- a/templates/templates/epub/page.erb +++ b/templates/templates/epub/page.erb @@ -1,14 +1,14 @@ - + - + <%= highlight_theme %> - +
      <%= content %>
      diff --git a/templates/templates/html/layout.erb b/templates/templates/html/layout.erb index 84c91c1..906cdb3 100644 --- a/templates/templates/html/layout.erb +++ b/templates/templates/html/layout.erb @@ -1,14 +1,21 @@ - + <%= title %> - + <%= highlight_theme %> + @@ -23,7 +30,7 @@
      -

      Contents

      +

      <%= I18n.t(:content) %>

      <%= toc %>
      diff --git a/templates/templates/styles/epub.css b/templates/templates/styles/epub.css index 2539d4a..83602be 100644 --- a/templates/templates/styles/epub.css +++ b/templates/templates/styles/epub.css @@ -1 +1,42 @@ -@import "./files/normalize.css"; +/* +Note: + +The epub flattens all files into the root directory. +This means you need to reference assets by assuming there's no directory +structure organizing your epub. + +This applies to everything, like importing css files, or using background +images. + +Also notice that the generated HTML will normalize image paths and stylesheets +imports. +*/ +@import "normalize.css"; + +/* TOC */ +ol { + list-style-type: none; + counter-reset: item; + margin: 0; + padding: 0; +} + +ol > li { + display: table; + counter-increment: item; + margin-bottom: 0.6em; +} + +ol > li:before { + content: counters(item, ".") ". "; + display: table-cell; + padding-right: 0.6em; +} + +ol > li ol > li { + margin: 0; +} + +li ol > li:before { + content: counters(item, ".") " "; +} diff --git a/templates/templates/styles/html.css b/templates/templates/styles/html.css index d4b83b5..00fd078 100644 --- a/templates/templates/styles/html.css +++ b/templates/templates/styles/html.css @@ -1,8 +1,8 @@ @import "./files/normalize.css"; :root { - --cover-color: #5091b1; - --link-color: var(--cover-color); + --cover-color: var(--accent-color); + --link-color: var(--accent-color); } html { @@ -11,6 +11,8 @@ html { } body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; width: 700px; padding: 50px; background: #fff; @@ -25,6 +27,7 @@ a { p code, li code { color: #3f9c55; + font-size: 13px; } /* Format cover page. */ @@ -65,14 +68,15 @@ li code { margin-top: 0; } -// Code highlighting -.highlight { +/* Code highlighting */ +div.highlight { padding: 15px; } -.highlight .gutter { - color: #ccc; - padding-right: 15px; +div.highlight pre { + margin: 0; + overflow: auto; + font-size: 13px; } /* Table of contents */ @@ -82,7 +86,7 @@ li code { border-bottom: 1px solid #eee; } -// Define chapter numbering +/* Define chapter numbering */ .chapters { counter-reset: chapter 1; } @@ -92,7 +96,7 @@ li code { } .chapter h2:before { - content: "Chapter " counter(chapter); + content: var(--chapter-text) " " counter(chapter); display: block; text-transform: uppercase; font-weight: normal; @@ -152,23 +156,23 @@ li code { } /* Format lexers list */ -.lexers-list { +.lexers { margin: 0; padding: 0; - -webkit-columns: 2; columns: 2; + gap: 50px; } -.lexers-list li { - -webkit-column-break-inside: avoid; +.lexers li { break-inside: avoid-column; } -.lexers-list span { +.lexers span { font-size: 13px; + word-wrap: anywhere; } -.lexers-list li + li { +.lexers li + li { margin-top: 15px; } @@ -210,7 +214,7 @@ li code { } .table-of-contents .level2:before { - content: "Chapter " counter(toc-level2); + content: var(--chapter-text) " " counter(toc-level2); font-size: 13px; text-transform: uppercase; font-weight: bold; diff --git a/templates/templates/styles/pdf.css b/templates/templates/styles/pdf.css index 2f529cc..e4a1e99 100644 --- a/templates/templates/styles/pdf.css +++ b/templates/templates/styles/pdf.css @@ -1,8 +1,7 @@ @import "./files/normalize.css"; :root { - --frontcover-color: #5091b1; - --frontcover-darker-color: #40748e; + --frontcover-color: var(--accent-color); --frontcover-padding: 50px; --page-height: 22.86cm; --page-height: 19.05cm; @@ -39,7 +38,6 @@ @page frontcover { margin: 0; background: var(--frontcover-color); - border-left: 50px solid var(--frontcover-darker-color); } @page chapter { @@ -86,7 +84,7 @@ a { } .white-page:before { - content: "This page intentionally left blank"; + content: var(--left-blank-text); text-transform: uppercase; position: absolute; left: 0; @@ -173,7 +171,7 @@ a { } .chapter > h2:before { - content: "Chapter " counter(chapter); + content: var(--chapter-text) " " counter(chapter); display: block; font-size: 18px; text-transform: uppercase; @@ -202,23 +200,23 @@ li code { } /* Format lexers list */ -.lexers-list { +.lexers { margin: 0; padding: 0; columns: 2; list-style: none; + gap: 50px; } -.lexers-list li { - -webkit-column-break-inside: avoid; +.lexers li { break-inside: avoid-column; } -.lexers-list span { +.lexers span { font-size: 13px; } -.lexers-list li + li { +.lexers li + li { margin-top: 15px; } diff --git a/templates/templates/styles/print.css b/templates/templates/styles/print.css index 3cf812f..775691f 100644 --- a/templates/templates/styles/print.css +++ b/templates/templates/styles/print.css @@ -1,2 +1,2 @@ -@import "./files/normalize.css"; -@import "./pdf.css"; +@import "normalize.css"; +@import "pdf.css"; diff --git a/templates/text/03_Syntax_Highlighting.md.erb b/templates/text/03_Syntax_Highlighting.md.erb index 10939f7..f67f218 100644 --- a/templates/text/03_Syntax_Highlighting.md.erb +++ b/templates/text/03_Syntax_Highlighting.md.erb @@ -33,9 +33,11 @@ class User end ``` -<% note do %> If you're using Sublime Text, make sure you install the -[Markdown Extended](https://packagecontrol.io/packages/Markdown%20Extended) -plugin; it enables code syntax highlighting on your Markdown files. <% end %> +> [!NOTE] +> +> If you're using Sublime Text, make sure you install the +> [Markdown Extended](https://packagecontrol.io/packages/Markdown%20Extended) +> plugin; it enables code syntax highlighting on your Markdown files. You can also provide inline options such as line numbers and inline rendering. diff --git a/templates/text/04_Dynamic_Content.md.erb b/templates/text/04_Dynamic_Content.md.erb index b124dbb..e270a32 100644 --- a/templates/text/04_Dynamic_Content.md.erb +++ b/templates/text/04_Dynamic_Content.md.erb @@ -15,28 +15,6 @@ created a helper that looks like this: To use it, I just needed to add `<%%= lexers_list %>` to my text file. This allows you to create anything you need! -Kitabu comes with some built-in helpers, such as `note`. With this helper, you -can create a note that generates a HTML structure, so you can easily style it. -The syntax for using the `note` helper is `note(type, &block)`. - -```erb -<%% note do %> - Some text that will be parsed as Markdown. -<%% end %> -``` - -By default, this will generate a `
      ` tag, but you can use -anything you want. - -```erb -<%% note :warning do %> - Some text that will be parsed as Markdown. -<%% end %> -``` - -[Check out the source](https://github.com/fnando/kitabu/blob/cleanup/lib/kitabu/helpers.rb) -for a sample on how to create block helpers like `note`. - ### Escaping ERb code If you want to write a book about Rails, you're likely to use lots of ERb tags.