bridgetown-core 0.19.2 → 0.21.0.beta3

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/bridgetown-core.gemspec +1 -2
  3. data/lib/bridgetown-core.rb +37 -29
  4. data/lib/bridgetown-core/cleaner.rb +9 -3
  5. data/lib/bridgetown-core/collection.rb +177 -78
  6. data/lib/bridgetown-core/commands/base.rb +9 -0
  7. data/lib/bridgetown-core/commands/build.rb +0 -11
  8. data/lib/bridgetown-core/commands/concerns/git_helpers.rb +20 -0
  9. data/lib/bridgetown-core/commands/configure.rb +8 -3
  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 +2 -2
  13. data/lib/bridgetown-core/commands/plugins.rb +14 -13
  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 +10 -10
  19. data/lib/bridgetown-core/concerns/site/content.rb +56 -15
  20. data/lib/bridgetown-core/concerns/site/localizable.rb +3 -5
  21. data/lib/bridgetown-core/concerns/site/processable.rb +6 -4
  22. data/lib/bridgetown-core/concerns/site/renderable.rb +26 -0
  23. data/lib/bridgetown-core/concerns/site/writable.rb +12 -2
  24. data/lib/bridgetown-core/concerns/validatable.rb +2 -5
  25. data/lib/bridgetown-core/configuration.rb +51 -30
  26. data/lib/bridgetown-core/configurations/bt-postcss.rb +1 -3
  27. data/lib/bridgetown-core/configurations/netlify.rb +1 -0
  28. data/lib/bridgetown-core/configurations/tailwindcss.rb +1 -3
  29. data/lib/bridgetown-core/converter.rb +23 -0
  30. data/lib/bridgetown-core/converters/erb_templates.rb +51 -35
  31. data/lib/bridgetown-core/converters/identity.rb +0 -9
  32. data/lib/bridgetown-core/converters/liquid_templates.rb +1 -1
  33. data/lib/bridgetown-core/converters/markdown.rb +14 -4
  34. data/lib/bridgetown-core/converters/markdown/kramdown_parser.rb +5 -38
  35. data/lib/bridgetown-core/converters/ruby_templates.rb +17 -0
  36. data/lib/bridgetown-core/converters/smartypants.rb +3 -1
  37. data/lib/bridgetown-core/core_ext/psych.rb +19 -0
  38. data/lib/bridgetown-core/current.rb +10 -0
  39. data/lib/bridgetown-core/document.rb +9 -16
  40. data/lib/bridgetown-core/drops/collection_drop.rb +1 -1
  41. data/lib/bridgetown-core/drops/page_drop.rb +4 -0
  42. data/lib/bridgetown-core/drops/relations_drop.rb +23 -0
  43. data/lib/bridgetown-core/drops/resource_drop.rb +83 -0
  44. data/lib/bridgetown-core/drops/site_drop.rb +33 -8
  45. data/lib/bridgetown-core/drops/unified_payload_drop.rb +5 -0
  46. data/lib/bridgetown-core/entry_filter.rb +17 -28
  47. data/lib/bridgetown-core/errors.rb +0 -2
  48. data/lib/bridgetown-core/filters.rb +3 -26
  49. data/lib/bridgetown-core/filters/from_liquid.rb +23 -0
  50. data/lib/bridgetown-core/filters/url_filters.rb +12 -0
  51. data/lib/bridgetown-core/frontmatter_defaults.rb +1 -1
  52. data/lib/bridgetown-core/generators/prototype_generator.rb +37 -19
  53. data/lib/bridgetown-core/helpers.rb +48 -9
  54. data/lib/bridgetown-core/layout.rb +28 -13
  55. data/lib/bridgetown-core/liquid_renderer/file.rb +1 -0
  56. data/lib/bridgetown-core/liquid_renderer/table.rb +1 -0
  57. data/lib/bridgetown-core/model/base.rb +138 -0
  58. data/lib/bridgetown-core/model/builder_origin.rb +40 -0
  59. data/lib/bridgetown-core/model/origin.rb +38 -0
  60. data/lib/bridgetown-core/model/repo_origin.rb +126 -0
  61. data/lib/bridgetown-core/page.rb +11 -2
  62. data/lib/bridgetown-core/plugin_manager.rb +1 -3
  63. data/lib/bridgetown-core/publisher.rb +8 -2
  64. data/lib/bridgetown-core/reader.rb +37 -22
  65. data/lib/bridgetown-core/readers/data_reader.rb +5 -5
  66. data/lib/bridgetown-core/readers/defaults_reader.rb +1 -1
  67. data/lib/bridgetown-core/readers/page_reader.rb +1 -0
  68. data/lib/bridgetown-core/readers/post_reader.rb +5 -4
  69. data/lib/bridgetown-core/regenerator.rb +9 -2
  70. data/lib/bridgetown-core/related_posts.rb +9 -6
  71. data/lib/bridgetown-core/renderer.rb +6 -13
  72. data/lib/bridgetown-core/resource/base.rb +313 -0
  73. data/lib/bridgetown-core/resource/destination.rb +49 -0
  74. data/lib/bridgetown-core/resource/permalink_processor.rb +179 -0
  75. data/lib/bridgetown-core/resource/relations.rb +132 -0
  76. data/lib/bridgetown-core/resource/taxonomy_term.rb +34 -0
  77. data/lib/bridgetown-core/resource/taxonomy_type.rb +56 -0
  78. data/lib/bridgetown-core/resource/transformer.rb +177 -0
  79. data/lib/bridgetown-core/ruby_template_view.rb +11 -11
  80. data/lib/bridgetown-core/site.rb +13 -6
  81. data/lib/bridgetown-core/static_file.rb +33 -10
  82. data/lib/bridgetown-core/tags/highlight.rb +2 -15
  83. data/lib/bridgetown-core/tags/include.rb +1 -1
  84. data/lib/bridgetown-core/tags/post_url.rb +2 -2
  85. data/lib/bridgetown-core/url.rb +1 -0
  86. data/lib/bridgetown-core/utils.rb +49 -43
  87. data/lib/bridgetown-core/utils/require_gems.rb +60 -0
  88. data/lib/bridgetown-core/utils/ruby_exec.rb +6 -9
  89. data/lib/bridgetown-core/utils/ruby_front_matter.rb +39 -0
  90. data/lib/bridgetown-core/version.rb +2 -2
  91. data/lib/bridgetown-core/watcher.rb +1 -1
  92. data/lib/bridgetown-core/yaml_parser.rb +22 -0
  93. data/lib/site_template/package.json.erb +2 -2
  94. data/lib/site_template/plugins/site_builder.rb +1 -1
  95. data/lib/site_template/src/_posts/0000-00-00-welcome-to-bridgetown.md.erb +1 -1
  96. data/lib/site_template/webpack.config.js.erb +26 -6
  97. metadata +39 -40
  98. data/lib/bridgetown-core/external.rb +0 -58
  99. data/lib/bridgetown-core/page_without_a_file.rb +0 -17
  100. data/lib/bridgetown-core/path_manager.rb +0 -31
  101. data/lib/bridgetown-core/readers/collection_reader.rb +0 -23
  102. data/lib/bridgetown-core/readers/static_file_reader.rb +0 -25
  103. data/lib/bridgetown-core/utils/exec.rb +0 -26
  104. data/lib/bridgetown-core/utils/internet.rb +0 -37
  105. data/lib/bridgetown-core/utils/platforms.rb +0 -80
  106. data/lib/bridgetown-core/utils/thread_event.rb +0 -31
  107. data/lib/bridgetown-core/utils/win_tz.rb +0 -75
