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