nanoc3 3.0.9 → 3.1.0a1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/LICENSE +1 -1
  2. data/NEWS.md +360 -0
  3. data/README.md +85 -0
  4. data/Rakefile +2 -2
  5. data/bin/nanoc3 +0 -4
  6. data/lib/nanoc3/base/code_snippet.rb +14 -6
  7. data/lib/nanoc3/base/compiler.rb +68 -49
  8. data/lib/nanoc3/base/compiler_dsl.rb +70 -29
  9. data/lib/nanoc3/base/context.rb +47 -0
  10. data/lib/nanoc3/base/core_ext/array.rb +4 -0
  11. data/lib/nanoc3/base/core_ext/hash.rb +5 -1
  12. data/lib/nanoc3/base/core_ext/string.rb +2 -0
  13. data/lib/nanoc3/base/data_source.rb +132 -96
  14. data/lib/nanoc3/base/dependency_tracker.rb +160 -185
  15. data/lib/nanoc3/base/directed_graph.rb +252 -0
  16. data/lib/nanoc3/base/errors.rb +52 -4
  17. data/lib/nanoc3/base/filter.rb +43 -28
  18. data/lib/nanoc3/base/item.rb +93 -25
  19. data/lib/nanoc3/base/item_rep.rb +166 -55
  20. data/lib/nanoc3/base/layout.rb +16 -13
  21. data/lib/nanoc3/base/notification_center.rb +28 -12
  22. data/lib/nanoc3/base/plugin_registry.rb +158 -0
  23. data/lib/nanoc3/base/rule.rb +27 -8
  24. data/lib/nanoc3/base/rule_context.rb +59 -46
  25. data/lib/nanoc3/base/site.rb +124 -77
  26. data/lib/nanoc3/base.rb +7 -2
  27. data/lib/nanoc3/cli/base.rb +4 -1
  28. data/lib/nanoc3/cli/commands/autocompile.rb +5 -4
  29. data/lib/nanoc3/cli/commands/compile.rb +28 -7
  30. data/lib/nanoc3/cli/commands/create_item.rb +1 -1
  31. data/lib/nanoc3/cli/commands/create_layout.rb +1 -1
  32. data/lib/nanoc3/cli/commands/create_site.rb +46 -22
  33. data/lib/nanoc3/cli/commands/debug.rb +100 -0
  34. data/lib/nanoc3/cli/commands/help.rb +1 -1
  35. data/lib/nanoc3/cli/commands/info.rb +1 -1
  36. data/lib/nanoc3/cli/commands/view.rb +85 -0
  37. data/lib/nanoc3/cli/commands.rb +2 -0
  38. data/lib/nanoc3/cli/logger.rb +7 -0
  39. data/lib/nanoc3/cli.rb +0 -3
  40. data/lib/nanoc3/data_sources/{delicious.rb → deprecated/delicious.rb} +1 -24
  41. data/lib/nanoc3/data_sources/{last_fm.rb → deprecated/last_fm.rb} +1 -27
  42. data/lib/nanoc3/data_sources/{twitter.rb → deprecated/twitter.rb} +1 -14
  43. data/lib/nanoc3/data_sources/filesystem.rb +188 -176
  44. data/lib/nanoc3/data_sources/filesystem_unified.rb +107 -0
  45. data/lib/nanoc3/data_sources/filesystem_verbose.rb +80 -0
  46. data/lib/nanoc3/data_sources.rb +18 -9
  47. data/lib/nanoc3/extra/core_ext/enumerable.rb +39 -0
  48. data/lib/nanoc3/extra/core_ext/time.rb +2 -2
  49. data/lib/nanoc3/extra/core_ext.rb +1 -0
  50. data/lib/nanoc3/extra/deployers/rsync.rb +49 -3
  51. data/lib/nanoc3/extra/file_proxy.rb +7 -0
  52. data/lib/nanoc3/extra/vcs.rb +25 -24
  53. data/lib/nanoc3/extra/vcses/bazaar.rb +4 -0
  54. data/lib/nanoc3/extra/vcses/dummy.rb +4 -0
  55. data/lib/nanoc3/extra/vcses/git.rb +4 -0
  56. data/lib/nanoc3/extra/vcses/mercurial.rb +4 -0
  57. data/lib/nanoc3/extra/vcses/subversion.rb +4 -0
  58. data/lib/nanoc3/extra.rb +4 -1
  59. data/lib/nanoc3/filters/erb.rb +1 -1
  60. data/lib/nanoc3/filters/erubis.rb +1 -1
  61. data/lib/nanoc3/filters/haml.rb +1 -1
  62. data/lib/nanoc3/filters/kramdown.rb +14 -0
  63. data/lib/nanoc3/filters/maruku.rb +1 -1
  64. data/lib/nanoc3/filters/rainpress.rb +1 -1
  65. data/lib/nanoc3/filters/rdiscount.rb +3 -1
  66. data/lib/nanoc3/filters.rb +2 -0
  67. data/lib/nanoc3/helpers/blogging.rb +91 -75
  68. data/lib/nanoc3/helpers/breadcrumbs.rb +18 -10
  69. data/lib/nanoc3/helpers/capturing.rb +24 -29
  70. data/lib/nanoc3/helpers/filtering.rb +20 -17
  71. data/lib/nanoc3/helpers/html_escape.rb +7 -4
  72. data/lib/nanoc3/helpers/link_to.rb +51 -41
  73. data/lib/nanoc3/helpers/rendering.rb +15 -8
  74. data/lib/nanoc3/helpers/tagging.rb +27 -21
  75. data/lib/nanoc3/helpers/text.rb +12 -8
  76. data/lib/nanoc3/helpers/xml_sitemap.rb +13 -15
  77. data/lib/nanoc3/tasks/deploy/rsync.rake +4 -1
  78. data/lib/nanoc3/tasks.rb +2 -1
  79. data/lib/nanoc3.rb +24 -1
  80. metadata +43 -87
  81. data/NEWS.rdoc +0 -328
  82. data/README.rdoc +0 -83
  83. data/lib/nanoc3/base/plugin.rb +0 -88
  84. data/lib/nanoc3/base/preprocessor_context.rb +0 -37
  85. data/lib/nanoc3/data_sources/filesystem_combined.rb +0 -214
  86. data/lib/nanoc3/data_sources/filesystem_common.rb +0 -22
  87. data/lib/nanoc3/data_sources/filesystem_compact.rb +0 -256
  88. data/lib/nanoc3/extra/context.rb +0 -24
  89. data/lib/nanoc3/package.rb +0 -107
  90. data/vendor/cri/ChangeLog +0 -0
  91. data/vendor/cri/LICENSE +0 -19
  92. data/vendor/cri/NEWS +0 -0
  93. data/vendor/cri/README +0 -4
  94. data/vendor/cri/Rakefile +0 -25
  95. data/vendor/cri/lib/cri/base.rb +0 -153
  96. data/vendor/cri/lib/cri/command.rb +0 -105
  97. data/vendor/cri/lib/cri/core_ext/string.rb +0 -41
  98. data/vendor/cri/lib/cri/core_ext.rb +0 -8
  99. data/vendor/cri/lib/cri/option_parser.rb +0 -186
  100. data/vendor/cri/lib/cri.rb +0 -12
  101. data/vendor/cri/test/test_base.rb +0 -6
  102. data/vendor/cri/test/test_command.rb +0 -6
  103. data/vendor/cri/test/test_core_ext.rb +0 -21
  104. data/vendor/cri/test/test_option_parser.rb +0 -279
