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.
Files changed (70) hide show
  1. data/LICENSE.md +21 -0
  2. data/README.md +204 -0
  3. data/bin/smartdown +41 -0
  4. data/lib/smartdown/engine/errors.rb +5 -0
  5. data/lib/smartdown/engine/predicate_evaluator.rb +23 -0
  6. data/lib/smartdown/engine/state.rb +63 -0
  7. data/lib/smartdown/engine/transition.rb +65 -0
  8. data/lib/smartdown/engine.rb +33 -0
  9. data/lib/smartdown/model/element/markdown_heading.rb +7 -0
  10. data/lib/smartdown/model/element/markdown_paragraph.rb +7 -0
  11. data/lib/smartdown/model/element/multiple_choice.rb +7 -0
  12. data/lib/smartdown/model/element/start_button.rb +7 -0
  13. data/lib/smartdown/model/flow.rb +24 -0
  14. data/lib/smartdown/model/front_matter.rb +37 -0
  15. data/lib/smartdown/model/nested_rule.rb +5 -0
  16. data/lib/smartdown/model/next_node_rules.rb +5 -0
  17. data/lib/smartdown/model/node.rb +47 -0
  18. data/lib/smartdown/model/predicate/equality.rb +7 -0
  19. data/lib/smartdown/model/predicate/named.rb +7 -0
  20. data/lib/smartdown/model/predicate/set_membership.rb +7 -0
  21. data/lib/smartdown/model/rule.rb +5 -0
  22. data/lib/smartdown/parser/base.rb +35 -0
  23. data/lib/smartdown/parser/directory_input.rb +61 -0
  24. data/lib/smartdown/parser/element/front_matter.rb +17 -0
  25. data/lib/smartdown/parser/element/markdown_heading.rb +14 -0
  26. data/lib/smartdown/parser/element/markdown_paragraph.rb +19 -0
  27. data/lib/smartdown/parser/element/multiple_choice_question.rb +24 -0
  28. data/lib/smartdown/parser/element/start_button.rb +15 -0
  29. data/lib/smartdown/parser/flow_interpreter.rb +50 -0
  30. data/lib/smartdown/parser/node_interpreter.rb +29 -0
  31. data/lib/smartdown/parser/node_parser.rb +37 -0
  32. data/lib/smartdown/parser/node_transform.rb +83 -0
  33. data/lib/smartdown/parser/predicates.rb +36 -0
  34. data/lib/smartdown/parser/rules.rb +51 -0
  35. data/lib/smartdown/version.rb +3 -0
  36. data/lib/smartdown.rb +9 -0
  37. data/spec/acceptance/parsing_spec.rb +109 -0
  38. data/spec/acceptance/smartdown_cli_spec.rb +16 -0
  39. data/spec/engine/predicate_evaluator_spec.rb +98 -0
  40. data/spec/engine/state_spec.rb +106 -0
  41. data/spec/engine/transition_spec.rb +150 -0
  42. data/spec/engine_spec.rb +79 -0
  43. data/spec/fixtures/acceptance/cover-sheet/cover-sheet.txt +14 -0
  44. data/spec/fixtures/acceptance/one-question/one-question.txt +3 -0
  45. data/spec/fixtures/acceptance/one-question/questions/q1.txt +9 -0
  46. data/spec/fixtures/acceptance/question-and-outcome/outcomes/o1.txt +3 -0
  47. data/spec/fixtures/acceptance/question-and-outcome/question-and-outcome.txt +3 -0
  48. data/spec/fixtures/acceptance/question-and-outcome/questions/q1.txt +9 -0
  49. data/spec/fixtures/directory_input/cover-sheet.txt +1 -0
  50. data/spec/fixtures/directory_input/outcomes/o1.txt +1 -0
  51. data/spec/fixtures/directory_input/questions/q1.txt +1 -0
  52. data/spec/fixtures/directory_input/scenarios/s1.txt +1 -0
  53. data/spec/fixtures/example.sd +17 -0
  54. data/spec/model/flow_spec.rb +42 -0
  55. data/spec/model/node_spec.rb +32 -0
  56. data/spec/parser/base_spec.rb +49 -0
  57. data/spec/parser/directory_input_spec.rb +56 -0
  58. data/spec/parser/element/front_matter_spec.rb +21 -0
  59. data/spec/parser/element/markdown_heading_spec.rb +24 -0
  60. data/spec/parser/element/markdown_paragraph_spec.rb +28 -0
  61. data/spec/parser/element/multiple_choice_question_spec.rb +31 -0
  62. data/spec/parser/element/start_button_parser_spec.rb +30 -0
  63. data/spec/parser/integration/cover_sheet_spec.rb +30 -0
  64. data/spec/parser/node_parser_spec.rb +133 -0
  65. data/spec/parser/predicates_spec.rb +65 -0
  66. data/spec/parser/rules_spec.rb +244 -0
  67. data/spec/spec_helper.rb +27 -0
  68. data/spec/support/model_builder.rb +83 -0
  69. data/spec/support_specs/model_builder_spec.rb +90 -0
  70. metadata +218 -0
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (C) 2014 HM Government (Government Digital Service)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # Smartdown
2
+
3
+ Smartdown is an [external
4
+ DSL](http://www.martinfowler.com/bliki/DomainSpecificLanguage.html) for
5
+ representing a series of questions and logical rules which determine the order
6
+ in which the questions are asked, according to user input.
7
+
8
+ The language is designed to look like
9
+ [Markdown](http://daringfireball.net/projects/markdown/), with some extensions
10
+ to allow expression of logical rules, questions and conditional blocks of
11
+ text.
12
+
13
+ ## Overview
14
+
15
+ A single smartdown flow has a cover sheet, a set of questions, a set of
16
+ outcomes and a set of test scenarios. Cover sheets, questions and outcomes are
17
+ all types of node. A node represents a single user interaction (normally a web
18
+ page, but in other media may be presented differently).
19
+
20
+ Each question and outcome is held in a separate file. The name of the files
21
+ are used to identify each question. Here's an example of the check-uk-visa
22
+ flow:
23
+
24
+ ```
25
+ -- check-uk-visa
26
+ |-- outcomes
27
+ | |-- general_y.txt
28
+ | |-- joining_family_m.txt
29
+ | |-- joining_family_y.txt
30
+ | |-- marriage.txt
31
+ | |-- medical_n.txt
32
+ | |-- medical_y.txt
33
+ | `-- ...
34
+ |-- scenarios
35
+ | |-- 1.test
36
+ | |-- 2.test
37
+ | `-- 3.test
38
+ |-- questions
39
+ | |-- planning_to_leave_airport.txt
40
+ | |-- purpose_of_visit.txt
41
+ | |-- staying_for_how_long.txt
42
+ | |-- what_passport_do_you_have.txt
43
+ `-- check-uk-visa.txt
44
+ ```
45
+
46
+ ## General node file syntax
47
+
48
+ Each file has three parts: front-matter, a model definition, rules/logic. Only the model definition is required.
49
+
50
+ * **front-matter** defines metadata in the form `property: value`
51
+ * the **model definition** is a markdown-like block which defines a flow, question or outcome.
52
+ * **rules/logic** defines 'next node' transition rules or other logic/predicate definitions
53
+
54
+ ## Cover sheet node
55
+
56
+ The cover sheet starts the flow off, its filename should match the flow name, e.g. 'check-uk-visa.txt'.
57
+
58
+ It has initial 'front matter' which defines metadata for the flow, including
59
+ the first question. It then defines the copy for the cover sheet in markdown
60
+ format. The h1 title is compulsory and used as the title for the smart answer.
61
+
62
+ ```
63
+ meta_description: You may need a visa to come to the UK to visit, study or work.
64
+ satisfies_need: 100982
65
+ start_with: what_passport_do_you_have
66
+
67
+ # Check if you need a UK visa
68
+
69
+ You may need a visa to come to the UK to visit, study or work.
70
+ ```
71
+
72
+ ## Question nodes
73
+
74
+ A question model definition has optional front matter, followed by a title and
75
+ question type.
76
+
77
+ The next sections define the various question types
78
+
79
+ ### Multiple choice
80
+
81
+ ```markdown
82
+ # Will you pass through UK Border Control?
83
+
84
+ You might pass through UK Border Control even if you don't leave the airport -
85
+ eg your bags aren't checked through and you need to collect them before transferring
86
+ to your outbound flight.
87
+
88
+ * yes: Yes
89
+ * no: No
90
+ ```
91
+
92
+ ### Country
93
+
94
+ ```markdown
95
+ # What passport do you have?
96
+
97
+ [country]
98
+ ```
99
+
100
+ Presents a drop-down list of countries from the built-in list.
101
+
102
+ Use front matter to exclude/include countries
103
+
104
+ ```
105
+ exclude_countries: country1, country2
106
+ include_countries: {country3: "Country 3", country4: "Country 4"}
107
+
108
+ [country]
109
+ ```
110
+
111
+ ### Date
112
+
113
+ ```markdown
114
+ # What is the baby’s due date?
115
+
116
+ [d/m/y: -1...+1]
117
+ ```
118
+
119
+ 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.
120
+
121
+ ### Value
122
+
123
+ ```markdown
124
+ [value]
125
+ ```
126
+
127
+ Asks for an arbitrary text input.
128
+
129
+ ### Money
130
+
131
+ ```markdown
132
+ [money]
133
+ ```
134
+
135
+ Asks for a numerical input which can have decimals and optional thousand-separating commas.
136
+
137
+ ### Salary
138
+
139
+ ```markdown
140
+ [salary]
141
+ ```
142
+
143
+ Asks for salary which can be expressed as either a weekly or monthly money amount. The user chooses between weekly/monthly
144
+
145
+ ### Checkbox
146
+
147
+ ```markdown
148
+ # Will you pass through UK Border Control?
149
+
150
+ * [ ] yes: Yes
151
+ * [ ] no: No
152
+ ```
153
+
154
+ ## Next node rules
155
+
156
+ Logical rules for transitioning to the next node are defined in 'Next node' section. This is declared using a markdown h1 'Next node'.
157
+
158
+ There are two constructs for defining rules:
159
+
160
+ ```
161
+ # Next node
162
+
163
+ * predicate => outcome
164
+ ```
165
+
166
+ defines a conditional transition
167
+
168
+ ```
169
+ # Next node
170
+
171
+ * predicate1
172
+ * predicate2 => outcome1
173
+ * predicate3 => outcome2
174
+ ```
175
+
176
+ defines nested rules.
177
+
178
+ ## Predicates
179
+
180
+ ```
181
+ variable_name is 'string'
182
+ ```
183
+
184
+ ## Conditional blocks in outcomes
185
+
186
+ ## Processing model
187
+
188
+ Each response to a question is assigned to a variable which corresponds to the question name (as determined by the filename).
189
+
190
+ ## Named predicates
191
+
192
+ Named predicates
193
+
194
+ ## Plugin API
195
+
196
+ A plugin API will be provided to allow more complex calculations to be defined
197
+ in an external ruby class.
198
+
199
+ ## Software design
200
+
201
+ The software design can be seen in this diagram:
202
+
203
+ ![Software design](https://raw.githubusercontent.com/alphagov/smartdown/master/doc/design.png)
204
+
data/bin/smartdown ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path("../lib/", File.dirname(__FILE__))
4
+
5
+ require 'smartdown/parser/directory_input'
6
+ require 'smartdown/parser/flow_interpreter'
7
+ require 'smartdown/engine'
8
+ require 'pp'
9
+
10
+ def usage
11
+ $stderr.puts "USAGE: <coversheet path> [responses...]"
12
+ end
13
+
14
+ if ARGV.size == 0
15
+ usage
16
+ exit(1)
17
+ else
18
+ coversheet_path, *responses = ARGV
19
+ begin
20
+ input = Smartdown::Parser::DirectoryInput.new(coversheet_path)
21
+ flow = Smartdown::Parser::FlowInterpreter.new(input).interpret
22
+ engine = Smartdown::Engine.new(flow)
23
+ end_state = engine.process(responses)
24
+
25
+ puts "RESPONSES: " + end_state.get(:responses).join(" / ")
26
+ puts "PATH: " + (end_state.get(:path) + [end_state.get(:current_node)]).join(" -> ")
27
+ node = flow.node(end_state.get(:current_node))
28
+ puts "# #{node.title}\n\n"
29
+ node.questions.each do |q|
30
+ pp q.choices
31
+ end
32
+
33
+ puts node.body
34
+ rescue Smartdown::Parser::ParseError => error
35
+ $stderr.puts error
36
+ exit(1)
37
+ rescue Smartdown::Engine::IndeterminateNextNode => error
38
+ $stderr.puts error.message
39
+ exit(1)
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ module Smartdown
2
+ class Engine
3
+ class IndeterminateNextNode < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ module Smartdown
2
+ class Engine
3
+ class PredicateEvaluator
4
+ def evaluate(predicate, state)
5
+ evaluator_for(predicate).call(state)
6
+ end
7
+
8
+ private
9
+ def evaluator_for(predicate)
10
+ case predicate
11
+ when Smartdown::Model::Predicate::Equality
12
+ ->(state) { state.get(predicate.varname) == predicate.expected_value }
13
+ when Smartdown::Model::Predicate::SetMembership
14
+ ->(state) { predicate.values.include?(state.get(predicate.varname)) }
15
+ when Smartdown::Model::Predicate::Named
16
+ ->(state) { state.get(predicate.name) }
17
+ else
18
+ raise "Unknown predicate type #{predicate.class}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ require 'set'
2
+
3
+ module Smartdown
4
+ class Engine
5
+ class UndefinedValue < StandardError; end
6
+
7
+ class State
8
+ def initialize(data = {})
9
+ @data = duplicate_and_normalize_hash(data)
10
+ @data["path"] ||= []
11
+ @data["responses"] ||= []
12
+ @cached = {}
13
+ raise ArgumentError, "must specify current_node" unless has_key?("current_node")
14
+ end
15
+
16
+ def has_key?(key)
17
+ @data.has_key?(key.to_s)
18
+ end
19
+
20
+ def has_value?(key, expected_value)
21
+ has_key?(key) && fetch(key) == expected_value
22
+ end
23
+
24
+ def get(key)
25
+ value = fetch(key)
26
+ if value.respond_to?(:call)
27
+ evaluate(value)
28
+ else
29
+ value
30
+ end
31
+ end
32
+
33
+ def put(name, value)
34
+ State.new(@data.merge(name.to_s => value))
35
+ end
36
+
37
+ def keys
38
+ Set.new(@data.keys)
39
+ end
40
+
41
+ def ==(other)
42
+ other.is_a?(self.class) && other.keys == self.keys && @data.all? { |k, v| other.has_value?(k, v) }
43
+ end
44
+
45
+ private
46
+ def duplicate_and_normalize_hash(hash)
47
+ ::Hash[hash.map { |key, value| [key.to_s, value] }]
48
+ end
49
+
50
+ def fetch(key)
51
+ if has_key?(key)
52
+ @data.fetch(key.to_s)
53
+ else
54
+ raise UndefinedValue
55
+ end
56
+ end
57
+
58
+ def evaluate(callable)
59
+ @cached[callable] ||= callable.call(self)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,65 @@
1
+ require 'smartdown/engine/predicate_evaluator'
2
+ require 'smartdown/engine/errors'
3
+
4
+ module Smartdown
5
+ class Engine
6
+ class Transition
7
+ attr_reader :state, :node, :input
8
+
9
+ def initialize(state, node, input, options = {})
10
+ @state = state
11
+ @node = node
12
+ @input = input
13
+ @predicate_evaluator = options[:predicate_evaluator] || PredicateEvaluator.new
14
+ end
15
+
16
+ def next_node
17
+ first_matching_rule(next_node_rules.rules).outcome
18
+ end
19
+
20
+ def next_state
21
+ state_with_input
22
+ .put(:path, state.get(:path) + [node.name])
23
+ .put(:responses, state.get(:responses) + [input])
24
+ .put(node.name, input)
25
+ .put(:current_node, next_node)
26
+ end
27
+
28
+ private
29
+ attr_reader :predicate_evaluator
30
+
31
+ def next_node_rules
32
+ node.elements.find { |e| e.is_a?(Smartdown::Model::NextNodeRules) } or \
33
+ raise Smartdown::Engine::IndeterminateNextNode, "No next node rules defined for '#{node.name}'"
34
+ end
35
+
36
+ def first_matching_rule(rules)
37
+ catch(:match) do
38
+ throw_first_matching_rule_in(rules)
39
+ end
40
+ end
41
+
42
+ def throw_first_matching_rule_in(rules)
43
+ rules.each do |rule|
44
+ case rule
45
+ when Smartdown::Model::Rule
46
+ if predicate_evaluator.evaluate(rule.predicate, state_with_input)
47
+ throw(:match, rule)
48
+ end
49
+ when Smartdown::Model::NestedRule
50
+ if predicate_evaluator.evaluate(rule.predicate, state_with_input)
51
+ throw_first_matching_rule_in(rule.children)
52
+ end
53
+ else
54
+ raise "Unknown rule type"
55
+ end
56
+ end
57
+ raise Smartdown::Engine::IndeterminateNextNode
58
+ end
59
+
60
+ def state_with_input
61
+ state.put(node.name, input)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,33 @@
1
+ require 'smartdown/engine/transition'
2
+ require 'smartdown/engine/state'
3
+
4
+ module Smartdown
5
+ class Engine
6
+ attr_reader :flow
7
+
8
+ def initialize(flow)
9
+ @flow = flow
10
+ end
11
+
12
+ def default_start_state
13
+ Smartdown::Engine::State.new(
14
+ default_predicates.merge(
15
+ current_node: flow.name
16
+ )
17
+ )
18
+ end
19
+
20
+ def default_predicates
21
+ {
22
+ otherwise: ->(_) { true }
23
+ }
24
+ end
25
+
26
+ def process(responses, start_state = nil)
27
+ responses.inject(start_state || default_start_state) do |state, input|
28
+ current_node = flow.node(state.get(:current_node))
29
+ Transition.new(state, current_node, input).next_state
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Element
4
+ MarkdownHeading = Struct.new(:content)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Element
4
+ MarkdownParagraph = Struct.new(:content)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Element
4
+ MultipleChoice = Struct.new(:name, :choices)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Element
4
+ StartButton = Struct.new(:start_node)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module Smartdown
2
+ module Model
3
+ class Flow
4
+ attr_reader :name, :nodes
5
+
6
+ def initialize(name, nodes = [])
7
+ @name = name
8
+ @nodes = nodes
9
+ end
10
+
11
+ def coversheet
12
+ node(name)
13
+ end
14
+
15
+ def node(node_name)
16
+ @nodes.find {|n| n.name.to_s == node_name.to_s } || raise("Unable to find #{node_name}")
17
+ end
18
+
19
+ def ==(other)
20
+ other.is_a?(self.class) && other.nodes == self.nodes && other.name == self.name
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ module Smartdown
2
+ module Model
3
+ class FrontMatter
4
+ def initialize(attributes = {})
5
+ @attributes = attributes
6
+ end
7
+
8
+ def method_missing(method_name, *args, &block)
9
+ if has_attribute?(method_name)
10
+ fetch(method_name)
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ def respond_to_missing?(method_name, include_private = false)
17
+ has_attribute?(method_name)
18
+ end
19
+
20
+ def has_attribute?(name)
21
+ @attributes.has_key?(name.to_s)
22
+ end
23
+
24
+ def fetch(name)
25
+ @attributes.fetch(name.to_s)
26
+ end
27
+
28
+ def to_hash
29
+ @attributes.dup
30
+ end
31
+
32
+ def ==(other)
33
+ other.is_a?(self.class) && self.to_hash == other.to_hash
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ module Smartdown
2
+ module Model
3
+ NestedRule = Struct.new(:predicate, :children)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Smartdown
2
+ module Model
3
+ NextNodeRules = Struct.new(:rules)
4
+ end
5
+ end
@@ -0,0 +1,47 @@
1
+ require 'smartdown/model/front_matter'
2
+
3
+ module Smartdown
4
+ module Model
5
+ Node = Struct.new(:name, :elements, :front_matter) do
6
+ def initialize(name, elements, front_matter = nil)
7
+ super(name, elements, front_matter || Smartdown::Model::FrontMatter.new)
8
+ end
9
+
10
+ def questions
11
+ elements_of_kind(Smartdown::Model::Element::MultipleChoice)
12
+ end
13
+
14
+ def title
15
+ h1s.first ? h1s.first.content : ""
16
+ end
17
+
18
+ def markdown_blocks
19
+ elements_of_kind(Smartdown::Model::Element::MarkdownHeading, Smartdown::Model::Element::MarkdownParagraph)
20
+ end
21
+
22
+ def h1s
23
+ elements_of_kind(Smartdown::Model::Element::MarkdownHeading)
24
+ end
25
+
26
+ def body
27
+ markdown_blocks[1..-1].map { |block| as_markdown(block) }.compact.join("\n")
28
+ end
29
+
30
+ private
31
+ def elements_of_kind(*kinds)
32
+ elements.select {|e| kinds.any? {|k| e.is_a?(k)} }
33
+ end
34
+
35
+ def as_markdown(block)
36
+ case block
37
+ when Smartdown::Model::Element::MarkdownHeading
38
+ "# #{block.content}\n"
39
+ when Smartdown::Model::Element::MarkdownParagraph
40
+ block.content
41
+ else
42
+ raise "Unknown markdown block type '#{block.class.to_s}'"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Predicate
4
+ Equality = Struct.new(:varname, :expected_value)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Predicate
4
+ Named = Struct.new(:name)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Smartdown
2
+ module Model
3
+ module Predicate
4
+ SetMembership = Struct.new(:varname, :values)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Smartdown
2
+ module Model
3
+ Rule = Struct.new(:predicate, :outcome)
4
+ end
5
+ end
@@ -0,0 +1,35 @@
1
+ require 'parslet'
2
+
3
+ module Smartdown
4
+ module Parser
5
+ class Base < Parslet::Parser
6
+ rule(:eof) { any.absent? }
7
+ rule(:ws_char) { match('\s') }
8
+ rule(:space_char) { str(" ") }
9
+ rule(:non_ws_char) { match('\S') }
10
+ rule(:newline) { str("\r\n") | str("\n\r") | str("\n") | str("\r") }
11
+ rule(:line_ending) { eof | newline }
12
+
13
+ rule(:optional_space) { space_char.repeat }
14
+ rule(:some_space) { space_char.repeat(1) }
15
+ rule(:ws) { ws_char.repeat }
16
+ rule(:non_ws) { non_ws.repeat }
17
+
18
+ rule(:whitespace_terminated_string) {
19
+ non_ws_char >> (non_ws_char | space_char.repeat(1) >> non_ws_char).repeat
20
+ }
21
+
22
+ rule(:identifier) {
23
+ match('[a-zA-Z_0-9-]').repeat(1)
24
+ }
25
+
26
+ rule(:question_identifier) {
27
+ identifier >> str('?').maybe
28
+ }
29
+
30
+ rule(:bullet) {
31
+ match('[*-]')
32
+ }
33
+ end
34
+ end
35
+ end