smartdown 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -285,6 +285,28 @@ Interpolations are currently supported into headings and paragraphs using the fo
285
285
  Your state pension age is %{state_pension_age}.
286
286
  ```
287
287
 
288
+ ## Snippets
289
+
290
+ Snippets work like partials, to re-use common text. They can be block or inline,
291
+ can be called recursivelty and can contain interpolation and conditional logic
292
+
293
+ They're called like so:
294
+
295
+ ```
296
+ ## My header
297
+
298
+ Markdown copy..
299
+
300
+ {{snippet: my_snippet}}
301
+
302
+ More copy...
303
+ ```
304
+
305
+ Where `snippet_name` is in a `snippets/` directory in the flow root with a `.txt`
306
+ extension, eg `my-flow-name/snippets/my_snippet.txt`.
307
+
308
+ The contents of `my_snippet` will be inseted into the outcome/question.
309
+
288
310
  ## Named predicates (tbd)
289
311
 
290
312
  Named predicates
@@ -23,6 +23,10 @@ module Smartdown
23
23
  read_dir("scenarios")
24
24
  end
25
25
 
26
+ def snippets
27
+ read_dir("snippets")
28
+ end
29
+
26
30
  def filenames_hash
27
31
  {
28
32
  coversheet: coversheet.to_s,
@@ -1,5 +1,6 @@
1
1
  require 'smartdown/model/flow'
2
2
  require 'smartdown/parser/node_interpreter'
3
+ require 'smartdown/parser/snippet_pre_parser'
3
4
 
4
5
  module Smartdown
5
6
  module Parser
@@ -20,7 +21,7 @@ module Smartdown
20
21
  attr_reader :flow_input
21
22
 
22
23
  def initialize(flow_input)
23
- @flow_input = flow_input
24
+ @flow_input = pre_parse(flow_input)
24
25
  end
25
26
 
26
27
  def interpret
@@ -33,11 +34,11 @@ module Smartdown
33
34
  end
34
35
 
35
36
  def questions
36
- flow_input.questions.map { |i| interpret_node(i) }
37
+ flow_input.questions.map { |question_data| interpret_node(question_data) }
37
38
  end
38
39
 
39
40
  def outcomes
40
- flow_input.outcomes.map { |i| interpret_node(i) }
41
+ flow_input.outcomes.map { |outcome_data| interpret_node(outcome_data) }
41
42
  end
42
43
 
43
44
  def interpret_node(input_data)
@@ -45,6 +46,10 @@ module Smartdown
45
46
  rescue Parslet::ParseFailed => error
46
47
  raise ParseError.new(input_data.to_s, error)
47
48
  end
49
+
50
+ def pre_parse(flow_input)
51
+ SnippetPreParser.parse(flow_input)
52
+ end
48
53
  end
49
54
  end
50
55
  end
@@ -0,0 +1,28 @@
1
+ module Smartdown
2
+ module Parser
3
+ class InputSet
4
+ attr_reader :coversheet, :questions, :outcomes, :snippets, :scenarios
5
+
6
+ def initialize(params = {})
7
+ @coversheet = params[:coversheet]
8
+ @questions = params[:questions]
9
+ @outcomes = params[:outcomes]
10
+ @snippets = params[:snippets]
11
+ @scenarios = params[:scenarios]
12
+ end
13
+ end
14
+
15
+ class InputData
16
+ attr_reader :name, :content
17
+
18
+ def initialize(name, content)
19
+ @name = name
20
+ @content = content
21
+ end
22
+
23
+ def read
24
+ content
25
+ end
26
+ end
27
+ end
28
+ end
@@ -30,7 +30,7 @@ module Smartdown
30
30
  }
31
31
 
32
32
  rule(:body) {
33
- markdown_blocks.as(:body)
33
+ markdown_blocks.as(:body) >> newline.repeat
34
34
  }
35
35
 
36
36
  rule(:flow) {
@@ -0,0 +1,44 @@
1
+ require 'smartdown/parser/input_set'
2
+
3
+ module Smartdown
4
+ module Parser
5
+ class SnippetPreParser
6
+ class SnippetNotFound < StandardError; end
7
+
8
+ attr_reader :input_data
9
+
10
+ def initialize(input_data)
11
+ @input_data = input_data
12
+ end
13
+
14
+ def parse
15
+ InputSet.new({
16
+ coversheet: parse_node_input(input_data.coversheet),
17
+ questions: input_data.questions.map { |question_data| parse_node_input(question_data) },
18
+ outcomes: input_data.outcomes.map { |outcome_data| parse_node_input(outcome_data) },
19
+ snippets: input_data.snippets,
20
+ scenarios: input_data.scenarios,
21
+ })
22
+ end
23
+
24
+ def self.parse(input_data)
25
+ self.new(input_data).parse
26
+ end
27
+
28
+ private
29
+ def parse_node_input(node_input)
30
+ InputData.new(node_input.name, parse_content(node_input.read))
31
+ end
32
+
33
+ def parse_content(content)
34
+ content.gsub(/\{\{snippet:\W?(.*)\}\}/i) { |_|
35
+ parse_content(get_snippet($1).read.strip)
36
+ }
37
+ end
38
+
39
+ def get_snippet(snippet_name)
40
+ input_data.snippets.find { |snippet| snippet.name == snippet_name } or raise SnippetNotFound.new(snippet_name)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,3 +1,3 @@
1
1
  module Smartdown
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -112,6 +112,14 @@ EXPECTED
112
112
  expect(flow.instance_of? Smartdown::Model::Flow).to eq(true)
113
113
  end
114
114
  end
115
+
116
+ context "snippets" do
117
+ subject(:flow) { Smartdown.parse(fixture("snippet")) }
118
+
119
+ it "has replaced the snippet tag with the snippet contents" do
120
+ expect(flow.nodes.first.elements.any? { |element| element.content.to_s.include? "snippet body" })
121
+ end
122
+ end
115
123
  end
116
124
 
117
125
 
@@ -0,0 +1,7 @@
1
+ meta_description: Snippets
2
+ satisfies_need: 999999
3
+ status: draft
4
+
5
+ # A snippet in use
6
+
7
+ {{snippet: the_snippet}}
@@ -0,0 +1 @@
1
+ snippet one
@@ -1,11 +1,5 @@
1
1
  require 'smartdown/parser/directory_input'
2
-
3
- shared_examples "flow input interface" do
4
- it { should respond_to(:coversheet) }
5
- it { should respond_to(:questions) }
6
- it { should respond_to(:outcomes) }
7
- it { should respond_to(:scenarios) }
8
- end
2
+ require 'support/flow_input_interface'
9
3
 
10
4
  describe Smartdown::Parser::DirectoryInput do
11
5
  it_should_behave_like "flow input interface"
@@ -53,4 +47,12 @@ describe Smartdown::Parser::DirectoryInput do
53
47
  expect(input.scenarios.first.read).to eq("scenario one\n")
54
48
  end
55
49
  end
50
+
51
+ describe "#snippets" do
52
+ it "returns an InputFile for every file in the snippets folder" do
53
+ expect(input.snippets).to match([instance_of(Smartdown::Parser::InputFile)])
54
+ expect(input.snippets.first.name).to eq("sn1")
55
+ expect(input.snippets.first.read).to eq("snippet one\n")
56
+ end
57
+ end
56
58
  end
@@ -0,0 +1,15 @@
1
+ require 'smartdown/parser/input_set'
2
+ require 'support/flow_input_interface'
3
+
4
+ describe Smartdown::Parser::InputSet do
5
+ it_should_behave_like "flow input interface"
6
+ end
7
+
8
+ describe Smartdown::Parser::InputData do
9
+ let(:name) { 'a name' }
10
+ let(:data) { 'some smartdown' }
11
+ subject { Smartdown::Parser::InputData.new(name, data) }
12
+
13
+ specify { expect(subject.name).to eql name }
14
+ specify { expect(subject.read).to eql data }
15
+ end
@@ -166,4 +166,30 @@ SOURCE
166
166
  })
167
167
  }
168
168
  end
169
+
170
+ context "body with extra trailing newlines" do
171
+ let(:source) {
172
+ <<SOURCE
173
+ # This is my title
174
+
175
+ This is a paragraph of text with stuff
176
+ that flows along
177
+
178
+ Another paragraph of text
179
+
180
+
181
+ SOURCE
182
+ }
183
+
184
+ it {
185
+ should parse(source).as({
186
+ body: [
187
+ {h1: "This is my title"},
188
+ {p: "This is a paragraph of text with stuff\nthat flows along\n"},
189
+ {p: "Another paragraph of text\n"}
190
+ ]
191
+ })
192
+ }
193
+ end
194
+
169
195
  end
@@ -0,0 +1,116 @@
1
+ require 'smartdown/parser/snippet_pre_parser'
2
+ require 'smartdown/parser/directory_input'
3
+ require 'ostruct'
4
+
5
+ describe Smartdown::Parser::SnippetPreParser do
6
+ let(:input_data) {
7
+ Smartdown::Parser::InputSet.new({
8
+ coversheet: Smartdown::Parser::InputData.new("coversheet_1", "some smartdown {{snippet: coversheet_snippet}}"),
9
+ questions: [
10
+ Smartdown::Parser::InputData.new("question_1", "some {{snippet: question_snippet}} smartdown"),
11
+ ],
12
+ outcomes: [
13
+ Smartdown::Parser::InputData.new("outcome_1", "some smartdown\n\n{{snippet: outcome_snippet}}\n\nmore smartdown"),
14
+ ],
15
+ snippets: [
16
+ Smartdown::Parser::InputData.new("question_snippet", "question snippet"),
17
+ Smartdown::Parser::InputData.new("outcome_snippet", "outcome snippet"),
18
+ Smartdown::Parser::InputData.new("coversheet_snippet", "coversheet snippet"),
19
+ ],
20
+ scenarios: [:some_scenario]
21
+ })
22
+ }
23
+
24
+ subject(:parsed_output) {
25
+ described_class.parse(input_data)
26
+ }
27
+
28
+ it "should replace the snippet tag with the snippet content for questions" do
29
+ expect(parsed_output.questions[0].read).to eql "some question snippet smartdown"
30
+ end
31
+
32
+ it "should replace the snippet tag with the snippet content for outcomes" do
33
+ expect(parsed_output.outcomes[0].read).to eql "some smartdown\n\noutcome snippet\n\nmore smartdown"
34
+ end
35
+
36
+ it "should replace the snippet tag with the snippet content for the coversheet" do
37
+ expect(parsed_output.coversheet.read).to eql "some smartdown coversheet snippet"
38
+ end
39
+
40
+ context "with nested snippets" do
41
+ let(:input_data) {
42
+ Smartdown::Parser::InputSet.new({
43
+ coversheet: Smartdown::Parser::InputData.new("coversheet_1", "some smartdown {{snippet: top_level_snippet}}"),
44
+ questions: [],
45
+ outcomes: [],
46
+ snippets: [
47
+ Smartdown::Parser::InputData.new("top_level_snippet", "top level snippet {{snippet: nested_snippet}}"),
48
+ Smartdown::Parser::InputData.new("nested_snippet", "nested snippet"),
49
+ ],
50
+ scenarios: []
51
+ })
52
+ }
53
+
54
+ it "shoud recursively process nested snippets" do
55
+ expect(parsed_output.coversheet.read).to eql "some smartdown top level snippet nested snippet"
56
+ end
57
+ end
58
+
59
+ context "when referencing a non-existent snippet" do
60
+ let(:input_data) {
61
+ Smartdown::Parser::InputSet.new({
62
+ coversheet: Smartdown::Parser::InputData.new("coversheet_1", "some smartdown {{snippet: non_existent_snippet}}"),
63
+ questions: [],
64
+ outcomes: [],
65
+ snippets: [],
66
+ scenarios: []
67
+ })
68
+ }
69
+
70
+ specify { expect { parsed_output }.to raise_exception(Smartdown::Parser::SnippetPreParser::SnippetNotFound) }
71
+ end
72
+
73
+ describe "whitespace handling" do
74
+ let(:snippet_smartdown) { "A snippet" }
75
+ let(:input_data) {
76
+ Smartdown::Parser::InputSet.new({
77
+ coversheet: Smartdown::Parser::InputData.new("coversheet_1", "Smartdown {{snippet: snippet}} more smartdown"),
78
+ questions: [],
79
+ outcomes: [],
80
+ snippets: [ Smartdown::Parser::InputData.new("snippet", snippet_smartdown) ],
81
+ scenarios: []
82
+ })
83
+ }
84
+
85
+ context "when snippet smartdown has leading / trailing whitespace" do
86
+ let(:snippet_smartdown) { "\n\n Snippet text\n\n with whitespace \n\n" }
87
+
88
+ it "should strip off the snippet content's leading and trailing whitespace" do
89
+ expect(parsed_output.coversheet.read).to eql "Smartdown Snippet text\n\n with whitespace more smartdown"
90
+ end
91
+ end
92
+ end
93
+
94
+ describe "alternative tag definitions" do
95
+ let(:snippet_tag) { "{{snippet: snippet_name}}" }
96
+ let(:input_data) {
97
+ Smartdown::Parser::InputSet.new({
98
+ coversheet: Smartdown::Parser::InputData.new("coversheet_1", snippet_tag),
99
+ questions: [],
100
+ outcomes: [],
101
+ snippets: [ Smartdown::Parser::InputData.new("snippet_name", "the snippet") ],
102
+ scenarios: []
103
+ })
104
+ }
105
+
106
+ context "with {{SNIPPET: snippet_name}}" do
107
+ let(:snippet_tag) { "{{SNIPPET: snippet_name}}" }
108
+ specify { expect(parsed_output.coversheet.read).to eql "the snippet" }
109
+ end
110
+
111
+ context "with {{snippet:snippet_name}}" do
112
+ let(:snippet_tag) { "{{snippet:snippet_name}}" }
113
+ specify { expect(parsed_output.coversheet.read).to eql "the snippet" }
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,7 @@
1
+ shared_examples "flow input interface" do
2
+ it { should respond_to(:coversheet) }
3
+ it { should respond_to(:questions) }
4
+ it { should respond_to(:outcomes) }
5
+ it { should respond_to(:scenarios) }
6
+ it { should respond_to(:snippets) }
7
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smartdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2014-07-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: parslet
16
- requirement: &10821840 !ruby/object:Gem::Requirement
16
+ requirement: &7733220 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.6.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *10821840
24
+ version_requirements: *7733220
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &10820700 !ruby/object:Gem::Requirement
27
+ requirement: &7731020 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 3.0.0
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *10820700
35
+ version_requirements: *7731020
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake
38
- requirement: &10818660 !ruby/object:Gem::Requirement
38
+ requirement: &7744660 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *10818660
46
+ version_requirements: *7744660
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: gem_publisher
49
- requirement: &10832280 !ruby/object:Gem::Requirement
49
+ requirement: &7742640 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *10832280
57
+ version_requirements: *7742640
58
58
  description:
59
59
  email: david.heath@digital.cabinet-office.gov.uk
60
60
  executables:
@@ -67,7 +67,9 @@ files:
67
67
  - lib/smartdown/parser/predicates.rb
68
68
  - lib/smartdown/parser/node_transform.rb
69
69
  - lib/smartdown/parser/node_interpreter.rb
70
+ - lib/smartdown/parser/snippet_pre_parser.rb
70
71
  - lib/smartdown/parser/base.rb
72
+ - lib/smartdown/parser/input_set.rb
71
73
  - lib/smartdown/parser/directory_input.rb
72
74
  - lib/smartdown/parser/rules.rb
73
75
  - lib/smartdown/parser/node_parser.rb
@@ -134,6 +136,7 @@ files:
134
136
  - spec/parser/directory_input_spec.rb
135
137
  - spec/parser/integration/cover_sheet_spec.rb
136
138
  - spec/parser/base_spec.rb
139
+ - spec/parser/input_set_spec.rb
137
140
  - spec/parser/rules_spec.rb
138
141
  - spec/parser/predicates_spec.rb
139
142
  - spec/parser/element/next_steps_spec.rb
@@ -146,6 +149,8 @@ files:
146
149
  - spec/parser/element/front_matter_spec.rb
147
150
  - spec/parser/element/salary_question_spec.rb
148
151
  - spec/parser/node_parser_spec.rb
152
+ - spec/parser/snippet_pre_parser_spec.rb
153
+ - spec/support/flow_input_interface.rb
149
154
  - spec/support/model_builder.rb
150
155
  - spec/spec_helper.rb
151
156
  - spec/engine/state_spec.rb
@@ -156,6 +161,7 @@ files:
156
161
  - spec/fixtures/directory_input/outcomes/o1.txt
157
162
  - spec/fixtures/directory_input/questions/q1.txt
158
163
  - spec/fixtures/directory_input/cover-sheet.txt
164
+ - spec/fixtures/directory_input/snippets/sn1.txt
159
165
  - spec/fixtures/acceptance/question-and-outcome/question-and-outcome.txt
160
166
  - spec/fixtures/acceptance/question-and-outcome/outcomes/o1.txt
161
167
  - spec/fixtures/acceptance/question-and-outcome/questions/q1.txt
@@ -169,6 +175,8 @@ files:
169
175
  - spec/fixtures/acceptance/cover-sheet/cover-sheet.txt
170
176
  - spec/fixtures/acceptance/one-question/one-question.txt
171
177
  - spec/fixtures/acceptance/one-question/questions/q1.txt
178
+ - spec/fixtures/acceptance/snippet/snippet.txt
179
+ - spec/fixtures/acceptance/snippet/snippets/the_snippet.txt
172
180
  - spec/fixtures/example.sd
173
181
  - spec/support_specs/model_builder_spec.rb
174
182
  - spec/engine_spec.rb
@@ -195,7 +203,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
195
203
  version: '0'
196
204
  segments:
197
205
  - 0
198
- hash: -1672361924519016433
206
+ hash: 2695022002413728177
199
207
  required_rubygems_version: !ruby/object:Gem::Requirement
200
208
  none: false
201
209
  requirements:
@@ -204,7 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
212
  version: '0'
205
213
  segments:
206
214
  - 0
207
- hash: -1672361924519016433
215
+ hash: 2695022002413728177
208
216
  requirements: []
209
217
  rubyforge_project:
210
218
  rubygems_version: 1.8.11
@@ -217,6 +225,7 @@ test_files:
217
225
  - spec/parser/directory_input_spec.rb
218
226
  - spec/parser/integration/cover_sheet_spec.rb
219
227
  - spec/parser/base_spec.rb
228
+ - spec/parser/input_set_spec.rb
220
229
  - spec/parser/rules_spec.rb
221
230
  - spec/parser/predicates_spec.rb
222
231
  - spec/parser/element/next_steps_spec.rb
@@ -229,6 +238,8 @@ test_files:
229
238
  - spec/parser/element/front_matter_spec.rb
230
239
  - spec/parser/element/salary_question_spec.rb
231
240
  - spec/parser/node_parser_spec.rb
241
+ - spec/parser/snippet_pre_parser_spec.rb
242
+ - spec/support/flow_input_interface.rb
232
243
  - spec/support/model_builder.rb
233
244
  - spec/spec_helper.rb
234
245
  - spec/engine/state_spec.rb
@@ -239,6 +250,7 @@ test_files:
239
250
  - spec/fixtures/directory_input/outcomes/o1.txt
240
251
  - spec/fixtures/directory_input/questions/q1.txt
241
252
  - spec/fixtures/directory_input/cover-sheet.txt
253
+ - spec/fixtures/directory_input/snippets/sn1.txt
242
254
  - spec/fixtures/acceptance/question-and-outcome/question-and-outcome.txt
243
255
  - spec/fixtures/acceptance/question-and-outcome/outcomes/o1.txt
244
256
  - spec/fixtures/acceptance/question-and-outcome/questions/q1.txt
@@ -252,6 +264,8 @@ test_files:
252
264
  - spec/fixtures/acceptance/cover-sheet/cover-sheet.txt
253
265
  - spec/fixtures/acceptance/one-question/one-question.txt
254
266
  - spec/fixtures/acceptance/one-question/questions/q1.txt
267
+ - spec/fixtures/acceptance/snippet/snippet.txt
268
+ - spec/fixtures/acceptance/snippet/snippets/the_snippet.txt
255
269
  - spec/fixtures/example.sd
256
270
  - spec/support_specs/model_builder_spec.rb
257
271
  - spec/engine_spec.rb