nanoc 4.1.6 → 4.2.0b1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/Gemfile.lock +2 -1
  4. data/NEWS.md +11 -4
  5. data/lib/nanoc/base/checksummer.rb +135 -46
  6. data/lib/nanoc/base/compilation/compiler.rb +18 -28
  7. data/lib/nanoc/base/compilation/dependency_tracker.rb +22 -32
  8. data/lib/nanoc/base/compilation/filter.rb +2 -4
  9. data/lib/nanoc/base/entities.rb +1 -0
  10. data/lib/nanoc/base/entities/content.rb +14 -3
  11. data/lib/nanoc/base/entities/document.rb +14 -6
  12. data/lib/nanoc/base/entities/item.rb +0 -31
  13. data/lib/nanoc/base/entities/item_rep.rb +1 -1
  14. data/lib/nanoc/base/entities/lazy_value.rb +36 -0
  15. data/lib/nanoc/base/entities/pattern.rb +3 -2
  16. data/lib/nanoc/base/entities/site.rb +2 -0
  17. data/lib/nanoc/base/memoization.rb +17 -10
  18. data/lib/nanoc/base/repos/compiled_content_cache.rb +1 -1
  19. data/lib/nanoc/base/repos/data_source.rb +10 -6
  20. data/lib/nanoc/base/services/executor.rb +22 -22
  21. data/lib/nanoc/base/services/item_rep_router.rb +4 -5
  22. data/lib/nanoc/base/views.rb +0 -1
  23. data/lib/nanoc/base/views/item_rep_view.rb +3 -9
  24. data/lib/nanoc/base/views/mixins/document_view_mixin.rb +4 -11
  25. data/lib/nanoc/base/views/view.rb +1 -0
  26. data/lib/nanoc/base/views/view_context.rb +5 -1
  27. data/lib/nanoc/cli/commands/compile.rb +0 -6
  28. data/lib/nanoc/data_sources.rb +5 -5
  29. data/lib/nanoc/data_sources/filesystem.rb +219 -90
  30. data/lib/nanoc/extra/checking/check.rb +1 -2
  31. data/lib/nanoc/extra/checking/checks.rb +2 -0
  32. data/lib/nanoc/extra/checking/checks/css.rb +6 -14
  33. data/lib/nanoc/extra/checking/checks/html.rb +6 -14
  34. data/lib/nanoc/extra/checking/checks/internal_links.rb +14 -3
  35. data/lib/nanoc/extra/checking/checks/w3c_validator.rb +28 -0
  36. data/lib/nanoc/extra/deployers/fog.rb +134 -78
  37. data/lib/nanoc/extra/link_collector.rb +14 -18
  38. data/lib/nanoc/filters/sass.rb +3 -3
  39. data/lib/nanoc/helpers.rb +1 -0
  40. data/lib/nanoc/helpers/capturing.rb +16 -58
  41. data/lib/nanoc/helpers/child_parent.rb +51 -0
  42. data/lib/nanoc/helpers/filtering.rb +0 -1
  43. data/lib/nanoc/helpers/html_escape.rb +5 -0
  44. data/lib/nanoc/helpers/link_to.rb +2 -0
  45. data/lib/nanoc/helpers/rendering.rb +3 -4
  46. data/lib/nanoc/rule_dsl/action_provider.rb +20 -4
  47. data/lib/nanoc/rule_dsl/recording_executor.rb +3 -1
  48. data/lib/nanoc/rule_dsl/rule_context.rb +0 -1
  49. data/lib/nanoc/rule_dsl/rule_memory_calculator.rb +4 -1
  50. data/lib/nanoc/spec.rb +217 -0
  51. data/lib/nanoc/version.rb +1 -1
  52. data/test/base/test_data_source.rb +4 -2
  53. data/test/base/test_dependency_tracker.rb +5 -11
  54. data/test/data_sources/test_filesystem.rb +605 -69
  55. data/test/extra/checking/checks/test_internal_links.rb +25 -0
  56. data/test/extra/deployers/test_fog.rb +0 -177
  57. data/test/filters/test_less.rb +9 -4
  58. data/test/helpers/test_capturing.rb +38 -212
  59. data/test/helpers/test_link_to.rb +0 -205
  60. data/test/helpers/test_xml_sitemap.rb +2 -1
  61. metadata +7 -12
  62. data/lib/nanoc/base/views/site_view.rb +0 -14
  63. data/lib/nanoc/data_sources/filesystem_unified.rb +0 -101
  64. data/test/data_sources/test_filesystem_unified.rb +0 -559
  65. data/test/helpers/test_breadcrumbs.rb +0 -60
  66. data/test/helpers/test_filtering.rb +0 -112
  67. data/test/helpers/test_html_escape.rb +0 -26
  68. data/test/helpers/test_rendering.rb +0 -147
  69. data/test/helpers/test_tagging.rb +0 -92
  70. data/test/helpers/test_text.rb +0 -18
