bridgetown-feed 1.0.0

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +27 -0
  5. data/Gemfile +11 -0
  6. data/History.markdown +5 -0
  7. data/LICENSE.txt +23 -0
  8. data/README.md +201 -0
  9. data/Rakefile +8 -0
  10. data/bridgetown-feed.gemspec +28 -0
  11. data/lib/bridgetown-feed.rb +12 -0
  12. data/lib/bridgetown-feed/feed.xml +96 -0
  13. data/lib/bridgetown-feed/generator.rb +111 -0
  14. data/lib/bridgetown-feed/meta-tag.rb +37 -0
  15. data/lib/bridgetown-feed/page-without-a-file.rb +9 -0
  16. data/lib/bridgetown-feed/version.rb +7 -0
  17. data/script/bootstrap +3 -0
  18. data/script/cibuild +7 -0
  19. data/script/fmt +10 -0
  20. data/script/release +7 -0
  21. data/script/test +4 -0
  22. data/spec/bridgetown-feed_spec.rb +525 -0
  23. data/spec/fixtures/bridgetown.config.yml +9 -0
  24. data/spec/fixtures/src/_collection/2018-01-01-collection-doc.md +4 -0
  25. data/spec/fixtures/src/_collection/2018-01-02-collection-category-doc.md +5 -0
  26. data/spec/fixtures/src/_data/authors.yml +5 -0
  27. data/spec/fixtures/src/_layouts/some_default.html +11 -0
  28. data/spec/fixtures/src/_posts/2013-12-12-dec-the-second.md +7 -0
  29. data/spec/fixtures/src/_posts/2014-03-02-march-the-second.md +6 -0
  30. data/spec/fixtures/src/_posts/2014-03-04-march-the-fourth.md +9 -0
  31. data/spec/fixtures/src/_posts/2015-01-12-a-draft.md +5 -0
  32. data/spec/fixtures/src/_posts/2015-01-18-jekyll-last-modified-at.md +5 -0
  33. data/spec/fixtures/src/_posts/2015-02-12-strip-newlines.md +6 -0
  34. data/spec/fixtures/src/_posts/2015-05-12-liquid.md +7 -0
  35. data/spec/fixtures/src/_posts/2015-05-12-pre.html +8 -0
  36. data/spec/fixtures/src/_posts/2015-05-18-author-detail.md +9 -0
  37. data/spec/fixtures/src/_posts/2015-08-08-stuck-in-the-middle.html +6 -0
  38. data/spec/fixtures/src/_posts/2016-04-25-author-reference.md +6 -0
  39. data/spec/fixtures/src/feed.xslt.xml +0 -0
  40. data/spec/spec_helper.rb +35 -0
  41. metadata +210 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownFeed
