smartdown 0.0.3 → 0.0.4

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.
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