jekyll 4.2.1 → 4.2.2

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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +350 -350
  3. data/LICENSE +21 -21
  4. data/README.markdown +86 -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 +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 -105
  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 -177
  18. data/lib/jekyll/commands/help.rb +34 -34
  19. data/lib/jekyll/commands/new.rb +172 -169
  20. data/lib/jekyll/commands/new_theme.rb +40 -40
  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 -362
  26. data/lib/jekyll/configuration.rb +313 -313
  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 -199
  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 -257
  33. data/lib/jekyll/deprecator.rb +50 -50
  34. data/lib/jekyll/document.rb +544 -544
  35. data/lib/jekyll/drops/collection_drop.rb +20 -20
  36. data/lib/jekyll/drops/document_drop.rb +70 -70
  37. data/lib/jekyll/drops/drop.rb +293 -293
  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 -140
  44. data/lib/jekyll/entry_filter.rb +121 -121
  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 -98
  51. data/lib/jekyll/filters.rb +535 -535
  52. data/lib/jekyll/frontmatter_defaults.rb +240 -240
  53. data/lib/jekyll/generator.rb +5 -5
  54. data/lib/jekyll/hooks.rb +107 -107
  55. data/lib/jekyll/inclusion.rb +32 -32
  56. data/lib/jekyll/layout.rb +67 -67
  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 -55
  60. data/lib/jekyll/liquid_renderer.rb +80 -80
  61. data/lib/jekyll/log_adapter.rb +151 -151
  62. data/lib/jekyll/mime.types +866 -866
  63. data/lib/jekyll/page.rb +217 -217
  64. data/lib/jekyll/page_excerpt.rb +25 -25
  65. data/lib/jekyll/page_without_a_file.rb +14 -14
  66. data/lib/jekyll/path_manager.rb +74 -74
  67. data/lib/jekyll/plugin.rb +92 -92
  68. data/lib/jekyll/plugin_manager.rb +115 -115
  69. data/lib/jekyll/profiler.rb +58 -58
  70. data/lib/jekyll/publisher.rb +23 -23
  71. data/lib/jekyll/reader.rb +192 -192
  72. data/lib/jekyll/readers/collection_reader.rb +23 -23
  73. data/lib/jekyll/readers/data_reader.rb +79 -79
  74. data/lib/jekyll/readers/layout_reader.rb +62 -62
  75. data/lib/jekyll/readers/page_reader.rb +25 -25
  76. data/lib/jekyll/readers/post_reader.rb +85 -85
  77. data/lib/jekyll/readers/static_file_reader.rb +25 -25
  78. data/lib/jekyll/readers/theme_assets_reader.rb +52 -52
  79. data/lib/jekyll/regenerator.rb +195 -195
  80. data/lib/jekyll/related_posts.rb +52 -52
  81. data/lib/jekyll/renderer.rb +265 -265
  82. data/lib/jekyll/site.rb +551 -551
  83. data/lib/jekyll/static_file.rb +208 -208
  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 -275
  87. data/lib/jekyll/tags/link.rb +42 -42
  88. data/lib/jekyll/tags/post_url.rb +106 -106
  89. data/lib/jekyll/theme.rb +86 -86
  90. data/lib/jekyll/theme_builder.rb +121 -121
  91. data/lib/jekyll/url.rb +167 -167
  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 -67
  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 -195
  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 -16
  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 +3 -3
