bridgetown-core 0.17.1 → 0.18.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/lib/bridgetown-core.rb +43 -28
  4. data/lib/bridgetown-core/collection.rb +5 -1
  5. data/lib/bridgetown-core/commands/apply.rb +2 -2
  6. data/lib/bridgetown-core/commands/new.rb +1 -1
  7. data/lib/bridgetown-core/concerns/layout_placeable.rb +1 -1
  8. data/lib/bridgetown-core/concerns/liquid_renderable.rb +10 -0
  9. data/lib/bridgetown-core/concerns/site/configurable.rb +21 -23
  10. data/lib/bridgetown-core/concerns/site/content.rb +44 -31
  11. data/lib/bridgetown-core/concerns/site/extensible.rb +14 -13
  12. data/lib/bridgetown-core/concerns/site/localizable.rb +6 -2
  13. data/lib/bridgetown-core/concerns/site/processable.rb +10 -9
  14. data/lib/bridgetown-core/concerns/site/renderable.rb +34 -26
  15. data/lib/bridgetown-core/concerns/site/writable.rb +7 -15
  16. data/lib/bridgetown-core/configuration.rb +5 -3
  17. data/lib/bridgetown-core/converter.rb +0 -42
  18. data/lib/bridgetown-core/converters/erb_templates.rb +75 -16
  19. data/lib/bridgetown-core/converters/liquid_templates.rb +96 -0
  20. data/lib/bridgetown-core/converters/markdown.rb +0 -3
  21. data/lib/bridgetown-core/document.rb +31 -18
  22. data/lib/bridgetown-core/drops/site_drop.rb +4 -0
  23. data/lib/bridgetown-core/drops/unified_payload_drop.rb +0 -1
  24. data/lib/bridgetown-core/drops/url_drop.rb +19 -3
  25. data/lib/bridgetown-core/excerpt.rb +1 -1
  26. data/lib/bridgetown-core/filters.rb +28 -7
  27. data/lib/bridgetown-core/generators/prototype_generator.rb +42 -25
  28. data/lib/bridgetown-core/helpers.rb +84 -0
  29. data/lib/bridgetown-core/liquid_renderer.rb +1 -1
  30. data/lib/bridgetown-core/log_writer.rb +2 -2
  31. data/lib/bridgetown-core/plugin_manager.rb +34 -1
  32. data/lib/bridgetown-core/reader.rb +1 -4
  33. data/lib/bridgetown-core/readers/post_reader.rb +28 -15
  34. data/lib/bridgetown-core/renderer.rb +42 -160
  35. data/lib/bridgetown-core/ruby_template_view.rb +22 -33
  36. data/lib/bridgetown-core/site.rb +12 -2
  37. data/lib/bridgetown-core/utils.rb +14 -0
  38. data/lib/bridgetown-core/version.rb +2 -2
  39. data/lib/bridgetown-core/watcher.rb +1 -0
  40. data/lib/site_template/src/images/.keep +1 -0
  41. metadata +7 -3
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ class RubyTemplateView
5
+ class Helpers
6
+ include Bridgetown::Filters
7
+
8
+ attr_reader :view, :site
9
+
10
+ Context = Struct.new(:registers)
11
+
12
+ def initialize(view, site)
13
+ @view = view
14
+ @site = site
15
+
16
+ # duck typing for Liquid context
17
+ @context = Context.new({ site: site })
18
+ end
19
+
20
+ def webpack_path(asset_type)
21
+ Bridgetown::Utils.parse_webpack_manifest_file(site, asset_type.to_s)
22
+ end
23
+
24
+ # @param pairs [Hash] A hash of key/value pairs.
25
+ #
26
+ # @return [String] Space-separated keys where the values are truthy.
27
+ def class_map(pairs = {})
28
+ pairs.select { |_key, truthy| truthy }.keys.join(" ")
29
+ end
30
+
31
+ # This helper will generate the correct permalink URL for the file path.
32
+ #
33
+ # @param relative_path [String, Object] source file path, e.g.
34
+ # "_posts/2020-10-20-my-post.md", or object that responds to `url`
35
+ # @return [String] the permalink URL for the file
36
+ # @raise [ArgumentError] if the file cannot be found
37
+ def url_for(relative_path)
38
+ path_string = !relative_path.is_a?(String) ? relative_path.url : relative_path
39
+
40
+ return path_string if path_string.start_with?("/", "http")
41
+
42
+ site.each_site_file do |item|
43
+ if item.relative_path == path_string || item.relative_path == "/#{path_string}"
44
+ return relative_url(item)
45
+ end
46
+ end
47
+
48
+ raise ArgumentError, <<~MSG
49
+ Could not find document '#{relative_path}' in 'url_for' helper.
50
+
51
+ Make sure the document exists and the path is correct.
52
+ MSG
53
+ end
54
+ alias_method :link, :url_for
55
+
56
+ # This helper will generate the correct permalink URL for the file path.
57
+ #
58
+ # @param text [String] the content inside the anchor tag
59
+ # @param relative_path [String, Object] source file path, e.g.
60
+ # "_posts/2020-10-20-my-post.md", or object that responds to `url`
61
+ # @param options [Hash] key-value pairs of HTML attributes to add to the tag
62
+ # @return [String] the anchor tag HTML
63
+ # @raise [ArgumentError] if the file cannot be found
64
+ def link_to(text, relative_path, options = {})
65
+ segments = []
66
+ segments << "a"
67
+ segments << "href=\"#{url_for(relative_path)}\""
68
+ options.each do |attr, option|
69
+ attr = attr.to_s.tr("_", "-")
70
+ segments << "#{attr}=\"#{Utils.xml_escape(option)}\""
71
+ end
72
+ "<#{segments.join(" ")}>#{text}</a>"
73
+ end
74
+
75
+ # Forward all arguments to I18n.t method
76
+ #
77
+ # @return [String] the translated string
78
+ # @see I18n
79
+ def t(*args)
80
+ I18n.send :t, *args
81
+ end
82
+ end
83
+ end
84
+ end
@@ -26,7 +26,7 @@ module Bridgetown
26
26
  def reset
