smartdown 0.0.2 → 0.0.3

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