Class: Sequence::SequenceTemplate::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/lucid/sequence/sequence_template.rb

Constant Summary collapse

InvalidInternalElements =

Invalid internal elements refers to spaces, any punctuation sign or delimiter that is forbidden between chevrons <…> template tags.

begin
  forbidden = ' !"#' + "$%&'()*+,-./:;<=>?[\\]^`{|}~"
  all_escaped = []
  forbidden.each_char() { |ch| all_escaped << Regexp.escape(ch) }
  pattern = all_escaped.join('|')
  Regexp.new(pattern)
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source) ⇒ Engine

Returns a new instance of Engine.



134
135
136
137
138
139
140
# File 'lib/lucid/sequence/sequence_template.rb', line 134

def initialize(source)
  @source = source
  
  # The generated source contains an internal representation of the
  # given template text.
  @generated_source = generate(source)
end

Instance Attribute Details

#sourceObject (readonly)

Returns the value of attribute source.



122
123
124
# File 'lib/lucid/sequence/sequence_template.rb', line 122

def source
  @source
end

Class Method Details

.indicate_parsing_error(line) ⇒ Object

Raises:

  • (StandardError)


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/lucid/sequence/sequence_template.rb', line 166

def self.indicate_parsing_error(line)
  # The regular expression will be looking to match \< or \>. Those are
  # escaped chevrons and will be replaced.
  no_escaped = line.gsub(/\\[<>]/, '--')
  unbalance_count = 0

  no_escaped.each_char do |ch|
    case ch
      when '<'
        unbalance_count += 1
      when '>'
        unbalance_count -= 1
    end

    raise StandardError, "Nested opening chevron '<'." if unbalance_count > 1
    raise StandardError, "Missing opening chevron '<'." if unbalance_count < 0
  end

  raise StandardError, "Missing closing chevron '>'." if unbalance_count == 1
end

.parse(line) ⇒ Object

The parse mechanism is designed to break up TDL lines into static and dynamic components. Dynamic components will correspond to tagged elements of the string, which indicate parameters in the original TDL phrase.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/lucid/sequence/sequence_template.rb', line 146

def self.parse(line)
  scanner = StringScanner.new(line)
  result = []
  
  until scanner.eos?
    tag_literal = scanner.scan(/<(?:[^\\<>]|\\.)*>/)
    
    unless tag_literal.nil?
      result << [:dynamic, tag_literal.gsub(/^<|>$/, '')]
    end

    text_literal = scanner.scan(/(?:[^\\<>]|\\.)+/)
    result << [:static, text_literal] unless text_literal.nil?

    indicate_parsing_error(line) if tag_literal.nil? && text_literal.nil?
  end

  return result
end

Instance Method Details

#generate(source) ⇒ Object

To “generate” means to create an internal representation of the template.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/lucid/sequence/sequence_template.rb', line 243

def generate(source)
  input_lines = source.split(/\r\n?|\n/)

  raw_lines = input_lines.map do |line|
    line_items = self.class.parse(line)
    line_items.each do |(kind, text)|
      if (kind == :dynamic) && text.strip.empty?
        raise EmptyParameterError.new(line.strip)
      end
    end
    line_items
  end

  template_lines = raw_lines.map { |line| generate_line(line) }
  return generate_sections(template_lines.flatten)
end

#generate_couple(item) ⇒ Object



289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/lucid/sequence/sequence_template.rb', line 289

def generate_couple(item)
  (kind, text) = item
  
  result = case kind
             when :static
               StaticText.new(text)
             when :dynamic
               parse_element(text)
           end
  
  return result
end

#generate_line(line) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/lucid/sequence/sequence_template.rb', line 260

def generate_line(line)
  line_rep = line.map { |item| generate_couple(item) }
  section_item = nil
  
  line_to_despace = line_rep.all? do |item|
    case item
      when StaticText
        item.source =~ /\s+/
      when Section, SectionEndMarker
        if section_item.nil?
          section_item = item
          true
        else
          false
        end
      else
        false
    end
  end

  if line_to_despace && ! section_item.nil?
    line_rep = [section_item]
  else
    line_rep_ending(line_rep)
  end
  
  return line_rep
end

#generate_sections(sequence) ⇒ Object



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/lucid/sequence/sequence_template.rb', line 302

def generate_sections(sequence)
  open_sections = []
  
  generated = sequence.each_with_object([]) do |element, result|
    case element
      when Section
        open_sections << element
      when SectionEndMarker
        validate_section_end(element, open_sections)
        result << open_sections.pop()
      else
        if open_sections.empty?
          result << element
        else
          open_sections.last.add_child(element)
        end
      end
    end

  unless open_sections.empty?
    error_message = "Unterminated section #{open_sections.last}."
    raise StandardError, error_message
  end
  
  return generated
end

#line_rep_ending(line) ⇒ Object



341
342
343
344
345
346
347
348
349
# File 'lib/lucid/sequence/sequence_template.rb', line 341

def line_rep_ending(line)
  if line.last.is_a?(SectionEndMarker)
    section_end = line.pop()
    line << EOLine.new
    line << section_end
  else
    line << EOLine.new
  end
end

#output(context, params) ⇒ Object

This general output method will provide a final template within the given scope object (Placeholder, StaticText, etc) and with any of the parameters specified.



233
234
235
236
237
238
239
# File 'lib/lucid/sequence/sequence_template.rb', line 233

def output(context, params)
  return '' if @generated_source.empty?
  result = @generated_source.each_with_object('') do |element, item|
    item << element.output(context, params)
  end
  return result
end

#parse_element(text) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/lucid/sequence/sequence_template.rb', line 187

def parse_element(text)
  # Check if the text matched is a ? or a / character. If the next bit
  # of text after the ? or / is a invalid element of if there is an
  # invalid element at all, an error will be raised.
  if text =~ /^[\?\/]/
    matching = InvalidInternalElements.match(text[1..-1])
  else
    matching = InvalidInternalElements.match(text)
  end

  raise InvalidElementError.new(text, matching[0]) if matching
  
  result = case text[0, 1]
             when '/'
               SectionEndMarker.new(text[1..-1])
             when '?'
               ConditionalSection.new(text[1..-1], true)
             else
               Placeholder.new(text)
           end
  
  return result
end

#validate_section_end(marker, sections) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
# File 'lib/lucid/sequence/sequence_template.rb', line 329

def validate_section_end(marker, sections)
  if sections.empty?
    msg = "End of section </#{marker.name}> found while no corresponding section is open."
    raise StandardError, msg
  end

  if marker.name != sections.last.name
    msg = "End of section </#{marker.name}> does not match current section '#{sections.last.name}'."
    raise StandardError, msg
  end
end

#variablesObject

This method is used to retrieve all of the variable elements, which will be placeholder names, that appear in the template.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/lucid/sequence/sequence_template.rb', line 213

def variables
  @variables ||= begin
    vars = @generated_source.each_with_object([]) do |element, result|
      case element
        when Placeholder
          result << element.name
        when Section
          result.concat(element.variables)
        else
          # noop
      end
    end
    vars.flatten.uniq
  end
  return @variables
end