@@ -18,7 +18,7 @@ module Bridgetown
18
18
 
19
19
  entries.each do |entry|
20
20
  path = @site.in_source_dir(entry)
21
- @path_defaults[File.dirname(path) + File::SEPARATOR] = SafeYAML.load_file(path)
21
+ @path_defaults[File.dirname(path) + File::SEPARATOR] = YAMLParser.load_file(path)
22
22
  end
23
23
 
24
24
  @path_defaults
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bridgetown
4
+ # TODO: to be retired once the Resource engine is made official
4
5
  class PageReader
5
6
  attr_reader :site, :dir, :unfiltered_content
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bridgetown
4
+ # TODO: to be retired once the Resource engine is made official
4
5
  class PostReader
5
6
  attr_reader :site, :unfiltered_content
6
7
 
@@ -47,7 +48,7 @@ module Bridgetown
47
48
  # Process as Document
48
49
  Document.new(path,
49
50
  site: @site,
50
- collection: @site.posts)
51
+ collection: @site.collections.posts)
51
52
  else
52
53
  # Process as Static File
53
54
  read_static_file(
@@ -66,7 +67,7 @@ module Bridgetown
66
67
  site.source,
67
68
  relative_dir,
68
69
  File.basename(full_path),
69
- @site.posts
70
+ @site.collections.posts
70
71
  )
