bridgetown-core 0.19.3 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/bridgetown-core.gemspec +1 -1
  3. data/lib/bridgetown-core.rb +30 -11
  4. data/lib/bridgetown-core/cleaner.rb +7 -1
  5. data/lib/bridgetown-core/collection.rb +173 -77
  6. data/lib/bridgetown-core/commands/base.rb +9 -0
  7. data/lib/bridgetown-core/commands/configure.rb +4 -0
  8. data/lib/bridgetown-core/commands/console.rb +4 -0
  9. data/lib/bridgetown-core/concerns/data_accessible.rb +1 -0
  10. data/lib/bridgetown-core/concerns/site/configurable.rb +7 -3
  11. data/lib/bridgetown-core/concerns/site/content.rb +57 -15
  12. data/lib/bridgetown-core/concerns/site/processable.rb +1 -0
  13. data/lib/bridgetown-core/concerns/site/renderable.rb +26 -0
  14. data/lib/bridgetown-core/concerns/site/writable.rb +11 -1
  15. data/lib/bridgetown-core/concerns/validatable.rb +1 -0
  16. data/lib/bridgetown-core/configuration.rb +39 -19
  17. data/lib/bridgetown-core/converter.rb +14 -0
  18. data/lib/bridgetown-core/converters/identity.rb +0 -9
  19. data/lib/bridgetown-core/converters/markdown.rb +14 -4
  20. data/lib/bridgetown-core/converters/markdown/kramdown_parser.rb +3 -0
  21. data/lib/bridgetown-core/current.rb +10 -0
  22. data/lib/bridgetown-core/document.rb +6 -14
  23. data/lib/bridgetown-core/drops/collection_drop.rb +1 -1
  24. data/lib/bridgetown-core/drops/page_drop.rb +4 -0
  25. data/lib/bridgetown-core/drops/resource_drop.rb +81 -0
  26. data/lib/bridgetown-core/drops/site_drop.rb +33 -8
  27. data/lib/bridgetown-core/drops/unified_payload_drop.rb +4 -0
  28. data/lib/bridgetown-core/entry_filter.rb +10 -23
  29. data/lib/bridgetown-core/errors.rb +0 -2
  30. data/lib/bridgetown-core/filters.rb +2 -1
  31. data/lib/bridgetown-core/generators/prototype_generator.rb +37 -19
  32. data/lib/bridgetown-core/layout.rb +2 -2
  33. data/lib/bridgetown-core/liquid_renderer/file.rb +1 -0
  34. data/lib/bridgetown-core/liquid_renderer/table.rb +1 -0
  35. data/lib/bridgetown-core/model/base.rb +138 -0
  36. data/lib/bridgetown-core/model/builder_origin.rb +40 -0
  37. data/lib/bridgetown-core/model/file_origin.rb +119 -0
  38. data/lib/bridgetown-core/model/origin.rb +38 -0
  39. data/lib/bridgetown-core/page.rb +9 -1
  40. data/lib/bridgetown-core/plugin_manager.rb +0 -2
  41. data/lib/bridgetown-core/publisher.rb +7 -1
  42. data/lib/bridgetown-core/reader.rb +25 -12
  43. data/lib/bridgetown-core/readers/data_reader.rb +3 -4
  44. data/lib/bridgetown-core/readers/post_reader.rb +1 -1
  45. data/lib/bridgetown-core/regenerator.rb +8 -1
  46. data/lib/bridgetown-core/related_posts.rb +1 -1
  47. data/lib/bridgetown-core/renderer.rb +5 -12
  48. data/lib/bridgetown-core/resource/base.rb +275 -0
  49. data/lib/bridgetown-core/resource/destination.rb +49 -0
  50. data/lib/bridgetown-core/resource/permalink_processor.rb +179 -0
  51. data/lib/bridgetown-core/resource/taxonomy_term.rb +25 -0
  52. data/lib/bridgetown-core/resource/taxonomy_type.rb +47 -0
  53. data/lib/bridgetown-core/resource/transformer.rb +173 -0
  54. data/lib/bridgetown-core/ruby_template_view.rb +4 -0
  55. data/lib/bridgetown-core/site.rb +9 -1
  56. data/lib/bridgetown-core/static_file.rb +33 -10
  57. data/lib/bridgetown-core/url.rb +1 -0
  58. data/lib/bridgetown-core/utils.rb +40 -40
  59. data/lib/bridgetown-core/utils/platforms.rb +1 -0
  60. data/lib/bridgetown-core/version.rb +2 -2
  61. data/lib/site_template/webpack.config.js.erb +8 -6
  62. metadata +28 -21
  63. data/lib/bridgetown-core/page_without_a_file.rb +0 -17
  64. data/lib/bridgetown-core/readers/collection_reader.rb +0 -23
  65. data/lib/bridgetown-core/utils/exec.rb +0 -26
  66. data/lib/bridgetown-core/utils/internet.rb +0 -37
  67. data/lib/bridgetown-core/utils/win_tz.rb +0 -75
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad7d5025a99240586457a8afa4aa3e9a9aba0e289484ff855648bd772b89e7a9
4
- data.tar.gz: 1ea0ad14dc78ca7465f7ff93bab6e178ec18b336e08641ac18bf75cf655b7246
3
+ metadata.gz: c58653595832ef2fb9e6bbea3f147a0b3be9edae554d169e974570b84565a507
4
+ data.tar.gz: 7af409e9a01570321171cf1afc562c00cacdcc6e0dce98f2b8eac4988a60e8d6
5
5
  SHA512:
