nanoc 2.0.4 → 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 +31 -1
  2. data/LICENSE +1 -1
  3. data/README +63 -3
  4. data/Rakefile +59 -12
  5. data/bin/nanoc +7 -199
  6. data/lib/nanoc.rb +83 -12
  7. data/lib/nanoc/base/asset.rb +113 -0
  8. data/lib/nanoc/base/asset_defaults.rb +21 -0
  9. data/lib/nanoc/base/asset_rep.rb +277 -0
  10. data/lib/nanoc/base/binary_filter.rb +44 -0
  11. data/lib/nanoc/base/code.rb +41 -0
  12. data/lib/nanoc/base/compiler.rb +46 -34
  13. data/lib/nanoc/base/core_ext/hash.rb +51 -7
  14. data/lib/nanoc/base/core_ext/string.rb +8 -0
  15. data/lib/nanoc/base/data_source.rb +253 -20
  16. data/lib/nanoc/base/defaults.rb +30 -0
  17. data/lib/nanoc/base/enhancements.rb +9 -84
  18. data/lib/nanoc/base/filter.rb +109 -6
  19. data/lib/nanoc/base/layout.rb +91 -0
  20. data/lib/nanoc/base/notification_center.rb +66 -0
  21. data/lib/nanoc/base/page.rb +94 -126
  22. data/lib/nanoc/base/page_defaults.rb +20 -0
  23. data/lib/nanoc/base/page_rep.rb +318 -0
  24. data/lib/nanoc/base/plugin.rb +57 -9
  25. data/lib/nanoc/base/proxies/asset_proxy.rb +29 -0
  26. data/lib/nanoc/base/proxies/asset_rep_proxy.rb +26 -0
  27. data/lib/nanoc/base/proxies/layout_proxy.rb +25 -0
  28. data/lib/nanoc/base/proxies/page_proxy.rb +35 -0
  29. data/lib/nanoc/base/proxies/page_rep_proxy.rb +28 -0
  30. data/lib/nanoc/base/proxy.rb +37 -0
  31. data/lib/nanoc/base/router.rb +72 -0
  32. data/lib/nanoc/base/site.rb +219 -88
  33. data/lib/nanoc/base/template.rb +64 -0
  34. data/lib/nanoc/binary_filters/image_science_thumbnail.rb +28 -0
  35. data/lib/nanoc/cli.rb +1 -0
  36. data/lib/nanoc/cli/base.rb +219 -0
  37. data/lib/nanoc/cli/cli.rb +16 -0
  38. data/lib/nanoc/cli/command.rb +105 -0
  39. data/lib/nanoc/cli/commands/autocompile.rb +80 -0
  40. data/lib/nanoc/cli/commands/compile.rb +273 -0
  41. data/lib/nanoc/cli/commands/create_layout.rb +85 -0
  42. data/lib/nanoc/cli/commands/create_page.rb +85 -0
  43. data/lib/nanoc/cli/commands/create_site.rb +327 -0
  44. data/lib/nanoc/cli/commands/create_template.rb +76 -0
  45. data/lib/nanoc/cli/commands/help.rb +69 -0
  46. data/lib/nanoc/cli/commands/info.rb +114 -0
  47. data/lib/nanoc/cli/commands/switch.rb +141 -0
  48. data/lib/nanoc/cli/commands/update.rb +91 -0
  49. data/lib/nanoc/cli/ext.rb +37 -0
  50. data/lib/nanoc/cli/logger.rb +66 -0
  51. data/lib/nanoc/cli/option_parser.rb +168 -0
  52. data/lib/nanoc/data_sources/filesystem.rb +645 -224
  53. data/lib/nanoc/data_sources/filesystem_combined.rb +495 -0
  54. data/lib/nanoc/extra/auto_compiler.rb +265 -0
  55. data/lib/nanoc/extra/context.rb +22 -0
  56. data/lib/nanoc/extra/core_ext/hash.rb +54 -0
  57. data/lib/nanoc/extra/core_ext/time.rb +13 -0
  58. data/lib/nanoc/extra/file_proxy.rb +29 -0
  59. data/lib/nanoc/extra/vcs.rb +48 -0
  60. data/lib/nanoc/extra/vcses/bazaar.rb +21 -0
  61. data/lib/nanoc/extra/vcses/dummy.rb +20 -0
  62. data/lib/nanoc/extra/vcses/git.rb +21 -0
  63. data/lib/nanoc/extra/vcses/mercurial.rb +21 -0
  64. data/lib/nanoc/extra/vcses/subversion.rb +21 -0
  65. data/lib/nanoc/filters/bluecloth.rb +13 -0
  66. data/lib/nanoc/filters/erb.rb +6 -22
  67. data/lib/nanoc/filters/erubis.rb +14 -0
  68. data/lib/nanoc/filters/haml.rb +7 -23
  69. data/lib/nanoc/filters/markaby.rb +5 -5
  70. data/lib/nanoc/filters/maruku.rb +14 -0
  71. data/lib/nanoc/filters/old.rb +19 -0
  72. data/lib/nanoc/filters/rdiscount.rb +13 -0
  73. data/lib/nanoc/filters/rdoc.rb +5 -4
  74. data/lib/nanoc/filters/redcloth.rb +14 -0
  75. data/lib/nanoc/filters/rubypants.rb +14 -0
  76. data/lib/nanoc/filters/sass.rb +13 -0
  77. data/lib/nanoc/helpers/blogging.rb +170 -0
  78. data/lib/nanoc/helpers/capturing.rb +59 -0
  79. data/lib/nanoc/helpers/html_escape.rb +23 -0
  80. data/lib/nanoc/helpers/link_to.rb +69 -0
  81. data/lib/nanoc/helpers/render.rb +47 -0
  82. data/lib/nanoc/helpers/tagging.rb +52 -0
  83. data/lib/nanoc/helpers/xml_sitemap.rb +58 -0
  84. data/lib/nanoc/routers/default.rb +54 -0
  85. data/lib/nanoc/routers/no_dirs.rb +66 -0
  86. data/lib/nanoc/routers/versioned.rb +79 -0
  87. metadata +112 -22
  88. data/lib/nanoc/base/auto_compiler.rb +0 -132
  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 -38
  99. data/lib/nanoc/layout_processors/markaby.rb +0 -16
