nanoc 4.1.6 → 4.2.0b1

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 (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