bridgetown-core 0.12.0 → 0.15.0.beta1

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -1
  3. data/bin/bridgetown +9 -48
  4. data/bridgetown-core.gemspec +10 -5
  5. data/lib/bridgetown-core.rb +20 -4
  6. data/lib/bridgetown-core/cleaner.rb +1 -0
  7. data/lib/bridgetown-core/commands/apply.rb +73 -0
  8. data/lib/bridgetown-core/commands/base.rb +45 -0
  9. data/lib/bridgetown-core/commands/build.rb +91 -86
  10. data/lib/bridgetown-core/commands/clean.rb +30 -29
  11. data/lib/bridgetown-core/commands/concerns/actions.rb +95 -0
  12. data/lib/bridgetown-core/commands/concerns/build_options.rb +76 -0
  13. data/lib/bridgetown-core/commands/concerns/configuration_overridable.rb +18 -0
  14. data/lib/bridgetown-core/commands/concerns/summarizable.rb +13 -0
  15. data/lib/bridgetown-core/commands/console.rb +46 -38
  16. data/lib/bridgetown-core/commands/doctor.rb +125 -135
  17. data/lib/bridgetown-core/commands/new.rb +120 -158
  18. data/lib/bridgetown-core/commands/plugins.rb +206 -0
  19. data/lib/bridgetown-core/commands/registrations.rb +16 -0
  20. data/lib/bridgetown-core/commands/serve.rb +214 -217
  21. data/lib/bridgetown-core/{convertible.rb → concerns/convertible.rb} +2 -2
  22. data/lib/bridgetown-core/concerns/site/configurable.rb +153 -0
  23. data/lib/bridgetown-core/concerns/site/content.rb +111 -0
  24. data/lib/bridgetown-core/concerns/site/extensible.rb +56 -0
  25. data/lib/bridgetown-core/concerns/site/processable.rb +74 -0
  26. data/lib/bridgetown-core/concerns/site/renderable.rb +50 -0
  27. data/lib/bridgetown-core/concerns/site/writable.rb +31 -0
  28. data/lib/bridgetown-core/configuration.rb +98 -108
  29. data/lib/bridgetown-core/converters/markdown/kramdown_parser.rb +0 -3
  30. data/lib/bridgetown-core/document.rb +1 -1
  31. data/lib/bridgetown-core/drops/bridgetown_drop.rb +6 -1
  32. data/lib/bridgetown-core/drops/site_drop.rb +1 -2
  33. data/lib/bridgetown-core/external.rb +17 -21
  34. data/lib/bridgetown-core/filters.rb +10 -0
  35. data/lib/bridgetown-core/generators/prototype_generator.rb +3 -1
  36. data/lib/bridgetown-core/hooks.rb +62 -62
  37. data/lib/bridgetown-core/layout.rb +10 -4
  38. data/lib/bridgetown-core/liquid_renderer.rb +2 -0
  39. data/lib/bridgetown-core/liquid_renderer/file_system.rb +5 -1
  40. data/lib/bridgetown-core/page.rb +9 -2
  41. data/lib/bridgetown-core/plugin.rb +2 -0
  42. data/lib/bridgetown-core/plugin_manager.rb +68 -14
  43. data/lib/bridgetown-core/reader.rb +5 -0
  44. data/lib/bridgetown-core/readers/data_reader.rb +15 -2
  45. data/lib/bridgetown-core/readers/layout_reader.rb +9 -2
  46. data/lib/bridgetown-core/readers/plugin_content_reader.rb +48 -0
  47. data/lib/bridgetown-core/renderer.rb +51 -32
  48. data/lib/bridgetown-core/site.rb +20 -449
  49. data/lib/bridgetown-core/static_file.rb +1 -5
  50. data/lib/bridgetown-core/tags/include.rb +12 -0
  51. data/lib/bridgetown-core/tags/render_content.rb +27 -16
  52. data/lib/bridgetown-core/tags/with.rb +15 -0
  53. data/lib/bridgetown-core/utils.rb +2 -27
  54. data/lib/bridgetown-core/utils/ruby_exec.rb +66 -0
  55. data/lib/bridgetown-core/version.rb +2 -2
  56. data/lib/bridgetown-core/watcher.rb +21 -10
  57. data/lib/site_template/Gemfile.erb +19 -0
  58. data/lib/site_template/plugins/{.keep → builders/.keep} +0 -0
  59. data/lib/site_template/plugins/site_builder.rb +4 -0
  60. data/lib/site_template/src/_components/footer.html +3 -0
  61. data/lib/site_template/src/_components/head.html +9 -0
  62. data/lib/site_template/src/{_includes → _components}/navbar.html +1 -0
  63. data/lib/site_template/src/_layouts/default.html +3 -3
  64. data/lib/site_template/src/{_components/.keep → favicon.ico} +0 -0
  65. data/lib/site_template/src/posts.md +15 -0
  66. data/lib/site_template/start.js +1 -1
  67. metadata +107 -19
  68. data/lib/bridgetown-core/command.rb +0 -106
  69. data/lib/bridgetown-core/commands/help.rb +0 -34
  70. data/lib/site_template/src/_includes/footer.html +0 -3
  71. data/lib/site_template/src/_includes/head.html +0 -9
