bridgetown-core 0.16.0.beta1 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
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