@@ -0,0 +1,37 @@
1
+ class String
2
+
3
+ # Word-wraps and indents the string.
4
+ #
5
+ # +width+:: The maximal width of each line. This also includes indentation,
6
+ # i.e. the actual maximal width of the text is width-indentation.
7
+ #
8
+ # +indentation+:: The number of spaces to indent each wrapped line.
9
+ def wrap_and_indent(width, indentation)
10
+ # Split into paragraphs
11
+ paragraphs = self.split("\n").map { |p| p.strip }.reject { |p| p == '' }
12
+
13
+ # Wrap and indent each paragraph
14
+ paragraphs.map do |paragraph|
15
+ # Initialize
16
+ lines = []
17
+ line = ''
18
+
19
+ # Split into words
20
+ paragraph.split(/\s/).each do |word|
21
+ # Begin new line if it's too long
22
+ if (line + ' ' + word).length >= width
23
+ lines << line
24
+ line = ''
25
+ end
26
+
27
+ # Add word to line
28
+ line += (line == '' ? '' : ' ' ) + word
29
+ end
30
+ lines << line
31
+
32
+ # Join lines
33
+ lines.map { |l| ' '*indentation + l }.join("\n")
34
+ end.join("\n\n")
35
+ end
36
+
37
+ end
@@ -0,0 +1,66 @@
1
+ require 'singleton'
2
+
3
+ module Nanoc::CLI
4
+
5
+ # Nanoc::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
+ }
15
+
16
+ include Singleton
17
+
18
+ # The log leve, which can be :high, :low or :off (which will log all
19
+ # messages, only high-priority messages, or no messages at all,
20
+ # respectively).
21
+ attr_accessor :level
22
+
23
+ def initialize # :nodoc:
24
+ @level = :high
25
+ end
26
+
27
+ # Logs a file-related action.
28
+ #
29
+ # +level+:: The importance of this action. Can be :high or :low.
30
+ #
31
+ # +action+:: The kind of file action. Can be :create, :update or
32
+ # :identical.
33
+ #
34
+ # +path+:: The path to the file the action was performed on.
35
+ def file(level, action, path, duration=nil)
36
+ log(
37
+ level,
38
+ '%s%12s%s %s%s' % [
39
+ ACTION_COLORS[action.to_sym],
40
+ action,
41
+ "\e[0m",
42
+ duration.nil? ? '' : "[%2.2fs] " % [ duration ],
43
+ path
44
+ ]
45
+ )
46
+ end
47
+
48
+ # Logs a message.
49
+ #
50
+ # +level+:: The importance of this message. Can be :high or :low.
51
+ #
52
+ # +s+:: The message to be logged.
53
+ #
54
+ # +io+:: The IO instance to which the message will be written. Defaults to
55
+ # standard output.
56
+ def log(level, s, io=$stdout)
57
+ # Don't log when logging is disabled
58
+ return if @level == :off
59
+
60
+ # Log when level permits it
61
+ io.puts(s) if (@level == :low or @level == level)
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,168 @@
1
+ module Nanoc::CLI
2
+
3
+ # Nanoc::CLI::OptionParser is used for parsing commandline options.
4
+ class OptionParser
5
+
6
+ # Error that will be raised when an unknown option is encountered.
7
+ class IllegalOptionError < RuntimeError ; end
8
+
9
+ # Error that will be raised when an option without argument is
10
+ # encountered.
11
+ class OptionRequiresAnArgumentError < RuntimeError ; end
12
+
13
+ # Parses the commandline arguments in +arguments_and_options+, using the
14
+ # commandline option definitions in +definitions+.
15
+ #
16
+ # +arguments_and_options+ is an array of commandline arguments and
17
+ # options. This will usually be +ARGV+.
18
+ #
19
+ # +definitions+ contains a list of hashes defining which options are
20
+ # allowed and how they will be handled. Such a hash has three keys:
21
+ #
22
+ # :short:: The short name of the option, e.g. +a+. Do not include the '-'
23
+ # prefix.
24
+ #
25
+ # :long:: The long name of the option, e.g. +all+. Do not include the '--'
26
+ # prefix.
27
+ #
28
+ # :argument:: Whether this option's argument is required (:required) or
29
+ # forbidden (:forbidden).
30
+ #
31
+ # A sample array of definition hashes could look like this:
32
+ #
33
+ # [
34
+ # { :short => 'a', :long => 'all', :argument => :forbidden },
35
+ # { :short => 'p', :long => 'port', :argument => :required },
36
+ # ]
37
+ #
38
+ # During parsing, two errors can be raised:
39
+ #
40
+ # IllegalOptionError:: An unrecognised option was encountered, i.e. an
41
+ # option that is not present in the list of option
42
+ # definitions.
43
+ #
44
+ # OptionRequiresAnArgumentError:: An option was found that did not have a
45
+ # value, even though this value was
46
+ # required.
47
+ #
48
+ # What will be returned, is a hash with two keys, :arguments and :options.
49
+ # The :arguments value contains a list of arguments, and the :options
50
+ # value contains a hash with key-value pairs for each option. Options
51
+ # without values will have a +nil+ value instead.
52
+ #
53
+ # For example, the following commandline options (which should not be
54
+ # passed as a string, but as an array of strings):
55
+ #
56
+ # foo bar -xyz -a hiss --level 50 --father=ani -n luke squeak
57
+ #
58
+ # with the following option definitions:
59
+ #
60
+ # [
61
+ # { :short => 'x', :long => 'xxx', :argument => :forbidden },
62
+ # { :short => 'y', :long => 'yyy', :argument => :forbidden },
63
+ # { :short => 'z', :long => 'zzz', :argument => :forbidden },
64
+ # { :short => 'a', :long => 'all', :argument => :forbidden },
65
+ # { :short => 'l', :long => 'level', :argument => :required },
66
+ # { :short => 'f', :long => 'father', :argument => :required },
67
+ # { :short => 'n', :long => 'name', :argument => :required }
68
+ # ]
69
+ #
70
+ # will be translated into:
71
+ #
72
+ # {
73
+ # :arguments => [ 'foo', 'bar', 'hiss', 'squeak' ],
74
+ # :options => {
75
+ # :xxx => nil,
76
+ # :yyy => nil,
77
+ # :zzz => nil,
78
+ # :all => nil,
79
+ # :level => '50',
80
+ # :father => 'ani',
81
+ # :name => 'luke'
82
+ # }
83
+ # }
84
+ def self.parse(arguments_and_options, definitions)
85
+ # Don't touch original argument
86
+ unprocessed_arguments_and_options = arguments_and_options.dup
87
+
88
+ # Initialize
89
+ arguments = []
90
+ options = {}
91
+
92
+ # Determines whether we've passed the '--' marker or not
93
+ no_more_options = false
94
+
95
+ loop do
96
+ # Get next item
97
+ e = unprocessed_arguments_and_options.shift
98
+ break if e.nil?
99
+
100
+ # Handle end-of-options marker
101
+ if e == '--'
102
+ no_more_options = true
103
+ # Handle incomplete options
104
+ elsif e =~ /^--./ and !no_more_options
105
+ # Get option key, and option value if included
106
+ if e =~ /^--([^=]+)=(.+)$/
107
+ option_key = $1
108
+ option_value = $2
109
+ else
110
+ option_key = e[2..-1]
111
+ option_value = nil
112
+ end
113
+
114
+ # Find definition
115
+ definition = definitions.find { |d| d[:long] == option_key }
116
+ raise IllegalOptionError.new(option_key) if definition.nil?
117
+
118
+ if definition[:argument] == :required
119
+ # Get option value if necessary
120
+ if option_value.nil?
121
+ option_value = unprocessed_arguments_and_options.shift
122
+ raise OptionRequiresAnArgumentError.new(option_key) if option_value.nil?
123
+ end
124
+
125
+ # Store option
126
+ options[definition[:long].to_sym] = option_value
127
+ else
128
+ # Store option
129
+ options[definition[:long].to_sym] = nil
130
+ end
131
+ # Handle -xyz options
132
+ elsif e =~ /^-./ and !no_more_options
133
+ # Get option keys
134
+ option_keys = e[1..-1].scan(/./)
135
+
136
+ # For each key
137
+ option_keys.each do |option_key|
138
+ # Find definition
139
+ definition = definitions.find { |d| d[:short] == option_key }
140
+ raise IllegalOptionError.new(option_key) if definition.nil?
141
+
142
+ if option_keys.length > 1 and definition[:argument] == :required
143
+ # This is a combined option and it requires an argument, so complain
144
+ raise OptionRequiresAnArgumentError.new(option_key) if option_value.nil?
145
+ elsif definition[:argument] == :required
146
+ # Get option value
147
+ option_value = unprocessed_arguments_and_options.shift
148
+ raise OptionRequiresAnArgumentError.new(option_key) if option_value.nil?
149
+
150
+ # Store option
151
+ options[definition[:long].to_sym] = option_value
152
+ else
153
+ # Store option
154
+ options[definition[:long].to_sym] = nil
155
+ end
156
+ end
157
+ # Handle normal arguments
158
+ else
159
+ arguments << e
160
+ end
161
+ end
162
+
163
+ { :options => options, :arguments => arguments }
164
+ end
165
+
166
+ end
167
+
168
+ end
@@ -1,276 +1,586 @@
1
- module Nanoc::DataSource::Filesystem
1
+ module Nanoc::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 Nanoc::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 < Nanoc::DataSource
73
+
74
+ PAGE_DEFAULTS_FILENAME = 'page_defaults.yaml'
75
+ PAGE_DEFAULTS_FILENAME_OLD = 'meta.yaml'
76
+ ASSET_DEFAULTS_FILENAME = 'asset_defaults.yaml'
2
77
 