4
+ class Generator < Bridgetown::Generator
5
+ priority :lowest
6
+
7
+ # Main plugin action, called by Bridgetown-core
8
+ def generate(site)
9
+ @site = site
10
+ collections.each do |name, meta|
11
+ Bridgetown.logger.info "Bridgetown Feed:", "Generating feed for #{name}"
12
+ (meta["categories"] + [nil]).each do |category|
13
+ path = feed_path(:collection => name, :category => category)
14
+ next if file_exists?(path)
15
+
16
+ @site.pages << make_page(path, :collection => name, :category => category)
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # Matches all whitespace that follows
24
+ # 1. A '>', which closes an XML tag or
25
+ # 2. A '}', which closes a Liquid tag
26
+ # We will strip all of this whitespace to minify the template
27
+ MINIFY_REGEX = %r!(?<=>|})\s+!.freeze
28
+
29
+ # Returns the plugin's config or an empty hash if not set
30
+ def config
31
+ @config ||= @site.config["feed"] || {}
32
+ end
33
+
34
+ # Determines the destination path of a given feed
35
+ #
36
+ # collection - the name of a collection, e.g., "posts"
37
+ # category - a category within that collection, e.g., "news"
38
+ #
39
+ # Will return "/feed.xml", or the config-specified default feed for posts
40
+ # Will return `/feed/category.xml` for post categories
41
+ # WIll return `/feed/collection.xml` for other collections
42
+ # Will return `/feed/collection/category.xml` for other collection categories
43
+ def feed_path(collection: "posts", category: nil)
44
+ prefix = collection == "posts" ? "/feed" : "/feed/#{collection}"
45
+ return "#{prefix}/#{category}.xml" if category
46
+
47
+ collections.dig(collection, "path") || "#{prefix}.xml"
48
+ end
49
+
50
+ # Returns a hash representing all collections to be processed and their metadata
51
+ # in the form of { collection_name => { categories = [...], path = "..." } }
52
+ def collections
53
+ return @collections if defined?(@collections)
54
+
55
+ @collections = if config["collections"].is_a?(Array)
56
+ config["collections"].map { |c| [c, {}] }.to_h
57
+ elsif config["collections"].is_a?(Hash)
58
+ config["collections"]
59
+ else
60
+ {}
61
+ end
62
+
63
+ @collections = normalize_posts_meta(@collections)
64
+ @collections.each_value do |meta|
65
+ meta["categories"] = (meta["categories"] || []).to_set
66
+ end
67
+
68
+ @collections
69
+ end
70
+
71
+ # Path to feed.xml template file
72
+ def feed_source_path
73
+ @feed_source_path ||= File.expand_path "feed.xml", __dir__
74
+ end
75
+
76
+ def feed_template
77
+ @feed_template ||= File.read(feed_source_path).gsub(MINIFY_REGEX, "")
78
+ end
79
+
80
+ # Checks if a file already exists in the site source
81
+ def file_exists?(file_path)
82
+ File.exist? @site.in_source_dir(file_path)
83
+ end
84
+
85
+ # Generates contents for a file
86
+
87
+ def make_page(file_path, collection: "posts", category: nil)
88
+ PageWithoutAFile.new(@site, __dir__, "", file_path).tap do |file|
89
+ file.content = feed_template
90
+ file.data.merge!(
91
+ "layout" => nil,
92
+ "sitemap" => false,
93
+ "xsl" => file_exists?("feed.xslt.xml"),
94
+ "collection" => collection,
95
+ "category" => category
96
+ )
97
+ file.output
98
+ end
99
+ end
100
+
101
+ # Special case the "posts" collection, which, for ease of use and backwards
102
+ # compatability, can be configured via top-level keys or directly as a collection
103
+ def normalize_posts_meta(hash)
104
+ hash["posts"] ||= {}
105
+ hash["posts"]["path"] ||= config["path"]
106
+ hash["posts"]["categories"] ||= config["categories"]
107
+ config["path"] ||= hash["posts"]["path"]
108
+ hash
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownFeed
4
+ class MetaTag < Liquid::Tag
5
+ # Use Bridgetown's native relative_url filter
6
+ include Bridgetown::Filters::URLFilters
7
+
8
+ def render(context)
9
+ @context = context
10
+ attrs = attributes.map { |k, v| %(#{k}="#{v}") }.join(" ")
11
+ "<link #{attrs} />"
12
+ end
13
+
14
+ private
15
+
16
+ def config
17
+ @config ||= @context.registers[:site].config
18
+ end
19
+
20
+ def attributes
21
+ {
22
+ :type => "application/atom+xml",
23
+ :rel => "alternate",
24
+ :href => absolute_url(path),
25
+ :title => title,
26
+ }.keep_if { |_, v| v }
27
+ end
28
+
29
+ def path
30
+ config.dig("feed", "path") || "feed.xml"
31
+ end
32
+
33
+ def title
34
+ config["title"] || config["name"]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownFeed
4
+ class PageWithoutAFile < Bridgetown::Page
5
+ def read_yaml(*)
6
+ @data ||= {}
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Feed
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ #! /bin/bash
2
+
3
+ bundle install
@@ -0,0 +1,7 @@
1
+ #! /bin/bash
2
+
3
+ set -e
4
+
5
+ script/test
6
+ script/fmt
7
+ bundle exec rake build
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Rubocop $(bundle exec rubocop --version)"
5
+ bundle exec rubocop -D -E $@
6
+ success=$?
7
+ if ((success != 0)); then
8
+ echo -e "\nTry running \`script/fmt -a\` to automatically fix errors"
9
+ fi
10
+ exit $success
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ # Tag and push a release.
3
+
4
+ set -e
5
+
6
+ script/cibuild
7
+ bundle exec rake release
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ set -ex
3
+
4
+ bundle exec rspec "$@"
@@ -0,0 +1,525 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe(BridgetownFeed) do
6
+ let(:overrides) { {} }
7
+ let(:config) do
8
+ Bridgetown.configuration(Bridgetown::Utils.deep_merge_hashes({
9
+ "full_rebuild" => true,
10
+ "root_dir" => root_dir,
11
+ "source" => source_dir,
12
+ "destination" => dest_dir,
13
+ "show_drafts" => true,
14
+ "url" => "http://example.org",
15
+ "name" => "My awesome site",
16
+ "author" => {
17
+ "name" => "Dr. Bridgetown",
18
+ },
19
+ "collections" => {
20
+ "my_collection" => { "output" => true },
21
+ "other_things" => { "output" => false },
22
+ },
23
+ }, overrides))
24
+ end
25
+ let(:site) { Bridgetown::Site.new(config) }
26
+ let(:contents) { File.read(dest_dir("feed.xml")) }
27
+ let(:context) { make_context(:site => site) }
28
+ let(:feed_meta) { Liquid::Template.parse("{% feed_meta %}").render!(context, {}) }
29
+ before(:each) do
30
+ site.process
31
+ end
32
+
33
+ it "has no layout" do
34
+ expect(contents).not_to match(%r!\ATHIS IS MY LAYOUT!)
35
+ end
36
+
37
+ it "creates a feed.xml file" do
38
+ expect(Pathname.new(dest_dir("feed.xml"))).to exist
39
+ end
40
+
41
+ it "doesn't have multiple new lines or trailing whitespace" do
42
+ expect(contents).to_not match %r!\s+\n!
43
+ expect(contents).to_not match %r!\n{2,}!
44
+ end
45
+
46
+ it "puts all the posts in the feed.xml file" do
47
+ expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
48
+ expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html"
49
+ expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html"
50
+ expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html"
51
+ expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html"
52
+ end
53
+
54
+ it "does not include assets or any static files that aren't .html" do
55
+ expect(contents).not_to match "http://example.org/images/hubot.png"
56
+ expect(contents).not_to match "http://example.org/feeds/atom.xml"
57
+ end
58
+
59
+ it "preserves linebreaks in preformatted text in posts" do
60
+ expect(contents).to match "Line 1\nLine 2\nLine 3"
61
+ end
62
+
63
+ it "supports post author name as an object" do
64
+ expect(contents).to match %r!<author>\s*<name>Ben</name>\s*<email>ben@example\.com</email>\s*<uri>http://ben\.balter\.com</uri>\s*</author>!
65
+ end
66
+
67
+ it "supports post author name as a string" do
68
+ expect(contents).to match %r!<author>\s*<name>Pat</name>\s*</author>!
69
+ end
70
+
71
+ it "does not output author tag no author is provided" do
72
+ expect(contents).not_to match %r!<author>\s*<name></name>\s*</author>!
73
+ end
74
+
75
+ it "does use author reference with data from _data/authors.yml" do
76
+ expect(contents).to match %r!<author>\s*<name>Garth</name>\s*<email>example@mail\.com</email>\s*<uri>http://garthdb\.com</uri>\s*</author>!
77
+ end
78
+
79
+ it "converts markdown posts to HTML" do
80
+ expect(contents).to match %r!&lt;p&gt;March the second\!&lt;/p&gt;!
81
+ end
82
+
83
+ it "uses last_modified_at where available" do
84
+ expect(contents).to match %r!<updated>2015-05-12T13:27:59\+00:00</updated>!
85
+ end
86
+
87
+ it "replaces newlines in posts to spaces" do
88
+ expect(contents).to match '<title type="html">The plugin will properly strip newlines.</title>'
89
+ end
90
+
91
+ it "renders Liquid inside posts" do
92
+ expect(contents).to match "Liquid is rendered."
93
+ expect(contents).not_to match "Liquid is not rendered."
94
+ end
95
+
96
+ context "images" do
97
+ let(:image1) { 'http://example.org/image.png' }
98
+ let(:image2) { 'https://cdn.example.org/absolute.png?h=188&amp;w=250' }
99
+ let(:image3) { 'http://example.org/object-image.png' }
100
+
101
+ it "includes the item image" do
102
+ expect(contents).to include(%(<media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="#{image1}" />))
103
+ expect(contents).to include(%(<media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="#{image2}" />))
104
+ expect(contents).to include(%(<media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="#{image3}" />))
105
+ end
106
+
107
+ it "included media content for mail templates (Mailchimp)" do
108
+ expect(contents).to include(%(<media:content medium="image" url="#{image1}" xmlns:media="http://search.yahoo.com/mrss/" />))
109
+ expect(contents).to include(%(<media:content medium="image" url="#{image2}" xmlns:media="http://search.yahoo.com/mrss/" />))
110
+ expect(contents).to include(%(<media:content medium="image" url="#{image3}" xmlns:media="http://search.yahoo.com/mrss/" />))
111
+ end
112
+ end
113
+
114
+ context "parsing" do
115
+ let(:feed) { RSS::Parser.parse(contents) }
116
+
117
+ it "outputs an RSS feed" do
118
+ expect(feed.feed_type).to eql("atom")
119
+ expect(feed.feed_version).to eql("1.0")
120
+ expect(feed.encoding).to eql("UTF-8")
121
+ expect(feed.lang).to be_nil
122
+ expect(feed.valid?).to eql(true)
123
+ end
124
+
125
+ it "outputs the link" do
126
+ expect(feed.link.href).to eql("http://example.org/feed.xml")
127
+ end
128
+
129
+ it "outputs the generator" do
130
+ expect(feed.generator.content).to eql("Bridgetown")
131
+ expect(feed.generator.version).to eql(Bridgetown::VERSION)
132
+ end
133
+
134
+ it "includes the items" do
135
+ expect(feed.items.count).to eql(10)
136
+ end
137
+
138
+ it "includes item contents" do
139
+ post = feed.items.last
140
+ expect(post.title.content).to eql("Dec The Second")
141
+ expect(post.link.href).to eql("http://example.org/news/2013/12/12/dec-the-second.html")
142
+ expect(post.published.content).to eql(Time.parse("2013-12-12"))
143
+ end
144
+
145
+ it "includes the item's excerpt" do
146
+ post = feed.items.last
147
+ expect(post.summary.content).to eql("Foo")
148
+ end
149
+
150
+ it "doesn't include the item's excerpt if blank" do
151
+ post = feed.items.first
152
+ expect(post.summary).to be_nil
153
+ end
154
+
155
+ context "with site.lang set" do
156
+ lang = "en_US"
157
+ let(:overrides) { { "lang" => lang } }
158
+ it "outputs a valid feed" do
159
+ expect(feed.feed_type).to eql("atom")
160
+ expect(feed.feed_version).to eql("1.0")
161
+ expect(feed.encoding).to eql("UTF-8")
162
+ expect(feed.valid?).to eql(true)
163
+ end
164
+
165
+ it "outputs the correct language" do
166
+ expect(feed.lang).to eql(lang)
167
+ end
168
+
169
+ it "sets the language of entries" do
170
+ post = feed.items.first
171
+ expect(post.lang).to eql(lang)
172
+ end
173
+
174
+ it "renders the feed meta" do
175
+ expected = %r!<link href="http://example.org/" rel="alternate" type="text/html" hreflang="#{lang}" />!
176
+ expect(contents).to match(expected)
177
+ end
178
+ end
179
+
180
+ context "with site.title set" do
181
+ let(:site_title) { "My Site Title" }
182
+ let(:overrides) { { "title" => site_title } }
183
+
184
+ it "uses site.title for the title" do
185
+ expect(feed.title.content).to eql(site_title)
186
+ end
187
+ end
188
+
189
+ context "with site.name set" do
190
+ let(:site_name) { "My Site Name" }
191
+ let(:overrides) { { "name" => site_name } }
192
+
193
+ it "uses site.name for the title" do
194
+ expect(feed.title.content).to eql(site_name)
195
+ end
196
+ end
197
+
198
+ context "with site.name and site.title set" do
199
+ let(:site_title) { "My Site Title" }
200
+ let(:site_name) { "My Site Name" }
201
+ let(:overrides) { { "title" => site_title, "name" => site_name } }
202
+
203
+ it "uses site.title for the title, dropping site.name" do
204
+ expect(feed.title.content).to eql(site_title)
205
+ end
206
+ end
207
+ end
208
+
209
+ context "smartify" do
210
+ let(:site_title) { "Pat's Site" }
211
+ let(:overrides) { { "title" => site_title } }
212
+ let(:feed) { RSS::Parser.parse(contents) }
213
+
214
+ it "processes site title with SmartyPants" do
215
+ expect(feed.title.content).to eql("Pat’s Site")
216
+ end
217
+ end
218
+
219
+ context "validation" do
220
+ it "validates" do
221
+ skip "Typhoeus couldn't find the 'libcurl' module on Windows" if Gem.win_platform?
222
+ # See https://validator.w3.org/docs/api.html
223
+ url = "https://validator.w3.org/feed/check.cgi?output=soap12"
224
+ response = Typhoeus.post(url, :body => { :rawdata => contents }, :accept_encoding => "gzip")
225
+ pending "Something went wrong with the W3 validator" unless response.success?
226
+ result = Nokogiri::XML(response.body)
227
+ result.remove_namespaces!
228
+
229
+ result.css("warning").each do |warning|
230
+ # Quiet a warning that results from us passing the feed as a string
231
+ next if warning.css("text").text =~ %r!Self reference doesn't match document location!
232
+
233
+ # Quiet expected warning that results from blank summary test case
234
+ next if warning.css("text").text =~ %r!(content|summary) should not be blank!
235
+
236
+ # Quiet expected warning about multiple posts with same updated time
237
+ next if warning.css("text").text =~ %r!Two entries with the same value for atom:updated!
238
+
239
+ warn "Validation warning: #{warning.css("text").text} on line #{warning.css("line").text} column #{warning.css("column").text}"
240
+ end
241
+
242
+ errors = result.css("error").map do |error|
243
+ "Validation error: #{error.css("text").text} on line #{error.css("line").text} column #{error.css("column").text}"
244
+ end
245
+
246
+ expect(result.css("validity").text).to eql("true"), errors.join("\n")
247
+ end
248
+ end
249
+
250
+ context "with a baseurl" do
251
+ let(:overrides) do
252
+ { "baseurl" => "/bass" }
253
+ end
254
+
255
+ it "correctly adds the baseurl to the posts" do
256
+ expect(contents).to match "http://example.org/bass/updates/bridgetown/2014/03/04/march-the-fourth.html"
257
+ expect(contents).to match "http://example.org/bass/news/2014/03/02/march-the-second.html"
258
+ expect(contents).to match "http://example.org/bass/news/2013/12/12/dec-the-second.html"
259
+ end
260
+
261
+ it "renders the feed meta" do
262
+ expected = 'href="http://example.org/bass/feed.xml"'
263
+ expect(feed_meta).to include(expected)
264
+ end
265
+ end
266
+
267
+ context "feed meta" do
268
+ it "renders the feed meta" do
269
+ expected = '<link type="application/atom+xml" rel="alternate" href="http://example.org/feed.xml" title="My awesome site" />'
270
+ expect(feed_meta).to eql(expected)
271
+ end
272
+
273
+ context "with a blank site name" do
274
+ let(:config) do
275
+ Bridgetown.configuration(
276
+ "source" => source_dir,
277
+ "destination" => dest_dir,
278
+ "url" => "http://example.org"
279
+ )
280
+ end
281
+
282
+ it "does not output blank title" do
283
+ expect(feed_meta).not_to include("title=")
284
+ end
285
+ end
286
+ end
287
+
288
+ context "changing the feed path" do
289
+ let(:overrides) do
290
+ {
291
+ "feed" => {
292
+ "path" => "atom.xml",
293
+ },
294
+ }
295
+ end
296
+
297
+ it "should write to atom.xml" do
298
+ expect(Pathname.new(dest_dir("atom.xml"))).to exist
299
+ end
300
+
301
+ it "renders the feed meta with custom feed path" do
302
+ expected = 'href="http://example.org/atom.xml"'
303
+ expect(feed_meta).to include(expected)
304
+ end
305
+ end
306
+
307
+ context "changing the file path via collection meta" do
308
+ let(:overrides) do
309
+ {
310
+ "feed" => {
311
+ "collections" => {
312
+ "posts" => {
313
+ "path" => "atom.xml",
314
+ },
315
+ },
316
+ },
317
+ }
318
+ end
319
+
320
+ it "should write to atom.xml" do
321
+ expect(Pathname.new(dest_dir("atom.xml"))).to exist
322
+ end
323
+
324
+ it "renders the feed meta with custom feed path" do
325
+ expected = 'href="http://example.org/atom.xml"'
326
+ expect(feed_meta).to include(expected)
327
+ end
328
+ end
329
+
330
+ context "feed stylesheet" do
331
+ it "includes the stylesheet" do
332
+ expect(contents).to include('<?xml-stylesheet type="text/xml" href="http://example.org/feed.xslt.xml"?>')
333
+ end
334
+ end
335
+
336
+ context "with site.lang set" do
337
+ let(:overrides) { { "lang" => "en-US" } }
338
+
339
+ it "should set the language" do
340
+ expect(contents).to match 'type="text/html" hreflang="en-US" />'
341
+ end
342
+ end
343
+
344
+ context "with post.lang set" do
345
+ it "should set the language for that entry" do
346
+ expect(contents).to match '<entry xml:lang="en">'
347
+ expect(contents).to match '<entry>'
348
+ end
349
+ end
350
+
351
+ context "categories" do
352
+ context "with top-level post categories" do
353
+ let(:overrides) do
354
+ {
355
+ "feed" => { "categories" => ["news"] },
356
+ }
357
+ end
358
+ let(:news_feed) { File.read(dest_dir("feed/news.xml")) }
359
+
360
+ it "outputs the primary feed" do
361
+ expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
362
+ expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html"
363
+ expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html"
364
+ expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html"
365
+ expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html"
366
+ end
367
+
368
+ it "outputs the category feed" do
369
+ expect(news_feed).to match '<title type="html">My awesome site | News</title>'
370
+ expect(news_feed).to match "http://example.org/news/2014/03/02/march-the-second.html"
371
+ expect(news_feed).to match "http://example.org/news/2013/12/12/dec-the-second.html"
372
+ expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
373
+ expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html"
374
+ end
375
+ end
376
+
377
+ context "with collection-level post categories" do
378
+ let(:overrides) do
379
+ {
380
+ "feed" => {
381
+ "collections" => {
382
+ "posts" => {
383
+ "categories" => ["news"],
384
+ },
385
+ },
386
+ },
387
+ }
388
+ end
389
+ let(:news_feed) { File.read(dest_dir("feed/news.xml")) }
390
+
391
+ it "outputs the primary feed" do
392
+ expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
393
+ expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html"
394
+ expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html"
395
+ expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html"
396
+ expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html"
397
+ end
398
+
399
+ it "outputs the category feed" do
400
+ expect(news_feed).to match '<title type="html">My awesome site | News</title>'
401
+ expect(news_feed).to match "http://example.org/news/2014/03/02/march-the-second.html"
402
+ expect(news_feed).to match "http://example.org/news/2013/12/12/dec-the-second.html"
403
+ expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
404
+ expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html"
405
+ end
406
+ end
407
+ end
408
+
409
+ context "collections" do
410
+ let(:collection_feed) { File.read(dest_dir("feed/collection.xml")) }
411
+
412
+ context "when initialized as an array" do
413
+ let(:overrides) do
414
+ {
415
+ "collections" => {
416
+ "collection" => {
417
+ "output" => true,
418
+ },
419
+ },
420
+ "feed" => { "collections" => ["collection"] },
421
+ }
422
+ end
423
+
424
+ it "outputs the collection feed" do
425
+ expect(collection_feed).to match '<title type="html">My awesome site | Collection</title>'
426
+ expect(collection_feed).to match "http://example.org/collection/2018-01-01-collection-doc.html"
427
+ expect(collection_feed).to match "http://example.org/collection/2018-01-02-collection-category-doc.html"
428
+ expect(collection_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
429
+ expect(collection_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html"
430
+ end
431
+ end
432
+
433
+ context "with categories" do
434
+ let(:overrides) do
435
+ {
436
+ "collections" => {
437
+ "collection" => {
438
+ "output" => true,
439
+ },
440
+ },
441
+ "feed" => {
442
+ "collections" => {
443
+ "collection" => {
444
+ "categories" => ["news"],
445
+ },
446
+ },
447
+ },
448
+ }
449
+ end
450
+ let(:news_feed) { File.read(dest_dir("feed/collection/news.xml")) }
451
+
452
+ it "outputs the collection category feed" do
453
+ expect(news_feed).to match '<title type="html">My awesome site | Collection | News</title>'
454
+ expect(news_feed).to match "http://example.org/collection/2018-01-02-collection-category-doc.html"
455
+ expect(news_feed).to_not match "http://example.org/collection/2018-01-01-collection-doc.html"
456
+ expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
457
+ expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html"
458
+ end
459
+ end
460
+
461
+ context "with a custom path" do
462
+ let(:overrides) do
463
+ {
464
+ "collections" => {
465
+ "collection" => {
466
+ "output" => true,
467
+ },
468
+ },
469
+ "feed" => {
470
+ "collections" => {
471
+ "collection" => {
472
+ "categories" => ["news"],
473
+ "path" => "custom.xml",
474
+ },
475
+ },
476
+ },
477
+ }
478
+ end
479
+
480
+ it "should write to the custom path" do
481
+ expect(Pathname.new(dest_dir("custom.xml"))).to exist
482
+ expect(Pathname.new(dest_dir("feed/collection.xml"))).to_not exist
483
+ expect(Pathname.new(dest_dir("feed/collection/news.xml"))).to exist
484
+ end
485
+ end
486
+ end
487
+
488
+ context "excerpt_only flag" do
489
+ context "backward compatibility for no excerpt_only flag" do
490
+ it "should be in contents" do
491
+ expect(contents).to match '<content '
492
+ end
493
+ end
494
+
495
+ context "when site.excerpt_only flag is true" do
496
+ let(:overrides) do
497
+ { "feed" => { "excerpt_only" => true } }
498
+ end
499
+
500
+ it "should not set any contents" do
501
+ expect(contents).to_not match '<content '
502
+ end
503
+ end
504
+
505
+ context "when site.excerpt_only flag is false" do
506
+ let(:overrides) do
507
+ { "feed" => { "excerpt_only" => false } }
508
+ end
509
+
510
+ it "should be in contents" do
511
+ expect(contents).to match '<content '
512
+ end
513
+ end
514
+
515
+ context "when post.excerpt_only flag is true" do
516
+ let(:overrides) do
517
+ { "feed" => { "excerpt_only" => false } }
518
+ end
519
+
520
+ it "should not be in contents" do
521
+ expect(contents).to_not match "This content should not be in feed.</content>"
522
+ end
523
+ end
524
+ end
525
+ end