bridgetown-core 0.19.1 → 0.21.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/bridgetown-core.gemspec +3 -4
  3. data/lib/bridgetown-core.rb +32 -16
  4. data/lib/bridgetown-core/cleaner.rb +7 -1
  5. data/lib/bridgetown-core/collection.rb +176 -77
  6. data/lib/bridgetown-core/commands/apply.rb +4 -3
  7. data/lib/bridgetown-core/commands/base.rb +9 -0
  8. data/lib/bridgetown-core/commands/build.rb +0 -11
  9. data/lib/bridgetown-core/commands/configure.rb +66 -0
  10. data/lib/bridgetown-core/commands/console.rb +4 -0
  11. data/lib/bridgetown-core/commands/doctor.rb +1 -19
  12. data/lib/bridgetown-core/commands/new.rb +8 -0
  13. data/lib/bridgetown-core/commands/plugins.rb +1 -0
  14. data/lib/bridgetown-core/commands/serve.rb +0 -14
  15. data/lib/bridgetown-core/component.rb +178 -0
  16. data/lib/bridgetown-core/concerns/data_accessible.rb +1 -0
  17. data/lib/bridgetown-core/concerns/front_matter_importer.rb +52 -0
  18. data/lib/bridgetown-core/concerns/site/configurable.rb +7 -3
  19. data/lib/bridgetown-core/concerns/site/content.rb +56 -15
  20. data/lib/bridgetown-core/concerns/site/processable.rb +1 -0
  21. data/lib/bridgetown-core/concerns/site/renderable.rb +26 -0
  22. data/lib/bridgetown-core/concerns/site/writable.rb +12 -2
  23. data/lib/bridgetown-core/concerns/validatable.rb +1 -4
  24. data/lib/bridgetown-core/configuration.rb +50 -29
  25. data/lib/bridgetown-core/configurations/.keep +0 -0
  26. data/lib/bridgetown-core/configurations/bt-postcss.rb +26 -0
  27. data/lib/bridgetown-core/configurations/bt-postcss/postcss.config.js +21 -0
  28. data/lib/bridgetown-core/configurations/minitesting.rb +95 -0
  29. data/lib/bridgetown-core/configurations/netlify.rb +6 -0
  30. data/lib/bridgetown-core/configurations/netlify/netlify.sh +14 -0
  31. data/lib/bridgetown-core/configurations/netlify/netlify.toml +44 -0
  32. data/lib/bridgetown-core/configurations/purgecss.rb +49 -0
  33. data/lib/bridgetown-core/configurations/stimulus.rb +49 -0
  34. data/lib/bridgetown-core/configurations/swup.rb +37 -0
  35. data/lib/bridgetown-core/configurations/tailwindcss.rb +29 -0
  36. data/lib/bridgetown-core/configurations/tailwindcss/css_imports.css +4 -0
  37. data/lib/bridgetown-core/configurations/tailwindcss/postcss.config.js +12 -0
  38. data/lib/bridgetown-core/configurations/turbo.rb +16 -0
  39. data/lib/bridgetown-core/converter.rb +23 -0
  40. data/lib/bridgetown-core/converters/erb_templates.rb +50 -41
  41. data/lib/bridgetown-core/converters/identity.rb +0 -9
  42. data/lib/bridgetown-core/converters/markdown.rb +14 -4
  43. data/lib/bridgetown-core/converters/markdown/kramdown_parser.rb +5 -38
  44. data/lib/bridgetown-core/converters/ruby_templates.rb +17 -0
  45. data/lib/bridgetown-core/converters/smartypants.rb +3 -1
  46. data/lib/bridgetown-core/current.rb +10 -0
  47. data/lib/bridgetown-core/document.rb +7 -14
  48. data/lib/bridgetown-core/drops/collection_drop.rb +1 -1
  49. data/lib/bridgetown-core/drops/page_drop.rb +4 -0
  50. data/lib/bridgetown-core/drops/relations_drop.rb +23 -0
  51. data/lib/bridgetown-core/drops/resource_drop.rb +83 -0
  52. data/lib/bridgetown-core/drops/site_drop.rb +33 -8
  53. data/lib/bridgetown-core/drops/unified_payload_drop.rb +5 -0
  54. data/lib/bridgetown-core/entry_filter.rb +12 -25
  55. data/lib/bridgetown-core/errors.rb +0 -2
  56. data/lib/bridgetown-core/filters.rb +4 -27
  57. data/lib/bridgetown-core/filters/from_liquid.rb +23 -0
  58. data/lib/bridgetown-core/filters/url_filters.rb +12 -0
  59. data/lib/bridgetown-core/generator.rb +2 -1
  60. data/lib/bridgetown-core/generators/prototype_generator.rb +37 -19
  61. data/lib/bridgetown-core/helpers.rb +48 -9
  62. data/lib/bridgetown-core/layout.rb +28 -13
  63. data/lib/bridgetown-core/liquid_renderer/file.rb +1 -0
  64. data/lib/bridgetown-core/liquid_renderer/table.rb +1 -0
  65. data/lib/bridgetown-core/model/base.rb +138 -0
  66. data/lib/bridgetown-core/model/builder_origin.rb +40 -0
  67. data/lib/bridgetown-core/model/origin.rb +38 -0
  68. data/lib/bridgetown-core/model/repo_origin.rb +126 -0
  69. data/lib/bridgetown-core/page.rb +11 -2
  70. data/lib/bridgetown-core/plugin.rb +2 -26
  71. data/lib/bridgetown-core/plugin_manager.rb +1 -3
  72. data/lib/bridgetown-core/publisher.rb +7 -1
  73. data/lib/bridgetown-core/reader.rb +36 -21
  74. data/lib/bridgetown-core/readers/data_reader.rb +4 -4
  75. data/lib/bridgetown-core/readers/page_reader.rb +1 -0
  76. data/lib/bridgetown-core/readers/post_reader.rb +5 -4
  77. data/lib/bridgetown-core/regenerator.rb +8 -1
  78. data/lib/bridgetown-core/related_posts.rb +5 -5
  79. data/lib/bridgetown-core/renderer.rb +6 -13
  80. data/lib/bridgetown-core/resource/base.rb +317 -0
  81. data/lib/bridgetown-core/resource/destination.rb +49 -0
  82. data/lib/bridgetown-core/resource/permalink_processor.rb +179 -0
  83. data/lib/bridgetown-core/resource/relations.rb +132 -0
  84. data/lib/bridgetown-core/resource/taxonomy_term.rb +34 -0
  85. data/lib/bridgetown-core/resource/taxonomy_type.rb +56 -0
  86. data/lib/bridgetown-core/resource/transformer.rb +175 -0
  87. data/lib/bridgetown-core/ruby_template_view.rb +12 -4
  88. data/lib/bridgetown-core/site.rb +9 -1
  89. data/lib/bridgetown-core/static_file.rb +33 -10
  90. data/lib/bridgetown-core/tags/include.rb +1 -1
  91. data/lib/bridgetown-core/tags/post_url.rb +2 -2
  92. data/lib/bridgetown-core/url.rb +1 -0
  93. data/lib/bridgetown-core/utils.rb +49 -43
  94. data/lib/bridgetown-core/utils/require_gems.rb +60 -0
  95. data/lib/bridgetown-core/utils/ruby_exec.rb +6 -9
  96. data/lib/bridgetown-core/utils/ruby_front_matter.rb +39 -0
  97. data/lib/bridgetown-core/version.rb +2 -2
  98. data/lib/bridgetown-core/watcher.rb +1 -1
  99. data/lib/site_template/README.md +70 -0
  100. data/lib/site_template/package.json.erb +2 -2
  101. data/lib/site_template/src/_posts/0000-00-00-welcome-to-bridgetown.md.erb +1 -1
  102. data/lib/site_template/webpack.config.js.erb +26 -6
  103. metadata +56 -44
  104. data/lib/bridgetown-core/external.rb +0 -58
  105. data/lib/bridgetown-core/page_without_a_file.rb +0 -17
  106. data/lib/bridgetown-core/path_manager.rb +0 -31
  107. data/lib/bridgetown-core/readers/collection_reader.rb +0 -23
  108. data/lib/bridgetown-core/readers/static_file_reader.rb +0 -25
  109. data/lib/bridgetown-core/utils/exec.rb +0 -26
  110. data/lib/bridgetown-core/utils/internet.rb +0 -37
  111. data/lib/bridgetown-core/utils/platforms.rb +0 -80
  112. data/lib/bridgetown-core/utils/thread_event.rb +0 -31
  113. data/lib/bridgetown-core/utils/win_tz.rb +0 -75
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Resource
5
+ class Base
6
+ include Comparable
7
+ include Bridgetown::Publishable
8
+ include Bridgetown::LayoutPlaceable
9
+ include Bridgetown::LiquidRenderable
10
+
11
+ # @return [HashWithDotAccess::Hash]
12
+ attr_reader :data
13
+
14
+ # @return [Destination]
15
+ attr_reader :destination
16
+
17
+ # @return [Bridgetown::Model::Base]
18
+ attr_reader :model
19
+
20
+ # @return [Bridgetown::Site]
21
+ attr_reader :site
22
+
23
+ # @return [String]
24
+ attr_accessor :content, :untransformed_content, :output
25
+
26
+ DATE_FILENAME_MATCHER = %r!^(?>.+/)*?(\d{2,4}-\d{1,2}-\d{1,2})-([^/]*)(\.[^.]+)$!.freeze
27
+
28
+ # @param site [Bridgetown::Site]
29
+ # @param origin [Bridgetown::Resource::Origin]
30
+ def initialize(model:)
31
+ @model = model
32
+ @site = model.site
33
+ self.data = HashWithDotAccess::Hash.new
34
+
35
+ trigger_hooks(:post_init)
36
+ end
37
+
38
+ # Collection associated with this resource
39
+ #
40
+ # @return [Bridgetown::Collection]
41
+ def collection
42
+ model.collection
43
+ end
44
+
45
+ # The relative path of source file or file-like origin
46
+ #
47
+ # @return [Pathname]
48
+ def relative_path
49
+ model.origin.relative_path
50
+ end
51
+
52
+ # @return [Bridgetown::Resource::Transformer]
53
+ def transformer
54
+ @transformer ||= Bridgetown::Resource::Transformer.new(self)
55
+ end
56
+
57
+ # @return [Bridgetown::Resource::Relations]
58
+ def relations
59
+ @relations ||= Bridgetown::Resource::Relations.new(self)
60
+ end
61
+
62
+ # @param new_data [HashWithDotAccess::Hash]
63
+ def data=(new_data)
64
+ unless new_data.is_a?(HashWithDotAccess::Hash)
65
+ raise "#{self.class} data should be of type HashWithDotAccess::Hash"
66
+ end
67
+
68
+ @data = new_data
69
+ @data.default_proc = proc do |_, key|
70
+ site.frontmatter_defaults.find(
71
+ relative_path.to_s,
72
+ collection.label.to_sym,
73
+ key.to_s
74
+ )
75
+ end
76
+ end
77
+
78
+ # @return [Bridgetown::Resource::Base]
79
+ def read!
80
+ self.data = model.data_attributes
81
+ self.content = model.content # could be nil
82
+
83
+ unless collection.data?
84
+ self.untransformed_content = content
85
+ normalize_categories_and_tags
86
+ import_taxonomies_from_data
87
+ ensure_default_data
88
+ transformer.execute_inline_ruby!
89
+ set_date_from_string(data.date)
90
+ end
91
+
92
+ @destination = Destination.new(self) if requires_destination?
93
+
94
+ trigger_hooks(:post_read)
95
+
96
+ self
97
+ end
98
+ alias_method :read, :read! # TODO: eventually use the bang version only
99
+
100
+ def transform!
101
+ transformer.process! if output_allowed?
102
+ end
103
+
104
+ def trigger_hooks(hook_name, *args)
105
+ Bridgetown::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection
106
+ Bridgetown::Hooks.trigger :resources, hook_name, self, *args
107
+ end
108
+
109
+ def around_hook(hook_suffix)
110
+ trigger_hooks :"pre_#{hook_suffix}"
111
+ yield
112
+ trigger_hooks :"post_#{hook_suffix}"
113
+ end
114
+
115
+ # @return [String]
116
+ def relative_path_basename_without_prefix
117
+ return_path = Pathname.new("")
118
+ relative_path.each_filename do |filename|
119
+ if matches = DATE_FILENAME_MATCHER.match(filename) # rubocop:disable Lint/AssignmentInCondition
120
+ filename = matches[2] + matches[3]
121
+ end
122
+
123
+ return_path += filename unless filename.starts_with?("_")
124
+ end
125
+
126
+ (return_path.dirname + return_path.basename(".*")).to_s
127
+ end
128
+
129
+ # @return [String]
130
+ def basename_without_ext
131
+ relative_path.basename(".*").to_s
132
+ end
133
+
134
+ # @return [String]
135
+ def extname
136
+ relative_path.extname
137
+ end
138
+
139
+ # @return [String, nil]
140
+ def permalink
141
+ data&.permalink
142
+ end
143
+
144
+ # @return [String]
145
+ def path
146
+ (model.origin.respond_to?(:original_path) ? model.origin.original_path : relative_path).to_s
147
+ end
148
+
149
+ # @return [String]
150
+ def absolute_url
151
+ format_url destination&.absolute_url
152
+ end
153
+
154
+ # @return [String]
155
+ def relative_url
156
+ format_url destination&.relative_url
157
+ end
158
+
159
+ # @return [String]
160
+ def id
161
+ model.origin.id
162
+ end
163
+
164
+ def date
165
+ data["date"] ||= site.time
166
+ end
167
+
168
+ # @return [Hash<String, Hash<String => Bridgetown::Resource::TaxonomyType,
169
+ # Array<Bridgetown::Resource::TaxonomyTerm>>>]
170
+ def taxonomies
171
+ @taxonomies ||= site.taxonomy_types.values.each_with_object(
172
+ HashWithDotAccess::Hash.new
173
+ ) do |taxonomy, hsh|
174
+ hsh[taxonomy.label] = {
175
+ type: taxonomy,
176
+ terms: [],
177
+ }
178
+ end
179
+ end
180
+
181
+ def output_allowed?
182
+ !collection.data? && data.config&.output != false
183
+ end
184
+
185
+ def requires_destination?
186
+ collection.write? && output_allowed?
187
+ end
188
+
189
+ def write?
190
+ requires_destination? && site.publisher.publish?(self)
191
+ end
192
+
193
+ # Write the generated Document file to the destination directory.
194
+ #
195
+ # dest - The String path to the destination dir.
196
+ #
197
+ # Returns nothing.
198
+ def write(_dest = nil)
199
+ destination.write(output)
200
+ trigger_hooks(:post_write)
201
+ end
202
+
203
+ def to_s
204
+ output || content || ""
205
+ end
206
+
207
+ # Create a Liquid-understandable version of this resource.
208
+ #
209
+ # @return [Drops::ResourceDrop] represents this resource's data.
210
+ def to_liquid
211
+ @to_liquid ||= Drops::ResourceDrop.new(self)
212
+ end
213
+
214
+ def to_h
215
+ {
216
+ id: id,
217
+ absolute_url: absolute_url,
218
+ relative_path: relative_path,
219
+ relative_url: relative_url,
220
+ date: date,
221
+ data: data,
222
+ taxonomies: taxonomies,
223
+ untransformed_content: untransformed_content,
224
+ content: content,
225
+ output: output,
226
+ }
227
+ end
228
+
229
+ def as_json(*)
230
+ to_h
231
+ end
232
+
233
+ ruby2_keywords def to_json(*options)
234
+ as_json(*options).to_json(*options)
235
+ end
236
+
237
+ def inspect
238
+ "#<#{self.class} #{id}>"
239
+ end
240
+
241
+ # Compare this document against another document.
242
+ # Comparison is a comparison between the 2 paths of the documents.
243
+ #
244
+ # Returns -1, 0, +1 or nil depending on whether this doc's path is less than,
245
+ # equal or greater than the other doc's path. See String#<=> for more details.
246
+ def <=>(other) # rubocop:todo Metrics/AbcSize
247
+ return nil unless other.respond_to?(:data)
248
+
249
+ if data.date.respond_to?(:to_datetime) && other.data.date.respond_to?(:to_datetime)
250
+ return data.date.to_datetime <=> other.data.date.to_datetime
251
+ end
252
+
253
+ cmp = data["date"] <=> other.data["date"]
254
+ cmp = path <=> other.path if cmp.nil? || cmp.zero?
255
+ cmp
256
+ end
257
+
258
+ def next_resource
259
+ pos = collection.resources.index { |item| item.equal?(self) }
260
+ collection.resources[pos + 1] if pos && pos < collection.resources.length - 1
261
+ end
262
+ alias_method :next_doc, :next_resource
263
+
264
+ def previous_resource
265
+ pos = collection.docs.index { |item| item.equal?(self) }
266
+ collection.resources[pos - 1] if pos&.positive?
267
+ end
268
+ alias_method :previous_doc, :previous_resource
269
+
270
+ private
271
+
272
+ def ensure_default_data
273
+ slug = if matches = relative_path.to_s.match(DATE_FILENAME_MATCHER) # rubocop:disable Lint/AssignmentInCondition
274
+ set_date_from_string(matches[1]) unless data.date
275
+ matches[2]
276
+ else
277
+ basename_without_ext
278
+ end
279
+
280
+ data.slug ||= slug
281
+ data.title ||= Bridgetown::Utils.titleize_slug(slug)
282
+ end
283
+
284
+ def set_date_from_string(new_date) # rubocop:disable Naming/AccessorMethodName
285
+ return unless new_date.is_a?(String)
286
+
287
+ data.date = Bridgetown::Utils.parse_date(
288
+ new_date,
289
+ "Document '#{relative_path}' does not have a valid date in the #{model}."
290
+ )
291
+ end
292
+
293
+ def normalize_categories_and_tags
294
+ data.categories = Bridgetown::Utils.pluralized_array_from_hash(
295
+ data, :category, :categories
296
+ )
297
+ data.tags = Bridgetown::Utils.pluralized_array_from_hash(
298
+ data, :tag, :tags
299
+ )
300
+ end
301
+
302
+ def import_taxonomies_from_data
303
+ taxonomies.each do |_label, metadata|
304
+ Array(data[metadata.type.key]).each do |term|
305
+ metadata.terms << TaxonomyTerm.new(
306
+ resource: self, label: term, type: metadata.type
307
+ )
308
+ end
309
+ end
310
+ end
311
+
312
+ def format_url(url)
313
+ url.to_s.sub(%r{index\.html?$}, "").sub(%r{\.html?$}, "")
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Resource
5
+ class Destination
6
+ # @return [Bridgetown::Resource::Base]
7
+ attr_accessor :resource
8
+
9
+ # @return [String]
10
+ attr_accessor :output_ext
11
+
12
+ # @param resource [Bridgetown::Resource::Base]
13
+ def initialize(resource)
14
+ @resource = resource
15
+ @output_ext = resource.transformer.final_ext
16
+ end
17
+
18
+ def absolute_url
19
+ Addressable::URI.parse(
20
+ resource.site.config.url.to_s + relative_url
21
+ ).normalize.to_s
22
+ end
23
+
24
+ def relative_url
25
+ @processor ||= PermalinkProcessor.new(resource)
26
+ @processor.transform
27
+ end
28
+
29
+ def final_ext
30
+ output_ext || resource.extname
31
+ end
32
+
33
+ def output_path
34
+ path = URL.unescape_path(relative_url)
35
+ path = path.delete_prefix(resource.site.baseurl) if resource.site.baseurl.present?
36
+ path = resource.site.in_dest_dir(path)
37
+ path = File.join(path, "index.html") if relative_url.end_with? "/"
38
+ path
39
+ end
40
+
41
+ def write(output)
42
+ path = output_path
43
+ FileUtils.mkdir_p(File.dirname(path))
44
+ Bridgetown.logger.debug "Writing:", path
45
+ File.write(path, output, mode: "wb")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Resource
5
+ class PermalinkProcessor
6
+ # @return [Bridgetown::Resource::Base]
7
+ attr_accessor :resource
8
+
9
+ attr_accessor :slugify_mode
10
+
11
+ def self.placeholder_processors
12
+ @placeholder_processors || {}
13
+ end
14
+
15
+ def self.register_placeholder(key, block)
16
+ @placeholder_processors ||= {}
17
+ @placeholder_processors[key] = block
18
+ end
19
+
20
+ def initialize(resource)
21
+ @resource = resource
22
+ @slugify_mode = @resource.site.config.slugify_mode
23
+ end
24
+
25
+ def final_ext
26
+ resource.method(:destination).arity == 1 ? resource.extname : resource.destination.final_ext
27
+ end
28
+
29
+ def transform
30
+ permalink = resource.data.permalink ||
31
+ permalink_for_permalink_style(resource.collection.default_permalink)
32
+
33
+ # Strip out file extension and process each segment of a URL to swap out
34
+ # placeholders such as :categories or :title
35
+ url_segments = Bridgetown::Filters::URLFilters.strip_extname(permalink).split("/")
36
+ new_url = url_segments.map do |segment|
37
+ segment.starts_with?(":") ? process_segment(segment.sub(%r{^:}, "")) : segment
38
+ end.select(&:present?).join("/")
39
+
40
+ # No relative URLs should ever end in /index.html
41
+ new_url.sub!(%r{/index$}, "") if final_ext == ".html"
42
+
43
+ add_baseurl finalize_permalink(new_url, permalink)
44
+ end
45
+
46
+ def process_segment(segment)
47
+ segment = segment.to_sym
48
+ if self.class.placeholder_processors[segment]
49
+ segment_value = self.class.placeholder_processors[segment].(resource)
50
+ if segment_value.is_a?(Hash)
51
+ segment_value[:raw_value]
52
+ elsif segment_value.is_a?(Array)
53
+ segment_value.map do |subsegment|
54
+ Utils.slugify(subsegment, mode: slugify_mode)
55
+ end.join("/")
56
+ else
57
+ Utils.slugify(segment_value, mode: slugify_mode)
58
+ end
59
+ else
60
+ segment
61
+ end
62
+ end
63
+
64
+ def permalink_for_permalink_style(permalink_style)
65
+ collection_prefix = ("/:collection" unless resource.collection.builtin?)
66
+
67
+ case permalink_style.to_sym
68
+ when :pretty
69
+ "#{collection_prefix}/:categories/:year/:month/:day/:slug/"
70
+ when :pretty_ext, :date
71
+ "#{collection_prefix}/:categories/:year/:month/:day/:slug.*"
72
+ when :simple
73
+ "#{collection_prefix}/:categories/:slug/"
74
+ when :simple_ext
75
+ "#{collection_prefix}/:categories/:slug.*"
76
+ else
77
+ permalink_style.to_s
78
+ end
79
+ end
80
+
81
+ # @param new_url [String]
82
+ # @param permalink [String]
83
+ def finalize_permalink(new_url, permalink)
84
+ # Handle .* style permalinks or files not ending in .html
85
+ if permalink.ends_with?(".*") || !%r{\.html?$}.match?(final_ext)
86
+ "/#{new_url}#{final_ext}"
87
+ # If permalink includes the file extension, add it back in to the URL
88
+ elsif permalink =~ %r{\.[^/]*$}
89
+ "/#{new_url}#{Regexp.last_match[0]}"
90
+ # Ensure index-style URLs get output correctly
91
+ elsif permalink.ends_with?("/")
92
+ "/#{new_url}/".sub(%r{^/index/$}, "/")
93
+ # We good :)
94
+ else
95
+ "/#{new_url}"
96
+ end
97
+ end
98
+
99
+ def add_baseurl(permalink)
100
+ resource.site.baseurl.present? ? "#{resource.site.baseurl}#{permalink}" : permalink
101
+ end
102
+
103
+ ### Default Placeholders Processors
104
+
105
+ # @param resource [Bridgetown::Resource::Base]
106
+ register_placeholder :path, ->(resource) do
107
+ { raw_value: resource.relative_path_basename_without_prefix }
108
+ end
109
+
110
+ # @param resource [Bridgetown::Resource::Base]
111
+ register_placeholder :name, ->(resource) do
112
+ resource.basename_without_ext
113
+ end
114
+
115
+ # @param resource [Bridgetown::Resource::Base]
116
+ register_placeholder :slug, ->(resource) do
117
+ resource.data.slug || placeholder_processors[:name].(resource)
118
+ end
119
+
120
+ # @param resource [Bridgetown::Resource::Base]
121
+ register_placeholder :title, ->(resource) do
122
+ resource.data.title || placeholder_processors[:slug].(resource)
123
+ end
124
+
125
+ # @param resource [Bridgetown::Resource::Base]
126
+ register_placeholder :locale, ->(resource) do
127
+ locale_data = resource.data.locale
128
+ resource.site.config.available_locales.include?(locale_data) ? locale_data : nil
129
+ end
130
+ register_placeholder :lang, placeholder_processors[:locale]
131
+
132
+ # @param resource [Bridgetown::Resource::Base]
133
+ register_placeholder :collection, ->(resource) do
134
+ resource.collection.label
135
+ end
136
+
137
+ # @param resource [Bridgetown::Resource::Base]
138
+ register_placeholder :categories, ->(resource) do
139
+ resource.taxonomies.category&.terms&.map(&:label)&.uniq
140
+ end
141
+
142
+ # YYYY
143
+ # @param resource [Bridgetown::Resource::Base]
144
+ register_placeholder :year, ->(resource) do
145
+ resource.date.strftime("%Y")
146
+ end
147
+
148
+ # MM: 01..12
149
+ # @param resource [Bridgetown::Resource::Base]
150
+ register_placeholder :month, ->(resource) do
151
+ resource.date.strftime("%m")
152
+ end
153
+
154
+ # DD: 01..31
155
+ # @param resource [Bridgetown::Resource::Base]
156
+ register_placeholder :day, ->(resource) do
157
+ resource.date.strftime("%d")
158
+ end
159
+
160
+ # D: 1..31
161
+ # @param resource [Bridgetown::Resource::Base]
162
+ register_placeholder :i_day, ->(resource) do
163
+ resource.date.strftime("%-d")
164
+ end
165
+
166
+ # M: 1..12
167
+ # @param resource [Bridgetown::Resource::Base]
168
+ register_placeholder :i_month, ->(resource) do
169
+ resource.date.strftime("%-m")
170
+ end
171
+
172
+ # YY: 00..99
173
+ # @param resource [Bridgetown::Resource::Base]
174
+ register_placeholder :short_year, ->(resource) do
175
+ resource.date.strftime("%y")
176
+ end
177
+ end
178
+ end
179
+ end