bridgetown-core 0.16.0.beta1 → 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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/bridgetown-core.gemspec +3 -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/concerns/actions.rb +2 -1
  8. data/lib/bridgetown-core/commands/console.rb +4 -4
  9. data/lib/bridgetown-core/commands/new.rb +1 -1
  10. data/lib/bridgetown-core/concerns/layout_placeable.rb +1 -1
  11. data/lib/bridgetown-core/concerns/liquid_renderable.rb +10 -0
  12. data/lib/bridgetown-core/concerns/site/configurable.rb +24 -22
  13. data/lib/bridgetown-core/concerns/site/content.rb +46 -33
  14. data/lib/bridgetown-core/concerns/site/extensible.rb +14 -13
  15. data/lib/bridgetown-core/concerns/site/localizable.rb +24 -0
  16. data/lib/bridgetown-core/concerns/site/processable.rb +12 -11
  17. data/lib/bridgetown-core/concerns/site/renderable.rb +35 -28
  18. data/lib/bridgetown-core/concerns/site/writable.rb +7 -15
  19. data/lib/bridgetown-core/concerns/validatable.rb +2 -2
  20. data/lib/bridgetown-core/configuration.rb +14 -6
  21. data/lib/bridgetown-core/converter.rb +0 -42
  22. data/lib/bridgetown-core/converters/erb_templates.rb +93 -17
  23. data/lib/bridgetown-core/converters/liquid_templates.rb +96 -0
  24. data/lib/bridgetown-core/converters/markdown.rb +0 -3
  25. data/lib/bridgetown-core/document.rb +34 -21
  26. data/lib/bridgetown-core/drops/site_drop.rb +5 -1
  27. data/lib/bridgetown-core/drops/unified_payload_drop.rb +0 -1
  28. data/lib/bridgetown-core/drops/url_drop.rb +19 -3
  29. data/lib/bridgetown-core/excerpt.rb +1 -1
  30. data/lib/bridgetown-core/filters.rb +37 -55
  31. data/lib/bridgetown-core/filters/condition_helpers.rb +56 -0
  32. data/lib/bridgetown-core/frontmatter_defaults.rb +17 -0
  33. data/lib/bridgetown-core/generators/prototype_generator.rb +42 -25
  34. data/lib/bridgetown-core/helpers.rb +84 -0
  35. data/lib/bridgetown-core/liquid_renderer.rb +1 -1
  36. data/lib/bridgetown-core/log_writer.rb +2 -2
  37. data/lib/bridgetown-core/page.rb +8 -2
  38. data/lib/bridgetown-core/plugin_manager.rb +44 -3
  39. data/lib/bridgetown-core/reader.rb +2 -4
  40. data/lib/bridgetown-core/readers/collection_reader.rb +1 -0
  41. data/lib/bridgetown-core/readers/data_reader.rb +4 -3
  42. data/lib/bridgetown-core/readers/defaults_reader.rb +27 -0
  43. data/lib/bridgetown-core/readers/layout_reader.rb +1 -0
  44. data/lib/bridgetown-core/readers/page_reader.rb +1 -0
  45. data/lib/bridgetown-core/readers/post_reader.rb +29 -15
  46. data/lib/bridgetown-core/readers/static_file_reader.rb +1 -0
  47. data/lib/bridgetown-core/renderer.rb +42 -160
  48. data/lib/bridgetown-core/ruby_template_view.rb +26 -8
  49. data/lib/bridgetown-core/site.rb +14 -2
  50. data/lib/bridgetown-core/tags/find.rb +86 -0
  51. data/lib/bridgetown-core/tags/t.rb +14 -0
  52. data/lib/bridgetown-core/tags/webpack_path.rb +6 -41
  53. data/lib/bridgetown-core/utils.rb +69 -2
  54. data/lib/bridgetown-core/utils/ruby_exec.rb +1 -1
  55. data/lib/bridgetown-core/version.rb +2 -2
  56. data/lib/bridgetown-core/watcher.rb +1 -0
  57. data/lib/site_template/src/_layouts/{default.html → default.liquid} +0 -0
  58. data/lib/site_template/src/_layouts/{home.html → home.liquid} +0 -0
  59. data/lib/site_template/src/_layouts/{page.html → page.liquid} +0 -0
  60. data/lib/site_template/src/_layouts/{post.html → post.liquid} +0 -0
  61. data/lib/site_template/src/images/.keep +1 -0
  62. metadata +47 -10
@@ -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
@@ -68,12 +71,20 @@ module Bridgetown
68
71
  # If that exact package hasn't been installed, execute yarn add
69
72
  #
70
73
  # Returns nothing.
71
- def self.install_yarn_dependencies(required_gems)
74
+ def self.install_yarn_dependencies(required_gems, single_gemname = nil)
72
75
  return unless File.exist?("package.json")
73
76
 
74
77
  package_json = JSON.parse(File.read("package.json"))
75
78
 
76
- required_gems.each do |loaded_gem|
79
+ gems_to_search = if single_gemname
80
+ required_gems.select do |loaded_gem|
81
+ loaded_gem.to_spec&.name == single_gemname.to_s
82
+ end
83
+ else
84
+ required_gems
85
+ end
86
+
87
+ gems_to_search.each do |loaded_gem|
77
88
  yarn_dependency = find_yarn_dependency(loaded_gem)
78
89
  next unless add_yarn_dependency?(yarn_dependency, package_json)
79
90
 
@@ -150,5 +161,35 @@ module Bridgetown
150
161
  Array(site.config["plugins_dir"]).map { |d| File.expand_path(d) }
151
162
  end
152
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
153
194
  end
154
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, ".*"))