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 +105 -20
- data/lib/smartdown/engine/conditional_resolver.rb +38 -0
- data/lib/smartdown/engine/interpolator.rb +55 -0
- data/lib/smartdown/engine/node_presenter.rb +9 -28
- data/lib/smartdown/engine/state.rb +1 -1
- data/lib/smartdown/engine/transition.rb +26 -4
- data/lib/smartdown/model/element/next_steps.rb +7 -0
- data/lib/smartdown/parser/element/next_steps.rb +33 -0
- data/lib/smartdown/parser/node_parser.rb +9 -7
- data/lib/smartdown/parser/node_transform.rb +9 -0
- data/lib/smartdown/version.rb +1 -1
- data/spec/engine/{node_presenter_spec.rb → conditional_resolver_spec.rb} +5 -5
- data/spec/engine/interpolator_spec.rb +81 -0
- data/spec/engine/transition_spec.rb +38 -5
- data/spec/engine_spec.rb +36 -18
- data/spec/parser/element/next_steps_spec.rb +33 -0
- data/spec/support/model_builder.rb +7 -0
- metadata +13 -5
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
|
-
|
54
|
-
* **
|
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,
|
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
|
61
|
-
|
62
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
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
|
-
*
|
181
|
-
*
|
182
|
-
*
|
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
|
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/
|
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
|
-
|
7
|
-
|
8
|
-
|
7
|
+
PRESENTERS = [
|
8
|
+
Smartdown::Engine::ConditionalResolver.new,
|
9
|
+
Smartdown::Engine::Interpolator.new
|
10
|
+
]
|
9
11
|
|
10
|
-
def present(
|
11
|
-
|
12
|
-
|
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
|
@@ -14,23 +14,45 @@ module Smartdown
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def next_node
|
17
|
-
|
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(
|
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) }
|
33
|
-
|
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,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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
33
|
-
|
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)
|
data/lib/smartdown/version.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require 'smartdown/engine/
|
1
|
+
require 'smartdown/engine/conditional_resolver'
|
2
2
|
require 'smartdown/engine/state'
|
3
3
|
|
4
|
-
describe Smartdown::Engine::
|
5
|
-
subject(:
|
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(
|
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(
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
108
|
-
|
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.
|
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/
|
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:
|
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:
|
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/
|
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
|