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,72 @@
1
+ require 'singleton'
2
+
3
+ module Nanoc2::CLI
4
+
5
+ # Nanoc2::CLI::Logger is a singleton class responsible for generating
6
+ # feedback in the terminal.
7
+ class Logger
8
+
9
+ ACTION_COLORS = {
10
+ :create => "\e[1m" + "\e[32m", # bold + green
11
+ :update => "\e[1m" + "\e[33m", # bold + yellow
12
+ :identical => "\e[1m", # bold
13
+ :skip => "\e[1m", # bold
14
+ :'not written' => "\e[1m" # bold
15
+ }
16
+
17
+ include Singleton
18
+
19
+ # The log level, which can be :high, :low or :off (which will log all
20
+ # messages, only high-priority messages, or no messages at all,
21
+ # respectively).
22
+ attr_accessor :level
23
+
24
+ # Whether to use color in log messages or not
25
+ attr_accessor :color
26
+ alias_method :color?, :color
27
+
28
+ def initialize # :nodoc:
29
+ @level = :high
30
+ @color = true
31
+ end
32
+
33
+ # Logs a file-related action.
34
+ #
35
+ # +level+:: The importance of this action. Can be :high or :low.
36
+ #
37
+ # +action+:: The kind of file action. Can be :create, :update or
38
+ # :identical.
39
+ #
40
+ # +path+:: The path to the file the action was performed on.
41
+ def file(level, action, path, duration=nil)
42
+ log(
43
+ level,
44
+ '%s%12s%s %s%s' % [
45
+ color? ? ACTION_COLORS[action.to_sym] : '',
46
+ action,
47
+ color? ? "\e[0m" : '',
48
+ duration.nil? ? '' : "[%2.2fs] " % [ duration ],
49
+ path
50
+ ]
51
+ )
52
+ end
53
+
54
+ # Logs a message.
55
+ #
56
+ # +level+:: The importance of this message. Can be :high or :low.
57
+ #
58
+ # +s+:: The message to be logged.
59
+ #
60
+ # +io+:: The IO instance to which the message will be written. Defaults to
61
+ # standard output.
62
+ def log(level, s, io=$stdout)
63
+ # Don't log when logging is disabled
64
+ return if @level == :off
65
+
66
+ # Log when level permits it
67
+ io.puts(s) if (@level == :low or @level == level)
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,2 @@
1
+ require 'nanoc2/data_sources/filesystem'
2
+ require 'nanoc2/data_sources/filesystem_combined'
@@ -0,0 +1,707 @@
1
+ module Nanoc2::DataSources
2
+
3
+ # The filesystem data source is the default data source for a new nanoc
4
+ # site. It stores all data as files on the hard disk.
5
+ #
6
+ # None of the methods are documented in this file. See Nanoc2::DataSource for
7
+ # documentation on the overridden methods instead.
8
+ #
9
+ # = Pages
10
+ #
11
+ # The filesystem data source stores its pages in nested directories. Each
12
+ # directory represents a single page. The root directory is the 'content'
13
+ # directory.
14
+ #
15
+ # Every directory has a content file and a meta file. The content file
16
+ # contains the actual page content, while the meta file contains the page's
17
+ # metadata, formatted as YAML.
18
+ #
19
+ # Both content files and meta files are named after its parent directory
20
+ # (i.e. page). For example, a page named 'foo' will have a directorynamed
21
+ # 'foo', with e.g. a 'foo.markdown' content file and a 'foo.yaml' meta file.
22
+ #
23
+ # Content file extensions are not used for determining the filter that
24
+ # should be run; the meta file defines the list of filters. The meta file
25
+ # extension must always be 'yaml', though.
26
+ #
27
+ # Content files can also have the 'index' basename. Similarly, meta files
28
+ # can have the 'meta' basename. For example, a parent directory named 'foo'
29
+ # can have an 'index.txt' content file and a 'meta.yaml' meta file. This is
30
+ # to preserve backward compatibility.
31
+ #
32
+ # = Page defaults
33
+ #
34
+ # The page defaults are loaded from a YAML-formatted file named
35
+ # 'page_defaults.yaml' at the top level of the nanoc site directory. For
36
+ # backward compatibility, the file can also be named 'meta.yaml'.
37
+ #
38
+ # = Assets
39
+ #
40
+ # Assets are stored in the 'assets' directory (surprise!). The structure is
41
+ # very similar to the structure of the 'content' directory, so see the Pages
42
+ # section for details on how this directory is structured.
43
+ #
44
+ # = Asset defaults
45
+ #
46
+ # The asset defaults are stored similar to the way page defaults are stored,
47
+ # except that the asset defaults file is named 'asset_defaults.yaml'
48
+ # instead.
49
+ #
50
+ # = Layouts
51
+ #
52
+ # Layouts are stored as directories in the 'layouts' directory. Each layout
53
+ # contains a content file and a meta file. The content file contain the
54
+ # actual layout, and the meta file describes how the page should be handled
55
+ # (contains the filter that should be used).
56
+ #
57
+ # For backward compatibility, a layout can also be a single file in the
58
+ # 'layouts' directory. Such a layout cannot have any metadata; the filter
59
+ # used for this layout is determined from the file extension.
60
+ #
61
+ # = Templates
62
+ #
63
+ # Templates are located in the 'templates' directroy. Every template is a
64
+ # directory consisting of a content file and a meta file, both named after
65
+ # the template. This is very similar to the way pages are stored, except
66
+ # that templates cannot be nested.
67
+ #
68
+ # = Code
69
+ #
70
+ # Code is stored in '.rb' files in the 'lib' directory. Code can reside in
71
+ # sub-directories.
72
+ class Filesystem < Nanoc2::DataSource
73
+
74
+ PAGE_DEFAULTS_FILENAME = 'page_defaults.yaml'
75
+ PAGE_DEFAULTS_FILENAME_OLD = 'meta.yaml'
76
+ ASSET_DEFAULTS_FILENAME = 'asset_defaults.yaml'
77
+
78
+ ########## Attributes ##########
79
+
80
+ identifier :filesystem
81
+
82
+ ########## VCSes ##########
83
+
84
+ attr_accessor :vcs
85
+
86
+ def vcs
87
+ @vcs ||= Nanoc2::Extra::VCSes::Dummy.new
88
+ end
89
+
90
+ ########## Preparation ##########
91
+
92
+ def up # :nodoc:
93
+ end
94
+
95
+ def down # :nodoc:
96
+ end
97
+
98
+ def setup # :nodoc:
99
+ # Create directories
100
+ %w( assets content templates layouts lib ).each do |dir|
101
+ FileUtils.mkdir_p(dir)
102
+ vcs.add(dir)
103
+ end
104
+ end
105
+
106
+ def destroy # :nodoc:
107
+ # Remove files
108
+ vcs.remove(ASSET_DEFAULTS_FILENAME) if File.file?(ASSET_DEFAULTS_FILENAME)
109
+ vcs.remove(PAGE_DEFAULTS_FILENAME) if File.file?(PAGE_DEFAULTS_FILENAME)
110
+ vcs.remove(PAGE_DEFAULTS_FILENAME_OLD) if File.file?(PAGE_DEFAULTS_FILENAME_OLD)
111
+
112
+ # Remove directories
113
+ vcs.remove('assets')
114
+ vcs.remove('content')
115
+ vcs.remove('templates')
116
+ vcs.remove('layouts')
117
+ vcs.remove('lib')
118
+ end
119
+
120
+ def update # :nodoc:
121
+ update_page_defaults
122
+ update_pages
123
+ update_templates
124
+ end
125
+
126
+ ########## Pages ##########
127
+
128
+ def pages # :nodoc:
129
+ meta_filenames('content').map do |meta_filename|
130
+ # Read metadata
131
+ meta = YAML.load_file(meta_filename) || {}
132
+
133
+ if meta['is_draft']
134
+ # Skip drafts
135
+ nil
136
+ else
137
+ # Get content
138
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
139
+ content = File.read(content_filename)
140
+
141
+ # Get attributes
142
+ attributes = meta.merge(:file => Nanoc2::Extra::FileProxy.new(content_filename))
143
+
144
+ # Get path
145
+ path = meta_filename.sub(/^content/, '').sub(/[^\/]+\.yaml$/, '')
146
+
147
+ # Get modification times
148
+ meta_mtime = File.stat(meta_filename).mtime
149
+ content_mtime = File.stat(content_filename).mtime
150
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
151
+
152
+ # Create page object
153
+ Nanoc2::Page.new(content, attributes, path, mtime)
154
+ end
155
+ end.compact
156
+ end
157
+
158
+ def save_page(page) # :nodoc:
159
+ # Determine possible meta file paths
160
+ last_component = page.path.split('/')[-1]
161
+ meta_filename_worst = 'content' + page.path + 'index.yaml'
162
+ meta_filename_best = 'content' + page.path + (last_component || 'content') + '.yaml'
163
+
164
+ # Get existing path
165
+ existing_path = nil
166
+ existing_path = meta_filename_best if File.file?(meta_filename_best)
167
+ existing_path = meta_filename_worst if File.file?(meta_filename_worst)
168
+
169
+ if existing_path.nil?
170
+ # Get filenames
171
+ dir_path = 'content' + page.path
172
+ meta_filename = meta_filename_best
173
+ content_filename = 'content' + page.path + (last_component || 'content') + '.html'
174
+
175
+ # Notify
176
+ Nanoc2::NotificationCenter.post(:file_created, meta_filename)
177
+ Nanoc2::NotificationCenter.post(:file_created, content_filename)
178
+
179
+ # Create directories if necessary
180
+ FileUtils.mkdir_p(dir_path)
181
+ else
182
+ # Get filenames
183
+ meta_filename = existing_path
184
+ content_filename = content_filename_for_dir(File.dirname(existing_path))
185
+
186
+ # Notify
187
+ Nanoc2::NotificationCenter.post(:file_updated, meta_filename)
188
+ Nanoc2::NotificationCenter.post(:file_updated, content_filename)
189
+ end
190
+
191
+ # Write files
192
+ File.open(meta_filename, 'w') { |io| io.write(page.attributes.to_split_yaml) }
193
+ File.open(content_filename, 'w') { |io| io.write(page.content) }
194
+
195
+ # Add to working copy if possible
196
+ if existing_path.nil?
197
+ vcs.add(meta_filename)
198
+ vcs.add(content_filename)
199
+ end
200
+ end
201
+
202
+ def move_page(page, new_path) # :nodoc:
203
+ # TODO implement
204
+ end
205
+
206
+ def delete_page(page) # :nodoc:
207
+ # TODO implement
208
+ end
209
+
210
+ ########## Assets ##########
211
+
212
+ def assets # :nodoc:
213
+ meta_filenames('assets').map do |meta_filename|
214
+ # Read metadata
215
+ meta = YAML.load_file(meta_filename) || {}
216
+
217
+ # Get content file
218
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
219
+ content_file = Nanoc2::Extra::FileProxy.new(content_filename)
220
+
221
+ # Get attributes
222
+ attributes = { 'extension' => File.extname(content_filename)[1..-1] }.merge(meta)
223
+
224
+ # Get path
225
+ path = meta_filename.sub(/^assets/, '').sub(/[^\/]+\.yaml$/, '')
226
+
227
+ # Get modification times
228
+ meta_mtime = File.stat(meta_filename).mtime
229
+ content_mtime = File.stat(content_filename).mtime
230
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
231
+
232
+ # Create asset object
233
+ Nanoc2::Asset.new(content_file, attributes, path, mtime)
234
+ end
235
+ end
236
+
237
+ def save_asset(asset) # :nodoc:
238
+ # Determine meta file path
239
+ last_component = asset.path.split('/')[-1]
240
+ meta_filename = 'assets' + asset.path + last_component + '.yaml'
241
+
242
+ # Get existing path
243
+ existing_path = nil
244
+ existing_path = meta_filename_best if File.file?(meta_filename_best)
245
+ existing_path = meta_filename_worst if File.file?(meta_filename_worst)
246
+
247
+ if meta_filename.nil?
248
+ # Get filenames
249
+ dir_path = 'assets' + asset.path
250
+ content_filename = 'assets' + asset.path + last_component + '.dat'
251
+
252
+ # Notify
253
+ Nanoc2::NotificationCenter.post(:file_created, meta_filename)
254
+ Nanoc2::NotificationCenter.post(:file_created, content_filename)
255
+
256
+ # Create directories if necessary
257
+ FileUtils.mkdir_p(dir_path)
258
+ else
259
+ # Get filenames
260
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
261
+
262
+ # Notify
263
+ Nanoc2::NotificationCenter.post(:file_updated, meta_filename)
264
+ Nanoc2::NotificationCenter.post(:file_updated, content_filename)
265
+ end
266
+
267
+ # Write files
268
+ File.open(meta_filename, 'w') { |io| io.write(asset.attributes.to_split_yaml) }
269
+ File.open(content_filename, 'w') { }
270
+
271
+ # Add to working copy if possible
272
+ if meta_filename.nil?
273
+ vcs.add(meta_filename)
274
+ vcs.add(content_filename)
275
+ end
276
+ end
277
+
278
+ def move_asset(asset, new_path) # :nodoc:
279
+ # TODO implement
280
+ end
281
+
282
+ def delete_asset(asset) # :nodoc:
283
+ # TODO implement
284
+ end
285
+
286
+ ########## Page Defaults ##########
287
+
288
+ def page_defaults # :nodoc:
289
+ # Get attributes
290
+ filename = File.file?(PAGE_DEFAULTS_FILENAME) ? PAGE_DEFAULTS_FILENAME : PAGE_DEFAULTS_FILENAME_OLD
291
+ attributes = YAML.load_file(filename) || {}
292
+
293
+ # Get mtime
294
+ mtime = File.stat(filename).mtime
295
+
296
+ # Build page defaults
297
+ Nanoc2::PageDefaults.new(attributes, mtime)
298
+ end
299
+
300
+ def save_page_defaults(page_defaults) # :nodoc:
301
+ # Notify
302
+ if File.file?(PAGE_DEFAULTS_FILENAME)
303
+ filename = PAGE_DEFAULTS_FILENAME
304
+ created = false
305
+ Nanoc2::NotificationCenter.post(:file_updated, filename)
306
+ elsif File.file?(PAGE_DEFAULTS_FILENAME_OLD)
307
+ filename = PAGE_DEFAULTS_FILENAME_OLD
308
+ created = false
309
+ Nanoc2::NotificationCenter.post(:file_updated, filename)
310
+ else
311
+ filename = PAGE_DEFAULTS_FILENAME
312
+ created = true
313
+ Nanoc2::NotificationCenter.post(:file_created, filename)
314
+ end
315
+
316
+ # Write
317
+ File.open(filename, 'w') do |io|
318
+ io.write(page_defaults.attributes.to_split_yaml)
319
+ end
320
+
321
+ # Add to working copy if possible
322
+ vcs.add(filename) if created
323
+ end
324
+
325
+ ########## Asset Defaults ##########
326
+
327
+ def asset_defaults # :nodoc:
328
+ if File.file?(ASSET_DEFAULTS_FILENAME)
329
+ # Get attributes
330
+ attributes = YAML.load_file(ASSET_DEFAULTS_FILENAME) || {}
331
+
332
+ # Get mtime
333
+ mtime = File.stat(ASSET_DEFAULTS_FILENAME).mtime
334
+
335
+ # Build asset defaults
336
+ Nanoc2::AssetDefaults.new(attributes, mtime)
337
+ else
338
+ Nanoc2::AssetDefaults.new({})
339
+ end
340
+ end
341
+
342
+ def save_asset_defaults(asset_defaults) # :nodoc:
343
+ # Notify
344
+ if File.file?(ASSET_DEFAULTS_FILENAME)
345
+ Nanoc2::NotificationCenter.post(:file_updated, ASSET_DEFAULTS_FILENAME)
346
+ created = false
347
+ else
348
+ Nanoc2::NotificationCenter.post(:file_created, ASSET_DEFAULTS_FILENAME)
349
+ created = true
350
+ end
351
+
352
+ # Write
353
+ File.open(ASSET_DEFAULTS_FILENAME, 'w') do |io|
354
+ io.write(asset_defaults.attributes.to_split_yaml)
355
+ end
356
+
357
+ # Add to working copy if possible
358
+ vcs.add(ASSET_DEFAULTS_FILENAME) if created
359
+ end
360
+
361
+ ########## Layouts ##########
362
+
363
+ def layouts # :nodoc:
364
+ # Determine what layout directory structure is being used
365
+ is_old_school = (Dir['layouts/*'].select { |f| File.file?(f) }.size > 0)
366
+
367
+ if is_old_school
368
+ # Warn about deprecation
369
+ warn(
370
+ 'DEPRECATION WARNING: nanoc 2.1 changes the way layouts are ' +
371
+ 'stored. Future versions will not support these outdated sites. ' +
372
+ 'To update your site, issue \'nanoc update\'.'
373
+ )
374
+
375
+ Dir[File.join('layouts', '*')].reject { |f| f =~ /~$/ }.map do |filename|
376
+ # Get content
377
+ content = File.read(filename)
378
+
379
+ # Get attributes
380
+ attributes = { :extension => File.extname(filename)}
381
+
382
+ # Get path
383
+ path = File.basename(filename, attributes[:extension])
384
+
385
+ # Get modification time
386
+ mtime = File.stat(filename).mtime
387
+
388
+ # Create layout object
389
+ Nanoc2::Layout.new(content, attributes, path, mtime)
390
+ end
391
+ else
392
+ meta_filenames('layouts').map do |meta_filename|
393
+ # Get content
394
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
395
+ content = File.read(content_filename)
396
+
397
+ # Get attributes
398
+ attributes = YAML.load_file(meta_filename) || {}
399
+
400
+ # Get path
401
+ path = meta_filename.sub(/^layouts\//, '').sub(/\/[^\/]+\.yaml$/, '')
402
+
403
+ # Get modification times
404
+ meta_mtime = File.stat(meta_filename).mtime
405
+ content_mtime = File.stat(content_filename).mtime
406
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
407
+
408
+ # Create layout object
409
+ Nanoc2::Layout.new(content, attributes, path, mtime)
410
+ end
411
+ end
412
+ end
413
+
414
+ def save_layout(layout) # :nodoc:
415
+ # Determine what layout directory structure is being used
416
+ is_old_school = (Dir['layouts/*'].select { |f| File.file?(f) }.size > 0)
417
+ error_outdated if is_old_school
418
+
419
+ # Get paths
420
+ last_component = layout.path.split('/')[-1]
421
+ dir_path = 'layouts' + layout.path
422
+ meta_filename = dir_path + last_component + '.yaml'
423
+ content_filename = Dir[dir_path + last_component + '.*'][0]
424
+
425
+ if File.file?(meta_filename)
426
+ created = false
427
+
428
+ # Notify
429
+ Nanoc2::NotificationCenter.post(:file_updated, meta_filename)
430
+ Nanoc2::NotificationCenter.post(:file_updated, content_filename)
431
+ else
432
+ created = true
433
+
434
+ # Create dir
435
+ FileUtils.mkdir_p(dir_path)
436
+
437
+ # Get content filename
438
+ content_filename = dir_path + last_component + '.html'
439
+
440
+ # Notify
441
+ Nanoc2::NotificationCenter.post(:file_created, meta_filename)
442
+ Nanoc2::NotificationCenter.post(:file_created, content_filename)
443
+ end
444
+
445
+ # Write files
446
+ File.open(meta_filename, 'w') { |io| io.write(layout.attributes.to_split_yaml) }
447
+ File.open(content_filename, 'w') { |io| io.write(layout.content) }
448
+
449
+ # Add to working copy if possible
450
+ if created
451
+ vcs.add(meta_filename)
452
+ vcs.add(content_filename)
453
+ end
454
+ end
455
+
456
+ def move_layout(layout, new_path) # :nodoc:
457
+ # TODO implement
458
+ end
459
+
460
+ def delete_layout(layout) # :nodoc:
461
+ # TODO implement
462
+ end
463
+
464
+ ########## Templates ##########
465
+
466
+ def templates # :nodoc:
467
+ meta_filenames('templates').map do |meta_filename|
468
+ # Get name
469
+ name = meta_filename.sub(/^templates\/(.*)\/[^\/]+\.yaml$/, '\1')
470
+
471
+ # Get content
472
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
473
+ content = File.read(content_filename)
474
+
475
+ # Get attributes
476
+ attributes = YAML.load_file(meta_filename) || {}
477
+
478
+ # Build template
479
+ Nanoc2::Template.new(content, attributes, name)
480
+ end
481
+ end
482
+
483
+ def save_template(template) # :nodoc:
484
+ # Determine possible meta file paths
485
+ meta_filename_worst = 'templates/' + template.name + '/index.yaml'
486
+ meta_filename_best = 'templates/' + template.name + '/' + template.name + '.yaml'
487
+
488
+ # Get existing path
489
+ existing_path = nil
490
+ existing_path = meta_filename_best if File.file?(meta_filename_best)
491
+ existing_path = meta_filename_worst if File.file?(meta_filename_worst)
492
+
493
+ if existing_path.nil?
494
+ # Get filenames
495
+ dir_path = 'templates/' + template.name
496
+ meta_filename = meta_filename_best
497
+ content_filename = 'templates/' + template.name + '/' + template.name + '.html'
498
+
499
+ # Notify
500
+ Nanoc2::NotificationCenter.post(:file_created, meta_filename)
501
+ Nanoc2::NotificationCenter.post(:file_created, content_filename)
502
+
503
+ # Create directories if necessary
504
+ FileUtils.mkdir_p(dir_path)
505
+ else
506
+ # Get filenames
507
+ meta_filename = existing_path
508
+ content_filename = content_filename_for_dir(File.dirname(existing_path))
509
+
510
+ # Notify
511
+ Nanoc2::NotificationCenter.post(:file_updated, meta_filename)
512
+ Nanoc2::NotificationCenter.post(:file_updated, content_filename)
513
+ end
514
+
515
+ # Write files
516
+ File.open(meta_filename, 'w') { |io| io.write(template.page_attributes.to_split_yaml) }
517
+ File.open(content_filename, 'w') { |io| io.write(template.page_content) }
518
+
519
+ # Add to working copy if possible
520
+ if existing_path.nil?
521
+ vcs.add(meta_filename)
522
+ vcs.add(content_filename)
523
+ end
524
+ end
525
+
526
+ def move_template(template, new_name) # :nodoc:
527
+ # TODO implement
528
+ end
529
+
530
+ def delete_template(template) # :nodoc:
531
+ # TODO implement
532
+ end
533
+
534
+ ########## Code ##########
535
+
536
+ def code # :nodoc:
537
+ # Get files
538
+ filenames = Dir['lib/**/*.rb'].sort
539
+
540
+ # Get data
541
+ data = filenames.map { |filename| File.read(filename) + "\n" }.join('')
542
+
543
+ # Get modification time
544
+ mtimes = filenames.map { |filename| File.stat(filename).mtime }
545
+ mtime = mtimes.inject { |memo, mtime| memo > mtime ? mtime : memo }
546
+
547
+ # Build code
548
+ Nanoc2::Code.new(data, mtime)
549
+ end
550
+
551
+ def save_code(code) # :nodoc:
552
+ # Check whether code existed
553
+ existed = File.file?('lib/default.rb')
554
+
555
+ # Remove all existing code files
556
+ Dir['lib/**/*.rb'].each do |file|
557
+ vcs.remove(file) unless file == 'lib/default.rb'
558
+ end
559
+
560
+ # Notify
561
+ if existed
562
+ Nanoc2::NotificationCenter.post(:file_updated, 'lib/default.rb')
563
+ else
564
+ Nanoc2::NotificationCenter.post(:file_created, 'lib/default.rb')
565
+ end
566
+
567
+ # Write new code
568
+ File.open('lib/default.rb', 'w') do |io|
569
+ io.write(code.data)
570
+ end
571
+
572
+ # Add to working copy if possible
573
+ vcs.add('lib/default.rb') unless existed
574
+ end
575
+
576
+ private
577
+
578
+ ########## Custom functions ##########
579
+
580
+ # Returns the list of all meta files in the given base directory as well
581
+ # as its subdirectories.
582
+ def meta_filenames(base)
583
+ # Find all possible meta file names
584
+ filenames = Dir[base + '/**/*.yaml']
585
+
586
+ # Filter out invalid meta files
587
+ good_filenames = []
588
+ bad_filenames = []
589
+ filenames.each do |filename|
590
+ if filename =~ /meta\.yaml$/ or filename =~ /([^\/]+)\/\1\.yaml$/
591
+ good_filenames << filename
592
+ else
593
+ bad_filenames << filename
594
+ end
595
+ end
596
+
597
+ # Warn about bad filenames
598
+ unless bad_filenames.empty?
599
+ raise RuntimeError.new(
600
+ "The following files appear to be meta files, " +
601
+ "but have an invalid name:\n - " +
602
+ bad_filenames.join("\n - ")
603
+ )
604
+ end
605
+
606
+ good_filenames
607
+ end
608
+
609
+ # Returns the filename of the content file in the given directory,
610
+ # ignoring any unwanted files (files that end with '~', '.orig', '.rej' or
611
+ # '.bak')
612
+ def content_filename_for_dir(dir)
613
+ # Find all files
614
+ filename_glob_1 = dir.sub(/([^\/]+)$/, '\1/\1.*')
615
+ filename_glob_2 = dir.sub(/([^\/]+)$/, '\1/index.*')
616
+ filenames = (Dir[filename_glob_1] + Dir[filename_glob_2]).uniq
617
+
618
+ # Reject meta files
619
+ filenames.reject! { |f| f =~ /\.yaml$/ }
620
+
621
+ # Reject backups
622
+ filenames.reject! { |f| f =~ /(~|\.orig|\.rej|\.bak)$/ }
623
+
624
+ # Make sure there is only one content file
625
+ if filenames.size != 1
626
+ raise RuntimeError.new(
627
+ "Expected 1 content file in #{dir} but found #{filenames.size}"
628
+ )
629
+ end
630
+
631
+ # Return content filename
632
+ filenames.first
633
+ end
634
+
635
+ # Raises an "outdated data format" error.
636
+ def error_outdated
637
+ raise RuntimeError.new(
638
+ 'This site\'s data is stored in an old format and must be updated. ' +
639
+ 'To do so, issue the \'nanoc update\' command. For help on ' +
640
+ 'updating a site\'s data, issue \'nanoc help update\'.'
641
+ )
642
+ end
643
+
644
+ # Updated outdated page defaults (renames page defaults file)
645
+ def update_page_defaults
646
+ return unless File.file?(PAGE_DEFAULTS_FILENAME_OLD)
647
+
648
+ vcs.move(PAGE_DEFAULTS_FILENAME_OLD, PAGE_DEFAULTS_FILENAME)
649
+ end
650
+
651
+ # Updates outdated pages (both content and meta file names).
652
+ def update_pages
653
+ # Update content files
654
+ # content/foo/bar/baz/index.ext -> content/foo/bar/baz/baz.ext
655
+ Dir['content/**/index.*'].select { |f| File.file?(f) }.each do |old_filename|
656
+ # Determine new name
657
+ if old_filename =~ /^content\/index\./
658
+ new_filename = old_filename.sub(/^content\/index\./, 'content/content.')
659
+ else
660
+ new_filename = old_filename.sub(/([^\/]+)\/index\.([^\/]+)$/, '\1/\1.\2')
661
+ end
662
+
663
+ # Move
664
+ vcs.move(old_filename, new_filename)
665
+ end
666
+
667
+ # Update meta files
668
+ # content/foo/bar/baz/meta.yaml -> content/foo/bar/baz/baz.yaml
669
+ Dir['content/**/meta.yaml'].select { |f| File.file?(f) }.each do |old_filename|
670
+ # Determine new name
671
+ if old_filename == 'content/meta.yaml'
672
+ new_filename = 'content/content.yaml'
673
+ else
674
+ new_filename = old_filename.sub(/([^\/]+)\/meta.yaml$/, '\1/\1.yaml')
675
+ end
676
+
677
+ # Move
678
+ vcs.move(old_filename, new_filename)
679
+ end
680
+ end
681
+
682
+ # Updates outdated templates (both content and meta file names).
683
+ def update_templates
684
+ # Update content files
685
+ # templates/foo/index.ext -> templates/foo/foo.ext
686
+ Dir['templates/**/index.*'].select { |f| File.file?(f) }.each do |old_filename|
687
+ # Determine new name
688
+ new_filename = old_filename.sub(/([^\/]+)\/index\.([^\/]+)$/, '\1/\1.\2')
689
+
690
+ # Move
691
+ vcs.move(old_filename, new_filename)
692
+ end
693
+
694
+ # Update meta files
695
+ # templates/foo/meta.yaml -> templates/foo/foo.yaml
696
+ Dir['templates/**/meta.yaml'].select { |f| File.file?(f) }.each do |old_filename|
697
+ # Determine new name
698
+ new_filename = old_filename.sub(/([^\/]+)\/meta.yaml$/, '\1/\1.yaml')
699
+
700
+ # Move
701
+ vcs.move(old_filename, new_filename)
702
+ end
703
+ end
704
+
705
+ end
706
+
707
+ end