Class: ERBook::Document

Inherits:
Object
  • Object
show all
Defined in:
lib/erbook/document.rb

Defined Under Namespace

Classes: Node

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(format_name, input_text, input_file, options = {}) ⇒ Document

Parameters

format_name

Either the short-hand name of a built-in format or the path to a format specification file.

input_text

The body of the input document.

input_file

Name of the file from which the input document originated.

Options

:unindent

If true, all node content is unindented hierarchically.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/erbook/document.rb', line 42

def initialize format_name, input_text, input_file, options = {}
  # process format specification
    @format_file = format_name.to_s

    File.file? @format_file or
      @format_file = File.join(ERBook::FORMATS_DIR, @format_file + '.yaml')

    begin
      @format = YAML.load_file(@format_file)
      @format[:file] = File.expand_path(@format_file)
      @format[:name] = File.basename(@format_file).sub(/\..*?$/, '')

      if @format.key? 'code'
        eval @format['code'].to_s, TOPLEVEL_BINDING, "#{@format_file}:code"
      end

    rescue Exception
      error "Could not load format specification file #{@format_file.inspect}"
    end

    @node_defs = @format['nodes']

  # 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| sandbox.instance_variable_set(k, v) }

      #:stopdoc:

      ##
      # Handles the method call from a node
      # placeholder in the input document.
      #
      def sandbox.__node_handler__ node_type, *node_args, &node_content
        node = Node.new(
          :type       => node_type,
          :definition => @format['nodes'][node_type],
          :arguments  => node_args,
          :backtrace  => caller,
          :parent     => @stack.last,
          :children   => []
        )

        Array(node.definition['params']).each do |param|
          break if node_args.empty?
          node.__send__ "#{param}=", node_args.shift
        end

        @nodes << node
        @nodes_by_type[node.type] << node

        # calculate ordinal number for this node
        if node.ordinal_number?
          @count_by_type ||= Hash.new {|h,k| h[k] = 0 }
          node.ordinal_number = (@count_by_type[node.type] += 1)
        end

        # assign node family
        if parent = node.parent
          parent.children << node
          node.parent = parent
          node.depth = parent.depth
          node.depth += 1 if node.anchor?

          # calculate section number for this node
          if node.section_number?
            ancestor = @stack.reverse.find {|n| n.section_number }
            branches = parent.children.select {|n| n.section_number }

            node.section_number = [
              ancestor.section_number,
              branches.length + 1
            ].join('.')
          end
        else
          @roots << node
          node.parent = nil
          node.depth = 0

          # calculate section number for this node
          if node.section_number?
            branches = @roots.select {|n| n.section_number }
            node.section_number = (branches.length + 1).to_s
          end
        end

        # assign node content
        if block_given?
          @stack.push node
          node.content = __block_content__(node, &node_content)
          @stack.pop
        end

        @buffer << node

        nil
      end

      #:startdoc:

      @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__; eval %{
          def sandbox.#{type} *node_args, &node_content
            __node_handler__ #{type.inspect}, *node_args, &node_content
          end
        }, binding, file, line
      end

    # evaluate the input & build the document tree
      template.render
      @processed_document = template.buffer

    # chain block-level nodes together for local navigation
      block_nodes = @nodes.select {|n| n.chain? }

      require 'enumerator'
      block_nodes.each_cons(2) do |a, b|
        a.next_node = b
        b.prev_node = a
      end

    # 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.definition['output'].to_s.chomp
        ).render_with(@template_vars.merge(:@node => n))

        # reveal child nodes' actual output in this node's actual output
        n.children.each do |c|
          if c.silent?
            # this child's output is not meant to be revealed at this time
            next

          elsif c.inline?
            actual_output[c.output] = actual_output_by_node[c]

          else
            # remove <p> around block-level child (added by Markdown)
            actual_output.sub! %r{(<p>\s*)?#{
              Regexp.quote c.output
            }(\s*</p>)?} do
              actual_output_by_node[c] +
                if $1 and $2
                  ''
                else
                  [$1, $2].join
                end
            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| 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 stack trace
    error "Could not process input document #{input_file.inspect}"
  end
end

Instance Attribute Details

#formatObject (readonly)

Data from the format specification file.



13
14
15
# File 'lib/erbook/document.rb', line 13

def format
  @format
end

#nodesObject (readonly)

All nodes in the document.



19
20
21
# File 'lib/erbook/document.rb', line 19

def nodes
  @nodes
end

#nodes_by_typeObject (readonly)

All nodes in the document arranged by node type.



22
23
24
# File 'lib/erbook/document.rb', line 22

def nodes_by_type
  @nodes_by_type
end

#rootsObject (readonly)

All root nodes in the document.



16
17
18
# File 'lib/erbook/document.rb', line 16

def roots
  @roots
end

Instance Method Details

#to_sObject

Returns the output of this document.



246
247
248
249
# File 'lib/erbook/document.rb', line 246

def to_s
  Template.new("#{@format_file}:output", @format['output'].to_s).
  render_with(@template_vars.merge(:@content => @processed_document.join))
end