27
27
  @stats = {}
28
28
  @cache = {}
29
- Renderer.cached_partials = {}
29
+ Bridgetown::Converters::LiquidTemplates.cached_partials = {}
30
30
  end
31
31
 
32
32
  def file(filename)
@@ -33,12 +33,12 @@ module Bridgetown
33
33
  true
34
34
  end
35
35
 
36
- # Log a +WARN+ message
36
+ # Log a `WARN` message
37
37
  def warn(progname = nil, &block)
38
38
  add(WARN, nil, progname.yellow, &block)
39
39
  end
40
40
 
41
- # Log an +ERROR+ message
41
+ # Log an `ERROR` message
42
42
  def error(progname = nil, &block)
43
43
  add(ERROR, nil, progname.red, &block)
44
44
  end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "zeitwerk"
4
+
3
5
  module Bridgetown
4
6
  class PluginManager
5
7
  PLUGINS_GROUP = :bridgetown_plugins
6
8
  YARN_DEPENDENCY_REGEXP = %r!(.+)@([^@]*)$!.freeze
7
9
 
8
- attr_reader :site
10
+ attr_reader :site, :component_loaders
9
11
 
10
12
  @source_manifests = Set.new
11
13
  @registered_plugins = Set.new
@@ -37,6 +39,7 @@ module Bridgetown
37
39
  # Returns nothing
38
40
  def initialize(site)
39
41
  @site = site
42
+ @component_loaders = {}
40
43
  end
41
44
 
42
45
  def self.require_from_bundler
@@ -158,5 +161,35 @@ module Bridgetown
158
161
  Array(site.config["plugins_dir"]).map { |d| File.expand_path(d) }
159
162
  end
160
163
  end
