Class: ERBook::Document

Attributes

Instance Attributes

format [RW] public

Data from the format specification file.

nodes_by_type [RW] public

All nodes in the document arranged by node type.

nodes [RW] public

All nodes in the document.

roots [RW] public

All root nodes in the document.

Constructor Summary

public initialize(format, input_text, input_file, options = )

Meta Tags

Parameters:

[String] format

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

[String] input_text

The body of the input document.

[String] input_file

Name of the file from which the input document originated.

[Hash] options (defaults to: )

Additional method parameters:

boolean
:unindent => If true, all node content is unindented hierarchically.
[View source]


35
36
37
38
39
40
41
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
# File 'lib/erbook/document.rb', line 35

def initialize format, input_text, input_file, options = ;{}
  # process format specification
    @format_file = format.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])

        @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) }

        class << template
          private

          # 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

            # 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']

              # 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
            end

            # 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)
            end

            node.content = content
            node.digest = digest

            digest
          end
        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
            end
          }, file, line
        end

      # evaluate the input & build the document tree
        @processed_document = template.instance_eval { result binding }

      # chain block-level nodes together for local navigation
        block_nodes = @nodes.reject {|n| @node_defs[n.type]['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
          ).render_with(@template_vars.merge(:@node => n))

          # expand all child nodes in this node's output
          n.children.each {|c| expander[c, n.output] }

          # replace this node's placeholder with its output in the buffer
          buf[n.digest] = @node_defs[n.type]['silent'] ? '' : n.output
        end

        @roots.each {|n| expander[n, @processed_document] }

    rescue Exception
      puts input_text # so the user can debug line numbers in the stack trace
      error "Could not process input document #{input_file.inspect}"
    end
end

Public Visibility

Public Class Method Summary

digest(input)

Returns a digest of the given string that will not be altered by String#to_xhtml.

Public Instance Method Summary

#to_s

Returns the output of this document.

Public Class Method Details

digest

public digest(input)

Returns a digest of the given string that will not be altered by String#to_xhtml.

[View source]


213
214
215
216
217
218
219
# File 'lib/erbook/document.rb', line 213

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

Public Instance Method Details

to_s

public to_s

Returns the output of this document.

[View source]


191
192
193
194
# File 'lib/erbook/document.rb', line 191

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