@@ -4,38 +4,47 @@ require 'pstore'
4
4
 
5
5
  module Nanoc3
6
6
 
7
- # Nanoc3::DependencyTracker is responsible for remembering dependencies
8
- # between items. It is used to speed up compilation by only letting an item
9
- # be recompiled when it is outdated or any of its dependencies (or
10
- # dependencies' dependencies, etc) is outdated.
7
+ # Responsible for remembering dependencies between items. It is used to
8
+ # speed up compilation by only letting an item be recompiled when it is
9
+ # outdated or any of its dependencies (or dependencies’ dependencies, etc)
10
+ # is outdated.
11
11
  #
12
12
  # The dependencies tracked by the dependency tracker are not dependencies
13
- # based on an item's content. When one item uses an attribute of another
13
+ # based on an items content. When one item uses an attribute of another
14
14
  # item, then this is also treated as a dependency. While dependencies based
15
- # on an item's content (handled in Nanoc3::Compiler) cannot be mutually
15
+ # on an items content (handled in {Nanoc3::Compiler}) cannot be mutually
16
16
  # recursive, the more general dependencies in Nanoc3::DependencyTracker can
17
17
  # (e.g. item A can use an attribute of item B and vice versa without
18
18
  # problems).
19
+ #
20
+ # The dependency tracker remembers the dependency information between runs.
21
+ # Dependency information is stored in the `tmp/dependencies` file. This file
22
+ # also contains a version number; when a dependencies file with an
23
+ # incompatible version is found, it is ignored.
19
24
  class DependencyTracker
