smartdown 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|