nanoc-filesystem-i18n 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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
+