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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Converters
5
+ class LiquidTemplates < Converter
6
+ priority :highest
7
+ input :liquid
8
+
9
+ attr_reader :site, :document, :layout
10
+
11
+ class << self
12
+ attr_accessor :cached_partials
13
+ end
14
+
15
+ # rubocop: disable Metrics/AbcSize
16
+ # rubocop: disable Metrics/MethodLength
17
+
18
+ # Logic to do the Liquid content conversion.
19
+ #
20
+ # @param content [String] Content of the file (without front matter).
21
+ # @params convertible [Bridgetown::Page, Bridgetown::Document, Bridgetown::Layout]
22
+ # The instantiated object which is processing the file.
23
+ #
24
+ # @return [String] The converted content.
25
+ def convert(content, convertible)
26
+ self.class.cached_partials ||= {}
27
+
28
+ @site = convertible.site
29
+ if convertible.is_a?(Bridgetown::Layout)
30
+ @document = convertible.current_document
31
+ @layout = convertible
32
+ configure_payload(layout.current_document_output)
33
+ else
34
+ @document = convertible
35
+ @layout = site.layouts[document.data["layout"]]
36
+ configure_payload
37
+ end
38
+
39
+ template = site.liquid_renderer.file(convertible.path).parse(content)
40
+ template.warnings.each do |e|
41
+ Bridgetown.logger.warn "Liquid Warning:",
42
+ LiquidRenderer.format_error(e, convertible.path)
43
+ end
44
+ template.render!(payload, liquid_context)
45
+ # rubocop: disable Lint/RescueException
46
+ rescue Exception => e
47
+ Bridgetown.logger.error "Liquid Exception:",
48
+ LiquidRenderer.format_error(e, convertible.path)
49
+ raise e
50
+ end
51
+ # rubocop: enable Lint/RescueException
52
+ # rubocop: enable Metrics/MethodLength
53
+ # rubocop: enable Metrics/AbcSize
54
+
55
+ def matches(ext, convertible)
56
+ return true if convertible.render_with_liquid?
57
+
58
+ super(ext)
59
+ end
60
+
61
+ def output_ext(ext)
62
+ ext == ".liquid" ? ".html" : ext
63
+ end
64
+
65
+ # Fetches the payload used in Liquid rendering.
66
+ # Falls back to site.site_payload if no payload is set.
67
+ #
68
+ # Returns a Bridgetown::Drops::UnifiedPayloadDrop
69
+ def payload
70
+ @payload ||= site.site_payload
71
+ end
72
+
73
+ # Set page content to payload and assign paginator if document has one.
74
+ #
75
+ # Returns nothing
76
+ def configure_payload(content = nil)
77
+ payload["page"] = document.to_liquid
78
+ payload["paginator"] = document.respond_to?(:paginator) ? document.paginator.to_liquid : nil
79
+ payload["layout"] = @layout ? @layout.data : {}
80
+ payload["content"] = content
81
+ end
82
+
83
+ def liquid_context
84
+ {
85
+ registers: {
86
+ site: site,
87
+ page: payload["page"],
88
+ cached_partials: self.class.cached_partials,
89
+ },
90
+ strict_filters: site.config["liquid"]["strict_filters"],
91
+ strict_variables: site.config["liquid"]["strict_variables"],
92
+ }
93
+ end
94
+ end
95
+ end
96
+ end
@@ -5,9 +5,6 @@ module Bridgetown
5
5
  # Markdown converter.
6
6
  # For more info on converters see https://bridgetownrb.com/docs/plugins/converters/
7
7
  class Markdown < Converter
8
- highlighter_prefix "\n"
9
- highlighter_suffix "\n"
10
-
11
8
  def initialize(config = {})
12
9
  super
13
10
 
@@ -330,24 +330,6 @@ module Bridgetown
330
330
  data.key?(method.to_s) || super
331
331
  end
332
332
 
