bridgetown-core 0.16.0 → 0.18.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/bridgetown-core.gemspec +2 -1
  4. data/lib/bridgetown-core.rb +45 -29
  5. data/lib/bridgetown-core/collection.rb +5 -1
  6. data/lib/bridgetown-core/commands/apply.rb +2 -2
  7. data/lib/bridgetown-core/commands/console.rb +4 -4
  8. data/lib/bridgetown-core/commands/new.rb +1 -1
  9. data/lib/bridgetown-core/concerns/layout_placeable.rb +1 -1
  10. data/lib/bridgetown-core/concerns/liquid_renderable.rb +10 -0
  11. data/lib/bridgetown-core/concerns/site/configurable.rb +24 -22
  12. data/lib/bridgetown-core/concerns/site/content.rb +46 -33
  13. data/lib/bridgetown-core/concerns/site/extensible.rb +14 -13
  14. data/lib/bridgetown-core/concerns/site/localizable.rb +24 -0
  15. data/lib/bridgetown-core/concerns/site/processable.rb +12 -15
  16. data/lib/bridgetown-core/concerns/site/renderable.rb +35 -28
  17. data/lib/bridgetown-core/concerns/site/writable.rb +7 -15
  18. data/lib/bridgetown-core/concerns/validatable.rb +2 -2
  19. data/lib/bridgetown-core/configuration.rb +14 -6
  20. data/lib/bridgetown-core/converter.rb +0 -42
  21. data/lib/bridgetown-core/converters/erb_templates.rb +75 -16
  22. data/lib/bridgetown-core/converters/liquid_templates.rb +96 -0
  23. data/lib/bridgetown-core/converters/markdown.rb +0 -3
  24. data/lib/bridgetown-core/document.rb +34 -21
  25. data/lib/bridgetown-core/drops/site_drop.rb +5 -1
  26. data/lib/bridgetown-core/drops/unified_payload_drop.rb +0 -1
  27. data/lib/bridgetown-core/drops/url_drop.rb +19 -3
  28. data/lib/bridgetown-core/excerpt.rb +1 -1
  29. data/lib/bridgetown-core/filters.rb +37 -55
  30. data/lib/bridgetown-core/filters/condition_helpers.rb +56 -0
  31. data/lib/bridgetown-core/frontmatter_defaults.rb +17 -0
  32. data/lib/bridgetown-core/generators/prototype_generator.rb +42 -25
  33. data/lib/bridgetown-core/helpers.rb +84 -0
  34. data/lib/bridgetown-core/liquid_renderer.rb +1 -1
  35. data/lib/bridgetown-core/log_writer.rb +2 -2
  36. data/lib/bridgetown-core/page.rb +8 -2
  37. data/lib/bridgetown-core/plugin_manager.rb +34 -1
  38. data/lib/bridgetown-core/reader.rb +2 -4
  39. data/lib/bridgetown-core/readers/collection_reader.rb +1 -0
  40. data/lib/bridgetown-core/readers/data_reader.rb +4 -3
  41. data/lib/bridgetown-core/readers/defaults_reader.rb +27 -0
  42. data/lib/bridgetown-core/readers/layout_reader.rb +1 -0
  43. data/lib/bridgetown-core/readers/page_reader.rb +1 -0
  44. data/lib/bridgetown-core/readers/post_reader.rb +29 -15
  45. data/lib/bridgetown-core/readers/static_file_reader.rb +1 -0
  46. data/lib/bridgetown-core/renderer.rb +42 -162
  47. data/lib/bridgetown-core/ruby_template_view.rb +26 -22
  48. data/lib/bridgetown-core/site.rb +14 -2
  49. data/lib/bridgetown-core/tags/find.rb +86 -0
  50. data/lib/bridgetown-core/tags/render_content.rb +2 -2
  51. data/lib/bridgetown-core/tags/t.rb +14 -0
  52. data/lib/bridgetown-core/utils.rb +16 -2
  53. data/lib/bridgetown-core/utils/ruby_exec.rb +1 -1
  54. data/lib/bridgetown-core/version.rb +2 -2
  55. data/lib/bridgetown-core/watcher.rb +1 -0
  56. data/lib/site_template/src/images/.keep +1 -0
  57. metadata +29 -6