@@ -63,6 +63,16 @@ module Bridgetown
63
63
  Utils.slugify(input, mode: mode)
64
64
  end
65
65
 
66
+ # Titleize a slug or identifier string.
67
+ #
68
+ # input - The string to titleize.
69
+ #
70
+ # Returns a transformed string with spaces and capitalized words.
71
+ # See Utils.titleize_slug for more detail.
72
+ def titleize(input)
73
+ Utils.titleize_slug(input)
74
+ end
75
+
66
76
  # XML escape a string for use. Replaces any special characters with
67
77
  # appropriate HTML entity replacements.
68
78
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Bridgetown::Hooks.register :pages, :post_init do |page|
3
+ Bridgetown::Hooks.register :pages, :post_init, reloadable: false do |page|
4
4
  if page.class != Bridgetown::PrototypePage && page.data["prototype"].is_a?(Hash)
5
5
  Bridgetown::PrototypeGenerator.add_matching_template(page)
6
6
  end
@@ -63,6 +63,8 @@ module Bridgetown
63
63
  new_page
64
64
  end
65
65
 
66
+ # TODO: this would be a great use of .try
67
+ # document.try(:collection).try(:label) == @configured_collection
66
68
  def terms_matching_pages(search_term)
67
69
  selected_docs = @site.documents.select do |document|
68
70
  document.respond_to?(:collection) && document.collection.label == @configured_collection
@@ -2,6 +2,19 @@
2
2
 
3
3
  module Bridgetown
4
4
  module Hooks
5
+ HookRegistration = Struct.new(
6
+ :owner,
7
+ :event,
8
+ :priority,
9
+ :reloadable,
10
+ :block,
11
+ keyword_init: true
12
+ ) do
13
+ def to_s
14
+ "#{owner}:#{event} for #{block}"
15
+ end
16
+ end
17
+
5
18
  DEFAULT_PRIORITY = 20
6
19
 
7
20
  # compatibility layer for octopress-hooks users
@@ -12,51 +25,11 @@ module Bridgetown
12
25
  }.freeze
13
26
 
14
27
  # initial empty hooks
15
- @registry = {
16
- site: {
17
- after_init: [],
18
- after_reset: [],
19
- post_read: [],
20
- pre_render: [],
21
- post_render: [],
22
- post_write: [],
23
- },
24
- pages: {
25
- post_init: [],
26
- pre_render: [],
27
- post_render: [],
28
- post_write: [],
29
- },
30
- posts: {
31
- post_init: [],
32
- pre_render: [],
33
- post_render: [],
34
- post_write: [],
35
- },
36
- documents: {
37
- post_init: [],
38
- pre_render: [],
39
- post_render: [],
40
- post_write: [],
41
- },
42
- clean: {
43
- on_obsolete: [],
44
- },
45
- }
46
-
47
- # map of all hooks and their priorities
48
- @hook_priority = {}
28
+ @registry = {}
49
29
 
