smartdown 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -78,15 +78,16 @@ question type.
78
78
 
79
79
  The next sections define the various question types
80
80
 
81
- ### Multiple choice
81
+ ### Radio buttons
82
82
 
83
83
  ```markdown
84
- # Will you pass through UK Border Control?
84
+ ## Will you pass through UK Border Control?
85
85
 
86
86
  You might pass through UK Border Control even if you don't leave the airport -
87
87
  eg your bags aren't checked through and you need to collect them before transferring
88
88
  to your outbound flight.
89
89
 
90
+ [choice: uk_border_control]
90
91
  * yes: Yes
91
92
  * no: No
92
93
  ```
@@ -94,28 +95,33 @@ to your outbound flight.
94
95
  ### Country (tbd)
95
96
 
96
97
  ```markdown
97
- # What passport do you have?
98
+ ## What passport do you have?
98
99
 
99
- [country]
100
+ [country: passport_country]
100
101
  ```
101
102
 
102
103
  Presents a drop-down list of countries from the built-in list.
103
104
 
104
105
  Use front matter to exclude/include countries
105
106
 
106
- ```
107
- exclude_countries: country1, country2
108
- include_countries: {country3: "Country 3", country4: "Country 4"}
107
+ ```markdown
108
+ ---
109
+ passport_country:
110
+ exclude_countries: country1, country2
111
+ include_countries: {country3: "Country 3", country4: "Country 4"}
112
+ ---
109
113
 
110
- [country]
114
+ ## What passport do you have?
115
+
116
+ [country: passport_country]
111
117
  ```
112
118
 
113
119
  ### Date (tbd)
114
120
 
115
121
  ```markdown
116
- # What is the baby’s due date?
122
+ ## What is the baby’s due date?
117
123
 
118
- [d/m/y: -1...+1]
124
+ [date: baby_due_date]
119
125
  ```
120
126
 
121
127
  Asks for a specific date in the given range. Ranges can be expressed as a relative number of years or absolute dates in YYYY-MM-DD format.
@@ -139,7 +145,7 @@ Asks for a numerical input which can have decimals and optional thousand-separat
139
145
  ### Salary (tbd)
140
146
 
141
147
  ```markdown
142
- [salary]
148
+ [salary: salary_value]
143
149
  ```
144
150
 
145
151
  Asks for salary which can be expressed as either a weekly or monthly money amount. The user chooses between weekly/monthly
@@ -147,8 +153,9 @@ Asks for salary which can be expressed as either a weekly or monthly money amoun
147
153
  ### Checkbox (tbd)
148
154
 
149
155
  ```markdown
150
- # Will you pass through UK Border Control?
156
+ ## Will you pass through UK Border Control?
151
157
 
158
+ [checkbox: uk_border_control]
152
159
  * [ ] yes: Yes
153
160
  * [ ] no: No
154
161
  ```
@@ -184,6 +191,13 @@ variable_name is 'string'
184
191
  variable_name in {this that the-other}
185
192
  ```
186
193
 
194
+ ### Date comparison predicates
195
+
196
+ ```
197
+ date_variable_name >= '14/07/2014'
198
+ date_variable_name < '14/07/2014'
199
+ ```
200
+
187
201
  ## Conditional blocks in outcomes (tbd)
188
202
 
189
203
  ## Processing model
data/bin/smartdown CHANGED
@@ -17,6 +17,7 @@ if ARGV.size == 0
17
17
  else
18
18
  coversheet_path, *responses = ARGV
19
19
  begin
20
+ #TODO: use same object as smartdown-frontend will be using here
20
21
  input = Smartdown::Parser::DirectoryInput.new(coversheet_path)
21
22
  flow = Smartdown::Parser::FlowInterpreter.new(input).interpret
22
23
  engine = Smartdown::Engine.new(flow)
@@ -24,7 +25,7 @@ else
24
25
 
25
26
  puts "RESPONSES: " + end_state.get(:responses).join(" / ")
26
27
  puts "PATH: " + (end_state.get(:path) + [end_state.get(:current_node)]).join(" -> ")
