dimples 4.3.2 → 5.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.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dimples
4
+ class Plugin
5
+ EVENTS = %i[
6
+ post_write
7
+ page_write
8
+ ].freeze
9
+
10
+ def self.inherited(subclass)
11
+ (@subclasses ||= []) << subclass
12
+ end
13
+
14
+ def self.plugins(site)
15
+ @plugins ||= @subclasses&.map { |subclass| subclass.new(site) }
16
+ end
17
+
18
+ def self.process(site, event, item, &block)
19
+ plugins(site)&.each do |plugin|
20
+ plugin.process(event, item, &block) if plugin.supports_event?(event)
21
+ end
22
+
23
+ yield if block_given?
24
+ end
25
+
26
+ def initialize(site)
27
+ @site = site
28
+ end
29
+
30
+ def process(event, item, &block); end
31
+
32
+ def supported_events
33
+ []
34
+ end
35
+
36
+ def supports_event?(event)
37
+ supported_events.include?(event)
38
+ end
39
+ end
40
+ end
data/lib/dimples/post.rb CHANGED
@@ -1,47 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dimples
4
- # A class that models a single site post.
5
4
  class Post < Page
6
- attr_accessor :slug
7
- attr_accessor :summary
8
- attr_accessor :categories
9
- attr_accessor :year
10
- attr_accessor :month
11
- attr_accessor :day
12
- attr_accessor :previous_post
13
- attr_accessor :next_post
14
- attr_reader :date
15
-
16
- FILENAME_DATE = /(\d{4})-(\d{2})-(\d{2})-(.+)/
5
+ POST_FILENAME = /(\d{4})-(\d{2})-(\d{2})-(.+)/
17
6
 
18
7
  def initialize(site, path)
19
- super(site, path)
8
+ super
20
9
 
21
- parts = File.basename(path, File.extname(path)).match(FILENAME_DATE)
10
+ parts = File.basename(path, File.extname(path)).match(POST_FILENAME)
22
11
 
23
- @filename = 'index'
24
- @slug = parts[4]
25
- @layout = @site.config[:layouts][:post]
12
+ @metadata[:layout] ||= @site.config.layouts.post
26
13
 
27
- self.date = Date.new(parts[1].to_i, parts[2].to_i, parts[3].to_i)
28
-
29
- @output_directory = File.join(
30
- @date.strftime(@site.output_paths[:posts]),
31
- @slug.to_s
32
- )
14
+ @metadata[:date] = Date.new(parts[1].to_i, parts[2].to_i, parts[3].to_i)
15
+ @metadata[:slug] = parts[4]
16
+ @metadata[:categories] ||= []
33
17
  end
34
18
 
35
- def date=(date)
36
- @date = date
19
+ def year
20
+ @year ||= @metadata[:date].strftime('%Y')
21
+ end
37
22
 
38
- @year = @date.strftime('%Y')
39
- @month = @date.strftime('%m')
40
- @day = @date.strftime('%d')
23
+ def month
24
+ @month ||= @metadata[:date].strftime('%m')
41
25
  end
42
26
 
43
- def inspect
44
- "#<#{self.class} @slug=#{@slug} @output_path=#{output_path}>"
27
+ def day
28
+ @day ||= @metadata[:date].strftime('%d')
45
29
  end
46
30
  end
47
31
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dimples
4
+ class Renderer
5
+ def initialize(site, source)
6
+ @site = site
7
+ @source = source
8
+ end
9
+
10
+ def render(context = {}, body = nil)
11
+ context[:site] ||= @site
12
+ context[:pagination] ||= nil
13
+
14
+ output = engine.render(scope, context) { body }.strip
15
+ @source.metadata[:rendered_contents] = output
16
+
17
+ template = @site.templates[@source.metadata[:layout]]
18
+ return output if template.nil?
19
+
20
+ template.render(context, output)
21
+ end
22
+
23
+ def engine
24
+ @engine ||= begin
25
+ callback = proc { @source.contents }
26
+
27
+ if @source.path
28
+ extension = File.extname(@source.path)
29
+ options = @site.config.rendering[extension.to_sym] || {}
30
+
31
+ Tilt.new(@source.path, options, &callback)
32
+ else
33
+ Tilt::StringTemplate.new(&callback)
34
+ end
35
+ end
36
+ end
37
+
38
+ def scope
39
+ @scope ||= Object.new.tap do |scope|
40
+ scope.instance_variable_set(:@site, @site)
41
+
42
+ scope.class.send(:define_method, :render) do |layout, locals = {}|
43
+ @site.templates[layout]&.render(locals)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/dimples/site.rb CHANGED
@@ -1,283 +1,262 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dimples
4
- # A class that models a single site.
5
4
  class Site