50
30
  NotAvailable = Class.new(RuntimeError)
51
31
  Uncallable = Class.new(RuntimeError)
52
32
 
53
- # register hook(s) to be called later, public API
54
- def self.register(owners, event, priority: DEFAULT_PRIORITY, &block)
55
- Array(owners).each do |owner|
56
- register_one(owner, event, priority_value(priority), &block)
57
- end
58
- end
59
-
60
33
  # Ensure the priority is a Fixnum
61
34
  def self.priority_value(priority)
62
35
  return priority if priority.is_a?(Integer)
@@ -64,39 +37,66 @@ module Bridgetown
64
37
  PRIORITY_MAP[priority] || DEFAULT_PRIORITY
65
38
  end
66
39
 
67
- # register a single hook to be called later, internal API
68
- def self.register_one(owner, event, priority, &block)
69
- @registry[owner] ||= {
70
- post_init: [],
71
- pre_render: [],
72
- post_render: [],
73
- post_write: [],
74
- }
75
-
76
- unless @registry[owner][event]
77
- raise NotAvailable, "Invalid hook. #{owner} supports only the " \
78
- "following hooks #{@registry[owner].keys.inspect}"
40
+ # register hook(s) to be called later
41
+ def self.register(owners, event, priority: DEFAULT_PRIORITY, reloadable: true, &block)
42
+ Array(owners).each do |owner|
43
+ register_one(owner, event, priority: priority, reloadable: reloadable, &block)
79
44
  end
45
+ end
46
+
47
+ # register a single hook to be called later
48
+ def self.register_one(owner, event, priority: DEFAULT_PRIORITY, reloadable: true, &block)
49
+ @registry[owner] ||= []
80
50
 
81
51
  raise Uncallable, "Hooks must respond to :call" unless block.respond_to? :call
82
52
 
83
- insert_hook owner, event, priority, &block
53
+ @registry[owner] << HookRegistration.new(
54
+ owner: owner,
55
+ event: event,
56
+ priority: priority_value(priority),
57
+ reloadable: reloadable,
58
+ block: block
59
+ )
60
+ if ENV["BRIDGETOWN_LOG_LEVEL"] == "debug"
61
+ if Bridgetown.respond_to?(:logger)
62
+ Bridgetown.logger.debug("Registering hook:", @registry[owner].last.to_s)
63
+ else
64
+ p "Registering hook:", @registry[owner].last.to_s
65
+ end
66
+ end
67
+
68
+ block
84
69
  end
85
70
 
86
- def self.insert_hook(owner, event, priority, &block)
87
- @hook_priority[block] = [-priority, @hook_priority.size]
88
- @registry[owner][event] << block
71
+ def self.remove_hook(owner, _event, block)
72
+ @registry[owner].delete_if { |item| item.block == block }
89
73
  end
90
74
 
91
- # interface for Bridgetown core components to trigger hooks
92
75
  def self.trigger(owner, event, *args)
93
76
  # proceed only if there are hooks to call
94
- hooks = @registry.dig(owner, event)
77
+ hooks = @registry[owner]&.select { |item| item.event == event }
95
78
  return if hooks.nil? || hooks.empty?
96
79
 
97
- # sort and call hooks according to priority and load order
98
- hooks.sort_by { |h| @hook_priority[h] }.each do |hook|
99
- hook.call(*args)
80
+ prioritized_hooks(hooks).each do |hook|
81
+ if ENV["BRIDGETOWN_LOG_LEVEL"] == "debug"
82
+ hook_info = args[0]&.respond_to?(:url) ? args[0].relative_path : hook.block
83
+ Bridgetown.logger.debug("Triggering hook:", "#{owner}:#{event} for #{hook_info}")
84
+ end
85
+ hook.block.call(*args)
86
+ end
87
+ end
88
+
89
+ def self.prioritized_hooks(hooks)
90
+ # sort hooks according to priority and load order
91
+ grouped_hooks = hooks.group_by(&:priority)
92
+ grouped_hooks.keys.sort.reverse.map { |priority| grouped_hooks[priority] }.flatten
93
+ end
94
+
95
+ def self.clear_reloadable_hooks
96
+ Bridgetown.logger.debug("Clearing reloadable hooks")
97
+
98
+ @registry.each_value do |hooks|
99
+ hooks.delete_if(&:reloadable)
100
100
  end
