nanoc2 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. data/ChangeLog +3 -0
  2. data/LICENSE +19 -0
  3. data/README +75 -0
  4. data/Rakefile +76 -0
  5. data/bin/nanoc2 +26 -0
  6. data/lib/nanoc2.rb +73 -0
  7. data/lib/nanoc2/base.rb +26 -0
  8. data/lib/nanoc2/base/asset.rb +117 -0
  9. data/lib/nanoc2/base/asset_defaults.rb +21 -0
  10. data/lib/nanoc2/base/asset_rep.rb +282 -0
  11. data/lib/nanoc2/base/binary_filter.rb +44 -0
  12. data/lib/nanoc2/base/code.rb +41 -0
  13. data/lib/nanoc2/base/compiler.rb +67 -0
  14. data/lib/nanoc2/base/core_ext.rb +2 -0
  15. data/lib/nanoc2/base/core_ext/hash.rb +78 -0
  16. data/lib/nanoc2/base/core_ext/string.rb +8 -0
  17. data/lib/nanoc2/base/data_source.rb +286 -0
  18. data/lib/nanoc2/base/defaults.rb +30 -0
  19. data/lib/nanoc2/base/filter.rb +93 -0
  20. data/lib/nanoc2/base/layout.rb +91 -0
  21. data/lib/nanoc2/base/notification_center.rb +66 -0
  22. data/lib/nanoc2/base/page.rb +132 -0
  23. data/lib/nanoc2/base/page_defaults.rb +20 -0
  24. data/lib/nanoc2/base/page_rep.rb +324 -0
  25. data/lib/nanoc2/base/plugin.rb +71 -0
  26. data/lib/nanoc2/base/proxies.rb +5 -0
  27. data/lib/nanoc2/base/proxies/asset_proxy.rb +29 -0
  28. data/lib/nanoc2/base/proxies/asset_rep_proxy.rb +26 -0
  29. data/lib/nanoc2/base/proxies/layout_proxy.rb +25 -0
  30. data/lib/nanoc2/base/proxies/page_proxy.rb +35 -0
  31. data/lib/nanoc2/base/proxies/page_rep_proxy.rb +28 -0
  32. data/lib/nanoc2/base/proxy.rb +37 -0
  33. data/lib/nanoc2/base/router.rb +72 -0
  34. data/lib/nanoc2/base/site.rb +274 -0
  35. data/lib/nanoc2/base/template.rb +64 -0
  36. data/lib/nanoc2/binary_filters.rb +1 -0
  37. data/lib/nanoc2/binary_filters/image_science_thumbnail.rb +28 -0
  38. data/lib/nanoc2/cli.rb +9 -0
  39. data/lib/nanoc2/cli/base.rb +132 -0
  40. data/lib/nanoc2/cli/commands.rb +10 -0
  41. data/lib/nanoc2/cli/commands/autocompile.rb +80 -0
  42. data/lib/nanoc2/cli/commands/compile.rb +312 -0
  43. data/lib/nanoc2/cli/commands/create_layout.rb +85 -0
  44. data/lib/nanoc2/cli/commands/create_page.rb +85 -0
  45. data/lib/nanoc2/cli/commands/create_site.rb +323 -0
  46. data/lib/nanoc2/cli/commands/create_template.rb +76 -0
  47. data/lib/nanoc2/cli/commands/help.rb +69 -0
  48. data/lib/nanoc2/cli/commands/info.rb +125 -0
  49. data/lib/nanoc2/cli/commands/switch.rb +141 -0
  50. data/lib/nanoc2/cli/commands/update.rb +91 -0
  51. data/lib/nanoc2/cli/logger.rb +72 -0
  52. data/lib/nanoc2/data_sources.rb +2 -0
  53. data/lib/nanoc2/data_sources/filesystem.rb +707 -0
  54. data/lib/nanoc2/data_sources/filesystem_combined.rb +495 -0
  55. data/lib/nanoc2/extra.rb +6 -0
  56. data/lib/nanoc2/extra/auto_compiler.rb +285 -0
  57. data/lib/nanoc2/extra/context.rb +22 -0
  58. data/lib/nanoc2/extra/core_ext.rb +2 -0
  59. data/lib/nanoc2/extra/core_ext/hash.rb +54 -0
  60. data/lib/nanoc2/extra/core_ext/time.rb +13 -0
  61. data/lib/nanoc2/extra/file_proxy.rb +29 -0
  62. data/lib/nanoc2/extra/vcs.rb +48 -0
  63. data/lib/nanoc2/extra/vcses.rb +5 -0
  64. data/lib/nanoc2/extra/vcses/bazaar.rb +21 -0
  65. data/lib/nanoc2/extra/vcses/dummy.rb +20 -0
  66. data/lib/nanoc2/extra/vcses/git.rb +21 -0
  67. data/lib/nanoc2/extra/vcses/mercurial.rb +21 -0
  68. data/lib/nanoc2/extra/vcses/subversion.rb +21 -0
  69. data/lib/nanoc2/filters.rb +16 -0
  70. data/lib/nanoc2/filters/bluecloth.rb +13 -0
  71. data/lib/nanoc2/filters/erb.rb +19 -0
  72. data/lib/nanoc2/filters/erubis.rb +14 -0
  73. data/lib/nanoc2/filters/haml.rb +21 -0
  74. data/lib/nanoc2/filters/markaby.rb +14 -0
  75. data/lib/nanoc2/filters/maruku.rb +14 -0
  76. data/lib/nanoc2/filters/old.rb +19 -0
  77. data/lib/nanoc2/filters/rainpress.rb +13 -0
  78. data/lib/nanoc2/filters/rdiscount.rb +13 -0
  79. data/lib/nanoc2/filters/rdoc.rb +23 -0
  80. data/lib/nanoc2/filters/redcloth.rb +14 -0
  81. data/lib/nanoc2/filters/relativize_paths.rb +16 -0
  82. data/lib/nanoc2/filters/relativize_paths_in_css.rb +16 -0
  83. data/lib/nanoc2/filters/relativize_paths_in_html.rb +16 -0
  84. data/lib/nanoc2/filters/rubypants.rb +14 -0
  85. data/lib/nanoc2/filters/sass.rb +18 -0
  86. data/lib/nanoc2/helpers.rb +9 -0
  87. data/lib/nanoc2/helpers/blogging.rb +217 -0
  88. data/lib/nanoc2/helpers/capturing.rb +63 -0
  89. data/lib/nanoc2/helpers/filtering.rb +54 -0
  90. data/lib/nanoc2/helpers/html_escape.rb +25 -0
  91. data/lib/nanoc2/helpers/link_to.rb +113 -0
  92. data/lib/nanoc2/helpers/render.rb +49 -0
  93. data/lib/nanoc2/helpers/tagging.rb +56 -0
  94. data/lib/nanoc2/helpers/text.rb +38 -0
  95. data/lib/nanoc2/helpers/xml_sitemap.rb +63 -0
  96. data/lib/nanoc2/routers.rb +3 -0
  97. data/lib/nanoc2/routers/default.rb +54 -0
  98. data/lib/nanoc2/routers/no_dirs.rb +66 -0
  99. data/lib/nanoc2/routers/versioned.rb +79 -0
  100. metadata +185 -0
