bunto 1.0.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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.markdown +59 -0
  4. data/bin/bunto +51 -0
  5. data/lib/bunto.rb +179 -0
  6. data/lib/bunto/cleaner.rb +105 -0
  7. data/lib/bunto/collection.rb +205 -0
  8. data/lib/bunto/command.rb +65 -0
  9. data/lib/bunto/commands/build.rb +77 -0
  10. data/lib/bunto/commands/clean.rb +42 -0
  11. data/lib/bunto/commands/doctor.rb +114 -0
  12. data/lib/bunto/commands/help.rb +31 -0
  13. data/lib/bunto/commands/new.rb +82 -0
  14. data/lib/bunto/commands/serve.rb +204 -0
  15. data/lib/bunto/commands/serve/servlet.rb +61 -0
  16. data/lib/bunto/configuration.rb +323 -0
  17. data/lib/bunto/converter.rb +48 -0
  18. data/lib/bunto/converters/identity.rb +21 -0
  19. data/lib/bunto/converters/markdown.rb +92 -0
  20. data/lib/bunto/converters/markdown/kramdown_parser.rb +117 -0
  21. data/lib/bunto/converters/markdown/rdiscount_parser.rb +33 -0
  22. data/lib/bunto/converters/markdown/redcarpet_parser.rb +102 -0
  23. data/lib/bunto/converters/smartypants.rb +34 -0
  24. data/lib/bunto/convertible.rb +297 -0
  25. data/lib/bunto/deprecator.rb +46 -0
  26. data/lib/bunto/document.rb +444 -0
  27. data/lib/bunto/drops/bunto_drop.rb +21 -0
  28. data/lib/bunto/drops/collection_drop.rb +22 -0
  29. data/lib/bunto/drops/document_drop.rb +27 -0
  30. data/lib/bunto/drops/drop.rb +176 -0
  31. data/lib/bunto/drops/site_drop.rb +38 -0
  32. data/lib/bunto/drops/unified_payload_drop.rb +25 -0
  33. data/lib/bunto/drops/url_drop.rb +83 -0
  34. data/lib/bunto/entry_filter.rb +72 -0
  35. data/lib/bunto/errors.rb +10 -0
  36. data/lib/bunto/excerpt.rb +127 -0
  37. data/lib/bunto/external.rb +59 -0
  38. data/lib/bunto/filters.rb +367 -0
  39. data/lib/bunto/frontmatter_defaults.rb +188 -0
  40. data/lib/bunto/generator.rb +3 -0
  41. data/lib/bunto/hooks.rb +101 -0
  42. data/lib/bunto/layout.rb +49 -0
  43. data/lib/bunto/liquid_extensions.rb +22 -0
  44. data/lib/bunto/liquid_renderer.rb +39 -0
  45. data/lib/bunto/liquid_renderer/file.rb +50 -0
  46. data/lib/bunto/liquid_renderer/table.rb +94 -0
  47. data/lib/bunto/log_adapter.rb +115 -0
  48. data/lib/bunto/mime.types +800 -0
  49. data/lib/bunto/page.rb +180 -0
  50. data/lib/bunto/plugin.rb +96 -0
  51. data/lib/bunto/plugin_manager.rb +95 -0
  52. data/lib/bunto/post.rb +329 -0
  53. data/lib/bunto/publisher.rb +21 -0
  54. data/lib/bunto/reader.rb +126 -0
  55. data/lib/bunto/readers/collection_reader.rb +20 -0
  56. data/lib/bunto/readers/data_reader.rb +69 -0
  57. data/lib/bunto/readers/layout_reader.rb +53 -0
  58. data/lib/bunto/readers/page_reader.rb +21 -0
  59. data/lib/bunto/readers/post_reader.rb +62 -0
  60. data/lib/bunto/readers/static_file_reader.rb +21 -0
  61. data/lib/bunto/regenerator.rb +175 -0
  62. data/lib/bunto/related_posts.rb +56 -0
  63. data/lib/bunto/renderer.rb +191 -0
  64. data/lib/bunto/site.rb +391 -0
  65. data/lib/bunto/static_file.rb +141 -0
  66. data/lib/bunto/stevenson.rb +58 -0
  67. data/lib/bunto/tags/highlight.rb +122 -0
  68. data/lib/bunto/tags/include.rb +190 -0
  69. data/lib/bunto/tags/post_url.rb +88 -0
  70. data/lib/bunto/url.rb +136 -0
  71. data/lib/bunto/utils.rb +287 -0
  72. data/lib/bunto/utils/ansi.rb +59 -0
  73. data/lib/bunto/utils/platforms.rb +30 -0
  74. data/lib/bunto/version.rb +3 -0
  75. data/lib/site_template/.gitignore +3 -0
  76. data/lib/site_template/_config.yml +21 -0
  77. data/lib/site_template/_includes/footer.html +38 -0
  78. data/lib/site_template/_includes/head.html +12 -0
  79. data/lib/site_template/_includes/header.html +27 -0
  80. data/lib/site_template/_includes/icon-github.html +1 -0
  81. data/lib/site_template/_includes/icon-github.svg +1 -0
  82. data/lib/site_template/_includes/icon-twitter.html +1 -0
  83. data/lib/site_template/_includes/icon-twitter.svg +1 -0
  84. data/lib/site_template/_layouts/default.html +20 -0
  85. data/lib/site_template/_layouts/page.html +14 -0
  86. data/lib/site_template/_layouts/post.html +15 -0
  87. data/lib/site_template/_posts/0000-00-00-welcome-to-bunto.markdown.erb +25 -0
  88. data/lib/site_template/_sass/_base.scss +206 -0
  89. data/lib/site_template/_sass/_layout.scss +242 -0
  90. data/lib/site_template/_sass/_syntax-highlighting.scss +71 -0
  91. data/lib/site_template/about.md +15 -0
  92. data/lib/site_template/css/main.scss +53 -0
  93. data/lib/site_template/feed.xml +30 -0
  94. data/lib/site_template/index.html +23 -0
  95. metadata +252 -0
