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
@@ -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
 
@@ -63,7 +63,7 @@ module Bridgetown
63
63
  # Returns a Hash containing the data. An empty hash is returned if
64
64
  # no data was read.
65
65
  def data
66
- @data ||= ActiveSupport::HashWithIndifferentAccess.new
66
+ @data ||= HashWithDotAccess::Hash.new
67
67
  end
68
68
 
69
69
  # Merge some data in with this document's data.
@@ -170,7 +170,7 @@ module Bridgetown
170
170
  #
171
171
  # Returns the permalink or nil if no permalink was set in the data.
172
172
  def permalink
173
- data && data.is_a?(Hash) && data["permalink"]
173
+ data&.permalink
174
174
  end
175
175
 
176
176
  # The computed URL for the document. See `Bridgetown::URL#to_s` for more details.
@@ -295,7 +295,7 @@ module Bridgetown
295
295
 
296
296
  def previous_doc
297
297
  pos = collection.docs.index { |post| post.equal?(self) }
298
- collection.docs[pos - 1] if pos && pos.positive?
298
+ collection.docs[pos - 1] if pos&.positive?
299
299
  end
300
300
 
301
301
  def trigger_hooks(hook_name, *args)
@@ -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")
@@ -8,7 +8,7 @@ module Bridgetown
8
8
  mutable false
9
9
 
10
10
  def_delegator :@obj, :data
11
- def_delegators :@obj, :time, :pages, :static_files, :tags, :categories
11
+ def_delegators :@obj, :locale, :time, :pages, :static_files, :tags, :categories
12
12
 
13
13
  private def_delegator :@obj, :config, :fallback_data
14
14
 
@@ -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
@@ -7,6 +7,7 @@ module Bridgetown
7
7
  include URLFilters
8
8
  include GroupingFilters
9
9
  include DateFilters
10
+ include ConditionHelpers
10
11
 
11
12
  # Convert a Markdown string into HTML output.
12
13
  #
@@ -78,16 +79,15 @@ module Bridgetown
78
79
  # XML escape a string for use. Replaces any special characters with
79
80
  # appropriate HTML entity replacements.
80
81
  #
81
- # input - The String to escape.
82
- #
83
82
  # Examples
84
83
  #
85
84
  # xml_escape('foo "bar" <baz>')
86
85
  # # => "foo &quot;bar&quot; &lt;baz&gt;"
87
86
  #
88
- # Returns the escaped String.
87
+ # @param input [String] The String to escape.
88
+ # @return [String] the escaped String.
89
89
  def xml_escape(input)
90
- input.to_s.encode(xml: :attr).gsub(%r!\A"|"\Z!, "")
90
+ Utils.xml_escape(input)
91
91
  end
92
92
 
93
93
  # CGI escape a string for use in a URL. Replaces any special characters
@@ -119,6 +119,21 @@ module Bridgetown
119
119
  Addressable::URI.normalize_component(input)
120
120
  end
121
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
+
122
137
  # Replace any whitespace in the input string with a single space
123
138
  #
124
139
  # input - The String on which to operate.
@@ -137,6 +152,14 @@ module Bridgetown
137
152
  input.split.length
138
153
  end
139
154
 
155
+ # Calculates the average reading time of the supplied content.
156
+ # @param input [String] the String of content to analyze.
157
+ # @return [Float] the number of minutes required to read the content.
158
+ def reading_time(input, round_to = 0)
159
+ wpm = @context.registers[:site].config[:reading_time_wpm] || 250
160
+ (number_of_words(input).to_f / wpm).ceil(round_to)
161
+ end
162
+
140
163
  # Join an array of things into a string by separating with commas and the
141
164
  # word "and" for the last one.
142
165
  #
@@ -307,15 +330,22 @@ module Bridgetown
307
330
 
308
331
  # Convert an object into its String representation for debugging
309
332
  #
310
- # input - The Object to be converted
333
+ # @param input [Object] The Object to be converted
311
334
  #
312
- # Returns a String representation of the object.
313
- def inspect(input)
335
+ # @return [String] the representation of the object.
336
+ def inspect(input = nil)
337
+ return super() if input.nil?
338
+
314
339
  xml_escape(input.inspect)
315
340
  end
316
341
 