3
- class FileProxy
78
+ ########## Attributes ##########
79
+
80
+ identifier :filesystem
4
81
 
5
- instance_methods.each { |m| undef_method m unless m =~ /^__/ }
82
+ ########## VCSes ##########
6
83
 
7
- def initialize(path)
8
- @path = path
84
+ attr_accessor :vcs
85
+
86
+ def vcs
87
+ @vcs ||= Nanoc::Extra::VCSes::Dummy.new
9
88
  end
10
89
 
11
- def method_missing(sym, *args, &block)
12
- File.new(@path).__send__(sym, *args, &block)
90
+ ########## Preparation ##########
91
+
92
+ def up # :nodoc:
13
93
  end
14
94
 
15
- end
95
+ def down # :nodoc:
96
+ end
16
97
 
17
- class FilesystemDataSource < Nanoc::DataSource
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
18
105
 
19
- ########## Attributes ##########
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
20
119
 
21
- identifier :filesystem
120
+ def update # :nodoc:
121
+ update_page_defaults
122
+ update_pages
123
+ update_layouts
124
+ update_templates
125
+ end
22
126
 
23
- ########## Preparation ##########
127
+ ########## Pages ##########
128
+
129
+ def pages # :nodoc:
130
+ meta_filenames('content').map do |meta_filename|
131
+ # Read metadata
132
+ meta = YAML.load_file(meta_filename) || {}
133
+
134
+ if meta['is_draft']
135
+ # Skip drafts
136
+ nil
137
+ else
138
+ # Get content
139
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
140
+ content = File.read(content_filename)
141
+
142
+ # Get attributes
143
+ attributes = meta.merge(:file => Nanoc::Extra::FileProxy.new(content_filename))
144
+
145
+ # Get path
146
+ path = meta_filename.sub(/^content/, '').sub(/[^\/]+\.yaml$/, '')
147
+
148
+ # Get modification times
149
+ meta_mtime = File.stat(meta_filename).mtime
150
+ content_mtime = File.stat(content_filename).mtime
151
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
152
+
153
+ # Create page object
154
+ Nanoc::Page.new(content, attributes, path, mtime)
155
+ end
156
+ end.compact
157
+ end
158
+
159
+ def save_page(page) # :nodoc:
160
+ # Determine possible meta file paths
161
+ last_component = page.path.split('/')[-1]
162
+ meta_filename_worst = 'content' + page.path + 'index.yaml'
163
+ meta_filename_best = 'content' + page.path + (last_component || 'content') + '.yaml'
164
+
165
+ # Get existing path
166
+ existing_path = nil
167
+ existing_path = meta_filename_best if File.file?(meta_filename_best)
168
+ existing_path = meta_filename_worst if File.file?(meta_filename_worst)
169
+
170
+ if existing_path.nil?
171
+ # Get filenames
172
+ dir_path = 'content' + page.path
173
+ meta_filename = meta_filename_best
174
+ content_filename = 'content' + page.path + (last_component || 'content') + '.html'
175
+
176
+ # Notify
177
+ Nanoc::NotificationCenter.post(:file_created, meta_filename)
178
+ Nanoc::NotificationCenter.post(:file_created, content_filename)
179
+
180
+ # Create directories if necessary
181
+ FileUtils.mkdir_p(dir_path)
182
+ else
183
+ # Get filenames
184
+ meta_filename = existing_path
185
+ content_filename = content_filename_for_dir(File.dirname(existing_path))
186
+
187
+ # Notify
188
+ Nanoc::NotificationCenter.post(:file_updated, meta_filename)
189
+ Nanoc::NotificationCenter.post(:file_updated, content_filename)
190
+ end
191
+
192
+ # Write files
193
+ File.open(meta_filename, 'w') { |io| io.write(page.attributes.to_split_yaml) }
194
+ File.open(content_filename, 'w') { |io| io.write(page.content) }
195
+
196
+ # Add to working copy if possible
197
+ if existing_path.nil?
198
+ vcs.add(meta_filename)
199
+ vcs.add(content_filename)
200
+ end
201
+ end
202
+
203
+ def move_page(page, new_path) # :nodoc:
204
+ # TODO implement
205
+ end
24
206
 
