smartdown 0.0.1

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