bridgetown-core 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +42 -0
  3. data/bridgetown-core.gemspec +46 -0
  4. data/lib/bridgetown-core.rb +202 -0
  5. data/lib/bridgetown-core/cache.rb +190 -0
  6. data/lib/bridgetown-core/cleaner.rb +111 -0
  7. data/lib/bridgetown-core/collection.rb +279 -0
  8. data/lib/bridgetown-core/command.rb +106 -0
  9. data/lib/bridgetown-core/commands/build.rb +96 -0
  10. data/lib/bridgetown-core/commands/clean.rb +43 -0
  11. data/lib/bridgetown-core/commands/console.rb +56 -0
  12. data/lib/bridgetown-core/commands/doctor.rb +172 -0
  13. data/lib/bridgetown-core/commands/help.rb +34 -0
  14. data/lib/bridgetown-core/commands/new.rb +148 -0
  15. data/lib/bridgetown-core/commands/serve.rb +273 -0
  16. data/lib/bridgetown-core/commands/serve/servlet.rb +68 -0
  17. data/lib/bridgetown-core/configuration.rb +323 -0
  18. data/lib/bridgetown-core/converter.rb +54 -0
  19. data/lib/bridgetown-core/converters/identity.rb +39 -0
  20. data/lib/bridgetown-core/converters/markdown.rb +108 -0
  21. data/lib/bridgetown-core/converters/markdown/kramdown_parser.rb +132 -0
  22. data/lib/bridgetown-core/converters/smartypants.rb +69 -0
  23. data/lib/bridgetown-core/convertible.rb +237 -0
  24. data/lib/bridgetown-core/deprecator.rb +50 -0
  25. data/lib/bridgetown-core/document.rb +475 -0
  26. data/lib/bridgetown-core/drops/bridgetown_drop.rb +32 -0
  27. data/lib/bridgetown-core/drops/collection_drop.rb +20 -0
  28. data/lib/bridgetown-core/drops/document_drop.rb +69 -0
  29. data/lib/bridgetown-core/drops/drop.rb +215 -0
  30. data/lib/bridgetown-core/drops/excerpt_drop.rb +19 -0
  31. data/lib/bridgetown-core/drops/page_drop.rb +14 -0
  32. data/lib/bridgetown-core/drops/site_drop.rb +62 -0
  33. data/lib/bridgetown-core/drops/static_file_drop.rb +14 -0
  34. data/lib/bridgetown-core/drops/unified_payload_drop.rb +26 -0
  35. data/lib/bridgetown-core/drops/url_drop.rb +132 -0
  36. data/lib/bridgetown-core/entry_filter.rb +108 -0
  37. data/lib/bridgetown-core/errors.rb +20 -0
  38. data/lib/bridgetown-core/excerpt.rb +202 -0
  39. data/lib/bridgetown-core/external.rb +62 -0
  40. data/lib/bridgetown-core/filters.rb +467 -0
  41. data/lib/bridgetown-core/filters/date_filters.rb +110 -0
  42. data/lib/bridgetown-core/filters/grouping_filters.rb +64 -0
  43. data/lib/bridgetown-core/filters/url_filters.rb +79 -0
  44. data/lib/bridgetown-core/frontmatter_defaults.rb +238 -0
  45. data/lib/bridgetown-core/generator.rb +5 -0
  46. data/lib/bridgetown-core/hooks.rb +103 -0
  47. data/lib/bridgetown-core/layout.rb +57 -0
  48. data/lib/bridgetown-core/liquid_extensions.rb +22 -0
  49. data/lib/bridgetown-core/liquid_renderer.rb +71 -0
  50. data/lib/bridgetown-core/liquid_renderer/file.rb +67 -0
  51. data/lib/bridgetown-core/liquid_renderer/table.rb +75 -0
  52. data/lib/bridgetown-core/log_adapter.rb +151 -0
  53. data/lib/bridgetown-core/log_writer.rb +60 -0
  54. data/lib/bridgetown-core/mime.types +867 -0
  55. data/lib/bridgetown-core/page.rb +214 -0
  56. data/lib/bridgetown-core/page_without_a_file.rb +14 -0
  57. data/lib/bridgetown-core/path_manager.rb +31 -0
  58. data/lib/bridgetown-core/plugin.rb +80 -0
  59. data/lib/bridgetown-core/plugin_manager.rb +60 -0
  60. data/lib/bridgetown-core/publisher.rb +23 -0
  61. data/lib/bridgetown-core/reader.rb +185 -0
  62. data/lib/bridgetown-core/readers/collection_reader.rb +22 -0
  63. data/lib/bridgetown-core/readers/data_reader.rb +75 -0
  64. data/lib/bridgetown-core/readers/layout_reader.rb +48 -0
  65. data/lib/bridgetown-core/readers/page_reader.rb +24 -0
  66. data/lib/bridgetown-core/readers/post_reader.rb +74 -0
  67. data/lib/bridgetown-core/readers/static_file_reader.rb +24 -0
  68. data/lib/bridgetown-core/regenerator.rb +195 -0
  69. data/lib/bridgetown-core/related_posts.rb +52 -0
  70. data/lib/bridgetown-core/renderer.rb +261 -0
  71. data/lib/bridgetown-core/site.rb +469 -0
  72. data/lib/bridgetown-core/static_file.rb +205 -0
  73. data/lib/bridgetown-core/tags/component.rb +34 -0
  74. data/lib/bridgetown-core/tags/highlight.rb +111 -0
  75. data/lib/bridgetown-core/tags/include.rb +220 -0
  76. data/lib/bridgetown-core/tags/link.rb +41 -0
  77. data/lib/bridgetown-core/tags/post_url.rb +107 -0
  78. data/lib/bridgetown-core/url.rb +164 -0
  79. data/lib/bridgetown-core/utils.rb +367 -0
  80. data/lib/bridgetown-core/utils/ansi.rb +57 -0
  81. data/lib/bridgetown-core/utils/exec.rb +26 -0
  82. data/lib/bridgetown-core/utils/internet.rb +37 -0
  83. data/lib/bridgetown-core/utils/platforms.rb +80 -0
  84. data/lib/bridgetown-core/utils/thread_event.rb +31 -0
  85. data/lib/bridgetown-core/utils/win_tz.rb +75 -0
  86. data/lib/bridgetown-core/version.rb +5 -0
  87. data/lib/bridgetown-core/watcher.rb +139 -0
  88. data/lib/site_template/.gitignore +6 -0
  89. data/lib/site_template/bridgetown.config.yml +21 -0
  90. data/lib/site_template/frontend/javascript/index.js +3 -0
  91. data/lib/site_template/frontend/styles/index.scss +17 -0
  92. data/lib/site_template/package.json +23 -0
  93. data/lib/site_template/src/404.html +9 -0
  94. data/lib/site_template/src/_data/site_metadata.yml +11 -0
  95. data/lib/site_template/src/_includes/footer.html +3 -0
  96. data/lib/site_template/src/_includes/head.html +9 -0
  97. data/lib/site_template/src/_includes/navbar.html +4 -0
  98. data/lib/site_template/src/_layouts/default.html +15 -0
  99. data/lib/site_template/src/_layouts/home.html +7 -0
  100. data/lib/site_template/src/_layouts/page.html +7 -0
  101. data/lib/site_template/src/_layouts/post.html +7 -0
  102. data/lib/site_template/src/_posts/0000-00-00-welcome-to-bridgetown.md.erb +26 -0
  103. data/lib/site_template/src/about.md +11 -0
  104. data/lib/site_template/src/index.md +7 -0
  105. data/lib/site_template/webpack.config.js +60 -0
  106. data/rake/release.rake +30 -0
  107. metadata +106 -1
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Tags
5
+ class Link < Liquid::Tag
6
+ include Bridgetown::Filters::URLFilters
7
+
8
+ class << self
9
+ def tag_name
10
+ name.split("::").last.downcase
11
+ end
12
+ end
13
+
14
+ def initialize(tag_name, relative_path, tokens)
15
+ super
16
+
17
+ @relative_path = relative_path.strip
18
+ end
19
+
20
+ def render(context)
21
+ @context = context
22
+ site = context.registers[:site]
23
+ relative_path = Liquid::Template.parse(@relative_path).render(context)
24
+
25
+ site.each_site_file do |item|
26
+ return relative_url(item) if item.relative_path == relative_path
27
+ # This takes care of the case for static files that have a leading /
28
+ return relative_url(item) if item.relative_path == "/#{relative_path}"
29
+ end
30
+
31
+ raise ArgumentError, <<~MSG
32
+ Could not find document '#{relative_path}' in tag '#{self.class.tag_name}'.
33
+
34
+ Make sure the document exists and the path is correct.
35
+ MSG
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ Liquid::Template.register_tag(Bridgetown::Tags::Link.tag_name, Bridgetown::Tags::Link)
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Tags
5
+ class PostComparer
6
+ MATCHER = %r!^(.+/)*(\d+-\d+-\d+)-(.*)$!.freeze
7
+
8
+ attr_reader :path, :date, :slug, :name
9
+
10
+ def initialize(name)
11
+ @name = name
12
+
13
+ all, @path, @date, @slug = *name.sub(%r!^/!, "").match(MATCHER)
14
+ unless all
15
+ raise Bridgetown::Errors::InvalidPostNameError,
16
+ "'#{name}' does not contain valid date and/or title."
17
+ end
18
+
19
+ escaped_slug = Regexp.escape(slug)
20
+ @name_regex = %r!^_posts/#{path}#{date}-#{escaped_slug}\.[^.]+|
21
+ ^#{path}_posts/?#{date}-#{escaped_slug}\.[^.]+!x
22
+ end
23
+
24
+ def post_date
25
+ @post_date ||= Utils.parse_date(
26
+ date,
27
+ "'#{date}' does not contain valid date and/or title."
28
+ )
29
+ end
30
+
31
+ def ==(other)
32
+ other.relative_path.match(@name_regex)
33
+ end
34
+
35
+ def deprecated_equality(other)
36
+ slug == post_slug(other) &&
37
+ post_date.year == other.date.year &&
38
+ post_date.month == other.date.month &&
39
+ post_date.day == other.date.day
40
+ end
41
+
42
+ private
43
+
44
+ # Construct the directory-aware post slug for a Bridgetown::Post
45
+ #
46
+ # other - the Bridgetown::Post
47
+ #
48
+ # Returns the post slug with the subdirectory (relative to _posts)
49
+ def post_slug(other)
50
+ path = other.basename.split("/")[0...-1].join("/")
51
+ if path.nil? || path == ""
52
+ other.data["slug"]
53
+ else
54
+ path + "/" + other.data["slug"]
55
+ end
56
+ end
57
+ end
58
+
59
+ class PostUrl < Liquid::Tag
60
+ include Bridgetown::Filters::URLFilters
61
+
62
+ def initialize(tag_name, post, tokens)
63
+ super
64
+ @orig_post = post.strip
65
+ begin
66
+ @post = PostComparer.new(@orig_post)
67
+ rescue StandardError => e
68
+ raise Bridgetown::Errors::PostURLError, <<~MSG
69
+ Could not parse name of post "#{@orig_post}" in tag 'post_url'.
70
+ Make sure the post exists and the name is correct.
71
+ #{e.class}: #{e.message}
72
+ MSG
73
+ end
74
+ end
75
+
76
+ def render(context)
77
+ @context = context
78
+ site = context.registers[:site]
79
+
80
+ site.posts.docs.each do |document|
81
+ return relative_url(document) if @post == document
82
+ end
83
+
84
+ # New matching method did not match, fall back to old method
85
+ # with deprecation warning if this matches
86
+
87
+ site.posts.docs.each do |document|
88
+ next unless @post.deprecated_equality document
89
+
90
+ Bridgetown::Deprecator.deprecation_message "A call to "\
91
+ "'{% post_url #{@post.name} %}' did not match " \
92
+ "a post using the new matching method of checking name " \
93
+ "(path-date-slug) equality. Please make sure that you " \
94
+ "change this tag to match the post's name exactly."
95
+ return relative_url(document)
96
+ end
97
+
98
+ raise Bridgetown::Errors::PostURLError, <<~MSG
99
+ Could not find post "#{@orig_post}" in tag 'post_url'.
100
+ Make sure the post exists and the name is correct.
101
+ MSG
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ Liquid::Template.register_tag("post_url", Bridgetown::Tags::PostUrl)
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public: Methods that generate a URL for a resource such as a Post or a Page.
4
+ #
5
+ # Examples
6
+ #
7
+ # URL.new({
8
+ # :template => /:categories/:title.html",
9
+ # :placeholders => {:categories => "ruby", :title => "something"}
10
+ # }).to_s
11
+ #
12
+ module Bridgetown
13
+ class URL
14
+ # options - One of :permalink or :template must be supplied.
15
+ # :template - The String used as template for URL generation,
16
+ # for example "/:path/:basename:output_ext", where
17
+ # a placeholder is prefixed with a colon.
18
+ # :placeholders - A hash containing the placeholders which will be
19
+ # replaced when used inside the template. E.g.
20
+ # { "year" => Time.now.strftime("%Y") } would replace
21
+ # the placeholder ":year" with the current year.
22
+ # :permalink - If supplied, no URL will be generated from the
23
+ # template. Instead, the given permalink will be
24
+ # used as URL.
25
+ def initialize(options)
26
+ @template = options[:template]
27
+ @placeholders = options[:placeholders] || {}
28
+ @permalink = options[:permalink]
29
+
30
+ if (@template || @permalink).nil?
31
+ raise ArgumentError, "One of :template or :permalink must be supplied."
32
+ end
33
+ end
34
+
35
+ # The generated relative URL of the resource
36
+ #
37
+ # Returns the String URL
38
+ def to_s
39
+ sanitize_url(generated_permalink || generated_url)
40
+ end
41
+
42
+ # Generates a URL from the permalink
43
+ #
44
+ # Returns the _unsanitized String URL
45
+ def generated_permalink
46
+ (@generated_permalink ||= generate_url(@permalink)) if @permalink
47
+ end
48
+
49
+ # Generates a URL from the template
50
+ #
51
+ # Returns the unsanitized String URL
52
+ def generated_url
53
+ @generated_url ||= generate_url(@template)
54
+ end
55
+
56
+ # Internal: Generate the URL by replacing all placeholders with their
57
+ # respective values in the given template
58
+ #
59
+ # Returns the unsanitized String URL
60
+ def generate_url(template)
61
+ if @placeholders.is_a? Drops::UrlDrop
62
+ generate_url_from_drop(template)
63
+ else
64
+ generate_url_from_hash(template)
65
+ end
66
+ end
67
+
68
+ def generate_url_from_hash(template)
69
+ @placeholders.inject(template) do |result, token|
70
+ break result if result.index(":").nil?
71
+
72
+ if token.last.nil?
73
+ # Remove leading "/" to avoid generating urls with `//`
74
+ result.gsub("/:#{token.first}", "")
75
+ else
76
+ result.gsub(":#{token.first}", self.class.escape_path(token.last))
77
+ end
78
+ end
79
+ end
80
+
81
+ # We include underscores in keys to allow for 'i_month' and so forth.
82
+ # This poses a problem for keys which are followed by an underscore
83
+ # but the underscore is not part of the key, e.g. '/:month_:day'.
84
+ # That should be :month and :day, but our key extraction regexp isn't
85
+ # smart enough to know that so we have to make it an explicit
86
+ # possibility.
87
+ def possible_keys(key)
88
+ if key.end_with?("_")
89
+ [key, key.chomp("_")]
90
+ else
91
+ [key]
92
+ end
93
+ end
94
+
95
+ def generate_url_from_drop(template)
96
+ template.gsub(%r!:([a-z_]+)!) do |match|
97
+ pool = possible_keys(match.sub(":", ""))
98
+
99
+ winner = pool.find { |key| @placeholders.key?(key) }
100
+ if winner.nil?
101
+ raise NoMethodError,
102
+ "The URL template doesn't have #{pool.join(" or ")} keys. "\
103
+ "Check your permalink template!"
104
+ end
105
+
106
+ value = @placeholders[winner]
107
+ value = "" if value.nil?
108
+ replacement = self.class.escape_path(value)
109
+
110
+ match.sub(":#{winner}", replacement)
111
+ end.squeeze("/")
112
+ end
113
+
114
+ # Returns a sanitized String URL, stripping "../../" and multiples of "/",
115
+ # as well as the beginning "/" so we can enforce and ensure it.
116
+
117
+ def sanitize_url(str)
118
+ "/#{str}".gsub("..", "/").gsub("./", "").squeeze("/")
119
+ end
120
+
121
+ # Escapes a path to be a valid URL path segment
122
+ #
123
+ # path - The path to be escaped.
124
+ #
125
+ # Examples:
126
+ #
127
+ # URL.escape_path("/a b")
128
+ # # => "/a%20b"
129
+ #
130
+ # Returns the escaped path.
131
+ def self.escape_path(path)
132
+ return path if path.empty? || %r!^[a-zA-Z0-9./-]+$!.match?(path)
133
+
134
+ # Because URI.escape doesn't escape "?", "[" and "]" by default,
135
+ # specify unsafe string (except unreserved, sub-delims, ":", "@" and "/").
136
+ #
137
+ # URI path segment is defined in RFC 3986 as follows:
138
+ # segment = *pchar
139
+ # pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
140
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
141
+ # pct-encoded = "%" HEXDIG HEXDIG
142
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
143
+ # / "*" / "+" / "," / ";" / "="
144
+ Addressable::URI.encode(path).encode("utf-8").sub("#", "%23")
145
+ end
146
+
147
+ # Unescapes a URL path segment
148
+ #
149
+ # path - The path to be unescaped.
150
+ #
151
+ # Examples:
152
+ #
153
+ # URL.unescape_path("/a%20b")
154
+ # # => "/a b"
155
+ #
156
+ # Returns the unescaped path.
157
+ def self.unescape_path(path)
158
+ path = path.encode("utf-8")
159
+ return path unless path.include?("%")
160
+
161
+ Addressable::URI.unencode(path)
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Utils
5
+ extend self
6
+ autoload :Ansi, "bridgetown-core/utils/ansi"
7
+ autoload :Exec, "bridgetown-core/utils/exec"
8
+ autoload :Internet, "bridgetown-core/utils/internet"
9
+ autoload :Platforms, "bridgetown-core/utils/platforms"
10
+ autoload :ThreadEvent, "bridgetown-core/utils/thread_event"
11
+ autoload :WinTZ, "bridgetown-core/utils/win_tz"
12
+
13
+ # Constants for use in #slugify
14
+ SLUGIFY_MODES = %w(raw default pretty simple ascii latin).freeze
15
+ SLUGIFY_RAW_REGEXP = Regexp.new('\\s+').freeze
16
+ SLUGIFY_DEFAULT_REGEXP = Regexp.new("[^[:alnum:]]+").freeze
17
+ SLUGIFY_PRETTY_REGEXP = Regexp.new("[^[:alnum:]._~!$&'()+,;=@]+").freeze
18
+ SLUGIFY_ASCII_REGEXP = Regexp.new("[^[A-Za-z0-9]]+").freeze
19
+
20
+ # Takes a slug and turns it into a simple title.
21
+ def titleize_slug(slug)
22
+ slug.split("-").map!(&:capitalize).join(" ")
23
+ end
24
+
25
+ # Non-destructive version of deep_merge_hashes! See that method.
26
+ #
27
+ # Returns the merged hashes.
28
+ def deep_merge_hashes(master_hash, other_hash)
29
+ deep_merge_hashes!(master_hash.dup, other_hash)
30
+ end
31
+
32
+ # Merges a master hash with another hash, recursively.
33
+ #
34
+ # master_hash - the "parent" hash whose values will be overridden
35
+ # other_hash - the other hash whose values will be persisted after the merge
36
+ #
37
+ # This code was lovingly stolen from some random gem:
38
+ # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
39
+ #
40
+ # Thanks to whoever made it.
41
+ def deep_merge_hashes!(target, overwrite)
42
+ merge_values(target, overwrite)
43
+ merge_default_proc(target, overwrite)
44
+ duplicate_frozen_values(target)
45
+
46
+ target
47
+ end
48
+
49
+ def mergable?(value)
50
+ value.is_a?(Hash) || value.is_a?(Drops::Drop)
51
+ end
52
+
53
+ def duplicable?(obj)
54
+ case obj
55
+ when nil, false, true, Symbol, Numeric
56
+ false
57
+ else
58
+ true
59
+ end
60
+ end
61
+
62
+ # Read array from the supplied hash favouring the singular key
63
+ # and then the plural key, and handling any nil entries.
64
+ #
65
+ # hash - the hash to read from
66
+ # singular_key - the singular key
67
+ # plural_key - the plural key
68
+ #
69
+ # Returns an array
70
+ def pluralized_array_from_hash(hash, singular_key, plural_key)
71
+ array = []
72
+ value = value_from_singular_key(hash, singular_key)
73
+ value ||= value_from_plural_key(hash, plural_key)
74
+
75
+ array << value
76
+ array.flatten!
77
+ array.compact!
78
+ array
79
+ end
80
+
81
+ def value_from_singular_key(hash, key)
82
+ hash[key] if hash.key?(key) || (hash.default_proc && hash[key])
83
+ end
84
+
85
+ def value_from_plural_key(hash, key)
86
+ if hash.key?(key) || (hash.default_proc && hash[key])
87
+ val = hash[key]
88
+ case val
89
+ when String
90
+ val.split
91
+ when Array
92
+ val.compact
93
+ end
94
+ end
95
+ end
96
+
97
+ def transform_keys(hash)
98
+ result = {}
99
+ hash.each_key do |key|
100
+ result[yield(key)] = hash[key]
101
+ end
102
+ result
103
+ end
104
+
105
+ # Apply #to_sym to all keys in the hash
106
+ #
107
+ # hash - the hash to which to apply this transformation
108
+ #
109
+ # Returns a new hash with symbolized keys
110
+ def symbolize_hash_keys(hash)
111
+ transform_keys(hash) { |key| key.to_sym rescue key }
112
+ end
113
+
114
+ # Apply #to_s to all keys in the Hash
115
+ #
116
+ # hash - the hash to which to apply this transformation
117
+ #
118
+ # Returns a new hash with stringified keys
119
+ def stringify_hash_keys(hash)
120
+ transform_keys(hash) { |key| key.to_s rescue key }
121
+ end
122
+
123
+ # Parse a date/time and throw an error if invalid
124
+ #
125
+ # input - the date/time to parse
126
+ # msg - (optional) the error message to show the user
127
+ #
128
+ # Returns the parsed date if successful, throws a FatalException
129
+ # if not
130
+ def parse_date(input, msg = "Input could not be parsed.")
131
+ Time.parse(input).localtime
132
+ rescue ArgumentError
133
+ raise Errors::InvalidDateError, "Invalid date '#{input}': #{msg}"
134
+ end
135
+
136
+ # Determines whether a given file has
137
+ #
138
+ # Returns true if the YAML front matter is present.
139
+ # rubocop: disable Naming/PredicateName
140
+ def has_yaml_header?(file)
141
+ File.open(file, "rb", &:readline).match? %r!\A---\s*\r?\n!
142
+ rescue EOFError
143
+ false
144
+ end
145
+
146
+ # Determine whether the given content string contains Liquid Tags or Vaiables
147
+ #
148
+ # Returns true is the string contains sequences of `{%` or `{{`
149
+ def has_liquid_construct?(content)
150
+ return false if content.nil? || content.empty?
151
+
152
+ content.include?("{%") || content.include?("{{")
153
+ end
154
+ # rubocop: enable Naming/PredicateName
155
+
156
+ # Slugify a filename or title.
157
+ #
158
+ # string - the filename or title to slugify
159
+ # mode - how string is slugified
160
+ # cased - whether to replace all uppercase letters with their
161
+ # lowercase counterparts
162
+ #
163
+ # When mode is "none", return the given string.
164
+ #
165
+ # When mode is "raw", return the given string,
166
+ # with every sequence of spaces characters replaced with a hyphen.
167
+ #
168
+ # When mode is "default", "simple", or nil, non-alphabetic characters are
169
+ # replaced with a hyphen too.
170
+ #
171
+ # When mode is "pretty", some non-alphabetic characters (._~!$&'()+,;=@)
172
+ # are not replaced with hyphen.
173
+ #
174
+ # When mode is "ascii", some everything else except ASCII characters
175
+ # a-z (lowercase), A-Z (uppercase) and 0-9 (numbers) are not replaced with hyphen.
176
+ #
177
+ # When mode is "latin", the input string is first preprocessed so that
178
+ # any letters with accents are replaced with the plain letter. Afterwards,
179
+ # it follows the "default" mode of operation.
180
+ #
181
+ # If cased is true, all uppercase letters in the result string are
182
+ # replaced with their lowercase counterparts.
183
+ #
184
+ # Examples:
185
+ # slugify("The _config.yml file")
186
+ # # => "the-config-yml-file"
187
+ #
188
+ # slugify("The _config.yml file", "pretty")
189
+ # # => "the-_config.yml-file"
190
+ #
191
+ # slugify("The _config.yml file", "pretty", true)
192
+ # # => "The-_config.yml file"
193
+ #
194
+ # slugify("The _config.yml file", "ascii")
195
+ # # => "the-config-yml-file"
196
+ #
197
+ # slugify("The _config.yml file", "latin")
198
+ # # => "the-config-yml-file"
199
+ #
200
+ # Returns the slugified string.
201
+ def slugify(string, mode: nil, cased: false)
202
+ mode ||= "default"
203
+ return nil if string.nil?
204
+
205
+ unless SLUGIFY_MODES.include?(mode)
206
+ return cased ? string : string.downcase
207
+ end
208
+
209
+ # Drop accent marks from latin characters. Everything else turns to ?
210
+ if mode == "latin"
211
+ I18n.config.available_locales = :en if I18n.config.available_locales.empty?
212
+ string = I18n.transliterate(string)
213
+ end
214
+
215
+ slug = replace_character_sequence_with_hyphen(string, :mode => mode)
216
+
217
+ # Remove leading/trailing hyphen
218
+ slug.gsub!(%r!^\-|\-$!i, "")
219
+
220
+ slug.downcase! unless cased
221
+ Bridgetown.logger.warn("Warning:", "Empty `slug` generated for '#{string}'.") if slug.empty?
222
+ slug
223
+ end
224
+
225
+ # Add an appropriate suffix to template so that it matches the specified
226
+ # permalink style.
227
+ #
228
+ # template - permalink template without trailing slash or file extension
229
+ # permalink_style - permalink style, either built-in or custom
230
+ #
231
+ # The returned permalink template will use the same ending style as
232
+ # specified in permalink_style. For example, if permalink_style contains a
233
+ # trailing slash (or is :pretty, which indirectly has a trailing slash),
234
+ # then so will the returned template. If permalink_style has a trailing
235
+ # ":output_ext" (or is :none, :date, or :ordinal) then so will the returned
236
+ # template. Otherwise, template will be returned without modification.
237
+ #
238
+ # Examples:
239
+ # add_permalink_suffix("/:basename", :pretty)
240
+ # # => "/:basename/"
241
+ #
242
+ # add_permalink_suffix("/:basename", :date)
243
+ # # => "/:basename:output_ext"
244
+ #
245
+ # add_permalink_suffix("/:basename", "/:year/:month/:title/")
246
+ # # => "/:basename/"
247
+ #
248
+ # add_permalink_suffix("/:basename", "/:year/:month/:title")
249
+ # # => "/:basename"
250
+ #
251
+ # Returns the updated permalink template
252
+ def add_permalink_suffix(template, permalink_style)
253
+ template = template.dup
254
+
255
+ case permalink_style
256
+ when :pretty, :simple
257
+ template << "/"
258
+ when :date, :ordinal, :none
259
+ template << ":output_ext"
260
+ else
261
+ template << "/" if permalink_style.to_s.end_with?("/")
262
+ template << ":output_ext" if permalink_style.to_s.end_with?(":output_ext")
263
+ end
264
+
265
+ template
266
+ end
267
+
268
+ # Work the same way as Dir.glob but seperating the input into two parts
269
+ # ('dir' + '/' + 'pattern') to make sure the first part('dir') does not act
270
+ # as a pattern.
271
+ #
272
+ # For example, Dir.glob("path[/*") always returns an empty array,
273
+ # because the method fails to find the closing pattern to '[' which is ']'
274
+ #
275
+ # Examples:
276
+ # safe_glob("path[", "*")
277
+ # # => ["path[/file1", "path[/file2"]
278
+ #
279
+ # safe_glob("path", "*", File::FNM_DOTMATCH)
280
+ # # => ["path/.", "path/..", "path/file1"]
281
+ #
282
+ # safe_glob("path", ["**", "*"])
283
+ # # => ["path[/file1", "path[/folder/file2"]
284
+ #
285
+ # dir - the dir where glob will be executed under
286
+ # (the dir will be included to each result)
287
+ # patterns - the patterns (or the pattern) which will be applied under the dir
288
+ # flags - the flags which will be applied to the pattern
289
+ #
290
+ # Returns matched pathes
291
+ def safe_glob(dir, patterns, flags = 0)
292
+ return [] unless Dir.exist?(dir)
293
+
294
+ pattern = File.join(Array(patterns))
295
+ return [dir] if pattern.empty?
296
+
297
+ Dir.chdir(dir) do
298
+ Dir.glob(pattern, flags).map { |f| File.join(dir, f) }
299
+ end
300
+ end
301
+
302
+ # Returns merged option hash for File.read of self.site (if exists)
303
+ # and a given param
304
+ def merged_file_read_opts(site, opts)
305
+ merged = (site ? site.file_read_opts : {}).merge(opts)
306
+ if merged[:encoding] && !merged[:encoding].start_with?("bom|")
307
+ merged[:encoding] = "bom|#{merged[:encoding]}"
308
+ end
309
+ if merged["encoding"] && !merged["encoding"].start_with?("bom|")
310
+ merged["encoding"] = "bom|#{merged["encoding"]}"
311
+ end
312
+ merged
313
+ end
314
+
315
+ private
316
+
317
+ def merge_values(target, overwrite)
318
+ target.merge!(overwrite) do |_key, old_val, new_val|
319
+ if new_val.nil?
320
+ old_val
321
+ elsif mergable?(old_val) && mergable?(new_val)
322
+ deep_merge_hashes(old_val, new_val)
323
+ else
324
+ new_val
325
+ end
326
+ end
327
+ end
328
+
329
+ def merge_default_proc(target, overwrite)
330
+ if target.is_a?(Hash) && overwrite.is_a?(Hash) && target.default_proc.nil?
331
+ target.default_proc = overwrite.default_proc
332
+ end
333
+ end
334
+
335
+ def duplicate_frozen_values(target)
336
+ target.each do |key, val|
337
+ target[key] = val.dup if val.frozen? && duplicable?(val)
338
+ end
339
+ end
340
+
341
+ # Replace each character sequence with a hyphen.
342
+ #
343
+ # See Utils#slugify for a description of the character sequence specified
344
+ # by each mode.
345
+ def replace_character_sequence_with_hyphen(string, mode: "default")
346
+ replaceable_char =
347
+ case mode
348
+ when "raw"
349
+ SLUGIFY_RAW_REGEXP
350
+ when "pretty"
351
+ # "._~!$&'()+,;=@" is human readable (not URI-escaped) in URL
352
+ # and is allowed in both extN and NTFS.
353
+ SLUGIFY_PRETTY_REGEXP
354
+ when "ascii"
355
+ # For web servers not being able to handle Unicode, the safe
356
+ # method is to ditch anything else but latin letters and numeric
357
+ # digits.
358
+ SLUGIFY_ASCII_REGEXP
359
+ else
360
+ SLUGIFY_DEFAULT_REGEXP
361
+ end
362
+
363
+ # Strip according to the mode
364
+ string.gsub(replaceable_char, "-")
365
+ end
366
+ end
367
+ end