ottogen 0.1.0 → 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.
- checksums.yaml +4 -4
- data/README.md +59 -43
- data/bin/otto +1 -0
- data/lib/ottogen/collection.rb +35 -0
- data/lib/ottogen/collection_item.rb +61 -0
- data/lib/ottogen/config.rb +126 -0
- data/lib/ottogen/front_matter.rb +30 -0
- data/lib/ottogen/layout.rb +65 -0
- data/lib/ottogen/otto.rb +36 -15
- data/lib/ottogen/ottogen.rb +168 -28
- data/lib/ottogen/page.rb +58 -0
- data/lib/ottogen/permalink.rb +35 -0
- data/lib/ottogen/post.rb +108 -0
- data/lib/ottogen/scaffold.rb +50 -0
- data/spec/ottogen/collection_spec.rb +61 -0
- data/spec/ottogen/config_spec.rb +249 -0
- data/spec/ottogen/layout_spec.rb +150 -0
- data/spec/ottogen/ottogen_spec.rb +430 -5
- data/spec/ottogen/page_spec.rb +101 -0
- data/spec/ottogen/permalink_spec.rb +49 -0
- data/spec/ottogen/post_spec.rb +172 -0
- data/spec/spec_helper.rb +51 -0
- metadata +176 -57
|
@@ -1,9 +1,434 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
RSpec.describe Ottogen::Ottogen do
|
|
4
|
+
it 'is loaded' do
|
|
5
|
+
expect(defined?(Ottogen::Ottogen)).to eq('constant')
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe '.build' do
|
|
9
|
+
it 'passes site_title as an Asciidoctor attribute, resolvable in pages' do
|
|
10
|
+
in_otto_project(config: "title: My Otto Site\n") do
|
|
11
|
+
File.write('pages/index.adoc', "= Index\n\nMy site is {site_title}.\n")
|
|
12
|
+
|
|
13
|
+
capture_stdout { described_class.build }
|
|
14
|
+
|
|
15
|
+
expect(File.read('_build/index.html')).to include('My site is My Otto Site.')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'passes custom config keys as site_<key> attributes' do
|
|
20
|
+
in_otto_project(config: "title: T\nauthor: Ada Lovelace\n") do
|
|
21
|
+
File.write('pages/index.adoc', "= Index\n\nWritten by {site_author}.\n")
|
|
22
|
+
|
|
23
|
+
capture_stdout { described_class.build }
|
|
24
|
+
|
|
25
|
+
expect(File.read('_build/index.html')).to include('Written by Ada Lovelace.')
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'still succeeds when config.yml has only a title' do
|
|
30
|
+
in_otto_project(config: "title: Minimal\n") do
|
|
31
|
+
File.write('pages/index.adoc', "= Index\n\nHello.\n")
|
|
32
|
+
|
|
33
|
+
capture_stdout { described_class.build }
|
|
34
|
+
|
|
35
|
+
expect(File.exist?('_build/index.html')).to be true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'resolves {page_title} from front matter in rendered HTML' do
|
|
40
|
+
in_otto_project do
|
|
41
|
+
File.write('pages/index.adoc', <<~ADOC)
|
|
42
|
+
---
|
|
43
|
+
title: My Page
|
|
44
|
+
---
|
|
45
|
+
= Index
|
|
46
|
+
|
|
47
|
+
Welcome to {page_title}.
|
|
48
|
+
ADOC
|
|
49
|
+
|
|
50
|
+
capture_stdout { described_class.build }
|
|
51
|
+
|
|
52
|
+
expect(File.read('_build/index.html')).to include('Welcome to My Page.')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'resolves arbitrary front matter keys in rendered HTML' do
|
|
57
|
+
in_otto_project do
|
|
58
|
+
File.write('pages/index.adoc', <<~ADOC)
|
|
59
|
+
---
|
|
60
|
+
author: Ada Lovelace
|
|
61
|
+
---
|
|
62
|
+
= Index
|
|
63
|
+
|
|
64
|
+
Written by {page_author}.
|
|
65
|
+
ADOC
|
|
66
|
+
|
|
67
|
+
capture_stdout { described_class.build }
|
|
68
|
+
|
|
69
|
+
expect(File.read('_build/index.html')).to include('Written by Ada Lovelace.')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'does not include front matter delimiters in rendered HTML' do
|
|
74
|
+
in_otto_project do
|
|
75
|
+
File.write('pages/index.adoc', <<~ADOC)
|
|
76
|
+
---
|
|
77
|
+
title: My Page
|
|
78
|
+
---
|
|
79
|
+
= Index
|
|
80
|
+
|
|
81
|
+
Body.
|
|
82
|
+
ADOC
|
|
83
|
+
|
|
84
|
+
capture_stdout { described_class.build }
|
|
85
|
+
|
|
86
|
+
html = File.read('_build/index.html')
|
|
87
|
+
expect(html).not_to include('---')
|
|
88
|
+
expect(html).not_to include('title: My Page')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'still builds pages without front matter' do
|
|
93
|
+
in_otto_project do
|
|
94
|
+
File.write('pages/index.adoc', "= Plain\n\nNo front matter here.\n")
|
|
95
|
+
|
|
96
|
+
capture_stdout { described_class.build }
|
|
97
|
+
|
|
98
|
+
expect(File.read('_build/index.html')).to include('No front matter here.')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'wraps a page in its declared layout' do
|
|
103
|
+
in_otto_project do
|
|
104
|
+
FileUtils.mkdir_p('_layouts')
|
|
105
|
+
File.write('_layouts/default.html.erb', '<html><body><%= content %></body></html>')
|
|
106
|
+
File.write('pages/index.adoc', "---\nlayout: default\n---\n= Hi\n\nHello.\n")
|
|
107
|
+
|
|
108
|
+
capture_stdout { described_class.build }
|
|
109
|
+
|
|
110
|
+
html = File.read('_build/index.html')
|
|
111
|
+
expect(html).to start_with('<html><body>')
|
|
112
|
+
expect(html).to include('Hello.')
|
|
113
|
+
expect(html).to end_with('</body></html>')
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'renders collection items when output: true' do
|
|
118
|
+
in_otto_project(config: <<~YAML) do
|
|
119
|
+
title: T
|
|
120
|
+
collections:
|
|
121
|
+
recipes:
|
|
122
|
+
output: true
|
|
123
|
+
YAML
|
|
124
|
+
FileUtils.mkdir_p('_recipes')
|
|
125
|
+
File.write('_recipes/pizza.adoc', "= Pizza\n\nDough.\n")
|
|
126
|
+
|
|
127
|
+
capture_stdout { described_class.build }
|
|
128
|
+
|
|
129
|
+
expect(File.exist?('_build/recipes/pizza.html')).to be true
|
|
130
|
+
expect(File.read('_build/recipes/pizza.html')).to include('Dough.')
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'does not render collection items when output: false' do
|
|
135
|
+
in_otto_project(config: <<~YAML) do
|
|
136
|
+
title: T
|
|
137
|
+
collections:
|
|
138
|
+
books:
|
|
139
|
+
output: false
|
|
140
|
+
YAML
|
|
141
|
+
FileUtils.mkdir_p('_books')
|
|
142
|
+
File.write('_books/midnight.adoc', "= Midnight\n")
|
|
143
|
+
|
|
144
|
+
capture_stdout { described_class.build }
|
|
145
|
+
|
|
146
|
+
expect(File.exist?('_build/books/midnight.html')).to be false
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'excludes drafts by default' do
|
|
151
|
+
in_otto_project do
|
|
152
|
+
FileUtils.mkdir_p('_drafts')
|
|
153
|
+
File.write('_drafts/wip.adoc', "= WIP\n\nDraft body.\n")
|
|
154
|
+
|
|
155
|
+
capture_stdout { described_class.build }
|
|
156
|
+
|
|
157
|
+
expect(File.exist?('_build/wip.html')).to be false
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'renders drafts when build is called with drafts: true' do
|
|
162
|
+
in_otto_project do
|
|
163
|
+
FileUtils.mkdir_p('_drafts')
|
|
164
|
+
File.write('_drafts/wip.adoc', "= WIP\n\nDraft body.\n")
|
|
165
|
+
|
|
166
|
+
capture_stdout { described_class.build(drafts: true) }
|
|
167
|
+
|
|
168
|
+
expect(File.exist?('_build/wip.html')).to be true
|
|
169
|
+
expect(File.read('_build/wip.html')).to include('Draft body.')
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'honors a per-document permalink: in front matter' do
|
|
174
|
+
in_otto_project do
|
|
175
|
+
FileUtils.mkdir_p('_posts')
|
|
176
|
+
File.write('_posts/2026-01-15-hello.adoc', "---\npermalink: /custom/path.html\n---\nBody.\n")
|
|
177
|
+
|
|
178
|
+
capture_stdout { described_class.build }
|
|
179
|
+
|
|
180
|
+
expect(File.exist?('_build/custom/path.html')).to be true
|
|
181
|
+
expect(File.exist?('_build/hello.html')).to be false
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'applies a global permalink: from config.yml to posts with date tokens' do
|
|
186
|
+
in_otto_project(config: "title: T\npermalink: /:year/:month/:day/:slug/\n") do
|
|
187
|
+
FileUtils.mkdir_p('_posts')
|
|
188
|
+
File.write('_posts/2026-01-15-hello.adoc', "= Hello\n\nBody.\n")
|
|
189
|
+
|
|
190
|
+
capture_stdout { described_class.build }
|
|
191
|
+
|
|
192
|
+
expect(File.exist?('_build/2026/01/15/hello/index.html')).to be true
|
|
193
|
+
expect(File.read('_build/2026/01/15/hello/index.html')).to include('Body.')
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'does not apply the global permalink to pages' do
|
|
198
|
+
in_otto_project(config: "title: T\npermalink: /:year/:month/:day/:slug/\n") do
|
|
199
|
+
File.write('pages/about.adoc', "= About\n\nAbout body.\n")
|
|
200
|
+
|
|
201
|
+
capture_stdout { described_class.build }
|
|
202
|
+
|
|
203
|
+
expect(File.exist?('_build/about.html')).to be true
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'renders posts to _build/<slug>.html' do
|
|
208
|
+
in_otto_project do
|
|
209
|
+
FileUtils.mkdir_p('_posts')
|
|
210
|
+
File.write('_posts/2026-01-15-hello-world.adoc', "= Hello\n\nBody.\n")
|
|
211
|
+
|
|
212
|
+
capture_stdout { described_class.build }
|
|
213
|
+
|
|
214
|
+
expect(File.exist?('_build/hello-world.html')).to be true
|
|
215
|
+
expect(File.read('_build/hello-world.html')).to include('Body.')
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'exposes site.posts to layouts' do
|
|
220
|
+
in_otto_project do
|
|
221
|
+
FileUtils.mkdir_p('_layouts')
|
|
222
|
+
FileUtils.mkdir_p('_posts')
|
|
223
|
+
File.write('_posts/2026-01-15-only-post.adoc', "= Hi\n\nBody.\n")
|
|
224
|
+
File.write('_layouts/default.html.erb', '<nav><%= site.posts.first.title %></nav><%= content %>')
|
|
225
|
+
File.write('pages/index.adoc', "---\nlayout: default\n---\nIndex.\n")
|
|
226
|
+
|
|
227
|
+
capture_stdout { described_class.build }
|
|
228
|
+
|
|
229
|
+
html = File.read('_build/index.html')
|
|
230
|
+
expect(html).to include('<nav>Only Post</nav>')
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it 'exposes site.data.<file> in layouts' do
|
|
235
|
+
in_otto_project do
|
|
236
|
+
FileUtils.mkdir_p('_layouts')
|
|
237
|
+
FileUtils.mkdir_p('_data')
|
|
238
|
+
File.write('_data/nav.yml', "- title: Home\n url: /\n")
|
|
239
|
+
File.write('_layouts/default.html.erb', "<%= site.data.nav.first['title'] %>|<%= content %>")
|
|
240
|
+
File.write('pages/index.adoc', "---\nlayout: default\n---\nBody.\n")
|
|
241
|
+
|
|
242
|
+
capture_stdout { described_class.build }
|
|
243
|
+
|
|
244
|
+
html = File.read('_build/index.html')
|
|
245
|
+
expect(html).to include('Home|')
|
|
246
|
+
expect(html).to include('Body.')
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'embeds an include declared in the page layout' do
|
|
251
|
+
in_otto_project do
|
|
252
|
+
FileUtils.mkdir_p('_layouts')
|
|
253
|
+
FileUtils.mkdir_p('_includes')
|
|
254
|
+
File.write('_includes/header.html', '<header>Site Header</header>')
|
|
255
|
+
File.write('_layouts/default.html.erb', "<html><%= partial 'header.html' %><body><%= content %></body></html>")
|
|
256
|
+
File.write('pages/index.adoc', "---\nlayout: default\n---\n= Hi\n\nHello.\n")
|
|
257
|
+
|
|
258
|
+
capture_stdout { described_class.build }
|
|
259
|
+
|
|
260
|
+
html = File.read('_build/index.html')
|
|
261
|
+
expect(html).to include('<header>Site Header</header>')
|
|
262
|
+
expect(html).to include('Hello.')
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it 'chains layouts (post -> default)' do
|
|
267
|
+
in_otto_project do
|
|
268
|
+
FileUtils.mkdir_p('_layouts')
|
|
269
|
+
File.write('_layouts/default.html.erb', '<html><body><%= content %></body></html>')
|
|
270
|
+
File.write('_layouts/post.html.erb', "---\nlayout: default\n---\n<article><%= content %></article>")
|
|
271
|
+
File.write('pages/index.adoc', "---\nlayout: post\n---\n= Hi\n\nHello.\n")
|
|
272
|
+
|
|
273
|
+
capture_stdout { described_class.build }
|
|
274
|
+
|
|
275
|
+
html = File.read('_build/index.html')
|
|
276
|
+
expect(html).to include('<html><body><article>')
|
|
277
|
+
expect(html).to include('Hello.')
|
|
278
|
+
expect(html).to include('</article></body></html>')
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it 'still emits standalone HTML for pages without a layout' do
|
|
283
|
+
in_otto_project do
|
|
284
|
+
File.write('pages/index.adoc', "= Hi\n\nNo layout here.\n")
|
|
285
|
+
|
|
286
|
+
capture_stdout { described_class.build }
|
|
287
|
+
|
|
288
|
+
html = File.read('_build/index.html')
|
|
289
|
+
expect(html).to include('<!DOCTYPE html>')
|
|
290
|
+
expect(html).to include('No layout here.')
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'exits with a friendly error when a page references a missing layout' do
|
|
295
|
+
in_otto_project do
|
|
296
|
+
File.write('pages/index.adoc', "---\nlayout: missing\n---\n= Hi\n\nHello.\n")
|
|
297
|
+
|
|
298
|
+
buffer = StringIO.new
|
|
299
|
+
original = $stdout
|
|
300
|
+
$stdout = buffer
|
|
301
|
+
begin
|
|
302
|
+
expect { described_class.build }.to raise_error(SystemExit)
|
|
303
|
+
ensure
|
|
304
|
+
$stdout = original
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
expect(buffer.string).to include('❌')
|
|
308
|
+
expect(buffer.string).to include('missing')
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'exits with a friendly error when a page has malformed front matter' do
|
|
313
|
+
in_otto_project do
|
|
314
|
+
File.write('pages/index.adoc', "---\ntitle: 'unclosed\n---\nBody\n")
|
|
315
|
+
|
|
316
|
+
buffer = StringIO.new
|
|
317
|
+
original = $stdout
|
|
318
|
+
$stdout = buffer
|
|
319
|
+
begin
|
|
320
|
+
expect { described_class.build }.to raise_error(SystemExit)
|
|
321
|
+
ensure
|
|
322
|
+
$stdout = original
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
expect(buffer.string).to include('❌')
|
|
326
|
+
expect(buffer.string).to include('pages/index.adoc')
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
describe '.init' do
|
|
332
|
+
it 'writes a config.yml with title, description, url, and baseurl keys' do
|
|
333
|
+
in_tmp_dir do
|
|
334
|
+
capture_stdout { described_class.init('site') }
|
|
335
|
+
|
|
336
|
+
config = YAML.safe_load_file('site/config.yml')
|
|
337
|
+
expect(config.keys).to include('title', 'description', 'url', 'baseurl')
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
it 'scaffolds _layouts/default.html.erb with a starter template' do
|
|
342
|
+
in_tmp_dir do
|
|
343
|
+
capture_stdout { described_class.init('site') }
|
|
344
|
+
|
|
345
|
+
path = 'site/_layouts/default.html.erb'
|
|
346
|
+
expect(File.exist?(path)).to be true
|
|
347
|
+
expect(File.read(path)).to include('<%= content %>')
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'scaffolds an _includes/ directory' do
|
|
352
|
+
in_tmp_dir do
|
|
353
|
+
capture_stdout { described_class.init('site') }
|
|
354
|
+
|
|
355
|
+
expect(Dir.exist?('site/_includes')).to be true
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
it 'scaffolds a _data/ directory' do
|
|
360
|
+
in_tmp_dir do
|
|
361
|
+
capture_stdout { described_class.init('site') }
|
|
362
|
+
|
|
363
|
+
expect(Dir.exist?('site/_data')).to be true
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
it 'scaffolds a _posts/ directory' do
|
|
368
|
+
in_tmp_dir do
|
|
369
|
+
capture_stdout { described_class.init('site') }
|
|
370
|
+
|
|
371
|
+
expect(Dir.exist?('site/_posts')).to be true
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it 'scaffolds a _drafts/ directory' do
|
|
376
|
+
in_tmp_dir do
|
|
377
|
+
capture_stdout { described_class.init('site') }
|
|
378
|
+
|
|
379
|
+
expect(Dir.exist?('site/_drafts')).to be true
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
describe '.new_post' do
|
|
385
|
+
it 'creates _posts/<date>-<slug>.adoc with title and date front matter' do
|
|
386
|
+
in_otto_project do
|
|
387
|
+
capture_stdout { described_class.new_post('Hello World') }
|
|
388
|
+
|
|
389
|
+
expected = "_posts/#{Date.today.iso8601}-hello-world.adoc"
|
|
390
|
+
expect(File.exist?(expected)).to be true
|
|
391
|
+
body = File.read(expected)
|
|
392
|
+
expect(body).to include('title: Hello World')
|
|
393
|
+
expect(body).to include("date: #{Date.today.iso8601}")
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
it 'slugifies the title (lowercase, dashed)' do
|
|
398
|
+
in_otto_project do
|
|
399
|
+
capture_stdout { described_class.new_post('My Cool, Fancy Post!') }
|
|
400
|
+
|
|
401
|
+
expected = "_posts/#{Date.today.iso8601}-my-cool-fancy-post.adoc"
|
|
402
|
+
expect(File.exist?(expected)).to be true
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
describe '.doctor' do
|
|
408
|
+
it 'reports OK in a healthy project' do
|
|
409
|
+
in_otto_project do
|
|
410
|
+
output = capture_stdout { described_class.doctor }
|
|
411
|
+
|
|
412
|
+
expect(output).to include('✅')
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it 'exits non-zero and reports problems when required files are missing' do
|
|
417
|
+
in_tmp_dir do
|
|
418
|
+
FileUtils.touch('.otto')
|
|
419
|
+
# config.yml and pages/ deliberately missing
|
|
420
|
+
|
|
421
|
+
buffer = StringIO.new
|
|
422
|
+
original = $stdout
|
|
423
|
+
$stdout = buffer
|
|
424
|
+
begin
|
|
425
|
+
expect { described_class.doctor }.to raise_error(SystemExit)
|
|
426
|
+
ensure
|
|
427
|
+
$stdout = original
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
expect(buffer.string).to include('config.yml')
|
|
431
|
+
end
|
|
7
432
|
end
|
|
8
433
|
end
|
|
9
434
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Ottogen::Page do
|
|
4
|
+
describe '.read' do
|
|
5
|
+
it 'parses YAML front matter delimited by ---' do
|
|
6
|
+
in_tmp_dir do
|
|
7
|
+
File.write('post.adoc', <<~ADOC)
|
|
8
|
+
---
|
|
9
|
+
title: Hello
|
|
10
|
+
author: Ada
|
|
11
|
+
---
|
|
12
|
+
= Hello
|
|
13
|
+
|
|
14
|
+
Body.
|
|
15
|
+
ADOC
|
|
16
|
+
|
|
17
|
+
page = described_class.read('post.adoc')
|
|
18
|
+
|
|
19
|
+
expect(page.front_matter).to eq('title' => 'Hello', 'author' => 'Ada')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'strips the front matter block from the body' do
|
|
24
|
+
in_tmp_dir do
|
|
25
|
+
File.write('post.adoc', <<~ADOC)
|
|
26
|
+
---
|
|
27
|
+
title: Hello
|
|
28
|
+
---
|
|
29
|
+
= Hello
|
|
30
|
+
|
|
31
|
+
Body.
|
|
32
|
+
ADOC
|
|
33
|
+
|
|
34
|
+
page = described_class.read('post.adoc')
|
|
35
|
+
|
|
36
|
+
expect(page.body).to eq("= Hello\n\nBody.\n")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'returns empty front matter when the file has no --- block' do
|
|
41
|
+
in_tmp_dir do
|
|
42
|
+
File.write('post.adoc', "= Plain\n\nBody.\n")
|
|
43
|
+
|
|
44
|
+
page = described_class.read('post.adoc')
|
|
45
|
+
|
|
46
|
+
expect(page.front_matter).to eq({})
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns the full file as body when there is no front matter' do
|
|
51
|
+
in_tmp_dir do
|
|
52
|
+
File.write('post.adoc', "= Plain\n\nBody.\n")
|
|
53
|
+
|
|
54
|
+
page = described_class.read('post.adoc')
|
|
55
|
+
|
|
56
|
+
expect(page.body).to eq("= Plain\n\nBody.\n")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'raises Page::Error for unclosed front matter' do
|
|
61
|
+
in_tmp_dir do
|
|
62
|
+
File.write('post.adoc', "---\ntitle: Hello\n\n= Body without close\n")
|
|
63
|
+
|
|
64
|
+
expect { described_class.read('post.adoc') }
|
|
65
|
+
.to raise_error(Ottogen::Page::Error)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'raises Page::Error for malformed YAML in front matter' do
|
|
70
|
+
in_tmp_dir do
|
|
71
|
+
File.write('post.adoc', "---\ntitle: 'unclosed\n---\nBody\n")
|
|
72
|
+
|
|
73
|
+
expect { described_class.read('post.adoc') }
|
|
74
|
+
.to raise_error(Ottogen::Page::Error)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '#asciidoctor_attributes' do
|
|
80
|
+
it 'prefixes every front matter key with page_' do
|
|
81
|
+
in_tmp_dir do
|
|
82
|
+
File.write('post.adoc', "---\ntitle: Hi\nauthor: Ada\n---\nBody.\n")
|
|
83
|
+
|
|
84
|
+
attrs = described_class.read('post.adoc').asciidoctor_attributes
|
|
85
|
+
|
|
86
|
+
expect(attrs).to include('page_title' => 'Hi', 'page_author' => 'Ada')
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'includes page_url derived from the source path' do
|
|
91
|
+
in_tmp_dir do
|
|
92
|
+
FileUtils.mkdir_p('pages')
|
|
93
|
+
File.write('pages/about.adoc', "---\ntitle: About\n---\nBody.\n")
|
|
94
|
+
|
|
95
|
+
attrs = described_class.read('pages/about.adoc').asciidoctor_attributes
|
|
96
|
+
|
|
97
|
+
expect(attrs['page_url']).to eq('/about.html')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Ottogen::Permalink do
|
|
6
|
+
let(:post) do
|
|
7
|
+
Ottogen::Post.new(
|
|
8
|
+
path: '_posts/2026-01-15-hello-world.adoc',
|
|
9
|
+
date: Date.new(2026, 1, 15),
|
|
10
|
+
slug: 'hello-world',
|
|
11
|
+
front_matter: { 'title' => 'Hello World!' },
|
|
12
|
+
body: ''
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '.expand' do
|
|
17
|
+
it 'replaces :year, :month, :day from the date' do
|
|
18
|
+
expanded = described_class.expand('/:year/:month/:day/', post)
|
|
19
|
+
|
|
20
|
+
expect(expanded.path).to eq('/2026/01/15/')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'replaces :slug' do
|
|
24
|
+
expanded = described_class.expand('/:slug.html', post)
|
|
25
|
+
|
|
26
|
+
expect(expanded.path).to eq('/hello-world.html')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'replaces :title with a slugified version' do
|
|
30
|
+
expanded = described_class.expand('/:title/', post)
|
|
31
|
+
|
|
32
|
+
expect(expanded.path).to eq('/hello-world/')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '#output_path' do
|
|
37
|
+
it 'appends index.html when the path ends with /' do
|
|
38
|
+
permalink = described_class.new('/2026/01/15/hello/')
|
|
39
|
+
|
|
40
|
+
expect(permalink.output_path('_build')).to eq('_build/2026/01/15/hello/index.html')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'uses the path as-is otherwise' do
|
|
44
|
+
permalink = described_class.new('/2026/01/15/hello.html')
|
|
45
|
+
|
|
46
|
+
expect(permalink.output_path('_build')).to eq('_build/2026/01/15/hello.html')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|