nanoc2 2.2.3

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 (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