@@ -0,0 +1,282 @@
1
+ module Nanoc2
2
+
3
+ # A Nanoc2::AssetRep is a single representation (rep) of an asset
4
+ # (Nanoc2::Asset). An asset can have multiple representations. A
5
+ # representation has its own attributes and its own output file. A single
6
+ # asset can therefore have multiple output files, each run through a
7
+ # different set of filters with a different layout.
8
+ #
9
+ # An asset representation is observable. The following events will be
10
+ # notified:
11
+ #
12
+ # * :compilation_started
13
+ # * :compilation_ended
14
+ # * :filtering_started
15
+ # * :filtering_ended
16
+ #
17
+ # The compilation-related events have one parameters (the page
18
+ # representation); the filtering-related events have two (the page
19
+ # representation, and a symbol containing the filter class name).
20
+ class AssetRep
21
+
22
+ # The asset (Nanoc2::Asset) to which this representation belongs.
23
+ attr_reader :asset
24
+
25
+ # A hash containing this asset representation's attributes.
26
+ attr_accessor :attributes
27
+
28
+ # This asset representation's unique name.
29
+ attr_reader :name
30
+
31
+ # Creates a new asset representation for the given asset and with the
32
+ # given attributes.
33
+ #
34
+ # +asset+:: The asset (Nanoc2::Asset) to which the new representation will
35
+ # belong.
36
+ #
37
+ # +attributes+:: A hash containing the new asset representation's
38
+ # attributes. This hash must have been run through
39
+ # Hash#clean before using it here.
40
+ #
41
+ # +name+:: The unique name for the new asset representation.
42
+ def initialize(asset, attributes, name)
43
+ # Set primary attributes
44
+ @asset = asset
45
+ @attributes = attributes
46
+ @name = name
47
+
48
+ # Reset flags
49
+ @compiled = false
50
+ @modified = false
51
+ @created = false
52
+
53
+ # Reset stages
54
+ @filtered = false
55
+ end
56
+
57
+ # Returns a proxy (Nanoc2::AssetRepProxy) for this asset representation.
58
+ def to_proxy
59
+ @proxy ||= AssetRepProxy.new(self)
60
+ end
61
+
62
+ # Returns true if this asset rep's output file was created during the last
63
+ # compilation session, or false if the output file did already exist.
64
+ def created?
65
+ @created
66
+ end
67
+
68
+ # Returns true if this asset rep's output file was modified during the
69
+ # last compilation session, or false if the output file wasn't changed.
70
+ def modified?
71
+ @modified
72
+ end
73
+
74
+ # Returns true if this page rep has been compiled, false otherwise.
75
+ def compiled?
76
+ @compiled
77
+ end
78
+
79
+ # Returns the path to the output file, including the path to the output
80
+ # directory specified in the site configuration, and including the
81
+ # filename and extension.
82
+ def disk_path
83
+ @disk_path ||= @asset.site.router.disk_path_for(self)
84
+ end
85
+
86
+ # Returns the path to the output file as it would be used in a web
87
+ # browser: starting with a slash (representing the web root), and only
88
+ # including the filename and extension if they cannot be ignored (i.e.
89
+ # they are not in the site configuration's list of index files).
90
+ def web_path
91
+ compile(false, false)
92
+
93
+ @web_path ||= @asset.site.router.web_path_for(self)
94
+ end
95
+
96
+ # Returns true if this asset rep's output file is outdated and must be
97
+ # regenerated, false otherwise.
98
+ def outdated?
99
+ # Outdated if we don't know
100
+ return true if @asset.mtime.nil?
101
+
102
+ # Outdated if compiled file doesn't exist
103
+ return true if !File.file?(disk_path)
104
+
105
+ # Get compiled mtime
106
+ compiled_mtime = File.stat(disk_path).mtime
107
+
108
+ # Outdated if file too old
109
+ return true if @asset.mtime > compiled_mtime
110
+
111
+ # Outdated if asset defaults outdated
112
+ return true if @asset.site.asset_defaults.mtime.nil?
113
+ return true if @asset.site.asset_defaults.mtime > compiled_mtime
114
+
115
+ # Outdated if code outdated
116
+ return true if @asset.site.code.mtime.nil?
117
+ return true if @asset.site.code.mtime > compiled_mtime
118
+
119
+ return false
120
+ end
121
+
122
+ # Returns the attribute with the given name. This method will look in
123
+ # several places for the requested attribute:
124
+ #
125
+ # 1. This asset representation's attributes;
126
+ # 2. The attributes of this asset representation's asset;
127
+ # 3. The asset defaults' representation corresponding to this asset
128
+ # representation;
129
+ # 4. The asset defaults in general;
130
+ # 5. The hardcoded asset defaults, if everything else fails.
131
+ def attribute_named(name)
132
+ # Check in here
133
+ return @attributes[name] if @attributes.has_key?(name)
134
+
135
+ # Check in asset
136
+ return @asset.attributes[name] if @asset.attributes.has_key?(name)
137
+
138
+ # Check in asset defaults' asset rep
139
+ asset_default_reps = @asset.site.asset_defaults.attributes[:reps] || {}
140
+ asset_default_rep = asset_default_reps[@name] || {}
141
+ return asset_default_rep[name] if asset_default_rep.has_key?(name)
142
+
143
+ # Check in site defaults (global)
144
+ asset_defaults_attrs = @asset.site.asset_defaults.attributes
145
+ return asset_defaults_attrs[name] if asset_defaults_attrs.has_key?(name)
146
+
147
+ # Check in hardcoded defaults
148
+ return Nanoc2::Asset::DEFAULTS[name]
149
+ end
150
+
151
+ # Compiles the asset representation and writes the result to the disk.
152
+ # This method should not be called directly; please use
153
+ # Nanoc2::Compiler#run instead, and pass this asset representation's asset
154
+ # as its first argument.
155
+ #
156
+ # The asset representation will only be compiled if it wasn't compiled
157
+ # before yet. To force recompilation of the asset rep, forgetting any
158
+ # progress, set +from_scratch+ to true.
159
+ #
160
+ # +even_when_not_outdated+:: true if the asset rep should be compiled even
161
+ # if it is not outdated, false if not.
162
+ #
163
+ # +from_scratch+:: true if the asset rep should be filtered again even if
164
+ # it has already been filtered, false otherwise.
165
+ def compile(even_when_not_outdated, from_scratch)
166
+ # Don't compile if already compiled
167
+ return if @compiled and !from_scratch
168
+
169
+ # Skip unless outdated
170
+ unless outdated? or even_when_not_outdated
171
+ Nanoc2::NotificationCenter.post(:compilation_started, self)
172
+ Nanoc2::NotificationCenter.post(:compilation_ended, self)
173
+ return
174
+ end
175
+
176
+ # Reset flags
177
+ @compiled = false
178
+ @modified = false
179
+ @created = !File.file?(self.disk_path)
180
+
181
+ # Forget progress if requested
182
+ @filtered = false if from_scratch
183
+
184
+ # Start
185
+ @asset.site.compiler.stack.push(self)
186
+ Nanoc2::NotificationCenter.post(:compilation_started, self)
187
+
188
+ # Compile
189
+ unless @filtered
190
+ if attribute_named(:binary) == true
191
+ compile_binary
192
+ else
193
+ compile_textual
194
+ end
195
+ end
196
+ @compiled = true
197
+
198
+ # Stop
199
+ @asset.site.compiler.stack.pop
200
+ Nanoc2::NotificationCenter.post(:compilation_ended, self)
201
+ end
202
+
203
+ private
204
+
205
+ # Computes and returns the MD5 digest for the given file.
206
+ def digest(filename)
207
+ # Create hash
208
+ incr_digest = Digest::MD5.new()
209
+
210
+ # Collect data
211
+ File.open(filename, 'r') do |io|
212
+ incr_digest << io.read(1000) until io.eof?
213
+ end
214
+
215
+ # Calculate hex hash
216
+ incr_digest.hexdigest
217
+ end
218
+
219
+ # Compiles the asset rep, treating its contents as binary data.
220
+ def compile_binary
221
+ # Calculate digest before
222
+ digest_before = File.file?(disk_path) ? digest(disk_path) : nil
223
+
224
+ # Run filters
225
+ current_file = @asset.file
226
+ attribute_named(:filters).each do |filter_name|
227
+ # Free resources so that this filter won't fail
228
+ GC.start
229
+
230
+ # Create filter
231
+ klass = Nanoc2::BinaryFilter.named(filter_name)
232
+ raise Nanoc2::Errors::UnknownFilterError.new(filter_name) if klass.nil?
233
+ filter = klass.new(self.to_proxy, @asset.to_proxy, @asset.site)
234
+
235
+ # Run filter
236
+ Nanoc2::NotificationCenter.post(:filtering_started, self, klass.identifier)
237
+ current_file = filter.run(current_file)
238
+ Nanoc2::NotificationCenter.post(:filtering_ended, self, klass.identifier)
239
+ end
240
+
241
+ # Write asset
242
+ FileUtils.mkdir_p(File.dirname(self.disk_path))
243
+ FileUtils.cp(current_file.path, disk_path)
244
+
245
+ # Calculate digest after
246
+ digest_after = digest(disk_path)
247
+ @modified = (digest_after != digest_before)
248
+ end
249
+
250
+ # Compiles the asset rep, treating its contents as textual data.
251
+ def compile_textual
252
+ # Get content
253
+ current_content = @asset.file.read
254
+
255
+ # Check modified
256
+ @modified = @created ? true : File.read(self.disk_path) != current_content
257
+
258
+ # Run filters
259
+ attribute_named(:filters).each do |filter_name|
260
+ # Create filter
261
+ klass = Nanoc2::Filter.named(filter_name)
262
+ raise Nanoc2::Errors::UnknownFilterError.new(filter_name) if klass.nil?
263
+ filter = klass.new(self)
264
+
265
+ # Run filter
266
+ Nanoc2::NotificationCenter.post(:filtering_started, self, klass.identifier)
267
+ current_content = filter.run(current_content)
268
+ Nanoc2::NotificationCenter.post(:filtering_ended, self, klass.identifier)
269
+ end
270
+
271
+ # Write asset
272
+ FileUtils.mkdir_p(File.dirname(self.disk_path))
273
+ File.open(self.disk_path, 'w') { |io| io.write(current_content) }
274
+ end
275
+
276
+ def inspect
277
+ "<#{self.class} name=#{self.name} asset.path=#{self.asset.path}>"
278
+ end
279
+
280
+ end
281
+
282
+ end
@@ -0,0 +1,44 @@
1
+ module Nanoc2
2
+
3
+ # Nanoc2::BinaryFilter is responsible for filtering binary assets. It is the
4
+ # (abstract) superclass for all binary filters. Subclasses should override
5
+ # the +run+ method.
6
+ class BinaryFilter < Plugin
7
+
8
+ # Creates a new binary filter for the given asset and site.
9
+ #
10
+ # +asset_rep+:: A proxy for the asset representation (Nanoc2::AssetRep)
11
+ # that should be compiled by this filter.
12
+ #
13
+ # +asset+:: A proxy for the asset (Nanoc2::Asset) for which +asset_rep+ is
14
+ # the representation.
15
+ #
16
+ # +site+:: The site (Nanoc2::Site) this filter belongs to.
17
+ #
18
+ # +other_assigns+:: A hash containing other variables that should be made
19
+ # available during filtering.
20
+ def initialize(asset_rep, asset, site, other_assigns={})
21
+ @asset_rep = asset_rep
22
+ @asset = asset
23
+ @pages = site.pages.map { |p| p.to_proxy }
24
+ @assets = site.assets.map { |a| a.to_proxy }
25
+ @layouts = site.layouts.map { |l| l.to_proxy }
26
+ @config = site.config
27
+ @site = site
28
+ @other_assigns = other_assigns
29
+ end
30
+
31
+ # Runs the filter. This method returns a File instance pointing to a new
32
+ # file, containing the filtered content.
33
+ #
34
+ # +file+:: A File instance representing the incoming file that should be
35
+ # filtered. This file should _not_ be modified.
36
+ #
37
+ # Subclasses must implement this method.
38
+ def run(file)
39
+ raise NotImplementedError.new("Nanoc2::BinaryFilter subclasses must implement #run")
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,41 @@
1
+ module Nanoc2
2
+
3
+ # Nanoc2::Code represent the custom code of a nanoc site. It contains the
4
+ # textual source code as well as a mtime, which is used to speed up site
5
+ # compilation.
6
+ class Code
7
+
8
+ # The Nanoc2::Site this code belongs to.
9
+ attr_accessor :site
10
+
11
+ # The textual source code representation.
12
+ attr_reader :data
13
+
14
+ # The time where the code was last modified.
15
+ attr_reader :mtime
16
+
17
+ # Creates a new code object. +data+ is the raw source code, which will be
18
+ # executed before compilation. +mtime+ is the time when the code was last
19
+ # modified (optional).
20
+ def initialize(data, mtime=nil)
21
+ @data = data
22
+ @mtime = mtime
23
+ end
24
+
25
+ # Loads the code by executing it.
26
+ def load
27
+ eval(@data, TOPLEVEL_BINDING)
28
+ end
29
+
30
+ # Saves the code in the database, creating it if it doesn't exist yet or
31
+ # updating it if it already exists. Tells the site's data source to save
32
+ # the code.
33
+ def save
34
+ @site.data_source.loading do
35
+ @site.data_source.save_code(self)
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,67 @@
1
+ module Nanoc2
2
+
3
+ # Nanoc2::Compiler is responsible for compiling a site's page and asset
4
+ # representations.
5
+ class Compiler
6
+
7
+ attr_reader :stack
8
+
9
+ # Creates a new compiler for the given site.
10
+ def initialize(site)
11
+ @site = site
12
+ @stack = []
13
+ end
14
+
15
+ # Compiles (part of) the site and writes out the compiled page and asset
16
+ # representations.
17
+ #
18
+ # +obj+:: The page or asset that should be compiled, along with their
19
+ # dependencies, or +nil+ if the entire site should be compiled.
20
+ #
21
+ # This method also accepts a few parameters:
22
+ #
23
+ # +:also_layout+:: true if the page rep should also be laid out and
24
+ # post-filtered, false if the page rep should only be
25
+ # pre-filtered. Only applicable to page reps, and not to
26
+ # asset reps. Defaults to true.
27
+ #
28
+ # +:even_when_not_outdated+:: true if the rep should be compiled even if
29
+ # it is not outdated, false if not. Defaults
30
+ # to false.
31
+ #
32
+ # +:from_scratch+:: true if all compilation stages (for page reps:
33
+ # pre-filter, layout, post-filter; for asset reps:
34
+ # filter) should be performed again even if they have
35
+ # already been performed, false otherwise. Defaults to
36
+ # false.
37
+ def run(objects=nil, params={})
38
+ # Parse params
39
+ also_layout = params[:also_layout] || true
40
+ even_when_not_outdated = params[:even_when_not_outdated] || false
41
+ from_scratch = params[:from_scratch] || false
42
+
43
+ # Load data
44
+ @site.load_data
45
+
46
+ # Create output directory if necessary
47
+ FileUtils.mkdir_p(@site.config[:output_dir])
48
+
49
+ # Initialize
50
+ @stack = []
51
+
52
+ # Get pages and asset reps
53
+ objects = @site.pages + @site.assets if objects.nil?
54
+ reps = objects.map { |o| o.reps }.flatten
55
+
56
+ # Compile everything
57
+ reps.each do |rep|
58
+ if rep.is_a?(Nanoc2::PageRep)
59
+ rep.compile(also_layout, even_when_not_outdated, from_scratch)
60
+ else
61
+ rep.compile(even_when_not_outdated, from_scratch)
62
+ end
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,2 @@
1
+ require 'nanoc2/base/core_ext/hash'
2
+ require 'nanoc2/base/core_ext/string'
@@ -0,0 +1,78 @@
1
+ require 'time'
2
+
3
+ class Hash
4
+
5
+ # Cleans up the hash and returns the result. It performs the following
6
+ # operations:
7
+ #
8
+ # * Values with keys ending in +_at+ and +_on+ are converted into +Time+ and
9
+ # and +Date+ objects, respectively
10
+ # * All keys are converted to symbols
11
+ # * Value strings 'true', 'false' and 'none' are converted into +true+,
12
+ # +false+ and +nil+, respectively
13
+ #
14
+ # Hashes are cleaned recursively, so the value of a hash pair will also be
15
+ # cleaned if the value is a hash.
16
+ #
17
+ # For example, the following hash:
18
+ #
19
+ # {
20
+ # 'foo' => 'bar',
21
+ # :created_on => '2008-05-19',
22
+ # :layout => 'none'
23
+ # }
24
+ #
25
+ # will be converted into:
26
+ #
27
+ # {
28
+ # :foo => 'bar',
29
+ # :created_on => Date.parse('2008-05-19'),
30
+ # :layout => nil
31
+ # }
32
+ def clean
33
+ inject({}) do |hash, (key, value)|
34
+ real_key = key.to_s
35
+
36
+ if real_key =~ /_on$/ and value.is_a?(String)
37
+ hash.merge(key.to_sym => Date.parse(value))
38
+ elsif real_key =~ /_at$/ and value.is_a?(String)
39
+ hash.merge(key.to_sym => Time.parse(value.to_s))
40
+ elsif value == 'true'
41
+ hash.merge(key.to_sym => true)
42
+ elsif value == 'false'
43
+ hash.merge(key.to_sym => false)
44
+ elsif value == 'none'
45
+ hash.merge(key.to_sym => nil)
46
+ elsif value.is_a?(Hash)
47
+ hash.merge(key.to_sym => value.clean)
48
+ else
49
+ hash.merge(key.to_sym => value)
50
+ end
51
+ end
52
+ end
53
+
54
+ # Returns the hash where all keys are converted to strings. Hash keys are
55
+ # converted to strings recursively, so the keys of a value of a hash pair
56
+ # will also be converted to strings if the value is a hash.
57
+ #
58
+ # For example, the following hash:
59
+ #
60
+ # {
61
+ # 'foo' => 'bar',
62
+ # :baz => 'quux'
63
+ # }
64
+ #
65
+ # will be converted into:
66
+ #
67
+ # {
68
+ # :foo => 'bar',
69
+ # :baz => 'quux'
70
+ # }
71
+ #
72
+ def stringify_keys
73
+ inject({}) do |hash, (key, value)|
74
+ hash.merge(key.to_s => value.is_a?(Hash) ? value.stringify_keys : value)
75
+ end
76
+ end
77
+
78
+ end