jekyll 4.2.1 → 4.3.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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +474 -350
  3. data/LICENSE +21 -21
  4. data/README.markdown +83 -86
  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 +186 -190
  12. data/lib/jekyll/cleaner.rb +111 -111
  13. data/lib/jekyll/collection.rb +310 -309
  14. data/lib/jekyll/command.rb +105 -105
  15. data/lib/jekyll/commands/build.rb +82 -93
  16. data/lib/jekyll/commands/clean.rb +44 -45
  17. data/lib/jekyll/commands/doctor.rb +177 -177
  18. data/lib/jekyll/commands/help.rb +34 -34
  19. data/lib/jekyll/commands/new.rb +168 -169
  20. data/lib/jekyll/commands/new_theme.rb +39 -40
  21. data/lib/jekyll/commands/serve/live_reload_reactor.rb +119 -122
  22. data/lib/jekyll/commands/serve/livereload_assets/livereload.js +1183 -1183
  23. data/lib/jekyll/commands/serve/mime_types_charset.json +71 -0
  24. data/lib/jekyll/commands/serve/servlet.rb +206 -202
  25. data/lib/jekyll/commands/serve/websockets.rb +81 -81
  26. data/lib/jekyll/commands/serve.rb +367 -362
  27. data/lib/jekyll/configuration.rb +313 -313
  28. data/lib/jekyll/converter.rb +54 -54
  29. data/lib/jekyll/converters/identity.rb +41 -41
  30. data/lib/jekyll/converters/markdown/kramdown_parser.rb +197 -199
  31. data/lib/jekyll/converters/markdown.rb +113 -113
  32. data/lib/jekyll/converters/smartypants.rb +70 -70
  33. data/lib/jekyll/convertible.rb +257 -257
  34. data/lib/jekyll/data_entry.rb +83 -0
  35. data/lib/jekyll/data_hash.rb +61 -0
  36. data/lib/jekyll/deprecator.rb +50 -50
  37. data/lib/jekyll/document.rb +543 -544
  38. data/lib/jekyll/drops/collection_drop.rb +20 -20
  39. data/lib/jekyll/drops/document_drop.rb +71 -70
  40. data/lib/jekyll/drops/drop.rb +293 -293
  41. data/lib/jekyll/drops/excerpt_drop.rb +23 -19
  42. data/lib/jekyll/drops/jekyll_drop.rb +32 -32
  43. data/lib/jekyll/drops/site_drop.rb +71 -66
  44. data/lib/jekyll/drops/static_file_drop.rb +14 -14
  45. data/lib/jekyll/drops/theme_drop.rb +36 -0
  46. data/lib/jekyll/drops/unified_payload_drop.rb +30 -26
  47. data/lib/jekyll/drops/url_drop.rb +140 -140
  48. data/lib/jekyll/entry_filter.rb +117 -121
  49. data/lib/jekyll/errors.rb +20 -20
  50. data/lib/jekyll/excerpt.rb +200 -201
  51. data/lib/jekyll/external.rb +75 -79
  52. data/lib/jekyll/filters/date_filters.rb +110 -110
  53. data/lib/jekyll/filters/grouping_filters.rb +64 -64
  54. data/lib/jekyll/filters/url_filters.rb +98 -98
  55. data/lib/jekyll/filters.rb +532 -535
  56. data/lib/jekyll/frontmatter_defaults.rb +238 -240
  57. data/lib/jekyll/generator.rb +5 -5
  58. data/lib/jekyll/hooks.rb +107 -107
  59. data/lib/jekyll/inclusion.rb +32 -32
  60. data/lib/jekyll/layout.rb +55 -67
  61. data/lib/jekyll/liquid_extensions.rb +22 -22
  62. data/lib/jekyll/liquid_renderer/file.rb +77 -77
  63. data/lib/jekyll/liquid_renderer/table.rb +55 -55
  64. data/lib/jekyll/liquid_renderer.rb +80 -80
  65. data/lib/jekyll/log_adapter.rb +151 -151
  66. data/lib/jekyll/mime.types +939 -866
  67. data/lib/jekyll/page.rb +215 -217
  68. data/lib/jekyll/page_excerpt.rb +25 -25
  69. data/lib/jekyll/page_without_a_file.rb +14 -14
  70. data/lib/jekyll/path_manager.rb +74 -74
  71. data/lib/jekyll/plugin.rb +92 -92
  72. data/lib/jekyll/plugin_manager.rb +123 -115
  73. data/lib/jekyll/profiler.rb +55 -58
  74. data/lib/jekyll/publisher.rb +23 -23
  75. data/lib/jekyll/reader.rb +209 -192
  76. data/lib/jekyll/readers/collection_reader.rb +23 -23
  77. data/lib/jekyll/readers/data_reader.rb +116 -79
  78. data/lib/jekyll/readers/layout_reader.rb +62 -62
  79. data/lib/jekyll/readers/page_reader.rb +25 -25
  80. data/lib/jekyll/readers/post_reader.rb +85 -85
  81. data/lib/jekyll/readers/static_file_reader.rb +25 -25
  82. data/lib/jekyll/readers/theme_assets_reader.rb +52 -52
  83. data/lib/jekyll/regenerator.rb +195 -195
  84. data/lib/jekyll/related_posts.rb +52 -52
  85. data/lib/jekyll/renderer.rb +263 -265
  86. data/lib/jekyll/site.rb +582 -551
  87. data/lib/jekyll/static_file.rb +205 -208
  88. data/lib/jekyll/stevenson.rb +60 -60
  89. data/lib/jekyll/tags/highlight.rb +114 -110
  90. data/lib/jekyll/tags/include.rb +275 -275
  91. data/lib/jekyll/tags/link.rb +42 -42
  92. data/lib/jekyll/tags/post_url.rb +106 -106
  93. data/lib/jekyll/theme.rb +90 -86
  94. data/lib/jekyll/theme_builder.rb +121 -121
  95. data/lib/jekyll/url.rb +167 -167
  96. data/lib/jekyll/utils/ansi.rb +57 -57
  97. data/lib/jekyll/utils/exec.rb +26 -26
  98. data/lib/jekyll/utils/internet.rb +37 -37
  99. data/lib/jekyll/utils/platforms.rb +67 -67
  100. data/lib/jekyll/utils/thread_event.rb +31 -31
  101. data/lib/jekyll/utils/win_tz.rb +46 -75
  102. data/lib/jekyll/utils.rb +378 -367
  103. data/lib/jekyll/version.rb +5 -5
  104. data/lib/jekyll.rb +197 -195
  105. data/lib/site_template/.gitignore +5 -5
  106. data/lib/site_template/404.html +25 -25
  107. data/lib/site_template/_config.yml +55 -55
  108. data/lib/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +29 -29
  109. data/lib/site_template/about.markdown +18 -18
  110. data/lib/site_template/index.markdown +6 -6
  111. data/lib/theme_template/CODE_OF_CONDUCT.md.erb +74 -74
  112. data/lib/theme_template/Gemfile +4 -4
  113. data/lib/theme_template/LICENSE.txt.erb +21 -21
  114. data/lib/theme_template/README.md.erb +50 -52
  115. data/lib/theme_template/_layouts/default.html +1 -1
  116. data/lib/theme_template/_layouts/page.html +5 -5
  117. data/lib/theme_template/_layouts/post.html +5 -5
  118. data/lib/theme_template/example/_config.yml.erb +1 -1
  119. data/lib/theme_template/example/_post.md +12 -12
  120. data/lib/theme_template/example/index.html +14 -14
  121. data/lib/theme_template/example/style.scss +7 -7
  122. data/lib/theme_template/gitignore.erb +6 -6
  123. data/lib/theme_template/theme.gemspec.erb +16 -16
  124. data/rubocop/jekyll/assert_equal_literal_actual.rb +149 -149
  125. data/rubocop/jekyll/no_p_allowed.rb +23 -23
  126. data/rubocop/jekyll/no_puts_allowed.rb +23 -23
  127. data/rubocop/jekyll.rb +5 -5
  128. metadata +62 -14
