bridgetown-core 0.17.1 → 0.18.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) 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 -13
  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 +80 -16
  19. data/lib/bridgetown-core/converters/liquid_templates.rb +104 -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/layout_reader.rb +1 -1
  34. data/lib/bridgetown-core/readers/post_reader.rb +28 -15
  35. data/lib/bridgetown-core/renderer.rb +42 -162
  36. data/lib/bridgetown-core/ruby_template_view.rb +23 -34
  37. data/lib/bridgetown-core/site.rb +12 -2
  38. data/lib/bridgetown-core/tags/render_content.rb +2 -2
  39. data/lib/bridgetown-core/utils.rb +14 -0
  40. data/lib/bridgetown-core/version.rb +2 -2
  41. data/lib/bridgetown-core/watcher.rb +1 -0
  42. data/lib/site_template/src/images/.keep +1 -0
  43. metadata +7 -3
@@ -0,0 +1,104 @@
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
+ return content if convertible.data[:template_engine] != "liquid"
27
+
28
+ self.class.cached_partials ||= {}
29
+ @payload = nil
30
+
31
+ @site = convertible.site
32
+ if convertible.is_a?(Bridgetown::Layout)
33
+ @document = convertible.current_document
34
+ @layout = convertible
35
+ configure_payload(layout.current_document_output)
36
+ else
37
+ @document = convertible
38
+ @layout = site.layouts[document.data["layout"]]
39
+ configure_payload
40
+ end
41
+
42
+ template = site.liquid_renderer.file(convertible.path).parse(content)
43
+ template.warnings.each do |e|
44
+ Bridgetown.logger.warn "Liquid Warning:",
45
+ LiquidRenderer.format_error(e, convertible.path)
46
+ end
47
+ template.render!(payload, liquid_context)
48
+ # rubocop: disable Lint/RescueException
49
+ rescue Exception => e
50
+ Bridgetown.logger.error "Liquid Exception:",
51
+ LiquidRenderer.format_error(e, convertible.path)
52
+ raise e
53
+ end
54
+ # rubocop: enable Lint/RescueException
55
+ # rubocop: enable Metrics/MethodLength
56
+ # rubocop: enable Metrics/AbcSize
57
+
58
+ def matches(ext, convertible)
59
+ if convertible.render_with_liquid?
60
+ convertible.data[:template_engine] = "liquid"
61
+ return true
62
+ end
63
+
64
+ super(ext).tap do |ext_matches|
65
+ convertible.data[:template_engine] = "liquid" if ext_matches
66
+ end
67
+ end
68
+
69
+ def output_ext(ext)
70
+ ext == ".liquid" ? ".html" : ext
71
+ end
72
+
73
+ # Fetches the payload used in Liquid rendering.
74
+ # Falls back to site.site_payload if no payload is set.
75
+ #
76
+ # Returns a Bridgetown::Drops::UnifiedPayloadDrop
77
+ def payload
78
+ @payload ||= site.site_payload
79
+ end
80
+
81
+ # Set page content to payload and assign paginator if document has one.
82
+ #
83
+ # Returns nothing
84
+ def configure_payload(content = nil)
85
+ payload["page"] = document.to_liquid
86
+ payload["paginator"] = document.respond_to?(:paginator) ? document.paginator.to_liquid : nil
87
+ payload["layout"] = @layout ? @layout.data : {}
88
+ payload["content"] = content
89
+ end
90
+
91
+ def liquid_context
92
+ {
93
+ registers: {
94
+ site: site,
95
+ page: payload["page"],
96
+ cached_partials: self.class.cached_partials,
97
+ },
98
+ strict_filters: site.config["liquid"]["strict_filters"],
99
+ strict_variables: site.config["liquid"]["strict_variables"],
100
+ }
101
+ end
102
+ end
103
+ end
104
+ 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")