nanoc-filesystem-i18n 0.1.0.pre1

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.
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2010 Yann Lugrin
2
+
3
+ The original source code is part of nanoc software with the same licence
4
+ and have the following copyright notice:
5
+ copyright (c) 2007-2010 Denis Defreyne and nanoc contributors
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
data/README.rdoc ADDED
@@ -0,0 +1,109 @@
1
+ = nanoc-filesystem-i18n
2
+
3
+ The filesystem_i18n data source is a localized data source for a nanoc
4
+ site. It stores all data as files on the hard disk and is fully compatible
5
+ with {Nanoc3::DataSources::FilesystemUnified} and {Nanoc3::DataSources::FilesystemVerbose}.
6
+
7
+ == Resources
8
+
9
+ - FilesystemI18n (http://github.com/yannlugrin/nanoc-filesystem-i18n)
10
+ - Nanoc (http://nanoc.stoneship.org)
11
+ - Nanoc on Github (http://github.com/ddfreyne/nanoc)
12
+ - Ruby I18n (http://github.com/svenfuchs/i18n)
13
+
14
+ == Documentation
15
+
16
+ === Installation
17
+
18
+ gem install nanoc-filesystem-i18n --pre
19
+
20
+ Add following require in your `lib/default.rb` file:
21
+
22
+ require 'nanoc3/data_sources/filesystem_i18n'
23
+
24
+ Edit `config.yaml` file of your nanoc site and change data_sources type
25
+ from `filesystem_unified` or `filesystem_verbose` to `filesystem_i18n`:
26
+
27
+ data_sources:
28
+ -
29
+ type: filesystem_i18n
30
+
31
+ In the data source section of `config.yaml` file, add following information:
32
+
33
+ config:
34
+ locale:
35
+ # list of availables locale, ser default to true to select default
36
+ # locale.
37
+ availables:
38
+ fr:
39
+ name: Francais
40
+ en:
41
+ name: English
42
+ default: true
43
+ # objects should be not localized
44
+ exclude:
45
+ item: ['/css*', '/js*']
46
+ layout: ['*']
47
+
48
+ See following configuration section for more information.
49
+
50
+ === Data source specifications
51
+
52
+ The filesystem_i18n data source stores its items and layouts in nested
53
+ directories or files. Each directory represents a single item or layout,
54
+ but an item can be a simple file in a directory. The root directory for
55
+ items is the `content` directory; for layouts it is the `layouts`
56
+ directory.
57
+
58
+ Every object (item or layout) is represented by a meta file and one or
59
+ more content files with a minimum of one file. The content file contains
60
+ he actual item content or layout, while the meta file contains the item’s
61
+ or the layout’s metadata, formatted as YAML.
62
+
63
+ Both meta files and content files are named after its parent directory
64
+ (i.e. item). For example, an item/layout named `foo` will have a directory
65
+ named `foo`, with e.g. a `foo.markdown` or `index.markdown` content file
66
+ and a `foo.yaml` or `index.yaml` meta file. An item/layout named `foo/bar`
67
+ can be also created in parent directory named `foo` without dedicated
68
+ directory, with e.g. a `foo.markdown` content file and `foo.yaml` meta
69
+ file. Root item already named `index.markdown` for content file (extension
70
+ can be different) and `index.yaml` for meta file.
71
+
72
+ Content file extensions are not used for determining the filter that
73
+ should be run; the meta file or configuration file defines the list of
74
+ filters. The meta file extension must always be `.yaml`, though.
75
+
76
+ An item/layout content file named `foo.markdown` contain default content
77
+ for all locales, but you can create a content file for each locales with
78
+ e.g. a `foo.fr.markdown` for locale `fr`. If default locale for site is
79
+ `fr` and the `foo.fr.markdown` file is present but `foo.markdow` file not,
80
+ the `fr` content file is used by default.
81
+
82
+ The identifier is calculated by stripping the extension (part after last dot)
83
+ and locale code.
84
+
85
+ === Configuration
86
+
87
+ soon...
88
+
89
+ === Manage locale meta and content
90
+
91
+ soon...
92
+
93
+ === Deployement
94
+
95
+ soon...
96
+
97
+ == Note on Patches/Pull Requests
98
+
99
+ * Fork the project.
100
+ * Make your feature addition or bug fix.
101
+ * Add tests for it. This is important so I don't break it in a
102
+ future version unintentionally.
103
+ * Commit, do not mess with rakefile, version, or history.
104
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
105
+ * Send me a pull request. Bonus points for topic branches.
106
+
107
+ == Copyright
108
+
109
+ Copyright (c) 2010 Yann Lugrin. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,59 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require File.dirname(__FILE__) + '/lib/nanoc3/data_sources/filesystem_i18n/version'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = 'nanoc-filesystem-i18n'
9
+ gem.version = Nanoc3::DataSources::FilesystemI18n::Version
10
+ gem.summary = %Q{I18n filesystem based data source for nanoc}
11
+ gem.description = %Q{I18n filesystem based data source for nanoc. Compatible with nanoc 3 and default filesystem based data source.}
12
+ gem.email = 'yann.lugrin@sans-savoir.net'
13
+ gem.homepage = 'http://github.com/yannlugrin/nanoc-filesystem-i18n'
14
+ gem.authors = ['Yann Lugrin']
15
+ gem.add_dependency 'nanoc', '>= 3.1.2'
16
+ gem.add_dependency 'i18n', '>= 0'
17
+ gem.add_development_dependency 'minitest', '>= 0'
18
+ gem.add_development_dependency 'yard', '>= 0'
19
+ gem.files.exclude '.gitignore', '.document', 'nanoc-filesystem-i18n.gemspec'
20
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
25
+ end
26
+
27
+ require 'rake/testtask'
28
+ Rake::TestTask.new(:test) do |test|
29
+ test.libs << 'lib' << 'test'
30
+ test.pattern = 'test/**/test_*.rb'
31
+ test.verbose = true
32
+ end
33
+
34
+ begin
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ end
41
+ rescue LoadError
42
+ task :rcov do
43
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
44
+ end
45
+ end
46
+
47
+ task :test => :check_dependencies
48
+
49
+ task :default => :test
50
+
51
+ begin
52
+ require 'nanoc3'
53
+ require 'yard'
54
+ YARD::Rake::YardocTask.new
55
+ rescue LoadError
56
+ task :yardoc do
57
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
58
+ end
59
+ end
@@ -0,0 +1,399 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nanoc3'
4
+ require 'nanoc3/extra/i18n'
5
+
6
+ module Nanoc3::DataSources
7
+
8
+ # The filesystem_i18n data source is a localized data source for a nanoc
9
+ # site. It stores all data as files on the hard disk and is fully compatible
10
+ # with {Nanoc3::DataSources::FilesystemUnified} and {Nanoc3::DataSources::FilesystemVerbose}.
11
+ #
12
+ # None of the public api methods are documented in this file. See
13
+ # {Nanoc3::DataSource} for documentation on the overridden methods instead.
14
+ #
15
+ # For more information about this data source specifications and configuration,
16
+ # please read the Readme file.
17
+ class FilesystemI18n < Nanoc3::DataSource
18
+ identifier :filesystem_i18n
19
+
20
+ # The VCS that will be called when adding, deleting and moving files. If
21
+ # no VCS has been set, or if the VCS has been set to `nil`, a dummy VCS
22
+ # will be returned.
23
+ #
24
+ # @return [Nanoc3::Extra::VCS, nil] The VCS that will be used.
25
+ def vcs
26
+ @vcs ||= Nanoc3::Extra::VCSes::Dummy.new
27
+ end
28
+ attr_writer :vcs
29
+
30
+ # See {Nanoc3::DataSource#up}.
31
+ def up
32
+ I18n.load_config(@config ? @config[:locale] : nil)
33
+ end
34
+
35
+ # See {Nanoc3::DataSource#down}.
36
+ def down
37
+ end
38
+
39
+ # See {Nanoc3::DataSource#setup}.
40
+ def setup
41
+ # Create directories
42
+ %w( content layouts lib ).each do |dir|
43
+ FileUtils.mkdir_p(dir)
44
+ vcs.add(dir)
45
+ end
46
+ end
47
+
48
+ # See {Nanoc3::DataSource#items}.
49
+ def items
50
+ load_objects('content', 'item', Nanoc3::Item)
51
+ end
52
+
53
+ # See {Nanoc3::DataSource#layouts}.
54
+ def layouts
55
+ load_objects('layouts', 'layout', Nanoc3::Layout)
56
+ end
57
+
58
+ # See {Nanoc3::DataSource#create_item}.
59
+ def create_item(content, attributes, identifier, params={})
60
+ create_object('content', content, attributes, identifier, params)
61
+ end
62
+
63
+ # See {Nanoc3::DataSource#create_layout}.
64
+ def create_layout(content, attributes, identifier, params={})
65
+ create_object('layouts', content, attributes, identifier, params)
66
+ end
67
+
68
+ private
69
+
70
+ # Creates a new object (item or layout) on disk in dir_name according to
71
+ # the given identifier. The file will have its attributes taken from the
72
+ # attributes hash argument and its content from the content argument.
73
+ def create_object(dir_name, content, attributes, identifier, params={})
74
+ # Check for periods
75
+ if (@config.nil? || !@config[:allow_periods_in_identifiers]) && identifier.include?('.')
76
+ raise RuntimeError,
77
+ "Attempted to create an object in #{dir_name} with identifier #{identifier} containing a period, but allow_periods_in_identifiers is not enabled in the site configuration. (Enabling allow_periods_in_identifiers may cause the site to break, though.)"
78
+ end
79
+
80
+ # Get filenames
81
+ ext = params[:extension] || '.html'
82
+ meta_filename = dir_name + (identifier == '/' ? '/index.yaml' : identifier[0..-2] + '.yaml')
83
+ content_filename = dir_name + (identifier == '/' ? '/index.html' : identifier[0..-2] + ext)
84
+ parent_path = File.dirname(meta_filename)
85
+
86
+ # Notify
87
+ Nanoc3::NotificationCenter.post(:file_created, meta_filename)
88
+ Nanoc3::NotificationCenter.post(:file_created, content_filename)
89
+
90
+ # Create files
91
+ FileUtils.mkdir_p(parent_path)
92
+ File.open(meta_filename, 'w') { |io| io.write(YAML.dump(attributes.stringify_keys)) }
93
+ File.open(content_filename, 'w') { |io| io.write(content) }
94
+ end
95
+
96
+ # Creates instances of klass corresponding to the files in dir_name. The
97
+ # kind attribute indicates the kind of object that is being loaded and is
98
+ # used solely for debugging purposes.
99
+ #
100
+ # This particular implementation loads objects from a filesystem-based
101
+ # data source where content and attributes can be spread over two separate
102
+ # files (one for metadata, with `yaml` extension, and one or more for content
103
+ # with same extension and locale code before extension, separated by dot. A
104
+ # content for default locale is used by default if file without locale code
105
+ # is not present.
106
+ #
107
+ # The contents and meta-file are optional (but at least one of them needs
108
+ # to be present, obviously. If a content file is present, file without locale
109
+ # code or with default locale code is needed) and the content file can start
110
+ # with a metadata section (if no metadata file is present).
111
+ def load_objects(dir_name, kind, klass)
112
+ all_split_files_in(dir_name).map do |base_filename, (meta_ext, content_ext, locales)|
113
+ I18n.locale = I18n.default_locale # Set current locale to default
114
+
115
+ # Get filenames
116
+ meta_filename = filename_for(base_filename, meta_ext)
117
+ content_filename = filename_for(base_filename, content_ext)
118
+
119
+ # is binary content?
120
+ is_binary = !!(content_filename && !@site.config[:text_extensions].include?(File.extname(content_filename)[1..-1]))
121
+
122
+ # Read content and metadata
123
+ meta, content_or_filename = parse(content_filename, meta_filename, kind, (is_binary && klass == Nanoc3::Item))
124
+
125
+ # Is locale content?
126
+ # - excluded content with locale meta IS a locale content
127
+ # - excluded content without locale meta IS NOT locale content
128
+ # - included content with or without locale meta IS locale content
129
+ # - included content with locale meta set to `false` IS NOT locale
130
+ # content
131
+ is_locale = !!(meta['locale'] || (meta['locale'] != false && locale_content?(content_filename || meta_filename, kind)))
132
+
133
+ # Create one item by locale, if content don't need a localized version,
134
+ # use default locale
135
+ (is_locale ? I18n.available_locales : [I18n.default_locale]).map do |locale|
136
+ I18n.locale = locale # Set current locale
137
+
138
+ # Read content and metadata (only if is localized, default is already
139
+ # loaded)
140
+ meta, content_or_filename = parse(content_filename, meta_filename, kind, (is_binary && klass == Nanoc3::Item)) if is_locale
141
+
142
+ # merge meta for current locale, default locale meta used by
143
+ # default is meta don't have key
144
+ if is_locale
145
+ meta_locale = meta.delete('locale') {|el| Hash.new }
146
+ meta = (meta_locale[I18n.default_locale] || Hash.new).merge(meta)
147
+ meta.merge!(meta_locale[locale.to_s] || Hash.new)
148
+ end
149
+
150
+ # Get attributes
151
+ attributes = {
152
+ :filename => content_filename,
153
+ :content_filename => content_filename,
154
+ :meta_filename => meta_filename,
155
+ :extension => content_filename ? ext_of(content_filename)[1..-1] : nil,
156
+ :locale => locale,
157
+ # WARNING :file is deprecated; please create a File object manually
158
+ # using the :content_filename or :meta_filename attributes.
159
+ # TODO [in nanoc 4.0] remove me
160
+ :file => content_filename ? Nanoc3::Extra::FileProxy.new(content_filename) : nil
161
+ }.merge(meta)
162
+
163
+ # Get identifier
164
+ if meta_filename
165
+ identifier = identifier_for_filename(meta_filename[(dir_name.length+1)..-1])
166
+ elsif content_filename
167
+ identifier = identifier_for_filename(content_filename[(dir_name.length+1)..-1])
168
+ else
169
+ raise RuntimeError, "meta_filename and content_filename are both nil"
170
+ end
171
+ # Prepend locale code to identifier if content is localized
172
+ identifier = "/#{locale}#{identifier}" if is_locale
173
+
174
+ # Get modification times
175
+ meta_mtime = meta_filename ? File.stat(meta_filename).mtime : nil
176
+ content_mtime = content_filename ? File.stat(content_filename).mtime : nil
177
+ if meta_mtime && content_mtime
178
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
179
+ elsif meta_mtime
180
+ mtime = meta_mtime
181
+ elsif content_mtime
182
+ mtime = content_mtime
183
+ else
184
+ raise RuntimeError, "meta_mtime and content_mtime are both nil"
185
+ end
186
+
187
+ # Create layout object
188
+ klass.new(
189
+ content_or_filename, attributes, identifier,
190
+ :binary => is_binary, :mtime => mtime
191
+ )
192
+ end
193
+ end.flatten # elements is an array with all locale item, flatten in to one items list
194
+ end
195
+
196
+ # Finds all items/layouts/... in the given base directory. Returns a hash
197
+ # in which the keys are the file's dirname + basenames, and the values is
198
+ # an array with three elements: the metafile extension, the content file
199
+ # extension and an array with locales of content file. The meta file
200
+ # extension or the content file extension can be, but not both. Backup
201
+ # files are ignored. For example:
202
+ #
203
+ # {
204
+ # 'content/foo' => [ 'yaml', 'html', ['en', 'fr'] ],
205
+ # 'content/bar' => [ 'yaml', nil , [] ],
206
+ # 'content/qux' => [ nil, 'html', ['en'] ]
207
+ # }
208
+ def all_split_files_in(dir_name)
209
+ # Get all good file names
210
+ filenames = Dir[dir_name + '/**/*'].select { |i| File.file?(i) }
211
+ filenames.reject! { |fn| fn =~ /(~|\.orig|\.rej|\.bak)$/ }
212
+
213
+ # Group by identifier
214
+ grouped_filenames = filenames.group_by { |fn| basename_of(fn).gsub('/index', '') }
215
+
216
+ # Convert values into metafile/content file extension tuple
217
+ grouped_filenames.each_pair do |key, filenames|
218
+ # Divide
219
+ meta_filenames = filenames.select { |fn| ext_of(fn) == '.yaml' }
220
+ content_filenames = filenames.select { |fn| ext_of(fn) != '.yaml' }
221
+
222
+ # Check number of files per type
223
+ if ![ 0, 1 ].include?(meta_filenames.size)
224
+ raise RuntimeError, "Found #{meta_filenames.size} meta files for #{key}; expected 0 or 1"
225
+ end
226
+ if !( 0 .. (I18n.available_locales.empty? ? 1 : I18n.available_locales.size) ).include?(content_filenames.size)
227
+ raise RuntimeError, "Found #{content_filenames.size} content files for #{key}; expected 0 to #{I18n.available_locales.size}"
228
+ end
229
+
230
+ # Check content file extensions and default file
231
+ if fn = content_filenames.find {|fn| ext_of(fn) != ext_of(content_filenames[0]) }
232
+ raise RuntimeError, "Found multiple content extensions for `#{basename_of(fn)}.???`"
233
+ end
234
+
235
+ # Reorder elements and convert to extnames
236
+ filenames[0] = meta_filenames[0] ? ext_of(meta_filenames[0])[1..-1] : nil
237
+ filenames[1] = content_filenames[0] ? ext_of(content_filenames[0])[1..-1] : nil
238
+ filenames[2] = []
239
+ content_filenames.each do |content_filename|
240
+ filenames[2] << locale_of(content_filename) if locale_of(content_filename)
241
+ end
242
+ end
243
+
244
+ # Done
245
+ grouped_filenames
246
+ end
247
+
248
+ # Returns the filename for the given base filename, current locale (or
249
+ # default locale) and the extension.
250
+ #
251
+ # If the extension is nil, this function should return nil as well.
252
+ #
253
+ # This implementation is compatible with simple file item and directory
254
+ # item (with index or named file), find order is directory with named
255
+ # file, directory with index file and simple file. For locale, find
256
+ # order is current locale file, without locale file and default locale
257
+ # file.
258
+ #
259
+ # Item priority order:
260
+ # /foo/foo.html
261
+ # /foo/index.html
262
+ # /foo.html
263
+ #
264
+ # Locale priority order:
265
+ # /foo/foo.{current locale}.html
266
+ # /foo/foo.html
267
+ # /foo/foo.{default locale}.html
268
+ #
269
+ def filename_for(base_filename, ext)
270
+ last_part = base_filename.split('/')[-1]
271
+ lang_part = "{.#{I18n.locale},,.#{I18n.default_locale}}"
272
+ base_glob = base_filename.split('/')[0..-2].join('/') + "{/,}#{last_part}{/index,}#{lang_part}."
273
+
274
+ ext ? Dir[base_glob + ext][0] : nil
275
+ end
276
+
277
+ # Returns the identifier that corresponds with the given filename, which
278
+ # can be the content filename or the meta filename.
279
+ def identifier_for_filename(filename)
280
+ # Item is a directory with an index file
281
+ if filename =~ /index(\.[a-z]{2})?\.[^\/]+$/
282
+ regex = ((@config && @config[:allow_periods_in_identifiers]) ? /index(\.[a-z]{2})?(\.[^\/\.]+)$/ : /index(\.[a-z]{2})?(\.[^\/]+)$/)
283
+ # Item is a directory with a named file
284
+ elsif basename_of(filename).split(/\//)[-1] == basename_of(filename).split(/\//)[-2]
285
+ regex = ((@config && @config[:allow_periods_in_identifiers]) ? /(\/[^\/]+)?(\.[a-z]{2})?(\.[^\/\.]+)$/ : /(\/[^\/]+)?(\.[a-z]{2})?(\.[^\/]+)$/)
286
+ # Item is a simple file
287
+ else
288
+ regex = ((@config && @config[:allow_periods_in_identifiers]) ? /(\.[a-z]{2})?(\.[^\/\.]+)$/ : /(\.[a-z]{2})?(\.[^\/]+)$/)
289
+ end
290
+
291
+ filename.sub(regex, '').cleaned_identifier
292
+ end
293
+
294
+ # Returns the base name of filename, i.e. filename with the first or all
295
+ # extensions stripped off. By default, all extensions are stripped off,
296
+ # but when allow_periods_in_identifiers is set to true in the site
297
+ # configuration, only the last extension will be stripped .
298
+ def basename_of(filename)
299
+ filename.sub(extension_regex, '')
300
+ end
301
+
302
+ # Returns the extension(s) of filename. Supports multiple extensions.
303
+ # Includes the leading period. Return empty string if don't found
304
+ # extension.
305
+ def ext_of(filename)
306
+ filename =~ extension_regex ? $2 : ''
307
+ end
308
+
309
+ # Returns a regex that is used for determining the extension of a file
310
+ # name. The first match group will be the locale code (with leading
311
+ # period) if exist and the second match group is entire extension,
312
+ # including the leading period.
313
+ def extension_regex
314
+ if @config && @config[:allow_periods_in_identifiers]
315
+ /(\.[a-z]{2})?(\.[^\/\.]+$)/
316
+ else
317
+ /(\.[a-z]{2})?(\.[^\/]+$)/
318
+ end
319
+ end
320
+
321
+ # Returnes the locale code of filename or nil if filename is the default
322
+ # file (without locale code).
323
+ def locale_of(filename)
324
+ locale = (filename =~ extension_regex ? $1 : nil)
325
+ locale ? locale.gsub(/^\./, '').to_sym : nil
326
+ end
327
+
328
+ # Returnes true if this content is localized (based on data source config)
329
+ def locale_content?(base_filename_or_identifier, kind)
330
+ base_filename_or_identifier =~ locale_exclude_regex(kind) ? false : true
331
+ end
332
+
333
+ # Returnes regex that is used for determing if content is excluded not
334
+ # localized (layouts, css, js, ...).
335
+ #
336
+ # Add to locale config follwing keys (exemple):
337
+ # exclude:
338
+ # item: ['/css*', '/js*']
339
+ # layout: ['*']
340
+ #
341
+ def locale_exclude_regex(kind)
342
+ Regexp.union(I18n.exclude_list(kind.to_sym).map do |identifier|
343
+ if identifier.is_a? String
344
+ # Add leading/trailing slashes if necessary
345
+ new_identifier = identifier.dup
346
+ new_identifier[/^/] = '/' if identifier[0,1] != '/'
347
+ new_identifier[/$/] = '/' unless [ '*', '/' ].include?(identifier[-1,1])
348
+
349
+ /^[^\/]*#{new_identifier.gsub('*', '(.*?)').gsub('+', '(.+?)')}$/
350
+ else
351
+ identifier
352
+ end
353
+ end)
354
+ end
355
+
356
+ # Parses the file named `filename` and returns an array with its first
357
+ # element a hash with the file's metadata, and with its second element the
358
+ # file content itself.
359
+ def parse(content_filename, meta_filename, kind, is_binary)
360
+ # Read content and metadata from separate files
361
+ if meta_filename || is_binary
362
+ meta = (meta_filename && YAML.load_file(meta_filename)) || {}
363
+
364
+ if is_binary
365
+ content = content_filename
366
+ else
367
+ content = content_filename ? File.read(content_filename) : ''
368
+ end
369
+
370
+ return [ meta, content ]
371
+ end
372
+
373
+ # Read data
374
+ data = File.read(content_filename)
375
+
376
+ # Check presence of metadata section
377
+ if data !~ /^(-{5}|-{3})/
378
+ return [ {}, data ]
379
+ end
380
+
381
+ # Split data
382
+ pieces = data.split(/^(-{5}|-{3})/)
383
+ if pieces.size < 4
384
+ raise RuntimeError.new(
385
+ "The file '#{content_filename}' does not seem to be a nanoc #{kind}"
386
+ )
387
+ end
388
+
389
+ # Parse
390
+ meta = YAML.load(pieces[2]) || {}
391
+ content = pieces[4..-1].join.strip
392
+
393
+ # Done
394
+ [ meta, content ]
395
+ end
396
+
397
+ end
398
+ end
399
+