# encoding: utf-8 # Copyright (c) 2008-2013 Hongli Lai # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'nokogiri' require 'mizuho' require 'mizuho/source_highlight' require 'mizuho/id_map' require 'mizuho/utils' module Mizuho class GenerationError < StandardError end class Generator def initialize(input, options = {}) @options = options @input_file = input @output_file = options[:output] || default_output_filename(input) @id_map_file = options[:id_map] || default_id_map_filename(input) @icons_dir = options[:icons_dir] @conf_file = options[:conf_file] @attributes = options[:attributes] || [] @enable_topbar = options[:topbar] @no_run = options[:no_run] @commenting_system = options[:commenting_system] @index = options[:index] @index_filename = options[:index_filename] || default_index_filename(input) if @commenting_system == 'juvia' require_options(options, :juvia_url, :juvia_site_key) end end def start if @commenting_system @id_map = IdMap.new if File.exist?(@id_map_file) @id_map.load(@id_map_file) else warn "No ID map file, generating one (#{@id_map_file})..." end end if !@no_run self.class.run_asciidoc(@input_file, @output_file, @icons_dir, @conf_file, @attributes) end transform(@output_file) if @commenting_system @id_map.save(@id_map_file) stats = @id_map.stats if stats[:fuzzy] > 0 warn "Warning: #{stats[:fuzzy]} fuzzy ID(s)" end if stats[:orphaned] > 0 warn "Warning: #{stats[:orphaned]} unused ID(s)" end end end def self.run_asciidoc(input, output, icons_dir = nil, conf_file = nil, attributes = []) args = [ ASCIIDOC, "-b", "html5", "-a", "theme=flask", "-a", "icons", "-n" ].flatten if icons_dir args << "-a" args << "iconsdir=#{icons_dir}" end attributes.each do |attribute| args << "-a" args << attribute end if conf_file # With the splat operator we support a string and an array of strings. [*conf_file].each do |cf| args << "-f" args << cf end end args += ["-o", output, input] if !system(*args) raise GenerationError, "Asciidoc failed." end end private def default_output_filename(input) return File.dirname(input) + "/" + File.basename(input, File.extname(input)) + ".html" end def default_id_map_filename(input) return File.dirname(input) + "/" + File.basename(input, File.extname(input)) + ".idmap.txt" end def default_index_filename(input) return File.dirname(input) + "/" + File.basename(input, File.extname(input)) + ".index.sqlite3" end def warn(message) STDERR.puts(message) end def transform(filename) File.open(filename, 'r+') do |f| doc = Nokogiri.HTML(f) head = (doc / "head")[0] body = (doc / "body")[0] title = (doc / "title")[0].text header_div = (doc / "#header")[0] headers = (doc / "#content" / "h1, h2, h3, h4") head.add_child(make_node(stylesheet_tag, doc)) # Remove footer with generation timestamp. (doc / "#footer-text").remove # Add commenting balloons. if @commenting_system titles = [] headers.each do |header| if header['class'] !~ /float/ titles << header.text end end @id_map.generate_associations(titles) headers.each do |header| if header['class'] !~ /float/ titles << header.text header['data-comment-topic'] = @id_map.associations[header.text] header.add_previous_sibling(make_node(create_comment_balloon, doc)) end end end # Add top bar. if @enable_topbar body.children.first.add_previous_sibling(make_node(topbar(title), doc)) end # Add Mizuho Javascript. body.add_child(javascript_tag(doc)) # Move preamble from content area to header area. if preamble = (doc / "#preamble")[0] preamble.remove header_div.add_child(make_node(preamble, doc)) end # Create a TOC after the preamble. toc_div = add_child_and_get(header_div, %Q{
}) if @commenting_system # Add a commenting balloon to the TOC title. toc_div.add_child(make_node(create_comment_balloon, doc)) end toc_div.add_child(make_node(%Q{
Table of Contents
}, doc)) headers.each do |header| if header['class'] !~ /float/ level = header.name.scan(/\d+/).first div = add_child_and_get(toc_div, "
") link = add_child_and_get(div, "") link['href'] = '#' + header['id'] link.content = header.text end end if @enable_topbar # Add invisible spans before each header so that anchor jumps # don't hide the header behind the top bar. # http://nicolasgallagher.com/jump-links-and-viewport-positioning/ headers.each do |header| span = add_previous_sibling_and_get(header, '') span['id'] = header['data-anchor'] = header['id'] header.remove_attribute('id') end end if @index # Add docid attributes to headers. headers.each do |header| next if header['class'] =~ /float/ docid = Utils.title_to_docid(header.text) header['data-docid'] = docid.to_s header['class'] = "#{header['class']} docid-#{docid}".strip end create_search_index(headers, @index_filename) end f.rewind f.truncate(0) f.puts(doc.to_html) end end def stylesheet_tag content = %Q{\n} return content end def topbar(title) content = render_template("topbar.html") content.gsub!(/\{TITLE\}/, title) return content end def javascript_tag(doc) node = Nokogiri::XML::Node.new('script', doc) content = "" content << File.read("#{TEMPLATES_DIR}/jquery-1.7.1.min.js") << "\n" content << File.read("#{TEMPLATES_DIR}/jquery.hashchange-1.0.0.js") << "\n" content << File.read("#{TEMPLATES_DIR}/mizuho.js") << "\n" if @enable_topbar content << File.read("#{TEMPLATES_DIR}/topbar.js") << "\n" end if @commenting_system == 'juvia' content << %Q{ var JUVIA_URL = '#{@options[:juvia_url]}'; var JUVIA_SITE_KEY = '#{@options[:juvia_site_key]}'; } content << File.read("#{TEMPLATES_DIR}/juvia.js") << "\n" end node.content = content return node end def create_comment_balloon return %Q{} end def render_template(name) content = File.read("#{TEMPLATES_DIR}/#{name}") content.gsub!(/\{INLINE_IMAGE:(.*?)\.png\}/) do data = File.open("#{TEMPLATES_DIR}/#{$1}.png", "rb") do |f| f.read end data = [data].pack('m') data.gsub!("\n", "") "data:image/png;base64,#{data}" end return content end def require_options(options, *required_keys) fail = false required_keys.each do |key| if !options.has_key?(key) fail = true argument_name = '--' + key.to_s.gsub('_', '-') STDERR.puts "You must also specify #{argument_name}!" end end exit 1 if fail end # For Nokogiri 1.4.0 compatibility def make_node(html, doc) result = Nokogiri::XML::Node.new('div', doc) result.inner_html = html return result.children[0] end # For Nokogiri 1.4.0 compatibility def add_child_and_get(node, html) result = node.add_child(make_node(html, node.document)) result = result[0] if result.is_a?(Array) return result end # For Nokogiri 1.4.0 compatibility def add_previous_sibling_and_get(node, html) result = node.add_previous_sibling(make_node(html, node.document)) result = result[0] if result.is_a?(Array) return result end def gather_content(header) result = [] elem = header while true elem = elem.next_sibling if !elem || elem.name =~ /^h/i break else text = elem.text.strip if !text.empty? text.gsub!(/\r?\n/, " ") result << text end end end return result.join(" ") end def create_search_index(headers, filename) require 'sqlite3' db = SQLite3::Database.new("#{filename}.tmp") db.transaction do db.execute(%q{ CREATE VIRTUAL TABLE book USING fts4( title TEXT NOT NULL, content TEXT NOT NULL, content="", tokenize=porter ) }) db.execute(%q{ CREATE TABLE version( version INTEGER NOT NULL ) }) db.execute("INSERT INTO version VALUES(1)") db.prepare("INSERT INTO book(docid, title, content) VALUES(?, ?, ?)") do |stmt| headers.each do |header| next if header['class'] =~ /float/ title = header.text.strip docid = Utils.title_to_docid(title) content = gather_content(header) stmt.execute(docid, title, content) end end end db.execute("INSERT INTO book(book) VALUES('optimize')") db.execute("VACUUM") db.close File.rename("#{filename}.tmp", filename) rescue Exception => e File.unlink("#{filename}.tmp") rescue nil raise e end end end