101
101
  end
102
102
  end
@@ -29,14 +29,20 @@ module Bridgetown
29
29
  #
30
30
  # site - The Site.
31
31
  # base - The String path to the source.
32
- # name - The String filename of the post file.
33
- def initialize(site, base, name)
32
+ # name - The String filename of the layout file.
33
+ # from_plugin - true if the layout comes from a Gem-based plugin folder.
34
+ def initialize(site, base, name, from_plugin: false)
34
35
  @site = site
35
36
  @base = base
36
37
  @name = name
37
38
 
38
- @base_dir = site.source
39
- @path = site.in_source_dir(base, name)
39
+ if from_plugin
40
+ @base_dir = base.sub("/layouts", "")
41
+ @path = File.join(base, name)
42
+ else
43
+ @base_dir = site.source
44
+ @path = site.in_source_dir(base, name)
45
+ end
40
46
  @relative_path = @path.sub(@base_dir, "")
41
47
 
42
48
  self.data = {}
@@ -17,6 +17,7 @@ module Bridgetown
17
17
  Liquid::Template.file_system = LiquidRenderer::FileSystem.new(
18
18
  @site.components_load_paths, "%s.liquid"
19
19
  )
20
+ Liquid::Template.file_system.site = site
20
21
 
21
22
  Liquid::Template.error_mode = @site.config["liquid"]["error_mode"].to_sym
22
23
  reset
@@ -25,6 +26,7 @@ module Bridgetown
25
26
  def reset
26
27
  @stats = {}
27
28
  @cache = {}
29
+ Renderer.cached_partials = {}
28
30
  end
29
31
 
30
32
  def file(filename)
@@ -3,6 +3,8 @@
3
3
  module Bridgetown
4
4
  class LiquidRenderer
5
5
  class FileSystem < Liquid::LocalFileSystem
6
+ attr_accessor :site
7
+
6
8
  def read_template_file(template_path)
7
9
  load_paths = root
8
10
  found_paths = []
@@ -29,7 +31,9 @@ module Bridgetown
29
31
  raise Liquid::FileSystemError, "No such template '#{template_path}'" if found_paths.empty?
30
32
 
31
33
  # Last path in the list wins
32
- ::File.read(found_paths.last)
34
+ LiquidComponent.parse(
35
+ ::File.read(found_paths.last, site.file_read_opts)
36
+ ).content
33
37
  end
34
38
  end
35
39
  end
@@ -35,12 +35,18 @@ module Bridgetown
35
35
  # base - The String path to the source.
36
36
  # dir - The String path between the source and the file.
37
37
  # name - The String filename of the file.
38
- def initialize(site, base, dir, name)
38
+ # from_plugin - true if the Page file is located in a Gem-based plugin folder
39
+ # rubocop:disable Metrics/ParameterLists
40
+ def initialize(site, base, dir, name, from_plugin: false)
39
41
  @site = site
40
42
  @base = base
41
43
  @dir = dir
42
44
  @name = name
43
- @path = site.in_source_dir(base, dir, name)
45
+ @path = if from_plugin
46
+ File.join(base, dir, name)
47
+ else
48
+ site.in_source_dir(base, dir, name)
49
+ end
44
50
 
45
51
  process(name)
46
52
  read_yaml(PathManager.join(base, dir), name)
@@ -51,6 +57,7 @@ module Bridgetown
51
57
 
52
58
  Bridgetown::Hooks.trigger :pages, :post_init, self
53
59
  end
60
+ # rubocop:enable Metrics/ParameterLists
54
61
 
55
62
  # The generated directory into which the page will be placed
56
63
  # upon generation. This is derived from the permalink or, if
@@ -10,6 +10,8 @@ module Bridgetown
10
10
  high: 10,
11
11
  }.freeze
12
12
 
13
+ SourceManifest = Struct.new(:origin, :components, :content, :layouts, keyword_init: true)
14
+
13
15
  #