@@ -32,24 +32,19 @@ module Nanoc
32
32
 
33
33
  # @see Hash#[]
34
34
  def [](key)
35
- Nanoc::Int::NotificationCenter.post(:visit_started, unwrap)
36
- Nanoc::Int::NotificationCenter.post(:visit_ended, unwrap)
37
-
35
+ @context.dependency_tracker.bounce(unwrap)
38
36
  unwrap.attributes[key]
39
37
  end
40
38
 
41
39
  # @return [Hash]
42
40
  def attributes
43
- Nanoc::Int::NotificationCenter.post(:visit_started, unwrap)
44
- Nanoc::Int::NotificationCenter.post(:visit_ended, unwrap)
45
-
41
+ @context.dependency_tracker.bounce(unwrap)
46
42
  unwrap.attributes
47
43
  end
48
44
 
49
45
  # @see Hash#fetch
50
46
  def fetch(key, fallback = NONE, &_block)
51
- Nanoc::Int::NotificationCenter.post(:visit_started, unwrap)
52
- Nanoc::Int::NotificationCenter.post(:visit_ended, unwrap)
47
+ @context.dependency_tracker.bounce(unwrap)
53
48
 
54
49
  if unwrap.attributes.key?(key)
55
50
  unwrap.attributes[key]
@@ -64,9 +59,7 @@ module Nanoc
64
59
 
65
60
  # @see Hash#key?
66
61
  def key?(key)
67
- Nanoc::Int::NotificationCenter.post(:visit_started, unwrap)
68
- Nanoc::Int::NotificationCenter.post(:visit_ended, unwrap)
69
-
62
+ @context.dependency_tracker.bounce(unwrap)
70
63
  unwrap.attributes.key?(key)
71
64
  end
72
65
 
@@ -5,6 +5,7 @@ module Nanoc
5
5
  @context = context
6
6
  end
7
7
 
8
+ # @api private
8
9
  def _context
9
10
  @context
10
11
  end
@@ -3,10 +3,14 @@ module Nanoc
3
3
  class ViewContext
4
4
  attr_reader :reps
5
5
  attr_reader :items
6
+ attr_reader :dependency_tracker
7
+ attr_reader :compiler
6
8
 
7
- def initialize(reps:, items:)
9
+ def initialize(reps:, items:, dependency_tracker:, compiler:)
8
10
  @reps = reps
9
11
  @items = items
12
+ @dependency_tracker = dependency_tracker
13
+ @compiler = compiler
10
14
  end
11
15
  end
12
16
  end
@@ -298,12 +298,6 @@ module Nanoc::CLI::Commands
298
298
  Nanoc::Int::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
299
299
  puts "*** Ended filtering #{rep.inspect} with #{filter_name}"
300
300
  end
301
- Nanoc::Int::NotificationCenter.on(:visit_started) do |item|
302
- puts "*** Started visiting #{item.inspect}"
303
- end
304
- Nanoc::Int::NotificationCenter.on(:visit_ended) do |item|
305
- puts "*** Ended visiting #{item.inspect}"
306
- end
307
301
  Nanoc::Int::NotificationCenter.on(:dependency_created) do |src, dst|
308
302
  puts "*** Dependency created from #{src.inspect} onto #{dst.inspect}"
309
303
  end
@@ -1,8 +1,8 @@
1
1
  # @api private
2
2
  module Nanoc::DataSources