333
- def populate_categories
334
- categories = Array(data["categories"]) + Utils.pluralized_array_from_hash(
335
- data, "category", "categories"
336
- )
337
- categories.map!(&:to_s)
338
- categories.flatten!
339
- categories.uniq!
340
-
341
- merge_data!({ "categories" => categories })
342
- end
343
-
344
- def populate_tags
345
- tags = Utils.pluralized_array_from_hash(data, "tag", "tags")
346
- tags.flatten!
347
-
348
- merge_data!({ "tags" => tags })
349
- end
350
-
351
333
  private
352
334
 
353
335
  def merge_categories!(other)
@@ -384,6 +366,7 @@ module Bridgetown
384
366
  populate_title
385
367
  populate_categories
386
368
  populate_tags
369
+ determine_locale
387
370
  generate_excerpt
388
371
  end
389
372
 
@@ -418,6 +401,36 @@ module Bridgetown
418
401
  data["ext"] ||= ext
419
402
  end
420
403
 
404
+ def populate_categories
405
+ categories = Array(data["categories"]) + Utils.pluralized_array_from_hash(
406
+ data, "category", "categories"
407
+ )
408
+ categories.map!(&:to_s)
409
+ categories.flatten!
410
+ categories.uniq!
411
+
412
+ merge_data!({ "categories" => categories })
413
+ end
414
+
415
+ def populate_tags
416
+ tags = Utils.pluralized_array_from_hash(data, "tag", "tags")
417
+ tags.flatten!
418
+
419
+ merge_data!({ "tags" => tags })
420
+ end
421
+
422
+ def determine_locale
423
+ unless data["locale"]
424
+ # if locale key isn't directly set, look for alternative front matter
425
+ # or look at the filename pattern: slug.locale.ext
426
+ alernative = data["language"] || data["lang"] ||
427
+ basename_without_ext.split(".")[1..-1].last
428
+
429
+ data["locale"] = alernative if !alernative.nil? &&
430
+ site.config[:available_locales].include?(alernative)
431
+ end
432
+ end
433
+
421
434
  def modify_date(date)
422
435
  if !data["date"] || data["date"].to_i == site.time.to_i
423
436
  merge_data!({ "date" => date }, source: "filename")
@@ -50,6 +50,10 @@ module Bridgetown
50
50
  @documents ||= @obj.documents
51
51
  end
52
52
 
53
+ def contents
54
+ @contents ||= @obj.contents
55
+ end
56
+
53
57
  def metadata
54
58
  @site_metadata ||= @obj.data["site_metadata"]
55
59
  end
@@ -6,7 +6,6 @@ module Bridgetown
6
6
  mutable true
7
7
 
8
8
  attr_accessor :page, :layout, :content, :paginator
9
- attr_accessor :highlighter_prefix, :highlighter_suffix
10
9
 
11
10
  def bridgetown
12
11
  BridgetownDrop.global
@@ -19,14 +19,19 @@ module Bridgetown
19
19
  end
20
20
 
21
21
  def title
22
- Utils.slugify(@obj.data["slug"], mode: "pretty", cased: true) ||
23
- Utils.slugify(@obj.basename_without_ext, mode: "pretty", cased: true)
22
+ Utils.slugify(qualified_slug_data, mode: "pretty", cased: true)
24
23
  end
25
24
 
26
25
  def slug
27
- Utils.slugify(@obj.data["slug"]) || Utils.slugify(@obj.basename_without_ext)
26
+ Utils.slugify(qualified_slug_data)
28
27
  end
29
28
 
29
+ def locale
30
+ locale_data = @obj.data["locale"]
31
+ @obj.site.config["available_locales"].include?(locale_data) ? locale_data : nil
32
+ end
33
+ alias_method :lang, :locale
34
+
30
35
  def categories
31
36
  category_set = Set.new
32
37
  Array(@obj.data["categories"]).each do |category|
