-
-
Notifications
You must be signed in to change notification settings - Fork 10k
/
site.rb
576 lines (493 loc) · 17.4 KB
/
site.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# frozen_string_literal: true
module Jekyll
class Site
attr_accessor :baseurl, :converters, :data, :drafts, :exclude,
:file_read_opts, :future, :gems, :generators, :highlighter,
:include, :inclusions, :keep_files, :layouts, :limit_posts,
:lsi, :pages, :permalink_style, :plugin_manager, :plugins,
:reader, :safe, :show_drafts, :static_files, :theme, :time,
:unpublished
attr_reader :cache_dir, :config, :dest, :filter_cache, :includes_load_paths,
:liquid_renderer, :profiler, :regenerator, :source
# Public: Initialize a new Site.
#
# config - A Hash containing site configuration details.
def initialize(config)
# Source and destination may not be changed after the site has been created.
@source = File.expand_path(config["source"]).freeze
@dest = File.expand_path(config["destination"]).freeze
self.config = config
@cache_dir = in_source_dir(config["cache_dir"])
@filter_cache = {}
@reader = Reader.new(self)
@profiler = Profiler.new(self)
@regenerator = Regenerator.new(self)
@liquid_renderer = LiquidRenderer.new(self)
Jekyll.sites << self
reset
setup
Jekyll::Hooks.trigger :site, :after_init, self
end
# Public: Set the site's configuration. This handles side-effects caused by
# changing values in the configuration.
#
# config - a Jekyll::Configuration, containing the new configuration.
#
# Returns the new configuration.
def config=(config)
@config = config.clone
%w(safe lsi highlighter baseurl exclude include future unpublished
show_drafts limit_posts keep_files).each do |opt|
send(:"#{opt}=", config[opt])
end
# keep using `gems` to avoid breaking change
self.gems = config["plugins"]
configure_cache
configure_plugins
configure_theme
configure_include_paths
configure_file_read_opts
self.permalink_style = config["permalink"].to_sym
# Read in a _config.yml from the current theme-gem at the very end.
@config = load_theme_configuration(config) if theme
@config
end
# Public: Read, process, and write this Site to output.
#
# Returns nothing.
def process
return profiler.profile_process if config["profile"]
reset
read
generate
render
cleanup
write
end
def print_stats
Jekyll.logger.info @liquid_renderer.stats_table
end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
#
# Reset Site details.
#
# Returns nothing
def reset
self.time = if config["time"]
Utils.parse_date(config["time"].to_s, "Invalid time in _config.yml.")
else
Time.now
end
self.layouts = {}
self.inclusions = {}
self.pages = []
self.static_files = []
self.data = {}
@post_attr_hash = {}
@site_data = nil
@collections = nil
@documents = nil
@docs_to_write = nil
@regenerator.clear_cache
@liquid_renderer.reset
@site_cleaner = nil
frontmatter_defaults.reset
raise ArgumentError, "limit_posts must be a non-negative number" if limit_posts.negative?
Jekyll::Cache.clear_if_config_changed config
Jekyll::Hooks.trigger :site, :after_reset, self
nil
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
# Load necessary libraries, plugins, converters, and generators.
#
# Returns nothing.
def setup
ensure_not_in_dest
plugin_manager.conscientious_require
self.converters = instantiate_subclasses(Jekyll::Converter)
self.generators = instantiate_subclasses(Jekyll::Generator)
end
# Check that the destination dir isn't the source dir or a directory
# parent to the source dir.
def ensure_not_in_dest
dest_pathname = Pathname.new(dest)
Pathname.new(source).ascend do |path|
if path == dest_pathname
raise Errors::FatalException,
"Destination directory cannot be or contain the Source directory."
end
end
end
# The list of collections and their corresponding Jekyll::Collection instances.
# If config['collections'] is set, a new instance is created
# for each item in the collection, a new hash is returned otherwise.
#
# Returns a Hash containing collection name-to-instance pairs.
def collections
@collections ||= collection_names.each_with_object({}) do |name, hsh|
hsh[name] = Jekyll::Collection.new(self, name)
end
end
# The list of collection names.
#
# Returns an array of collection names from the configuration,
# or an empty array if the `collections` key is not set.
def collection_names
case config["collections"]
when Hash
config["collections"].keys
when Array
config["collections"]
when nil
[]
else
raise ArgumentError, "Your `collections` key must be a hash or an array."
end
end
# Read Site data from disk and load it into internal data structures.
#
# Returns nothing.
def read
reader.read
limit_posts!
Jekyll::Hooks.trigger :site, :post_read, self
nil
end
# Run each of the Generators.
#
# Returns nothing.
def generate
generators.each do |generator|
start = Time.now
generator.generate(self)
Jekyll.logger.debug "Generating:",
"#{generator.class} finished in #{Time.now - start} seconds."
end
nil
end
# Render the site to the destination.
#
# Returns nothing.
def render
relative_permalinks_are_deprecated
payload = site_payload
Jekyll::Hooks.trigger :site, :pre_render, self, payload
render_docs(payload)
render_pages(payload)
Jekyll::Hooks.trigger :site, :post_render, self, payload
nil
end
# Remove orphaned files and empty directories in destination.
#
# Returns nothing.
def cleanup
site_cleaner.cleanup!
nil
end
# Write static files, pages, and posts.
#
# Returns nothing.
def write
Jekyll::Commands::Doctor.conflicting_urls(self)
each_site_file do |item|
item.write(dest) if regenerator.regenerate?(item)
end
regenerator.write_metadata
Jekyll::Hooks.trigger :site, :post_write, self
nil
end
def posts
collections["posts"] ||= Collection.new(self, "posts")
end
# Construct a Hash of Posts indexed by the specified Post attribute.
#
# post_attr - The String name of the Post attribute.
#
# Examples
#
# post_attr_hash('categories')
# # => { 'tech' => [<Post A>, <Post B>],
# # 'ruby' => [<Post B>] }
#
# Returns the Hash: { attr => posts } where
# attr - One of the values for the requested attribute.
# posts - The Array of Posts with the given attr value.
def post_attr_hash(post_attr)
# Build a hash map based on the specified post attribute ( post attr =>
# array of posts ) then sort each array in reverse order.
@post_attr_hash[post_attr] ||= begin
hash = Hash.new { |h, key| h[key] = [] }
posts.docs.each do |p|
p.data[post_attr]&.each { |t| hash[t] << p }
end
hash.each_value { |posts| posts.sort!.reverse! }
hash
end
end
def tags
post_attr_hash("tags")
end
def categories
post_attr_hash("categories")
end
# Prepare site data for site payload. The method maintains backward compatibility
# if the key 'data' is already used in _config.yml.
#
# Returns the Hash to be hooked to site.data.
def site_data
@site_data ||= (config["data"] || data)
end
# The Hash payload containing site-wide data.
#
# Returns the Hash: { "site" => data } where data is a Hash with keys:
# "time" - The Time as specified in the configuration or the
# current time if none was specified.
# "posts" - The Array of Posts, sorted chronologically by post date
# and then title.
# "pages" - The Array of all Pages.
# "html_pages" - The Array of HTML Pages.
# "categories" - The Hash of category values and Posts.
# See Site#post_attr_hash for type info.
# "tags" - The Hash of tag values and Posts.
# See Site#post_attr_hash for type info.
def site_payload
Drops::UnifiedPayloadDrop.new self
end
alias_method :to_liquid, :site_payload
# Get the implementation class for the given Converter.
# Returns the Converter instance implementing the given Converter.
# klass - The Class of the Converter to fetch.
def find_converter_instance(klass)
@find_converter_instance ||= {}
@find_converter_instance[klass] ||= converters.find do |converter|
converter.instance_of?(klass)
end || \
raise("No Converters found for #{klass}")
end
# klass - class or module containing the subclasses.
# Returns array of instances of subclasses of parameter.
# Create array of instances of the subclasses of the class or module
# passed in as argument.
def instantiate_subclasses(klass)
klass.descendants.select { |c| !safe || c.safe }.tap do |result|
result.sort!
result.map! { |c| c.new(config) }
end
end
# Warns the user if permanent links are relative to the parent
# directory. As this is a deprecated function of Jekyll.
#
# Returns
def relative_permalinks_are_deprecated
if config["relative_permalinks"]
Jekyll.logger.abort_with "Since v3.0, permalinks for pages " \
"in subfolders must be relative to the " \
"site source directory, not the parent " \
"directory. Check https://jekyllrb.com/docs/upgrading/ " \
"for more info."
end
end
# Get the to be written documents
#
# Returns an Array of Documents which should be written
def docs_to_write
documents.select(&:write?)
end
# Get the to be written static files
#
# Returns an Array of StaticFiles which should be written
def static_files_to_write
static_files.select(&:write?)
end
# Get all the documents
#
# Returns an Array of all Documents
def documents
collections.each_with_object(Set.new) do |(_, collection), set|
set.merge(collection.docs).merge(collection.files)
end.to_a
end
def each_site_file
pages.each { |page| yield page }
static_files.each { |file| yield(file) if file.write? }
collections.each_value { |coll| coll.docs.each { |doc| yield(doc) if doc.write? } }
end
# Returns the FrontmatterDefaults or creates a new FrontmatterDefaults
# if it doesn't already exist.
#
# Returns The FrontmatterDefaults
def frontmatter_defaults
@frontmatter_defaults ||= FrontmatterDefaults.new(self)
end
# Whether to perform a full rebuild without incremental regeneration
#
# Returns a Boolean: true for a full rebuild, false for normal build
def incremental?(override = {})
override["incremental"] || config["incremental"]
end
# Returns the publisher or creates a new publisher if it doesn't
# already exist.
#
# Returns The Publisher
def publisher
@publisher ||= Publisher.new(self)
end
# Public: Prefix a given path with the source directory.
#
# paths - (optional) path elements to a file or directory within the
# source directory
#
# Returns a path which is prefixed with the source directory.
def in_source_dir(*paths)
paths.reduce(source) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the theme directory.
#
# paths - (optional) path elements to a file or directory within the
# theme directory
#
# Returns a path which is prefixed with the theme root directory.
def in_theme_dir(*paths)
return nil unless theme
paths.reduce(theme.root) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the destination directory.
#
# paths - (optional) path elements to a file or directory within the
# destination directory
#
# Returns a path which is prefixed with the destination directory.
def in_dest_dir(*paths)
paths.reduce(dest) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the cache directory.
#
# paths - (optional) path elements to a file or directory within the
# cache directory
#
# Returns a path which is prefixed with the cache directory.
def in_cache_dir(*paths)
paths.reduce(cache_dir) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: The full path to the directory that houses all the collections registered
# with the current site.
#
# Returns the source directory or the absolute path to the custom collections_dir
def collections_path
dir_str = config["collections_dir"]
@collections_path ||= dir_str.empty? ? source : in_source_dir(dir_str)
end
# Public
#
# Returns the object as a debug String.
def inspect
"#<#{self.class} @source=#{@source}>"
end
private
def load_theme_configuration(config)
return config if config["ignore_theme_config"] == true
theme_config_file = in_theme_dir("_config.yml")
return config unless File.exist?(theme_config_file)
# Bail out if the theme_config_file is a symlink file irrespective of safe mode
return config if File.symlink?(theme_config_file)
theme_config = SafeYAML.load_file(theme_config_file)
return config unless theme_config.is_a?(Hash)
Jekyll.logger.info "Theme Config file:", theme_config_file
# theme_config should not be overriding Jekyll's defaults
theme_config.delete_if { |key, _| Configuration::DEFAULTS.key?(key) }
# Override theme_config with existing config and return the result.
# Additionally ensure we return a `Jekyll::Configuration` instance instead of a Hash.
Utils.deep_merge_hashes(theme_config, config)
.each_with_object(Jekyll::Configuration.new) do |(key, value), conf|
conf[key] = value
end
end
# Limits the current posts; removes the posts which exceed the limit_posts
#
# Returns nothing
def limit_posts!
if limit_posts.positive?
limit = [posts.docs.length, limit_posts].min
posts.docs = posts.docs[-limit, limit]
end
end
# Returns the Cleaner or creates a new Cleaner if it doesn't
# already exist.
#
# Returns The Cleaner
def site_cleaner
@site_cleaner ||= Cleaner.new(self)
end
def hide_cache_dir_from_git
@cache_gitignore_path ||= in_source_dir(config["cache_dir"], ".gitignore")
return if File.exist?(@cache_gitignore_path)
cache_dir_path = in_source_dir(config["cache_dir"])
FileUtils.mkdir_p(cache_dir_path) unless File.directory?(cache_dir_path)
File.open(@cache_gitignore_path, "wb") do |file|
file.puts("# ignore everything in this directory\n*")
end
end
# Disable Marshaling cache to disk in Safe Mode
def configure_cache
Jekyll::Cache.cache_dir = in_source_dir(config["cache_dir"], "Jekyll/Cache")
if safe || config["disable_disk_cache"]
Jekyll::Cache.disable_disk_cache!
else
hide_cache_dir_from_git
end
end
def configure_plugins
self.plugin_manager = Jekyll::PluginManager.new(self)
self.plugins = plugin_manager.plugins_path
end
def configure_theme
self.theme = nil
return if config["theme"].nil?
self.theme =
if config["theme"].is_a?(String)
Jekyll::Theme.new(config["theme"])
else
Jekyll.logger.warn "Theme:", "value of 'theme' in config should be String to use " \
"gem-based themes, but got #{config["theme"].class}"
nil
end
end
def configure_include_paths
@includes_load_paths = Array(in_source_dir(config["includes_dir"].to_s))
@includes_load_paths << theme.includes_path if theme&.includes_path
end
def configure_file_read_opts
self.file_read_opts = {}
file_read_opts[:encoding] = config["encoding"] if config["encoding"]
self.file_read_opts = Jekyll::Utils.merged_file_read_opts(self, {})
end
def render_docs(payload)
collections.each_value do |collection|
collection.docs.each do |document|
render_regenerated(document, payload)
end
end
end
def render_pages(payload)
pages.each do |page|
render_regenerated(page, payload)
end
end
def render_regenerated(document, payload)
return unless regenerator.regenerate?(document)
document.renderer.payload = payload
document.output = document.renderer.run
document.trigger_hooks(:post_render)
end
end
end