6
- include Pagination
7
-
8
- attr_accessor :source_paths
9
- attr_accessor :output_paths
10
- attr_accessor :config
11
- attr_accessor :templates
12
- attr_accessor :categories
13
- attr_accessor :archives
14
- attr_accessor :pages
15
- attr_accessor :posts
16
- attr_accessor :latest_post
17
- attr_accessor :errors
5
+ attr_reader :config
6
+ attr_reader :categories
7
+ attr_reader :errors
8
+ attr_reader :paths
9
+ attr_reader :posts
10
+ attr_reader :pages
11
+ attr_reader :templates
12
+ attr_reader :latest_post
18
13
 
19
14
  def initialize(config = {})
20
- @config = Dimples::Configuration.new(config)
21
-
22
- @templates = {}
23
- @categories = {}
24
- @pages = []
25
- @posts = []
26
- @errors = []
27
-
28
- @archives = { year: {}, month: {}, day: {} }
29
- @latest_post = false
30
-
31
- @source_paths = { root: File.expand_path(@config[:source_path]) }
32
- @output_paths = { site: File.expand_path(@config[:destination_path]) }
33
-
34
- %w[pages posts public templates].each do |path|
35
- @source_paths[path.to_sym] = File.join(@source_paths[:root], path)
15
+ @config = Hashie::Mash.new(Configuration.defaults).deep_merge(config)
16
+
17
+ @paths = {}.tap do |paths|
18
+ paths[:base] = Dir.pwd
19
+ paths[:output] = File.join(paths[:base], @config.paths.output)
20
+ paths[:sources] = {}.tap do |sources|
21
+ %w[pages posts public templates].each do |type|
22
+ sources[type.to_sym] = File.join(paths[:base], type)
23
+ end
24
+ end
36
25
  end
37
26
 
38
- %w[archives posts categories].each do |path|
39
- @output_paths[path.to_sym] = File.join(
40
- @output_paths[:site], @config[:paths][path.to_sym]
41
- )
42
- end
27
+ prepare
43
28
  end
44
29
 
45
30
  def generate
46
- scan_files
47
- prepare_output_directory
48
-
49
- publish_pages unless @pages.count.zero?
31
+ prepare
50
32
 
51
- unless @posts.count.zero?
52
- publish_posts
53
- publish_archives
54
- publish_categories if @config[:generation][:categories]
55
- end
33
+ read_templates
34
+ read_posts
35
+ read_pages
56
36
 
37
+ create_output_directory
57
38
  copy_assets
58
- rescue Errors::RenderingError,
59
- Errors::PublishingError,
60
- Errors::GenerationError => e
61
- @errors << e.message
39
+
40
+ publish_posts
41
+ publish_pages
42
+ publish_archives
43
+ publish_categories if @config.generation.categories
44
+ rescue PublishingError, RenderingError, GenerationError => error
45
+ @errors << error
62
46
  end
63
47
 
64
- def generated?
65
- @errors.count.zero?
48
+ def inspect
49
+ "#<#{self.class} @paths=#{paths}>"
66
50
  end
67
51
 
68
- def scan_files
69
- Dimples.logger.debug('Scanning files...') if @config[:verbose_logging]
52
+ private
70
53
 
71
- scan_templates
72
- scan_pages
73
- scan_posts
74
- end
54
+ def prepare
55
+ @archives = { year: {}, month: {}, day: {} }
75
56
 
