prt 0.3.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.
- checksums.yaml +7 -0
- data/.githooks/pre-commit +17 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.rubocop.yml +28 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +8 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +340 -0
- data/Rakefile +26 -0
- data/bin/setup +11 -0
- data/exe/parrot +12 -0
- data/lib/helpers.rb +19 -0
- data/lib/parrot/commands/build.rb +1274 -0
- data/lib/parrot/commands/new.rb +25 -0
- data/lib/parrot/commands/post.rb +83 -0
- data/lib/parrot/commands/serve.rb +129 -0
- data/lib/parrot/constants.rb +45 -0
- data/lib/parrot/file_cache.rb +45 -0
- data/lib/parrot/logger.rb +23 -0
- data/lib/parrot/metadata.rb +4 -0
- data/lib/parrot/runner.rb +29 -0
- data/lib/parrot/template_handler.rb +19 -0
- data/lib/parrot.rb +120 -0
- data/parrot.gemspec +36 -0
- data/skel/.gitignore +3 -0
- data/skel/config.yaml +68 -0
- data/skel/css/app.scss +189 -0
- data/skel/images/.keep +0 -0
- data/skel/images/apple-touch-icon.png +0 -0
- data/skel/images/favicon.ico +0 -0
- data/skel/images/favicon.svg +40 -0
- data/skel/images/parrot.jpeg +0 -0
- data/skel/javascripts/app.js +37 -0
- data/skel/public/.keep +0 -0
- data/skel/views/404.md +7 -0
- data/skel/views/about.md +17 -0
- data/skel/views/layout.html.erb +72 -0
- data/skel/views/posts/about_parrot.md +63 -0
- data/skel/views/posts/sample.md +37 -0
- data/spec/parrot/commands/build_spec.rb +747 -0
- data/spec/parrot/commands/new_spec.rb +43 -0
- data/spec/parrot/commands/post_spec.rb +80 -0
- data/spec/parrot/commands/serve_spec.rb +66 -0
- data/spec/parrot/logger_spec.rb +39 -0
- data/spec/parrot/metadata_spec.rb +11 -0
- data/spec/parrot/runner_spec.rb +28 -0
- data/spec/parrot_spec.rb +37 -0
- data/spec/spec_helper.rb +7 -0
- metadata +237 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Parrot::Commands do
|
|
4
|
+
let(:config) { Parrot::Config.new(Dir.pwd, TestLogger) }
|
|
5
|
+
|
|
6
|
+
before do
|
|
7
|
+
# create a new application
|
|
8
|
+
Parrot::Commands::NewCommand.new(%w[blog], config).run
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
after do
|
|
12
|
+
# cleanup
|
|
13
|
+
FileUtils.rm_rf('blog')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context 'BuildCommand' do
|
|
17
|
+
it 'has a run method' do
|
|
18
|
+
expect(Parrot::Commands::BuildCommand.new([], config)).to respond_to(:run)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context 'per-page head metadata' do
|
|
22
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), TestLogger) }
|
|
23
|
+
|
|
24
|
+
before { Parrot::Commands::BuildCommand.new([], build_config).run }
|
|
25
|
+
|
|
26
|
+
it 'sets the <title> from the post header' do
|
|
27
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<title>About Parrot</title>')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'keeps the layout title on the index page' do
|
|
31
|
+
expect(File.read('blog/public/index.html')).to include('<title>Parrot</title>')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'points og:url and canonical at each generated page' do
|
|
35
|
+
post = File.read('blog/public/sample.html')
|
|
36
|
+
expect(post).to include('<meta property="og:url" content="https://example.com/sample.html">')
|
|
37
|
+
expect(post).to include('<link rel="canonical" href="https://example.com/sample.html">')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'points the index og:url and canonical at the site root' do
|
|
41
|
+
index = File.read('blog/public/index.html')
|
|
42
|
+
expect(index).to include('<meta property="og:url" content="https://example.com/">')
|
|
43
|
+
expect(index).to include('<link rel="canonical" href="https://example.com/">')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'sets og:title per page from the same source as <title>' do
|
|
47
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<meta property="og:title" content="About Parrot">')
|
|
48
|
+
expect(File.read('blog/public/index.html')).to include('<meta property="og:title" content="Parrot">')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'rewrites a relative og:image to an absolute URL and ships the file' do
|
|
52
|
+
expect(File.read('blog/public/about_parrot.html'))
|
|
53
|
+
.to include('<meta property="og:image" content="https://example.com/images/parrot.jpeg">')
|
|
54
|
+
expect(File.exist?('blog/public/images/parrot.jpeg')).to be true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'keeps the Twitter Card tag from the layout' do
|
|
58
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<meta name="twitter:card" content="summary_large_image">')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'marks posts as og:type article with an ISO article:published_time' do
|
|
62
|
+
post = File.read('blog/public/about_parrot.html')
|
|
63
|
+
expect(post).to include('<meta property="og:type" content="article">')
|
|
64
|
+
expect(post).to include('<meta property="article:published_time" content="2026-09-08">')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'keeps the index as og:type website with no article date' do
|
|
68
|
+
index = File.read('blog/public/index.html')
|
|
69
|
+
expect(index).to include('<meta property="og:type" content="website">')
|
|
70
|
+
expect(index).not_to include('article:published_time')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'writes a sitemap.xml covering the index and every post' do
|
|
74
|
+
sitemap = File.read('blog/public/sitemap.xml')
|
|
75
|
+
expect(sitemap).to start_with('<?xml version="1.0" encoding="UTF-8"?>')
|
|
76
|
+
expect(sitemap).to include('<loc>https://example.com/</loc>')
|
|
77
|
+
expect(sitemap).to include('<loc>https://example.com/about_parrot.html</loc>')
|
|
78
|
+
expect(sitemap).to include('<loc>https://example.com/sample.html</loc>')
|
|
79
|
+
expect(sitemap).to include('<lastmod>2026-09-08</lastmod>')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'writes a robots.txt pointing at the sitemap' do
|
|
83
|
+
robots = File.read('blog/public/robots.txt')
|
|
84
|
+
expect(robots).to include('User-agent: *')
|
|
85
|
+
expect(robots).to include('Sitemap: https://example.com/sitemap.xml')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'sets <html lang> from the post header' do
|
|
89
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<html lang="en">')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'loads app.js with defer and warms up the CDNs' do
|
|
93
|
+
head = File.read('blog/public/about_parrot.html')
|
|
94
|
+
expect(head).to include('<script src="app.js" defer>')
|
|
95
|
+
expect(head).to include('<link rel="preconnect" href="https://cdn.simplecss.org"')
|
|
96
|
+
expect(head).to include('<link rel="preconnect" href="https://cdn.jsdelivr.net"')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'ships every icon referenced from the layout' do
|
|
100
|
+
expect(File.exist?('blog/public/images/favicon.ico')).to be true
|
|
101
|
+
expect(File.exist?('blog/public/images/favicon.svg')).to be true
|
|
102
|
+
expect(File.exist?('blog/public/images/apple-touch-icon.png')).to be true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'fills description, og:description and twitter:description from the first paragraph' do
|
|
106
|
+
head = File.read('blog/public/about_parrot.html')
|
|
107
|
+
summary = 'Parrot turns a folder of Markdown into a static blog.'
|
|
108
|
+
expect(head).to include(%(<meta name="description" content="#{summary}))
|
|
109
|
+
expect(head).to include(%(<meta property="og:description" content="#{summary}))
|
|
110
|
+
expect(head).to include(%(<meta name="twitter:description" content="#{summary}))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'maps lang onto og:locale' do
|
|
114
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<meta property="og:locale" content="en_US">')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'renders exactly one <h1> per page' do
|
|
118
|
+
%w[about_parrot sample].each do |name|
|
|
119
|
+
expect(File.read("blog/public/#{name}.html").scan('<h1').size).to eq(1)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'links back to the index from the top of each post, but not from the index itself' do
|
|
124
|
+
post = File.read('blog/public/about_parrot.html')
|
|
125
|
+
expect(post).to match(%r{<main>\s*<p class="back-link"><a href="index\.html">.*?</a></p>})
|
|
126
|
+
|
|
127
|
+
expect(File.read('blog/public/index.html')).not_to include('back-link')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'embeds BlogPosting JSON-LD on posts' do
|
|
131
|
+
data = JSON.parse(File.read('blog/public/about_parrot.html')[%r{<script type="application/ld\+json">(.+?)</script>}m, 1])
|
|
132
|
+
expect(data['@type']).to eq('BlogPosting')
|
|
133
|
+
expect(data['headline']).to eq('About Parrot')
|
|
134
|
+
expect(data['datePublished']).to eq('2026-09-08')
|
|
135
|
+
expect(data['author']['name']).to eq('Your name')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'embeds WebSite JSON-LD on the index' do
|
|
139
|
+
data = JSON.parse(File.read('blog/public/index.html')[%r{<script type="application/ld\+json">(.+?)</script>}m, 1])
|
|
140
|
+
expect(data['@type']).to eq('WebSite')
|
|
141
|
+
expect(data['url']).to eq('https://example.com/')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'writes an RSS feed and links it for autodiscovery' do
|
|
145
|
+
feed = File.read('blog/public/feed.xml')
|
|
146
|
+
expect(feed).to include('<rss version="2.0"')
|
|
147
|
+
expect(feed).to include('<link>https://example.com/about_parrot.html</link>')
|
|
148
|
+
expect(feed).to include('<pubDate>Tue, 08 Sep 2026 00:00:00 -0000</pubDate>')
|
|
149
|
+
expect(File.read('blog/public/about_parrot.html'))
|
|
150
|
+
.to include('<link rel="alternate" type="application/rss+xml" title="Parrot" href="https://example.com/feed.xml">')
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'emits og:image:width/height read from the image file' do
|
|
154
|
+
head = File.read('blog/public/about_parrot.html')
|
|
155
|
+
expect(head).to include('<meta property="og:image:width" content="1024">')
|
|
156
|
+
expect(head).to include('<meta property="og:image:height" content="1024">')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'gives the sitemap index entry the newest post date as <lastmod>' do
|
|
160
|
+
sitemap = File.read('blog/public/sitemap.xml')
|
|
161
|
+
expect(sitemap).to match(%r{<loc>https://example\.com/</loc>\s*<lastmod>2026-09-08</lastmod>})
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'builds a noindex 404 page with a parrot image, outside the sitemap and feed' do
|
|
165
|
+
page = File.read('blog/public/404.html')
|
|
166
|
+
expect(page).to include('<title>Page not found</title>')
|
|
167
|
+
expect(page).to include('<meta name="robots" content="noindex">')
|
|
168
|
+
expect(page).to include('images/parrot.jpeg')
|
|
169
|
+
expect(page.scan('<h1').size).to eq(1)
|
|
170
|
+
expect(File.exist?('blog/public/images/parrot.jpeg')).to be true
|
|
171
|
+
|
|
172
|
+
expect(File.read('blog/public/sitemap.xml')).not_to include('404.html')
|
|
173
|
+
expect(File.read('blog/public/feed.xml')).not_to include('404.html')
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
context 'per-post language' do
|
|
178
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
179
|
+
|
|
180
|
+
it 'uses the post header lang for that page only' do
|
|
181
|
+
File.write('blog/views/posts/story.md', "<!--\ntitle: കഥ\nlang: ml\n-->\n\n# കഥ\n")
|
|
182
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
183
|
+
|
|
184
|
+
expect(File.read('blog/public/story.html')).to include('<html lang="ml">')
|
|
185
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<html lang="en">')
|
|
186
|
+
expect(File.read('blog/public/index.html')).to include('<html lang="en">')
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
context 'draft posts' do
|
|
191
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
192
|
+
|
|
193
|
+
it 'builds draft posts while editing posts' do
|
|
194
|
+
File.write('blog/views/posts/story.md', "<!--\ntitle: Story\ndraft: true\n-->\n\n# story\n")
|
|
195
|
+
builder = Parrot::Commands::BuildCommand.new([], build_config)
|
|
196
|
+
builder.unset_build_mode!
|
|
197
|
+
builder.run
|
|
198
|
+
|
|
199
|
+
expect(File.read('blog/public/story.html')).to include('<html lang="en">')
|
|
200
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<html lang="en">')
|
|
201
|
+
expect(File.read('blog/public/index.html')).to include('<html lang="en">')
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'skip draft posts while building posts' do
|
|
205
|
+
File.write('blog/views/posts/story.md', "<!--\ntitle: Story\ndraft: true\n-->\n\n# story\n")
|
|
206
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
207
|
+
|
|
208
|
+
expect(File.exist?('blog/public/story.html')).to be false
|
|
209
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<html lang="en">')
|
|
210
|
+
expect(File.read('blog/public/index.html')).to include('<html lang="en">')
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'leaves skipped drafts out of the index listing, sitemap and feed' do
|
|
214
|
+
File.write('blog/views/posts/story.md', "<!--\ntitle: Story\ndate: 01/10/2026\ndraft: true\n-->\n\n# story\n")
|
|
215
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
216
|
+
|
|
217
|
+
%w[index.html sitemap.xml feed.xml].each do |name|
|
|
218
|
+
expect(File.read("blog/public/#{name}")).not_to include('story.html')
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'lists drafts in the index listing, sitemap and feed while editing, since they are built' do
|
|
223
|
+
File.write('blog/views/posts/story.md', "<!--\ntitle: Story\ndate: 01/10/2026\ndraft: true\n-->\n\n# story\n")
|
|
224
|
+
builder = Parrot::Commands::BuildCommand.new([], build_config)
|
|
225
|
+
builder.unset_build_mode!
|
|
226
|
+
builder.run
|
|
227
|
+
|
|
228
|
+
%w[index.html sitemap.xml feed.xml].each do |name|
|
|
229
|
+
expect(File.read("blog/public/#{name}")).to include('story.html')
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
context 'post tags' do
|
|
235
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
236
|
+
|
|
237
|
+
def build_story(header)
|
|
238
|
+
File.write('blog/views/posts/story.md', "<!--\ntitle: Story\ndate: 01/01/2012\n#{header}-->\n\n# story\n\nSome text.\n")
|
|
239
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
240
|
+
Nokogiri::HTML(File.read('blog/public/story.html'))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'lists the header tags at the bottom of the post' do
|
|
244
|
+
html = build_story("tags: algorithms, coding\n")
|
|
245
|
+
|
|
246
|
+
last = html.at('main').element_children.last
|
|
247
|
+
expect(last.name).to eq('p')
|
|
248
|
+
expect(last['class']).to eq('post-tags')
|
|
249
|
+
expect(last.css('span.tag').map(&:text)).to eq(%w[algorithms coding])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it 'trims tags and drops blanks and duplicates' do
|
|
253
|
+
html = build_story("tags: a, , b ,a\n")
|
|
254
|
+
|
|
255
|
+
expect(html.css('.post-tags span.tag').map(&:text)).to eq(%w[a b])
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'adds nothing to posts without tags or to the index' do
|
|
259
|
+
html = build_story('')
|
|
260
|
+
|
|
261
|
+
expect(html.at('.post-tags')).to be_nil
|
|
262
|
+
expect(File.read('blog/public/index.html')).not_to include('post-tags')
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it 'emits article:tag metas and JSON-LD keywords' do
|
|
266
|
+
html = build_story("tags: algorithms, coding\n")
|
|
267
|
+
|
|
268
|
+
expect(html.css('head meta[property="article:tag"]').map { |m| m['content'] }).to eq(%w[algorithms coding])
|
|
269
|
+
json = JSON.parse(html.at('script[type="application/ld+json"]').text)
|
|
270
|
+
expect(json['keywords']).to eq('algorithms, coding')
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
context 'generated post index listing' do
|
|
275
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
276
|
+
|
|
277
|
+
it 'lists posts newest first using the default format, with no views/index.md' do
|
|
278
|
+
expect(File.exist?('blog/views/index.md')).to be false
|
|
279
|
+
|
|
280
|
+
File.write('blog/views/posts/oldest.md', "<!--\ntitle: Oldest\ndate: 01/01/2020\n-->\n\n# Oldest\n")
|
|
281
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
282
|
+
|
|
283
|
+
index = File.read('blog/public/index.html')
|
|
284
|
+
about_pos = index.index('about_parrot.html')
|
|
285
|
+
sample_pos = index.index('sample.html')
|
|
286
|
+
oldest_pos = index.index('oldest.html')
|
|
287
|
+
|
|
288
|
+
# the default list_format's {post_date} goes through
|
|
289
|
+
# post_date_format.on_list ("%m/%Y" by default), not the raw header date
|
|
290
|
+
expect(index).to include('09/2026')
|
|
291
|
+
expect(index).to include('01/2020')
|
|
292
|
+
expect([about_pos, sample_pos]).to all(be < oldest_pos)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it 'titles the index page from config.yaml post_listing.list_title' do
|
|
296
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
297
|
+
expect(File.read('blog/public/index.html')).to include('<h3>Post listing</h3>')
|
|
298
|
+
|
|
299
|
+
File.write('blog/config.yaml', "post_listing:\n list_title: \"Latest writing\"\n")
|
|
300
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
301
|
+
|
|
302
|
+
expect(File.read('blog/public/index.html')).to include('<h3>Latest writing</h3>')
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it 'omits the index heading entirely when list_title is empty' do
|
|
306
|
+
File.write('blog/config.yaml', "post_listing:\n list_title: \"\"\n")
|
|
307
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
308
|
+
|
|
309
|
+
index = File.read('blog/public/index.html')
|
|
310
|
+
expect(index).not_to include('<h1')
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it 'uses config.yaml post_listing.back_link_text for the post back link' do
|
|
314
|
+
File.write('blog/config.yaml', "post_listing:\n back_link_text: \"Home\"\n")
|
|
315
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
316
|
+
|
|
317
|
+
expect(File.read('blog/public/about_parrot.html')).to include('<p class="back-link"><a href="index.html">Home</a></p>')
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it 'omits the post back link entirely when back_link_text is empty' do
|
|
321
|
+
File.write('blog/config.yaml', "post_listing:\n back_link_text: \"\"\n")
|
|
322
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
323
|
+
|
|
324
|
+
expect(File.read('blog/public/about_parrot.html')).not_to include('back-link')
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it 'honours a custom list_format from config.yaml' do
|
|
328
|
+
File.write('blog/config.yaml', "post_listing:\n list_format: \"{post_title} -- {%A, %B %d %Y}\"\n")
|
|
329
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
330
|
+
|
|
331
|
+
index = File.read('blog/public/index.html')
|
|
332
|
+
expect(index).to include('Tuesday, September 08 2026')
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it 'links only the title when {post_link} wraps just {post_title}' do
|
|
336
|
+
File.write('blog/config.yaml', "post_listing:\n list_format: \"{%d/%m} ~ [{post_title}]({post_link})\"\n")
|
|
337
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
338
|
+
|
|
339
|
+
index = File.read('blog/public/index.html')
|
|
340
|
+
expect(index).to include('08/09 ~ <a href="about_parrot.html">About Parrot</a>')
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it 'links the whole line when {post_link} wraps the entire format' do
|
|
344
|
+
File.write('blog/config.yaml', "post_listing:\n list_format: \"[{%d/%m} ~ {post_title}]({post_link})\"\n")
|
|
345
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
346
|
+
|
|
347
|
+
index = File.read('blog/public/index.html')
|
|
348
|
+
expect(index).to include('<a href="about_parrot.html">08/09 ~ About Parrot</a>')
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'exposes any custom header field as {post_<key>}' do
|
|
352
|
+
File.write('blog/config.yaml', "post_listing:\n list_format: \"{post_title} [{post_category}]\"\n")
|
|
353
|
+
File.write('blog/views/posts/story.md', "<!--\ntitle: A story\ncategory: Fiction\n-->\n\n# A story\n")
|
|
354
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
355
|
+
|
|
356
|
+
index = File.read('blog/public/index.html')
|
|
357
|
+
expect(index).to include('A story [Fiction]')
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it 'formats {post_date} in list_format per post_date_format.on_list' do
|
|
361
|
+
File.write('blog/config.yaml',
|
|
362
|
+
"post_listing:\n list_format: \"{post_date} ~ {post_title}\"\npost_date_format:\n on_list: \"%m/%Y\"\n")
|
|
363
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
364
|
+
|
|
365
|
+
index = File.read('blog/public/index.html')
|
|
366
|
+
expect(index).to include('09/2026 ~ About Parrot')
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it 'formats a literal {post_date} inside a post body per post_date_format.on_post' do
|
|
370
|
+
File.write('blog/config.yaml', "post_date_format:\n on_post: \"%A, %B %d %Y\"\n")
|
|
371
|
+
File.write('blog/views/posts/story.md', "<!--\ntitle: A story\ndate: 08/09/2026\n-->\n\n# A story\n\n_{post_date}_\n")
|
|
372
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
373
|
+
|
|
374
|
+
expect(File.read('blog/public/story.html')).to include('Tuesday, September 08 2026')
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it 'groups posts by year under a heading when group_by is year' do
|
|
378
|
+
File.write('blog/config.yaml', "post_listing:\n group_by: year\n")
|
|
379
|
+
File.write('blog/views/posts/old.md', "<!--\ntitle: Old\ndate: 01/01/2020\n-->\n\n# Old\n")
|
|
380
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
381
|
+
|
|
382
|
+
index = File.read('blog/public/index.html')
|
|
383
|
+
expect(index).to include('<h2>2026</h2>')
|
|
384
|
+
expect(index).to include('<h2>2020</h2>')
|
|
385
|
+
expect(index.index('2026</h2>')).to be < index.index('2020</h2>')
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
context 'post index pagination' do
|
|
390
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
391
|
+
|
|
392
|
+
it 'keeps every post on a single index.html when per_page is unset' do
|
|
393
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
394
|
+
expect(File.exist?('blog/public/index2.html')).to be false
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
it 'splits the listing across index.html, index2.html, etc. once per_page is exceeded' do
|
|
398
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
399
|
+
File.write('blog/views/posts/oldest.md', "<!--\ntitle: Oldest\ndate: 01/01/2020\n-->\n\n# Oldest\n")
|
|
400
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
401
|
+
|
|
402
|
+
expect(File.exist?('blog/public/index.html')).to be true
|
|
403
|
+
expect(File.exist?('blog/public/index2.html')).to be true
|
|
404
|
+
expect(File.exist?('blog/public/index3.html')).to be true
|
|
405
|
+
expect(File.exist?('blog/public/index4.html')).to be false
|
|
406
|
+
expect(File.read('blog/public/index3.html')).to include('oldest.html')
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
it 'links older/newer pages with the default pager text' do
|
|
410
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
411
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
412
|
+
|
|
413
|
+
page1 = File.read('blog/public/index.html')
|
|
414
|
+
expect(page1).to include('<a href="index2.html">Older posts →</a>')
|
|
415
|
+
expect(page1).not_to include('Newer posts')
|
|
416
|
+
|
|
417
|
+
page2 = File.read('blog/public/index2.html')
|
|
418
|
+
expect(page2).to include('<a href="index.html">← Newer posts</a>')
|
|
419
|
+
expect(page2).not_to include('Older posts')
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
it 'honours custom newer_link_text/older_link_text from config.yaml' do
|
|
423
|
+
File.write('blog/config.yaml',
|
|
424
|
+
"post_listing:\n per_page: 1\n newer_link_text: \"Prev\"\n older_link_text: \"Next\"\n")
|
|
425
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
426
|
+
|
|
427
|
+
expect(File.read('blog/public/index.html')).to include('>Next<')
|
|
428
|
+
expect(File.read('blog/public/index2.html')).to include('>Prev<')
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it 'omits the pager entirely when the only applicable link text is set to empty' do
|
|
432
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n older_link_text: \"\"\n")
|
|
433
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
434
|
+
|
|
435
|
+
expect(File.read('blog/public/index.html')).not_to include('pagination')
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
it 'omits the list_title heading on pages after the first' do
|
|
439
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
440
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
441
|
+
|
|
442
|
+
expect(File.read('blog/public/index.html')).to include('<h3')
|
|
443
|
+
expect(File.read('blog/public/index2.html')).not_to include('<h3')
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it "points a pushed-down post's back-link at the index page it actually appears on" do
|
|
447
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
448
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
449
|
+
|
|
450
|
+
main_html = File.read('blog/public/index2.html')[%r{<main>.*?</main>}m]
|
|
451
|
+
pushed_post = main_html[/href="([^"]+\.html)"/, 1]
|
|
452
|
+
expect(File.read("blog/public/#{pushed_post}"))
|
|
453
|
+
.to include(%(<a href="index2.html">← Back to all posts</a>))
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it 'removes a stale trailing index page once the post count drops back below per_page' do
|
|
457
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
458
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
459
|
+
expect(File.exist?('blog/public/index2.html')).to be true
|
|
460
|
+
|
|
461
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 10\n")
|
|
462
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
463
|
+
expect(File.exist?('blog/public/index2.html')).to be false
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
it 'lists every index page in the sitemap' do
|
|
467
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
468
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
469
|
+
|
|
470
|
+
sitemap = File.read('blog/public/sitemap.xml')
|
|
471
|
+
expect(sitemap).to include('<loc>https://example.com/</loc>')
|
|
472
|
+
expect(sitemap).to include('<loc>https://example.com/index2.html</loc>')
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
it 'rewrites the sitemap when the watcher rebuilds after per_page changes in config.yaml' do
|
|
476
|
+
command = Parrot::Commands::BuildCommand.new([], build_config)
|
|
477
|
+
command.run
|
|
478
|
+
expect(File.read('blog/public/sitemap.xml')).not_to include('index2.html')
|
|
479
|
+
|
|
480
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
481
|
+
command.build('config.yaml')
|
|
482
|
+
|
|
483
|
+
expect(File.read('blog/public/sitemap.xml')).to include('<loc>https://example.com/index2.html</loc>')
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
it "updates a pushed post's back-link when the watcher rebuilds after a new post shifts pagination" do
|
|
487
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
488
|
+
command = Parrot::Commands::BuildCommand.new([], build_config)
|
|
489
|
+
command.run
|
|
490
|
+
|
|
491
|
+
new_post = 'blog/views/posts/newest.md'
|
|
492
|
+
File.write(new_post, "<!--\ntitle: Newest\ndate: 01/01/2027\n-->\n\n# Newest\n")
|
|
493
|
+
command.build(new_post)
|
|
494
|
+
|
|
495
|
+
main_html = File.read('blog/public/index2.html')[%r{<main>.*?</main>}m]
|
|
496
|
+
pushed_post = main_html[/href="([^"]+\.html)"/, 1]
|
|
497
|
+
expect(File.read("blog/public/#{pushed_post}")).to include('<a href="index2.html">')
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
context 'about page' do
|
|
502
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
503
|
+
let(:command) { Parrot::Commands::BuildCommand.new([], build_config) }
|
|
504
|
+
|
|
505
|
+
before { command.run }
|
|
506
|
+
|
|
507
|
+
it 'builds about.html with its own title, description and social links' do
|
|
508
|
+
page = File.read('blog/public/about.html')
|
|
509
|
+
expect(page).to include('<title>About</title>')
|
|
510
|
+
expect(page).to include('<meta name="description" content="Who writes this blog, and where else to find them.">')
|
|
511
|
+
expect(page).to include('<meta property="og:url" content="https://example.com/about.html">')
|
|
512
|
+
expect(page).to include('href="https://github.com/your-username"')
|
|
513
|
+
expect(page.scan('<h1').size).to eq(1)
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
it 'is an AboutPage, not an article' do
|
|
517
|
+
page = File.read('blog/public/about.html')
|
|
518
|
+
expect(page).to include('<meta property="og:type" content="website">')
|
|
519
|
+
expect(page).not_to include('article:published_time')
|
|
520
|
+
expect(page).to include('"@type": "AboutPage"')
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
it 'is linked from the nav on every page' do
|
|
524
|
+
%w[index.html sample.html about.html 404.html].each do |name|
|
|
525
|
+
nav = File.read("blog/public/#{name}")[%r{<nav class="site-nav">.*?</nav>}m]
|
|
526
|
+
expect(nav).to include('<a href="/about.html">About</a>')
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
it 'is in the sitemap but not the index listing or the feed' do
|
|
531
|
+
expect(File.read('blog/public/sitemap.xml')).to include('<loc>https://example.com/about.html</loc>')
|
|
532
|
+
expect(File.read('blog/public/index.html')[%r{<main>.*?</main>}m]).not_to include('about.html')
|
|
533
|
+
expect(File.read('blog/public/feed.xml')).not_to include('about.html')
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
it 'is rebuilt by the watcher when views/about.md changes' do
|
|
537
|
+
File.write('blog/views/about.md', "# About me\n\nSomething new about the author of this blog.\n")
|
|
538
|
+
command.build('views/about.md')
|
|
539
|
+
|
|
540
|
+
page = File.read('blog/public/about.html')
|
|
541
|
+
expect(page).to include('<title>About me</title>')
|
|
542
|
+
expect(page).to include('Something new about the author of this blog.')
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
it 'is removed, along with its sitemap entry, when views/about.md is deleted' do
|
|
546
|
+
File.delete('blog/views/about.md')
|
|
547
|
+
command.build('views/about.md')
|
|
548
|
+
|
|
549
|
+
expect(File.exist?('blog/public/about.html')).to be false
|
|
550
|
+
expect(File.read('blog/public/sitemap.xml')).not_to include('about.html')
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
context 'post categories' do
|
|
555
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
556
|
+
let(:command) { Parrot::Commands::BuildCommand.new([], build_config) }
|
|
557
|
+
|
|
558
|
+
def write_post(name, title, date, category = nil)
|
|
559
|
+
header = "title: #{title}\ndate: #{date}\n"
|
|
560
|
+
header << "category: #{category}\n" if category
|
|
561
|
+
File.write("blog/views/posts/#{name}.md", "<!--\n#{header}-->\n\n# #{title}\n\nBody text.\n")
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def main_of(file)
|
|
565
|
+
File.read("blog/public/#{file}")[%r{<main>.*?</main>}m]
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
before do
|
|
569
|
+
write_post('ruby-old', 'Old Ruby', '01/01/2024', 'Ruby')
|
|
570
|
+
write_post('ruby-new', 'New Ruby', '01/01/2025', 'Ruby')
|
|
571
|
+
write_post('web', 'Web things', '01/06/2025', 'Web Dev')
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
it 'lists only that category\'s posts, newest first, under its name' do
|
|
575
|
+
command.run
|
|
576
|
+
main = main_of('category-ruby.html')
|
|
577
|
+
|
|
578
|
+
expect(main).to include('<h3>Ruby</h3>')
|
|
579
|
+
expect(main).not_to include('web.html')
|
|
580
|
+
expect(main).not_to include('sample.html')
|
|
581
|
+
expect(main.index('ruby-new.html')).to be < main.index('ruby-old.html')
|
|
582
|
+
expect(File.read('blog/public/category-ruby.html')).to include('<title>Ruby posts</title>')
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
it 'names pages after the slugified category' do
|
|
586
|
+
command.run
|
|
587
|
+
expect(File.exist?('blog/public/category-web-dev.html')).to be true
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
it 'paginates a category past per_page, with a pager between its pages' do
|
|
591
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
592
|
+
command.run
|
|
593
|
+
|
|
594
|
+
page1 = main_of('category-ruby.html')
|
|
595
|
+
page2 = main_of('category-ruby_2.html')
|
|
596
|
+
expect(page1).to include('ruby-new.html')
|
|
597
|
+
expect(page1).to include('<a href="category-ruby_2.html">Older posts →</a>')
|
|
598
|
+
expect(page2).to include('ruby-old.html')
|
|
599
|
+
expect(page2).to include('<a href="category-ruby.html">← Newer posts</a>')
|
|
600
|
+
expect(page2).to include('<h3>Ruby</h3>')
|
|
601
|
+
expect(File.exist?('blog/public/category-ruby_3.html')).to be false
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
it 'lists every category with its post count on categories.html' do
|
|
605
|
+
command.run
|
|
606
|
+
main = main_of('categories.html')
|
|
607
|
+
|
|
608
|
+
expect(main).to include('<a href="category-guides.html">Guides</a> (2)')
|
|
609
|
+
expect(main).to include('<a href="category-ruby.html">Ruby</a> (2)')
|
|
610
|
+
expect(main).to include('<a href="category-web-dev.html">Web Dev</a> (1)')
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
it 'still writes categories.html when no post has a category' do
|
|
614
|
+
Dir['blog/views/posts/*.md'].each { |path| File.write(path, File.read(path).gsub(/^category:.*\n/, '')) }
|
|
615
|
+
command.run
|
|
616
|
+
|
|
617
|
+
expect(main_of('categories.html')).to include('No categories yet.')
|
|
618
|
+
expect(Dir['blog/public/category-*.html']).to be_empty
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
it 'puts the date and a category link right after the post title' do
|
|
622
|
+
command.run
|
|
623
|
+
expect(main_of('ruby-new.html')).to include(
|
|
624
|
+
'<h1>New Ruby</h1>' \
|
|
625
|
+
"\n" \
|
|
626
|
+
'<p class="post-meta"><time datetime="2025-01-01">01/01/2025</time> · ' \
|
|
627
|
+
'<a class="category-tag" href="category-ruby.html">Ruby</a></p>'
|
|
628
|
+
)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
it 'shows just the date for a post without a category, and nothing without either' do
|
|
632
|
+
write_post('plain', 'Plain', '01/02/2025')
|
|
633
|
+
File.write('blog/views/posts/bare.md', "# Bare\n\nBody text.\n")
|
|
634
|
+
command.run
|
|
635
|
+
|
|
636
|
+
expect(main_of('plain.html')).to include('<p class="post-meta"><time datetime="2025-02-01">01/02/2025</time></p>')
|
|
637
|
+
expect(main_of('bare.html')).not_to include('post-meta')
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
it 'replaces an old _{post_date}_ line instead of showing the date twice' do
|
|
641
|
+
File.write('blog/views/posts/story.md',
|
|
642
|
+
"<!--\ntitle: A story\ndate: 08/09/2026\ncategory: Fiction\n-->\n\n# A story\n\n_{post_date}_\n\nBody.\n")
|
|
643
|
+
command.run
|
|
644
|
+
|
|
645
|
+
# the header comment is copied into <main> as-is, so leave it out
|
|
646
|
+
main = main_of('story.html').gsub(/<!--.*?-->/m, '')
|
|
647
|
+
expect(main.scan('08/09/2026').length).to eq(1)
|
|
648
|
+
expect(main).to include('class="category-tag" href="category-fiction.html"')
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
it 'shows a category tag in the index listing by default' do
|
|
652
|
+
command.run
|
|
653
|
+
expect(main_of('index.html')).to include('<a href="category-ruby.html" class="category-tag">Ruby</a>')
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
it 'leaves the tag out when list_format drops {post_category_tag}' do
|
|
657
|
+
File.write('blog/config.yaml', "post_listing:\n list_format: \"[{post_title}]({post_link})\"\n")
|
|
658
|
+
command.run
|
|
659
|
+
expect(main_of('index.html')).not_to include('category-tag')
|
|
660
|
+
expect(main_of('category-ruby.html')).not_to include('category-tag')
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it 'lists categories.html and every category page in the sitemap' do
|
|
664
|
+
File.write('blog/config.yaml', "post_listing:\n per_page: 1\n")
|
|
665
|
+
command.run
|
|
666
|
+
|
|
667
|
+
sitemap = File.read('blog/public/sitemap.xml')
|
|
668
|
+
expect(sitemap).to include('<loc>https://example.com/categories.html</loc>')
|
|
669
|
+
expect(sitemap).to include("<loc>https://example.com/category-ruby.html</loc>\n <lastmod>2025-01-01</lastmod>")
|
|
670
|
+
expect(sitemap).to include('<loc>https://example.com/category-ruby_2.html</loc>')
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
it 'removes a category\'s pages once the watcher sees its last post move away' do
|
|
674
|
+
command.run
|
|
675
|
+
write_post('web', 'Web things', '01/06/2025', 'Ruby')
|
|
676
|
+
command.build('views/posts/web.md')
|
|
677
|
+
|
|
678
|
+
expect(File.exist?('blog/public/category-web-dev.html')).to be false
|
|
679
|
+
expect(main_of('category-ruby.html')).to include('web.html')
|
|
680
|
+
expect(main_of('categories.html')).not_to include('Web Dev')
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
it 'shares one page between names that slugify the same' do
|
|
684
|
+
write_post('cpp', 'C plus plus', '01/03/2025', 'Web-Dev')
|
|
685
|
+
command.run
|
|
686
|
+
main = main_of('category-web-dev.html')
|
|
687
|
+
|
|
688
|
+
expect(main).to include('web.html')
|
|
689
|
+
expect(main).to include('cpp.html')
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
it 'fails the build when a post is named like a category page' do
|
|
693
|
+
File.write('blog/views/posts/category-ruby.md', "# Clash\n")
|
|
694
|
+
expect { command.run }.to raise_error(%r{Reserved post filename: views/posts/category-ruby\.md})
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
it 'builds a post named category-<something> when no such category exists' do
|
|
698
|
+
File.write('blog/views/posts/category-theory.md', "# Category theory\n")
|
|
699
|
+
expect { command.run }.not_to raise_error
|
|
700
|
+
expect(File.exist?('blog/public/category-theory.html')).to be true
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
context 'reserved post filenames' do
|
|
705
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
706
|
+
let(:command) { Parrot::Commands::BuildCommand.new([], build_config) }
|
|
707
|
+
|
|
708
|
+
%w[about.md 404.md index.md index3.md About.md now.md post.md posts.md note.md notes.md categories.md category.md].each do |name|
|
|
709
|
+
it "fails the build on views/posts/#{name} before writing anything" do
|
|
710
|
+
command.run
|
|
711
|
+
File.write("blog/views/posts/#{name}", "# Clash\n")
|
|
712
|
+
|
|
713
|
+
expect { command.run }.to raise_error(%r{Reserved post filename: views/posts/#{Regexp.escape(name)}})
|
|
714
|
+
expect(File.exist?('blog/public/index.html')).to be true
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
it 'fails the watcher rebuild when such a post appears' do
|
|
719
|
+
command.run
|
|
720
|
+
File.write('blog/views/posts/about.md', "# Clash\n")
|
|
721
|
+
|
|
722
|
+
expect { command.build('views/posts/about.md') }.to raise_error(/Reserved post filename/)
|
|
723
|
+
expect(File.read('blog/public/about.html')).to include('<title>About</title>')
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
it 'builds posts whose names only contain a reserved word' do
|
|
727
|
+
File.write('blog/views/posts/about-me.md', "# About me\n")
|
|
728
|
+
expect { command.run }.not_to raise_error
|
|
729
|
+
expect(File.exist?('blog/public/about-me.html')).to be true
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
context 'post header description' do
|
|
734
|
+
let(:build_config) { Parrot::Config.new(File.join(Dir.pwd, 'blog'), Logger.new(File::NULL)) }
|
|
735
|
+
|
|
736
|
+
it 'wins over the first-paragraph fallback' do
|
|
737
|
+
File.write('blog/views/posts/story.md',
|
|
738
|
+
"<!--\ntitle: A story\ndescription: Hand-written summary.\n-->\n\n# A story\n\nThe opening paragraph.\n")
|
|
739
|
+
Parrot::Commands::BuildCommand.new([], build_config).run
|
|
740
|
+
|
|
741
|
+
head = File.read('blog/public/story.html')
|
|
742
|
+
expect(head).to include('<meta name="description" content="Hand-written summary.">')
|
|
743
|
+
expect(head).to include('<meta property="og:description" content="Hand-written summary.">')
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
end
|