3
- autoload 'Filesystem', 'nanoc/data_sources/filesystem'
4
- autoload 'FilesystemUnified', 'nanoc/data_sources/filesystem_unified'
5
-
6
- Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemUnified', :filesystem_unified
7
- Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemUnified', :filesystem
8
3
  end
4
+
5
+ require_relative 'data_sources/filesystem'
6
+
7
+ Nanoc::DataSource.register ::Nanoc::DataSources::Filesystem, :filesystem
8
+ Nanoc::DataSource.register ::Nanoc::DataSources::Filesystem, :filesystem_unified
@@ -1,8 +1,51 @@
1
1
  module Nanoc::DataSources
2
- # Provides functionality common across all filesystem data sources.
2
+ # The filesystem data source stores its items and layouts in nested
3
+ # directories. Items and layouts are represented by one or two files; if it
4
+ # is represented using one file, the metadata can be contained in this file.
5
+ #
6
+ # The default root directory for items is the `content` directory; for
7
+ # layouts, this is the `layouts` directory. This can be overridden
8
+ # in the data source configuration:
9
+ #
10
+ # data_sources:
11
+ # - type: filesystem
12
+ # content_dir: items
13
+ # layouts_dir: layouts
14
+ #
15
+ # The metadata for items and layouts can be stored in a separate file with
16
+ # the same base name but with the `.yaml` extension. If such a file is
17
+ # found, metadata is read from that file. Alternatively, the content file
18
+ # itself can start with a metadata section: it can be stored at the top of
19
+ # the file, between `---` (three dashes) separators. For example:
20
+ #
21
+ # ---
22
+ # title: "Moo!"
23
+ # ---
24
+ # h1. Hello!
25
+ #
26
+ # The metadata section can be omitted. If the file does not start with
27
+ # three or five dashes, the entire file will be considered as content.
28
+ #
29
+ # The identifier of items and layouts is the filename itself, without the
30
+ # root directory (as determined by the `content_dir` or `layouts_dir`
31
+ # configuration attribute, for items resp. layouts). For example:
32
+ #
33
+ # foo/bar/index.html → /foo/bar/index.html
34
+ # foo/bar.html → /foo/bar.html
35
+ #
36
+ # Note that each item must have an unique identifier. Nanoc will display an
37
+ # error if two items with the same identifier are found.
38
+ #
39
+ # The file extension does not determine the filters to run on items; the
40
+ # Rules file is used to specify processing instructors for each item.
41
+ #
42
+ # It is possible to set an explicit encoding that should be used when reading
43
+ # files. In the data source configuration, set `encoding` to an encoding
44
+ # understood by Ruby’s `Encoding`. If no encoding is set in the configuration,
45
+ # one will be inferred from the environment.
3
46
  #
4
47
  # @api private
5
- module Filesystem
48
+ class Filesystem < Nanoc::DataSource
6
49
  # See {Nanoc::DataSource#up}.
7
50
  def up
8
51
  end
@@ -21,16 +64,72 @@ module Nanoc::DataSources
21
64
 
22
65
  # See {Nanoc::DataSource#items}.
23
66
  def items
24
- load_objects(content_dir_name, 'item', Nanoc::Int::Item)
67
+ load_objects(content_dir_name, Nanoc::Int::Item)
25
68
  end
26
69
 
27
70
  # See {Nanoc::DataSource#layouts}.
28
71
  def layouts
29
- load_objects(layouts_dir_name, 'layout', Nanoc::Int::Layout)
72
+ load_objects(layouts_dir_name, Nanoc::Int::Layout)
30
73
  end
31
74
 
32
75
  protected
33
76
 