@@ -54,6 +54,10 @@ module Bridgetown
54
54
  #
55
55
  # Returns the default value or nil if none was found
56
56
  def find(path, type, setting)
57
+ merged_data = {}
58
+ merge_data_cascade_for_path(path, merged_data)
59
+ return merged_data[setting] if merged_data.key?(setting)
60
+
57
61
  value = nil
58
62
  old_scope = nil
59
63
 
@@ -74,6 +78,9 @@ module Bridgetown
74
78
  # Returns a hash with all default values (an empty hash if there are none)
75
79
  def all(path, type)
76
80
  defaults = {}
81
+
82
+ merge_data_cascade_for_path(path, defaults)
83
+
77
84
  old_scope = nil
78
85
  matching_sets(path, type).each do |set|
79
86
  if has_precedence?(old_scope, set["scope"])
@@ -88,6 +95,16 @@ module Bridgetown
88
95
 
89
96
  private
90
97
 
98
+ def merge_data_cascade_for_path(path, merged_data)
99
+ absolute_path = @site.in_source_dir(path)
100
+ @site.defaults_reader.path_defaults
101
+ .select { |k, _v| absolute_path.include? k }
102
+ .sort_by { |k, _v| k.length }
103
+ .each do |defaults|
104
+ merged_data.merge!(defaults[1])
105
+ end
106
+ end
107
+
91
108
  # Checks if a given default setting scope matches the given path and type
92
109
  #
93
110
  # scope - the hash indicating the scope, as defined in bridgetown.config.yml
@@ -10,6 +10,9 @@ module Bridgetown
10
10
  class PrototypeGenerator < Generator
11
11
  priority :low
12
12
 
13
+ # @return [Bridgetown::Site]
14
+ attr_reader :site
15
+
13
16
  @matching_templates = []
14
17
 
15
18
  def self.add_matching_template(template)
@@ -17,6 +20,7 @@ module Bridgetown
17
20
  end
18
21
 
19
22
  class << self
23
+ # @return [Array<Page>]
20
24
  attr_reader :matching_templates
21
25
  end
22
26
 
@@ -44,7 +48,13 @@ module Bridgetown
44
48
  end
45
49
  end
46
50
 
51
+ # Check incoming prototype configuration and normalize options.
52
+ #
53
+ # @param prototype_page [PrototypePage]
54
+ #
55
+ # @return [String, nil]
47
56
  def validate_search_term(prototype_page)
57
+ # @type [String]
48
58
  search_term = prototype_page.data["prototype"]["term"]
49
59
  return nil unless search_term.is_a?(String)
50
60
 
@@ -52,6 +62,8 @@ module Bridgetown
52
62
  @configured_collection = prototype_page.data["prototype"]["collection"]
53
63
  end
54
64
 
65
+ return nil unless site.collections[@configured_collection]
66
+
55
67
  # Categories and Tags are unique in that singular and plural front matter
56
68
  # can be present for each
57
69
  search_term.sub(%r!^category$!, "categories").sub(%r!^tag$!, "tags")
@@ -59,71 +71,75 @@ module Bridgetown
59
71
 
60
72
  def generate_new_page_from_prototype(prototype_page, search_term, term)
61
73
  new_page = PrototypePage.new(prototype_page, @configured_collection, search_term, term)
62
- @site.pages << new_page
74
+ site.pages << new_page
63
75
  new_page
64
76
  end
65
77
 
66
- # TODO: this would be a great use of .try
67
- # document.try(:collection).try(:label) == @configured_collection
78
+ # Provide a list of all relevent indexed values for the given term.
79
+ #
80
+ # @param search_term [String]
81
+ #
82
+ # @return [Array<String>]
68
83
  def terms_matching_pages(search_term)
69
- selected_docs = @site.documents.select do |document|
70
- document.respond_to?(:collection) && document.collection.label == @configured_collection
71
- end
72
-
73
84
  Bridgetown::Paginate::PaginationIndexer.index_documents_by(
74
- selected_docs, search_term
85
+ site.collections[@configured_collection].docs, search_term
75
86
  ).keys