6
- metadata.gz: 95b1542d9fa33741d18832a026aa43e720f99520b8367b12672e2689c7be19200e0cedd53b8070666c15e7594af43dec3c1885fa8570e6c7daf463afcae3405f
7
- data.tar.gz: '00048d6aa2ca15728544ecbf46ee05b4b813630a03a0f35154b45096a2f9720ea645e23faac13fcab18a85e395fa6782eee522cbd39248b88b0f2a80a7fcff41'
6
+ metadata.gz: b21d9c304dd99dd306b70e97e172d1ec172f0d1fa48034610cc3d9e6bc9c40b98e7fe10479226103cb2ff501c22c7c2841f20630235262cc7214661be62bcf3e
7
+ data.tar.gz: cab7afb0f441e412850ce648d6e7ca9a3996f106b0800c31ef24a0cf63af9db770498f307da275860c0572d6d6f2a5f1e56570d10349bb11cd4f17f96b3266ee
@@ -31,6 +31,7 @@ Gem::Specification.new do |s|
31
31
  s.required_ruby_version = ">= 2.5.0"
32
32
  s.required_rubygems_version = ">= 2.7.0"
33
33
 
34
+ s.add_runtime_dependency("activemodel", "~> 6.0")
34
35
  s.add_runtime_dependency("activesupport", "~> 6.0")
35
36
  s.add_runtime_dependency("addressable", "~> 2.4")
36
37
  s.add_runtime_dependency("amazing_print", "~> 1.2")
@@ -45,7 +46,6 @@ Gem::Specification.new do |s|
45
46
  s.add_runtime_dependency("liquid", "~> 5.0")
46
47
  s.add_runtime_dependency("liquid-component", ">= 0.1")
47
48
  s.add_runtime_dependency("listen", "~> 3.0")
48
- s.add_runtime_dependency("pathutil", "~> 0.9")
49
49
  s.add_runtime_dependency("rouge", "~> 3.0")
50
50
  s.add_runtime_dependency("safe_yaml", "~> 1.0")
51
51
  s.add_runtime_dependency("terminal-table", "~> 1.8")
@@ -18,6 +18,7 @@ end
18
18
  require "rubygems"
19
19
 
