require 'fileutils' module Softcover module Builders class Html < Builder include Softcover::Utils def setup Dir.mkdir "html" unless File.directory?("html") html_styles = path('html/stylesheets') unless File.directory?(html_styles) Dir.mkdir html_styles end template_dir = path("#{File.dirname(__FILE__)}/../template") custom_css = path("#{template_dir}/html/stylesheets/custom.css") target = path("#{html_styles}/custom.css") FileUtils.cp(custom_css, target) unless File.exist?(target) clean! end def build(options = {}) if Softcover::profiling? require 'ruby-prof' RubyProf.start end write_master_latex_file(manifest) if manifest.markdown? unless options[:'find-overfull'] remove_unneeded_polytex_files end manifest.chapters.each do |chapter| write_latex_files(chapter, options) end # Reset the manifest to use PolyTeX. self.manifest = Softcover::BookManifest.new(source: :polytex, verify_paths: false, origin: :markdown) end if manifest.polytex? basename = File.basename(manifest.filename, '.tex') @html = converted_html(basename) @title = basename @mathjax = Softcover::Mathjax::config(chapter_number: false) @src = Softcover::Mathjax::AMS_SVG erb_file = File.read(File.join(File.dirname(__FILE__), '..', 'server', 'views', 'book.html.erb')) file_content = ERB.new(erb_file).result(binding) write_full_html_file(manifest.slug, file_content) write_chapter_html_files(Nokogiri::HTML(file_content), erb_file) end if Softcover::profiling? result = RubyProf.stop printer = RubyProf::GraphPrinter.new(result) printer.print(STDOUT, {}) end true end # Removes any PolyTeX files not corresponding to current MD chapters. def remove_unneeded_polytex_files files_to_keep = manifest.chapters.map do |chapter| path("#{manifest.polytex_dir}/#{chapter.slug}.tex") end all_files = Dir.glob(path("#{manifest.polytex_dir}/*.tex")) files_to_remove = all_files - files_to_keep FileUtils.rm(files_to_remove) end # Writes the LaTeX files for a given Markdown chapter. def write_latex_files(chapter, options = {}) polytex_filename = path("#{manifest.polytex_dir}/#{chapter.slug}.tex") if chapter.source == :polytex FileUtils.cp path("chapters/#{chapter.full_name}"), polytex_filename else mkdir Softcover::Directories::TMP markdown = File.read(path("chapters/#{chapter.full_name}")) # Only write if the Markdown file hasn't changed since the last time # it was converted, as then the current PolyTeX file is up-to-date. # The call to File.exist?(filename) is just in case the PolyTeX file # corresponding to the Markdown file was removed by hand in the # interim. unless (File.exist?(chapter.cache_filename) && File.read(chapter.cache_filename) == digest(markdown) && File.exist?(polytex_filename) && !markdown.include?('\input')) File.write(polytex_filename, polytex(chapter, markdown)) end end end # Returns the PolyTeX for the chapter. # As a side-effect, we cache a digest of the Markdown to prevent # unnecessary conversions. def polytex(chapter, markdown) File.write(chapter.cache_filename, digest(markdown)) p = Polytexnic::Pipeline.new(markdown, source: :markdown, custom_commands: Softcover.custom_styles, language_labels: language_labels) p.polytex end # Returns the converted HTML. def converted_html(basename) polytex_filename = basename + '.tex' polytex = File.read(polytex_filename) # Replace the includes with the file contents, padding with a trailing # newline for safety. polytex.gsub!(/(^\s*\\include{(.*?)})/) do File.read($2 + '.tex') + "\n" end Polytexnic::Pipeline.new(polytex, custom_commands: Softcover.custom_styles, language_labels: language_labels).to_html end # Writes the full HTML file for the book. # The resulting file is a self-contained HTML document suitable # for viewing in isolation. def write_full_html_file(basename, file_content) html_filename = File.join('html', basename + '.html') File.open(html_filename, 'w') do |f| f.write(file_content) end polytexnic_css = File.join('html', 'stylesheets', 'softcover.css') source_css = File.join(File.dirname(__FILE__), "../template/#{polytexnic_css}") FileUtils.cp source_css, polytexnic_css write_pygments_file(:html, File.join('html', 'stylesheets')) built_files.push html_filename end # Writes the full HTML file for each chapter. # The resulting files are self-contained HTML documents suitable # for viewing in isolation. def write_chapter_html_files(html, erb_file) reference_cache = split_into_chapters(html) target_cache = build_target_cache(html) manifest.chapters.each_with_index do |chapter, i| update_cross_references(chapter, reference_cache, target_cache) write_fragment_file(chapter) write_complete_file(chapter, erb_file, i) end end # Splits the full XML document into chapters. def split_into_chapters(xml) chapter_number = 0 current_chapter = manifest.chapters.first reference_cache = {} xml.css('#book>div').each do |node| klass = node.attributes['class'].to_s id = node.attributes['id'].to_s if klass == 'chapter' || id == 'frontmatter' current_chapter = manifest.chapters[chapter_number] node['data-chapter'] = current_chapter.slug chapter_number += 1 end reference_cache[node['data-tralics-id']] = current_chapter node.xpath('.//*[@data-tralics-id]').each do |labeled_node| reference_cache[labeled_node['data-tralics-id']] = current_chapter end current_chapter.nodes.push node end reference_cache end # Builds a cache of targets for cross-references. def build_target_cache(xml) {}.tap do |target_cache| xml.xpath("//*[@id]").each do |target| target_cache[target['id']] = target end end end # Updates the book's cross-references. def update_cross_references(chapter, ref_map, target_cache) chapter.nodes.each do |node| node.css('a.hyperref').each do |ref_node| ref_id = ref_node['href'][1..-1] # i.e., 'cha-foo_bar' target = target_cache[ref_id] unless target.nil? id = target['id'] ref_chapter = if target['data-tralics-id'].nil? # This branch is true for chapter-star. chapter else ref_map[target['data-tralics-id']] end ref_node['href'] = "#{ref_chapter.fragment_name}##{id}" end end end end # Writes the chapter fragment HTML (omitting, e.g., tags, etc.) def write_fragment_file(chapter) html_filename = File.join('html', "#{chapter.slug}_fragment.html") File.open(html_filename, 'w') do |f| chapter.nodes.each do |node| f.write(node.to_xhtml) end end built_files.push html_filename end # Writes the chapter as a complete, self-contained HTML document. def write_complete_file(chapter, erb_file, n) html_filename = File.join('html', chapter.slug + '.html') File.open(html_filename, 'w') do |f| @html = chapter.nodes.map(&:to_xhtml).join("\n") @mathjax = Softcover::Mathjax::config(chapter_number: n) @src = Softcover::Mathjax::AMS_SVG file_content = ERB.new(erb_file).result(binding) f.write(file_content) end built_files.push html_filename end def clean! # It's safe to remove HTML files in the html/ directory, # as they are regenerated every time the book gets built. # This also arranges to clear out unused HTML files, as happens when, # e.g., the name of a LaTeX chapter file changes. FileUtils.rm(Dir.glob(path('html/*.html'))) end end end end