bunto 1.0.0

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