20
20
  # stdlib
21
+ require "find"
21
22
  require "forwardable"
22
23
  require "fileutils"
23
24
  require "time"
@@ -31,13 +32,16 @@ require "json"
31
32
  # 3rd party
32
33
  require "active_support"
33
34
  require "active_support/core_ext/hash/keys"
35
+ require "active_support/core_ext/module/delegation"
34
36
  require "active_support/core_ext/object/blank"
37
+ require "active_support/core_ext/object/deep_dup"
38
+ require "active_support/core_ext/object/inclusion"
35
39
  require "active_support/core_ext/string/inflections"
36
40
  require "active_support/core_ext/string/inquiry"
37
41
  require "active_support/core_ext/string/starts_ends_with"
42
+ require "active_support/current_attributes"
38
43
  require "active_support/descendants_tracker"
39
44
  require "hash_with_dot_access"
40
- require "pathutil"
41
45
  require "addressable/uri"
42
46
  require "safe_yaml/load"
43
47
  require "liquid"
@@ -47,6 +51,7 @@ require "colorator"
47
51
  require "i18n"
48
52
  require "faraday"
49
53
  require "thor"
54
+ require "zeitwerk"
50
55
 
51
56
  module HashWithDotAccess
52
57
  class Hash # :nodoc:
@@ -73,7 +78,6 @@ if RUBY_VERSION.start_with?("3.0")
73
78
  end
74
79
 
75
80
  module Bridgetown
76
- # internal requires
77
81
  autoload :Cleaner, "bridgetown-core/cleaner"
78
82
  autoload :Collection, "bridgetown-core/collection"
79
83
  autoload :Configuration, "bridgetown-core/configuration"
@@ -81,30 +85,38 @@ module Bridgetown
81
85
  autoload :Deprecator, "bridgetown-core/deprecator"
82
86
  autoload :Document, "bridgetown-core/document"
83
87
  autoload :EntryFilter, "bridgetown-core/entry_filter"
88
+ # TODO: we have too many errors! This is silly
84
89
  autoload :Errors, "bridgetown-core/errors"
85
90
  autoload :Excerpt, "bridgetown-core/excerpt"
91
+ # TODO: this is a poorly named, unclear class. Relocate to Utils:
86
92
  autoload :External, "bridgetown-core/external"
87
93
  autoload :FrontmatterDefaults, "bridgetown-core/frontmatter_defaults"
88
94
  autoload :Hooks, "bridgetown-core/hooks"
89
95
  autoload :Layout, "bridgetown-core/layout"
90
96
  autoload :LayoutPlaceable, "bridgetown-core/concerns/layout_placeable"
91
97
  autoload :Cache, "bridgetown-core/cache"
92
- autoload :CollectionReader, "bridgetown-core/readers/collection_reader"
98
+ autoload :Current, "bridgetown-core/current"
99
+ # TODO: remove this when legacy content engine is gone:
93
100
  autoload :DataReader, "bridgetown-core/readers/data_reader"
94
101
  autoload :DefaultsReader, "bridgetown-core/readers/defaults_reader"
95
102
  autoload :LayoutReader, "bridgetown-core/readers/layout_reader"
103
+ # TODO: remove this when legacy content engine is gone:
96
104
  autoload :PostReader, "bridgetown-core/readers/post_reader"
105
+ # TODO: we can merge this back into Reader class:
97
106
  autoload :PageReader, "bridgetown-core/readers/page_reader"
98
107
  autoload :PluginContentReader, "bridgetown-core/readers/plugin_content_reader"
108
+ # TODO: also merge this:
99
109
  autoload :StaticFileReader, "bridgetown-core/readers/static_file_reader"
100
110
  autoload :LogAdapter, "bridgetown-core/log_adapter"
101
111
  autoload :Page, "bridgetown-core/page"
