jekyll 4.0.1 → 4.2.1

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