76
- def scan_templates
77
- Dir.glob(File.join(@source_paths[:templates], '**', '*.*')).each do |path|
78
- template = Dimples::Template.new(self, path)
57
+ @categories = {}
58
+ @templates = {}
79
59
 
80
- parent_path = File.dirname(path)
81
- if parent_path == @source_paths[:templates]
82
- slug = template.slug
83
- else
84
- relative_path = parent_path.sub(@source_paths[:templates], '')[1..-1]
85
- slug = relative_path.sub(File::SEPARATOR, '.') + ".#{template.slug}"
86
- end
60
+ @pages = []
61
+ @posts = []
62
+ @errors = []
87
63
 
88
- @templates[slug] = template
89
- end
64
+ @latest_post = nil
90
65
  end
91
66
 
92
- def scan_pages
93
- Dir.glob(File.join(@source_paths[:pages], '**', '*.*')).each do |path|
94
- @pages << scan_page(path)
95
- end
96
- end
67
+ def read_templates
68
+ @templates = {}
97
69
 
98
- def scan_page(path)
99
- Dimples::Page.new(self, path)
100
- end
70
+ globbed_files(@paths[:sources][:templates]).each do |path|
71
+ basename = File.basename(path, File.extname(path))
72
+ dir_name = File.dirname(path)
101
73
 
102
- def scan_posts
103
- paths = Dir.glob(File.join(@source_paths[:posts], '*.*')).sort
74
+ unless dir_name == @paths[:sources][:templates]
75
+ basename = dir_name.split(File::SEPARATOR)[-1] + '.' + basename
76
+ end
104
77
 
105
- paths.reverse_each do |path|
106
- @posts << scan_post(path)
78
+ @templates[basename] = Template.new(self, path)
107
79
  end
80
+ end
108
81
 
109
- @posts.each_index do |index|
110
- if (index - 1) >= 0
111
- @posts[index].next_post = @posts.fetch(index - 1, nil)
82
+ def read_posts
83
+ @posts = globbed_files(@paths[:sources][:posts]).sort.map do |path|
84
+ Post.new(self, path).tap do |post|
85
+ add_archive_post(post)
86
+ categorise_post(post)
112
87
  end
88
+ end.reverse
113
89
 
114
- if (index + 1) < @posts.count
115
- @posts[index].previous_post = @posts.fetch(index + 1, nil)
116
- end
90
+ @latest_post = @posts[0]
91
+ end
92
+
93
+ def read_pages
94
+ @pages = globbed_files(@paths[:sources][:pages]).sort.map do |path|
95
+ Page.new(self, path)
117
96
  end
97
+ end
118
98
 
119
- @latest_post = @posts.first
99
+ def add_archive_post(post)
100
+ archive_year(post.year) << post
101
+ archive_month(post.year, post.month) << post
102
+ archive_day(post.year, post.month, post.day) << post
120
103
  end
121
104
 
122
- def scan_post(path)
123
- post_class.new(self, path).tap do |post|
124
- post.categories&.each do |slug|
125
- @categories[slug] ||= Dimples::Category.new(self, slug)
126
- @categories[slug].posts << post
127
- end
105
+ def categorise_post(post)
106
+ post.categories.each do |slug|
107
+ slug_sym = slug.to_sym
128
108
 
129
- archive_year(post.year) << post
130
- archive_month(post.year, post.month) << post
131
- archive_day(post.year, post.month, post.day) << post
109
+ @categories[slug_sym] ||= Category.new(self, slug)
110
+ @categories[slug_sym].posts << post
132
111
  end
133
112
  end
134
113
 
135
- def prepare_output_directory
136
- if Dir.exist?(@output_paths[:site])
137
- FileUtils.remove_dir(@output_paths[:site])
138
- end
114
+ def create_output_directory
115
+ FileUtils.remove_dir(@paths[:output]) if Dir.exist?(@paths[:output])
116
+ Dir.mkdir(@paths[:output])
117
+ rescue StandardError => e
118
+ message = "Couldn't prepare the output directory (#{e.message})"
119
+ raise GenerationError, message
120
+ end
139
121
 