76
87
  end
77
88
  end
78
89
 
79
90
  class PrototypePage < Page
80
- def initialize(prototype_page, collection, search_term, term)
81
- @site = prototype_page.site
91
+ # @return [Page]
92
+ attr_reader :prototyped_page
93
+
94
+ def initialize(prototyped_page, collection, search_term, term)
95
+ @prototyped_page = prototyped_page
96
+ @site = prototyped_page.site
82
97
  @url = ""
83
98
  @name = "index.html"
84
- @path = prototype_page.path
99
+ @path = prototyped_page.path
85
100
 
86
101
  process(@name)
87
102
 
88
- self.data = Bridgetown::Utils.deep_merge_hashes prototype_page.data, {}
89
- self.content = prototype_page.content
103
+ self.data = Bridgetown::Utils.deep_merge_hashes prototyped_page.data, {}
104
+ self.content = prototyped_page.content
90
105
 
91
106
  # Perform some validation that is also performed in Bridgetown::Page
92
- validate_data! prototype_page.path
93
- validate_permalink! prototype_page.path
107
+ validate_data! prototyped_page.path
108
+ validate_permalink! prototyped_page.path
94
109
 
95
- @dir = Pathname.new(prototype_page.relative_path).dirname.to_s
110
+ @dir = Pathname.new(prototyped_page.relative_path).dirname.to_s
96
111
  @path = site.in_source_dir(@dir, @name)
97
112
 
98
- process_prototype_page_data(prototype_page, collection, search_term, term)
113
+ process_prototype_page_data(collection, search_term, term)
99
114
 
100
115
  Bridgetown::Hooks.trigger :pages, :post_init, self
101
116
  end
102
117
 
103
- def process_prototype_page_data(prototype_page, collection, search_term, term)
118
+ def process_prototype_page_data(collection, search_term, term)
104
119
  # Fill in pagination details to be handled later by Bridgetown::Paginate
