smartdown 0.0.1
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/LICENSE.md +21 -0
- data/README.md +204 -0
- data/bin/smartdown +41 -0
- data/lib/smartdown/engine/errors.rb +5 -0
- data/lib/smartdown/engine/predicate_evaluator.rb +23 -0
- data/lib/smartdown/engine/state.rb +63 -0
- data/lib/smartdown/engine/transition.rb +65 -0
- data/lib/smartdown/engine.rb +33 -0
- data/lib/smartdown/model/element/markdown_heading.rb +7 -0
- data/lib/smartdown/model/element/markdown_paragraph.rb +7 -0
- data/lib/smartdown/model/element/multiple_choice.rb +7 -0
- data/lib/smartdown/model/element/start_button.rb +7 -0
- data/lib/smartdown/model/flow.rb +24 -0
- data/lib/smartdown/model/front_matter.rb +37 -0
- data/lib/smartdown/model/nested_rule.rb +5 -0
- data/lib/smartdown/model/next_node_rules.rb +5 -0
- data/lib/smartdown/model/node.rb +47 -0
- data/lib/smartdown/model/predicate/equality.rb +7 -0
- data/lib/smartdown/model/predicate/named.rb +7 -0
- data/lib/smartdown/model/predicate/set_membership.rb +7 -0
- data/lib/smartdown/model/rule.rb +5 -0
- data/lib/smartdown/parser/base.rb +35 -0
- data/lib/smartdown/parser/directory_input.rb +61 -0
- data/lib/smartdown/parser/element/front_matter.rb +17 -0
- data/lib/smartdown/parser/element/markdown_heading.rb +14 -0
- data/lib/smartdown/parser/element/markdown_paragraph.rb +19 -0
- data/lib/smartdown/parser/element/multiple_choice_question.rb +24 -0
- data/lib/smartdown/parser/element/start_button.rb +15 -0
- data/lib/smartdown/parser/flow_interpreter.rb +50 -0
- data/lib/smartdown/parser/node_interpreter.rb +29 -0
- data/lib/smartdown/parser/node_parser.rb +37 -0
- data/lib/smartdown/parser/node_transform.rb +83 -0
- data/lib/smartdown/parser/predicates.rb +36 -0
- data/lib/smartdown/parser/rules.rb +51 -0
- data/lib/smartdown/version.rb +3 -0
- data/lib/smartdown.rb +9 -0
- data/spec/acceptance/parsing_spec.rb +109 -0
- data/spec/acceptance/smartdown_cli_spec.rb +16 -0
- data/spec/engine/predicate_evaluator_spec.rb +98 -0
- data/spec/engine/state_spec.rb +106 -0
- data/spec/engine/transition_spec.rb +150 -0
- data/spec/engine_spec.rb +79 -0
- data/spec/fixtures/acceptance/cover-sheet/cover-sheet.txt +14 -0
- data/spec/fixtures/acceptance/one-question/one-question.txt +3 -0
- data/spec/fixtures/acceptance/one-question/questions/q1.txt +9 -0
- data/spec/fixtures/acceptance/question-and-outcome/outcomes/o1.txt +3 -0
- data/spec/fixtures/acceptance/question-and-outcome/question-and-outcome.txt +3 -0
- data/spec/fixtures/acceptance/question-and-outcome/questions/q1.txt +9 -0
- data/spec/fixtures/directory_input/cover-sheet.txt +1 -0
- data/spec/fixtures/directory_input/outcomes/o1.txt +1 -0
- data/spec/fixtures/directory_input/questions/q1.txt +1 -0
- data/spec/fixtures/directory_input/scenarios/s1.txt +1 -0
- data/spec/fixtures/example.sd +17 -0
- data/spec/model/flow_spec.rb +42 -0
- data/spec/model/node_spec.rb +32 -0
- data/spec/parser/base_spec.rb +49 -0
- data/spec/parser/directory_input_spec.rb +56 -0
- data/spec/parser/element/front_matter_spec.rb +21 -0
- data/spec/parser/element/markdown_heading_spec.rb +24 -0
- data/spec/parser/element/markdown_paragraph_spec.rb +28 -0
- data/spec/parser/element/multiple_choice_question_spec.rb +31 -0
- data/spec/parser/element/start_button_parser_spec.rb +30 -0
- data/spec/parser/integration/cover_sheet_spec.rb +30 -0
- data/spec/parser/node_parser_spec.rb +133 -0
- data/spec/parser/predicates_spec.rb +65 -0
- data/spec/parser/rules_spec.rb +244 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/model_builder.rb +83 -0
- data/spec/support_specs/model_builder_spec.rb +90 -0
- metadata +218 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Parser
|
5
|
+
class DirectoryInput
|
6
|
+
def initialize(coversheet_path)
|
7
|
+
@coversheet_path = Pathname.new(coversheet_path.to_s)
|
8
|
+
end
|
9
|
+
|
10
|
+
def coversheet
|
11
|
+
InputFile.new(@coversheet_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def questions
|
15
|
+
read_dir("questions")
|
16
|
+
end
|
17
|
+
|
18
|
+
def outcomes
|
19
|
+
read_dir("outcomes")
|
20
|
+
end
|
21
|
+
|
22
|
+
def scenarios
|
23
|
+
read_dir("scenarios")
|
24
|
+
end
|
25
|
+
|
26
|
+
def filenames_hash
|
27
|
+
{
|
28
|
+
coversheet: coversheet.to_s,
|
29
|
+
questions: questions.map(&:to_s),
|
30
|
+
outcomes: outcomes.map(&:to_s),
|
31
|
+
scenarios: scenarios.map(&:to_s)
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def read_dir(dir)
|
37
|
+
Dir[@coversheet_path.dirname + dir + "*.txt"].map do |filename|
|
38
|
+
InputFile.new(filename)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class InputFile
|
44
|
+
def initialize(path)
|
45
|
+
@path = Pathname.new(path.to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
def name
|
49
|
+
@path.basename.to_s.split(".").first
|
50
|
+
end
|
51
|
+
|
52
|
+
def read
|
53
|
+
File.read(@path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
@path.to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'smartdown/parser/base'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Parser
|
5
|
+
module Element
|
6
|
+
class FrontMatter < Base
|
7
|
+
rule(:front_matter_line) {
|
8
|
+
identifier.as(:name) >> str(":") >> ws >> whitespace_terminated_string.as(:value) >> line_ending
|
9
|
+
}
|
10
|
+
rule(:front_matter) {
|
11
|
+
front_matter_line.repeat(1).as(:front_matter)
|
12
|
+
}
|
13
|
+
root(:front_matter)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'smartdown/parser/base'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Parser
|
5
|
+
module Element
|
6
|
+
class MarkdownHeading < Base
|
7
|
+
rule(:markdown_heading) {
|
8
|
+
str('# ') >> (whitespace_terminated_string).as(:h1) >> optional_space >> line_ending
|
9
|
+
}
|
10
|
+
root(:markdown_heading)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'smartdown/parser/base'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Parser
|
5
|
+
module Element
|
6
|
+
class MarkdownParagraph < Base
|
7
|
+
rule(:markdown_line) {
|
8
|
+
optional_space >> whitespace_terminated_string >> optional_space
|
9
|
+
}
|
10
|
+
|
11
|
+
rule(:markdown_paragraph) {
|
12
|
+
(markdown_line >> line_ending).repeat(1).as(:p)
|
13
|
+
}
|
14
|
+
|
15
|
+
root(:markdown_paragraph)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'smartdown/parser/base'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Parser
|
5
|
+
module Element
|
6
|
+
class MultipleChoiceQuestion < Base
|
7
|
+
rule(:option_definition_line) {
|
8
|
+
bullet >>
|
9
|
+
optional_space >>
|
10
|
+
identifier.as(:value) >>
|
11
|
+
str(":") >>
|
12
|
+
optional_space >>
|
13
|
+
whitespace_terminated_string.as(:label) >>
|
14
|
+
optional_space
|
15
|
+
}
|
16
|
+
|
17
|
+
rule(:multiple_choice_question) {
|
18
|
+
(option_definition_line >> line_ending).repeat(1).as(:multiple_choice)
|
19
|
+
}
|
20
|
+
root(:multiple_choice_question)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'smartdown/parser/base'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Parser
|
5
|
+
module Element
|
6
|
+
class StartButton < Base
|
7
|
+
rule(:start_button) {
|
8
|
+
str('[start: ') >> question_identifier.as(:start_button) >> str(']') >> line_ending
|
9
|
+
}
|
10
|
+
|
11
|
+
root(:start_button)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'smartdown/model/flow'
|
2
|
+
require 'smartdown/parser/node_interpreter'
|
3
|
+
|
4
|
+
module Smartdown
|
5
|
+
module Parser
|
6
|
+
class ParseError < StandardError
|
7
|
+
attr_reader :filename, :parse_error
|
8
|
+
|
9
|
+
def initialize(filename, parse_error)
|
10
|
+
@filename = filename
|
11
|
+
@parse_error = parse_error
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s(full = true)
|
15
|
+
"Parse error in '#{filename}':\n\n" + @parse_error.cause.ascii_tree
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class FlowInterpreter
|
20
|
+
attr_reader :flow_input
|
21
|
+
|
22
|
+
def initialize(flow_input)
|
23
|
+
@flow_input = flow_input
|
24
|
+
end
|
25
|
+
|
26
|
+
def interpret
|
27
|
+
Smartdown::Model::Flow.new(coversheet.name, [coversheet] + questions + outcomes)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def coversheet
|
32
|
+
interpret_node(flow_input.coversheet)
|
33
|
+
end
|
34
|
+
|
35
|
+
def questions
|
36
|
+
flow_input.questions.map { |i| interpret_node(i) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def outcomes
|
40
|
+
flow_input.outcomes.map { |i| interpret_node(i) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def interpret_node(input_data)
|
44
|
+
Smartdown::Parser::NodeInterpreter.new(input_data.name, input_data.read).interpret
|
45
|
+
rescue Parslet::ParseFailed => error
|
46
|
+
raise ParseError.new(input_data.to_s, error)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'smartdown/model/flow'
|
3
|
+
require 'smartdown/model/node'
|
4
|
+
require 'smartdown/parser/node_parser'
|
5
|
+
require 'smartdown/parser/node_transform'
|
6
|
+
|
7
|
+
module Smartdown
|
8
|
+
module Parser
|
9
|
+
class NodeInterpreter
|
10
|
+
attr_reader :name, :source
|
11
|
+
|
12
|
+
def initialize(name, source, options = {})
|
13
|
+
@name = name
|
14
|
+
@source = source
|
15
|
+
@parser = options.fetch(:parser, Smartdown::Parser::NodeParser.new)
|
16
|
+
@transform = options.fetch(:transform, Smartdown::Parser::NodeTransform.new)
|
17
|
+
end
|
18
|
+
|
19
|
+
def interpret
|
20
|
+
transform.apply(parser.parse(source),
|
21
|
+
node_name: name
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
attr_reader :parser, :transform
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'smartdown/parser/base'
|
2
|
+
require 'smartdown/parser/rules'
|
3
|
+
require 'smartdown/parser/element/front_matter'
|
4
|
+
require 'smartdown/parser/element/start_button'
|
5
|
+
require 'smartdown/parser/element/multiple_choice_question'
|
6
|
+
require 'smartdown/parser/element/markdown_heading'
|
7
|
+
require 'smartdown/parser/element/markdown_paragraph'
|
8
|
+
|
9
|
+
module Smartdown
|
10
|
+
module Parser
|
11
|
+
class NodeParser < Base
|
12
|
+
rule(:markdown_block) {
|
13
|
+
Element::MarkdownHeading.new |
|
14
|
+
Element::MultipleChoiceQuestion.new |
|
15
|
+
Rules.new |
|
16
|
+
Element::StartButton.new |
|
17
|
+
Element::MarkdownParagraph.new
|
18
|
+
}
|
19
|
+
|
20
|
+
rule(:markdown_blocks) {
|
21
|
+
markdown_block >> (newline.repeat(1) >> markdown_block).repeat
|
22
|
+
}
|
23
|
+
|
24
|
+
rule(:body) {
|
25
|
+
markdown_blocks.as(:body)
|
26
|
+
}
|
27
|
+
|
28
|
+
rule(:flow) {
|
29
|
+
Element::FrontMatter.new >> newline.repeat(1) >> body |
|
30
|
+
Element::FrontMatter.new |
|
31
|
+
ws >> body
|
32
|
+
}
|
33
|
+
|
34
|
+
root(:flow)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'parslet/transform'
|
2
|
+
require 'smartdown/model/node'
|
3
|
+
require 'smartdown/model/front_matter'
|
4
|
+
require 'smartdown/model/rule'
|
5
|
+
require 'smartdown/model/nested_rule'
|
6
|
+
require 'smartdown/model/next_node_rules'
|
7
|
+
require 'smartdown/model/element/multiple_choice'
|
8
|
+
require 'smartdown/model/element/start_button'
|
9
|
+
require 'smartdown/model/element/markdown_heading'
|
10
|
+
require 'smartdown/model/element/markdown_paragraph'
|
11
|
+
require 'smartdown/model/predicate/equality'
|
12
|
+
require 'smartdown/model/predicate/set_membership'
|
13
|
+
require 'smartdown/model/predicate/named'
|
14
|
+
|
15
|
+
module Smartdown
|
16
|
+
module Parser
|
17
|
+
class NodeTransform < Parslet::Transform
|
18
|
+
rule(body: subtree(:body)) {
|
19
|
+
Smartdown::Model::Node.new(
|
20
|
+
node_name, body, Smartdown::Model::FrontMatter.new({})
|
21
|
+
)
|
22
|
+
}
|
23
|
+
|
24
|
+
rule(h1: simple(:content)) {
|
25
|
+
Smartdown::Model::Element::MarkdownHeading.new(content)
|
26
|
+
}
|
27
|
+
|
28
|
+
rule(p: simple(:content)) {
|
29
|
+
Smartdown::Model::Element::MarkdownParagraph.new(content)
|
30
|
+
}
|
31
|
+
|
32
|
+
rule(:start_button => simple(:start_node)) {
|
33
|
+
Smartdown::Model::Element::StartButton.new(start_node)
|
34
|
+
}
|
35
|
+
|
36
|
+
rule(:front_matter => subtree(:attrs), body: subtree(:body)) {
|
37
|
+
Smartdown::Model::Node.new(
|
38
|
+
node_name, body, Smartdown::Model::FrontMatter.new(Hash[attrs])
|
39
|
+
)
|
40
|
+
}
|
41
|
+
rule(:front_matter => subtree(:attrs)) {
|
42
|
+
[Smartdown::Model::FrontMatter.new(Hash[attrs])]
|
43
|
+
}
|
44
|
+
rule(:name => simple(:name), :value => simple(:value)) {
|
45
|
+
[name.to_s, value.to_s]
|
46
|
+
}
|
47
|
+
|
48
|
+
rule(:value => simple(:value), :label => simple(:label)) {
|
49
|
+
[value.to_s, label.to_s]
|
50
|
+
}
|
51
|
+
|
52
|
+
rule(:multiple_choice => subtree(:choices)) {
|
53
|
+
Smartdown::Model::Element::MultipleChoice.new(
|
54
|
+
node_name, Hash[choices]
|
55
|
+
)
|
56
|
+
}
|
57
|
+
|
58
|
+
rule(:equality_predicate => { varname: simple(:varname), expected_value: simple(:expected_value) }) {
|
59
|
+
Smartdown::Model::Predicate::Equality.new(varname, expected_value)
|
60
|
+
}
|
61
|
+
|
62
|
+
rule(:set_value => simple(:value)) { value }
|
63
|
+
rule(:set_membership_predicate => { varname: simple(:varname), values: subtree(:values) }) {
|
64
|
+
Smartdown::Model::Predicate::SetMembership.new(varname, values)
|
65
|
+
}
|
66
|
+
|
67
|
+
rule(:named_predicate => simple(:name) ) {
|
68
|
+
Smartdown::Model::Predicate::Named.new(name)
|
69
|
+
}
|
70
|
+
|
71
|
+
rule(:rule => {predicate: subtree(:predicate), outcome: simple(:outcome_name) } ) {
|
72
|
+
Smartdown::Model::Rule.new(predicate, outcome_name)
|
73
|
+
}
|
74
|
+
rule(:nested_rule => {predicate: subtree(:predicate), child_rules: subtree(:child_rules) } ) {
|
75
|
+
Smartdown::Model::NestedRule.new(predicate, child_rules)
|
76
|
+
}
|
77
|
+
rule(:next_node_rules => subtree(:rules)) {
|
78
|
+
Smartdown::Model::NextNodeRules.new(rules)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'smartdown/parser/base'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Parser
|
5
|
+
class Predicates < Base
|
6
|
+
rule(:equality_predicate) {
|
7
|
+
identifier.as(:varname) >> some_space >>
|
8
|
+
str('is') >> some_space >>
|
9
|
+
str("'") >> match("[^']").repeat.as(:expected_value) >> str("'")
|
10
|
+
}
|
11
|
+
|
12
|
+
rule(:set_value) {
|
13
|
+
match('[^\s}]').repeat(1).as(:set_value)
|
14
|
+
}
|
15
|
+
|
16
|
+
rule(:set_values) {
|
17
|
+
(set_value >> some_space).repeat >> set_value
|
18
|
+
}
|
19
|
+
|
20
|
+
rule(:set_membership_predicate) {
|
21
|
+
identifier.as(:varname) >> some_space >>
|
22
|
+
str('in') >> some_space >>
|
23
|
+
str("{") >> optional_space >> set_values.maybe.as(:values) >> optional_space >> str("}")
|
24
|
+
}
|
25
|
+
|
26
|
+
rule(:named_predicate) {
|
27
|
+
question_identifier.as(:named_predicate)
|
28
|
+
}
|
29
|
+
rule(:predicates) {
|
30
|
+
equality_predicate.as(:equality_predicate) | set_membership_predicate.as(:set_membership_predicate) | named_predicate
|
31
|
+
}
|
32
|
+
|
33
|
+
root(:predicates)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'smartdown/parser/base'
|
2
|
+
require 'smartdown/parser/predicates'
|
3
|
+
|
4
|
+
module Smartdown
|
5
|
+
module Parser
|
6
|
+
class Rules < Base
|
7
|
+
def indent(depth)
|
8
|
+
str(' ').repeat(depth).capture(:indent)
|
9
|
+
end
|
10
|
+
|
11
|
+
def conditional_line(depth)
|
12
|
+
indent(depth) >> bullet >> optional_space >> Smartdown::Parser::Predicates.new.as(:predicate) >> optional_space >> line_ending
|
13
|
+
end
|
14
|
+
|
15
|
+
def children
|
16
|
+
dynamic do |s,c|
|
17
|
+
current_indent = c.captures[:indent].size
|
18
|
+
condition_with_children_or_rule(current_indent + 1).repeat(1)
|
19
|
+
end.as(:child_rules)
|
20
|
+
end
|
21
|
+
|
22
|
+
def condition_with_children(depth)
|
23
|
+
conditional_line(depth) >> children
|
24
|
+
end
|
25
|
+
|
26
|
+
def rule(depth)
|
27
|
+
(
|
28
|
+
indent(depth) >> bullet >> optional_space >>
|
29
|
+
Smartdown::Parser::Predicates.new.as(:predicate) >> optional_space >>
|
30
|
+
str("=>") >> optional_space >>
|
31
|
+
question_identifier.as(:outcome) >>
|
32
|
+
optional_space >> line_ending
|
33
|
+
).as(:rule)
|
34
|
+
end
|
35
|
+
|
36
|
+
def condition_with_children_or_rule(depth)
|
37
|
+
condition_with_children(depth).as(:nested_rule) | rule(depth)
|
38
|
+
end
|
39
|
+
|
40
|
+
rule(:one_top_level_rule) {
|
41
|
+
condition_with_children_or_rule(0)
|
42
|
+
}
|
43
|
+
|
44
|
+
rule(:rules) {
|
45
|
+
one_top_level_rule.repeat(1).as(:next_node_rules)
|
46
|
+
}
|
47
|
+
|
48
|
+
root(:rules)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/smartdown.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'smartdown/parser/flow_interpreter'
|
2
|
+
require 'smartdown/parser/directory_input'
|
3
|
+
|
4
|
+
module Smartdown
|
5
|
+
def self.parse(coversheet_file)
|
6
|
+
input = Smartdown::Parser::DirectoryInput.new(coversheet_file)
|
7
|
+
Smartdown::Parser::FlowInterpreter.new(input).interpret
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'smartdown.rb'
|
2
|
+
|
3
|
+
describe "Smartdown.parse" do
|
4
|
+
|
5
|
+
def fixture(name)
|
6
|
+
File.dirname(__FILE__) + "/../fixtures/acceptance/#{name}/#{name}.txt"
|
7
|
+
end
|
8
|
+
|
9
|
+
context "flow with only a cover-sheet" do
|
10
|
+
subject(:flow) { Smartdown.parse(fixture("cover-sheet")) }
|
11
|
+
|
12
|
+
it "builds a Flow model" do
|
13
|
+
expect(flow).to be_a(Smartdown::Model::Flow)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "derives the name from the filename" do
|
17
|
+
expect(flow.name).to eq("cover-sheet")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "has a single coversheet node" do
|
21
|
+
expect(flow.nodes).to match([instance_of(Smartdown::Model::Node)])
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#coversheet" do
|
25
|
+
subject(:coversheet) { flow.coversheet }
|
26
|
+
|
27
|
+
it "returns the coversheet node" do
|
28
|
+
expect(flow.coversheet).to be_a(Smartdown::Model::Node)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "has front matter" do
|
32
|
+
expect(coversheet.front_matter).to be_a(Smartdown::Model::FrontMatter)
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "front matter" do
|
36
|
+
it "has satisfies_need" do
|
37
|
+
expect(coversheet.front_matter.satisfies_need).to eq("1234")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "has meta_description" do
|
41
|
+
expect(coversheet.front_matter.meta_description).to eq("Blah blah")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "has a title extracted from the first markdown H1" do
|
46
|
+
expect(coversheet.title).to eq("My coversheet")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "has a body" do
|
50
|
+
expect(coversheet.body).to eq(<<-EXPECTED)
|
51
|
+
This is the body markdown.
|
52
|
+
|
53
|
+
It has many paragraphs
|
54
|
+
of text.
|
55
|
+
|
56
|
+
* it
|
57
|
+
* can
|
58
|
+
* have
|
59
|
+
* lists
|
60
|
+
EXPECTED
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "flow with a cover-sheet and a question" do
|
66
|
+
subject(:flow) { Smartdown.parse(fixture("one-question")) }
|
67
|
+
|
68
|
+
it "has two nodes" do
|
69
|
+
expect(flow.nodes.size).to eq(2)
|
70
|
+
expect(flow.nodes[0]).to eq(flow.coversheet)
|
71
|
+
expect(flow.nodes[1]).to be_a(Smartdown::Model::Node)
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "the question node" do
|
75
|
+
subject(:question_node) { flow.nodes[1] }
|
76
|
+
|
77
|
+
it "has a title" do
|
78
|
+
expect(question_node.title).to eq("Question one")
|
79
|
+
end
|
80
|
+
|
81
|
+
it "has two body paras" do
|
82
|
+
expect(question_node.body).to eq(<<-EXPECTED)
|
83
|
+
Body text line 1.
|
84
|
+
|
85
|
+
Body text
|
86
|
+
para 2.
|
87
|
+
EXPECTED
|
88
|
+
end
|
89
|
+
|
90
|
+
it "has a multiple choice question" do
|
91
|
+
expect(question_node.questions).to match([instance_of(Smartdown::Model::Element::MultipleChoice)])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "flow coversheet, question and outcome" do
|
97
|
+
subject(:flow) { Smartdown.parse(fixture("question-and-outcome")) }
|
98
|
+
|
99
|
+
it "has three nodes" do
|
100
|
+
expect(flow.nodes.size).to eq(3)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "has node names" do
|
104
|
+
expect(flow.nodes.map(&:name)).to eq(["question-and-outcome", "q1", "o1"])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
describe "bin/smartdown" do
|
2
|
+
subject(:the_executable) { "bin/smartdown" }
|
3
|
+
|
4
|
+
it "is executable" do
|
5
|
+
expect(File.executable?(the_executable)).to eq(true)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "invocation with no arguments" do
|
9
|
+
subject(:output) { `#{the_executable} 2>&1` }
|
10
|
+
|
11
|
+
it "reports usage" do
|
12
|
+
expect(output).to match(/usage/i)
|
13
|
+
expect($?.exitstatus).to eq(1)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'smartdown/engine/predicate_evaluator'
|
2
|
+
require 'smartdown/model/predicate/equality'
|
3
|
+
require 'smartdown/model/predicate/set_membership'
|
4
|
+
require 'smartdown/model/predicate/named'
|
5
|
+
require 'smartdown/engine/state'
|
6
|
+
|
7
|
+
describe Smartdown::Engine::PredicateEvaluator do
|
8
|
+
subject(:evalutator) { described_class.new }
|
9
|
+
|
10
|
+
context "equality predicate" do
|
11
|
+
let(:predicate) { Smartdown::Model::Predicate::Equality.new("my_var", "some value") }
|
12
|
+
|
13
|
+
describe "#evaluate" do
|
14
|
+
context "state missing expected variable" do
|
15
|
+
let(:state) { Smartdown::Engine::State.new(current_node: "n") }
|
16
|
+
|
17
|
+
it "raises an UndefinedValue error" do
|
18
|
+
expect { evalutator.evaluate(predicate, state) }.to raise_error(Smartdown::Engine::UndefinedValue)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "state has expected variable with same value" do
|
23
|
+
let(:state) { Smartdown::Engine::State.new(current_node: "n", my_var: "some value") }
|
24
|
+
|
25
|
+
it "evalutes to true" do
|
26
|
+
expect(evalutator.evaluate(predicate, state)).to eq(true)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "state has expected variable with a different value" do
|
31
|
+
let(:state) { Smartdown::Engine::State.new(current_node: "n", my_var: "some other value") }
|
32
|
+
|
33
|
+
it "evalutes to false" do
|
34
|
+
expect(evalutator.evaluate(predicate, state)).to eq(false)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "set membership predicate" do
|
41
|
+
let(:expected_values) { ["v1", "v2", "v3"] }
|
42
|
+
let(:varname) { "my_var" }
|
43
|
+
let(:predicate) { Smartdown::Model::Predicate::SetMembership.new(varname, expected_values) }
|
44
|
+
|
45
|
+
describe "#evaluate" do
|
46
|
+
context "state missing expected variable" do
|
47
|
+
let(:state) { Smartdown::Engine::State.new(current_node: "n") }
|
48
|
+
|
49
|
+
it "raises an UndefinedValue error" do
|
50
|
+
expect { evalutator.evaluate(predicate, state) }.to raise_error(Smartdown::Engine::UndefinedValue)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "state has expected variable with one of the expected values" do
|
55
|
+
it "evalutes to true" do
|
56
|
+
expected_values.each do |value|
|
57
|
+
state = Smartdown::Engine::State.new(current_node: "n", varname => value)
|
58
|
+
expect(evalutator.evaluate(predicate, state)).to eq(true)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "state has expected variable with a value not in the list of expected values" do
|
64
|
+
let(:state) { Smartdown::Engine::State.new(current_node: "n", varname => "some other value") }
|
65
|
+
|
66
|
+
it "evalutes to false" do
|
67
|
+
expect(evalutator.evaluate(predicate, state)).to eq(false)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "named predicate" do
|
74
|
+
let(:predicate_name) { "my_pred?" }
|
75
|
+
let(:predicate) { Smartdown::Model::Predicate::Named.new(predicate_name) }
|
76
|
+
|
77
|
+
describe "#evaluate" do
|
78
|
+
context "state missing predicate definition" do
|
79
|
+
let(:state) { Smartdown::Engine::State.new(current_node: "n") }
|
80
|
+
|
81
|
+
it "raises an UndefinedValue error" do
|
82
|
+
expect { evalutator.evaluate(predicate, state) }.to raise_error(Smartdown::Engine::UndefinedValue)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "state has predicate definition" do
|
87
|
+
let(:state) {
|
88
|
+
Smartdown::Engine::State.new("current_node" => "n", "my_pred?" => true )
|
89
|
+
}
|
90
|
+
|
91
|
+
it "fetches the predicate value from the state" do
|
92
|
+
expect(evalutator.evaluate(predicate, state)).to eq(true)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|