smartdown 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -49,36 +49,52 @@ flow:
49
49
 
50
50
  Each file has three parts: front-matter, a model definition, rules/logic. Only the model definition is required.
51
51
 
52
- * **front-matter** defines metadata in the form `property: value`
53
- * the **model definition** is a markdown-like block which defines a flow, question or outcome.
54
- * **rules/logic** defines 'next node' transition rules or other logic/predicate definitions
52
+ * **front-matter** defines metadata in the form `property: value`. Note: this
53
+ does not support full YAML syntax.
54
+ * the **model definition* is a markdown-like block which defines a flow,
55
+ question or outcome.
56
+ * **rules/logic** defines 'next node' transition rules or other
57
+ logic/predicate definitions
55
58
 
56
59
  ## Cover sheet node
57
60
 
58
- The cover sheet starts the flow off, its filename should match the flow name, e.g. 'check-uk-visa.txt'.
61
+ The cover sheet starts the flow off, its filename should match the flow name,
62
+ e.g. 'check-uk-visa.txt'.
59
63
 
60
- It has initial 'front matter' which defines metadata for the flow, including
61
- the first question. It then defines the copy for the cover sheet in markdown
62
- format. The h1 title is compulsory and used as the title for the smart answer.
64
+ It has initial 'front matter' which defines metadata for the flow. It then
65
+ defines the copy for the cover sheet in markdown format. The h1 title is
66
+ compulsory and used as the title for the smart answer.
67
+
68
+ A start button determines which question node is presented first.
63
69
 
64
70
  ```
65
71
  meta_description: You may need a visa to come to the UK to visit, study or work.
66
72
  satisfies_need: 100982
67
- start_with: what_passport_do_you_have
68
73
 
69
74
  # Check if you need a UK visa
70
75
 
71
76
  You may need a visa to come to the UK to visit, study or work.
77
+
78
+ [start_button: what_passport_do_you_have]
72
79
  ```
73
80
 
74
81
  ## Question nodes
75
82
 
76
- A question model definition has optional front matter, followed by a title and
77
- question type.
83
+ Question nodes follow the same standard structure outlined above.
84
+
85
+ Smartdown currently allows multiple questions to be defined per node, but this
86
+ feature is in development and the behaviour may change.
78
87
 
79
- The next sections define the various question types
88
+ The next sections define the various question types available.
80
89
 
81
- ### Radio buttons
90
+ Note that at present only the 'choice' question type has been implemented.
91
+ Unimplemented question types are marked with **(tbd)** in the heading. For
92
+ these question types, consider this documentation to be a proposal of how they
93
+ might work.
94
+
95
+ ### "Choice" questions (aka. radio buttons)
96
+
97
+ A choice question allows the user to select a single option from a list of choices.
82
98
 
83
99
  ```markdown
84
100
  ## Will you pass through UK Border Control?
@@ -106,7 +122,7 @@ Use front matter to exclude/include countries
106
122
 
107
123
  ```markdown
108
124
  ---
109
- passport_country:
125
+ passport_country:
110
126
  exclude_countries: country1, country2
111
127
  include_countries: {country3: "Country 3", country4: "Country 4"}
112
128
  ---
@@ -160,6 +176,17 @@ Asks for salary which can be expressed as either a weekly or monthly money amoun
160
176
  * [ ] no: No