14
16
 
15
17
  def self.inherited(const)
@@ -2,8 +2,33 @@
2
2
 
3
3
  module Bridgetown
4
4
  class PluginManager
5
+ PLUGINS_GROUP = :bridgetown_plugins
6
+
5
7
  attr_reader :site
6
8
 
9
+ @source_manifests = Set.new
10
+ @registered_plugins = Set.new
11
+
12
+ def self.add_source_manifest(source_manifest)
13
+ unless source_manifest.is_a?(Bridgetown::Plugin::SourceManifest)
14
+ raise "You must add a SourceManifest instance"
15
+ end
16
+
17
+ @source_manifests << source_manifest
18
+ end
19
+
20
+ def self.new_source_manifest(*args)
21
+ add_source_manifest(Bridgetown::Plugin::SourceManifest.new(*args))
22
+ end
23
+
24
+ def self.add_registered_plugin(gem_or_plugin_file)
25
+ @registered_plugins << gem_or_plugin_file
26
+ end
27
+
28
+ class << self
29
+ attr_reader :source_manifests, :registered_plugins
30
+ end
31
+
7
32
  # Create an instance of this class.
8
33
  #
9
34
  # site - the instance of Bridgetown::Site we're concerned with
@@ -13,22 +38,23 @@ module Bridgetown
13
38
  @site = site
14
39
  end
15
40
 
16
- # Require all the plugins which are allowed.
17
- #
18
- # Returns nothing
19
- def conscientious_require
20
- require_plugin_files
21
- end
22
-
23
41
  def self.require_from_bundler
24
42
  if !ENV["BRIDGETOWN_NO_BUNDLER_REQUIRE"] && File.file?("Gemfile")
25
43
  require "bundler"
26
44
 
27
- Bundler.setup
28
- required_gems = Bundler.require(:bridgetown_plugins)
45
+ required_gems = Bundler.require PLUGINS_GROUP
46
+ required_gems.select! do |dep|
47
+ (dep.groups & [PLUGINS_GROUP]).any? && dep.should_include?
48
+ end
49
+
29
50
  install_yarn_dependencies(required_gems)
30
- message = "Required #{required_gems.map(&:name).join(", ")}"
31
- Bridgetown.logger.debug("PluginManager:", message)
51
+
52
+ required_gems.each do |installed_gem|
53
+ add_registered_plugin installed_gem
54
+ end
55
+
56
+ Bridgetown.logger.debug("PluginManager:",
57
+ "Required #{required_gems.map(&:name).join(", ")}")
32
58
  ENV["BRIDGETOWN_NO_BUNDLER_REQUIRE"] = "true"
33
59
 
34
60
  true
@@ -41,6 +67,7 @@ module Bridgetown
41
67
  # If that exact package hasn't been installed, execute yarn add
42
68
  #
43
69
  # Returns nothing.
70
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
44
71
  def self.install_yarn_dependencies(required_gems)
45
72
  return unless File.exist?("package.json")
46
73
 
@@ -53,14 +80,17 @@ module Bridgetown
53
80
  next unless yarn_add_dependency.length == 2
54
81
 
55
82
  # check matching version number is see if it's already installed
56
- current_package = package_json["dependencies"].dig(yarn_add_dependency.first)
57
- next unless current_package.nil? || current_package != yarn_add_dependency.last
83
+ if package_json["dependencies"]
84
+ current_package = package_json["dependencies"].dig(yarn_add_dependency.first)
85
+ next unless current_package.nil? || current_package != yarn_add_dependency.last
86
+ end
58
87
 
59
88
  # all right, time to install the package
60
89
  cmd = "yarn add #{yarn_add_dependency.join("@")}"
61
90
  system cmd
62
91
  end
63
92
  end
93
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
64
94
 
65
95
  # Require all .rb files
66
96
  #
@@ -68,7 +98,31 @@ module Bridgetown
68
98
  def require_plugin_files
69
99
  plugins_path.each do |plugin_search_path|
70
100
  plugin_files = Utils.safe_glob(plugin_search_path, File.join("**", "*.rb"))