27
- node = flow.node(end_state.get(:current_node))
28
+ node = engine.evaluate_node(end_state)
28
29
  puts "# #{node.title}\n\n"
29
30
  node.questions.each do |q|
30
31
  pp q.choices
@@ -0,0 +1,38 @@
1
+ require 'smartdown/engine/predicate_evaluator'
2
+
3
+ module Smartdown
4
+ class Engine
5
+ class NodePresenter
6
+ def initialize(predicate_evaluator = nil)
7
+ @predicate_evaluator = predicate_evaluator || PredicateEvaluator.new
8
+ end
9
+
10
+ def present(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
@@ -1,5 +1,6 @@
1
1
  require 'smartdown/engine/transition'
2
2
  require 'smartdown/engine/state'
3
+ require 'smartdown/engine/node_presenter'
3
4
 
4
5
  module Smartdown
5
6
  class Engine
@@ -29,5 +30,10 @@ module Smartdown
29
30
  Transition.new(state, current_node, input).next_state
30
31
  end
31
32
  end
33
+
34
+ def evaluate_node(state)
35
+ current_node = flow.node(state.get(:current_node))
36
+ NodePresenter.new.present(current_node, state)
37
+ end
32
38
  end
33
39
  end
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Element
4
+ Conditional = Struct.new(:predicate, :true_case, :false_case)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ require 'smartdown/parser/base'
2
+ require 'smartdown/parser/node_parser'
3
+ require 'smartdown/parser/predicates'
4
+
5
+ module Smartdown
6
+ module Parser
7
+ module Element
8
+ class Conditional < Base
9
+ rule(:markdown_block_inside_conditional) {
10
+ str("$").absent? >> NodeParser.new.markdown_block
11
+ }
12
+
13
+ rule(:markdown_blocks_inside_conditional) {
14
+ markdown_block_inside_conditional.repeat(1,1) >> (newline.repeat(1) >> markdown_block_inside_conditional).repeat
15
+ }
16
+
17
+ rule(:else_clause) {
18
+ str("$ELSE") >> optional_space >> newline.repeat(2) >>
19
+ (markdown_blocks_inside_conditional.as(:false_case) >> newline).maybe
20
+ }
21
+
22
+ rule(:conditional_clause) {
23
+ (
24
+ str("$IF ") >>
25
+ Predicates.new.as(:predicate) >>
26
+ optional_space >> newline.repeat(2) >>
27
+ (markdown_blocks_inside_conditional.as(:true_case) >> newline).maybe >>
28
+ else_clause.maybe >>
29
+ str("$ENDIF") >> optional_space >> line_ending
30
+ ).as(:conditional)
31
+ }
32
+
33
+ root(:conditional_clause)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -4,6 +4,16 @@ module Smartdown
4
4
  module Parser
5
5
  module Element
6
6
  class MultipleChoiceQuestion < Base
7
+ rule(:multiple_choice_question_tag) {
8
+ str("[choice:") >>
9
+ optional_space >>
10
+ question_identifier.as(:identifier) >>
11
+ optional_space >>
12
+ str("]") >>
13
+ optional_space >>
14
+ line_ending
15
+ }
16
+
7
17
  rule(:option_definition_line) {
8
18
  bullet >>
9
19
  optional_space >>
@@ -15,7 +25,10 @@ module Smartdown
15
25
  }
16
26
 
17
27
  rule(:multiple_choice_question) {
18
- (option_definition_line >> line_ending).repeat(1).as(:multiple_choice)
28
+ (
29
+ multiple_choice_question_tag >>
30
+ (option_definition_line >> line_ending).repeat(1).as(:options)
31
+ ).as(:multiple_choice)
19
32
  }
20
33
  root(:multiple_choice_question)
21
34
  end
@@ -5,12 +5,14 @@ require 'smartdown/parser/element/start_button'
5
5
  require 'smartdown/parser/element/multiple_choice_question'
6
6
  require 'smartdown/parser/element/markdown_heading'
7
7
  require 'smartdown/parser/element/markdown_paragraph'
8
+ require 'smartdown/parser/element/conditional'
8
9
 
9
10
  module Smartdown
10
11
  module Parser
11
12
  class NodeParser < Base
12
13
  rule(:markdown_block) {
13
- Element::MarkdownHeading.new |
14
+ Element::Conditional.new |
15
+ Element::MarkdownHeading.new |
14
16
  Element::MultipleChoiceQuestion.new |
15
17
  Rules.new |
16
18
  Element::StartButton.new |
@@ -18,7 +20,7 @@ module Smartdown
18
20
  }
19
21
 
20
22
  rule(:markdown_blocks) {
21
- markdown_block >> (newline.repeat(1) >> markdown_block).repeat
23
+ markdown_block.repeat(1, 1) >> (newline.repeat(1) >> markdown_block).repeat
22
24
  }
23
25
 
24
26
  rule(:body) {
@@ -8,6 +8,7 @@ require 'smartdown/model/element/multiple_choice'
8
8
  require 'smartdown/model/element/start_button'
9
9
  require 'smartdown/model/element/markdown_heading'
10
10
  require 'smartdown/model/element/markdown_paragraph'
11
+ require 'smartdown/model/element/conditional'
11
12
  require 'smartdown/model/predicate/equality'
12
13
  require 'smartdown/model/predicate/set_membership'
13
14
  require 'smartdown/model/predicate/named'
@@ -49,12 +50,34 @@ module Smartdown
49
50
  [value.to_s, label.to_s]
50
51
  }
51
52
 
52
- rule(:multiple_choice => subtree(:choices)) {
53
+ rule(:multiple_choice => {identifier: simple(:identifier), options: subtree(:choices)}) {
53
54
  Smartdown::Model::Element::MultipleChoice.new(
54
- node_name, Hash[choices]
55
+ identifier, Hash[choices]
55
56
  )
56
57
  }
57
58
 
59
+ # Conditional with no content in true-case
60
+ rule(:conditional => {:predicate => subtree(:predicate)}) {
61
+ Smartdown::Model::Element::Conditional.new(predicate)
62
+ }
63
+
64
+ # Conditional with content in true-case
65
+ rule(:conditional => {
66
+ :predicate => subtree(:predicate),
67
+ :true_case => subtree(:true_case)
68
+ }) {
69
+ Smartdown::Model::Element::Conditional.new(predicate, true_case)
70
+ }
71
+
72
+ # Conditional with content in both true-case and false-case
73
+ rule(:conditional => {
74
+ :predicate => subtree(:predicate),
75
+ :true_case => subtree(:true_case),
76
+ :false_case => subtree(:false_case)
77
+ }) {
78
+ Smartdown::Model::Element::Conditional.new(predicate, true_case, false_case)
79
+ }
80
+
58
81
  rule(:equality_predicate => { varname: simple(:varname), expected_value: simple(:expected_value) }) {
59
82
  Smartdown::Model::Predicate::Equality.new(varname, expected_value)
60
83
  }
@@ -1,3 +1,3 @@
1
1
  module Smartdown
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -0,0 +1,60 @@
1
+ require 'smartdown/engine/node_presenter'
2
+ require 'smartdown/engine/state'
3
+
4
+ describe Smartdown::Engine::NodePresenter do
5
+ subject(:node_presenter) { described_class.new }
6
+
7
+ context "a node with a conditional" do
8
+ let(:node) {
9
+ model_builder.node("outcome_no_visa_needed") do
10
+ conditional do
11
+ named_predicate "pred?"
12
+ true_case do
13
+ paragraph("True case")
14
+ end
15
+ false_case do
16
+ paragraph("False case")
17
+ end
18
+ end
19
+ end
20
+ }
21
+
22
+ context "pred is true" do
23
+ let(:state) {
24
+ Smartdown::Engine::State.new(
25
+ current_node: node.name,
26
+ pred?: true
27
+ )
28
+ }
29
+
30
+ let(:expected_node_after_presentation) {
31
+ model_builder.node("outcome_no_visa_needed") do
32
+ paragraph("True case")
33
+ end
34
+ }
35
+
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)
38
+ end
39
+ end
40
+
41
+ context "pred is false" do
42
+ let(:state) {
43
+ Smartdown::Engine::State.new(
44
+ current_node: node.name,
45
+ pred?: false
46
+ )
47
+ }
48
+
49
+ let(:expected_node_after_presentation) {
50
+ model_builder.node("outcome_no_visa_needed") do
51
+ paragraph("False case")
52
+ end
53
+ }
54
+
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)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -19,7 +19,7 @@ describe Smartdown::Engine::State do
19
19
  expect { subject.get(:a) }.to raise_error(Smartdown::Engine::UndefinedValue)