102
- autoload :PageWithoutAFile, "bridgetown-core/page_without_a_file"
112
+ autoload :GeneratedPage, "bridgetown-core/page"
113
+ # TODO: figure out how to get rid of this seemingly banal class:
103
114
  autoload :PathManager, "bridgetown-core/path_manager"
104
115
  autoload :PluginManager, "bridgetown-core/plugin_manager"
105
116
  autoload :Publishable, "bridgetown-core/concerns/publishable"
106
117
  autoload :Publisher, "bridgetown-core/publisher"
107
118
  autoload :Reader, "bridgetown-core/reader"
119
+ # TODO: remove this when the incremental regenerator is gone:
108
120
  autoload :Regenerator, "bridgetown-core/regenerator"
109
121
  autoload :RelatedPosts, "bridgetown-core/related_posts"
110
122
  autoload :Renderer, "bridgetown-core/renderer"
@@ -130,6 +142,7 @@ module Bridgetown
130
142
 
131
143
  require "bridgetown-core/drops/drop"
132
144
  require "bridgetown-core/drops/document_drop"
145
+ require "bridgetown-core/drops/resource_drop"
133
146
  require_all "bridgetown-core/converters"
134
147
  require_all "bridgetown-core/converters/markdown"
135
148
  require_all "bridgetown-core/drops"
@@ -193,11 +206,7 @@ module Bridgetown
193
206
  # @return [void]
194
207
  # rubocop:disable Naming/AccessorMethodName
195
208
  def set_timezone(timezone)
196
- ENV["TZ"] = if Utils::Platforms.really_windows?
197
- Utils::WinTZ.calculate(timezone)
198
- else
199
- timezone
200
- end
209
+ ENV["TZ"] = timezone
201
210
  end
202
211
  # rubocop:enable Naming/AccessorMethodName
203
212
 
@@ -218,11 +227,11 @@ module Bridgetown
218
227
  @logger = LogAdapter.new(writer, (ENV["BRIDGETOWN_LOG_LEVEL"] || :info).to_sym)
219
228
  end
220
229
 
221
- # An array of sites. Currently only ever a single entry.
230
+ # Deprecated. Now using the Current site.
222
231
  #
223
232
  # @return [Array<Bridgetown::Site>] the Bridgetown sites created.
224
233
  def sites
225
- @sites ||= []
234
+ [Bridgetown::Current.site].compact
226
235
  end
227
236
 
228
237
  # Ensures the questionable path is prefixed with the base directory
@@ -259,3 +268,13 @@ module Bridgetown
259
268
  Bridgetown::External.require_if_present("liquid/c")
260
269
  end
261
270
  end
271
+
272
+ module Bridgetown
273
+ module Model; end
274
+ module Resource; end
275
+ end
276
+
277
+ loader = Zeitwerk::Loader.new
278
+ loader.push_dir File.join(__dir__, "bridgetown-core/model"), namespace: Bridgetown::Model
279
+ loader.push_dir File.join(__dir__, "bridgetown-core/resource"), namespace: Bridgetown::Resource
280
+ loader.setup # ready!
@@ -58,7 +58,13 @@ module Bridgetown
58
58
  # Returns a Set with the file paths
59
59
  def new_files
60
60
  @new_files ||= Set.new.tap do |files|
61
- site.each_site_file { |item| files << item.destination(site.dest) }
61
+ site.each_site_file do |item|
62
+ files << if item.method(:destination).arity == 1
63
+ item.destination(site.dest)
64
+ else
65
+ item.destination.output_path
66
+ end
67
+ end
62
68
  end
63
69
  end
64
70
 
@@ -8,82 +8,104 @@ module Bridgetown
8
8
  attr_reader :label, :metadata
9
9
  attr_writer :docs
10
10
 
11
+ attr_writer :resources
12
+
11
13
  # Create a new Collection.
12
14
  #