25
- def up
207
+ def delete_page(page) # :nodoc:
208
+ # TODO implement
26
209
  end
27
210
 
28
- def down
211
+ ########## Assets ##########
212
+
213
+ def assets # :nodoc:
214
+ meta_filenames('assets').map do |meta_filename|
215
+ # Read metadata
216
+ meta = YAML.load_file(meta_filename) || {}
217
+
218
+ # Get content file
219
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
220
+ content_file = File.new(content_filename)
221
+
222
+ # Get attributes
223
+ attributes = meta.merge(:extension => File.extname(content_filename)[1..-1])
224
+
225
+ # Get path
226
+ path = meta_filename.sub(/^assets/, '').sub(/[^\/]+\.yaml$/, '')
227
+
228
+ # Get modification times
229
+ meta_mtime = File.stat(meta_filename).mtime
230
+ content_mtime = File.stat(content_filename).mtime
231
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
232
+
233
+ # Create asset object
234
+ Nanoc::Asset.new(content_file, attributes, path, mtime)
235
+ end
29
236
  end
30
237
 
31
- def setup
32
- # Create page
33
- FileManager.create_file 'content/content.txt' do
34
- "I'm a brand new root page. Please edit me!\n"
238
+ def save_asset(asset) # :nodoc:
239
+ # Determine meta file path
240
+ last_component = asset.path.split('/')[-1]
241
+ meta_filename = 'assets' + asset.path + last_component + '.yaml'
242
+
243
+ # Get existing path
244
+ existing_path = nil
245
+ existing_path = meta_filename_best if File.file?(meta_filename_best)
246
+ existing_path = meta_filename_worst if File.file?(meta_filename_worst)
247
+
248
+ if meta_filename.nil?
249
+ # Get filenames
250
+ dir_path = 'assets' + asset.path
251
+ content_filename = 'assets' + asset.path + last_component + '.dat'
252
+
253
+ # Notify
254
+ Nanoc::NotificationCenter.post(:file_created, meta_filename)
255
+ Nanoc::NotificationCenter.post(:file_created, content_filename)
256
+
257
+ # Create directories if necessary
258
+ FileUtils.mkdir_p(dir_path)
259
+ else
260
+ # Get filenames
261
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
262
+
263
+ # Notify
264
+ Nanoc::NotificationCenter.post(:file_updated, meta_filename)
265
+ Nanoc::NotificationCenter.post(:file_updated, content_filename)
35
266
  end
36
- FileManager.create_file 'content/content.yaml' do
37
- "# Built-in\n" +
38
- "\n" +
39
- "# Custom\n" +
40
- "title: \"A New Root Page\"\n"
267
+
268
+ # Write files
269
+ File.open(meta_filename, 'w') { |io| io.write(asset.attributes.to_split_yaml) }
270
+ File.open(content_filename, 'w') { }
271
+
272
+ # Add to working copy if possible
273
+ if meta_filename.nil?
274
+ vcs.add(meta_filename)
275
+ vcs.add(content_filename)
41
276
  end
277
+ end
278
+
279
+ def move_asset(asset, new_path) # :nodoc:
280
+ # TODO implement
281
+ end
282
+
283
+ def delete_asset(asset) # :nodoc:
284
+ # TODO implement
285
+ end
286
+
287
+ ########## Page Defaults ##########
288
+
289
+ def page_defaults # :nodoc:
290
+ # Get attributes
291
+ filename = File.file?(PAGE_DEFAULTS_FILENAME) ? PAGE_DEFAULTS_FILENAME : PAGE_DEFAULTS_FILENAME_OLD
292
+ attributes = YAML.load_file(filename) || {}
42
293
 
43
- # Create page defaults
44
- FileManager.create_file 'meta.yaml' do
45
- "# This file contains the default values for all metafiles.\n" +
46
- "# Other metafiles can override the contents of this one.\n" +
47
- "\n" +
48
- "# Built-in\n" +
49
- "custom_path: none\n" +
50
- "extension: \"html\"\n" +
51
- "filename: \"index\"\n" +
52
- "filters_post: []\n" +
53
- "filters_pre: []\n" +
54
- "is_draft: false\n" +
55
- "layout: \"default\"\n" +
56
- "skip_output: false\n" +
57
- "\n" +
58
- "# Custom\n"
294
+ # Get mtime
295
+ mtime = File.stat(filename).mtime
296
+
297
+ # Build page defaults
298
+ Nanoc::PageDefaults.new(attributes, mtime)
299
+ end
300
+
301
+ def save_page_defaults(page_defaults) # :nodoc:
302
+ # Notify
303
+ if File.file?(PAGE_DEFAULTS_FILENAME)
304
+ filename = PAGE_DEFAULTS_FILENAME
305
+ created = false
306
+ Nanoc::NotificationCenter.post(:file_updated, filename)
307
+ elsif File.file?(PAGE_DEFAULTS_FILENAME_OLD)
308
+ filename = PAGE_DEFAULTS_FILENAME_OLD
309
+ created = false
310
+ Nanoc::NotificationCenter.post(:file_updated, filename)
311
+ else
312
+ filename = PAGE_DEFAULTS_FILENAME
313
+ created = true
314
+ Nanoc::NotificationCenter.post(:file_created, filename)
59
315
  end