20
20
  end
21
21
 
22
- it "by string or symbol" do
22
+ it "is indifferent to symbols and strings" do
23
23
  expect(subject.get(:current_node)).to eq(:start_state)
24
24
  expect(subject.get("current_node")).to eq(:start_state)
25
25
  end
@@ -38,7 +38,7 @@ describe Smartdown::Engine::State do
38
38
  expect(new_state.get(:a)).to eq 1
39
39
  end
40
40
 
41
- it "by string or symbol" do
41
+ it "is indifferent to symbols and strings" do
42
42
  s2 = subject.put(:b, 1)
43
43
  expect(s2.get(:b)).to eq(1)
44
44
  expect(s2.get("b")).to eq(1)
data/spec/engine_spec.rb CHANGED
@@ -29,6 +29,18 @@ describe Smartdown::Engine do
29
29
  end
30
30
  end
31
31
  end
32
+
33
+ node("outcome_no_visa_needed") do
34
+ conditional do
35
+ named_predicate "pred?"
36
+ true_case do
37
+ paragraph("True case")
38
+ end
39
+ false_case do
40
+ paragraph("False case")
41
+ end
42
+ end
43
+ end
32
44
  end
33
45
  }
34
46
 
@@ -47,11 +59,13 @@ describe Smartdown::Engine do
47
59
  context "start button response only" do
