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.
- checksums.yaml +4 -4
- data/Rakefile +42 -0
- data/bridgetown-core.gemspec +46 -0
- data/lib/bridgetown-core.rb +202 -0
- data/lib/bridgetown-core/cache.rb +190 -0
- data/lib/bridgetown-core/cleaner.rb +111 -0
- data/lib/bridgetown-core/collection.rb +279 -0
- data/lib/bridgetown-core/command.rb +106 -0
- data/lib/bridgetown-core/commands/build.rb +96 -0
- data/lib/bridgetown-core/commands/clean.rb +43 -0
- data/lib/bridgetown-core/commands/console.rb +56 -0
- data/lib/bridgetown-core/commands/doctor.rb +172 -0
- data/lib/bridgetown-core/commands/help.rb +34 -0
- data/lib/bridgetown-core/commands/new.rb +148 -0
- data/lib/bridgetown-core/commands/serve.rb +273 -0
- data/lib/bridgetown-core/commands/serve/servlet.rb +68 -0
- data/lib/bridgetown-core/configuration.rb +323 -0
- data/lib/bridgetown-core/converter.rb +54 -0
- data/lib/bridgetown-core/converters/identity.rb +39 -0
- data/lib/bridgetown-core/converters/markdown.rb +108 -0
- data/lib/bridgetown-core/converters/markdown/kramdown_parser.rb +132 -0
- data/lib/bridgetown-core/converters/smartypants.rb +69 -0
- data/lib/bridgetown-core/convertible.rb +237 -0
- data/lib/bridgetown-core/deprecator.rb +50 -0
- data/lib/bridgetown-core/document.rb +475 -0
- data/lib/bridgetown-core/drops/bridgetown_drop.rb +32 -0
- data/lib/bridgetown-core/drops/collection_drop.rb +20 -0
- data/lib/bridgetown-core/drops/document_drop.rb +69 -0
- data/lib/bridgetown-core/drops/drop.rb +215 -0
- data/lib/bridgetown-core/drops/excerpt_drop.rb +19 -0
- data/lib/bridgetown-core/drops/page_drop.rb +14 -0
- data/lib/bridgetown-core/drops/site_drop.rb +62 -0
- data/lib/bridgetown-core/drops/static_file_drop.rb +14 -0
- data/lib/bridgetown-core/drops/unified_payload_drop.rb +26 -0
- data/lib/bridgetown-core/drops/url_drop.rb +132 -0
- data/lib/bridgetown-core/entry_filter.rb +108 -0
- data/lib/bridgetown-core/errors.rb +20 -0
- data/lib/bridgetown-core/excerpt.rb +202 -0
- data/lib/bridgetown-core/external.rb +62 -0
- data/lib/bridgetown-core/filters.rb +467 -0
- data/lib/bridgetown-core/filters/date_filters.rb +110 -0
- data/lib/bridgetown-core/filters/grouping_filters.rb +64 -0
- data/lib/bridgetown-core/filters/url_filters.rb +79 -0
- data/lib/bridgetown-core/frontmatter_defaults.rb +238 -0
- data/lib/bridgetown-core/generator.rb +5 -0
- data/lib/bridgetown-core/hooks.rb +103 -0
- data/lib/bridgetown-core/layout.rb +57 -0
- data/lib/bridgetown-core/liquid_extensions.rb +22 -0
- data/lib/bridgetown-core/liquid_renderer.rb +71 -0
- data/lib/bridgetown-core/liquid_renderer/file.rb +67 -0
- data/lib/bridgetown-core/liquid_renderer/table.rb +75 -0
- data/lib/bridgetown-core/log_adapter.rb +151 -0
- data/lib/bridgetown-core/log_writer.rb +60 -0
- data/lib/bridgetown-core/mime.types +867 -0
- data/lib/bridgetown-core/page.rb +214 -0
- data/lib/bridgetown-core/page_without_a_file.rb +14 -0
- data/lib/bridgetown-core/path_manager.rb +31 -0
- data/lib/bridgetown-core/plugin.rb +80 -0
- data/lib/bridgetown-core/plugin_manager.rb +60 -0
- data/lib/bridgetown-core/publisher.rb +23 -0
- data/lib/bridgetown-core/reader.rb +185 -0
- data/lib/bridgetown-core/readers/collection_reader.rb +22 -0
- data/lib/bridgetown-core/readers/data_reader.rb +75 -0
- data/lib/bridgetown-core/readers/layout_reader.rb +48 -0
- data/lib/bridgetown-core/readers/page_reader.rb +24 -0
- data/lib/bridgetown-core/readers/post_reader.rb +74 -0
- data/lib/bridgetown-core/readers/static_file_reader.rb +24 -0
- data/lib/bridgetown-core/regenerator.rb +195 -0
- data/lib/bridgetown-core/related_posts.rb +52 -0
- data/lib/bridgetown-core/renderer.rb +261 -0
- data/lib/bridgetown-core/site.rb +469 -0
- data/lib/bridgetown-core/static_file.rb +205 -0
- data/lib/bridgetown-core/tags/component.rb +34 -0
- data/lib/bridgetown-core/tags/highlight.rb +111 -0
- data/lib/bridgetown-core/tags/include.rb +220 -0
- data/lib/bridgetown-core/tags/link.rb +41 -0
- data/lib/bridgetown-core/tags/post_url.rb +107 -0
- data/lib/bridgetown-core/url.rb +164 -0
- data/lib/bridgetown-core/utils.rb +367 -0
- data/lib/bridgetown-core/utils/ansi.rb +57 -0
- data/lib/bridgetown-core/utils/exec.rb +26 -0
- data/lib/bridgetown-core/utils/internet.rb +37 -0
- data/lib/bridgetown-core/utils/platforms.rb +80 -0
- data/lib/bridgetown-core/utils/thread_event.rb +31 -0
- data/lib/bridgetown-core/utils/win_tz.rb +75 -0
- data/lib/bridgetown-core/version.rb +5 -0
- data/lib/bridgetown-core/watcher.rb +139 -0
- data/lib/site_template/.gitignore +6 -0
- data/lib/site_template/bridgetown.config.yml +21 -0
- data/lib/site_template/frontend/javascript/index.js +3 -0
- data/lib/site_template/frontend/styles/index.scss +17 -0
- data/lib/site_template/package.json +23 -0
- data/lib/site_template/src/404.html +9 -0
- data/lib/site_template/src/_data/site_metadata.yml +11 -0
- data/lib/site_template/src/_includes/footer.html +3 -0
- data/lib/site_template/src/_includes/head.html +9 -0
- data/lib/site_template/src/_includes/navbar.html +4 -0
- data/lib/site_template/src/_layouts/default.html +15 -0
- data/lib/site_template/src/_layouts/home.html +7 -0
- data/lib/site_template/src/_layouts/page.html +7 -0
- data/lib/site_template/src/_layouts/post.html +7 -0
- data/lib/site_template/src/_posts/0000-00-00-welcome-to-bridgetown.md.erb +26 -0
- data/lib/site_template/src/about.md +11 -0
- data/lib/site_template/src/index.md +7 -0
- data/lib/site_template/webpack.config.js +60 -0
- data/rake/release.rake +30 -0
- 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
|