60
316
 
61
- # Create template
62
- FileManager.create_file 'templates/default/default.txt' do
63
- "Hi, I'm a new page!\n"
317
+ # Write
318
+ File.open(filename, 'w') do |io|
319
+ io.write(page_defaults.attributes.to_split_yaml)
64
320
  end
65
- FileManager.create_file 'templates/default/default.yaml' do
66
- "# Built-in\n" +
67
- "\n" +
68
- "# Custom\n" +
69
- "title: \"A New Page\"\n"
321
+
322
+ # Add to working copy if possible
323
+ vcs.add(filename) if created
324
+ end
325
+
326
+ ########## Asset Defaults ##########
327
+
328
+ def asset_defaults # :nodoc:
329
+ if File.file?(ASSET_DEFAULTS_FILENAME)
330
+ # Get attributes
331
+ attributes = YAML.load_file(ASSET_DEFAULTS_FILENAME) || {}
332
+
333
+ # Get mtime
334
+ mtime = File.stat(ASSET_DEFAULTS_FILENAME).mtime
335
+
336
+ # Build asset defaults
337
+ Nanoc::AssetDefaults.new(attributes, mtime)
338
+ else
339
+ Nanoc::AssetDefaults.new({})
70
340
  end
341
+ end
71
342
 
72
- # Create layout
73
- FileManager.create_file 'layouts/default.erb' do
74
- "<html>\n" +
75
- " <head>\n" +
76
- " <title><%= @page.title %></title>\n" +
77
- " </head>\n" +
78
- " <body>\n" +
79
- "<%= @page.content %>\n" +
80
- " </body>\n" +
81
- "</html>\n"
343
+ def save_asset_defaults(asset_defaults) # :nodoc:
344
+ # Notify
345
+ if File.file?(ASSET_DEFAULTS_FILENAME)
346
+ Nanoc::NotificationCenter.post(:file_updated, ASSET_DEFAULTS_FILENAME)
347
+ created = false
348
+ else
349
+ Nanoc::NotificationCenter.post(:file_created, ASSET_DEFAULTS_FILENAME)
350
+ created = true
82
351
  end
83
352
 
84
- # Create code
85
- FileManager.create_file 'lib/default.rb' do
86
- "\# All files in the 'lib' directory will be loaded\n" +
87
- "\# before nanoc starts compiling.\n" +
88
- "\n" +
89
- "def html_escape(str)\n" +
90
- " str.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('\"', '&quot;')\n" +
91
- "end\n" +
92
- "alias h html_escape\n"
353
+ # Write
354
+ File.open(ASSET_DEFAULTS_FILENAME, 'w') do |io|
355
+ io.write(asset_defaults.attributes.to_split_yaml)
93
356
  end
94
357
 
358
+ # Add to working copy if possible
359
+ vcs.add(ASSET_DEFAULTS_FILENAME) if created
95
360
  end
96
361
 
97
- ########## Loading data ##########
98
-
99
- # The filesystem data source stores its pages in nested directories. Each
100
- # directory represents a single page. The root directory is the 'content'
101
- # directory.
102
- #
103
- # Every directory has a content file and a meta file. The content file
104
- # contains the actual page content, while the meta file contains the
105
- # page's metadata.
106
- #
107
- # Both content files and meta files are named after its parent directory
108
- # (i.e. page). For example, a page named 'foo' will have a directory named
109
- # 'foo', with e.g. a 'foo.markdown' content file and a 'foo.yaml' meta
110
- # file.
111
- #
112
- # Content file extensions are ignored by nanoc. The content file extension
113
- # does not determine the filters to run on it; the meta file defines the
114
- # list of filters. The meta file extension must always be 'yaml', though.
115
- #
116
- # Content files can also have the 'index' basename. Similarly, meta files
117
- # can have the 'meta' basename. For example, a parent directory named
118
- # 'foo' can have an 'index.txt' content file and a 'meta.yaml' meta file.
119
- # This is to preserve backward compatibility.
120
- def pages
121
- meta_filenames.inject([]) do |pages, filename|
122
- # Read metadata
123
- meta = (YAML.load_file(filename) || {}).clean
362
+ ########## Layouts ##########
124
363
 
125
- if meta[:is_draft]
126
- # Skip drafts
127
- pages
128
- else
129
- # Get extra info
130
- path = filename.sub(/^content/, '').sub(/[^\/]+\.yaml$/, '')
131
- file = content_file_for_dir(File.dirname(filename))
132
- extras = {
133
- :path => path,
134
- :file => FileProxy.new(file.path),
135
- :uncompiled_content => file.read
136
- }
137
-
138
- # Add to list of pages
139
- pages + [ meta.merge(extras) ]
364
+ def layouts # :nodoc:
365
+ # Determine what layout directory structure is being used
366
+ dir_count = Dir[File.join('layouts', '*')].select { |f| File.directory?(f) }.size
367
+ is_old_school = (dir_count == 0)
368
+
369
+ if is_old_school
370
+ # Warn about deprecation
371
+ warn(
372
+ 'nanoc 2.1 changes the way layouts are stored. Future versions will not support these outdated sites. To update your site, issue \'nanoc update\'.',
373
+ 'DEPRECATION WARNING'
374
+ )
375
+
376
+ Dir[File.join('layouts', '*')].reject { |f| f =~ /~$/ }.map do |filename|
377
+ # Get content
378
+ content = File.read(filename)
379
+
380
+ # Get attributes
381
+ attributes = { :extension => File.extname(filename)}
382
+
383
+ # Get path
384
+ path = File.basename(filename, attributes[:extension])
385
+
386
+ # Get modification time
387
+ mtime = File.stat(filename).mtime
388
+
389
+ # Create layout object
390
+ Nanoc::Layout.new(content, attributes, path, mtime)
140
391
  end