71
- Bridgetown::External.require_with_graceful_fail(plugin_files)
101
+
102
+ # Require "site_builder.rb" first if present so subclasses can all
103
+ # inherit from SiteBuilder without needing explicit require statements
104
+ sorted_plugin_files = plugin_files.select do |path|
105
+ path.include?("site_builder.rb")
106
+ end + plugin_files.reject do |path|
107
+ path.include?("site_builder.rb")
108
+ end
109
+
110
+ sorted_plugin_files.each do |plugin_file|
111
+ self.class.add_registered_plugin plugin_file
112
+ end
113
+ Bridgetown::External.require_with_graceful_fail(sorted_plugin_files)
114
+ end
115
+ end
116
+
117
+ # Reload .rb plugin files via the watcher
118
+ def reload_plugin_files
119
+ plugins_path.each do |plugin_search_path|
120
+ plugin_files = Utils.safe_glob(plugin_search_path, File.join("**", "*.rb"))
121
+ Array(plugin_files).each do |name|
122
+ Bridgetown.logger.debug "Reloading:", name.to_s
123
+ self.class.add_registered_plugin name
124
+ load name
125
+ end
72
126
  end
73
127
  end
74
128
 
@@ -11,6 +11,7 @@ module Bridgetown
11
11
  # Read Site data from disk and load it into internal data structures.
12
12
  #
13
13
  # Returns nothing.
14
+ # rubocop:disable Metrics/AbcSize
14
15
  def read
15
16
  @site.layouts = LayoutReader.new(site).read
16
17
  read_directories
@@ -18,7 +19,11 @@ module Bridgetown
18
19
  sort_files!
19
20
  @site.data = DataReader.new(site).read(site.config["data_dir"])
20
21
  CollectionReader.new(site).read
22
+ Bridgetown::PluginManager.source_manifests.map(&:content).compact.each do |plugin_content_dir|
23
+ PluginContentReader.new(site, plugin_content_dir).read
24
+ end
21
25
  end
26
+ # rubocop:enable Metrics/AbcSize
22
27
 
23
28
  # Sorts posts, pages, and static files.
24
29
  def sort_files!
@@ -5,7 +5,7 @@ module Bridgetown
5
5
  attr_reader :site, :content
6
6
  def initialize(site)
7
7
  @site = site
8
- @content = {}
8
+ @content = ActiveSupport::HashWithIndifferentAccess.new
9
9
  @entry_filter = EntryFilter.new(site)
10
10
  end
11
11
 
@@ -18,6 +18,7 @@ module Bridgetown
18
18
  def read(dir)
19
19
  base = site.in_source_dir(dir)
20
20
  read_data_to(base, @content)
21
+ merge_environment_specific_metadata!
21
22
  @content
22
23
  end
23
24
 
@@ -40,7 +41,10 @@ module Bridgetown
40
41
  next if @entry_filter.symlink?(path)
41
42
 
42
43
  if File.directory?(path)
43
- read_data_to(path, data[sanitize_filename(entry)] = {})
44
+ read_data_to(
45
+ path,
46
+ data[sanitize_filename(entry)] = ActiveSupport::HashWithIndifferentAccess.new
47
+ )
44
48
  else
45
49
  key = sanitize_filename(File.basename(entry, ".*"))
46
50
  data[key] = read_data_file(path)
@@ -67,6 +71,15 @@ module Bridgetown
67
71
  end
68
72
  end
69
73
 
74
+ def merge_environment_specific_metadata!
75
+ if @content["site_metadata"]
76
+ @content["site_metadata"][Bridgetown.environment]&.each_key do |k|
77
+ @content["site_metadata"][k] = @content["site_metadata"][Bridgetown.environment][k]
78
+ end
79
+ @content["site_metadata"].delete(Bridgetown.environment)
80
+ end
81
+ end
82
+
70
83
  def sanitize_filename(name)
71
84
  name.gsub(%r![^\w\s-]+|(?<=^|\b\s)\s+(?=$|\s?\b)!, "")
72
85
  .gsub(%r!\s+!, "_")