20
25
 
26
+ # @return [String] The name of the file in which dependency information is
27
+ # stored
21
28
  attr_accessor :filename
22
29
 
23
- # FIXME The way the graph is stored is not exactly great. An adjacency
24
- # matrix (wrapped in a Graph class, perhaps) would be a lot easier to work
25
- # with, and it would not require an inverse graph to be maintained.
30
+ # The version of the file format used to store dependencies.
31
+ STORE_VERSION = 2
26
32
 
27
33
  # Creates a new dependency tracker for the given items.
34
+ #
35
+ # @param [Array<Nanoc3::Item>] item The list of items whose dependencies
36
+ # should be managed
28
37
  def initialize(items)
29
- @items = items
30
-
31
- @filename = 'tmp/dependencies'
32
-
33
- @graph = {}
34
- @inverse_graph = {}
38
+ @items = items
39
+ @filename = 'tmp/dependencies'
40
+ @graph = Nanoc3::DirectedGraph.new([ nil ] + @items)
41
+ @previous_items = []
35
42
  end
36
43
 
37
- # Starts listening for dependency messages (+:visit_started+ and
38
- # +:visit_ended+) and start recording dependencies.
44
+ # Starts listening for dependency messages (`:visit_started` and
45
+ # `:visit_ended`) and start recording dependencies.
46
+ #
47
+ # @return [void]
39
48
  def start
40
49
  # Initialize dependency stack. An item will be pushed onto this stack
41
50
  # when it is visited. Therefore, an item on the stack always depends on
@@ -44,8 +53,11 @@ module Nanoc3
44
53
 
45
54
  # Register start of visits
46
55
  Nanoc3::NotificationCenter.on(:visit_started, self) do |item|
56
+ $stderr.puts "*** IN stack.size=#{@stack.size} item=#{item.inspect}" if $DEBUG
57
+
47
58
  # Record possible dependency
48
59
  unless @stack.empty?
60
+ $stderr.puts "*** Recording dependency #{@stack[-1].inspect} -> #{item.inspect}" if $DEBUG
49
61
  self.record_dependency(@stack[-1], item)
50
62
  end
51
63
 
@@ -54,236 +66,199 @@ module Nanoc3
54
66
 
55
67
  # Register end of visits
56
68
  Nanoc3::NotificationCenter.on(:visit_ended, self) do |item|
69
+ $stderr.puts "*** OUT stack.size=#{@stack.size} item=#{item.inspect}" if $DEBUG
70
+
57
71
  @stack.pop
58
72
  end
59
73
  end
60
74
 
61
75
  # Stop listening for dependency messages and stop recording dependencies.
76
+ #
77
+ # @return [void]
62
78
  def stop
63
79
  # Unregister
64
80
  Nanoc3::NotificationCenter.remove(:visit_started, self)
65
81
  Nanoc3::NotificationCenter.remove(:visit_ended, self)
66
82
  end
67
83
 
68
- # Returns the direct dependencies for +item+, i.e. the items that, when
69
- # outdated, will cause +item+ to be marked as outdated. Indirect
70
- # dependencies will not be returned (e.g. if A depends on B which depends
71
- # on C, then the direct dependencies of A do not include C).
72
- def direct_dependencies_for(item)
73
- @graph[item] || []
84
+ # Returns the direct dependencies for `item`.
85
+ #
86
+ # The direct dependencies of `item` include the items that, when outdated
87
+ # will cause `item` to be marked as outdated. Indirect dependencies will
88
+ # not be returned (e.g. if A depends on B which depends on C, then the
89
+ # direct dependencies of A do not include C).
90
+ #
91
+ # @param [Nanoc3::Item] item The item for which to fetch the direct
92
+ # predecessors
93
+ #
94
+ # @return [Array<Nanoc3::Item>] The direct predecessors of the given item
95
+ def direct_predecessors_of(item)
96
+ @graph.direct_predecessors_of(item).compact
74
97
  end