48
60
  let(:responses) { %w{yes} }
49
61
 
50
- it "is on what_passport_do_you_have?" do
62
+ it { should be_a(Smartdown::Engine::State) }
63
+
64
+ it "current_node of 'what_passport_do_you_have?'" do
51
65
  expect(subject.get(:current_node)).to eq("what_passport_do_you_have?")
52
66
  end
53
67
 
54
- it "has recorded input" do
68
+ it "input recorded in state variable corresponding to node name" do
55
69
  expect(subject.get("check-uk-visa")).to eq("yes")
56
70
  end
57
71
  end
@@ -59,7 +73,7 @@ describe Smartdown::Engine do
59
73
  context "greek passport" do
60
74
  let(:responses) { %w{yes greek} }
61
75
 
62
- it "is on what_passport_do_you_have?" do
76
+ it "is on outcome_no_visa_needed" do
63
77
  expect(subject.get(:current_node)).to eq("outcome_no_visa_needed")
64
78
  end
65
79
 
@@ -76,4 +90,23 @@ describe Smartdown::Engine do
76
90
  end
77
91
  end
78
92
  end
93
+
94
+ 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")
104
+ end
105
+ }
106
+
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)
109
+ end
110
+ end
111
+
79
112
  end
@@ -5,5 +5,6 @@ Body text line 1.
5
5
  Body text
6
6
  para 2.
7
7
 
8
+ [choice: q1]
8
9
  * yes: Yes
9
10
  * no: No
@@ -5,5 +5,6 @@ Body text line 1.
5
5
  Body text
6
6
  para 2.
7
7
 
8
+ [choice: q1]
8
9
  * yes: Yes
9
10
  * no: No