140
- Dir.mkdir(@output_paths[:site])
122
+ def copy_assets
123
+ return unless Dir.exist?(@paths[:sources][:public])
124
+ FileUtils.cp_r(File.join(@paths[:sources][:public], '.'), @paths[:output])
141
125
  rescue StandardError => e
142
- error_message = "Couldn't prepare the output directory (#{e.message})"
143
- raise Errors::GenerationError, error_message
126
+ raise GenerationError, "Failed to copy site assets (#{e.message})"
144
127
  end
145
128
 
146
129
  def publish_posts
147
- if @config[:verbose_logging]
148
- Dimples.logger.debug_generation('posts', @posts.length)
130
+ @posts.each do |post|
131
+ Plugin.process(self, :post_write, post) do
132
+ path = File.join(
133
+ @paths[:output],
134
+ post.date.strftime(@config.paths.posts),
135
+ post.slug
136
+ )
137
+
138
+ post.write(path)
139
+ end
149
140
  end
150
141
 
151
- @posts.each(&:write)
152
-
153
- paginate(
154
- self,
155
- @posts,
156
- @output_paths[:archives],
157
- @config[:layouts][:posts]
158
- )
159
-
160
- publish_posts_feeds if @config[:generation][:feeds]
142
+ publish_feeds(@posts, @paths[:output]) if @config.generation.main_feed
161
143
  end
162
144
 
163
145
  def publish_pages
164
- if @config[:verbose_logging]
165
- Dimples.logger.debug_generation('pages', @pages.length)
146
+ @pages.each do |page|
147
+ Plugin.process(self, :page_write, page) do
148
+ path = if page.path
149
+ File.dirname(page.path).sub(
150
+ @paths[:sources][:pages],
151
+ @paths[:output]
152
+ )
153
+ else
154
+ @paths[:output]
155
+ end
156
+
157
+ page.write(path)
158
+ end
166
159
  end
167
-
168
- @pages.each(&:write)
169
160
  end
170
161
 
171
- def publish_categories
172
- if @config[:verbose_logging]
173
- Dimples.logger.debug_generation('category pages', @categories.length)
162
+ def publish_archives
163
+ if @config.generation.archives
164
+ paginate_posts(
165
+ @posts,
166
+ File.join(@paths[:output], @config.paths.archives),
167
+ @config.layouts.archive
168
+ )
174
169
  end
175
170
 
176
- @categories.each_value do |category|
177
- path = File.join(@output_paths[:categories], category.slug)
171
+ %w[year month day].each do |date_type|
172
+ next unless @config.generation["#{date_type}_archives"]
173
+ publish_date_archive(date_type.to_sym)
174
+ end
175
+ end
178
176
 
179
- options = {
180
- context: { category: category.slug },
181
- title: category.name
182
- }
177
+ def publish_date_archive(date_type)
178
+ @archives[date_type].each do |date, posts|
179
+ date_parts = date.split('-')
180
+ path = File.join(@paths[:output], @config.paths.archives, date_parts)
183
181
 