@@ -0,0 +1,88 @@
1
+ module Bunto
2
+ module Tags
3
+ class PostComparer
4
+ MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)$/
5
+
6
+ attr_reader :path, :date, :slug, :name
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ all, @path, @date, @slug = *name.sub(/^\//, "").match(MATCHER)
11
+ raise ArgumentError.new("'#{name}' does not contain valid date and/or title.") unless all
12
+
13
+ @name_regex = /^#{path}#{date}-#{slug}\.[^.]+/
14
+ end
15
+
16
+ def ==(other)
17
+ other.basename.match(@name_regex)
18
+ end
19
+
20
+ def deprecated_equality(other)
21
+ date = Utils.parse_date(name, "'#{name}' does not contain valid date and/or title.")
22
+ slug == post_slug(other) &&
23
+ date.year == other.date.year &&
24
+ date.month == other.date.month &&
25
+ date.day == other.date.day
26
+ end
27
+
28
+ private
29
+ # Construct the directory-aware post slug for a Bunto::Post
30
+ #
31
+ # other - the Bunto::Post
32
+ #
33
+ # Returns the post slug with the subdirectory (relative to _posts)
34
+ def post_slug(other)
35
+ path = other.basename.split("/")[0...-1].join("/")
36
+ if path.nil? || path == ""
37
+ other.data['slug']
38
+ else
39
+ path + '/' + other.data['slug']
40
+ end
41
+ end
42
+ end
43
+
44
+ class PostUrl < Liquid::Tag
45
+ def initialize(tag_name, post, tokens)
46
+ super
47
+ @orig_post = post.strip
48
+ begin
49
+ @post = PostComparer.new(@orig_post)
50
+ rescue
51
+ raise ArgumentError.new <<-eos
52
+ Could not parse name of post "#{@orig_post}" in tag 'post_url'.
53
+
54
+ Make sure the post exists and the name is correct.
55
+ eos
56
+ end
57
+ end
58
+
59
+ def render(context)
60
+ site = context.registers[:site]
61
+
62
+ site.posts.docs.each do |p|
63
+ return p.url if @post == p
64
+ end
65
+
66
+ # New matching method did not match, fall back to old method
67
+ # with deprecation warning if this matches
68
+
69
+ site.posts.docs.each do |p|
70
+ next unless @post.deprecated_equality p
71
+ Bunto::Deprecator.deprecation_message "A call to '{{ post_url #{@post.name} }}' did not match " \
72
+ "a post using the new matching method of checking name " \
73
+ "(path-date-slug) equality. Please make sure that you " \
74
+ "change this tag to match the post's name exactly."
75
+ return p.url
76
+ end
77
+
78
+ raise ArgumentError.new <<-eos
79
+ Could not find post "#{@orig_post}" in tag 'post_url'.
80
+
81
+ Make sure the post exists and the name is correct.
82
+ eos
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ Liquid::Template.register_tag('post_url', Bunto::Tags::PostUrl)
data/lib/bunto/url.rb ADDED
@@ -0,0 +1,136 @@
1
+ require 'uri'
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 Bunto
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
+ if token.last.nil?
72
+ # Remove leading '/' to avoid generating urls with `//`
73
+ result.gsub(/\/:#{token.first}/, '')
74
+ else
75
+ result.gsub(/:#{token.first}/, self.class.escape_path(token.last))
76
+ end
77
+ end
78
+ end
79
+
80
+ def generate_url_from_drop(template)
81
+ template.gsub(/:([a-z_]+)/.freeze) do |match|
82
+ replacement = @placeholders.public_send(match.sub(':'.freeze, ''.freeze))
83
+ if replacement.nil?
84
+ ''.freeze
85
+ else
86
+ self.class.escape_path(replacement)
87
+ end
88
+ end.gsub(/\/\//.freeze, '/'.freeze)
89
+ end
90
+
91
+ # Returns a sanitized String URL, stripping "../../" and multiples of "/",
92
+ # as well as the beginning "/" so we can enforce and ensure it.
93
+
94
+ def sanitize_url(str)
95
+ "/" + str.gsub(/\/{2,}/, "/").gsub(/\.+\/|\A\/+/, "")
96
+ end
97
+
98
+ # Escapes a path to be a valid URL path segment
99
+ #
100
+ # path - The path to be escaped.
101
+ #
102
+ # Examples:
103
+ #
104
+ # URL.escape_path("/a b")
105
+ # # => "/a%20b"
106
+ #
107
+ # Returns the escaped path.
108
+ def self.escape_path(path)
109
+ # Because URI.escape doesn't escape '?', '[' and ']' by default,
110
+ # specify unsafe string (except unreserved, sub-delims, ":", "@" and "/").
111
+ #
112
+ # URI path segment is defined in RFC 3986 as follows:
113
+ # segment = *pchar
114
+ # pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
115
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
116
+ # pct-encoded = "%" HEXDIG HEXDIG
117
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
118
+ # / "*" / "+" / "," / ";" / "="
119
+ URI.escape(path, /[^a-zA-Z\d\-._~!$&'()*+,;=:@\/]/).encode('utf-8')
120
+ end
121
+
122
+ # Unescapes a URL path segment
123
+ #
124
+ # path - The path to be unescaped.
125
+ #
126
+ # Examples:
127
+ #
128
+ # URL.unescape_path("/a%20b")
129
+ # # => "/a b"
130
+ #
131
+ # Returns the unescaped path.
132
+ def self.unescape_path(path)
133
+ URI.unescape(path.encode('utf-8'))
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,287 @@
1
+ module Bunto
2
+ module Utils
3
+ extend self
4
+ autoload :Platforms, 'bunto/utils/platforms'
5
+ autoload :Ansi, "bunto/utils/ansi"
6
+
7
+ # Constants for use in #slugify
8
+ SLUGIFY_MODES = %w(raw default pretty)
9
+ SLUGIFY_RAW_REGEXP = Regexp.new('\\s+').freeze
10
+ SLUGIFY_DEFAULT_REGEXP = Regexp.new('[^[:alnum:]]+').freeze
11
+ SLUGIFY_PRETTY_REGEXP = Regexp.new("[^[:alnum:]._~!$&'()+,;=@]+").freeze
12
+
13
+ # Takes an indented string and removes the preceding spaces on each line
14
+
15
+ def strip_heredoc(str)
16
+ str.gsub(/^[ \t]{#{(str.scan(/^[ \t]*(?=\S)/).min || "").size}}/, "")
17
+ end
18
+
19
+ # Takes a slug and turns it into a simple title.
20
+
21
+ def titleize_slug(slug)
22
+ slug.split("-").map! do |val|
23
+ val.capitalize!
24
+ end.join(" ")
25
+ end
26
+
27
+ # Non-destructive version of deep_merge_hashes! See that method.
28
+ #
29
+ # Returns the merged hashes.
30
+ def deep_merge_hashes(master_hash, other_hash)
31
+ deep_merge_hashes!(master_hash.dup, other_hash)
32
+ end
33
+
34
+ # Merges a master hash with another hash, recursively.
35
+ #
36
+ # master_hash - the "parent" hash whose values will be overridden
37
+ # other_hash - the other hash whose values will be persisted after the merge
38
+ #
39
+ # This code was lovingly stolen from some random gem:
40
+ # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
41
+ #
42
+ # Thanks to whoever made it.
43
+ def deep_merge_hashes!(target, overwrite)
44
+ target.merge!(overwrite) do |key, old_val, new_val|
45
+ if new_val.nil?
46
+ old_val
47
+ else
48
+ mergable?(old_val) && mergable?(new_val) ? deep_merge_hashes(old_val, new_val) : new_val
49
+ end
50
+ end
51
+
52
+ if target.respond_to?(:default_proc) && overwrite.respond_to?(:default_proc) && target.default_proc.nil?
53
+ target.default_proc = overwrite.default_proc
54
+ end
55
+
56
+ target
57
+ end
58
+
59
+ def mergable?(value)
60
+ value.is_a?(Hash) || value.is_a?(Drops::Drop)
61
+ end
62
+
63
+ # Read array from the supplied hash favouring the singular key
64
+ # and then the plural key, and handling any nil entries.
65
+ #
66
+ # hash - the hash to read from
67
+ # singular_key - the singular key
68
+ # plural_key - the plural key
69
+ #
70
+ # Returns an array
71
+ def pluralized_array_from_hash(hash, singular_key, plural_key)
72
+ [].tap do |array|
73
+ array << (value_from_singular_key(hash, singular_key) || value_from_plural_key(hash, plural_key))
74
+ end.flatten.compact
75
+ end
76
+
77
+ def value_from_singular_key(hash, key)
78
+ hash[key] if hash.key?(key) || (hash.default_proc && hash[key])
79
+ end
80
+
81
+ def value_from_plural_key(hash, key)
82
+ if hash.key?(key) || (hash.default_proc && hash[key])
83
+ val = hash[key]
84
+ case val
85
+ when String
86
+ val.split
87
+ when Array
88
+ val.compact
89
+ end
90
+ end
91
+ end
92
+
93
+ def transform_keys(hash)
94
+ result = {}
95
+ hash.each_key do |key|
96
+ result[yield(key)] = hash[key]
97
+ end
98
+ result
99
+ end
100
+
101
+ # Apply #to_sym to all keys in the hash
102
+ #
103
+ # hash - the hash to which to apply this transformation
104
+ #
105
+ # Returns a new hash with symbolized keys
106
+ def symbolize_hash_keys(hash)
107
+ transform_keys(hash) { |key| key.to_sym rescue key }
108
+ end
109
+
110
+ # Apply #to_s to all keys in the Hash
111
+ #
112
+ # hash - the hash to which to apply this transformation
113
+ #
114
+ # Returns a new hash with stringified keys
115
+ def stringify_hash_keys(hash)
116
+ transform_keys(hash) { |key| key.to_s rescue key }
117
+ end
118
+
119
+ # Parse a date/time and throw an error if invalid
120
+ #
121
+ # input - the date/time to parse
122
+ # msg - (optional) the error message to show the user
123
+ #
124
+ # Returns the parsed date if successful, throws a FatalException
125
+ # if not
126
+ def parse_date(input, msg = "Input could not be parsed.")
127
+ Time.parse(input).localtime
128
+ rescue ArgumentError
129
+ raise Errors::FatalException.new("Invalid date '#{input}': " + msg)
130
+ end
131
+
132
+ # Determines whether a given file has
133
+ #
134
+ # Returns true if the YAML front matter is present.
135
+ def has_yaml_header?(file)
136
+ !!(File.open(file, 'rb') { |f| f.readline } =~ /\A---\s*\r?\n/)
137
+ rescue EOFError
138
+ false
139
+ end
140
+
141
+ # Slugify a filename or title.
142
+ #
143
+ # string - the filename or title to slugify
144
+ # mode - how string is slugified
145
+ # cased - whether to replace all uppercase letters with their
146
+ # lowercase counterparts
147
+ #
148
+ # When mode is "none", return the given string.
149
+ #
150
+ # When mode is "raw", return the given string,
151
+ # with every sequence of spaces characters replaced with a hyphen.
152
+ #
153
+ # When mode is "default" or nil, non-alphabetic characters are
154
+ # replaced with a hyphen too.
155
+ #
156
+ # When mode is "pretty", some non-alphabetic characters (._~!$&'()+,;=@)
157
+ # are not replaced with hyphen.
158
+ #
159
+ # If cased is true, all uppercase letters in the result string are
160
+ # replaced with their lowercase counterparts.
161
+ #
162
+ # Examples:
163
+ # slugify("The _config.yml file")
164
+ # # => "the-config-yml-file"
165
+ #
166
+ # slugify("The _config.yml file", "pretty")
167
+ # # => "the-_config.yml-file"
168
+ #
169
+ # slugify("The _config.yml file", "pretty", true)
170
+ # # => "The-_config.yml file"
171
+ #
172
+ # Returns the slugified string.
173
+ def slugify(string, mode: nil, cased: false)
174
+ mode ||= 'default'
175
+ return nil if string.nil?
176
+
177
+ unless SLUGIFY_MODES.include?(mode)
178
+ return cased ? string : string.downcase
179
+ end
180
+
181
+ # Replace each character sequence with a hyphen
182
+ re =
183
+ case mode
184
+ when 'raw'
185
+ SLUGIFY_RAW_REGEXP
186
+ when 'default'
187
+ SLUGIFY_DEFAULT_REGEXP
188
+ when 'pretty'
189
+ # "._~!$&'()+,;=@" is human readable (not URI-escaped) in URL
190
+ # and is allowed in both extN and NTFS.
191
+ SLUGIFY_PRETTY_REGEXP
192
+ end
193
+
194
+ # Strip according to the mode
195
+ slug = string.gsub(re, '-')
196
+
197
+ # Remove leading/trailing hyphen
198
+ slug.gsub!(/^\-|\-$/i, '')
199
+
200
+ slug.downcase! unless cased
201
+ slug
202
+ end
203
+
204
+ # Add an appropriate suffix to template so that it matches the specified
205
+ # permalink style.
206
+ #
207
+ # template - permalink template without trailing slash or file extension
208
+ # permalink_style - permalink style, either built-in or custom
209
+ #
210
+ # The returned permalink template will use the same ending style as
211
+ # specified in permalink_style. For example, if permalink_style contains a
212
+ # trailing slash (or is :pretty, which indirectly has a trailing slash),
213
+ # then so will the returned template. If permalink_style has a trailing
214
+ # ":output_ext" (or is :none, :date, or :ordinal) then so will the returned
215
+ # template. Otherwise, template will be returned without modification.
216
+ #
217
+ # Examples:
218
+ # add_permalink_suffix("/:basename", :pretty)
219
+ # # => "/:basename/"
220
+ #
221
+ # add_permalink_suffix("/:basename", :date)
222
+ # # => "/:basename:output_ext"
223
+ #
224
+ # add_permalink_suffix("/:basename", "/:year/:month/:title/")
225
+ # # => "/:basename/"
226
+ #
227
+ # add_permalink_suffix("/:basename", "/:year/:month/:title")
228
+ # # => "/:basename"
229
+ #
230
+ # Returns the updated permalink template
231
+ def add_permalink_suffix(template, permalink_style)
232
+ case permalink_style
233
+ when :pretty
234
+ template << "/"
235
+ when :date, :ordinal, :none
236
+ template << ":output_ext"
237
+ else
238
+ template << "/" if permalink_style.to_s.end_with?("/")
239
+ template << ":output_ext" if permalink_style.to_s.end_with?(":output_ext")
240
+ end
241
+ template
242
+ end
243
+
244
+ # Work the same way as Dir.glob but seperating the input into two parts
245
+ # ('dir' + '/' + 'pattern') to make sure the first part('dir') does not act
246
+ # as a pattern.
247
+ #
248
+ # For example, Dir.glob("path[/*") always returns an empty array,
249
+ # because the method fails to find the closing pattern to '[' which is ']'
250
+ #
251
+ # Examples:
252
+ # safe_glob("path[", "*")
253
+ # # => ["path[/file1", "path[/file2"]
254
+ #
255
+ # safe_glob("path", "*", File::FNM_DOTMATCH)
256
+ # # => ["path/.", "path/..", "path/file1"]
257
+ #
258
+ # safe_glob("path", ["**", "*"])
259
+ # # => ["path[/file1", "path[/folder/file2"]
260
+ #
261
+ # dir - the dir where glob will be executed under
262
+ # (the dir will be included to each result)
263
+ # patterns - the patterns (or the pattern) which will be applied under the dir
264
+ # flags - the flags which will be applied to the pattern
265
+ #
266
+ # Returns matched pathes
267
+ def safe_glob(dir, patterns, flags = 0)
268
+ return [] unless Dir.exist?(dir)
269
+ pattern = File.join(Array patterns)
270
+ return [dir] if pattern.empty?
271
+ Dir.chdir(dir) do
272
+ Dir.glob(pattern, flags).map { |f| File.join(dir, f) }
273
+ end
274
+ end
275
+
276
+ # Returns merged option hash for File.read of self.site (if exists)
277
+ # and a given param
278
+ def merged_file_read_opts(site, opts)
279
+ merged = (site ? site.file_read_opts : {}).merge(opts)
280
+ if merged["encoding"] && !merged["encoding"].start_with?("bom|")
281
+ merged["encoding"].insert(0, "bom|")
282
+ end
283
+ merged
284
+ end
285
+
286
+ end
287
+ end