@@ -0,0 +1,154 @@
1
+ require 'smartdown/parser/element/conditional'
2
+ require 'smartdown/parser/node_interpreter'
3
+
4
+ describe Smartdown::Parser::Element::Conditional do
5
+
6
+ subject(:parser) { described_class.new }
7
+ let(:node_name) { "my_node" }
8
+
9
+ context "simple IF" do
10
+ let(:source) { <<-SOURCE
11
+ $IF pred1?
12
+
13
+ #{true_body}
14
+
15
+ $ENDIF
16
+ SOURCE
17
+ }
18
+
19
+ context "with one-line true case" do
20
+ let(:true_body) { "Text if true" }
21
+
22
+ it {
23
+ should parse(source).as(
24
+ conditional: {
25
+ predicate: {named_predicate: "pred1?"},
26
+ true_case: [{p: "#{true_body}\n"}]
27
+ }
28
+ )
29
+ }
30
+
31
+ describe "transformed" do
32
+ subject(:transformed) {
33
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
34
+ }
35
+
36
+ it {
37
+ should eq(
38
+ Smartdown::Model::Element::Conditional.new(
39
+ Smartdown::Model::Predicate::Named.new("pred1?"),
40
+ [Smartdown::Model::Element::MarkdownParagraph.new(true_body + "\n")]
41
+ )
42
+ )
43
+ }
44
+ end
45
+
46
+ end
47
+
48
+ context "with multi-line true case" do
49
+ let(:one_line) { "Text if true" }
50
+ let(:true_body) { "#{one_line}\n\n#{one_line}" }
51
+
52
+ it {
53
+ should parse(source).as(
54
+ conditional: {
55
+ predicate: {named_predicate: "pred1?"},
56
+ true_case: [{p: "#{one_line}\n"}, {p: "#{one_line}\n"}]
57
+ }
58
+ )
59
+ }
60
+
61
+ describe "transformed" do
62
+ subject(:transformed) {
63
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
64
+ }
65
+
66
+ it {
67
+ should eq(
68
+ Smartdown::Model::Element::Conditional.new(
69
+ Smartdown::Model::Predicate::Named.new("pred1?"),
70
+ [
71
+ Smartdown::Model::Element::MarkdownParagraph.new(one_line + "\n"),
72
+ Smartdown::Model::Element::MarkdownParagraph.new(one_line + "\n")
73
+ ]
74
+ )
75
+ )
76
+ }
77
+ end
78
+ end
79
+
80
+ context "with empty true case case" do
81
+ let(:true_body) { "" }
82
+
83
+ it {
84
+ should parse(source).as(
85
+ conditional: {
86
+ predicate: {named_predicate: "pred1?"}
87
+ }
88
+ )
89
+ }
90
+
91
+ describe "transformed" do
92
+ subject(:transformed) {
93
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
94
+ }
95
+
96
+ it {
97
+ should eq(
98
+ Smartdown::Model::Element::Conditional.new(
99
+ Smartdown::Model::Predicate::Named.new("pred1?")
100
+ )
101
+ )
102
+ }
103
+ end
104
+ end
105
+ end
106
+
107
+ context "simple IF-ELSE" do
108
+ let(:source) { <<-SOURCE
109
+ $IF pred1?
110
+
111
+ #{true_body}
112
+
113
+ $ELSE
114
+
115
+ #{false_body}
116
+
117
+ $ENDIF
118
+ SOURCE
119
+ }
120
+
121
+ context "with one-line true case" do
122
+ let(:true_body) { "Text if true" }
123
+ let(:false_body) { "Text if false" }
124
+
125
+ it {
126
+ should parse(source).as(
127
+ conditional: {
128
+ predicate: {named_predicate: "pred1?"},
129
+ true_case: [{p: "#{true_body}\n"}],
130
+ false_case: [{p: "#{false_body}\n"}]
131
+ }
132
+ )
133
+ }
134
+
135
+ describe "transformed" do
136
+ subject(:transformed) {
137
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
138
+ }
139
+
140
+ it {
141
+ should eq(
142
+ Smartdown::Model::Element::Conditional.new(
143
+ Smartdown::Model::Predicate::Named.new("pred1?"),
144
+ [Smartdown::Model::Element::MarkdownParagraph.new(true_body + "\n")],
145
+ [Smartdown::Model::Element::MarkdownParagraph.new(false_body + "\n")]
146
+ )
147
+ )
148
+ }
149
+ end
150
+ end
151
+ end
152
+
153
+ end
154
+
@@ -4,28 +4,48 @@ require 'smartdown/parser/node_interpreter'
4
4
 
5
5
  describe Smartdown::Parser::Element::MultipleChoiceQuestion do