105
120
  data["pagination"] = Bridgetown::Utils.deep_merge_hashes(
106
- prototype_page.data["pagination"].to_h, {
121
+ prototyped_page.data["pagination"].to_h, {
107
122
  "enabled" => true,
108
123
  "collection" => collection,
109
124
  "where_query" => [search_term, term],
110
125
  }
111
126
  )
112
127
  # Use the original prototype page term so we get "tag" back, not "tags":
113
- data[prototype_page.data["prototype"]["term"]] = term
128
+ data[prototyped_page.data["prototype"]["term"]] = term
114
129
  # Process title and slugs/URLs:
115
- process_title_data_placeholder(prototype_page, search_term, term)
130
+ process_title_data_placeholder(search_term, term)
116
131
  process_title_simple_placeholders(term)
117
132
  slugify_term(term)
118
133
  end
119
134
 
120
- def process_title_data_placeholder(prototype_page, search_term, term)
121
- if prototype_page["prototype"]["data"]
135
+ # rubocop:disable Metrics/AbcSize
136
+ def process_title_data_placeholder(search_term, term)
137
+ if prototyped_page.data["prototype"]["data"]
122
138
  if data["title"]&.include?(":prototype-data-label")
123
- related_data = @site.data[prototype_page["prototype"]["data"]].dig(term)
139
+ related_data = site.data[prototyped_page.data["prototype"]["data"]].dig(term)
124
140
  if related_data
125
141
  data["#{search_term}_data"] = related_data
126
- data_label = related_data[prototype_page["prototype"]["data_label"]]
142
+ data_label = related_data[prototyped_page.data["prototype"]["data_label"]]
127
143
  data["title"] = data["title"].gsub(
128
144
  ":prototype-data-label", data_label
129
145
  )
@@ -131,6 +147,7 @@ module Bridgetown
131
147
  end
132
148
  end
133
149
  end
150
+ # rubocop:enable Metrics/AbcSize
134
151
 
135
152
  def process_title_simple_placeholders(term)
136
153
  if data["title"]&.include?(":prototype-term-titleize")
@@ -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
@@ -9,7 +9,7 @@ module Bridgetown
9
9
  include Validatable
10
10
 
11
11
  attr_writer :dir
12
- attr_accessor :site, :pager
12
+ attr_accessor :site, :paginator, :pager
13
13
  attr_accessor :name, :ext, :basename
14
14
  attr_accessor :data, :content, :output
15
15
 
@@ -121,12 +121,18 @@ module Bridgetown
121
121
  # desired placeholder replacements. For details see "url.rb"
122
122
  def url_placeholders
123
123
  {
124
- path: @dir,
124
+ path: qualified_pages_path_for_url,
125
125
  basename: basename,
126
126
  output_ext: output_ext,
127
127
  }
128
128
  end
129
129
 
130
+ # Strips _pages prefix off if needed for the url/destination generation
131
+ # @return [String]
132
+ def qualified_pages_path_for_url
133
+ @dir.sub(%r!^/_pages!, "")
134
+ end
135
+
130
136
  # Extract information from the page filename.
131
137
  #
132
138
  # name - The String filename of the page file.
@@ -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
@@ -13,6 +13,7 @@ module Bridgetown
13
13
  # Returns nothing.
14
14
  # rubocop:disable Metrics/AbcSize
15
15
  def read
16
+ @site.defaults_reader.read
16
17
  @site.layouts = LayoutReader.new(site).read
17
18
  read_directories
18
19
  read_included_excludes
@@ -75,10 +76,7 @@ module Bridgetown
75
76
  def retrieve_posts(dir)
76
77
  return if outside_configured_directory?(dir)
77
78
 
78
- post_reader.read_posts(dir).tap do |entries|
79
- site.posts.docs.concat(entries.select { |entry| entry.is_a?(Document) })
80
- site.posts.files.concat(entries.select { |entry| entry.is_a?(StaticFile) })
81
- end
79
+ post_reader.read_posts(dir)
82
80
  end
83
81
 
84
82
  # Recursively traverse directories with the read_directories function.
@@ -5,6 +5,7 @@ module Bridgetown
5
5
  SPECIAL_COLLECTIONS = %w(posts data).freeze
6
6
 
7
7
  attr_reader :site, :content
8
+
8
9
  def initialize(site)
9
10
  @site = site
10
11
  @content = {}
@@ -3,9 +3,10 @@
3
3
  module Bridgetown
4
4
  class DataReader
5
5
  attr_reader :site, :content
6
+
6
7
  def initialize(site)
7
8
  @site = site
8
- @content = ActiveSupport::HashWithIndifferentAccess.new
9
+ @content = {}
9
10
  @entry_filter = EntryFilter.new(site)
10
11
  end
11
12
 
@@ -19,7 +20,7 @@ module Bridgetown
19
20
  base = site.in_source_dir(dir)
20
21
  read_data_to(base, @content)
21
22
  merge_environment_specific_metadata!
22
- @content
23
+ @content = @content.with_dot_access
23
24
  end
24
25
 
25
26
  # Read and parse all .yaml, .yml, .json, .csv and .tsv
@@ -43,7 +44,7 @@ module Bridgetown
43
44
  if File.directory?(path)
44
45
  read_data_to(
45
46
  path,
46
- data[sanitize_filename(entry)] = ActiveSupport::HashWithIndifferentAccess.new
47
+ data[sanitize_filename(entry)] = {}
47
48
  )
48
49
  else
49
50
  key = sanitize_filename(File.basename(entry, ".*"))
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ class DefaultsReader
5
+ attr_reader :site, :path_defaults
6
+
7
+ def initialize(site)
8
+ @site = site
9
+ @path_defaults = HashWithDotAccess::Hash.new
10
+ end
11
+
12
+ def read
13
+ return unless File.directory?(site.source)
14
+
15
+ entries = Dir.chdir(site.source) do
16
+ Dir["**/_defaults.{yaml,yml,json}"]
17
+ end
18
+
19
+ entries.each do |entry|
20
+ path = @site.in_source_dir(entry)
21
+ @path_defaults[File.dirname(path) + File::SEPARATOR] = SafeYAML.load_file(path)
22
+ end
23
+
24
+ @path_defaults
25
+ end
26
+ end
27
+ end