13
- # site - the site to which this collection belongs.
14
- # label - the name of the collection
15
- #
16
- # Returns nothing.
15
+ # @param site [Bridgetown::Site] the site to which this collection belongs
16
+ # @param label [String] the name of the collection
17
17
  def initialize(site, label)
18
18
  @site = site
19
19
  @label = sanitize_label(label)
20
20
  @metadata = extract_metadata
21
21
  end
22
22
 
23
+ def builtin?
24
+ label.in? %w(posts pages data).freeze
25
+ end
26
+
27
+ def legacy_reader?
28
+ label.in? %w(posts data).freeze
29
+ end
30
+
31
+ def data?
32
+ label == "data"
33
+ end
34
+
23
35
  # Fetch the Documents in this collection.
24
36
  # Defaults to an empty array if no documents have been read in.
25
37
  #
26
- # Returns an array of Bridgetown::Document objects.
38
+ # @return [Array<Bridgetown::Document>]
27
39
  def docs
28
40
  @docs ||= []
29
41
  end
30
42
 
31
- # Override of normal respond_to? to match method_missing's logic for
32
- # looking in @data.
33
- def respond_to_missing?(method, include_private = false)
34
- docs.respond_to?(method.to_sym, include_private) || super
43
+ # Fetch the Resources in this collection.
44
+ # Defaults to an empty array if no resources have been read in.
45
+ #
46
+ # @return [Array<Bridgetown::Resource::Base>]
47
+ def resources
48
+ @resources ||= []
35
49
  end
36
50
 
37
- # Override of method_missing to check in @data for the key.
38
- def method_missing(method, *args, &blck)
39
- if docs.respond_to?(method.to_sym)
40
- Bridgetown.logger.warn "Deprecation:",
41
- "#{label}.#{method} should be changed to #{label}.docs.#{method}."
42
- Bridgetown.logger.warn "", "Called by #{caller(0..0)}."
43
- docs.public_send(method.to_sym, *args, &blck)
44
- else
45
- super
46
- end
51
+ # Iterate over either Resources or Documents depending on how the site is
52
+ # configured
53
+ def each(&block)
54
+ site.uses_resource? ? resources.each(&block) : docs.each(&block)
47
55
  end
48
56
 
49
57
  # Fetch the static files in this collection.
50
58
  # Defaults to an empty array if no static files have been read in.
51
59
  #
52
- # Returns an array of Bridgetown::StaticFile objects.
60
+ # @return [Array<Bridgetown::StaticFile>]
61
+ def static_files
62
+ @static_files ||= []
63
+ end
64
+
53
65
  def files
54
- @files ||= []
66
+ Bridgetown::Deprecator.deprecation_message "Collection#files is now Collection#static_files"
67
+ static_files
55
68
  end
56
69
 
57
70
  # Read the allowed documents into the collection's array of docs.
58
71
  #
59
- # Returns the sorted array of docs.
60
- def read
72
+ # @return [Bridgetown::Collection] self
73
+ def read # rubocop:todo Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
61
74
  filtered_entries.each do |file_path|
62
75
  full_path = collection_dir(file_path)
63
76
  next if File.directory?(full_path)
64
77
 
65
- if Utils.has_yaml_header? full_path
78
+ if site.uses_resource?
79
+ next if File.basename(file_path).starts_with?("_")
80
+
81
+ if label == "data" || Utils.has_yaml_header?(full_path)
82
+ read_resource(full_path)
83
+ else
84
+ read_static_file(file_path, full_path)
85
+ end
86
+ elsif Utils.has_yaml_header? full_path
66
87
  read_document(full_path)
67
88
  else
68
89
  read_static_file(file_path, full_path)
69
90
  end
70
91
  end
71
- site.static_files.concat(files)
92
+ site.static_files.concat(static_files)
72
93
  sort_docs!
94
+
95
+ self
73
96
  end
74
97
 
75
98
  # All the entries in this collection.
76
99
  #