6
6
  subject(:parser) { described_class.new }
7
- let(:source) {
8
- [
9
- "* yes: Yes",
10
- "* no: No"
11
- ].join("\n")
12
- }
13
7
 
14
- it "parses" do
15
- should parse(source).as(
16
- multiple_choice: [
17
- {value: "yes", label: "Yes"},
18
- {value: "no", label: "No"}
19
- ]
20
- )
8
+ context "with question tag" do
9
+ let(:source) {
10
+ [
11
+ "[choice: yes_or_no]",
12
+ "* yes: Yes",
13
+ "* no: No"
14
+ ].join("\n")
15
+ }
16
+
17
+ it "parses" do
18
+ should parse(source).as(
19
+ multiple_choice: {
20
+ identifier: "yes_or_no",
21
+ options: [
22
+ {value: "yes", label: "Yes"},
23
+ {value: "no", label: "No"}
24
+ ]
25
+ }
26
+ )
27
+ end
28
+
29
+ describe "transformed" do
30
+ let(:node_name) { "my_node" }
31
+ subject(:transformed) {
32
+ Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
33
+ }
34
+
35
+ it { should eq(Smartdown::Model::Element::MultipleChoice.new("yes_or_no", {"yes"=>"Yes", "no"=>"No"})) }
36
+ end
21
37
  end
22
38
 
