#!/usr/bin/ruby -w # # erbook is an extensible document processor based on eRuby. # # * The standard input stream will be read if an input file is not specified. # # * The final output document will be written to the standard output stream. # # * If an error occurs, the input document will be written to the standard # output stream, so that you can investigate line numbers in the error. # # Usage: # # erbook [Option...] FormatName [InputFile] # erbook [Option...] FormatFile [InputFile] # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'erbook' module ERBook # Prints the given message and raises the given error. def ERBook.error aMessage, aError = $! STDERR.printf "%s:\n\n", aMessage raise aError end require 'digest/sha1' # Returns a digest of this string that's not altered by String#to_html. def ERBook.digest aInput # XXX: surround all digits with alphabets so # Maruku doesn't change them into HTML Digest::SHA1.hexdigest(aInput.to_s).gsub(/\d/, 'z\&z') end require 'erb' class Template < ERB # The result of template evaluation thus far. attr_reader :buffer # aSource:: String that replaces the ambiguous '(erb)' # identifier in stack traces, so that the user # can better determine the source of an error. # # aInput:: String containing eRuby directives. This # string will be modified by this method! # # aSafeLevel:: See safe_level in ERB::new(). # def initialize aSource, aInput, aUnindent = false, aSafeLevel = nil # convert "% at beginning of line" usage into <% normal %> usage aInput.gsub! %r{^([ \t]*)(%[=# \t].*)$}, '\1<\2 %>' aInput.gsub! %r{^([ \t]*)%%}, '\1%' # silence the code-only <% ... %> directive, just like PHP does aInput.gsub! %r{^[ \t]*(<%[^%=]((?!<%).)*?[^%]%>)[ \t]*\r?\n}m, '\1' # unindent node content hierarchically if aUnindent tags = aInput.scan(/<%(?:.(?!<%))*?%>/m) margins = [] result = [] buffer = aInput tags.each do |tag| chunk, buffer = buffer.split(tag, 2) chunk << tag # perform unindentation result << chunk.gsub(/^#{margins.last}/, '') # prepare for next unindentation case tag when /<%[^%=].*?\bdo\b.*?%>/m margins.push buffer[/^[ \t]*(?=\S)/] when /<%\s*end\s*%>/m margins.pop end end result << buffer aInput = result.join end # use @buffer to store the result of the ERB template super aInput, aSafeLevel, nil, :@buffer self.filename = aSource end # Renders this template within a fresh object that # is populated with the given instance variables. def render_with aInstVars = {} context = Object.new.instance_eval do aInstVars.each_pair do |var, val| instance_variable_set var, val end binding end result(context) end private # Returns the content that the given block wants to append to # the buffer. If the given block does not want to append to the # buffer, then returns the result of invoking the given block. def content_from_block *aBlockArgs raise ArgumentError, 'block must be given' unless block_given? head = @buffer.length body = yield(*aBlockArgs) # this will do: @buffer << content tail = @buffer.length if tail > head @buffer.slice! head..tail else body end.to_s end end require 'ostruct' class Node < OpenStruct # deprecated in Ruby 1.8; removed in Ruby 1.9 undef id if respond_to? :id undef type if respond_to? :type end # XXX: the basename() is for being launched by a RubyGems executable if __FILE__ == $0 or File.basename(__FILE__) == File.basename($0) require 'yaml' # parse command-line options begin require 'rubygems' rescue LoadError end require 'trollop' opts = Trollop::options do # show program description located at the top of this file banner File.read(__FILE__)[/\A.*?^$\n/m]. gsub(/^# ?/, '').sub(/\A.*?\n/, '') banner '' # show list of available formats banner 'FormatName:' ERBook::FORMAT_FILES.each do |file| name = File.basename(file, '.yaml') desc = YAML.load_file(file)['desc'] rescue nil banner '%16s: %s' % [name, desc] end banner '' # show list of command-line options banner 'Option:' opt :unindent, 'Unindent node content hierarchically' # show program version information version [ "project: #{ERBook::PROJECT}", "version: #{ERBook::VERSION}", "release: #{ERBook::RELEASE}", "website: #{ERBook::WEBSITE}", "install: #{ERBook::INSTALL_DIR}", ].join("\n") end # load format specification file spec_file = ARGV.shift or raise ArgumentError, "Format was not specified. Run `#{$0} -h` for help." File.file? spec_file or spec_file = File.join(ERBook::FORMATS_DIR, spec_file + '.yaml') begin spec_data = YAML.load_file(spec_file) spec_data[:file] = File.expand_path(spec_file) spec_data[:name] = File.basename(spec_file).sub(/\..*?$/, '') if spec_data.key? 'code' eval spec_data['code'].to_s, TOPLEVEL_BINDING, "#{spec_file}:code" end rescue Exception error "Error when loading the format specification file (#{spec_file.inspect})" end # load input document if input_file = ARGV.shift input = File.read input_file else input_file, input = 'STDIN', STDIN.read end begin # expand all "include" directives in the input begin end while input.gsub! %r{<%#\s*include\s+(.+?)\s*#%>} do file, line = $1, $`.count("\n").next # provide more accurate stack trace for # errors originating from included files. # # NOTE: eRuby does NOT seem to provide line numbers for trace # entries that are deeper than the input document itself # "<% begin %>#{File.read file}<% rescue Exception => err bak = err.backtrace # set the input document's originating line number to # where this file was included in the input document top = bak.find {|t| t =~ /#{/#{input_file}/}:\\d+$/ } top.sub! %r/\\d+$/, '#{line}' # add a stack trace entry mentioning this included file ins = bak.index top bak.insert ins, #{file.inspect} raise err end %>" end # create sandbox for input evaluation template = Template.new(input_file, input, opts[:unindent]) template_vars = { :@spec => spec_data, :@roots => roots = [], # root nodes of all trees :@nodes => nodes = [], # all nodes in the forest :@types => types = Hash.new {|h,k| h[k] = []}, # nodes by type }.each_pair {|k,v| template.instance_variable_set(k, v) } node_defs = spec_data['nodes'].each_pair do |type, defn| template.instance_eval %{ # # XXX: using a string because define_method() # does not accept a block until Ruby 1.9 # def #{type} *aArgs, &aBlock node = Node.new( :type => #{type.inspect}, :args => aArgs, :trace => caller, :children => [] ) @nodes << node @types[node.type] << node # calculate occurrence number for this node if #{defn['number']} @count ||= Hash.new {|h,k| h[k] = []} node.number = (@count[node.type] << node).length end @stack ||= [] # assign node family if parent = @stack.last parent.children << node node.parent = parent node.depth = parent.depth.next # calculate latex-style index number for this node if #{defn['index']} branches = parent.children.select {|n| n.index} node.index = [parent.index, branches.length.next].join('.') end else @roots << node node.parent = nil node.depth = 0 # calculate latex-style index number for this node if #{defn['index']} branches = @roots.select {|n| n.index} node.index = branches.length.next.to_s end end # assign node content if block_given? @stack.push node content = content_from_block(node, &aBlock) @stack.pop digest = ERBook.digest(content) self.buffer << digest else content = nil digest = ERBook.digest(node.object_id) end node.content = content node.digest = digest digest end }, __FILE__, Kernel.caller.first[/\d+/].to_i.next end # build the document tree document = template.instance_eval { result(binding) } # replace nodes with output expander = lambda do |n, buf| # calculate node output source = "#{spec_file}:nodes:#{n.type}:output" n.output = Template.new( source, node_defs[n.type]['output'].to_s.chomp). render_with(template_vars.merge(:@node => n)) # replace node with output if node_defs[n.type]['silent'] buf[n.digest] = '' buf = n.output else buf[n.digest] = n.output end # repeat for all child nodes n.children.each {|c| expander[c, buf] } end roots.each {|n| expander[n, document] } rescue Exception => e # omit erbook internals from the stack trace e.backtrace.reject! {|t| t =~ /^#{$0}:\d+/ } unless $DEBUG puts input # so the user can debug the line numbers in the stack trace error "Error when processing the input document (#{input_file})" end # emit output document puts Template.new("#{spec_file}:output", spec_data['output'].to_s). render_with(template_vars.merge(:@content => document)) end end