75
98
 
76
- # Returns all dependencies (direct and indirect) for +item+, i.e. the
77
- # items that, when outdated, will cause +item+ to be marked as outdated.
78
- def all_dependencies_for(item)
79
- # FIXME can result in an infinite loop
80
-
81
- direct_dependencies = direct_dependencies_for(item)
82
- indirect_dependencies = direct_dependencies.map { |i| all_dependencies_for(i) }
83
-
84
- (direct_dependencies + indirect_dependencies).flatten
99
+ # Returns all dependencies (direct and indirect) for `item`.
100
+ #
101
+ # The dependencies of `item` include the items that, when outdated, will
102
+ # cause `item` to be marked as outdated.
103
+ #
104
+ # @param [Nanoc3::Item] item The item for which to fetch all direct and
105
+ # indirect predecessors
106
+ #
107
+ # @return [Array<Nanoc3::Item>] The predecessors of the given item
108
+ def predecessors_of(item)
109
+ @graph.predecessors_of(item).compact
85
110
  end
86
111
 
87
- # Returns the direct inverse dependencies for +item+, i.e. the items that
88
- # will be marked as outdated when +item+ is outdated. Indirect
89
- # dependencies will not be returned (e.g. if A depends on B which depends
90
- # on C, then the direct inverse dependencies of C do not include A).
91
- def direct_inverse_dependencies_for(item)
92
- inverted_graph[item] || []
112
+ # Returns the direct inverse dependencies for `item`.
113
+ #
114
+ # The direct inverse dependencies of `item` include the items that will be
115
+ # marked as outdated when`+item` is outdated. Indirect dependencies will
116
+ # not be returned (e.g. if A depends on B which depends on C, then the
117
+ # direct inverse dependencies of C do not include A).
118
+ #
119
+ # @param [Nanoc3::Item] item The item for which to fetch the direct
120
+ # successors
121
+ #
122
+ # @return [Array<Nanoc3::Item>] The direct successors of the given item
123
+ def direct_successors_of(item)
124
+ @graph.direct_successors_of(item).compact
93
125
  end
94
126
 
95
- # Returns all inverse dependencies (direct and indirect) for +item+, i.e.
96
- # the items that will be marked as outdated when +item+ is outdated.
97
- def all_inverse_dependencies_for(item)
98
- # Init list of all found dependencies
99
- all_dependencies = []
100
-
101
- # Init lists with already checked and not yet checked dependencies
102
- checked_direct_dependencies = []
103
- pending_direct_dependencies = direct_inverse_dependencies_for(item)
104
-
105
- while !pending_direct_dependencies.empty?
106
- # Get next unchecked dependency
107
- dependency = pending_direct_dependencies.shift
108
- next if checked_direct_dependencies.include?(dependency)
109
-
110
- # Add dependencies of this unchecked dependency
111
- pending_direct_dependencies += direct_inverse_dependencies_for(dependency)
112
-
113
- # Mark this dependency as handled
114
- all_dependencies << dependency
115
- checked_direct_dependencies << dependency
116
- end
117
-
118
- all_dependencies
127
+ # Returns all inverse dependencies (direct and indirect) for `item`.
128
+ #
129
+ # The inverse dependencies of `item` include the items that will be marked
130
+ # as outdated when `item` is outdated.
131
+ #
132
+ # @param [Nanoc3::Item] item The item for which to fetch all direct and
133
+ # indirect successors
134
+ #
135
+ # @return [Array<Nanoc3::Item>] The successors of the given item
136
+ def successors_of(item)
137
+ @graph.successors_of(item).compact
119
138
  end
120
139
 
121
- # Records a dependency from +src+ to +dst+ in the dependency graph. When
122
- # +dst+ is oudated, +src+ will also become outdated.
140
+ # Records a dependency from `src` to `dst` in the dependency graph. When
141
+ # `dst` is oudated, `src` will also become outdated.
142
+ #
143
+ # @param [Nanoc3::Item] src The source of the dependency, i.e. the item
144
+ # that will become outdated if dst is outdated
145
+ #
146
+ # @param [Nanoc3::Item] dst The destination of the dependency, i.e. the
147
+ # item that will cause the source to become outdated if the destination
148
+ # is outdated
149
+ #
150
+ # @return [void]
123
151
  def record_dependency(src, dst)
