nanoc 2.0 → 2.1

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 (99) hide show
  1. data/ChangeLog +54 -0
  2. data/LICENSE +1 -1
  3. data/README +63 -3
  4. data/Rakefile +61 -14
  5. data/bin/nanoc +7 -199
  6. data/lib/nanoc/base/asset.rb +113 -0
  7. data/lib/nanoc/base/asset_defaults.rb +21 -0
  8. data/lib/nanoc/base/asset_rep.rb +277 -0
  9. data/lib/nanoc/base/binary_filter.rb +44 -0
  10. data/lib/nanoc/base/code.rb +41 -0
  11. data/lib/nanoc/base/compiler.rb +46 -34
  12. data/lib/nanoc/base/core_ext/hash.rb +51 -7
  13. data/lib/nanoc/base/core_ext/string.rb +8 -0
  14. data/lib/nanoc/base/data_source.rb +253 -20
  15. data/lib/nanoc/base/defaults.rb +30 -0
  16. data/lib/nanoc/base/enhancements.rb +9 -84
  17. data/lib/nanoc/base/filter.rb +109 -6
  18. data/lib/nanoc/base/layout.rb +91 -0
  19. data/lib/nanoc/base/notification_center.rb +66 -0
  20. data/lib/nanoc/base/page.rb +94 -121
  21. data/lib/nanoc/base/page_defaults.rb +20 -0
  22. data/lib/nanoc/base/page_rep.rb +318 -0
  23. data/lib/nanoc/base/plugin.rb +57 -9
  24. data/lib/nanoc/base/proxies/asset_proxy.rb +29 -0
  25. data/lib/nanoc/base/proxies/asset_rep_proxy.rb +26 -0
  26. data/lib/nanoc/base/proxies/layout_proxy.rb +25 -0
  27. data/lib/nanoc/base/proxies/page_proxy.rb +35 -0
  28. data/lib/nanoc/base/proxies/page_rep_proxy.rb +28 -0
  29. data/lib/nanoc/base/proxy.rb +37 -0
  30. data/lib/nanoc/base/router.rb +72 -0
  31. data/lib/nanoc/base/site.rb +220 -91
  32. data/lib/nanoc/base/template.rb +64 -0
  33. data/lib/nanoc/binary_filters/image_science_thumbnail.rb +28 -0
  34. data/lib/nanoc/cli/base.rb +219 -0
  35. data/lib/nanoc/cli/cli.rb +16 -0
  36. data/lib/nanoc/cli/command.rb +105 -0
  37. data/lib/nanoc/cli/commands/autocompile.rb +80 -0
  38. data/lib/nanoc/cli/commands/compile.rb +273 -0
  39. data/lib/nanoc/cli/commands/create_layout.rb +85 -0
  40. data/lib/nanoc/cli/commands/create_page.rb +85 -0
  41. data/lib/nanoc/cli/commands/create_site.rb +327 -0
  42. data/lib/nanoc/cli/commands/create_template.rb +76 -0
  43. data/lib/nanoc/cli/commands/help.rb +69 -0
  44. data/lib/nanoc/cli/commands/info.rb +114 -0
  45. data/lib/nanoc/cli/commands/switch.rb +141 -0
  46. data/lib/nanoc/cli/commands/update.rb +91 -0
  47. data/lib/nanoc/cli/ext.rb +37 -0
  48. data/lib/nanoc/cli/logger.rb +66 -0
  49. data/lib/nanoc/cli/option_parser.rb +168 -0
  50. data/lib/nanoc/cli.rb +1 -0
  51. data/lib/nanoc/data_sources/filesystem.rb +650 -215
  52. data/lib/nanoc/data_sources/filesystem_combined.rb +495 -0
  53. data/lib/nanoc/extra/auto_compiler.rb +265 -0
  54. data/lib/nanoc/extra/context.rb +22 -0
  55. data/lib/nanoc/extra/core_ext/hash.rb +54 -0
  56. data/lib/nanoc/extra/core_ext/time.rb +13 -0
  57. data/lib/nanoc/extra/file_proxy.rb +29 -0
  58. data/lib/nanoc/extra/vcs.rb +48 -0
  59. data/lib/nanoc/extra/vcses/bazaar.rb +21 -0
  60. data/lib/nanoc/extra/vcses/dummy.rb +20 -0
  61. data/lib/nanoc/extra/vcses/git.rb +21 -0
  62. data/lib/nanoc/extra/vcses/mercurial.rb +21 -0
  63. data/lib/nanoc/extra/vcses/subversion.rb +21 -0
  64. data/lib/nanoc/filters/bluecloth.rb +13 -0
  65. data/lib/nanoc/filters/erb.rb +6 -22
  66. data/lib/nanoc/filters/erubis.rb +14 -0
  67. data/lib/nanoc/filters/haml.rb +11 -6
  68. data/lib/nanoc/filters/markaby.rb +5 -5
  69. data/lib/nanoc/filters/maruku.rb +14 -0
  70. data/lib/nanoc/filters/old.rb +19 -0
  71. data/lib/nanoc/filters/rdiscount.rb +13 -0
  72. data/lib/nanoc/filters/rdoc.rb +5 -4
  73. data/lib/nanoc/filters/redcloth.rb +14 -0
  74. data/lib/nanoc/filters/rubypants.rb +14 -0
  75. data/lib/nanoc/filters/sass.rb +13 -0
  76. data/lib/nanoc/helpers/blogging.rb +170 -0
  77. data/lib/nanoc/helpers/capturing.rb +59 -0
  78. data/lib/nanoc/helpers/html_escape.rb +23 -0
  79. data/lib/nanoc/helpers/link_to.rb +69 -0
  80. data/lib/nanoc/helpers/render.rb +47 -0
  81. data/lib/nanoc/helpers/tagging.rb +52 -0
  82. data/lib/nanoc/helpers/xml_sitemap.rb +58 -0
  83. data/lib/nanoc/routers/default.rb +54 -0
  84. data/lib/nanoc/routers/no_dirs.rb +66 -0
  85. data/lib/nanoc/routers/versioned.rb +79 -0
  86. data/lib/nanoc.rb +83 -12
  87. metadata +144 -47
  88. data/lib/nanoc/base/auto_compiler.rb +0 -124
  89. data/lib/nanoc/base/layout_processor.rb +0 -33
  90. data/lib/nanoc/base/page_proxy.rb +0 -31
  91. data/lib/nanoc/base/plugin_manager.rb +0 -33
  92. data/lib/nanoc/data_sources/database.rb +0 -259
  93. data/lib/nanoc/data_sources/trivial.rb +0 -145
  94. data/lib/nanoc/filters/markdown.rb +0 -13
  95. data/lib/nanoc/filters/smartypants.rb +0 -13
  96. data/lib/nanoc/filters/textile.rb +0 -13
  97. data/lib/nanoc/layout_processors/erb.rb +0 -35
  98. data/lib/nanoc/layout_processors/haml.rb +0 -18
  99. data/lib/nanoc/layout_processors/markaby.rb +0 -16
