summaryrefslogtreecommitdiff
path: root/vendor/bundle/ruby/3.4.0/gems/jekyll-4.4.1/lib/jekyll/site.rb
blob: adec4cc06120de25779649b56967ccf5712802a5 (plain)
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