164
+
165
+ # rubocop:disable Metrics/AbcSize
166
+ def setup_component_loaders
167
+ unless @component_loaders.keys.empty?
168
+ @component_loaders.each do |_path, loader|
169
+ loader.unload
170
+ end
171
+ @component_loaders = {}
172
+ end
173
+
174
+ # Because "first constant wins" in Zeitwerk, we need to load the local
175
+ # source components _before_ we load any from plugins
176
+ site.components_load_paths.reverse_each do |load_path|
177
+ next unless Dir.exist? load_path
178
+ next if Zeitwerk::Registry.loaders.find { |loader| loader.manages?(load_path) }
179
+
180
+ @component_loaders[load_path] = Zeitwerk::Loader.new
181
+ @component_loaders[load_path].push_dir(load_path)
182
+ @component_loaders[load_path].enable_reloading if load_path.start_with?(site.root_dir)
183
+ @component_loaders[load_path].ignore(File.join(load_path, "**", "*.js.rb"))
184
+ @component_loaders[load_path].setup
185
+ end
186
+ end
187
+ # rubocop:enable Metrics/AbcSize
188
+
189
+ def reload_component_loaders
190
+ @component_loaders.each do |path, loader|
191
+ loader.reload if path.start_with?(site.root_dir)
192
+ end
193
+ end
161
194
  end
162
195
  end
@@ -76,10 +76,7 @@ module Bridgetown
76
76
  def retrieve_posts(dir)
77
77
  return if outside_configured_directory?(dir)
78
78
 
79
- post_reader.read_posts(dir).tap do |entries|
80
- site.posts.docs.concat(entries.select { |entry| entry.is_a?(Document) })
81
- site.posts.files.concat(entries.select { |entry| entry.is_a?(StaticFile) })
82
- end
79
+ post_reader.read_posts(dir)
83
80
  end
84
81
 
85
82
  # Recursively traverse directories with the read_directories function.
@@ -21,13 +21,13 @@ module Bridgetown
21
21
  # Read all the files in <source>/<dir>/<magic_dir> and create a new
22
22
  # Document object with each one insofar as it matches the regexp matcher.
23
23
  #
24
- # dir - The String relative path of the directory to read.
24
+ # @param dir [String] relative path of the directory to read.
25
25
  #
26
- # Returns nothing.
26
+ # @return [Array<Document, StaticFile>]
27
27
  def read_publishable(dir, magic_dir, matcher)
28
28
  read_content(dir, magic_dir, matcher)
29
- .tap { |entries| entries.select { |entry| entry.respond_to?(:read) }.each(&:read) }
30
- .select { |entry| processable?(entry) }
29
+ .tap { |items| items.select { |item| item.respond_to?(:read) }.each(&:read) }
30
+ .select { |item| item_added_to_site?(item) }
31
31
  end
32
32
 
33
33
  # Read all the content files from <source>/<dir>/magic_dir
@@ -70,26 +70,39 @@ module Bridgetown
70
70
  )
71
71
  end
72
72
 
73
- def processable?(doc)
74
- return true if doc.is_a?(StaticFile)
73
+ def processable?(item)
74
+ return true if item.is_a?(StaticFile)
75
75
 
76
- if doc.content.nil?
77
- Bridgetown.logger.debug "Skipping:", "Content in #{doc.relative_path} is nil"
76
+ if item.content.nil?
77
+ Bridgetown.logger.debug "Skipping:", "Content in #{item.relative_path} is nil"
78
78
  false
79
- elsif !doc.content.valid_encoding?
80
- Bridgetown.logger.debug "Skipping:", "#{doc.relative_path} is not valid UTF-8"
79
+ elsif !item.content.valid_encoding?
80
+ Bridgetown.logger.debug "Skipping:", "#{item.relative_path} is not valid UTF-8"
81
81
  false
82
82
  else
83
- publishable?(doc)
83
+ publishable?(item)
84
84
  end
85
85
  end
86
86
 