@@ -0,0 +1,277 @@
1
+ module Nanoc
2
+
3
+ # A Nanoc::AssetRep is a single representation (rep) of an asset
4
+ # (Nanoc::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 (Nanoc::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 (Nanoc::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 (Nanoc::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 Nanoc::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
+ # Nanoc::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
+ Nanoc::NotificationCenter.post(:compilation_started, self)
172
+ Nanoc::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
+ Nanoc::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
+ Nanoc::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(file)
207
+ # Create hash
208
+ incr_digest = Digest::MD5.new()
209
+
210
+ # Collect data
211
+ file.rewind
212
+ incr_digest << file.read(1000) until file.eof?
213
+
214
+ # Calculate hex hash
215
+ incr_digest.hexdigest
216
+ end
217
+
218
+ # Compiles the asset rep, treating its contents as binary data.
219
+ def compile_binary
220
+ # Calculate digest before
221
+ digest_before = File.file?(disk_path) ? digest(File.open(disk_path, 'r')) : nil
222
+
223
+ # Run filters
224
+ current_file = @asset.file
225
+ attribute_named(:filters).each do |filter_name|
226
+ # Free resources so that this filter won't fail
227
+ GC.start
228
+
229
+ # Create filter
230
+ klass = Nanoc::BinaryFilter.named(filter_name)
231
+ raise Nanoc::Errors::UnknownFilterError.new(filter_name) if klass.nil?
232
+ filter = klass.new(self.to_proxy, @asset.to_proxy, @asset.site)
233
+
234
+ # Run filter
235
+ Nanoc::NotificationCenter.post(:filtering_started, self, klass.identifier)
236
+ current_file = filter.run(current_file)
237
+ Nanoc::NotificationCenter.post(:filtering_ended, self, klass.identifier)
238
+ end
239
+
240
+ # Write asset
241
+ FileUtils.mkdir_p(File.dirname(self.disk_path))
242
+ FileUtils.cp(current_file.path, disk_path)
243
+
244
+ # Calculate digest after
245
+ digest_after = digest(current_file)
246
+ @modified = (digest_after != digest_before)
247
+ end
248
+
249
+ # Compiles the asset rep, treating its contents as textual data.
250
+ def compile_textual
251
+ # Get content
252
+ current_content = @asset.file.read
253
+
254
+ # Check modified
255
+ @modified = @created ? true : File.read(self.disk_path) != current_content
256
+
257
+ # Run filters
258
+ attribute_named(:filters).each do |filter_name|
259
+ # Create filter
260
+ klass = Nanoc::Filter.named(filter_name)
261
+ raise Nanoc::Errors::UnknownFilterError.new(filter_name) if klass.nil?
262
+ filter = klass.new(self)
263
+
264
+ # Run filter
265
+ Nanoc::NotificationCenter.post(:filtering_started, self, klass.identifier)
266
+ current_content = filter.run(current_content)
267
+ Nanoc::NotificationCenter.post(:filtering_ended, self, klass.identifier)
268
+ end
269
+
270
+ # Write asset
271
+ FileUtils.mkdir_p(File.dirname(self.disk_path))
272
+ File.open(self.disk_path, 'w') { |io| io.write(current_content) }
273
+ end
274
+
275
+ end
276
+
277
+ end
@@ -0,0 +1,44 @@
1
+ module Nanoc
2
+
3
+ # Nanoc::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 (Nanoc::AssetRep)
11
+ # that should be compiled by this filter.
12
+ #
13
+ # +asset+:: A proxy for the asset (Nanoc::Asset) for which +asset_rep+ is
14
+ # the representation.
15
+ #
16
+ # +site+:: The site (Nanoc::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("Nanoc::BinaryFilter subclasses must implement #run")
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,41 @@
1
+ module Nanoc
2
+
3
+ # Nanoc::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 Nanoc::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
@@ -1,54 +1,66 @@
1
1
  module Nanoc