184
- paginate(
185
- self,
186
- category.posts,
182
+ paginate_posts(
183
+ posts.reverse,
187
184
  path,
188
- @config[:layouts][:category],
189
- options
185
+ @config.layouts.date_archive,
186
+ page: {
187
+ title: posts[0].date.strftime(@config.date_formats[date_type]),
188
+ archive_date: posts[0].date,
189
+ archive_type: date_type
190
+ }
190
191
  )
191
192
  end
192
-
193
- publish_category_feeds if @config[:generation][:category_feeds]
194
193
  end
195
194
 
196
- def publish_archives
197
- %w[year month day].each do |date_type|
198
- date_archives_sym = "#{date_type}_archives".to_sym
199
- next unless @config[:generation][date_archives_sym]
200
-
201
- @archives[date_type.to_sym].each do |date, posts|
202
- year, month, day = date.split('-')
203
-
204
- dates = { year: year }
205
- dates[:month] = month if month
206
- dates[:day] = day if day
207
-
208
- path = File.join(@output_paths[:archives], dates.values)
195
+ def publish_categories
196
+ @categories.each_value do |category|
197
+ path = File.join(
198
+ @paths[:output],
199
+ @config.paths.categories,
200
+ category.slug
201
+ )
209
202
 
210
- layout = @config[:layouts][date_archives_sym]
211
- date_format = @config[:date_formats][date_type.to_sym]
203
+ category_posts = category.posts.reverse
204
+ context = { page: { title: category.name, category: category } }
212
205
 
213
- options = {
214
- context: { archive_date: posts[0].date, archive_type: date_type },
215
- title: posts[0].date.strftime(date_format)
216
- }
206
+ paginate_posts(
207
+ category_posts,
208
+ path,
209
+ @config.layouts.category,
210
+ context
211
+ )
217
212
 
218
- paginate(self, posts, path, layout, options)
219
- end
213
+ publish_feeds(category_posts, path, context)
220
214
  end
221
215
  end
222
216
 
223
- def publish_feeds(path, context)
224
- feed_templates.each do |format|
225
- next unless @templates[format]
217
+ def paginate_posts(posts, path, layout, context = {})
218
+ pager = Pager.new(
219
+ path.sub(@paths[:output], '') + '/',
220
+ posts,
221
+ @config.pagination
222
+ )
226
223
 
227
- feed = Dimples::Page.new(self)
224
+ pager.each do |index|
225
+ page = Page.new(self)
226
+ page.layout = layout
228
227
 
229
- feed.output_directory = path
230
- feed.filename = 'feed'
231
- feed.extension = @templates[format].slug
232
- feed.layout = format
228
+ page_prefix = @config.pagination.page_prefix
229
+ page_path = if index == 1
230
+ path
231
+ else
232
+ File.join(path, "#{page_prefix}#{index}")
233
+ end
233
234
 
234
- feed.write(context)
235
+ page.write(
236
+ page_path,
237
+ context.merge(pagination: pager.to_context)
238
+ )
235
239
  end
236
240
  end
237
241
 
238
- def publish_posts_feeds
239
- posts = @posts[0..@config[:pagination][:per_page] - 1]
240
- publish_feeds(@output_paths[:site], posts: posts)
241
- end
242
+ def publish_feeds(posts, path, context = {})
243
+ @config.feed_formats.each do |feed_format|
244
+ feed_layout = "feeds.#{feed_format}"
245
+ next unless @templates.key?(feed_layout)
242
246
 
243
- def publish_category_feeds
244
- @categories.each_value do |category|
245
- path = File.join(@output_paths[:categories], category.slug)
246
- posts = category.posts[0..@config[:pagination][:per_page] - 1]
247
+ page = Page.new(self)
247
248
 
248
- publish_feeds(path, posts: posts, category: category.slug)
249
- end
250
- end
249
+ page.layout = feed_layout
250
+ page.feed_posts = posts.slice(0, @config.pagination.per_page)
251
+ page.filename = 'feed'
252
+ page.extension = feed_format
251
253
 
252
- def feed_templates
253
- @feed_templates ||= @templates.keys.select { |k| k =~ /^feeds\./ }
254
- end
255
-
256
- def copy_assets
257
- if Dir.exist?(@source_paths[:public])
258
- Dimples.logger.debug('Copying assets...') if @config[:verbose_logging]
259
-
260
- path = File.join(@source_paths[:public], '.')
261
- FileUtils.cp_r(path, @output_paths[:site])
254
+ page.write(path, context)
262
255
  end
263
- rescue StandardError => e
264
- raise Errors::GenerationError, "Site assets failed to copy (#{e.message})"
265
256
  end
266
257
 
267
- def inspect
268
- "#<#{self.class} " \
269
- "@source_paths=#{@source_paths} " \
270
- "@output_paths=#{@output_paths}>"
271
- end
272
-
273
- private
274
-
275
- def post_class
276
- @post_class ||= if @config[:class_overrides][:post]
277
- Object.const_get(config[:class_overrides][:post])
278
- else
279
- Dimples::Post
280
- end
258
+ def globbed_files(path)
259
+ Dir.glob(File.join(path, '**', '*.*'))
281
260
  end
282
261
 
283
262
  def archive_year(year)