317
342
  private
318
343
 
344
+ # Perform a rot47 rotation for obfuscation
345
+ def rot47(input)
346
+ input.tr "!-~", "P-~!-O"
347
+ end
348
+
319
349
  # Sort the input Enumerable by the given property.
320
350
  # If the property doesn't exist, return the sort order respective of
321
351
  # which item doesn't have the property.
@@ -423,54 +453,6 @@ module Bridgetown
423
453
  end
424
454
  end
425
455
  end
426
-
427
- # ----------- The following set of code was *adapted* from Liquid::If
428
- # ----------- ref: https://git.io/vp6K6
429
-
430
- # Parse a string to a Liquid Condition
431
- def parse_condition(exp)
432
- parser = Liquid::Parser.new(exp)
433
- condition = parse_binary_comparison(parser)
434
-
435
- parser.consume(:end_of_string)
436
- condition
437
- end
438
-
439
- # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
440
- # the parsed expression based on whether the expression consists of binary operations with
441
- # Liquid operators `and` or `or`
442
- #
443
- # - parser: an instance of Liquid::Parser
444
- #
445
- # Returns an instance of Liquid::Condition
446
- def parse_binary_comparison(parser)
447
- condition = parse_comparison(parser)
448
- first_condition = condition
449
- while (binary_operator = parser.id?("and") || parser.id?("or"))
450
- child_condition = parse_comparison(parser)
451
- condition.send(binary_operator, child_condition)
452
- condition = child_condition
453
- end
454
- first_condition
455
- end
456
-
457
- # Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
458
- # expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
459
- #
460
- # - parser: an instance of Liquid::Parser
461
- #
462
- # Returns an instance of Liquid::Condition
463
- def parse_comparison(parser)
464
- left_operand = Liquid::Expression.parse(parser.expression)
465
- operator = parser.consume?(:comparison)
466
-
467
- # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
468
- return Liquid::Condition.new(left_operand) unless operator
469
-
470
- # Parse what remained after extracting the left operand and the `:comparison` operator
471
- # and initialize a Liquid::Condition object using the operands and the comparison-operator
472
- Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
473
- end
474
456
  end
475
457
  end
476
458
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Filters
5
+ module ConditionHelpers
6
+ # ----------- The following set of code was *adapted* from Liquid::If
7
+ # ----------- ref: https://git.io/vp6K6
8
+
9
+ # Parse a string to a Liquid Condition
10
+ def parse_condition(exp)
11
+ parser = Liquid::Parser.new(exp)
12
+ condition = parse_binary_comparison(parser)
13
+
14
+ parser.consume(:end_of_string)
15
+ condition
16
+ end
17
+
18
+ # Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
19
+ # the parsed expression based on whether the expression consists of binary operations with
20
+ # Liquid operators `and` or `or`
21
+ #
22
+ # - parser: an instance of Liquid::Parser
23
+ #
24
+ # Returns an instance of Liquid::Condition
25
+ def parse_binary_comparison(parser)
26
+ condition = parse_comparison(parser)
27
+ first_condition = condition
28
+ while (binary_operator = parser.id?("and") || parser.id?("or"))
29
+ child_condition = parse_comparison(parser)
30
+ condition.send(binary_operator, child_condition)
31
+ condition = child_condition
32
+ end
33
+ first_condition
34
+ end
35
+
36
+ # Generates a Liquid::Condition object from a Liquid::Parser object based
37
+ # on whether the parsed expression involves a "comparison" operator
38
+ # (e.g. <, ==, >, !=, etc)
39
+ #
40
+ # - parser: an instance of Liquid::Parser
41
+ #
42
+ # Returns an instance of Liquid::Condition
43
+ def parse_comparison(parser)
44
+ left_operand = Liquid::Expression.parse(parser.expression)
45
+ operator = parser.consume?(:comparison)
46
+
47
+ # No comparison-operator detected. Initialize a Liquid::Condition using only left operand
48
+ return Liquid::Condition.new(left_operand) unless operator
49
+
50
+ # Parse what remained after extracting the left operand and the `:comparison` operator
51
+ # and initialize a Liquid::Condition object using the operands and the comparison-operator
52
+ Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
53
+ end
54
+ end
55
+ end
56
+ end