71
72
  end
72
73
 
@@ -96,9 +97,9 @@ module Bridgetown
96
97
  return false unless processable?(item)
97
98
 
98
99
  if item.is_a?(Document)
99
- site.posts.docs << item
100
+ site.collections.posts.docs << item
100
101
  elsif item.is_a?(StaticFile)
101
- site.posts.files << item
102
+ site.collections.posts.static_files << item
102
103
  site.static_files << item
103
104
  end
104
105
 
@@ -19,7 +19,7 @@ module Bridgetown
19
19
  # Checks if a renderable object needs to be regenerated
20
20
  #
21
21
  # Returns a boolean.
22
- def regenerate?(document)
22
+ def regenerate?(document) # rubocop:todo Metrics/CyclomaticComplexity
23
23
  return true if disabled
24
24
 
25
25
  case document
@@ -27,6 +27,8 @@ module Bridgetown
27
27
  regenerate_page?(document)
28
28
  when Document
29
29
  regenerate_document?(document)
30
+ when Bridgetown::Resource::Base
31
+ regenerate_resource?(document)
30
32
  else
31
33
  source_path = document.respond_to?(:path) ? document.path : nil
32
34
  dest_path = document.destination(@site.dest) if document.respond_to?(:destination)
@@ -152,7 +154,7 @@ module Bridgetown
152
154
  begin
153
155
  Marshal.load(content)
154
156
  rescue TypeError
155
- SafeYAML.load(content)
157
+ YAMLParser.load(content)
156
158
  rescue ArgumentError => e
157
159
  Bridgetown.logger.warn("Failed to load #{metadata_file}: #{e}")
158
160
  {}
@@ -176,6 +178,11 @@ module Bridgetown
176
178
  )
177
179
  end
178
180
 
181
+ # TODO: need to manage dependencies, but for now always regenerate
182
+ def regenerate_resource?(_resource)
183
+ true
184
+ end
185
+
179
186
  def existing_file_modified?(path)
180
187
  # If one of this file dependencies have been modified,
181
188
  # set the regeneration bit for both the dependency and the file to true
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bridgetown
4
+ # TODO: to be retired once the Resource engine is made official
4
5
  class RelatedPosts
5
6
  class << self
6
7
  attr_accessor :lsi
@@ -11,13 +12,15 @@ module Bridgetown
11
12
  def initialize(post)
12
13
  @post = post
13
14
  @site = post.site
14
- Bridgetown::External.require_with_graceful_fail("classifier-reborn") if site.lsi
15
+ if site.config.lsi
16
+ Bridgetown::Utils::RequireGems.require_with_graceful_fail("classifier-reborn")
17
+ end
15
18
  end
16
19
 
17
20
  def build