87
- def publishable?(doc)
88
- site.publisher.publish?(doc).tap do |will_publish|
89
- if !will_publish && site.publisher.hidden_in_the_future?(doc)
90
- Bridgetown.logger.warn "Skipping:", "#{doc.relative_path} has a future date"
87
+ def publishable?(item)
88
+ site.publisher.publish?(item).tap do |will_publish|
89
+ if !will_publish && site.publisher.hidden_in_the_future?(item)
90
+ Bridgetown.logger.warn "Skipping:", "#{item.relative_path} has a future date"
91
91
  end
92
92
  end
93
93
  end
94
+
95
+ def item_added_to_site?(item)
96
+ return false unless processable?(item)
97
+
98
+ if item.is_a?(Document)
99
+ site.posts.docs << item
100
+ elsif item.is_a?(StaticFile)
101
+ site.posts.files << item
102
+ site.static_files << item
103
+ end
104
+
105
+ true
106
+ end
94
107
  end
95
108
  end
@@ -3,37 +3,10 @@
3
3
  module Bridgetown
4
4
  class Renderer
5
5
  attr_reader :document, :site
6
- attr_writer :layouts, :payload
7
6
 
8
- class << self
9
- attr_accessor :cached_partials
10
- end
11
-
12
- def initialize(site, document, site_payload = nil)
7
+ def initialize(site, document)
13
8
  @site = site
14
9
  @document = document
15
- @payload = site_payload
16
- @layouts = nil
17
- self.class.cached_partials ||= {}
18
- end
19
-
20
- # Fetches the payload used in Liquid rendering.
21
- # It can be written with #payload=(new_payload)
22
- # Falls back to site.site_payload if no payload is set.
23
- #
24
- # Returns a Bridgetown::Drops::UnifiedPayloadDrop
25
- def payload
26
- @payload ||= site.site_payload
27
- end
28
-
29
- # The list of layouts registered for this Renderer.
30
- # It can be written with #layouts=(new_layouts)
31
- # Falls back to site.layouts if no layouts are registered.
32
- #
33
- # Returns a Hash of String => Bridgetown::Layout identified
34
- # as basename without the extension name.
35
- def layouts
36
- @layouts || site.layouts
37
10
  end
38
11
 
39
12
  # Determine which converters to use based on this document's
@@ -41,7 +14,13 @@ module Bridgetown
41
14
  #
42
15
  # Returns Array of Converter instances.
43
16
  def converters
44
- @converters ||= site.converters.select { |c| c.matches(document.extname) }.sort
17
+ @converters ||= site.converters.select do |converter|
18
+ if converter.method(:matches).arity == 1
19
+ converter.matches(document.extname)
20
+ else
21
+ converter.matches(document.extname, document)
22
+ end
23
+ end.sort
45
24
  end
46
25
 
47
26
  # Determine the extname the outputted file should have
@@ -51,93 +30,42 @@ module Bridgetown
51
30
  @output_ext ||= (permalink_ext || converter_output_ext)
52
31
  end
53
32
 
54
- # Prepare payload and render the document
33
+ # Run hooks and render the document
55
34
  #
56
35
  # Returns nothing
57
36
  def run
58
37
  Bridgetown.logger.debug "Rendering:", document.relative_path
59
38
 
60
- assign_pages!
61
- # TODO: this can be eliminated I think:
62
- assign_current_document!
63
- assign_highlighter_options!
64
- assign_layout_data!
65
-
66
- document.trigger_hooks(:pre_render, payload)
39
+ document.trigger_hooks :pre_render
67
40
  document.output = render_document
68
- document.trigger_hooks(:post_render)
41
+ document.trigger_hooks :post_render
69
42
  end
70
43
 
71
44
  # Render the document.
72
45
  #
73
46
  # Returns String rendered document output
74
- # rubocop: disable Metrics/AbcSize
75
47
  def render_document
76
- liquid_context = provide_liquid_context
77
-
78
48
  execute_inline_ruby!
79
49
 
80
50
  output = document.content
