dimples 4.3.2 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)