23
- describe "transformed" do
24
- let(:node_name) { "my_node" }
25
- subject(:transformed) {
26
- Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
39
+ context "without question tag" do
40
+ let(:source) {
41
+ [
42
+ "* yes: Yes",
43
+ "* no: No"
44
+ ].join("\n")
27
45
  }
28
46
 
29
- it { should eq(Smartdown::Model::Element::MultipleChoice.new(node_name, {"yes"=>"Yes", "no"=>"No"})) }
47
+ it "is not parsable" do
48
+ should_not parse(source)
49
+ end
30
50
  end
31
51
  end
@@ -60,6 +60,7 @@ SOURCE
60
60
  <<SOURCE
61
61
  # This is my title
62
62
 
63
+ [choice: my_question]
63
64
  * yes: Yes
64
65
  * no: No
65
66
  SOURCE
@@ -69,10 +70,13 @@ SOURCE
69
70
  should parse(source).as({
70
71
  body: [
71
72
  {h1: "This is my title"},
72
- {multiple_choice: [
73
- {value: "yes", label: "Yes"},
74
- {value: "no", label: "No"}
75
- ]}
73
+ {multiple_choice: {
74
+ identifier: "my_question",
75
+ options: [
76
+ {value: "yes", label: "Yes"},
77
+ {value: "no", label: "No"}
78
+ ]}
79
+ }
76
80
  ]
77
81
  })
78
82
  end
@@ -83,6 +87,7 @@ SOURCE
83
87
  <<SOURCE
84
88
  # This is my title
85
89
 
90
+ [choice: my_question]
86
91
  * yes: Yes
87
92
  * no: No
88
93
 
@@ -96,10 +101,13 @@ SOURCE
96
101
  should parse(source).as({
97
102
  body: [
98
103
  {h1: "This is my title"},
99
- {multiple_choice: [
100
- {value: "yes", label: "Yes"},
101
- {value: "no", label: "No"}
102
- ]},
104
+ {multiple_choice: {
105
+ identifier: "my_question",
106
+ options: [
107
+ {value: "yes", label: "Yes"},
108
+ {value: "no", label: "No"}
109
+ ]}
110
+ },
103
111
  {h1: "Next node rules"},
104
112
  {next_node_rules: [{rule: {predicate: {named_predicate: "pred1?"}, outcome: "outcome"}}]}
105
113
  ]
@@ -115,7 +123,7 @@ SOURCE
115
123
  let(:expected_elements) {
116
124
  [
117
125
  Smartdown::Model::Element::MarkdownHeading.new("This is my title"),
118
- Smartdown::Model::Element::MultipleChoice.new(node_name, "yes"=>"Yes", "no"=>"No"),
126
+ Smartdown::Model::Element::MultipleChoice.new("my_question", "yes"=>"Yes", "no"=>"No"),
119
127
  Smartdown::Model::Element::MarkdownHeading.new("Next node rules"),
120
128
  Smartdown::Model::NextNodeRules.new([
121
129
  Smartdown::Model::Rule.new(Smartdown::Model::Predicate::Named.new("pred1?"), "outcome")
@@ -130,4 +138,32 @@ SOURCE
130
138
  it { should eq(expected_node_model) }
131
139
  end
132
140
  end
141
+
142
+ describe "body with conditional" do
143
+ let(:source) {
144
+ <<SOURCE
145
+ $IF pred1?
146
+
147
+ Text when true
148
+
149
+ $ELSE
150
+
151
+ Text when false
152
+
153
+ $ENDIF
154
+ SOURCE
155
+ }
156
+
157
+ it {
158
+ should parse(source).as({
159
+ body: [
160
+ {conditional: {
161
+ predicate: {named_predicate: "pred1?"},
162
+ true_case: [{p: "Text when true\n"}],
163
+ false_case: [{p: "Text when false\n"}]
164
+ }}
165
+ ]
166
+ })
167
+ }
168
+ end
133
169
  end
@@ -4,6 +4,7 @@ require 'smartdown/model/element/markdown_heading'
4
4
  require 'smartdown/model/element/markdown_paragraph'
5
5
  require 'smartdown/model/element/start_button'
6
6
  require 'smartdown/model/element/multiple_choice'
7
+ require 'smartdown/model/element/conditional'
7
8
  require 'smartdown/model/next_node_rules'
8
9
  require 'smartdown/model/rule'
9
10
  require 'smartdown/model/predicate/named'
@@ -67,6 +68,34 @@ class ModelBuilder
67
68
  @rules.last
68
69
  end
69
70
 
71
+ def conditional(&block)
72
+ @predicate = nil
73
+ @true_case = nil
74
+ @false_case = nil
75
+ @elements ||= []
76
+ instance_eval(&block) if block_given?
77
+ @elements << Smartdown::Model::Element::Conditional.new(@predicate, @true_case, @false_case)
78
+ @elements.last
79
+ end
80
+
81
+ def true_case(&block)
82
+ @outer_elements = @elements
83
+ @elements = []
84
+ instance_eval(&block) if block_given?
85
+ @true_case = @elements
86
+ @elements = @outer_elements
87
+ @true_case
88
+ end
89
+
90
+ def false_case(&block)
91
+ @outer_elements = @elements
92
+ @elements = []
93
+ instance_eval(&block) if block_given?
94
+ @false_case = @elements
95
+ @elements = @outer_elements
96
+ @false_case
97
+ end
98
+
70
99
  def named_predicate(name)
71
100
  @predicate = Smartdown::Model::Predicate::Named.new(name)
72
101
  end
@@ -76,8 +105,12 @@ class ModelBuilder
76
105
  end
77
106
 
78
107
  module DSL
108
+ def model_builder
109
+ ModelBuilder.new
110
+ end
111
+
79
112
  def build_flow(name, &block)
80
- ModelBuilder.new.flow(name, &block)
113
+ model_builder.flow(name, &block)
81
114
  end
82
115
  end
83
116
  end
@@ -62,6 +62,32 @@ describe ModelBuilder do
62
62
  end
63
63
  end
64
64
 
65
+ describe "#conditional" do
66
+ let(:expected) {
67
+ Smartdown::Model::Element::Conditional.new(
68
+ Smartdown::Model::Predicate::Named.new("pred?"),
69
+ [Smartdown::Model::Element::MarkdownParagraph.new("True case")],
70
+ [Smartdown::Model::Element::MarkdownParagraph.new("False case")]
71
+ )
72
+ }
73
+
74
+ subject(:model) {
75
+ builder.conditional do
76
+ named_predicate "pred?"
77
+ true_case do
78
+ paragraph("True case")
79
+ end
80
+ false_case do
81
+ paragraph("False case")
82
+ end
83
+ end
84
+ }
85
+
86
+ it "builds a conditional" do
87
+ should eq(expected)
88
+ end
89
+ end
90
+
65
91
  describe "#rule" do
66
92
  let(:predicate) { Smartdown::Model::Predicate::Named.new("my_pred") }
67
93
 
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.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -91,6 +91,7 @@ files:
91
91
  - lib/smartdown/parser/directory_input.rb
92
92
  - lib/smartdown/parser/rules.rb
93
93
  - lib/smartdown/parser/node_parser.rb
94
+ - lib/smartdown/parser/element/conditional.rb
94
95
  - lib/smartdown/parser/element/markdown_paragraph.rb
95
96
  - lib/smartdown/parser/element/start_button.rb
96
97
  - lib/smartdown/parser/element/front_matter.rb
@@ -98,6 +99,7 @@ files:
98
99
  - lib/smartdown/parser/element/markdown_heading.rb
99
100
  - lib/smartdown/parser/flow_interpreter.rb
100
101
  - lib/smartdown/version.rb
102
+ - lib/smartdown/engine/node_presenter.rb
101
103
  - lib/smartdown/engine/state.rb
102
104
  - lib/smartdown/engine/transition.rb
103
105
  - lib/smartdown/engine/predicate_evaluator.rb
@@ -108,6 +110,7 @@ files:
108
110
  - lib/smartdown/model/predicate/equality.rb
109
111
  - lib/smartdown/model/node.rb
110
112
  - lib/smartdown/model/front_matter.rb
113
+ - lib/smartdown/model/element/conditional.rb
111
114
  - lib/smartdown/model/element/markdown_paragraph.rb
112
115
  - lib/smartdown/model/element/start_button.rb
113
116
  - lib/smartdown/model/element/markdown_heading.rb
@@ -126,6 +129,7 @@ files:
126
129
  - spec/parser/rules_spec.rb
127
130
  - spec/parser/predicates_spec.rb
128
131
  - spec/parser/element/multiple_choice_question_spec.rb
132
+ - spec/parser/element/conditional_spec.rb
129
133
  - spec/parser/element/start_button_parser_spec.rb
130
134
  - spec/parser/element/markdown_paragraph_spec.rb
131
135
  - spec/parser/element/markdown_heading_spec.rb
@@ -134,6 +138,7 @@ files:
134
138
  - spec/support/model_builder.rb
135
139
  - spec/spec_helper.rb
136
140
  - spec/engine/state_spec.rb
141
+ - spec/engine/node_presenter_spec.rb
137
142
  - spec/engine/predicate_evaluator_spec.rb
138
143
  - spec/engine/transition_spec.rb
139
144
  - spec/fixtures/directory_input/scenarios/s1.txt
@@ -166,7 +171,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
166
171
  version: '0'
167
172
  segments:
168
173
  - 0
169
- hash: -3653828789997533623
174
+ hash: 1408391630094207002
170
175
  required_rubygems_version: !ruby/object:Gem::Requirement
171
176
  none: false
172
177
  requirements:
@@ -175,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
180
  version: '0'
176
181
  segments:
177
182
  - 0
178
- hash: -3653828789997533623
183
+ hash: 1408391630094207002
179
184
  requirements: []
180
185
  rubyforge_project:
181
186
  rubygems_version: 1.8.23
@@ -191,6 +196,7 @@ test_files:
191
196
  - spec/parser/rules_spec.rb
192
197
  - spec/parser/predicates_spec.rb
193
198
  - spec/parser/element/multiple_choice_question_spec.rb
199
+ - spec/parser/element/conditional_spec.rb
194
200
  - spec/parser/element/start_button_parser_spec.rb
195
201
  - spec/parser/element/markdown_paragraph_spec.rb
196
202
  - spec/parser/element/markdown_heading_spec.rb
@@ -199,6 +205,7 @@ test_files:
199
205
  - spec/support/model_builder.rb
200
206
  - spec/spec_helper.rb
201
207
  - spec/engine/state_spec.rb
208
+ - spec/engine/node_presenter_spec.rb
202
209
  - spec/engine/predicate_evaluator_spec.rb
203
210
  - spec/engine/transition_spec.rb
204
211
  - spec/fixtures/directory_input/scenarios/s1.txt