81
- if document.render_with_liquid?
82
- Bridgetown.logger.debug "Rendering Liquid:", document.relative_path
83
- output = render_liquid(output, payload, liquid_context, document.path)
84
- end
85
-
86
51
  Bridgetown.logger.debug "Rendering Markup:", document.relative_path
87
52
  output = convert(output.to_s, document)
88
53
  document.content = output
89
54
 
90
55
  if document.place_in_layout?
91
56
  Bridgetown.logger.debug "Rendering Layout:", document.relative_path
92
- output = place_in_layouts(output, payload, liquid_context)
57
+ output = place_in_layouts(output)
93
58
  end
94
59
 
95
60
  output
96
61
  end
97
62
 
98
- def provide_liquid_context
99
- {
100
- registers: {
101
- site: site,
102
- page: payload["page"],
103
- cached_partials: self.class.cached_partials,
104
- },
105
- strict_filters: liquid_options["strict_filters"],
106
- strict_variables: liquid_options["strict_variables"],
107
- }
108
- end
109
-
110
63
  def execute_inline_ruby!
111
64
  return unless site.config.should_execute_inline_ruby?
112
65
 
113
66
  Bridgetown::Utils::RubyExec.search_data_for_ruby_code(document, self)
114
67
  end
115
68
 
116
- # rubocop: enable Metrics/AbcSize
117
-
118
- # Render the given content with the payload and context
119
- #
120
- # content -
121
- # payload -
122
- # context -
123
- # path - (optional) the path to the file, for use in ex
124
- #
125
- # Returns String the content, rendered by Liquid.
126
- def render_liquid(content, payload, liquid_context, path = nil)
127
- template = site.liquid_renderer.file(path).parse(content)
128
- template.warnings.each do |e|
129
- Bridgetown.logger.warn "Liquid Warning:",
130
- LiquidRenderer.format_error(e, path || document.relative_path)
131
- end
132
- template.render!(payload, liquid_context)
133
- # rubocop: disable Lint/RescueException
134
- rescue Exception => e
135
- Bridgetown.logger.error "Liquid Exception:",
136
- LiquidRenderer.format_error(e, path || document.relative_path)
137
- raise e
138
- end
139
- # rubocop: enable Lint/RescueException
140
-
141
69
  # Convert the document using the converters which match this renderer's document.
142
70
  #
143
71
  # Returns String the converted content.
@@ -157,30 +85,18 @@ module Bridgetown
157
85
  end
158
86
  end
159
87
 
160
- # Checks if the layout specified in the document actually exists
161
- #
162
- # layout - the layout to check
163
- #
164
- # Returns Boolean true if the layout is invalid, false if otherwise
165
- def invalid_layout?(layout)
166
- !document.data["layout"].nil? && layout.nil? && !(document.is_a? Bridgetown::Excerpt)
167
- end
168
-
169
88
  # Render layouts and place document content inside.
170
89
  #
171
90
  # Returns String rendered content
172
- def place_in_layouts(content, payload, liquid_context)
91
+ def place_in_layouts(content)
173
92
  output = content.dup
174
- layout = layouts[document.data["layout"].to_s]
93
+ layout = site.layouts[document.data["layout"]]
175
94
  validate_layout(layout)
176
95
 
177
96
  used = Set.new([layout])
178
97
 
179
- # Reset the payload layout data to ensure it starts fresh for each page.
180
- payload["layout"] = nil
181
-
182
98
  while layout
183
- output = render_layout(output, layout, liquid_context)
99
+ output = render_layout(output, layout)
184
100
  add_regenerator_dependencies(layout)
185
101
 
186
102
  next unless (layout = site.layouts[layout.data["layout"]])
@@ -198,47 +114,45 @@ module Bridgetown
198
114
  # layout - the layout to check
199
115
  # Returns nothing
200
116
  def validate_layout(layout)
201
- return unless invalid_layout?(layout)
117
+ return unless document.data["layout"].present? &&
118
+ layout.nil? &&
119
+ !(document.is_a? Bridgetown::Excerpt)
202
120
 