77
- # Returns an Array of file paths to the documents in this collection
78
- # relative to the collection's directory
100
+ # @return [Array<String>] file paths to the documents in this collection
101
+ # relative to the collection's folder
79
102
  def entries
80
103
  return [] unless exists?
81
104
 
82
105
  @entries ||= begin
83
106
  collection_dir_slash = "#{collection_dir}/"
84
107
  Utils.safe_glob(collection_dir, ["**", "*"], File::FNM_DOTMATCH).map do |entry|
85
- entry[collection_dir_slash] = ""
86
- entry
108
+ entry.sub(collection_dir_slash, "")
87
109
  end
88
110
  end
89
111
  end
@@ -91,85 +113,87 @@ module Bridgetown
91
113
  # Filtered version of the entries in this collection.
92
114
  # See `Bridgetown::EntryFilter#filter` for more information.
93
115
  #
94
- # Returns a list of filtered entry paths.
116
+ # @return [Array<String>] list of filtered entry paths
95
117
  def filtered_entries
96
118
  return [] unless exists?
97
119
 
98
120
  @filtered_entries ||=
99
- Dir.chdir(directory) do
121
+ Dir.chdir(absolute_path) do
100
122
  entry_filter.filter(entries).reject do |f|
101
123
  path = collection_dir(f)
102
- File.directory?(path) || entry_filter.symlink?(f)
124
+ File.directory?(path)
103
125
  end
104
126
  end
105
127
  end
106
128
 
107
- # The directory for this Collection, relative to the site source or the directory
108
- # containing the collection.
129
+ # The folder name of this Collection, e.g. `_posts` or `_events`
109
130
  #
110
- # Returns a String containing the directory name where the collection
111
- # is stored on the filesystem.
112
- def relative_directory
113
- @relative_directory ||= "_#{label}"
131
+ # @return [String]
132
+ def folder_name
133
+ @folder_name ||= "_#{label}"
114
134
  end
135
+ alias_method :relative_directory, :folder_name
115
136
 
116
- # The relative path to the directory containing the collection.
137
+ # The relative path to the folder containing the collection.
117
138
  #
118
- # Returns a String containing the directory name where the collection
119
- # is stored relative to the source directory
139
+ # @return [String] folder where the collection is stored relative to the
140
+ # configured collections folder (usually the site source)
120
141
  def relative_path
121
- Pathname.new(container).join(relative_directory).to_s
142
+ Pathname.new(container).join(folder_name).to_s
122
143
  end
123
144
 
124
- # The full path to the directory containing the collection.
145
+ # The full path to the folder containing the collection.
125
146
  #
126
- # Returns a String containing the directory name where the collection
127
- # is stored on the filesystem.
128
- def directory
129
- @directory ||= site.in_source_dir(relative_path)
147
+ # @return [String] full path where the collection is stored on the filesystem
148
+ def absolute_path
149
+ @absolute_path ||= site.in_source_dir(relative_path)
130
150
  end
151
+ alias_method :directory, :absolute_path
131
152
 
132
- # The full path to the directory containing the collection, with
153
+ # The full path to the folder containing the collection, with
133
154
  # optional subpaths.
134
155
  #
135
- # *files - (optional) any other path pieces relative to the
136
- # directory to append to the path
137
- #
138
- # Returns a String containing th directory name where the collection
139
- # is stored on the filesystem.
156
+ # @param *files [Array<String>] any other path pieces relative to the
157
+ # folder to append to the path
158
+ # @return [String]
140
159
  def collection_dir(*files)
141
- return directory if files.empty?
160
+ return absolute_path if files.empty?
142
161
 
143
- site.in_source_dir(container, relative_directory, *files)
162
+ site.in_source_dir(relative_path, *files)
144
163
  end
145
164
 
146
- # Checks whether the directory "exists" for this collection.
165
+ # Checks whether the folder "exists" for this collection.
166
+ #
167
+ # @return [Boolean]
147
168
  def exists?