392
+ else
393
+ meta_filenames('layouts').map do |meta_filename|
394
+ # Get content
395
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
396
+ content = File.read(content_filename)
397
+
398
+ # Get attributes
399
+ attributes = YAML.load_file(meta_filename) || {}
400
+
401
+ # Get path
402
+ path = meta_filename.sub(/^layouts\//, '').sub(/\/[^\/]+\.yaml$/, '')
403
+
404
+ # Get modification times
405
+ meta_mtime = File.stat(meta_filename).mtime
406
+ content_mtime = File.stat(content_filename).mtime
407
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
408
+
409
+ # Create layout object
410
+ Nanoc::Layout.new(content, attributes, path, mtime)
411
+ end
412
+ end
413
+ end
414
+
415
+ def save_layout(layout) # :nodoc:
416
+ # Determine what layout directory structure is being used
417
+ layout_file_count = Dir[File.join('layouts', '*')].select { |f| File.file?(f) }.size
418
+ error_outdated if layout_file_count > 0
419
+
420
+ # Get paths
421
+ last_component = layout.path.split('/')[-1]
422
+ dir_path = 'layouts' + layout.path
423
+ meta_filename = dir_path + last_component + '.yaml'
424
+ content_filename = Dir[dir_path + last_component + '.*'][0]
425
+
426
+ if File.file?(meta_filename)
427
+ created = false
428
+
429
+ # Notify
430
+ Nanoc::NotificationCenter.post(:file_updated, meta_filename)
431
+ Nanoc::NotificationCenter.post(:file_updated, content_filename)
432
+ else
433
+ created = true
434
+
435
+ # Create dir
436
+ FileUtils.mkdir_p(dir_path)
437
+
438
+ # Get content filename
439
+ content_filename = dir_path + last_component + '.html'
440
+
441
+ # Notify
442
+ Nanoc::NotificationCenter.post(:file_created, meta_filename)
443
+ Nanoc::NotificationCenter.post(:file_created, content_filename)
141
444
  end
445
+
446
+ # Write files
447
+ File.open(meta_filename, 'w') { |io| io.write(layout.attributes.to_split_yaml) }
448
+ File.open(content_filename, 'w') { |io| io.write(layout.content) }
449
+
450
+ # Add to working copy if possible
451
+ if created
452
+ vcs.add(meta_filename)
453
+ vcs.add(content_filename)
454
+ end
455
+ end
456
+
457
+ def move_layout(layout, new_path) # :nodoc:
458
+ # TODO implement
142
459
  end
143
460
 
144
- # The page defaults are loaded from a 'meta.yaml' file
145
- def page_defaults
146
- (YAML.load_file('meta.yaml') || {}).clean
461
+ def delete_layout(layout) # :nodoc:
462
+ # TODO implement
147
463
  end
148
464
 
149
- # Layouts are stored as files in the 'layouts' directory. Each layout has
150
- # a basename (the part before the extension) and an extension. Unlike page
151
- # content files, the extension _is_ used for determining the layout
152
- # processor; which extension maps to which layout processor is defined in
153
- # the layout processors.
154
- def layouts
155
- Dir["layouts/*"].reject { |f| f =~ /~$/ }.map do |filename|
156
- # Get layout details
157
- extension = File.extname(filename)
158
- name = File.basename(filename, extension)
159
- content = File.read(filename)
465
+ ########## Templates ##########
160
466
 
161
- # Build hash for layout
162
- { :name => name, :content => content, :extension => extension }
467
+ def templates # :nodoc:
468
+ meta_filenames('templates').map do |meta_filename|
469
+ # Get name
470
+ name = meta_filename.sub(/^templates\/(.*)\/[^\/]+\.yaml$/, '\1')
471
+
472
+ # Get content
473
+ content_filename = content_filename_for_dir(File.dirname(meta_filename))
474
+ content = File.read(content_filename)
475
+
476
+ # Get attributes
477
+ attributes = YAML.load_file(meta_filename) || {}
478
+
479
+ # Build template
480
+ Nanoc::Template.new(content, attributes, name)
163
481
  end
164
482
  end
165
483
 
166
- # Templates are located in the 'templates' directroy. Every template is a
167
- # directory consisting of a content file and a meta file, both named after
168
- # the template. This is very similar to the way pages are stored, except
169
- # that templates cannot be nested.
170
- def templates
171
- meta_filenames('templates').inject([]) do |templates, filename|
172
- # Get template name
173
- name = filename.sub(/^templates\/(.*)\/[^\/]+\.yaml$/, '\1')
174
-
175
- # Get file names
176
- meta_filename = filename
177
- content_filenames = Dir['templates/' + name + '/' + name + '.*'] +
178
- Dir['templates/' + name + '/index.*'] -
179
- Dir['templates/' + name + '/*.yaml' ]
180
-
181
- # Read files
182
- extension = nil
183
- content = nil
184
- content_filenames.each do |content_filename|
185
- content = File.read(content_filename)
186
- extension = File.extname(content_filename)
187
- end
188
- meta = File.read(meta_filename)
189
-
190
- # Add it to the list of templates
191
- templates + [{
192
- :name => name,
193
- :extension => extension,
194
- :content => content,
195
- :meta => meta
196
- }]
484
+ def save_template(template) # :nodoc:
485
+ # Determine possible meta file paths
486
+ meta_filename_worst = 'templates/' + template.name + '/index.yaml'
487
+ meta_filename_best = 'templates/' + template.name + '/' + template.name + '.yaml'
488
+
489
+ # Get existing path
490
+ existing_path = nil
491
+ existing_path = meta_filename_best if File.file?(meta_filename_best)
492
+ existing_path = meta_filename_worst if File.file?(meta_filename_worst)
493
+
494
+ if existing_path.nil?
495
+ # Get filenames
496
+ dir_path = 'templates/' + template.name
497
+ meta_filename = meta_filename_best
498
+ content_filename = 'templates/' + template.name + '/' + template.name + '.html'
499
+
500
+ # Notify
501
+ Nanoc::NotificationCenter.post(:file_created, meta_filename)
502
+ Nanoc::NotificationCenter.post(:file_created, content_filename)
503
+
504
+ # Create directories if necessary
505
+ FileUtils.mkdir_p(dir_path)
506
+ else
507
+ # Get filenames
508
+ meta_filename = existing_path
509
+ content_filename = content_filename_for_dir(File.dirname(existing_path))
510
+
511
+ # Notify
512
+ Nanoc::NotificationCenter.post(:file_updated, meta_filename)
513
+ Nanoc::NotificationCenter.post(:file_updated, content_filename)
197
514
  end
515
+
516
+ # Write files
517
+ File.open(meta_filename, 'w') { |io| io.write(template.page_attributes.to_split_yaml) }
518
+ File.open(content_filename, 'w') { |io| io.write(template.page_content) }
519
+
520
+ # Add to working copy if possible
521
+ if existing_path.nil?
522
+ vcs.add(meta_filename)
523
+ vcs.add(content_filename)
524
+ end
525
+ end
526
+
527
+ def move_template(template, new_name) # :nodoc:
528
+ # TODO implement
198
529
  end
199
530
 
200
- # Code is stored in '.rb' files in the 'lib' directory. Code can reside
201
- # in sub-directories.
202
- def code
203
- Dir['lib/**/*.rb'].sort.inject('') { |m, f| m + File.read(f) + "\n" }
531
+ def delete_template(template) # :nodoc:
532
+ # TODO implement
204
533
  end
205
534
 
206
- ########## Creating data ##########
535
+ ########## Code ##########
207
536
 
208
- # Creating a page creates a page directory with the name of the page in
209
- # the 'content' directory, as well as a content file named xxx.txt and a
210
- # meta file named xxx.yaml (with xxx being the name of the page).
211
- def create_page(path, template)
212
- # Make sure path does not start or end with a slash
213
- sanitized_path = path.gsub(/^\/+|\/+$/, '')
537
+ def code # :nodoc:
538
+ # Get files
539
+ filenames = Dir['lib/**/*.rb'].sort
214
540
 
215
- # Get paths
216
- dir_path = 'content/' + sanitized_path
217
- name = sanitized_path.sub(/.*\/([^\/]+)$/, '\1')
218
- meta_path = dir_path + '/' + name + '.yaml'
219
- content_path = dir_path + '/' + name + template[:extension]
220
-
221
- # Make sure the page doesn't exist yet
222
- error "A page named '#{path}' already exists." if File.exist?(meta_path)
223
-
224
- # Create index and meta file
225
- FileManager.create_file(meta_path) { template[:meta] }
226
- FileManager.create_file(content_path) { template[:content] }
227
- end
228
-
229
- # Creating a layout creates a single file in the 'layouts' directory,
230
- # named xxx.erb (with xxx being the name of the layout).
231
- def create_layout(name)
232
- # Get details
233
- path = 'layouts/' + name + '.erb'
234
-
235
- # Make sure the layout doesn't exist yet
236
- error "A layout named '#{name}' already exists." if File.exist?(path)
237
-
238
- # Create layout file
239
- FileManager.create_file(path) do
240
- "<html>\n" +
241
- " <head>\n" +
242
- " <title><%= @page.title %></title>\n" +
243
- " </head>\n" +
244
- " <body>\n" +
245
- "<%= @page.content %>\n" +
246
- " </body>\n" +
247
- "</html>\n"
248
- end
541
+ # Get data
542
+ data = filenames.map { |filename| File.read(filename) + "\n" }.join('')
543
+
544
+ # Get modification time
545
+ mtimes = filenames.map { |filename| File.stat(filename).mtime }
546
+ mtime = mtimes.inject { |memo, mtime| memo > mtime ? mtime : memo }
547
+
548
+ # Build code
549
+ Nanoc::Code.new(data, mtime)
249
550
  end
250
551
 
251
- # Creating a template creates a template directory with the name of the
252
- # template in the 'templates' directory, as well as a content file named
253
- # xxx.txt and a meta file named xxx.yaml (with xxx being the name of the
254
- # template).
255
- def create_template(name)
256
- # Get paths
257
- meta_path = 'templates/' + name + '/' + name + '.yaml'
258
- content_path = 'templates/' + name + '/' + name + '.txt'
552
+ def save_code(code) # :nodoc:
553
+ # Check whether code existed
554
+ existed = File.file?('lib/default.rb')
259
555
 
260
- # Make sure the template doesn't exist yet
261
- error "A template named '#{name}' already exists." if File.exist?(meta_path)
556
+ # Remove all existing code files
557
+ Dir['lib/**/*.rb'].each do |file|
558
+ vcs.remove(file) unless file == 'lib/default.rb'
559
+ end
560
+
561
+ # Notify
562
+ if existed
563
+ Nanoc::NotificationCenter.post(:file_updated, 'lib/default.rb')
564
+ else
565
+ Nanoc::NotificationCenter.post(:file_created, 'lib/default.rb')
566
+ end
262
567
 
263
- # Create index and meta file
264
- FileManager.create_file(meta_path) { "# Built-in\n\n# Custom\ntitle: A New Page\n" }
265
- FileManager.create_file(content_path) { "Hi, I'm new here!\n" }
568
+ # Write new code
569
+ File.open('lib/default.rb', 'w') do |io|
570
+ io.write(code.data)
571
+ end
572
+
573
+ # Add to working copy if possible
574
+ vcs.add('lib/default.rb') unless existed
266
575
  end
267
576
 
268
577
  private
269
578
 
270
579
  ########## Custom functions ##########
271
580
 
272
- # Returns the list of meta files in the given (optional) base directory.
273
- def meta_filenames(base='content')
581
+ # Returns the list of all meta files in the given base directory as well
582
+ # as its subdirectories.
583
+ def meta_filenames(base)
274
584
  # Find all possible meta file names
275
585
  filenames = Dir[base + '/**/*.yaml']
276
586
 
@@ -287,16 +597,20 @@ module Nanoc::DataSource::Filesystem
287
597
 
288
598
  # Warn about bad filenames
289
599
  unless bad_filenames.empty?
290
- error "The following files appear to be meta files, " +
291
- "but have an invalid name:\n - " +
292
- bad_filenames.join("\n - ")
600
+ raise RuntimeError.new(
601
+ "The following files appear to be meta files, " +
602
+ "but have an invalid name:\n - " +
603
+ bad_filenames.join("\n - ")
604
+ )
293
605
  end
294
606
 
295
607
  good_filenames
296
608
  end
297
609
 
298
- # Returns a File object for the content file in the given directory
299
- def content_file_for_dir(dir)
610
+ # Returns the filename of the content file in the given directory,
611
+ # ignoring any unwanted files (files that end with '~', '.orig', '.rej' or
612
+ # '.bak')
613
+ def content_filename_for_dir(dir)
300
614
  # Find all files
301
615
  filename_glob_1 = dir.sub(/([^\/]+)$/, '\1/\1.*')
302
616
  filename_glob_2 = dir.sub(/([^\/]+)$/, '\1/index.*')
@@ -306,15 +620,122 @@ module Nanoc::DataSource::Filesystem
306
620
  filenames.reject! { |f| f =~ /\.yaml$/ }
307
621
 
308
622
  # Reject backups
309
- filenames.reject! { |f| f =~ /~$/ }
623
+ filenames.reject! { |f| f =~ /(~|\.orig|\.rej|\.bak)$/ }
310
624
 
311
625
  # Make sure there is only one content file
312
626
  if filenames.size != 1
313
- error "Expected 1 content file in #{dir} but found #{filenames.size}"
627
+ raise RuntimeError.new(
628
+ "Expected 1 content file in #{dir} but found #{filenames.size}"
629
+ )
314
630
  end
315
631
 
316
- # Read content file
317
- File.new(filenames.first)
632
+ # Return content filename
633
+ filenames.first
634
+ end
635
+
636
+ # Raises an "outdated data format" error.
637
+ def error_outdated
638
+ raise RuntimeError.new(
639
+ 'This site\'s data is stored in an old format and must be updated. ' +
640
+ 'To do so, issue the \'nanoc update\' command. For help on ' +
641
+ 'updating a site\'s data, issue \'nanoc help update\'.'
642
+ )
643
+ end
644
+
645
+ # Updated outdated page defaults (renames page defaults file)
646
+ def update_page_defaults
647
+ return unless File.file?(PAGE_DEFAULTS_FILENAME_OLD)
648
+
649
+ vcs.move(PAGE_DEFAULTS_FILENAME_OLD, PAGE_DEFAULTS_FILENAME)
650
+ end
651
+
652
+ # Updates outdated pages (both content and meta file names).
653
+ def update_pages
654
+ # Update content files
655
+ # content/foo/bar/baz/index.ext -> content/foo/bar/baz/baz.ext
656
+ Dir['content/**/index.*'].select { |f| File.file?(f) }.each do |old_filename|
657
+ # Determine new name
658
+ if old_filename =~ /^content\/index\./
659
+ new_filename = old_filename.sub(/^content\/index\./, 'content/content.')
660
+ else
661
+ new_filename = old_filename.sub(/([^\/]+)\/index\.([^\/]+)$/, '\1/\1.\2')
662
+ end
663
+
664
+ # Move
665
+ vcs.move(old_filename, new_filename)
666
+ end
667
+
668
+ # Update meta files
669
+ # content/foo/bar/baz/meta.yaml -> content/foo/bar/baz/baz.yaml
670
+ Dir['content/**/meta.yaml'].select { |f| File.file?(f) }.each do |old_filename|
671
+ # Determine new name
672
+ if old_filename == 'content/meta.yaml'
673
+ new_filename = 'content/content.yaml'
674
+ else
675
+ new_filename = old_filename.sub(/([^\/]+)\/meta.yaml$/, '\1/\1.yaml')
676
+ end
677
+
678
+ # Move
679
+ vcs.move(old_filename, new_filename)
680
+ end
681
+ end
682
+
683
+ # Updates outdated layouts.
684
+ def update_layouts
685
+ # layouts/abc.ext -> layouts/abc/abc.{html,yaml}
686
+ Dir[File.join('layouts', '*')].select { |f| File.file?(f) }.each do |filename|
687
+ # Get filter class
688
+ filter_class = Nanoc::Filter.with_extension(File.extname(filename))
689
+
690
+ # Get data
691
+ content = File.read(filename)
692
+ attributes = { :filter => filter_class.identifier.to_s }
693
+ path = File.basename(filename, File.extname(filename))
694
+
695
+ # Get layout
696
+ tmp_layout = Nanoc::Layout.new(content, attributes, path)
697
+
698
+ # Get filenames
699
+ last_component = tmp_layout.path.split('/')[-1]
700
+ dir_path = 'layouts' + tmp_layout.path
701
+ meta_filename = dir_path + last_component + '.yaml'
702
+ content_filename = dir_path + last_component + File.extname(filename)
703
+
704
+ # Create new files
705
+ FileUtils.mkdir_p(dir_path)
706
+ File.open(meta_filename, 'w') { |io| io.write(tmp_layout.attributes.to_split_yaml) }
707
+ File.open(content_filename, 'w') { |io| io.write(tmp_layout.content) }
708
+
709
+ # Add
710
+ vcs.add(meta_filename)
711
+ vcs.add(content_filename)
712
+
713
+ # Delete old files
714
+ vcs.remove(filename)
715
+ end
716
+ end
717
+
718
+ # Updates outdated templates (both content and meta file names).
719
+ def update_templates
720
+ # Update content files
721
+ # templates/foo/index.ext -> templates/foo/foo.ext
722
+ Dir['templates/**/index.*'].select { |f| File.file?(f) }.each do |old_filename|
723
+ # Determine new name
724
+ new_filename = old_filename.sub(/([^\/]+)\/index\.([^\/]+)$/, '\1/\1.\2')
725
+
726
+ # Move
727
+ vcs.move(old_filename, new_filename)
728
+ end
729
+
730
+ # Update meta files
731
+ # templates/foo/meta.yaml -> templates/foo/foo.yaml
732
+ Dir['templates/**/meta.yaml'].select { |f| File.file?(f) }.each do |old_filename|
733
+ # Determine new name
734
+ new_filename = old_filename.sub(/([^\/]+)\/meta.yaml$/, '\1/\1.yaml')
735
+
736
+ # Move
737
+ vcs.move(old_filename, new_filename)
738
+ end
318
739
  end
319
740
 
320
741
  end