124
- # Initialize graph if necessary
125
- @graph[src] ||= []
126
-
127
- # Don't include self or doubles in dependencies
128
- return if src == dst
129
- return if @graph[src].include?(dst)
130
-
131
- # Record dependency
132
- invalidate_inverted_graph
133
- @graph[src] << dst
152
+ # Warning! dst and src are *reversed* here!
153
+ @graph.add_edge(dst, src) unless src == dst
134
154
  end
135
155
 
136
- # Stores the dependency graph into the file specified by the +filename+
156
+ # Stores the dependency graph into the file specified by the {#filename}
137
157
  # attribute.
158
+ #
159
+ # @return [void]
138
160
  def store_graph
139
- # Create dir
140
161
  FileUtils.mkdir_p(File.dirname(self.filename))
141
-
142
- # Complete the graph
143
- complete_graph
144
-
145
- # Convert graph of items into graph of item identifiers
146
- new_graph = {}
147
- @graph.each_pair do |second_item, first_items|
148
- # Don't store nil because that would be pointless (if first_item is
149
- # outdated, something that does not exist is also outdated… makes no
150
- # sense).
151
- # FIXME can second_item really be nil?
152
- next if second_item.nil?
153
-
154
- new_graph[second_item.identifier] = first_items.map { |f| f && f.identifier }.compact
155
- end
156
-
157
- # Store dependencies
158
162
  store = PStore.new(self.filename)
159
163
  store.transaction do
160
- store[:dependencies] = new_graph
164
+ store[:version] = STORE_VERSION
165
+ store[:vertices] = @graph.vertices.map { |i| i && i.identifier }
166
+ store[:edges] = @graph.edges
161
167
  end
162
168
  end
163
169
 
164
- # Loads the dependency graph from the file specified by the +filename+
170
+ # Loads the dependency graph from the file specified by the {#filename}
165
171
  # attribute. This method will overwrite an existing dependency graph.
172
+ #
173
+ # @return [void]
166
174
  def load_graph
167
175
  # Create new graph
168
- @graph = {}
176
+ @graph = Nanoc3::DirectedGraph.new([ nil ] + @items)
169
177
 
170
- # Don't do anything if dependencies haven't been stored yet
178
+ # Get store
171
179
  return if !File.file?(self.filename)
180
+ store = PStore.new(self.filename)
172
181
 
173
182
  # Load dependencies
174
- store = PStore.new(self.filename)
175
183
  store.transaction do
176
- # Convert graph of identifiers into graph of items
177
- store[:dependencies].each_pair do |second_item_identifier, first_item_identifiers|
178
- # Convert second and first item identifiers into items
179
- second_item = item_with_identifier(second_item_identifier)
180
- first_items = first_item_identifiers.map { |p| item_with_identifier(p) }
184
+ # Verify version
185
+ return if store[:version] != STORE_VERSION
186
+
187
+ # Load vertices
188
+ @previous_items = store[:vertices].map do |v|
189
+ @items.find { |i| i.identifier == v }
190
+ end
181
191
 
182
- @graph[second_item] = first_items
192
+ # Load edges
193
+ store[:edges].each do |edge|
194
+ from_index, to_index = *edge
195
+ from, to = @previous_items[from_index], @previous_items[to_index]
196
+ @graph.add_edge(from, to)
183
197
  end
184
198
  end
185
199
  end
186
200
 
187
201
  # Traverses the dependency graph and marks all items that (directly or
188
202
  # indirectly) depend on an outdated item as outdated.
203
+ #
204
+ # @return [void]
189
205
  def mark_outdated_items
190
206
  # Unmark everything