148
- File.directory?(directory)
169
+ File.directory?(absolute_path)
149
170
  end
150
171
 
151
172
  # The entry filter for this collection.
152
- # Creates an instance of Bridgetown::EntryFilter.
173
+ # Creates an instance of Bridgetown::EntryFilter if needed.
153
174
  #
154
- # Returns the instance of Bridgetown::EntryFilter for this collection.
175
+ # @return [Bridgetown::EntryFilter]
155
176
  def entry_filter
156
- @entry_filter ||= Bridgetown::EntryFilter.new(site, relative_directory)
177
+ @entry_filter ||= Bridgetown::EntryFilter.new(
178
+ site,
179
+ base_directory: folder_name,
180
+ include_underscores: site.uses_resource?
181
+ )
157
182
  end
158
183
 
159
184
  # An inspect string.
160
185
  #
161
- # Returns the inspect string
186
+ # @return [String]
162
187
  def inspect
163
- "#<#{self.class} @label=#{label} docs=#{docs}>"
188
+ "#<#{self.class} @label=#{label} docs=#{docs} resources=#{resources}>"
164
189
  end
165
190
 
166
191
  # Produce a sanitized label name
167
192
  # Label names may not contain anything but alphanumeric characters,
168
193
  # underscores, and hyphens.
169
194
  #
170
- # label - the possibly-unsafe label
171
- #
172
- # Returns a sanitized version of the label.
195
+ # @param label [String] the possibly-unsafe label
196
+ # @return [String] sanitized version of the label.
173
197
  def sanitize_label(label)
174
198
  label.gsub(%r![^a-z0-9_\-\.]!i, "")
175
199
  end
@@ -179,7 +203,8 @@ module Bridgetown
179
203
  # - label
180
204
  # - docs
181
205
  #
182
- # Returns a representation of this collection for use in Liquid.
206
+ # @return [Bridgetown::Drops::CollectionDrop] representation of this
207
+ # collection for use in Liquid
183
208
  def to_liquid
184
209
  Drops::CollectionDrop.new self
185
210
  end
@@ -187,14 +212,22 @@ module Bridgetown
187
212
  # Whether the collection's documents ought to be written as individual
188
213
  # files in the output.
189
214
  #
190
- # Returns true if the 'write' metadata is true, false otherwise.
215
+ # @return [Boolean] true if the 'write' metadata is true, false otherwise.
191
216
  def write?
192
217
  !!metadata.fetch("output", false)
193
218
  end
194
219
 
195
- # The URL template to render collection's documents at.
220
+ # Used by Resource's permalink processor
221
+ # @return [String]
222
+ def default_permalink
223
+ metadata.fetch("permalink") do
224
+ "/:collection/:path/"
225
+ end
226
+ end
227
+
228
+ # LEGACY: The URL template to render collection's documents at.
196
229
  #
197
- # Returns the URL template to render collection's documents at.
230
+ # @return [String]
198
231
  def url_template
199
232
  @url_template ||= metadata.fetch("permalink") do
200
233
  Utils.add_permalink_suffix("/:collection/:path", site.permalink_style)
@@ -203,13 +236,47 @@ module Bridgetown
203
236
 
204
237
  # Extract options for this collection from the site configuration.
205
238
  #
206
- # Returns the metadata for this collection
239
+ # @return [HashWithDotAccess::Hash]
207
240
  def extract_metadata
208
- if site.config["collections"].is_a?(Hash)
209
- site.config["collections"][label] || {}
210
- else
211
- {}
241
+ site.config.collections[label] || HashWithDotAccess::Hash.new
242
+ end
243
+
244
+ def merge_data_resources
245
+ data_contents = {}
246
+
247
+ sanitize_filename = ->(name) do
248
+ name.gsub(%r![^\w\s-]+|(?<=^|\b\s)\s+(?=$|\s?\b)!, "")
249
+ .gsub(%r!\s+!, "_")
212
250
  end