161
177
  ```
162
178
 
179
+ ## Next steps
180
+
181
+ Markdown to be displayed as part of an outcome to direct the users to other information of potential interest to them.
182
+
183
+ ```markdown
184
+ [next_steps]
185
+ * Any kind of markdown
186
+ [A link](https://gov.uk/somewhere)
187
+ [end_next_steps]
188
+ ```
189
+
163
190
  ## Next node rules
164
191
 
165
192
  Logical rules for transitioning to the next node are defined in 'Next node' section. This is declared using a markdown h1 'Next node'.
@@ -177,32 +204,90 @@ defines a conditional transition
177
204
  ```
178
205
  # Next node
179
206
 
180
- * predicate1
181
- * predicate2 => outcome1
182
- * predicate3 => outcome2
207
+ * reddish?
208
+ * yellowish? => orange
209
+ * blueish? => purple
183
210
  ```
184
211
 
185
212
  defines nested rules.
186
213
 
214
+ In the example above the node `orange` would be selected if both `reddish?` and `yellowish?` were true.
215
+
187
216
  ## Predicates
188
217
 
218
+ As well as 'named' predicates which might be defined by a plugin or other
219
+ mechanism, there's also a basic expression language for predicates.
220
+
221
+ The currently supported operations are:
222
+
189
223
  ```
190
224
  variable_name is 'string'
191
225
  variable_name in {this that the-other}
192
226
  ```
193
227
 
194
- ### Date comparison predicates
228
+ ### Date comparison predicates (tbd)
195
229
 
196
230
  ```
197
231
  date_variable_name >= '14/07/2014'
198
232
  date_variable_name < '14/07/2014'
199
233
  ```
200
234
 
201
- ## Conditional blocks in outcomes (tbd)
202
-
203
235
  ## Processing model
204
236
 
205
- Each response to a question is assigned to a variable which corresponds to the question name (as determined by the filename).
237
+ Each response to a question is assigned to a variable which corresponds to the
238
+ question name (as determined by the filename).
239
+
240
+ ## Conditional blocks in outcomes
241
+
242
+ The syntax is:
243
+
244
+ ```markdown
245
+
246
+ $IF pred?
247
+
248
+ Text if true
249
+
250
+ more text if you like
251
+
252
+ $ENDIF
253
+ ```
254
+
255
+ You can also have an else clause:
256
+
257
+ ```markdown
258
+
259
+ $IF pred?
260
+
261
+ Text if true
262
+
263
+ $ELSE
264
+
265
+ Text if false
266
+
267
+ $ENDIF
268
+ ```
269
+
270
+ It's required to have a blank line between each if statement and the next paragraph of text, in other words this would be **invalid**:
271
+
272
+ ```markdown
273
+
274
+ $IF pred?
275
+ Text if true
276
+ $ENDIF
277
+ ```
278
+
279
+ ## Interpolation
280
+
281
+ It's possible to interpolate values from calculations, responses to questions, plugins etc.
282
+
283
+ Interpolations are currently supported into headings and paragraphs using the following syntax:
284
+
285
+ ```markdown
286
+
287
+ # State pension age calculation for %{name}
288
+
289
+ Your state pension age is %{state_pension_age}.
290
+ ```
206
291
 
207
292
  ## Named predicates (tbd)
208
293
 
@@ -0,0 +1,38 @@
1
+ require 'smartdown/engine/predicate_evaluator'
2
+
3
+ module Smartdown
4
+ class Engine
5
+ class ConditionalResolver
6
+ def initialize(predicate_evaluator = nil)
7
+ @predicate_evaluator = predicate_evaluator || PredicateEvaluator.new
8
+ end
9
+
10
+ def call(node, state)
11
+ node.dup.tap do |new_node|
12
+ new_node.elements = resolve_conditionals(node.elements, state)
13
+ end
14
+ end
15
+
16
+ private
17
+ attr_accessor :predicate_evaluator
18
+
19
+ def evaluate(conditional, state)
20
+ if predicate_evaluator.evaluate(conditional.predicate, state)
21
+ conditional.true_case
22
+ else
23
+ conditional.false_case
24
+ end
25
+ end
26
+
27
+ def resolve_conditionals(elements, state)
28
+ elements.map do |element|
29
+ if element.is_a?(Smartdown::Model::Element::Conditional)
30
+ evaluate(element, state)
31
+ else
32
+ element
33
+ end
34
+ end.flatten
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ module Smartdown
2
+ class Engine
3
+ class Interpolator
4
+ def call(node, state)
5
+ node.dup.tap do |new_node|
6
+ new_node.elements = interpolate_elements(node.elements, state)
7
+ end
8
+ end
9
+
10
+ private
11
+ def interpolator_for(element)
12
+ INTERPOLATOR_CONFIG.fetch(element.class, DEFAULT_INTERPOLATOR).new(element)
13
+ end
14
+
15
+ def interpolate_elements(elements, state)
16
+ elements.map do |element|
17
+ interpolator_for(element).call(state)
18
+ end
19
+ end
20
+
21
+ class NullElementInterpolator
22
+ attr_reader :element
23
+
24
+ def initialize(element)
25
+ @element = element
26
+ end
27
+
28
+ def call(state)
29
+ element
30
+ end
31
+ end
32
+
33
+ class ElementContentInterpolator < NullElementInterpolator
34
+ def call(state)
35
+ element.dup.tap do |e|
36
+ e.content = interpolate(e.content, state)
37
+ end
38
+ end
39
+
40
+ private
41
+ def interpolate(text, state)
42
+ text.to_s.gsub(/%{([^}]+)}/) { |_| state.get($1) }
43
+ end
44
+ end
45
+
46
+ INTERPOLATOR_CONFIG = {
47
+ Smartdown::Model::Element::MarkdownParagraph => ElementContentInterpolator,
48
+ Smartdown::Model::Element::MarkdownHeading => ElementContentInterpolator
49
+ }
50
+
51
+ DEFAULT_INTERPOLATOR = NullElementInterpolator
52
+
53
+ end
54
+ end
55
+ end
@@ -1,38 +1,19 @@
1
- require 'smartdown/engine/predicate_evaluator'
1
+ require 'smartdown/engine/conditional_resolver'
2
+ require 'smartdown/engine/interpolator'
2
3
 
3
4
  module Smartdown
4
5
  class Engine
5
6
  class NodePresenter
6
- def initialize(predicate_evaluator = nil)
7
- @predicate_evaluator = predicate_evaluator || PredicateEvaluator.new
8
- end
7
+ PRESENTERS = [
8
+ Smartdown::Engine::ConditionalResolver.new,
9
+ Smartdown::Engine::Interpolator.new
10
+ ]
9
11
 
10
- def present(node, state)
11
- node.dup.tap do |new_node|
12
- new_node.elements = resolve_conditionals(node.elements, state)
12
+ def present(unpresented_node, state)
13
+ PRESENTERS.inject(unpresented_node) do |node, presenter_class|
14
+ presenter_class.call(node, state)
13
15
  end
14
16
  end
15
-
16
- private
17
- attr_accessor :predicate_evaluator
18
-
19
- def evaluate(conditional, state)
20
- if predicate_evaluator.evaluate(conditional.predicate, state)
21
- conditional.true_case
22
- else
23
- conditional.false_case
24
- end
25
- end
26
-
27
- def resolve_conditionals(elements, state)
28
- elements.map do |element|
29
- if element.is_a?(Smartdown::Model::Element::Conditional)
30
- evaluate(element, state)
31
- else
32
- element
33
- end
34
- end.flatten
35
- end
36
17
  end
37
18
  end
38
19
  end
@@ -51,7 +51,7 @@ module Smartdown
51
51
  if has_key?(key)
52
52
  @data.fetch(key.to_s)
53
53
  else
54
- raise UndefinedValue
54
+ raise UndefinedValue, "variable '#{key}' not defined", caller
55
55
  end
56
56
  end
57
57
 
@@ -14,23 +14,45 @@ module Smartdown
14
14
  end
15
15
 
16
16
  def next_node
17
- first_matching_rule(next_node_rules.rules).outcome
17
+ next_node_from_next_node_rules ||
18
+ next_node_from_start_button ||
19
+ raise(Smartdown::Engine::IndeterminateNextNode, "No next node rules defined for '#{node.name}'", caller)
18
20
  end
19
21
 
20
22
  def next_state
21
23
  state_with_input
22
24
  .put(:path, state.get(:path) + [node.name])
23
25
  .put(:responses, state.get(:responses) + [input])
24
- .put(node.name, input)
26
+ .put(input_variable_name, input)
25
27
  .put(:current_node, next_node)
26
28
  end
27
29
 
28
30
  private
29
31
  attr_reader :predicate_evaluator
30
32
 
33
+ def next_node_from_next_node_rules
34
+ next_node_rules && first_matching_rule(next_node_rules.rules).outcome
35
+ end
36
+
37
+ def next_node_from_start_button
38
+ start_button && start_button.start_node
39
+ end
40
+
41
+ def input_variable_name
42
+ input_variable_name_from_question || node.name
43
+ end
44
+
45
+ def input_variable_name_from_question
46
+ question = node.elements.find { |e| e.is_a?(Smartdown::Model::Element::MultipleChoice) }
47
+ question && question.name
48
+ end
49
+
31
50
  def next_node_rules
32
- node.elements.find { |e| e.is_a?(Smartdown::Model::NextNodeRules) } or \
33
- raise Smartdown::Engine::IndeterminateNextNode, "No next node rules defined for '#{node.name}'"
51
+ node.elements.find { |e| e.is_a?(Smartdown::Model::NextNodeRules) }
52
+ end
53
+
54
+ def start_button
55
+ node.elements.find { |e| e.is_a?(Smartdown::Model::Element::StartButton) }
34
56
  end
35
57
 
36
58
  def first_matching_rule(rules)
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Element
4
+ NextSteps = Struct.new(:content)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ require 'smartdown/parser/base'
2
+
3
+ module Smartdown
4
+ module Parser
5
+ module Element
6
+ class NextSteps < Base
7
+
8
+ rule(:markdown) { (str("[end_next_steps]").absnt? >> any).repeat.as(:content) }
9
+
10
+ rule(:next_steps_tag) {
11
+ str("[next_steps]") >>
12
+ optional_space >>
13
+ line_ending
14
+ }
15
+
16
+ rule(:next_steps_end_tag) {
17
+ str("[end_next_steps]") >>
18
+ optional_space >>
19
+ line_ending
20
+ }
21
+
22
+ rule(:next_steps) {
23
+ (
24
+ next_steps_tag >>
25
+ markdown >>
26
+ next_steps_end_tag
27
+ ).as(:next_steps)
28
+ }
29
+ root(:next_steps)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -6,17 +6,19 @@ require 'smartdown/parser/element/multiple_choice_question'
6
6
  require 'smartdown/parser/element/markdown_heading'
7
7
  require 'smartdown/parser/element/markdown_paragraph'
8
8
  require 'smartdown/parser/element/conditional'
9
+ require 'smartdown/parser/element/next_steps'
9
10
 
10
11
  module Smartdown
11
12
  module Parser
12
13
  class NodeParser < Base
13
14
  rule(:markdown_block) {
14
15
  Element::Conditional.new |
15
- Element::MarkdownHeading.new |
16
- Element::MultipleChoiceQuestion.new |
17
- Rules.new |
18
- Element::StartButton.new |
19
- Element::MarkdownParagraph.new
16
+ Element::MarkdownHeading.new |
17
+ Element::MultipleChoiceQuestion.new |
18
+ Rules.new |
19
+ Element::StartButton.new |
20
+ Element::NextSteps.new |
21
+ Element::MarkdownParagraph.new
20
22
  }
21
23
 
22
24
  rule(:markdown_blocks) {
@@ -29,8 +31,8 @@ module Smartdown
29
31
 
30
32
  rule(:flow) {
31
33
  Element::FrontMatter.new >> newline.repeat(1) >> body |
32
- Element::FrontMatter.new |
33
- ws >> body
34
+ Element::FrontMatter.new |
35
+ ws >> body
34
36
  }
35
37
 
36
38
  root(:flow)
@@ -9,6 +9,7 @@ require 'smartdown/model/element/start_button'
9
9
  require 'smartdown/model/element/markdown_heading'
10
10
  require 'smartdown/model/element/markdown_paragraph'
11
11
  require 'smartdown/model/element/conditional'
12
+ require 'smartdown/model/element/next_steps'
12
13
  require 'smartdown/model/predicate/equality'
13
14
  require 'smartdown/model/predicate/set_membership'
14
15
  require 'smartdown/model/predicate/named'
@@ -50,12 +51,20 @@ module Smartdown
50
51
  [value.to_s, label.to_s]
51
52
  }
52
53
 
54
+ rule(:url => simple(:url), :label => simple(:label)) {
55
+ [url.to_s, label.to_s]
56
+ }
57
+
53
58
  rule(:multiple_choice => {identifier: simple(:identifier), options: subtree(:choices)}) {
54
59
  Smartdown::Model::Element::MultipleChoice.new(
55
60
  identifier, Hash[choices]
56
61
  )
57
62
  }
58
63
 
64
+ rule(:next_steps => { content: simple(:content) }) {
65
+ Smartdown::Model::Element::NextSteps.new(content.to_s)
66
+ }
67
+
59
68
  # Conditional with no content in true-case
60
69
  rule(:conditional => {:predicate => subtree(:predicate)}) {
61
70
  Smartdown::Model::Element::Conditional.new(predicate)
@@ -1,3 +1,3 @@
1
1
  module Smartdown
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -1,8 +1,8 @@
1
- require 'smartdown/engine/node_presenter'
1
+ require 'smartdown/engine/conditional_resolver'
2
2
  require 'smartdown/engine/state'
3
3
 
4
- describe Smartdown::Engine::NodePresenter do
5
- subject(:node_presenter) { described_class.new }
4
+ describe Smartdown::Engine::ConditionalResolver do
5
+ subject(:conditional_resolver) { described_class.new }
6
6
 
7
7
  context "a node with a conditional" do
8
8
  let(:node) {
@@ -34,7 +34,7 @@ describe Smartdown::Engine::NodePresenter do
34
34
  }
35
35
 
36
36
  it "should resolve the conditional and preserve the 'True case' paragraph block" do
37
- expect(node_presenter.present(node, state)).to eq(expected_node_after_presentation)
37
+ expect(conditional_resolver.call(node, state)).to eq(expected_node_after_presentation)
38
38
  end
39
39
  end
40
40
 
@@ -53,7 +53,7 @@ describe Smartdown::Engine::NodePresenter do
53
53
  }
54
54
 
55
55
  it "should resolve the conditional and preserve the 'False case' paragraph block" do
56
- expect(node_presenter.present(node, state)).to eq(expected_node_after_presentation)
56
+ expect(conditional_resolver.call(node, state)).to eq(expected_node_after_presentation)
57
57
  end
58
58
  end
59
59
  end
@@ -0,0 +1,81 @@
1
+ require 'smartdown/engine/interpolator'
2
+ require 'smartdown/engine/state'
3
+ require 'parslet'
4
+
5
+ describe Smartdown::Engine::Interpolator do
6
+ subject(:interpolator) { described_class.new }
7
+
8
+ let(:example_name) { "Neil" }
9
+
10
+ let(:state) {
11
+ Smartdown::Engine::State.new(
12
+ current_node: node.name,
13
+ name: example_name
14
+ )
15
+ }
16
+
17
+ let(:interpolated_node) { interpolator.call(node, state) }
18
+
19
+ let(:node) {
20
+ Smartdown::Model::Node.new("example", elements)
21
+ }
22
+
23
+ context "a node with no elements" do
24
+ let(:elements) { [] }
25
+
26
+ it "returns the node unchanged" do
27
+ expect(interpolator.call(node, state)).to eq(node)
28
+ end
29
+ end
30
+
31
+ context "a node with elements with no interpolations" do
32
+ let(:node) {
33
+ model_builder.node("example") do
34
+ heading("a heading")
35
+ paragraph("some_stuff")
36
+ conditional do
37
+ named_predicate "pred?"
38
+ true_case do
39
+ paragraph("True case")
40
+ end
41
+ false_case do
42
+ paragraph("False case")
43
+ end
44
+ end
45
+ multiple_choice({})
46
+ next_node_rules
47
+ end
48
+ }
49
+
50
+ it "returns the node unchanged" do
51
+ expect(interpolated_node).to eq(node)
52
+ end
53
+ end
54
+
55
+ context "a node with a paragraph containing an interpolation" do
56
+ let(:elements) { [Smartdown::Model::Element::MarkdownParagraph.new('Hello %{name}')] }
57
+
58
+ it "interpolates the name into the paragraph content" do
59
+ expect(interpolated_node.elements.first.content).to eq("Hello #{example_name}")
60
+ end
61
+ end
62
+
63
+ context "a node with a heading containing an interpolation" do
64
+ let(:elements) { [Smartdown::Model::Element::MarkdownHeading.new('Hello %{name}')] }
65
+
66
+ it "interpolates the name into the paragraph content" do
67
+ expect(interpolated_node.elements.first.content).to eq("Hello #{example_name}")
68
+ end
69
+ end
70
+
71
+ context "a paragraph containing a parslet slice" do
72
+ let(:elements) { [Smartdown::Model::Element::MarkdownParagraph.new(Parslet::Slice.new(0, 'Hello %{name}'))] }
73
+
74
+ it "interpolates without raising an error about gsub missing" do
75
+ # Note: the parser actually produces parslet 'slices' rather than strings.
76
+ # A parslet slice behaves like a string but doesn't have all of the methods of string.
77
+ # This test is to document that fact and catch any regressions.
78
+ expect(interpolated_node.elements.first.content).to eq("Hello #{example_name}")
79
+ end
80
+ end
81
+ end
@@ -8,7 +8,7 @@ describe Smartdown::Engine::Transition do
8
8
  let(:start_state) { Smartdown::Engine::State.new(current_node: current_node_name) }
9
9
  let(:input) { "yes" }
10
10
  subject(:transition) { described_class.new(start_state, current_node, input, predicate_evaluator: predicate_evaluator) }
11
- let(:predicate_evaluator) { instance_double("Smartdown::Engine::PredicateEvaluator") }
11
+ let(:predicate_evaluator) { instance_double("Smartdown::Engine::PredicateEvaluator", evaluate: true) }
12
12
  let(:state_including_input) {
13
13
  start_state.put(current_node.name, input)
14
14
  }
@@ -25,6 +25,20 @@ describe Smartdown::Engine::Transition do
25
25
  end
26
26
  end
27
27
 
28
+ context "no next node rules, but start button" do
29
+ let(:current_node) {
30
+ Smartdown::Model::Node.new(current_node_name, [
31
+ Smartdown::Model::Element::StartButton.new("first_question")
32
+ ])
33
+ }
34
+
35
+ describe "#next_node" do
36
+ it "take the next node from the start button" do
37
+ expect(transition.next_node).to eq("first_question")
38
+ end
39
+ end
40
+ end
41
+
28
42
  context "next node rules defined with a simple rule" do
29
43
  let(:predicate1) { double("predicate1") }
30
44
  let(:outcome_name1) { "o1" }
@@ -72,10 +86,6 @@ describe Smartdown::Engine::Transition do
72
86
  end
73
87
 
74
88
  describe "#next_state" do
75
- before(:each) do
76
- allow(predicate_evaluator).to receive(:evaluate).and_return(true)
77
- end
78
-
79
89
  it "returns a state including a record of responses, path, and new current_node" do
80
90
  expected_state = start_state
81
91
  .put(:responses, [input])
@@ -147,4 +157,27 @@ describe Smartdown::Engine::Transition do
147
157
  end
148
158
  end
149
159
 
160
+ context "next node rules and a named question" do
161
+ let(:question_name) { "my_question" }
162
+
163
+ let(:current_node) {
164
+ Smartdown::Model::Node.new(
165
+ current_node_name,
166
+ [
167
+ Smartdown::Model::Element::MultipleChoice.new(question_name, {"a" => "Apple"}),
168
+ Smartdown::Model::NextNodeRules.new(
169
+ [Smartdown::Model::Rule.new(double("predicate1"), "o1")]
170
+ )
171
+ ]
172
+ )
173
+ }
174
+
175
+ describe "#next_state" do
176
+ it "assigns the input value to a variable matching the question name" do
177
+ expect(transition.next_state.get(question_name)).to eq(input)
178
+ end
179
+ end
180
+
181
+ end
182
+
150
183
  end
data/spec/engine_spec.rb CHANGED
@@ -7,12 +7,6 @@ describe Smartdown::Engine do
7
7
  heading("Check uk visa")
8
8
  paragraph("This is the paragraph")
9
9
  start_button("what_passport_do_you_have?")
10
- next_node_rules do
11
- rule do
12
- named_predicate("otherwise")
13
- outcome("what_passport_do_you_have?")
14
- end
15
- end
16
10
  end
17
11
 
18
12
  node("what_passport_do_you_have?") do
@@ -41,6 +35,10 @@ describe Smartdown::Engine do
41
35
  end
42
36
  end
43
37
  end
38
+
39
+ node("outcome_with_interpolation") do
40
+ paragraph("The answer is %{interpolated_variable}")
41
+ end
44
42
  end
45
43
  }
46
44
 
@@ -92,20 +90,40 @@ describe Smartdown::Engine do
92
90
  end
93
91
 
94
92
  describe "#evaluate_node" do
95
- let(:current_state) {
96
- start_state
97
- .put(:current_node, "outcome_no_visa_needed")
98
- .put(:pred?, true)
99
- }
100
-
101
- let(:expected_node_after_conditional_resolution) {
102
- model_builder.node("outcome_no_visa_needed") do
103
- paragraph("True case")
93
+ context "conditional resolution" do
94
+ let(:current_state) {
95
+ start_state
96
+ .put(:current_node, "outcome_no_visa_needed")
97
+ .put(:pred?, true)
98
+ }
99
+
100
+ let(:expected_node_after_conditional_resolution) {
101
+ model_builder.node("outcome_no_visa_needed") do
102
+ paragraph("True case")
103
+ end
104
+ }
105
+
106
+ it "evaluates the current node of the given state, resolving any conditionals" do
107
+ expect(engine.evaluate_node(current_state)).to eq(expected_node_after_conditional_resolution)
104
108
  end
105
- }
109
+ end
110
+
111
+ context "interpolation" do
112
+ let(:current_state) {
113
+ start_state
114
+ .put(:current_node, "outcome_with_interpolation")
115
+ .put(:interpolated_variable, "42")
116
+ }
106
117
 
107
- it "evaluates the current node of the given state, resolving any conditionals" do
108
- expect(engine.evaluate_node(current_state)).to eq(expected_node_after_conditional_resolution)
118
+ let(:expected_node_after_conditional_resolution) {
119
+ model_builder.node("outcome_with_interpolation") do
120
+ paragraph("The answer is 42")
121
+ end
122
+ }
123
+
124
+ it "evaluates the current node of the given state, resolving any conditionals" do
125
+ expect(engine.evaluate_node(current_state)).to eq(expected_node_after_conditional_resolution)
126
+ end
109
127
  end
110
128
  end
111
129
 
@@ -0,0 +1,33 @@
1
+ require 'smartdown/parser/element/next_steps'
2
+ require 'smartdown/parser/node_parser'
3
+ require 'smartdown/parser/node_interpreter'
4
+
5
+ describe Smartdown::Parser::Element::NextSteps do
6
+ subject(:parser) { described_class.new }
7
+
8
+ let(:source) {
9
+ [
10
+ "[next_steps]",
11
+ "some markdown",
12
+ "some more markdown",
13
+ "[end_next_steps]"
14
+ ].join("\n")
15
+ }
16
+
17
+ it "parses" do
18
+ should parse(source).as(
19
+ next_steps: {
20
+ content: "some markdown\nsome more markdown\n"
21
+ }
22
+ )
23
+ end
24
+
25
+ describe "transformed" do
26
+ let(:node_name) { "my_node" }
27
+ subject(:transformed) {
28
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
29
+ }
30
+
31
+ it { should eq(Smartdown::Model::Element::NextSteps.new("some markdown\nsome more markdown\n")) }
32
+ end
33
+ end
@@ -52,6 +52,13 @@ class ModelBuilder
52
52
  @elements.last
53
53
  end
54
54
 
55
+ def next_steps(urls)
56
+ @elements ||= []
57
+ urls_with_string_keys = ::Hash[urls.map {|k,v| [k.to_s, v]}]
58
+ @elements << Smartdown::Model::Element::NextSteps.new(urls_with_string_keys)
59
+ @elements.last
60
+ end
61
+
55
62
  def next_node_rules(&block)
56
63
  @rules = []
57
64
  instance_eval(&block) if block_given?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smartdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -96,12 +96,15 @@ files:
96
96
  - lib/smartdown/parser/element/start_button.rb
97
97
  - lib/smartdown/parser/element/front_matter.rb
98
98
  - lib/smartdown/parser/element/multiple_choice_question.rb
99
+ - lib/smartdown/parser/element/next_steps.rb
99
100
  - lib/smartdown/parser/element/markdown_heading.rb
100
101
  - lib/smartdown/parser/flow_interpreter.rb
101
102
  - lib/smartdown/version.rb
102
103
  - lib/smartdown/engine/node_presenter.rb
103
104
  - lib/smartdown/engine/state.rb
105
+ - lib/smartdown/engine/conditional_resolver.rb
104
106
  - lib/smartdown/engine/transition.rb
107
+ - lib/smartdown/engine/interpolator.rb
105
108
  - lib/smartdown/engine/predicate_evaluator.rb
106
109
  - lib/smartdown/engine/errors.rb
107
110
  - lib/smartdown/model/flow.rb
@@ -113,6 +116,7 @@ files:
113
116
  - lib/smartdown/model/element/conditional.rb
114
117
  - lib/smartdown/model/element/markdown_paragraph.rb
115
118
  - lib/smartdown/model/element/start_button.rb
119
+ - lib/smartdown/model/element/next_steps.rb
116
120
  - lib/smartdown/model/element/markdown_heading.rb
117
121
  - lib/smartdown/model/element/multiple_choice.rb
118
122
  - lib/smartdown/model/next_node_rules.rb
@@ -128,6 +132,7 @@ files:
128
132
  - spec/parser/base_spec.rb
129
133
  - spec/parser/rules_spec.rb
130
134
  - spec/parser/predicates_spec.rb
135
+ - spec/parser/element/next_steps_spec.rb
131
136
  - spec/parser/element/multiple_choice_question_spec.rb
132
137
  - spec/parser/element/conditional_spec.rb
133
138
  - spec/parser/element/start_button_parser_spec.rb
@@ -138,9 +143,10 @@ files:
138
143
  - spec/support/model_builder.rb
139
144
  - spec/spec_helper.rb
140
145
  - spec/engine/state_spec.rb
141
- - spec/engine/node_presenter_spec.rb
146
+ - spec/engine/interpolator_spec.rb
142
147
  - spec/engine/predicate_evaluator_spec.rb
143
148
  - spec/engine/transition_spec.rb
149
+ - spec/engine/conditional_resolver_spec.rb
144
150
  - spec/fixtures/directory_input/scenarios/s1.txt
145
151
  - spec/fixtures/directory_input/outcomes/o1.txt
146
152
  - spec/fixtures/directory_input/questions/q1.txt
@@ -171,7 +177,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
171
177
  version: '0'
172
178
  segments:
173
179
  - 0
174
- hash: 1408391630094207002
180
+ hash: 3856298649637769047
175
181
  required_rubygems_version: !ruby/object:Gem::Requirement
176
182
  none: false
177
183
  requirements:
@@ -180,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
180
186
  version: '0'
181
187
  segments:
182
188
  - 0
183
- hash: 1408391630094207002
189
+ hash: 3856298649637769047
184
190
  requirements: []
185
191
  rubyforge_project:
186
192
  rubygems_version: 1.8.23
@@ -195,6 +201,7 @@ test_files:
195
201
  - spec/parser/base_spec.rb
196
202
  - spec/parser/rules_spec.rb
197
203
  - spec/parser/predicates_spec.rb
204
+ - spec/parser/element/next_steps_spec.rb
198
205
  - spec/parser/element/multiple_choice_question_spec.rb
199
206
  - spec/parser/element/conditional_spec.rb
200
207
  - spec/parser/element/start_button_parser_spec.rb
@@ -205,9 +212,10 @@ test_files:
205
212
  - spec/support/model_builder.rb
206
213
  - spec/spec_helper.rb
207
214
  - spec/engine/state_spec.rb
208
- - spec/engine/node_presenter_spec.rb
215
+ - spec/engine/interpolator_spec.rb
209
216
  - spec/engine/predicate_evaluator_spec.rb
210
217
  - spec/engine/transition_spec.rb
218
+ - spec/engine/conditional_resolver_spec.rb
211
219
  - spec/fixtures/directory_input/scenarios/s1.txt
212
220
  - spec/fixtures/directory_input/outcomes/o1.txt
213
221
  - spec/fixtures/directory_input/questions/q1.txt