Class: ERBook::Template

  • ERB
    • ERBook::Template

An eRuby template which allows access to the underlying result buffer (which contains the result of template evaluation thus far) and provides sandboxing for isolated template rendering.

In addition to the standard <% eRuby %> directives, this template supports:

  • Lines that begin with ’%’ are treated as normal eRuby directives.
  • Include directives (<%#include YOUR_PATH #%>) are replaced by the result of reading and evaluating the YOUR_PATH file in the current context.
    • Unless YOUR_PATH is an absolute path, it is treated as being relative to the file which contains the include directive.
    • Errors originating from included files are given a proper stack trace which shows the chain of inclusion plus any further trace steps originating from the included file itself.
  • eRuby directives delimiting Ruby blocks (<% … do %> … <% end %>) can be heirarchically unindented by the crown margin of the opening (<% … do %>) delimiter.

Attributes

Instance Attributes

buffer [RW] public

The result of template evaluation thus far.

Constructor Summary

public initialize(source, input, unindent = false, safe_level = nil)

Meta Tags

Parameters:

[String] source

Replacement for the ambiguous ’(erb)’ identifier in stack traces; so that the user can better determine the source of an error.

[String] input

A string containing eRuby directives.

[boolean] unindent (defaults to: false)

If true, then all content blocks will be unindented hierarchically, by the leading space of their ‘do’ and ‘end’ delimiters.

[Object] safe_level (defaults to: nil)

See safe_level in ERB::new().

[View source]


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
# File 'lib/erbook/template.rb', line 47

def initialize source, input, unindent = false, safe_level = nil
  # expand all "include" directives in the input
    expander = lambda do |src_file, src_text, path_stack, stack_trace|
      src_path = File.expand_path(src_file)
      src_line = 1 # line number of the current include directive in src_file

      chunks = src_text.split(/<%#\s*include\s+(.+?)\s*#%>/)

      path_stack.push src_path
      chunks.each_with_index do |chunk, i|
        # even number: chunk is not an include directive
        if i & 1 == 0
          src_line += chunk.count("\n")

        # odd number: chunk is the target of the include directive
        else
          # resolve correct path of target file
            dst_file = chunk

            unless Pathname.new(dst_file).absolute?
              # target is relative to the file in
              # which the include directive exists
              dst_file = File.join(File.dirname(src_file), dst_file)
            end

            dst_path = File.expand_path(dst_file)

          # include the target file
            if path_stack.include? dst_file
              raise "Cannot include #{dst_file.inspect} at #{src_file.inspect}:#{src_line} because that would cause an infinite loop in the inclusion stack: #{path_stack.inspect}."
            else
              stack_trace.push "#{src_path}:#{src_line}"
              dst_text = eval('File.read dst_file', binding, src_file, src_line)

              # recursively expand any include directives within
              # the expansion of the current include directive
              dst_text = expander[dst_file, dst_text, path_stack, stack_trace]

              # provide more accurate stack trace for
              # errors originating from included files
              line_var = "__erbook_var_#{dst_file.object_id.abs}__"
              dst_text = %{<%
                #{line_var} = __LINE__ + 2 # content is 2 newlines below
                begin
                  %>#{dst_text}<%
                rescue Exception => err
                  bak = err.backtrace

                  top = []
                  found_top = false
                  prev_line = nil

                  bak.each do |step|
                    if step =~ /^#{/#{source}/}:(\\d+)(.*)/
                      line, desc = $1, $2
                      line = line.to_i - #{line_var} + 1

                      if line > 0 and line != prev_line
                        top << "#{dst_path}:\#{line}\#{desc}"
                        found_top = true
                        prev_line = line
                      end
                    elsif !found_top
                      top << step
                    end
                  end

                  if found_top
                    bak.replace top
                    bak.concat #{stack_trace.reverse.inspect}
                  end

                  raise err
                end
              %>}

              stack_trace.pop
            end

            chunks[i] = dst_text
        end
      end
      path_stack.pop

      chunks.join
    end

    input = expander[source, input, [], []]

  # convert "% at beginning of line" usage into <% normal %> usage
    input.gsub! %r{^([ \t]*)(%[=# \t].*)$}, '\1<\2 %>'
    input.gsub! %r{^([ \t]*)%%}, '\1%'

  # unindent node content hierarchically
    if unindent
      tags = input.scan(/<%(?:.(?!<%))*?%>/m)
      margins = []
      result = []

      buffer = input
      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

      input = result.join
    end

  # silence the code-only <% ... %> directive, just like PHP does
    input.gsub! %r{^[ \t]*(<%[^%=]((?!<%).)*?[^%]%>)[ \t]*\r?\n}m, '\1'

  # use @buffer to store the result of the ERB template
  super input, safe_level, nil, :@buffer

  self.filename = source
end

Public Visibility

Public Instance Method Summary

#render_with(inst_vars = )

Renders this template within a fresh object that is populated with the given instance variables, whose names must be prefixed with ’@’.

Public Instance Method Details

render_with

public render_with(inst_vars = )

Renders this template within a fresh object that is populated with the given instance variables, whose names must be prefixed with ’@’.

[View source]


179
180
181
182
183
184
185
186
187
188
189
# File 'lib/erbook/template.rb', line 179

def render_with inst_vars = ;{}
  context = Object.new.instance_eval do
    inst_vars.each_pair do |var, val|
      instance_variable_set var, val
    end

    binding
  end

  result context
end

Protected Visibility

Protected Instance Method Summary

#content_from_block(*block_args)

Returns the content that the given block wants to append to the buffer.

Protected Instance Method Details

content_from_block

protected content_from_block(*block_args)

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.

Meta Tags

Parameters:

Raises:

[ArgumentError]
[View source]


196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/erbook/template.rb', line 196

def content_from_block *block_args
  raise ArgumentError, 'block must be given' unless block_given?

  head = @buffer.length
  body = yield(*block_args) # this appends 'content' to '@buffer'
  tail = @buffer.length

  if tail > head
    @buffer.slice! head..tail
  else
    body
  end.to_s
end