lib/erbook/document.rb in erbook-6.1.0 vs lib/erbook/document.rb in erbook-7.0.0

- old
+ new

@@ -1,7 +1,13 @@ +#-- +# Copyright 2008 Suraj N. Kurapati +# See the LICENSE file for details. +#++ + require 'yaml' require 'erbook/template' +require 'digest/sha1' module ERBook class Document # Data from the format specification file. attr_reader :format @@ -14,29 +20,30 @@ # All nodes in the document arranged by node type. attr_reader :nodes_by_type ## - # @param [String] format + # ==== Parameters + # + # [format_name] # Either the short-hand name of a built-in format # or the path to a format specification file. # - # @param [String] input_text + # [input_text] # The body of the input document. # - # @param [String] input_file + # [input_file] # Name of the file from which the input document originated. # - # @param [Hash] options - # Additional method parameters: + # ==== Options # - # [boolean] :unindent => + # [:unindent] # If true, all node content is unindented hierarchically. # - def initialize format, input_text, input_file, options = {} + def initialize format_name, input_text, input_file, options = {} # process format specification - @format_file = format.to_s + @format_file = format_name.to_s File.file? @format_file or @format_file = File.join(ERBook::FORMATS_DIR, @format_file + '.yaml') begin @@ -56,167 +63,180 @@ # process input document begin # create sandbox for input evaluation template = Template.new(input_file, input_text, options[:unindent]) + sandbox = template.sandbox @template_vars = { :@format => @format, :@roots => @roots = [], # root nodes of all trees :@nodes => @nodes = [], # all nodes in the forest :@nodes_by_type => @nodes_by_type = Hash.new {|h,k| h[k] = [] }, :@stack => [], # stack for all nodes - }.each_pair {|k,v| template.instance_variable_set(k, v) } + }.each_pair {|k,v| sandbox.instance_variable_set(k, v) } - class << template - private + # Handles the method call from a node + # placeholder in the input document. + def sandbox.__node_impl__ node_type, *node_args, &node_content + node = Node.new( + :type => node_type, + :defn => @format['nodes'][node_type], + :args => node_args, + :children => [], - # Handles the method call from a node - # placeholder in the input document. - def __node__ node_type, *node_args, &node_content - node = Node.new( - :type => node_type, - :defn => @format['nodes'][node_type], - :args => node_args, - :children => [], + # omit erbook internals from the stack trace + :trace => caller.reject {|t| + [$0, ERBook::INSTALL].any? {|f| t.index(f) == 0 } + } + ) + @nodes << node + @nodes_by_type[node.type] << node - # omit erbook internals from the stack trace - :trace => caller.reject {|t| - [$0, ERBook::INSTALL].any? {|f| t.index(f) == 0 } - } - ) - @nodes << node - @nodes_by_type[node.type] << node + # calculate occurrence number for this node + if node.defn['number'] + @count ||= Hash.new {|h,k| h[k] = []} + node.number = (@count[node.type] << node).length + end - # calculate occurrence number for this node - if node.defn['number'] - @count ||= Hash.new {|h,k| h[k] = []} - node.number = (@count[node.type] << node).length - end + # assign node family + if parent = @stack.last + parent.children << node + node.parent = parent + node.depth = parent.depth + node.depth += 1 if node.defn['depth'] - # assign node family - if parent = @stack.last - parent.children << node - node.parent = parent - node.depth = parent.depth - node.depth += 1 if node.defn['depth'] + # calculate latex-style index number for this node + if node.defn['index'] + ancestry = @stack.reverse.find {|n| n.defn['index'] }.index + branches = node.parent.children.select {|n| n.index } - # calculate latex-style index number for this node - if node.defn['index'] - ancestry = @stack.reverse.find {|n| n.defn['index'] }.index - branches = node.parent.children.select {|n| n.index } - - node.index = [ancestry, branches.length + 1].join('.') - end - else - @roots << node - node.parent = nil - node.depth = 0 - - # calculate latex-style index number for this node - if node.defn['index'] - branches = @roots.select {|n| n.index } - node.index = (branches.length + 1).to_s - end + node.index = [ancestry, branches.length + 1].join('.') end + else + @roots << node + node.parent = nil + node.depth = 0 - # assign node content - if block_given? - @stack.push node - content = content_from_block(node, &node_content) - @stack.pop - - digest = Document.digest(content) - self.buffer << digest - else - content = nil - digest = Document.digest(node.object_id) + # calculate latex-style index number for this node + if node.defn['index'] + branches = @roots.select {|n| n.index } + node.index = (branches.length + 1).to_s end + end - node.content = content - node.digest = digest - - digest + # assign node content + if block_given? + @stack.push node + node.content = __block_content__(node, &node_content) + @stack.pop end + + @buffer << node + + nil end @node_defs.each_key do |type| # XXX: using a string because define_method() # does not accept a block until Ruby 1.9 - file, line = __FILE__, __LINE__ + 1 - template.instance_eval %{ - def #{type} *node_args, &node_content - __node__ #{type.inspect}, *node_args, &node_content + file, line = __FILE__, __LINE__; eval %{ + def sandbox.#{type} *node_args, &node_content + __node_impl__ #{type.inspect}, *node_args, &node_content end - }, file, line + }, binding, file, line end # evaluate the input & build the document tree - @processed_document = template.instance_eval { result binding } + template.render + @processed_document = template.buffer # chain block-level nodes together for local navigation - block_nodes = @nodes.reject {|n| @node_defs[n.type]['inline'] } + block_nodes = @nodes.reject {|n| n.defn['bypass'] || + n.defn['inline'] } require 'enumerator' block_nodes.each_cons(2) do |a, b| a.next_node = b b.prev_node = a end - # replace node placeholders with their corresponding output - expander = lambda do |n, buf| - # calculate node output - source = "#{@format_file}:nodes:#{n.type}:output" - n.output = Template.new( - source, @node_defs[n.type]['output'].to_s.chomp + # calculate output for all nodes + actual_output_by_node = {} + + visitor = lambda do |n| + # + # allow child nodes to calculate their actual + # output and to set their identifier as Node#output + # + # we do this nodes first because this node's + # content contains the child nodes' output + # + n.children.each {|c| visitor.call c } + + # calculate the output for this node + actual_output = Template.new( + "#{@format_file}:nodes:#{n.type}:output", + n.defn['output'].to_s.chomp ).render_with(@template_vars.merge(:@node => n)) - # expand all child nodes in this node's output - n.children.each {|c| expander[c, n.output] } + # reveal child nodes' actual output in this node's actual output + n.children.each do |c| + if c.defn['inline'] && !c.defn['bypass'] + actual_output[c.output] = actual_output_by_node[c] - # replace this node's placeholder with its output in the buffer - buf[n.digest] = @node_defs[n.type]['silent'] ? '' : n.output + else + # pull block-level node out of paragraph tag added by Maruku + actual_output.sub! %r/(<p>\s*)?#{Regexp.quote c.output}/ do + actual_output_by_node[c] + $1.to_s + end + end + end + + actual_output_by_node[n] = actual_output + + # + # allow the parent node to calculate its actual + # output without interference from the output of + # this node (Node#to_s is aliased to Node#output) + # + # this assumes that having this node's string + # representation be a consecutive sequence of digits + # will not interfere with the text-to-whatever + # transformation defined by the format specification + # + n.output = Digest::SHA1.digest(n.object_id.to_s).unpack('I*').join end - @roots.each {|n| expander[n, @processed_document] } + @roots.each {|n| visitor.call n } + # replace the temporary identifier with each node's actual output + @nodes.each {|n| n.output = actual_output_by_node[n] } + rescue Exception - puts input_text # so the user can debug line numbers in the stack trace + puts input_text # so the user can debug line numbers in stack trace error "Could not process input document #{input_file.inspect}" end end ## # Returns the output of this document. # def to_s Template.new("#{@format_file}:output", @format['output'].to_s). - render_with(@template_vars.merge(:@content => @processed_document)) + render_with(@template_vars.merge(:@content => @processed_document.join)) 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 # Returns the output of this node. def to_s - output + defn['silent'] ? '' : output end - end - - require 'digest/sha1' - ## - # Returns a digest of the given string that - # will not be altered by String#to_xhtml. - # - def Document.digest input - Digest::SHA1.hexdigest(input.to_s). - - # XXX: surround all digits with alphabets so - # Maruku doesn't change them into HTML - gsub(/\d/, 'z\&z') end private ##