@@ -1,544 +1,544 @@
1
- # frozen_string_literal: true
2
-
3
- module Jekyll
4
- class Document
5
- include Comparable
6
- extend Forwardable
7
-
8
- attr_reader :path, :site, :extname, :collection, :type
9
- attr_accessor :content, :output
10
-
11
- def_delegator :self, :read_post_data, :post_read
12
-
13
- YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m.freeze
14
- DATELESS_FILENAME_MATCHER = %r!^(?:.+/)*(.*)(\.[^.]+)$!.freeze
15
- DATE_FILENAME_MATCHER = %r!^(?>.+/)*?(\d{2,4}-\d{1,2}-\d{1,2})-([^/]*)(\.[^.]+)$!.freeze
16
-
17
- SASS_FILE_EXTS = %w(.sass .scss).freeze
18
- YAML_FILE_EXTS = %w(.yaml .yml).freeze
19
-
20
- #
21
-
22
- # Class-wide cache to stash and retrieve regexp to detect "super-directories"
23
- # of a particular Jekyll::Document object.
24
- #
25
- # dirname - The *special directory* for the Document.
26
- # e.g. "_posts" or "_drafts" for Documents from the `site.posts` collection.
27
- def self.superdirs_regex(dirname)
28
- @superdirs_regex ||= {}
29
- @superdirs_regex[dirname] ||= %r!#{dirname}.*!
30
- end
31
-
32
- #
33
-
34
- # Create a new Document.
35
- #
36
- # path - the path to the file
37
- # relations - a hash with keys :site and :collection, the values of which
38
- # are the Jekyll::Site and Jekyll::Collection to which this
39
- # Document belong.
40
- #
41
- # Returns nothing.
42
- def initialize(path, relations = {})
43
- @site = relations[:site]
44
- @path = path
45
- @extname = File.extname(path)
46
- @collection = relations[:collection]
47
- @type = @collection.label.to_sym
48
-
49
- @has_yaml_header = nil
50
-
51
- if draft?
52
- categories_from_path("_drafts")
53
- else
54
- categories_from_path(collection.relative_directory)
55
- end
56
-
57
- data.default_proc = proc do |_, key|
58
- site.frontmatter_defaults.find(relative_path, type, key)
59
- end
60
-
61
- trigger_hooks(:post_init)
62
- end
63
-
64
- # Fetch the Document's data.
65
- #
66
- # Returns a Hash containing the data. An empty hash is returned if
67
- # no data was read.
68
- def data
69
- @data ||= {}
70
- end
71
-
72
- # Merge some data in with this document's data.
73
- #
74
- # Returns the merged data.
75
- def merge_data!(other, source: "YAML front matter")
76
- merge_categories!(other)
77
- Utils.deep_merge_hashes!(data, other)
78
- merge_date!(source)
79
- data
80
- end
81
-
82
- # Returns the document date. If metadata is not present then calculates it
83
- # based on Jekyll::Site#time or the document file modification time.
84
- #
85
- # Return document date string.
86
- def date
87
- data["date"] ||= (draft? ? source_file_mtime : site.time)
88
- end
89
-
90
- # Return document file modification time in the form of a Time object.
91
- #
92
- # Return document file modification Time object.
93
- def source_file_mtime
94
- File.mtime(path)
95
- end
96
-
97
- # Returns whether the document is a draft. This is only the case if
98
- # the document is in the 'posts' collection but in a different
99
- # directory than '_posts'.
100
- #
101
- # Returns whether the document is a draft.
102
- def draft?
103
- data["draft"] ||= relative_path.index(collection.relative_directory).nil? &&
104
- collection.label == "posts"
105
- end
106
-
107
- # The path to the document, relative to the collections_dir.
108
- #
109
- # Returns a String path which represents the relative path from the collections_dir
110
- # to this document.
111
- def relative_path
112
- @relative_path ||= path.sub("#{site.collections_path}/", "")
113
- end
114
-
115
- # The output extension of the document.
116
- #
117
- # Returns the output extension
118
- def output_ext
119
- renderer.output_ext
120
- end
121
-
122
- # The base filename of the document, without the file extname.
123
- #
124
- # Returns the basename without the file extname.
125
- def basename_without_ext
126
- @basename_without_ext ||= File.basename(path, ".*")
127
- end
128
-
129
- # The base filename of the document.
130
- #
131
- # Returns the base filename of the document.
132
- def basename
133
- @basename ||= File.basename(path)
134
- end
135
-
136
- def renderer
137
- @renderer ||= Jekyll::Renderer.new(site, self)
138
- end
139
-
140
- # Produces a "cleaned" relative path.
141
- # The "cleaned" relative path is the relative path without the extname
142
- # and with the collection's directory removed as well.
143
- # This method is useful when building the URL of the document.
144
- #
145
- # NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)
146
- #
147
- # Examples:
148
- # When relative_path is "_methods/site/generate...md":
149
- # cleaned_relative_path
150
- # # => "/site/generate"
151
- #
152
- # Returns the cleaned relative path of the document.
153
- def cleaned_relative_path
154
- @cleaned_relative_path ||=
155
- relative_path[0..-extname.length - 1]
156
- .sub(collection.relative_directory, "")
157
- .gsub(%r!\.*\z!, "")
158
- end
159
-
160
- # Determine whether the document is a YAML file.
161
- #
162
- # Returns true if the extname is either .yml or .yaml, false otherwise.
163
- def yaml_file?
164
- YAML_FILE_EXTS.include?(extname)
165
- end
166
-
167
- # Determine whether the document is an asset file.
168
- # Asset files include CoffeeScript files and Sass/SCSS files.
169
- #
170
- # Returns true if the extname belongs to the set of extensions
171
- # that asset files use.
172
- def asset_file?
173
- sass_file? || coffeescript_file?
174
- end
175
-
176
- # Determine whether the document is a Sass file.
177
- #
178
- # Returns true if extname == .sass or .scss, false otherwise.
179
- def sass_file?
180
- SASS_FILE_EXTS.include?(extname)
181
- end
182
-
183
- # Determine whether the document is a CoffeeScript file.
184
- #
185
- # Returns true if extname == .coffee, false otherwise.
186
- def coffeescript_file?
187
- extname == ".coffee"
188
- end
189
-
190
- # Determine whether the file should be rendered with Liquid.
191
- #
192
- # Returns false if the document is either an asset file or a yaml file,
193
- # or if the document doesn't contain any Liquid Tags or Variables,
194
- # true otherwise.
195
- def render_with_liquid?
196
- return false if data["render_with_liquid"] == false
197
-
198
- !(coffeescript_file? || yaml_file? || !Utils.has_liquid_construct?(content))
199
- end
200
-
201
- # Determine whether the file should be rendered with a layout.
202
- #
203
- # Returns true if the Front Matter specifies that `layout` is set to `none`.
204
- def no_layout?
205
- data["layout"] == "none"
206
- end
207
-
208
- # Determine whether the file should be placed into layouts.
209
- #
210
- # Returns false if the document is set to `layouts: none`, or is either an
211
- # asset file or a yaml file. Returns true otherwise.
212
- def place_in_layout?
213
- !(asset_file? || yaml_file? || no_layout?)
214
- end
215
-
216
- # The URL template where the document would be accessible.
217
- #
218
- # Returns the URL template for the document.
219
- def url_template
220
- collection.url_template
221
- end
222
-
223
- # Construct a Hash of key-value pairs which contain a mapping between
224
- # a key in the URL template and the corresponding value for this document.
225
- #
226
- # Returns the Hash of key-value pairs for replacement in the URL.
227
- def url_placeholders
228
- @url_placeholders ||= Drops::UrlDrop.new(self)
229
- end
230
-
231
- # The permalink for this Document.
232
- # Permalink is set via the data Hash.
233
- #
234
- # Returns the permalink or nil if no permalink was set in the data.
235
- def permalink
236
- data && data.is_a?(Hash) && data["permalink"]
237
- end
238
-
239
- # The computed URL for the document. See `Jekyll::URL#to_s` for more details.
240
- #
241
- # Returns the computed URL for the document.
242
- def url
243
- @url ||= URL.new(
244
- :template => url_template,
245
- :placeholders => url_placeholders,
246
- :permalink => permalink
247
- ).to_s
248
- end
249
-
250
- def [](key)
251
- data[key]
252
- end
253
-
254
- # The full path to the output file.
255
- #
256
- # base_directory - the base path of the output directory
257
- #
258
- # Returns the full path to the output file of this document.
259
- def destination(base_directory)
260
- @destination ||= {}
261
- @destination[base_directory] ||= begin
262
- path = site.in_dest_dir(base_directory, URL.unescape_path(url))
263
- if url.end_with? "/"
264
- path = File.join(path, "index.html")
265
- else
266
- path << output_ext unless path.end_with? output_ext
267
- end
268
- path
269
- end
270
- end
271
-
272
- # Write the generated Document file to the destination directory.
273
- #
274
- # dest - The String path to the destination dir.
275
- #
276
- # Returns nothing.
277
- def write(dest)
278
- path = destination(dest)
279
- FileUtils.mkdir_p(File.dirname(path))
280
- Jekyll.logger.debug "Writing:", path
281
- File.write(path, output, :mode => "wb")
282
-
283
- trigger_hooks(:post_write)
284
- end
285
-
286
- # Whether the file is published or not, as indicated in YAML front-matter
287
- #
288
- # Returns 'false' if the 'published' key is specified in the
289
- # YAML front-matter and is 'false'. Otherwise returns 'true'.
290
- def published?
291
- !(data.key?("published") && data["published"] == false)
292
- end
293
-
294
- # Read in the file and assign the content and data based on the file contents.
295
- # Merge the frontmatter of the file with the frontmatter default
296
- # values
297
- #
298
- # Returns nothing.
299
- def read(opts = {})
300
- Jekyll.logger.debug "Reading:", relative_path
301
-
302
- if yaml_file?
303
- @data = SafeYAML.load_file(path)
304
- else
305
- begin
306
- merge_defaults
307
- read_content(**opts)
308
- read_post_data
309
- rescue StandardError => e
310
- handle_read_error(e)
311
- end
312
- end
313
- end
314
-
315
- # Create a Liquid-understandable version of this Document.
316
- #
317
- # Returns a Hash representing this Document's data.
318
- def to_liquid
319
- @to_liquid ||= Drops::DocumentDrop.new(self)
320
- end
321
-
322
- # The inspect string for this document.
323
- # Includes the relative path and the collection label.
324
- #
325
- # Returns the inspect string for this document.
326
- def inspect
327
- "#<#{self.class} #{relative_path} collection=#{collection.label}>"
328
- end
329
-
330
- # The string representation for this document.
331
- #
332
- # Returns the content of the document
333
- def to_s
334
- output || content || "NO CONTENT"
335
- end
336
-
337
- # Compare this document against another document.
338
- # Comparison is a comparison between the 2 paths of the documents.
339
- #
340
- # Returns -1, 0, +1 or nil depending on whether this doc's path is less than,
341
- # equal or greater than the other doc's path. See String#<=> for more details.
342
- def <=>(other)
343
- return nil unless other.respond_to?(:data)
344
-
345
- cmp = data["date"] <=> other.data["date"]
346
- cmp = path <=> other.path if cmp.nil? || cmp.zero?
347
- cmp
348
- end
349
-
350
- # Determine whether this document should be written.
351
- # Based on the Collection to which it belongs.
352
- #
353
- # True if the document has a collection and if that collection's #write?
354
- # method returns true, and if the site's Publisher will publish the document.
355
- # False otherwise.
356
- #
357
- # rubocop:disable Naming/MemoizedInstanceVariableName
358
- def write?
359
- return @write_p if defined?(@write_p)
360
-
361
- @write_p = collection&.write? && site.publisher.publish?(self)
362
- end
363
- # rubocop:enable Naming/MemoizedInstanceVariableName
364
-
365
- # The Document excerpt_separator, from the YAML Front-Matter or site
366
- # default excerpt_separator value
367
- #
368
- # Returns the document excerpt_separator
369
- def excerpt_separator
370
- @excerpt_separator ||= (data["excerpt_separator"] || site.config["excerpt_separator"]).to_s
371
- end
372
-
373
- # Whether to generate an excerpt
374
- #
375
- # Returns true if the excerpt separator is configured.
376
- def generate_excerpt?
377
- !excerpt_separator.empty?
378
- end
379
-
380
- def next_doc
381
- pos = collection.docs.index { |post| post.equal?(self) }
382
- collection.docs[pos + 1] if pos && pos < collection.docs.length - 1
383
- end
384
-
385
- def previous_doc
386
- pos = collection.docs.index { |post| post.equal?(self) }
387
- collection.docs[pos - 1] if pos && pos.positive?
388
- end
389
-
390
- def trigger_hooks(hook_name, *args)
391
- Jekyll::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection
392
- Jekyll::Hooks.trigger :documents, hook_name, self, *args
393
- end
394
-
395
- def id
396
- @id ||= File.join(File.dirname(url), (data["slug"] || basename_without_ext).to_s)
397
- end
398
-
399
- # Calculate related posts.
400
- #
401
- # Returns an Array of related Posts.
402
- def related_posts
403
- @related_posts ||= Jekyll::RelatedPosts.new(self).build
404
- end
405
-
406
- # Override of method_missing to check in @data for the key.
407
- def method_missing(method, *args, &blck)
408
- if data.key?(method.to_s)
409
- Jekyll::Deprecator.deprecation_message "Document##{method} is now a key "\
410
- "in the #data hash."
411
- Jekyll::Deprecator.deprecation_message "Called by #{caller(0..0)}."
412
- data[method.to_s]
413
- else
414
- super
415
- end
416
- end
417
-
418
- def respond_to_missing?(method, *)
419
- data.key?(method.to_s) || super
420
- end
421
-
422
- # Add superdirectories of the special_dir to categories.
423
- # In the case of es/_posts, 'es' is added as a category.
424
- # In the case of _posts/es, 'es' is NOT added as a category.
425
- #
426
- # Returns nothing.
427
- def categories_from_path(special_dir)
428
- if relative_path.start_with?(special_dir)
429
- superdirs = []
430
- else
431
- superdirs = relative_path.sub(Document.superdirs_regex(special_dir), "")
432
- superdirs = superdirs.split(File::SEPARATOR)
433
- superdirs.reject! { |c| c.empty? || c == special_dir || c == basename }
434
- end
435
-
436
- merge_data!({ "categories" => superdirs }, :source => "file path")
437
- end
438
-
439
- def populate_categories
440
- categories = Array(data["categories"]) + Utils.pluralized_array_from_hash(
441
- data, "category", "categories"
442
- )
443
- categories.map!(&:to_s)
444
- categories.flatten!
445
- categories.uniq!
446
-
447
- merge_data!({ "categories" => categories })
448
- end
449
-
450
- def populate_tags
451
- tags = Utils.pluralized_array_from_hash(data, "tag", "tags")
452
- tags.flatten!
453
-
454
- merge_data!({ "tags" => tags })
455
- end
456
-
457
- private
458
-
459
- def merge_categories!(other)
460
- if other.key?("categories") && !other["categories"].nil?
461
- other["categories"] = other["categories"].split if other["categories"].is_a?(String)
462
-
463
- if data["categories"].is_a?(Array)
464
- other["categories"] = data["categories"] | other["categories"]
465
- end
466
- end
467
- end
468
-
469
- def merge_date!(source)
470
- if data.key?("date")
471
- data["date"] = Utils.parse_date(
472
- data["date"].to_s,
473
- "Document '#{relative_path}' does not have a valid date in the #{source}."
474
- )
475
- end
476
- end
477
-
478
- def merge_defaults
479
- defaults = @site.frontmatter_defaults.all(relative_path, type)
480
- merge_data!(defaults, :source => "front matter defaults") unless defaults.empty?
481
- end
482
-
483
- def read_content(**opts)
484
- self.content = File.read(path, **Utils.merged_file_read_opts(site, opts))
485
- if content =~ YAML_FRONT_MATTER_REGEXP
486
- self.content = Regexp.last_match.post_match
487
- data_file = SafeYAML.load(Regexp.last_match(1))
488
- merge_data!(data_file, :source => "YAML front matter") if data_file
489
- end
490
- end
491
-
492
- def read_post_data
493
- populate_title
494
- populate_categories
495
- populate_tags
496
- generate_excerpt
497
- end
498
-
499
- def handle_read_error(error)
500
- if error.is_a? Psych::SyntaxError
501
- Jekyll.logger.error "Error:", "YAML Exception reading #{path}: #{error.message}"
502
- else
503
- Jekyll.logger.error "Error:", "could not read file #{path}: #{error.message}"
504
- end
505
-
506
- if site.config["strict_front_matter"] || error.is_a?(Jekyll::Errors::FatalException)
507
- raise error
508
- end
509
- end
510
-
511
- def populate_title
512
- if relative_path =~ DATE_FILENAME_MATCHER
513
- date, slug, ext = Regexp.last_match.captures
514
- modify_date(date)
515
- elsif relative_path =~ DATELESS_FILENAME_MATCHER
516
- slug, ext = Regexp.last_match.captures
517
- end
518
- # `slug` will be nil for documents without an extension since the regex patterns
519
- # above tests for an extension as well.
520
- # In such cases, assign `basename_without_ext` as the slug.
521
- slug ||= basename_without_ext
522
-
523
- # slugs shouldn't end with a period
524
- # `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
525
- slug.gsub!(%r!\.*\z!, "")
526
-
527
- # Try to ensure the user gets a title.
528
- data["title"] ||= Utils.titleize_slug(slug)
529
- # Only overwrite slug & ext if they aren't specified.
530
- data["slug"] ||= slug
531
- data["ext"] ||= ext
532
- end
533
-
534
- def modify_date(date)
535
- if !data["date"] || data["date"].to_i == site.time.to_i
536
- merge_data!({ "date" => date }, :source => "filename")
537
- end
538
- end
539
-
540
- def generate_excerpt
541
- data["excerpt"] ||= Jekyll::Excerpt.new(self) if generate_excerpt?
542
- end
543
- end
544
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ class Document
5
+ include Comparable
6
+ extend Forwardable
7
+
8
+ attr_reader :path, :site, :extname, :collection, :type
9
+ attr_accessor :content, :output
10
+
11
+ def_delegator :self, :read_post_data, :post_read
12
+
13
+ YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m.freeze
14
+ DATELESS_FILENAME_MATCHER = %r!^(?:.+/)*(.*)(\.[^.]+)$!.freeze
15
+ DATE_FILENAME_MATCHER = %r!^(?>.+/)*?(\d{2,4}-\d{1,2}-\d{1,2})-([^/]*)(\.[^.]+)$!.freeze
16
+
17
+ SASS_FILE_EXTS = %w(.sass .scss).freeze
18
+ YAML_FILE_EXTS = %w(.yaml .yml).freeze
19
+
20
+ #
21
+
22
+ # Class-wide cache to stash and retrieve regexp to detect "super-directories"
23
+ # of a particular Jekyll::Document object.
24
+ #
25
+ # dirname - The *special directory* for the Document.
26
+ # e.g. "_posts" or "_drafts" for Documents from the `site.posts` collection.
27
+ def self.superdirs_regex(dirname)
28
+ @superdirs_regex ||= {}
29
+ @superdirs_regex[dirname] ||= %r!#{dirname}.*!
30
+ end
31
+
32
+ #
33
+
34
+ # Create a new Document.
35
+ #
36
+ # path - the path to the file
37
+ # relations - a hash with keys :site and :collection, the values of which
38
+ # are the Jekyll::Site and Jekyll::Collection to which this
39
+ # Document belong.
40
+ #
41
+ # Returns nothing.
42
+ def initialize(path, relations = {})
43
+ @site = relations[:site]
44
+ @path = path
45
+ @extname = File.extname(path)
46
+ @collection = relations[:collection]
47
+ @type = @collection.label.to_sym
48
+
49
+ @has_yaml_header = nil
50
+
51
+ if draft?
52
+ categories_from_path("_drafts")
53
+ else
54
+ categories_from_path(collection.relative_directory)
55
+ end
56
+
57
+ data.default_proc = proc do |_, key|
58
+ site.frontmatter_defaults.find(relative_path, type, key)
59
+ end
60
+
61
+ trigger_hooks(:post_init)
62
+ end
63
+
64
+ # Fetch the Document's data.
65
+ #
66
+ # Returns a Hash containing the data. An empty hash is returned if
67
+ # no data was read.
68
+ def data
69
+ @data ||= {}
70
+ end
71
+
72
+ # Merge some data in with this document's data.
73
+ #
74
+ # Returns the merged data.
75
+ def merge_data!(other, source: "YAML front matter")
76
+ merge_categories!(other)
77
+ Utils.deep_merge_hashes!(data, other)
78
+ merge_date!(source)
79
+ data
80
+ end
81
+
82
+ # Returns the document date. If metadata is not present then calculates it
83
+ # based on Jekyll::Site#time or the document file modification time.
84
+ #
85
+ # Return document date string.
86
+ def date
87
+ data["date"] ||= (draft? ? source_file_mtime : site.time)
88
+ end
89
+
90
+ # Return document file modification time in the form of a Time object.
91
+ #
92
+ # Return document file modification Time object.
93
+ def source_file_mtime
94
+ File.mtime(path)
95
+ end
96
+
97
+ # Returns whether the document is a draft. This is only the case if
98
+ # the document is in the 'posts' collection but in a different
99
+ # directory than '_posts'.
100
+ #
101
+ # Returns whether the document is a draft.
102
+ def draft?
103
+ data["draft"] ||= relative_path.index(collection.relative_directory).nil? &&
104
+ collection.label == "posts"
105
+ end
106
+
107
+ # The path to the document, relative to the collections_dir.
108
+ #
109
+ # Returns a String path which represents the relative path from the collections_dir
110
+ # to this document.
111
+ def relative_path
112
+ @relative_path ||= path.sub("#{site.collections_path}/", "")
113
+ end
114
+
115
+ # The output extension of the document.
116
+ #
117
+ # Returns the output extension
118
+ def output_ext
119
+ renderer.output_ext
120
+ end
121
+
122
+ # The base filename of the document, without the file extname.
123
+ #
124
+ # Returns the basename without the file extname.
125
+ def basename_without_ext
126
+ @basename_without_ext ||= File.basename(path, ".*")
127
+ end
128
+
129
+ # The base filename of the document.
130
+ #
131
+ # Returns the base filename of the document.
132
+ def basename
133
+ @basename ||= File.basename(path)
134
+ end
135
+
136
+ def renderer
137
+ @renderer ||= Jekyll::Renderer.new(site, self)
138
+ end
139
+
140
+ # Produces a "cleaned" relative path.
141
+ # The "cleaned" relative path is the relative path without the extname
142
+ # and with the collection's directory removed as well.
143
+ # This method is useful when building the URL of the document.
144
+ #
145
+ # NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)
146
+ #
147
+ # Examples:
148
+ # When relative_path is "_methods/site/generate...md":
149
+ # cleaned_relative_path
150
+ # # => "/site/generate"
151
+ #
152
+ # Returns the cleaned relative path of the document.
153
+ def cleaned_relative_path
154
+ @cleaned_relative_path ||=
155
+ relative_path[0..-extname.length - 1]
156
+ .sub(collection.relative_directory, "")
157
+ .gsub(%r!\.*\z!, "")
158
+ end
159
+
160
+ # Determine whether the document is a YAML file.
161
+ #
162
+ # Returns true if the extname is either .yml or .yaml, false otherwise.
163
+ def yaml_file?
164
+ YAML_FILE_EXTS.include?(extname)
165
+ end
166
+
167
+ # Determine whether the document is an asset file.
168
+ # Asset files include CoffeeScript files and Sass/SCSS files.
169
+ #
170
+ # Returns true if the extname belongs to the set of extensions
171
+ # that asset files use.
172
+ def asset_file?
173
+ sass_file? || coffeescript_file?
174
+ end
175
+
176
+ # Determine whether the document is a Sass file.
177
+ #
178
+ # Returns true if extname == .sass or .scss, false otherwise.
179
+ def sass_file?
180
+ SASS_FILE_EXTS.include?(extname)
181
+ end
182
+
183
+ # Determine whether the document is a CoffeeScript file.
184
+ #
185
+ # Returns true if extname == .coffee, false otherwise.
186
+ def coffeescript_file?
187
+ extname == ".coffee"
188
+ end
189
+
190
+ # Determine whether the file should be rendered with Liquid.
191
+ #
192
+ # Returns false if the document is either an asset file or a yaml file,
193
+ # or if the document doesn't contain any Liquid Tags or Variables,
194
+ # true otherwise.
195
+ def render_with_liquid?
196
+ return false if data["render_with_liquid"] == false
197
+
198
+ !(coffeescript_file? || yaml_file? || !Utils.has_liquid_construct?(content))
199
+ end
200
+
201
+ # Determine whether the file should be rendered with a layout.
202
+ #
203
+ # Returns true if the Front Matter specifies that `layout` is set to `none`.
204
+ def no_layout?
205
+ data["layout"] == "none"
206
+ end
207
+
208
+ # Determine whether the file should be placed into layouts.
209
+ #
210
+ # Returns false if the document is set to `layouts: none`, or is either an
211
+ # asset file or a yaml file. Returns true otherwise.
212
+ def place_in_layout?
213
+ !(asset_file? || yaml_file? || no_layout?)
214
+ end
215
+
216
+ # The URL template where the document would be accessible.
217
+ #
218
+ # Returns the URL template for the document.
219
+ def url_template
220
+ collection.url_template
221
+ end
222
+
223
+ # Construct a Hash of key-value pairs which contain a mapping between
224
+ # a key in the URL template and the corresponding value for this document.
225
+ #
226
+ # Returns the Hash of key-value pairs for replacement in the URL.
227
+ def url_placeholders
228
+ @url_placeholders ||= Drops::UrlDrop.new(self)
229
+ end
230
+
231
+ # The permalink for this Document.
232
+ # Permalink is set via the data Hash.
233
+ #
234
+ # Returns the permalink or nil if no permalink was set in the data.
235
+ def permalink
236
+ data && data.is_a?(Hash) && data["permalink"]
237
+ end
238
+
239
+ # The computed URL for the document. See `Jekyll::URL#to_s` for more details.
240
+ #
241
+ # Returns the computed URL for the document.
242
+ def url
243
+ @url ||= URL.new(
244
+ :template => url_template,
245
+ :placeholders => url_placeholders,
246
+ :permalink => permalink
247
+ ).to_s
248
+ end
249
+
250
+ def [](key)
251
+ data[key]
252
+ end
253
+
254
+ # The full path to the output file.
255
+ #
256
+ # base_directory - the base path of the output directory
257
+ #
258
+ # Returns the full path to the output file of this document.
259
+ def destination(base_directory)
260
+ @destination ||= {}
261
+ @destination[base_directory] ||= begin
262
+ path = site.in_dest_dir(base_directory, URL.unescape_path(url))
263
+ if url.end_with? "/"
264
+ path = File.join(path, "index.html")
265
+ else
266
+ path << output_ext unless path.end_with? output_ext
267
+ end
268
+ path
269
+ end
270
+ end
271
+
272
+ # Write the generated Document file to the destination directory.
273
+ #
274
+ # dest - The String path to the destination dir.
275
+ #
276
+ # Returns nothing.
277
+ def write(dest)
278
+ path = destination(dest)
279
+ FileUtils.mkdir_p(File.dirname(path))
280
+ Jekyll.logger.debug "Writing:", path
281
+ File.write(path, output, :mode => "wb")
282
+
283
+ trigger_hooks(:post_write)
284
+ end
285
+
286
+ # Whether the file is published or not, as indicated in YAML front-matter
287
+ #
288
+ # Returns 'false' if the 'published' key is specified in the
289
+ # YAML front-matter and is 'false'. Otherwise returns 'true'.
290
+ def published?
291
+ !(data.key?("published") && data["published"] == false)
292
+ end
293
+
294
+ # Read in the file and assign the content and data based on the file contents.
295
+ # Merge the frontmatter of the file with the frontmatter default
296
+ # values
297
+ #
298
+ # Returns nothing.
299
+ def read(opts = {})
300
+ Jekyll.logger.debug "Reading:", relative_path
301
+
302
+ if yaml_file?
303
+ @data = SafeYAML.load_file(path)
304
+ else
305
+ begin
306
+ merge_defaults
307
+ read_content(**opts)
308
+ read_post_data
309
+ rescue StandardError => e
310
+ handle_read_error(e)
311
+ end
312
+ end
313
+ end
314
+
315
+ # Create a Liquid-understandable version of this Document.
316
+ #
317
+ # Returns a Hash representing this Document's data.
318
+ def to_liquid
319
+ @to_liquid ||= Drops::DocumentDrop.new(self)
320
+ end
321
+
322
+ # The inspect string for this document.
323
+ # Includes the relative path and the collection label.
324
+ #
325
+ # Returns the inspect string for this document.
326
+ def inspect
327
+ "#<#{self.class} #{relative_path} collection=#{collection.label}>"
328
+ end
329
+
330
+ # The string representation for this document.
331
+ #
332
+ # Returns the content of the document
333
+ def to_s
334
+ output || content || "NO CONTENT"
335
+ end
336
+
337
+ # Compare this document against another document.
338
+ # Comparison is a comparison between the 2 paths of the documents.
339
+ #
340
+ # Returns -1, 0, +1 or nil depending on whether this doc's path is less than,
341
+ # equal or greater than the other doc's path. See String#<=> for more details.
342
+ def <=>(other)
343
+ return nil unless other.respond_to?(:data)
344
+
345
+ cmp = data["date"] <=> other.data["date"]
346
+ cmp = path <=> other.path if cmp.nil? || cmp.zero?
347
+ cmp
348
+ end
349
+
350
+ # Determine whether this document should be written.
351
+ # Based on the Collection to which it belongs.
352
+ #
353
+ # True if the document has a collection and if that collection's #write?
354
+ # method returns true, and if the site's Publisher will publish the document.
355
+ # False otherwise.
356
+ #
357
+ # rubocop:disable Naming/MemoizedInstanceVariableName
358
+ def write?
359
+ return @write_p if defined?(@write_p)
360
+
361
+ @write_p = collection&.write? && site.publisher.publish?(self)
362
+ end
363
+ # rubocop:enable Naming/MemoizedInstanceVariableName
364
+
365
+ # The Document excerpt_separator, from the YAML Front-Matter or site
366
+ # default excerpt_separator value
367
+ #
368
+ # Returns the document excerpt_separator
369
+ def excerpt_separator
370
+ @excerpt_separator ||= (data["excerpt_separator"] || site.config["excerpt_separator"]).to_s
371
+ end
372
+
373
+ # Whether to generate an excerpt
374
+ #
375
+ # Returns true if the excerpt separator is configured.
376
+ def generate_excerpt?
377
+ !excerpt_separator.empty?
378
+ end
379
+
380
+ def next_doc
381
+ pos = collection.docs.index { |post| post.equal?(self) }
382
+ collection.docs[pos + 1] if pos && pos < collection.docs.length - 1
383
+ end
384
+
385
+ def previous_doc
386
+ pos = collection.docs.index { |post| post.equal?(self) }
387
+ collection.docs[pos - 1] if pos && pos.positive?
388
+ end
389
+
390
+ def trigger_hooks(hook_name, *args)
391
+ Jekyll::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection
392
+ Jekyll::Hooks.trigger :documents, hook_name, self, *args
393
+ end
394
+
395
+ def id
396
+ @id ||= File.join(File.dirname(url), (data["slug"] || basename_without_ext).to_s)
397
+ end
398
+
399
+ # Calculate related posts.
400
+ #
401
+ # Returns an Array of related Posts.
402
+ def related_posts
403
+ @related_posts ||= Jekyll::RelatedPosts.new(self).build
404
+ end
405
+
406
+ # Override of method_missing to check in @data for the key.
407
+ def method_missing(method, *args, &blck)
408
+ if data.key?(method.to_s)
409
+ Jekyll::Deprecator.deprecation_message "Document##{method} is now a key "\
410
+ "in the #data hash."
411
+ Jekyll::Deprecator.deprecation_message "Called by #{caller(0..0)}."
412
+ data[method.to_s]
413
+ else
414
+ super
415
+ end
416
+ end
417
+
418
+ def respond_to_missing?(method, *)
419
+ data.key?(method.to_s) || super
420
+ end
421
+
422
+ # Add superdirectories of the special_dir to categories.
423
+ # In the case of es/_posts, 'es' is added as a category.
424
+ # In the case of _posts/es, 'es' is NOT added as a category.
425
+ #
426
+ # Returns nothing.
427
+ def categories_from_path(special_dir)
428
+ if relative_path.start_with?(special_dir)
429
+ superdirs = []
430
+ else
431
+ superdirs = relative_path.sub(Document.superdirs_regex(special_dir), "")
432
+ superdirs = superdirs.split(File::SEPARATOR)
433
+ superdirs.reject! { |c| c.empty? || c == special_dir || c == basename }
434
+ end
435
+
436
+ merge_data!({ "categories" => superdirs }, :source => "file path")
437
+ end
438
+
439
+ def populate_categories
440
+ categories = Array(data["categories"]) + Utils.pluralized_array_from_hash(
441
+ data, "category", "categories"
442
+ )
443
+ categories.map!(&:to_s)
444
+ categories.flatten!
445
+ categories.uniq!
446
+
447
+ merge_data!({ "categories" => categories })
448
+ end
449
+
450
+ def populate_tags
451
+ tags = Utils.pluralized_array_from_hash(data, "tag", "tags")
452
+ tags.flatten!
453
+
454
+ merge_data!({ "tags" => tags })
455
+ end
456
+
457
+ private
458
+
459
+ def merge_categories!(other)
460
+ if other.key?("categories") && !other["categories"].nil?
461
+ other["categories"] = other["categories"].split if other["categories"].is_a?(String)
462
+
463
+ if data["categories"].is_a?(Array)
464
+ other["categories"] = data["categories"] | other["categories"]
465
+ end
466
+ end
467
+ end
468
+
469
+ def merge_date!(source)
470
+ if data.key?("date")
471
+ data["date"] = Utils.parse_date(
472
+ data["date"].to_s,
473
+ "Document '#{relative_path}' does not have a valid date in the #{source}."
474
+ )
475
+ end
476
+ end
477
+
478
+ def merge_defaults
479
+ defaults = @site.frontmatter_defaults.all(relative_path, type)
480
+ merge_data!(defaults, :source => "front matter defaults") unless defaults.empty?
481
+ end
482
+
483
+ def read_content(**opts)
484
+ self.content = File.read(path, **Utils.merged_file_read_opts(site, opts))
485
+ if content =~ YAML_FRONT_MATTER_REGEXP
486
+ self.content = Regexp.last_match.post_match
487
+ data_file = SafeYAML.load(Regexp.last_match(1))
488
+ merge_data!(data_file, :source => "YAML front matter") if data_file
489
+ end
490
+ end
491
+
492
+ def read_post_data
493
+ populate_title
494
+ populate_categories
495
+ populate_tags
496
+ generate_excerpt
497
+ end
498
+
499
+ def handle_read_error(error)
500
+ if error.is_a? Psych::SyntaxError
501
+ Jekyll.logger.error "Error:", "YAML Exception reading #{path}: #{error.message}"
502
+ else
503
+ Jekyll.logger.error "Error:", "could not read file #{path}: #{error.message}"
504
+ end
505
+
506
+ if site.config["strict_front_matter"] || error.is_a?(Jekyll::Errors::FatalException)
507
+ raise error
508
+ end
509
+ end
510
+
511
+ def populate_title
512
+ if relative_path =~ DATE_FILENAME_MATCHER
513
+ date, slug, ext = Regexp.last_match.captures
514
+ modify_date(date)
515
+ elsif relative_path =~ DATELESS_FILENAME_MATCHER
516
+ slug, ext = Regexp.last_match.captures
517
+ end
518
+ # `slug` will be nil for documents without an extension since the regex patterns
519
+ # above tests for an extension as well.
520
+ # In such cases, assign `basename_without_ext` as the slug.
521
+ slug ||= basename_without_ext
522
+
523
+ # slugs shouldn't end with a period
524
+ # `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
525
+ slug.gsub!(%r!\.*\z!, "")
526
+
527
+ # Try to ensure the user gets a title.
528
+ data["title"] ||= Utils.titleize_slug(slug)
529
+ # Only overwrite slug & ext if they aren't specified.
530
+ data["slug"] ||= slug
531
+ data["ext"] ||= ext
532
+ end
533
+
534
+ def modify_date(date)
535
+ if !data["date"] || data["date"].to_i == site.time.to_i
536
+ merge_data!({ "date" => date }, :source => "filename")
537
+ end
538
+ end
539
+
540
+ def generate_excerpt
541
+ data["excerpt"] ||= Jekyll::Excerpt.new(self) if generate_excerpt?
542
+ end
543
+ end
544
+ end