77
+ class ProtoDocument
78
+ attr_reader :attributes
79
+ attr_reader :checksum_data
80
+ attr_reader :is_binary
81
+ alias binary? is_binary
82
+
83
+ def initialize(is_binary:, content: nil, filename: nil, attributes:, checksum_data: nil)
84
+ if content.nil? && filename.nil?
85
+ raise ArgumentError, '#initialize needs at least content or filename'
86
+ end
87
+
88
+ @is_binary = is_binary
89
+ @content = content
90
+ @filename = filename
91
+ @attributes = attributes
92
+ @checksum_data = checksum_data
93
+ end
94
+
95
+ def content
96
+ if binary?
97
+ raise ArgumentError, 'cannot fetch content of binary item'
98
+ else
99
+ @content
100
+ end
101
+ end
102
+
103
+ def filename
104
+ if binary?
105
+ @filename
106
+ else
107
+ raise ArgumentError, 'cannot fetch filename of non-binary item'
108
+ end
109
+ end
110
+ end
111
+
112
+ def read_proto_document(content_filename, meta_filename, klass)
113
+ is_binary = content_filename && !@site_config[:text_extensions].include?(File.extname(content_filename)[1..-1])
114
+
115
+ if is_binary && klass == Nanoc::Int::Item
116
+ meta = (meta_filename && YAML.load_file(meta_filename)) || {}
117
+
118
+ ProtoDocument.new(is_binary: true, filename: content_filename, attributes: meta)
119
+ elsif is_binary && klass == Nanoc::Int::Layout
120
+ raise "The layout file '#{content_filename}' is a binary file, but layouts can only be textual"
121
+ else
122
+ parse_result = parse(content_filename, meta_filename)
123
+
124
+ ProtoDocument.new(
125
+ is_binary: false,
126
+ content: parse_result.content,
127
+ attributes: parse_result.attributes,
128
+ checksum_data: "content=#{parse_result.content},meta=#{parse_result.attributes_data}",
129
+ )
130
+ end
131
+ end
132
+
34
133
  # Creates instances of klass corresponding to the files in dir_name. The
35
134
  # kind attribute indicates the kind of object that is being loaded and is
36
135
  # used solely for debugging purposes.
@@ -42,76 +141,75 @@ module Nanoc::DataSources
42
141
  # metadata section.
43
142
  #
44
143
  # @see Nanoc::DataSources::Filesystem#load_objects
45
- def load_objects(dir_name, kind, klass)
144
+ def load_objects(dir_name, klass)
46
145
  res = []
47
146
 
48
147
  return [] if dir_name.nil?
49
148
 
50
149
  all_split_files_in(dir_name).each do |base_filename, (meta_ext, content_exts)|
51
150
  content_exts.each do |content_ext|
52
- # Get filenames
53
151
  meta_filename = filename_for(base_filename, meta_ext)
54
152
  content_filename = filename_for(base_filename, content_ext)
55
153
 
56
- # Read content and metadata
57
- is_binary = content_filename && !@site_config[:text_extensions].include?(File.extname(content_filename)[1..-1])
58
- if is_binary && klass == Nanoc::Int::Item
59
- meta = (meta_filename && YAML.load_file(meta_filename)) || {}
60
- content_or_filename = content_filename
61
- elsif is_binary && klass == Nanoc::Int::Layout
62
- raise "The layout file '#{content_filename}' is a binary file, but layouts can only be textual"
63
- else
64
- meta, content_or_filename = parse(content_filename, meta_filename, kind)
65
- end
154
+ proto_doc = read_proto_document(content_filename, meta_filename, klass)
66
155
 
67
- # Get attributes
68
- attributes = {
69
- filename: content_filename,
70
- content_filename: content_filename,
71
- meta_filename: meta_filename,
72
- extension: content_filename ? ext_of(content_filename)[1..-1] : nil,
73
- }.merge(meta)
74
-
75
- # Get identifier
76
- if content_filename
77
- identifier = identifier_for_filename(content_filename[dir_name.length..-1])
78
- elsif meta_filename
79
- identifier = identifier_for_filename(meta_filename[dir_name.length..-1])
80
- else
81
- raise 'meta_filename and content_filename are both nil'
82
- end
156
+ content = content_for(proto_doc, content_filename)
157
+ attributes = attributes_for(proto_doc, content_filename, meta_filename)
158
+ identifier = identifier_for(content_filename, meta_filename, dir_name)
83
159
 