@@ -128,6 +133,17 @@ module Bridgetown
128
133
 
129
134
  private
130
135
 
136
+ def qualified_slug_data
137
+ slug_data = @obj.data["slug"] || @obj.basename_without_ext
138
+ if @obj.data["locale"]
139
+ slug_data.split(".").tap do |segments|
140
+ segments.pop if segments.length > 1 && segments.last == @obj.data["locale"]
141
+ end.join(".")
142
+ else
143
+ slug_data
144
+ end
145
+ end
146
+
131
147
  def fallback_data
132
148
  @fallback_data ||= {}
133
149
  end
@@ -83,7 +83,7 @@ module Bridgetown
83
83
 
84
84
  def output
85
85
  @output || (
86
- Renderer.new(doc.site, self, site.site_payload).run
86
+ Renderer.new(doc.site, self).run
87
87
  @output
88
88
  )
89
89
  end
@@ -79,16 +79,15 @@ module Bridgetown
79
79
  # XML escape a string for use. Replaces any special characters with
80
80
  # appropriate HTML entity replacements.
81
81
  #
82
- # input - The String to escape.
83
- #
84
82
  # Examples
85
83
  #
86
84
  # xml_escape('foo "bar" <baz>')
87
85
  # # => "foo &quot;bar&quot; &lt;baz&gt;"
88
86
  #
89
- # Returns the escaped String.
87
+ # @param input [String] The String to escape.
88
+ # @return [String] the escaped String.
90
89
  def xml_escape(input)
91
- input.to_s.encode(xml: :attr).gsub(%r!\A"|"\Z!, "")
90
+ Utils.xml_escape(input)
92
91
  end
93
92
 
94
93
  # CGI escape a string for use in a URL. Replaces any special characters
@@ -120,6 +119,21 @@ module Bridgetown
120
119
  Addressable::URI.normalize_component(input)
121
120
  end
122
121
 
122
+ # Obfuscate an email, telephone number etc.
123
+ #
124
+ # @param input[String] the String containing the contact information (email, phone etc.)
125
+ # @param prefix[String] the URL scheme to prefix (default "mailto")
126
+ # @return [String] a link unreadable for bots but will be recovered on focus or mouseover
127
+ def obfuscate_link(input, prefix = "mailto")
128
+ link = "<a href=\"#{prefix}:#{input}\">#{input}</a>"
129
+ script = "<script type=\"text/javascript\">document.currentScript.insertAdjacentHTML("
130
+ script += "beforebegin', '#{rot47(link)}'.replace(/[!-~]/g,"
131
+ script += "function(c){{var j=c.charCodeAt(0);if((j>=33)&&(j<=126)){"
132
+ script += "return String.fromCharCode(33+((j+ 14)%94));}"
133
+ script += "else{return String.fromCharCode(j);}}}));</script>"
134
+ script
135
+ end
136
+
123
137
  # Replace any whitespace in the input string with a single space
124
138
  #
125
139
  # input - The String on which to operate.
@@ -316,15 +330,22 @@ module Bridgetown
316
330
 
317
331
  # Convert an object into its String representation for debugging
318
332
  #
319
- # input - The Object to be converted
333
+ # @param input [Object] The Object to be converted
320
334
  #
321
- # Returns a String representation of the object.
322
- def inspect(input)
335
+ # @return [String] the representation of the object.
336
+ def inspect(input = nil)
337
+ return super() if input.nil?
338
+
323
339
  xml_escape(input.inspect)
324
340
  end
325
341
 
326
342
  private
327
343
 
344
+ # Perform a rot47 rotation for obfuscation
345
+ def rot47(input)
346
+ input.tr "!-~", "P-~!-O"
347
+ end
348
+
328
349
  # Sort the input Enumerable by the given property.
329
350
  # If the property doesn't exist, return the sort order respective of
330
351
  # which item doesn't have the property.
@@ -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")