18
- return [] unless site.posts.docs.size > 1
21
+ return [] unless site.collections.posts.docs.size > 1
19
22
 
20
- if site.lsi
23
+ if site.config.lsi
21
24
  build_index
22
25
  lsi_related_posts
23
26
  else
@@ -30,7 +33,7 @@ module Bridgetown
30
33
  lsi = ClassifierReborn::LSI.new(auto_rebuild: false)
31
34
  Bridgetown.logger.info("Populating LSI...")
32
35
 
33
- site.posts.docs.each do |x|
36
+ site.collections.posts.docs.each do |x|
34
37
  lsi.add_item(x)
35
38
  end
36
39
 
@@ -42,11 +45,11 @@ module Bridgetown
42
45
  end
43
46
 
44
47
  def lsi_related_posts
45
- self.class.lsi.find_related(post, 11)
48
+ self.class.lsi.find_related(post, 11) - [post]
46
49
  end
47
50
 
48
51
  def most_recent_posts
49
- @most_recent_posts ||= (site.posts.docs.last(11).reverse - [post]).first(10)
52
+ @most_recent_posts ||= (site.collections.posts.docs.last(11).reverse - [post]).first(10)
50
53
  end
51
54
  end
52
55
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bridgetown
4
+ # This class handles the output rendering and layout placement of pages and
5
+ # documents. For rendering of resources in particular, see Bridgetown::Resource::Transformer
4
6
  class Renderer
5
7
  attr_reader :document, :site
6
8
 
@@ -50,7 +52,7 @@ module Bridgetown
50
52
  output = document.content
51
53
  Bridgetown.logger.debug "Rendering Markup:", document.relative_path
52
54
  output = convert(output.to_s, document)
53
- document.content = output
55
+ document.content = output.html_safe
54
56
 
55
57
  if document.place_in_layout?
56
58
  Bridgetown.logger.debug "Rendering Layout:", document.relative_path
@@ -121,21 +123,11 @@ module Bridgetown
121
123
  "in #{document.relative_path} does not exist."
122
124
  end
123
125
 
124
- def converters_for_layout(layout)
125
- site.converters.select do |converter|
126
- if converter.method(:matches).arity == 1
127
- converter.matches(layout.ext)
128
- else
129
- converter.matches(layout.ext, layout)
130
- end
131
- end.sort
132
- end
133
-
134
126
  # Render layout content into document.output
135
127
  #
136
128
  # Returns String rendered content
137
129
  def render_layout(output, layout)
138
- layout_converters = converters_for_layout(layout)
130
+ layout_converters = site.matched_converters_for_convertible(layout)
139
131
 
140
132
  layout_content = layout.content.dup
141
133
  layout_converters.reduce(layout_content) do |layout_output, converter|
@@ -163,7 +155,8 @@ module Bridgetown
163
155
 
164
156
  def permalink_ext
165
157
  document_permalink = document.permalink
166
- if document_permalink && !document_permalink.end_with?("/")
158
+ if document_permalink &&
159
+ !document_permalink.end_with?("/")
167
160
  permalink_ext = File.extname(document_permalink)
168
161
  permalink_ext unless permalink_ext.empty?
169
162
  end