84
- # Get modification times
85
- meta_mtime = meta_filename ? File.stat(meta_filename).mtime : nil
86
- content_mtime = content_filename ? File.stat(content_filename).mtime : nil
87
- if meta_mtime && content_mtime
88
- mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
89
- elsif meta_mtime
90
- mtime = meta_mtime
91
- elsif content_mtime
92
- mtime = content_mtime
93
- else
94
- raise 'meta_mtime and content_mtime are both nil'
95
- end
96
- attributes[:mtime] = mtime
97
-
98
- # Create content
99
- full_content_filename = content_filename && File.expand_path(content_filename)
100
- content =
101
- if is_binary
102
- Nanoc::Int::BinaryContent.new(full_content_filename)
103
- else
104
- Nanoc::Int::TextualContent.new(content_or_filename, filename: full_content_filename)
105
- end
106
-
107
- # Create object
108
- res << klass.new(content, attributes, identifier)
160
+ res << klass.new(content, attributes, identifier, checksum_data: proto_doc.checksum_data)
109
161
  end
110
162
  end
111
163
 
112
164
  res
113
165
  end
114
166
 
167
+ def attributes_for(proto_doc, content_filename, meta_filename)
168
+ extra_attributes = {
169
+ filename: content_filename,
170
+ content_filename: content_filename,
171
+ meta_filename: meta_filename,
172
+ extension: content_filename ? ext_of(content_filename)[1..-1] : nil,
173
+ mtime: mtime_of(content_filename, meta_filename),
174
+ }
175
+
176
+ extra_attributes.merge(proto_doc.attributes)
177
+ end
178
+
179
+ def identifier_for(content_filename, meta_filename, dir_name)
180
+ if content_filename
181
+ identifier_for_filename(content_filename[dir_name.length..-1])
182
+ elsif meta_filename
183
+ identifier_for_filename(meta_filename[dir_name.length..-1])
184
+ else
185
+ raise 'meta_filename and content_filename are both nil'
186
+ end
187
+ end
188
+
189
+ def content_for(proto_doc, content_filename)
190
+ full_content_filename = content_filename && File.expand_path(content_filename)
191
+
192
+ if proto_doc.binary?
193
+ Nanoc::Int::BinaryContent.new(full_content_filename)
194
+ else
195
+ Nanoc::Int::TextualContent.new(proto_doc.content, filename: full_content_filename)
196
+ end
197
+ end
198
+
199
+ def mtime_of(content_filename, meta_filename)
200
+ meta_mtime = meta_filename ? File.stat(meta_filename).mtime : nil
201
+ content_mtime = content_filename ? File.stat(content_filename).mtime : nil
202
+ if meta_mtime && content_mtime
203
+ meta_mtime > content_mtime ? meta_mtime : content_mtime
204
+ elsif meta_mtime
205
+ meta_mtime
206
+ elsif content_mtime
207
+ content_mtime
208
+ else
209
+ raise 'meta_mtime and content_mtime are both nil'
210
+ end
211
+ end
212
+
115
213
  # e.g.
116
214
  #
117
215
  # {
@@ -167,18 +265,30 @@ module Nanoc::DataSources
167
265
  # data sources may prefer to implement this differently (for example,
168
266
  # {Nanoc::DataSources::FilesystemVerbose} doubles the last part of the
169
267
  # basename before concatenating it with a period and the extension).
170
- def filename_for(_base_filename, _ext)
171
- raise NotImplementedError.new(
172
- "#{self.class} does not implement #filename_for",
173
- )
268
+ def filename_for(base_filename, ext)
269
+ if ext.nil?
270
+ nil
271
+ elsif ext.empty?
272
+ base_filename
273
+ else
274
+ base_filename + '.' + ext
275
+ end
174
276
  end
175
277
 
176
278
  # Returns the identifier that corresponds with the given filename, which
177
279
  # can be the content filename or the meta filename.
178
- def identifier_for_filename(_filename)
179
- raise NotImplementedError.new(
180
- "#{self.class} does not implement #identifier_for_filename",
181
- )
280
+ def identifier_for_filename(filename)
281
+ if config[:identifier_type] == 'full'
282
+ return Nanoc::Identifier.new(filename)
283
+ end
284
+
285
+ regex =
286
+ if filename =~ /(^|\/)index(\.[^\/]+)?$/
287
+ @config && @config[:allow_periods_in_identifiers] ? /\/?(index)?(\.[^\/\.]+)?$/ : /\/?index(\.[^\/]+)?$/
288
+ else
289
+ @config && @config[:allow_periods_in_identifiers] ? /\.[^\/\.]+$/ : /\.[^\/]+$/
290
+ end
291
+ Nanoc::Identifier.new(filename.sub(regex, ''), type: :legacy)
182
292
  end