203
121
  Bridgetown.logger.warn "Build Warning:", "Layout '#{document.data["layout"]}' requested " \
204
122
  "in #{document.relative_path} does not exist."
205
123
  end
206
124
 
125
+ def converters_for_layout(layout)
126
+ site.converters.select do |converter|
127
+ if converter.method(:matches).arity == 1
128
+ converter.matches(layout.ext)
129
+ else
130
+ converter.matches(layout.ext, layout)
131
+ end
132
+ end.sort
133
+ end
134
+
207
135
  # Render layout content into document.output
208
136
  #
209
137
  # Returns String rendered content
210
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
211
- def render_layout(output, layout, liquid_context)
212
- if layout.render_with_liquid?
213
- payload["content"] = output
214
- payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {})
138
+ def render_layout(output, layout)
139
+ layout_converters = converters_for_layout(layout)
215
140
 
216
- render_liquid(
217
- layout.content,
218
- payload,
219
- liquid_context,
220
- layout.path
221
- )
222
- else
223
- layout_converters ||= site.converters.select { |c| c.matches(layout.ext) }.sort
224
-
225
- layout_content = layout.content.dup
226
- layout_converters.reduce(layout_content) do |layout_output, converter|
227
- next(layout_output) unless converter.method(:convert).arity == 2
141
+ layout_content = layout.content.dup
142
+ layout_converters.reduce(layout_content) do |layout_output, converter|
143
+ next(layout_output) unless converter.method(:convert).arity == 2
228
144
 
229
- layout.current_document = document
230
- layout.current_document_output = output
231
- converter.convert layout_output, layout
232
- rescue StandardError => e
233
- Bridgetown.logger.error "Conversion error:",
234
- "#{converter.class} encountered an error while "\
235
- "converting '#{document.relative_path}':"
236
- Bridgetown.logger.error("", e.to_s)
237
- raise e
238
- end
145
+ layout.current_document = document
146
+ layout.current_document_output = output
147
+ converter.convert layout_output, layout
239
148
  end
149
+ rescue StandardError => e
150
+ Bridgetown.logger.error "Conversion error:",
151
+ "#{converter.class} encountered an error while "\
152
+ "converting '#{document.relative_path}':"
153
+ Bridgetown.logger.error("", e.to_s)
154
+ raise e
240
155
  end
241
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
242
156
 
243
157
  def add_regenerator_dependencies(layout)
244
158
  return unless document.write?
@@ -249,34 +163,6 @@ module Bridgetown
249
163
  )
250
164
  end
251
165
 
252
- # Set page content to payload and assign paginator if document has one.
253
- #
254
- # Returns nothing
255
- def assign_pages!
256
- payload["page"] = document.to_liquid
257
- payload["paginator"] = document.paginator.to_liquid if document.respond_to?(:paginator)
258
- end
259
-
260
- # Set related posts to payload if document is a post.
261
- #
262
- # Returns nothing
263
- def assign_current_document!
264
- payload["site"].current_document = document
265
- end
266
-
267
- # Set highlighter prefix and suffix
268
- #
269
- # Returns nothing
270
- def assign_highlighter_options!
271
- payload["highlighter_prefix"] = converters.first.highlighter_prefix
272
- payload["highlighter_suffix"] = converters.first.highlighter_suffix
273
- end
274
-
275
- def assign_layout_data!
276
- layout = layouts[document.data["layout"]]
277
- payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {}) if layout
278
- end
279
-
280
166
  def permalink_ext
281
167
  document_permalink = document.permalink
282
168
  if document_permalink && !document_permalink.end_with?("/")
@@ -298,9 +184,5 @@ module Bridgetown
298
184
  c.output_ext(document.extname)
299
185
  end.compact
300
186
  end
301
-
302
- def liquid_options
303
- @liquid_options ||= site.config["liquid"]
304
- end
305
187
  end
306
188
  end