data/lib/jekyll/site.rb CHANGED
@@ -1,551 +1,582 @@
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
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ class Site
5
+ attr_accessor :baseurl, :converters, :data, :drafts, :exclude,
6
+ :file_read_opts, :future, :gems, :generators, :highlighter,
7
+ :include, :inclusions, :keep_files, :layouts, :limit_posts,
8
+ :lsi, :pages, :permalink_style, :plugin_manager, :plugins,
9
+ :reader, :safe, :show_drafts, :static_files, :theme, :time,
10
+ :unpublished
11
+
12
+ attr_reader :cache_dir, :config, :dest, :filter_cache, :includes_load_paths,
13
+ :liquid_renderer, :profiler, :regenerator, :source
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] ||= converters.find do |converter|
308
+ converter.instance_of?(klass)
309
+ end || \
310
+ raise("No Converters found for #{klass}")
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 the to be written static files
347
+ #
348
+ # Returns an Array of StaticFiles which should be written
349
+ def static_files_to_write
350
+ static_files.select(&:write?)
351
+ end
352
+
353
+ # Get all the documents
354
+ #
355
+ # Returns an Array of all Documents
356
+ def documents
357
+ collections.each_with_object(Set.new) do |(_, collection), set|
358
+ set.merge(collection.docs).merge(collection.files)
359
+ end.to_a
360
+ end
361
+
362
+ def each_site_file
363
+ seen_files = []
364
+ %w(pages static_files_to_write docs_to_write).each do |type|
365
+ send(type).each do |item|
366
+ next if seen_files.include?(item)
367
+
368
+ yield item
369
+ seen_files << item
370
+ end
371
+ end
372
+ end
373
+
374
+ # Returns the FrontmatterDefaults or creates a new FrontmatterDefaults
375
+ # if it doesn't already exist.
376
+ #
377
+ # Returns The FrontmatterDefaults
378
+ def frontmatter_defaults
379
+ @frontmatter_defaults ||= FrontmatterDefaults.new(self)
380
+ end
381
+
382
+ # Whether to perform a full rebuild without incremental regeneration
383
+ #
384
+ # Returns a Boolean: true for a full rebuild, false for normal build
385
+ def incremental?(override = {})
386
+ override["incremental"] || config["incremental"]
387
+ end
388
+
389
+ # Returns the publisher or creates a new publisher if it doesn't
390
+ # already exist.
391
+ #
392
+ # Returns The Publisher
393
+ def publisher
394
+ @publisher ||= Publisher.new(self)
395
+ end
396
+
397
+ # Public: Prefix a given path with the source directory.
398
+ #
399
+ # paths - (optional) path elements to a file or directory within the
400
+ # source directory
401
+ #
402
+ # Returns a path which is prefixed with the source directory.
403
+ def in_source_dir(*paths)
404
+ paths.reduce(source) do |base, path|
405
+ Jekyll.sanitized_path(base, path)
406
+ end
407
+ end
408
+
409
+ # Public: Prefix a given path with the theme directory.
410
+ #
411
+ # paths - (optional) path elements to a file or directory within the
412
+ # theme directory
413
+ #
414
+ # Returns a path which is prefixed with the theme root directory.
415
+ def in_theme_dir(*paths)
416
+ return nil unless theme
417
+
418
+ paths.reduce(theme.root) do |base, path|
419
+ Jekyll.sanitized_path(base, path)
420
+ end
421
+ end
422
+
423
+ # Public: Prefix a given path with the destination directory.
424
+ #
425
+ # paths - (optional) path elements to a file or directory within the
426
+ # destination directory
427
+ #
428
+ # Returns a path which is prefixed with the destination directory.
429
+ def in_dest_dir(*paths)
430
+ paths.reduce(dest) do |base, path|
431
+ Jekyll.sanitized_path(base, path)
432
+ end
433
+ end
434
+
435
+ # Public: Prefix a given path with the cache directory.
436
+ #
437
+ # paths - (optional) path elements to a file or directory within the
438
+ # cache directory
439
+ #
440
+ # Returns a path which is prefixed with the cache directory.
441
+ def in_cache_dir(*paths)
442
+ paths.reduce(cache_dir) do |base, path|
443
+ Jekyll.sanitized_path(base, path)
444
+ end
445
+ end
446
+
447
+ # Public: The full path to the directory that houses all the collections registered
448
+ # with the current site.
449
+ #
450
+ # Returns the source directory or the absolute path to the custom collections_dir
451
+ def collections_path
452
+ dir_str = config["collections_dir"]
453
+ @collections_path ||= dir_str.empty? ? source : in_source_dir(dir_str)
454
+ end
455
+
456
+ # Public
457
+ #
458
+ # Returns the object as a debug String.
459
+ def inspect
460
+ "#<#{self.class} @source=#{@source}>"
461
+ end
462
+
463
+ private
464
+
465
+ def load_theme_configuration(config)
466
+ return config if config["ignore_theme_config"] == true
467
+
468
+ theme_config_file = in_theme_dir("_config.yml")
469
+ return config unless File.exist?(theme_config_file)
470
+
471
+ # Bail out if the theme_config_file is a symlink file irrespective of safe mode
472
+ return config if File.symlink?(theme_config_file)
473
+
474
+ theme_config = SafeYAML.load_file(theme_config_file)
475
+ return config unless theme_config.is_a?(Hash)
476
+
477
+ Jekyll.logger.info "Theme Config file:", theme_config_file
478
+
479
+ # theme_config should not be overriding Jekyll's defaults
480
+ theme_config.delete_if { |key, _| Configuration::DEFAULTS.key?(key) }
481
+
482
+ # Override theme_config with existing config and return the result.
483
+ # Additionally ensure we return a `Jekyll::Configuration` instance instead of a Hash.
484
+ Utils.deep_merge_hashes(theme_config, config)
485
+ .each_with_object(Jekyll::Configuration.new) do |(key, value), conf|
486
+ conf[key] = value
487
+ end
488
+ end
489
+
490
+ # Limits the current posts; removes the posts which exceed the limit_posts
491
+ #
492
+ # Returns nothing
493
+ def limit_posts!
494
+ if limit_posts.positive?
495
+ limit = posts.docs.length < limit_posts ? posts.docs.length : limit_posts
496
+ posts.docs = posts.docs[-limit, limit]
497
+ end
498
+ end
499
+
500
+ # Returns the Cleaner or creates a new Cleaner if it doesn't
501
+ # already exist.
502
+ #
503
+ # Returns The Cleaner
504
+ def site_cleaner
505
+ @site_cleaner ||= Cleaner.new(self)
506
+ end
507
+
508
+ def hide_cache_dir_from_git
509
+ @cache_gitignore_path ||= in_source_dir(config["cache_dir"], ".gitignore")
510
+ return if File.exist?(@cache_gitignore_path)
511
+
512
+ cache_dir_path = in_source_dir(config["cache_dir"])
513
+ FileUtils.mkdir_p(cache_dir_path) unless File.directory?(cache_dir_path)
514
+
515
+ File.open(@cache_gitignore_path, "wb") do |file|
516
+ file.puts("# ignore everything in this directory\n*")
517
+ end
518
+ end
519
+
520
+ # Disable Marshaling cache to disk in Safe Mode
521
+ def configure_cache
522
+ Jekyll::Cache.cache_dir = in_source_dir(config["cache_dir"], "Jekyll/Cache")
523
+ if safe || config["disable_disk_cache"]
524
+ Jekyll::Cache.disable_disk_cache!
525
+ else
526
+ hide_cache_dir_from_git
527
+ end
528
+ end
529
+
530
+ def configure_plugins
531
+ self.plugin_manager = Jekyll::PluginManager.new(self)
532
+ self.plugins = plugin_manager.plugins_path
533
+ end
534
+
535
+ def configure_theme
536
+ self.theme = nil
537
+ return if config["theme"].nil?
538
+
539
+ self.theme =
540
+ if config["theme"].is_a?(String)
541
+ Jekyll::Theme.new(config["theme"])
542
+ else
543
+ Jekyll.logger.warn "Theme:", "value of 'theme' in config should be String to use " \
544
+ "gem-based themes, but got #{config["theme"].class}"
545
+ nil
546
+ end
547
+ end
548
+
549
+ def configure_include_paths
550
+ @includes_load_paths = Array(in_source_dir(config["includes_dir"].to_s))
551
+ @includes_load_paths << theme.includes_path if theme&.includes_path
552
+ end
553
+
554
+ def configure_file_read_opts
555
+ self.file_read_opts = {}
556
+ file_read_opts[:encoding] = config["encoding"] if config["encoding"]
557
+ self.file_read_opts = Jekyll::Utils.merged_file_read_opts(self, {})
558
+ end
559
+
560
+ def render_docs(payload)
561
+ collections.each_value do |collection|
562
+ collection.docs.each do |document|
563
+ render_regenerated(document, payload)
564
+ end
565
+ end
566
+ end
567
+
568
+ def render_pages(payload)
569
+ pages.each do |page|
570
+ render_regenerated(page, payload)
571
+ end
572
+ end
573
+
574
+ def render_regenerated(document, payload)
575
+ return unless regenerator.regenerate?(document)
576
+
577
+ document.renderer.payload = payload
578
+ document.output = document.renderer.run
579
+ document.trigger_hooks(:post_render)
580
+ end
581
+ end
582
+ end