183
293
 
184
294
  # Returns the base name of filename, i.e. filename with the first or all
@@ -198,6 +308,8 @@ module Nanoc::DataSources
198
308
  # Returns a regex that is used for determining the extension of a file
199
309
  # name. The first match group will be the entire extension, including the
200
310
  # leading period.
311
+ #
312
+ # @return [Regex]
201
313
  def extension_regex
202
314
  if @config && @config[:allow_periods_in_identifiers]
203
315
  /(\.[^\/\.]+$)/
@@ -206,32 +318,31 @@ module Nanoc::DataSources
206
318
  end
207
319
  end
208
320
 
209
- # Parses the file named `filename` and returns an array with its first
210
- # element a hash with the file's metadata, and with its second element the
211
- # file content itself.
212
- def parse(content_filename, meta_filename, _kind)
213
- # Read content and metadata from separate files
321
+ # @return [ParseResult]
322
+ def parse(content_filename, meta_filename)
214
323
  if meta_filename
215
- content = content_filename ? read(content_filename) : ''
216
- meta_raw = read(meta_filename)
217
- begin
218
- meta = YAML.load(meta_raw) || {}
219
- rescue Exception => e
220
- raise "Could not parse YAML for #{meta_filename}: #{e.message}"
221
- end
222
- verify_meta(meta, meta_filename)
223
- return [meta, content]
324
+ parse_with_separate_meta_filename(content_filename, meta_filename)
325
+ else
326
+ parse_with_frontmatter(content_filename)
224
327
  end
328
+ end
329
+
330
+ # @return [ParseResult]
331
+ def parse_with_separate_meta_filename(content_filename, meta_filename)
332
+ content = content_filename ? read(content_filename) : ''
333
+ meta_raw = read(meta_filename)
334
+ meta = parse_metadata(meta_raw, meta_filename)
335
+ ParseResult.new(content: content, attributes: meta, attributes_data: meta_raw)
336
+ end
225
337
 
226
- # Read data
338
+ # @return [ParseResult]
339
+ def parse_with_frontmatter(content_filename)
227
340
  data = read(content_filename)
228
341
 
229
- # Check presence of metadata section
230
342
  if data !~ /\A-{3,5}\s*$/
231
- return [{}, data]
343
+ return ParseResult.new(content: data, attributes: {}, attributes_data: '')
232
344
  end
233
345
 
234
- # Split data
235
346
  pieces = data.split(/^(-{5}|-{3})[ \t]*\r?\n?/, 3)
236
347
  if pieces.size < 4
237
348
  raise RuntimeError.new(
@@ -239,17 +350,35 @@ module Nanoc::DataSources
239
350
  )
240
351
  end
241
352
 
242
- # Parse
353
+ meta = parse_metadata(pieces[2], content_filename)
354
+ content = pieces[4]
355
+
356
+ ParseResult.new(content: content, attributes: meta, attributes_data: pieces[2])
357
+ end
358
+
359
+ # @return [Hash]
360
+ def parse_metadata(data, filename)
243
361
  begin
244
- meta = YAML.load(pieces[2]) || {}
362
+ meta = YAML.load(data) || {}
245
363
  rescue Exception => e
246
- raise "Could not parse YAML for #{content_filename}: #{e.message}"
364
+ raise "Could not parse YAML for #{filename}: #{e.message}"
247
365
  end
248
- verify_meta(meta, content_filename)
249
- content = pieces[4]
250
366
 
251
- # Done
252
- [meta, content]
367
+ verify_meta(meta, filename)
368
+
369
+ meta
370
+ end
371
+
372
+ class ParseResult
373
+ attr_reader :content
374
+ attr_reader :attributes
375
+ attr_reader :attributes_data
376
+
377
+ def initialize(content:, attributes:, attributes_data:)
378
+ @content = content
379
+ @attributes = attributes
380
+ @attributes_data = attributes_data
381
+ end
253
382
  end
254
383
 
255
384
  class InvalidMetadataError < Nanoc::Error