191
- @items.each { |i| i.dependencies_outdated = false }
192
-
193
- # Mark items that appear in @items but not in the dependency graph
194
- added_items = @items - @graph.keys
195
- added_items.each { |i| i.dependencies_outdated = true }
196
-
197
- # Walk graph and mark items as outdated if necessary
198
- # (#keys and #sort is used instead of #each_pair to add determinism)
199
- first_items = inverted_graph.keys.sort_by { |i| i.nil? ? '/' : i.identifier }
200
- something_changed = true
201
- while something_changed
202
- something_changed = false
203
-
204
- first_items.each do |first_item|
205
- second_items = inverted_graph[first_item]
206
-
207
- if first_item.nil? || # item was removed
208
- first_item.outdated? || # item itself is outdated
209
- first_item.dependencies_outdated? # item is outdated because of its dependencies
210
- second_items.each do |item|
211
- # Ignore this item
212
- next if item.nil?
213
-
214
- something_changed = true if !item.dependencies_outdated?
215
- item.dependencies_outdated = true
216
- end
217
- end
207
+ @items.each { |i| i.outdated_due_to_dependencies = false }
208
+
209
+ # Mark new items as outdated
210
+ added_items = @items - @previous_items
211
+ added_items.each { |i| i.outdated_due_to_dependencies = true }
212
+
213
+ # Mark successors of nil as outdated
214
+ self.successors_of(nil).each do |i|
215
+ i.outdated_due_to_dependencies = true
216
+ end
217
+
218
+ # For each outdated item...
219
+ @items.select { |i| i.outdated? }.each do |outdated_item|
220
+ # ... mark all its successors as outdated
221
+ self.successors_of(outdated_item).each do |i|
222
+ i.outdated_due_to_dependencies = true
218
223
  end
219
224
  end
220
225
  end
221
226
 
222
227
  # Empties the list of dependencies for the given item. This is necessary
223
228
  # before recompiling the given item, because otherwise old dependencies
224
- # will stick around and new dependencies will appear twice.
229
+ # will stick around and new dependencies will appear twice. This function
230
+ # removes all incoming edges for the given vertex.
231
+ #
232
+ # @param [Nanoc3::Item] item The item for which to forget all dependencies
233
+ #
234
+ # @return [void]
225
235
  def forget_dependencies_for(item)
226
- @graph[item] = []
227
- end
228
-
229
- private
230
-
231
- # Returns the item with the given identifier, or nil if no item is found.
232
- def item_with_identifier(identifier)
233
- @items.find { |i| i.identifier == identifier }
234
- end
235
-
236
- # Returns the inverted dependency graph, creating it first if it does not
237
- # exist yet or is outdated. In this graph, the keys will be outdated when
238
- # any of the values are outdated.
239
- def inverted_graph
240
- @inverted_graph ||= invert_graph(@graph)
241
- end
242
-
243
- # Marks the inverted graph as outdated so that it will be regenerated the
244
- # next time it is used.
245
- def invalidate_inverted_graph
246
- @inverted_graph = nil
236
+ @graph.vertices.each do |v|
237
+ @graph.remove_edge(v, item)
238
+ end
247
239
  end
248
240
 
249
- # Inverts the given graph (keys become values and values become keys).
250
- #
251
- # For example, this graph
252
- #
253
- # {
254
- # :a => [ :b, :c ],
255
- # :b => [ :x, :c ]
256
- # }
257
- #
258
- # is turned into
241
+ # Prints the dependency graph in human-readable form.
259
242
  #
260
- # {
261
- # :b => [ :a ],
262
- # :c => [ :a, :b ],
263
- # :x => [ :b ]
264
- # }
265
- def invert_graph(graph)
266
- inverted_graph = {}
267
-
268
- graph.each_pair do |key, values|
269
- values.each do |v|
270
- inverted_graph[v] ||= []
271
- inverted_graph[v] << key
243
+ # @return [void]
244
+ def print_graph
245
+ @items.each do |item|
246
+ puts "#{item.inspect} depends on:"
247
+
248
+ predecessors = direct_predecessors_of(item)
249
+ predecessors.each do |pred|
250
+ puts " #{pred.inspect}"
272
251
  end
252
+ puts " (nothing!)" if predecessors.empty?
253
+ puts
273
254
  end
274
-
275
- inverted_graph
276
255
  end
277
256
 
278
- # Ensures that all items in the dependency graph have a list of
279
- # dependecies, even if it is empty. Items without a list of dependencies
280
- # will be treated as "added" and will depend on all other pages, which is
281
- # not necessary for non-added items.
282
- def complete_graph
283
- @items.each do |item|
284
- @graph[item] ||= []
285
- end
257
+ private
286
258
 
259
+ # Returns the item with the given identifier, or nil if no item is found.
260
+ def item_with_identifier(identifier)
261
+ @items.find { |i| i.identifier == identifier }
287
262
  end
288
263
 
289
264
  end