251
+
252
+ resources.each do |data_resource|
253
+ segments = data_resource.relative_path.each_filename.to_a[1..-1]
254
+ nested = []
255
+ segments.each_with_index do |segment, index|
256
+ sanitized_segment = sanitize_filename.(File.basename(segment, ".*"))
257
+ hsh = nested.empty? ? data_contents : data_contents.dig(*nested)
258
+ hsh[sanitized_segment] = if index == segments.length - 1
259
+ data_resource.data.array || data_resource.data
260
+ else
261
+ {}
262
+ end
263
+ nested << sanitized_segment
264
+ end
265
+ end
266
+
267
+ merge_environment_specific_metadata(data_contents).with_dot_access
268
+ end
269
+
270
+ def merge_environment_specific_metadata(data_contents)
271
+ if data_contents["site_metadata"]
272
+ data_contents["site_metadata"][Bridgetown.environment]&.each_key do |k|
273
+ data_contents["site_metadata"][k] =
274
+ data_contents["site_metadata"][Bridgetown.environment][k]
275
+ end
276
+ data_contents["site_metadata"].delete(Bridgetown.environment)
277
+ end
278
+
279
+ data_contents
213
280
  end
214
281
 
215
282
  private
@@ -219,17 +286,28 @@ module Bridgetown
219
286
  end
220
287
 
221
288
  def read_document(full_path)
222
- doc = Document.new(full_path, site: site, collection: self)
223
- doc.read
289
+ doc = Document.new(full_path, site: site, collection: self).tap(&:read)
224
290
  docs << doc if site.unpublished || doc.published?
225
291
  end
226
292
 
293
+ def read_resource(full_path)
294
+ id = "file://#{label}.collection/" + Addressable::URI.escape(
295
+ Pathname(full_path).relative_path_from(Pathname(site.source)).to_s
296
+ )
297
+ resource = Bridgetown::Model::Base.find(id).to_resource.read!
298
+ resources << resource if site.unpublished || resource.published?
299
+ end
300
+
227
301
  def sort_docs!
228
302
  if metadata["sort_by"].is_a?(String)
229
303
  sort_docs_by_key!
304
+ sort_resources_by_key!
230
305
  else
231
306
  docs.sort!
307
+ resources.sort!
232
308
  end
309
+ docs.reverse! if metadata.sort_direction == "descending"
310
+ resources.reverse! if metadata.sort_direction == "descending"
233
311
  end
234
312
 
235
313
  # A custom sort function based on Schwartzian transform
@@ -252,6 +330,24 @@ module Bridgetown
252
330
  end.map!(&:last)
253
331
  end
254
332
 
333
+ def sort_resources_by_key!
334
+ meta_key = metadata["sort_by"]
335
+ # Modify `docs` array to cache document's property along with the Document instance
336
+ resources.map! { |doc| [doc.data[meta_key], doc] }.sort! do |apples, olives|
337
+ order = determine_sort_order(meta_key, apples, olives)
338
+
339
+ # Fall back to `Document#<=>` if the properties were equal or were non-sortable
340
+ # Otherwise continue with current sort-order
341
+ if order.nil? || order.zero?
342
+ apples[-1] <=> olives[-1]
343
+ else
344
+ order
345
+ end
346
+
347
+ # Finally restore the `docs` array with just the Document objects themselves
348
+ end.map!(&:last)
349
+ end
350
+
255
351
  def determine_sort_order(sort_key, apples, olives)
256
352
  apple_property, apple_document = apples
257
353
  olive_property, olive_document = olives
@@ -277,7 +373,7 @@ module Bridgetown
277
373
  File.dirname(file_path)
278
374
  ).chomp("/.")
279
375
 
280
- files << StaticFile.new(
376
+ static_files << StaticFile.new(
281
377
  site,
282
378
  site.source,
283
379
  relative_dir,