2
+
3
+ # Nanoc::Compiler is responsible for compiling a site's page and asset
4
+ # representations.
2
5
  class Compiler
3
6
 
4
7
  attr_reader :stack
5
8
 
9
+ # Creates a new compiler for the given site.
6
10
  def initialize(site)
7
- @site = site
11
+ @site = site
12
+ @stack = []
8
13
  end
9
14
 
10
- def run(page=nil)
11
- # Give feedback
12
- log(:high, "Compiling #{page.nil? ? 'site' : 'page'}...")
13
- time_before = Time.now
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
14
42
 
15
- # Get the data we need
43
+ # Load data
16
44
  @site.load_data
17
- eval(@site.code, $nanoc_binding)
18
45
 
19
46
  # Create output directory if necessary
20
47
  FileUtils.mkdir_p(@site.config[:output_dir])
21
48
 
22
- # Compile
49
+ # Initialize
23
50
  @stack = []
24
- pages = (page.nil? ? @site.pages : [ page ])
25
- pages.each do |current_page|
26
- begin
27
- current_page.compile
28
- rescue => exception
29
- handle_exception(exception, current_page, !page.nil?)
30
- end
31
- end
32
51
 
33
- # Give feedback
34
- log(:high, "No pages were modified.") unless pages.any? { |page| page.modified? }
35
- log(:high, "#{page.nil? ? 'Site' : 'Pages'} compiled in #{format('%.2f', Time.now - time_before)}s.")
36
- end
52
+ # Get pages and asset reps
53
+ objects = @site.pages + @site.assets if objects.nil?
54
+ reps = objects.map { |o| o.reps }.flatten
37
55
 
38
- def handle_exception(exception, page, single_page)
39
- raise exception if single_page
40
-
41
- log(:high, "ERROR: An exception occured while compiling page #{page.path}.", $stderr)
42
- log(:high, "", $stderr)
43
- log(:high, "If you think this is a bug in nanoc, please do report it at", $stderr)
44
- log(:high, "<http://nanoc.stoneship.org/trac/newticket> -- thanks!", $stderr)
45
- log(:high, "", $stderr)
46
- log(:high, 'Message:', $stderr)
47
- log(:high, ' ' + exception.message, $stderr)
48
- log(:high, 'Backtrace:', $stderr)
49
- log(:high, exception.backtrace.map { |t| ' - ' + t }.join("\n"), $stderr)
50
-
51
- exit(1)
56
+ # Compile everything
57
+ reps.each do |rep|
58
+ if rep.is_a?(Nanoc::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
52
64
  end
53
65
 
54
66
  end
@@ -5,18 +5,38 @@ class Hash
5
5
  # Cleans up the hash and returns the result. It performs the following
6
6
  # operations:
7
7
  #
8
- # * Values with keys ending in _at and _on are converted into Times and
9
- # Dates, respectively
8
+ # * Values with keys ending in +_at+ and +_on+ are converted into +Time+ and
9
+ # and +Date+ objects, respectively
10
10
  # * All keys are converted to symbols
11
- # * Value strings 'true', 'false', and 'none' are converted into
12
- # true, false, and nil, respectively
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
+ # }
13
32
  def clean
14
33
  inject({}) do |hash, (key, value)|
15
34
  real_key = key.to_s
16
- if real_key =~ /_on$/
35
+
36
+ if real_key =~ /_on$/ and value.is_a?(String)
17
37
  hash.merge(key.to_sym => Date.parse(value))
18
- elsif real_key =~ /_at$/
19
- hash.merge(key.to_sym => Time.parse(value))
38
+ elsif real_key =~ /_at$/ and value.is_a?(String)
39
+ hash.merge(key.to_sym => Time.parse(value.to_s))
20
40
  elsif value == 'true'
21
41
  hash.merge(key.to_sym => true)
22
42
  elsif value == 'false'
@@ -31,4 +51,28 @@ class Hash
31
51
  end
32
52
  end
33
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
+
34
78
  end
@@ -0,0 +1,8 @@
1
+ class String
2
+
3
+ # Transforms string into an actual path
4
+ def cleaned_path
5
+ "/#{self}/".gsub(/^\/+|\/+$/, '/')
6
+ end
7
+
8
+ end