lucid 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/HISTORY.md +8 -0
- data/LICENSE +3 -0
- data/README.md +11 -2
- data/Rakefile +10 -1
- data/lib/autotest/discover.rb +5 -5
- data/lib/autotest/lucid_mixin.rb +34 -35
- data/lib/lucid/ast/table.rb +5 -4
- data/lib/lucid/cli/configuration.rb +0 -6
- data/lib/lucid/cli/options.rb +3 -12
- data/lib/lucid/formatter/condensed.rb +46 -0
- data/lib/lucid/formatter/html.rb +9 -3
- data/lib/lucid/formatter/junit.rb +6 -8
- data/lib/lucid/formatter/standard.rb +1 -7
- data/lib/lucid/generators/project/events-symbiont.rb +1 -1
- data/lib/lucid/platform.rb +1 -1
- data/lib/lucid/runtime.rb +1 -1
- data/lib/lucid/sequence.rb +5 -0
- data/lib/lucid/sequence/sequence_errors.rb +64 -0
- data/lib/lucid/sequence/sequence_group.rb +35 -0
- data/lib/lucid/sequence/sequence_phrase.rb +166 -0
- data/lib/lucid/sequence/sequence_steps.rb +20 -0
- data/lib/lucid/sequence/sequence_support.rb +26 -0
- data/lib/lucid/sequence/sequence_template.rb +354 -0
- data/lib/lucid/spec_file.rb +3 -1
- data/lib/lucid/step_match.rb +1 -1
- data/lib/lucid/wire_support/wire_packet.rb +1 -1
- data/lucid.gemspec +11 -9
- data/spec/lucid/app_spec.rb +42 -0
- data/spec/lucid/configuration_spec.rb +112 -0
- data/spec/lucid/sequences/sequence_conditional_spec.rb +74 -0
- data/spec/lucid/sequences/sequence_group_spec.rb +55 -0
- data/spec/lucid/sequences/sequence_phrase_spec.rb +122 -0
- data/spec/lucid/sequences/sequence_placeholder_spec.rb +56 -0
- data/spec/lucid/sequences/sequence_section_spec.rb +61 -0
- data/spec/lucid/sequences/sequence_support_spec.rb +65 -0
- data/spec/lucid/sequences/sequence_template_spec.rb +298 -0
- data/spec/spec_helper.rb +13 -0
- metadata +86 -54
- data/.rspec +0 -1
data/lib/lucid/platform.rb
CHANGED
@@ -2,7 +2,7 @@ require 'rbconfig'
|
|
2
2
|
|
3
3
|
module Lucid
|
4
4
|
unless defined?(Lucid::VERSION)
|
5
|
-
VERSION = '0.
|
5
|
+
VERSION = '0.2.0'
|
6
6
|
BINARY = File.expand_path(File.dirname(__FILE__) + '/../../bin/lucid')
|
7
7
|
LIBDIR = File.expand_path(File.dirname(__FILE__) + '/../../lib')
|
8
8
|
JRUBY = defined?(JRUBY_VERSION)
|
data/lib/lucid/runtime.rb
CHANGED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Sequence
|
2
|
+
|
3
|
+
class SequenceError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class DuplicateSequenceError < SequenceError
|
7
|
+
def initialize(phrase)
|
8
|
+
super("A sequence with phrase '#{phrase}' already exists.")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class UnknownSequenceError < SequenceError
|
13
|
+
def initialize(phrase)
|
14
|
+
super("Unknown sequence step with phrase: '#{phrase}'.")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class EmptyParameterError < SequenceError
|
19
|
+
def initialize(text)
|
20
|
+
super("An empty or blank parameter occurred in '#{text}'.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class InvalidElementError < SequenceError
|
25
|
+
def initialize(tag, invalid_element)
|
26
|
+
msg = "The invalid element '#{invalid_element}' occurs in the parameter '#{tag}'."
|
27
|
+
super(msg)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class UselessPhraseParameter < SequenceError
|
32
|
+
def initialize(param)
|
33
|
+
super("The phrase parameter '#{param}' does not appear in any step.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class DataTableNotFound < SequenceError
|
38
|
+
def initialize(phrase)
|
39
|
+
msg = "The step with phrase [#{phrase}]: requires a data table."
|
40
|
+
super(msg)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class UnknownParameterError < SequenceError
|
45
|
+
def initialize(name)
|
46
|
+
super("Unknown sequence step parameter '#{name}'.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class AmbiguousParameterValue < SequenceError
|
51
|
+
def initialize(name, phrase, table)
|
52
|
+
msg = "The sequence parameter '#{name}' has value '#{phrase}' and '#{table}'."
|
53
|
+
super(msg)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class UnreachableStepParameter < SequenceError
|
58
|
+
def initialize(param)
|
59
|
+
msg = "The step parameter '#{param}' does not appear in the phrase."
|
60
|
+
super(msg)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'lucid/sequence/sequence_phrase'
|
3
|
+
|
4
|
+
module Sequence
|
5
|
+
class SequenceGroup
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def add_sequence(phrase, sequence, data)
|
9
|
+
new_sequence = SequencePhrase.new(phrase, sequence, data)
|
10
|
+
raise DuplicateSequenceError.new(phrase) if find_sequence(phrase, data)
|
11
|
+
sequence_steps[new_sequence.key] = new_sequence
|
12
|
+
end
|
13
|
+
|
14
|
+
def sequence_steps
|
15
|
+
@sequence_steps ||= {}
|
16
|
+
return @sequence_steps
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_steps(phrase, data = nil)
|
20
|
+
data_table =! data.nil?
|
21
|
+
sequence = find_sequence(phrase, data_table)
|
22
|
+
raise UnknownSequenceError.new(phrase) if sequence.nil?
|
23
|
+
return sequence.expand(phrase, data)
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear
|
27
|
+
sequence_steps.clear
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_sequence(phrase, data)
|
31
|
+
key = SequencePhrase.sequence_key(phrase, data, :invoke)
|
32
|
+
return sequence_steps[key]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'lucid/sequence/sequence_template'
|
2
|
+
require 'lucid/sequence/sequence_errors'
|
3
|
+
|
4
|
+
module Sequence
|
5
|
+
class SequencePhrase
|
6
|
+
|
7
|
+
attr_reader :key
|
8
|
+
attr_reader :values
|
9
|
+
attr_reader :template
|
10
|
+
attr_reader :phrase_params
|
11
|
+
|
12
|
+
ParameterConstant = { 'quotes' => '"""'}
|
13
|
+
|
14
|
+
def initialize(phrase, sequence, data)
|
15
|
+
@key = self.class.sequence_key(phrase, data, :define)
|
16
|
+
#puts "*** [Sequence Step] - Key: #{@key}"
|
17
|
+
|
18
|
+
@phrase_params = scan_parameters(phrase, :define)
|
19
|
+
#puts "*** [Sequence Step] - Phrase Params: #{@phrase_params}"
|
20
|
+
|
21
|
+
transformed_steps = preprocess(sequence)
|
22
|
+
#puts "*** [Sequence Step] - Steps: \n#{transformed_steps}"
|
23
|
+
|
24
|
+
@template = SequenceTemplate::Engine.new(transformed_steps)
|
25
|
+
#puts "\n*** [Sequence Step] - Template: #{@template.inspect}\n"
|
26
|
+
|
27
|
+
phrase_variables = template.variables
|
28
|
+
#puts "\n*** [Sequence Step] - Phrase Variables: #{phrase_variables}"
|
29
|
+
|
30
|
+
@values = validate_phrase_values(@phrase_params, phrase_variables)
|
31
|
+
@values.concat(phrase_variables)
|
32
|
+
#puts "*** [Sequence Step] - Values: #{@values}"
|
33
|
+
|
34
|
+
@values.uniq!
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.sequence_key(phrase, data, mode)
|
38
|
+
new_phrase = phrase.strip
|
39
|
+
|
40
|
+
# These lines replace every series of whitespace with an underscore.
|
41
|
+
# For that to work, I have to make sure there are no existing
|
42
|
+
# underscore characters in the first place so any pre-existing
|
43
|
+
# underscores get removed first.
|
44
|
+
new_phrase.gsub!(/_/, '')
|
45
|
+
new_phrase.gsub!(/\s+/, '_')
|
46
|
+
|
47
|
+
pattern = case mode
|
48
|
+
when :define
|
49
|
+
/<(?:[^\\<>]|\\.)*>/
|
50
|
+
when :invoke
|
51
|
+
/"([^\\"]|\\.)*"/
|
52
|
+
end
|
53
|
+
|
54
|
+
# Here 'patterned' means that for a given phrase, any bit of text
|
55
|
+
# between quotes or chevrons is replaced by the letter X.
|
56
|
+
patterned = new_phrase.gsub(pattern, 'X')
|
57
|
+
|
58
|
+
key = patterned + (data ? '_T' : '')
|
59
|
+
|
60
|
+
return key
|
61
|
+
end
|
62
|
+
|
63
|
+
# "Scanning parameters" means that a phrase will be scanned to find any
|
64
|
+
# text between chevrons or double quotes. These will be placed into an
|
65
|
+
# array.
|
66
|
+
def scan_parameters(phrase, mode)
|
67
|
+
pattern = case mode
|
68
|
+
when :define
|
69
|
+
/<((?:[^\\<>]|\\.)*)>/
|
70
|
+
when :invoke
|
71
|
+
/"((?:[^\\"]|\\.)*)"/
|
72
|
+
end
|
73
|
+
|
74
|
+
result = phrase.scan(pattern)
|
75
|
+
params = result.flatten.compact
|
76
|
+
|
77
|
+
# Any escaped double quotes need to be replaced by a double quote.
|
78
|
+
params.map! { |item| item.sub(/\\"/, '"') } if mode == :invoke
|
79
|
+
|
80
|
+
return params
|
81
|
+
end
|
82
|
+
|
83
|
+
def preprocess(sequence)
|
84
|
+
# Split text into individual lines and make sure to remove any lines
|
85
|
+
# with hash style comments.
|
86
|
+
lines = sequence.split(/\r\n?|\n/)
|
87
|
+
processed = lines.reject { |line| line =~ /\s*#/ }
|
88
|
+
|
89
|
+
return processed.join("\n")
|
90
|
+
end
|
91
|
+
|
92
|
+
# The goal of this method is to check for various inconsistencies that
|
93
|
+
# can occur between parameter names in the sequence phrase and those
|
94
|
+
# in the actual steps.
|
95
|
+
def validate_phrase_values(params, phrase_variables)
|
96
|
+
# This covers the case when phrase names a parameter that never
|
97
|
+
# occurs in any of the steps.
|
98
|
+
params.each do |param|
|
99
|
+
unless phrase_variables.include? param
|
100
|
+
raise UselessPhraseParameter.new(param)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# This covers the case when a step has a parameter that never appears
|
105
|
+
# in the sequence phrase and when data table is not being used.
|
106
|
+
unless data_table_required?
|
107
|
+
phrase_variables.each do |variable|
|
108
|
+
unless params.include?(variable) || ParameterConstant.include?(variable)
|
109
|
+
raise UnreachableStepParameter.new(variable)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
return phrase_params.dup
|
115
|
+
end
|
116
|
+
|
117
|
+
def data_table_required?
|
118
|
+
return key =~ /_T$/
|
119
|
+
end
|
120
|
+
|
121
|
+
def expand(phrase, data)
|
122
|
+
params = validate_params(phrase, data)
|
123
|
+
params = ParameterConstant.merge(params)
|
124
|
+
return template.output(nil, params)
|
125
|
+
end
|
126
|
+
|
127
|
+
def validate_params(phrase, data)
|
128
|
+
sequence_parameters = {}
|
129
|
+
|
130
|
+
quoted_values = scan_parameters(phrase, :invoke)
|
131
|
+
quoted_values.each_with_index do |val, index|
|
132
|
+
sequence_parameters[phrase_params[index]] = val
|
133
|
+
end
|
134
|
+
|
135
|
+
unless data.nil?
|
136
|
+
data.each do |row|
|
137
|
+
(key, value) = validate_row(row, sequence_parameters)
|
138
|
+
if sequence_parameters.include? key
|
139
|
+
if sequence_parameters[key].kind_of?(Array)
|
140
|
+
sequence_parameters[key] << value
|
141
|
+
else
|
142
|
+
sequence_parameters[key] = [sequence_parameters[key], value]
|
143
|
+
end
|
144
|
+
else
|
145
|
+
sequence_parameters[key] = value
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
return sequence_parameters
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate_row(row, params)
|
154
|
+
(key, value) = row
|
155
|
+
|
156
|
+
raise UnknownParameterError.new(key) unless values.include? key
|
157
|
+
|
158
|
+
if (phrase_params.include? key) && (params[key] != value)
|
159
|
+
raise AmbiguousParameterValue.new(key, params[key], value)
|
160
|
+
end
|
161
|
+
|
162
|
+
return row
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# The only test definitions that should go in here are those that
|
2
|
+
# will build sequence steps. Meaning, each test step here is meant
|
3
|
+
# to be one that is equivalent to a sequence of test steps.
|
4
|
+
|
5
|
+
Given(/^the step "(?:Given|When|Then|\*) \[((?:[^\\\]]|\\.)+)\](:?)" is defined to mean:$/) do |phrase, table, sequence|
|
6
|
+
data_provided = (table == ':')
|
7
|
+
add_sequence(phrase, sequence, data_provided)
|
8
|
+
end
|
9
|
+
|
10
|
+
When(/^\[((?:[^\\\]]|\\.)+)\]$/) do |phrase|
|
11
|
+
invoke_sequence(phrase)
|
12
|
+
end
|
13
|
+
|
14
|
+
When(/^\[([^\]]+)\]:$/) do |phrase, data_table|
|
15
|
+
unless data_table.kind_of?(Lucid::AST::Table)
|
16
|
+
raise Sequence::DataTableNotFound.new(phrase)
|
17
|
+
end
|
18
|
+
|
19
|
+
invoke_sequence(phrase, data_table.raw)
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'lucid/sequence/sequence_group'
|
2
|
+
require 'lucid/sequence/sequence_errors'
|
3
|
+
|
4
|
+
module Sequence
|
5
|
+
module SequenceSupport
|
6
|
+
|
7
|
+
def add_sequence(phrase, sequence, data)
|
8
|
+
SequenceGroup.instance.add_sequence(phrase, sequence, data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def invoke_sequence(phrase, data = nil)
|
12
|
+
# It's necessary to generate textual versions of all the steps that
|
13
|
+
# are to be executed.
|
14
|
+
group = SequenceGroup.instance
|
15
|
+
generated_steps = group.generate_steps(phrase, data)
|
16
|
+
|
17
|
+
# This statement causes Lucid to execute the generated test steps.
|
18
|
+
steps(generated_steps)
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear_sequences
|
22
|
+
SequenceGroup.instance.clear
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,354 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Sequence
|
4
|
+
module SequenceTemplate
|
5
|
+
|
6
|
+
class StaticText
|
7
|
+
attr_reader :source
|
8
|
+
|
9
|
+
def initialize(source)
|
10
|
+
@source = source
|
11
|
+
end
|
12
|
+
|
13
|
+
def output(context, params)
|
14
|
+
return source
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class UnaryElement
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
def initialize(name)
|
22
|
+
@name = name
|
23
|
+
end
|
24
|
+
|
25
|
+
def retrieve_value_from(context, params)
|
26
|
+
actual_value = params[name]
|
27
|
+
|
28
|
+
if actual_value.nil? && context.respond_to?(name.to_sym)
|
29
|
+
actual_value = context.send(name.to_sym)
|
30
|
+
end
|
31
|
+
|
32
|
+
return actual_value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Placeholder < UnaryElement
|
37
|
+
def output(context, params)
|
38
|
+
actual_value = retrieve_value_from(context, params)
|
39
|
+
|
40
|
+
result = case actual_value
|
41
|
+
when NilClass
|
42
|
+
''
|
43
|
+
when Array
|
44
|
+
actual_value.join('<br/>')
|
45
|
+
when String
|
46
|
+
actual_value
|
47
|
+
else
|
48
|
+
actual_value.to_s()
|
49
|
+
end
|
50
|
+
|
51
|
+
return result
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class EOLine
|
56
|
+
def output(context, params)
|
57
|
+
return "\n"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Section < UnaryElement
|
62
|
+
attr_reader :children
|
63
|
+
|
64
|
+
def initialize(name)
|
65
|
+
super(name)
|
66
|
+
@children = []
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_child(child)
|
70
|
+
children << child
|
71
|
+
end
|
72
|
+
|
73
|
+
def variables
|
74
|
+
section_variables = children.each_with_object([]) do |child, result|
|
75
|
+
case child
|
76
|
+
when Placeholder
|
77
|
+
result << child.name
|
78
|
+
when Section
|
79
|
+
result.concat(child.variables)
|
80
|
+
else
|
81
|
+
# noop
|
82
|
+
end
|
83
|
+
end
|
84
|
+
return section_variables.flatten.uniq
|
85
|
+
end
|
86
|
+
|
87
|
+
def output(context, params)
|
88
|
+
msg = "Method Section.#{__method__} must be implemented in subclass."
|
89
|
+
raise NotImplementedError, msg
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class ConditionalSection < Section
|
94
|
+
attr_reader :existence
|
95
|
+
|
96
|
+
def initialize(name, generate)
|
97
|
+
super(name)
|
98
|
+
@existence = generate
|
99
|
+
end
|
100
|
+
|
101
|
+
def output(context, params)
|
102
|
+
value = retrieve_value_from(context, params)
|
103
|
+
if (!value.nil? && existence) || (value.nil? && !existence)
|
104
|
+
result = children.each_with_object('') do |child, item|
|
105
|
+
item << child.output(context, params)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
result = ''
|
109
|
+
end
|
110
|
+
|
111
|
+
return result
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_s
|
115
|
+
return "<?#{name}>"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
SectionEndMarker = Struct.new(:name)
|
120
|
+
|
121
|
+
class Engine
|
122
|
+
attr_reader :source
|
123
|
+
|
124
|
+
# Invalid internal elements refers to spaces, any punctuation sign or
|
125
|
+
# delimiter that is forbidden between chevrons <...> template tags.
|
126
|
+
InvalidInternalElements = begin
|
127
|
+
forbidden = ' !"#' + "$%&'()*+,-./:;<=>?[\\]^`{|}~"
|
128
|
+
all_escaped = []
|
129
|
+
forbidden.each_char() { |ch| all_escaped << Regexp.escape(ch) }
|
130
|
+
pattern = all_escaped.join('|')
|
131
|
+
Regexp.new(pattern)
|
132
|
+
end
|
133
|
+
|
134
|
+
def initialize(source)
|
135
|
+
@source = source
|
136
|
+
|
137
|
+
# The generated source contains an internal representation of the
|
138
|
+
# given template text.
|
139
|
+
@generated_source = generate(source)
|
140
|
+
end
|
141
|
+
|
142
|
+
# The parse mechanism is designed to break up TDL lines into static
|
143
|
+
# and dynamic components. Dynamic components will correspond to
|
144
|
+
# tagged elements of the string, which indicate parameters in the
|
145
|
+
# original TDL phrase.
|
146
|
+
def self.parse(line)
|
147
|
+
scanner = StringScanner.new(line)
|
148
|
+
result = []
|
149
|
+
|
150
|
+
until scanner.eos?
|
151
|
+
tag_literal = scanner.scan(/<(?:[^\\<>]|\\.)*>/)
|
152
|
+
|
153
|
+
unless tag_literal.nil?
|
154
|
+
result << [:dynamic, tag_literal.gsub(/^<|>$/, '')]
|
155
|
+
end
|
156
|
+
|
157
|
+
text_literal = scanner.scan(/(?:[^\\<>]|\\.)+/)
|
158
|
+
result << [:static, text_literal] unless text_literal.nil?
|
159
|
+
|
160
|
+
indicate_parsing_error(line) if tag_literal.nil? && text_literal.nil?
|
161
|
+
end
|
162
|
+
|
163
|
+
return result
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.indicate_parsing_error(line)
|
167
|
+
# The regular expression will be looking to match \< or \>. Those are
|
168
|
+
# escaped chevrons and will be replaced.
|
169
|
+
no_escaped = line.gsub(/\\[<>]/, '--')
|
170
|
+
unbalance_count = 0
|
171
|
+
|
172
|
+
no_escaped.each_char do |ch|
|
173
|
+
case ch
|
174
|
+
when '<'
|
175
|
+
unbalance_count += 1
|
176
|
+
when '>'
|
177
|
+
unbalance_count -= 1
|
178
|
+
end
|
179
|
+
|
180
|
+
raise StandardError, "Nested opening chevron '<'." if unbalance_count > 1
|
181
|
+
raise StandardError, "Missing opening chevron '<'." if unbalance_count < 0
|
182
|
+
end
|
183
|
+
|
184
|
+
raise StandardError, "Missing closing chevron '>'." if unbalance_count == 1
|
185
|
+
end
|
186
|
+
|
187
|
+
def parse_element(text)
|
188
|
+
# Check if the text matched is a ? or a / character. If the next bit
|
189
|
+
# of text after the ? or / is a invalid element of if there is an
|
190
|
+
# invalid element at all, an error will be raised.
|
191
|
+
if text =~ /^[\?\/]/
|
192
|
+
matching = InvalidInternalElements.match(text[1..-1])
|
193
|
+
else
|
194
|
+
matching = InvalidInternalElements.match(text)
|
195
|
+
end
|
196
|
+
|
197
|
+
raise InvalidElementError.new(text, matching[0]) if matching
|
198
|
+
|
199
|
+
result = case text[0, 1]
|
200
|
+
when '/'
|
201
|
+
SectionEndMarker.new(text[1..-1])
|
202
|
+
when '?'
|
203
|
+
ConditionalSection.new(text[1..-1], true)
|
204
|
+
else
|
205
|
+
Placeholder.new(text)
|
206
|
+
end
|
207
|
+
|
208
|
+
return result
|
209
|
+
end
|
210
|
+
|
211
|
+
# This method is used to retrieve all of the variable elements, which
|
212
|
+
# will be placeholder names, that appear in the template.
|
213
|
+
def variables
|
214
|
+
@variables ||= begin
|
215
|
+
vars = @generated_source.each_with_object([]) do |element, result|
|
216
|
+
case element
|
217
|
+
when Placeholder
|
218
|
+
result << element.name
|
219
|
+
when Section
|
220
|
+
result.concat(element.variables)
|
221
|
+
else
|
222
|
+
# noop
|
223
|
+
end
|
224
|
+
end
|
225
|
+
vars.flatten.uniq
|
226
|
+
end
|
227
|
+
return @variables
|
228
|
+
end
|
229
|
+
|
230
|
+
# This general output method will provide a final template within
|
231
|
+
# the given scope object (Placeholder, StaticText, etc) and with
|
232
|
+
# any of the parameters specified.
|
233
|
+
def output(context, params)
|
234
|
+
return '' if @generated_source.empty?
|
235
|
+
result = @generated_source.each_with_object('') do |element, item|
|
236
|
+
item << element.output(context, params)
|
237
|
+
end
|
238
|
+
return result
|
239
|
+
end
|
240
|
+
|
241
|
+
# To "generate" means to create an internal representation of
|
242
|
+
# the template.
|
243
|
+
def generate(source)
|
244
|
+
input_lines = source.split(/\r\n?|\n/)
|
245
|
+
|
246
|
+
raw_lines = input_lines.map do |line|
|
247
|
+
line_items = self.class.parse(line)
|
248
|
+
line_items.each do |(kind, text)|
|
249
|
+
if (kind == :dynamic) && text.strip.empty?
|
250
|
+
raise EmptyParameterError.new(line.strip)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
line_items
|
254
|
+
end
|
255
|
+
|
256
|
+
template_lines = raw_lines.map { |line| generate_line(line) }
|
257
|
+
return generate_sections(template_lines.flatten)
|
258
|
+
end
|
259
|
+
|
260
|
+
def generate_line(line)
|
261
|
+
line_rep = line.map { |item| generate_couple(item) }
|
262
|
+
section_item = nil
|
263
|
+
|
264
|
+
line_to_despace = line_rep.all? do |item|
|
265
|
+
case item
|
266
|
+
when StaticText
|
267
|
+
item.source =~ /\s+/
|
268
|
+
when Section, SectionEndMarker
|
269
|
+
if section_item.nil?
|
270
|
+
section_item = item
|
271
|
+
true
|
272
|
+
else
|
273
|
+
false
|
274
|
+
end
|
275
|
+
else
|
276
|
+
false
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
if line_to_despace && ! section_item.nil?
|
281
|
+
line_rep = [section_item]
|
282
|
+
else
|
283
|
+
line_rep_ending(line_rep)
|
284
|
+
end
|
285
|
+
|
286
|
+
return line_rep
|
287
|
+
end
|
288
|
+
|
289
|
+
def generate_couple(item)
|
290
|
+
(kind, text) = item
|
291
|
+
|
292
|
+
result = case kind
|
293
|
+
when :static
|
294
|
+
StaticText.new(text)
|
295
|
+
when :dynamic
|
296
|
+
parse_element(text)
|
297
|
+
end
|
298
|
+
|
299
|
+
return result
|
300
|
+
end
|
301
|
+
|
302
|
+
def generate_sections(sequence)
|
303
|
+
open_sections = []
|
304
|
+
|
305
|
+
generated = sequence.each_with_object([]) do |element, result|
|
306
|
+
case element
|
307
|
+
when Section
|
308
|
+
open_sections << element
|
309
|
+
when SectionEndMarker
|
310
|
+
validate_section_end(element, open_sections)
|
311
|
+
result << open_sections.pop()
|
312
|
+
else
|
313
|
+
if open_sections.empty?
|
314
|
+
result << element
|
315
|
+
else
|
316
|
+
open_sections.last.add_child(element)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
unless open_sections.empty?
|
322
|
+
error_message = "Unterminated section #{open_sections.last}."
|
323
|
+
raise StandardError, error_message
|
324
|
+
end
|
325
|
+
|
326
|
+
return generated
|
327
|
+
end
|
328
|
+
|
329
|
+
def validate_section_end(marker, sections)
|
330
|
+
if sections.empty?
|
331
|
+
msg = "End of section </#{marker.name}> found while no corresponding section is open."
|
332
|
+
raise StandardError, msg
|
333
|
+
end
|
334
|
+
|
335
|
+
if marker.name != sections.last.name
|
336
|
+
msg = "End of section </#{marker.name}> does not match current section '#{sections.last.name}'."
|
337
|
+
raise StandardError, msg
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def line_rep_ending(line)
|
342
|
+
if line.last.is_a?(SectionEndMarker)
|
343
|
+
section_end = line.pop()
|
344
|
+
line << EOLine.new
|
345
|
+
line << section_end
|
346
|
+
else
|
347
|
+
line << EOLine.new
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
end # class: Engine
|
352
|
+
|
353
|
+
end
|
354
|
+
end
|