@@ -0,0 +1,313 @@
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! unless collection.data?
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 requires_destination?
182
+ collection.write? && data.config&.output != false
183
+ end
184
+
185
+ def write?
186
+ requires_destination? && site.publisher.publish?(self)
187
+ end
188
+
189
+ # Write the generated Document file to the destination directory.
190
+ #
191
+ # dest - The String path to the destination dir.
192
+ #
193
+ # Returns nothing.
194
+ def write(_dest = nil)
195
+ destination.write(output)
196
+ trigger_hooks(:post_write)
197
+ end
198
+
199
+ def to_s
200
+ output || content || ""
201
+ end
202
+
203
+ # Create a Liquid-understandable version of this resource.
204
+ #
205
+ # @return [Drops::ResourceDrop] represents this resource's data.
206
+ def to_liquid
207
+ @to_liquid ||= Drops::ResourceDrop.new(self)
208
+ end
209
+
210
+ def to_h
211
+ {
212
+ id: id,
213
+ absolute_url: absolute_url,
214
+ relative_path: relative_path,
215
+ relative_url: relative_url,
216
+ date: date,
217
+ data: data,
218
+ taxonomies: taxonomies,
219
+ untransformed_content: untransformed_content,
220
+ content: content,
221
+ output: output,
222
+ }
223
+ end
224
+
225
+ def as_json(*)
226
+ to_h
227
+ end
228
+
229
+ ruby2_keywords def to_json(*options)
230
+ as_json(*options).to_json(*options)
231
+ end
232
+
233
+ def inspect
234
+ "#<#{self.class} #{id}>"
235
+ end
236
+
237
+ # Compare this document against another document.
238
+ # Comparison is a comparison between the 2 paths of the documents.
239
+ #
240
+ # Returns -1, 0, +1 or nil depending on whether this doc's path is less than,
241
+ # equal or greater than the other doc's path. See String#<=> for more details.
242
+ def <=>(other) # rubocop:todo Metrics/AbcSize
243
+ return nil unless other.respond_to?(:data)
244
+
245
+ if data.date.respond_to?(:to_datetime) && other.data.date.respond_to?(:to_datetime)
246
+ return data.date.to_datetime <=> other.data.date.to_datetime
247
+ end
248
+
249
+ cmp = data["date"] <=> other.data["date"]
250
+ cmp = path <=> other.path if cmp.nil? || cmp.zero?
251
+ cmp
252
+ end
253
+
254
+ def next_resource
255
+ pos = collection.resources.index { |item| item.equal?(self) }
256
+ collection.resources[pos + 1] if pos && pos < collection.resources.length - 1
257
+ end
258
+ alias_method :next_doc, :next_resource
259
+
260
+ def previous_resource
261
+ pos = collection.resources.index { |item| item.equal?(self) }
262
+ collection.resources[pos - 1] if pos&.positive?
263
+ end
264
+ alias_method :previous_doc, :previous_resource
265
+
266
+ private
267
+
268
+ def ensure_default_data
269
+ slug = if matches = relative_path.to_s.match(DATE_FILENAME_MATCHER) # rubocop:disable Lint/AssignmentInCondition
270
+ set_date_from_string(matches[1]) unless data.date
271
+ matches[2]
272
+ else
273
+ basename_without_ext
274
+ end
275
+
276
+ data.slug ||= slug
277
+ data.title ||= Bridgetown::Utils.titleize_slug(slug)
278
+ end
279
+
280
+ def set_date_from_string(new_date) # rubocop:disable Naming/AccessorMethodName
281
+ return unless new_date.is_a?(String)
282
+
283
+ data.date = Bridgetown::Utils.parse_date(
284
+ new_date,
285
+ "Document '#{relative_path}' does not have a valid date in the #{model}."
286
+ )
287
+ end
288
+
289
+ def normalize_categories_and_tags
290
+ data.categories = Bridgetown::Utils.pluralized_array_from_hash(
291
+ data, :category, :categories
292
+ )
293
+ data.tags = Bridgetown::Utils.pluralized_array_from_hash(
294
+ data, :tag, :tags
295
+ )
296
+ end
297
+
298
+ def import_taxonomies_from_data
299
+ taxonomies.each do |_label, metadata|
300
+ Array(data[metadata.type.key]).each do |term|
301
+ metadata.terms << TaxonomyTerm.new(
302
+ resource: self, label: term, type: metadata.type
303
+ )
304
+ end
305
+ end
306
+ end
307
+
308
+ def format_url(url)
309
+ url.to_s.sub(%r{index\.html?$}, "").sub(%r{\.html?$}, "")
310
+ end
311
+ end
312
+ end
313
+ end