jekyll-relationships 0.1.0.alpha → 0.2.0.alpha

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jekyll-relationships/configuration/debug_setting.rb +72 -12
  3. data/lib/jekyll-relationships/configuration/defaults.rb +3 -1
  4. data/lib/jekyll-relationships/configuration/frontmatter.rb +28 -1
  5. data/lib/jekyll-relationships/configuration/parser.rb +20 -3
  6. data/lib/jekyll-relationships/configuration/prune_rule_settings.rb +33 -5
  7. data/lib/jekyll-relationships/configuration/tree_frontmatter.rb +8 -2
  8. data/lib/jekyll-relationships/configuration.rb +43 -8
  9. data/lib/jekyll-relationships/debug_logger.rb +94 -3
  10. data/lib/jekyll-relationships/definitions/normal_relationship.rb +13 -2
  11. data/lib/jekyll-relationships/definitions/prune_rule.rb +19 -3
  12. data/lib/jekyll-relationships/definitions/tree_relationship.rb +8 -2
  13. data/lib/jekyll-relationships/documents/registry.rb +182 -33
  14. data/lib/jekyll-relationships/engine/normal_seed.rb +222 -0
  15. data/lib/jekyll-relationships/engine/persisted_links.rb +365 -0
  16. data/lib/jekyll-relationships/engine/raw_path_state.rb +46 -116
  17. data/lib/jekyll-relationships/engine/relationship_state.rb +67 -56
  18. data/lib/jekyll-relationships/engine/session.rb +159 -19
  19. data/lib/jekyll-relationships/engine/write_back.rb +3 -42
  20. data/lib/jekyll-relationships/engine.rb +168 -46
  21. data/lib/jekyll-relationships/pruning/rule_pruner.rb +7 -7
  22. data/lib/jekyll-relationships/pruning/tree_phase.rb +128 -124
  23. data/lib/jekyll-relationships/pruning/tree_provenance.rb +1 -1
  24. data/lib/jekyll-relationships/references/accumulator.rb +87 -9
  25. data/lib/jekyll-relationships/references/template.rb +46 -11
  26. data/lib/jekyll-relationships/resolvers/base.rb +59 -2
  27. data/lib/jekyll-relationships/support/frontmatter_matcher.rb +76 -0
  28. data/lib/jekyll-relationships/support/placeholders.rb +1 -0
  29. data/lib/jekyll-relationships/trees/edge_builder.rb +43 -11
  30. data/lib/jekyll-relationships/trees/graph.rb +135 -17
  31. data/lib/jekyll-relationships/trees/root_distances.rb +44 -0
  32. data/lib/jekyll-relationships/version.rb +1 -1
  33. data/lib/jekyll-relationships.rb +1 -0
  34. data/readme.md +185 -26
  35. metadata +6 -2
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jekyll-relationships/trees/edge_builder'
4
+ require 'jekyll-relationships/trees/root_distances'
4
5
 
5
6
  module Jekyll
6
7
  module Plugins
@@ -27,12 +28,16 @@ class Graph
27
28
  @child_edge_entries = Hash.new { |hash, key| hash[key] = [] }
28
29
  @ancestor_cache = {}
29
30
  @descendant_cache = {}
31
+ @root_distance_cache = nil
30
32
  @primary_path_by_collection = {}
33
+ @scope_fields_by_collection = {}
31
34
  @tree_collections = @configuration.tree_relationships.each_with_object(Set.new) do |definition, collections|
32
35
  collections << definition.from_collection
33
36
  collections << definition.to_collection
34
37
  @primary_path_by_collection[definition.from_collection] ||= definition.primary_path
35
38
  @primary_path_by_collection[definition.to_collection] ||= definition.primary_path
39
+ @scope_fields_by_collection[definition.from_collection] ||= definition.scope_fields
40
+ @scope_fields_by_collection[definition.to_collection] ||= definition.scope_fields
36
41
  end
37
42
  @edge_builder = EdgeBuilder.new(
38
43
  graph: self,
@@ -48,6 +53,35 @@ class Graph
48
53
  @edge_builder.build!
49
54
  end
50
55
 
56
+ # Returns one deep copy of the current tree graph state.
57
+ def deep_dup
58
+ duplicated_graph = self.class.new(
59
+ site: @site,
60
+ configuration: @configuration,
61
+ registry: @registry,
62
+ data_path: @data_path,
63
+ debug_logger: @debug_logger,
64
+ active_document_ids: @active_document_ids.nil? ? nil : @active_document_ids.keys
65
+ )
66
+ duplicated_graph.instance_variable_set(
67
+ :@parents,
68
+ duplicate_document_adjacency(@parents)
69
+ )
70
+ duplicated_graph.instance_variable_set(
71
+ :@children,
72
+ duplicate_document_adjacency(@children)
73
+ )
74
+ duplicated_graph.instance_variable_set(
75
+ :@parent_edge_entries,
76
+ duplicate_edge_entries(@parent_edge_entries)
77
+ )
78
+ duplicated_graph.instance_variable_set(
79
+ :@child_edge_entries,
80
+ duplicate_edge_entries(@child_edge_entries)
81
+ )
82
+ duplicated_graph
83
+ end
84
+
51
85
  # Returns true when one document is part of any tree relationship.
52
86
  def participating?(document)
53
87
  @tree_collections.include?(document.collection.label)
@@ -68,19 +102,21 @@ class Graph
68
102
  end
69
103
 
70
104
  # Returns immediate parents as canonical reference hashes.
71
- def parents_for(document, primary_path: nil)
105
+ def parents_for(document, primary_path: nil, scope_fields: nil)
72
106
  return [] unless active_document?(document)
73
107
 
74
108
  resolved_primary_path = resolve_primary_path(document: document, primary_path: primary_path)
75
- @parents[document].map { |parent| build_reference(parent, primary_path: resolved_primary_path) }
109
+ resolved_scope_fields = resolve_scope_fields(document: document, scope_fields: scope_fields)
110
+ @parents[document].map { |parent| build_reference(parent, primary_path: resolved_primary_path, scope_fields: resolved_scope_fields) }
76
111
  end
77
112
 
78
113
  # Returns immediate children as canonical reference hashes.
79
- def children_for(document, primary_path: nil)
114
+ def children_for(document, primary_path: nil, scope_fields: nil)
80
115
  return [] unless active_document?(document)
81
116
 
82
117
  resolved_primary_path = resolve_primary_path(document: document, primary_path: primary_path)
83
- @children[document].map { |child| build_reference(child, primary_path: resolved_primary_path) }
118
+ resolved_scope_fields = resolve_scope_fields(document: document, scope_fields: scope_fields)
119
+ @children[document].map { |child| build_reference(child, primary_path: resolved_primary_path, scope_fields: resolved_scope_fields) }
84
120
  end
85
121
 
86
122
  # Returns the immediate parent documents in deterministic insertion order.
@@ -112,31 +148,42 @@ class Graph
112
148
  end
113
149
 
114
150
  # Returns ancestor references filtered by minimum and maximum distance.
115
- def ancestors_for(document, primary_path: nil, min: 0, max: -1)
151
+ def ancestors_for(document, primary_path: nil, scope_fields: nil, min: 0, max: -1)
116
152
  return [] unless active_document?(document)
117
153
 
118
154
  resolved_primary_path = resolve_primary_path(document: document, primary_path: primary_path)
155
+ resolved_scope_fields = resolve_scope_fields(document: document, scope_fields: scope_fields)
119
156
  filter_distances(
120
157
  distance_map_for(document: document, direction: :up),
121
158
  primary_path: resolved_primary_path,
159
+ scope_fields: resolved_scope_fields,
122
160
  min: min,
123
161
  max: max
124
162
  )
125
163
  end
126
164
 
127
165
  # Returns descendant references filtered by minimum and maximum distance.
128
- def descendants_for(document, primary_path: nil, min: 0, max: -1)
166
+ def descendants_for(document, primary_path: nil, scope_fields: nil, min: 0, max: -1)
129
167
  return [] unless active_document?(document)
130
168
 
131
169
  resolved_primary_path = resolve_primary_path(document: document, primary_path: primary_path)
170
+ resolved_scope_fields = resolve_scope_fields(document: document, scope_fields: scope_fields)
132
171
  filter_distances(
133
172
  distance_map_for(document: document, direction: :down),
134
173
  primary_path: resolved_primary_path,
174
+ scope_fields: resolved_scope_fields,
135
175
  min: min,
136
176
  max: max
137
177
  )
138
178
  end
139
179
 
180
+ # Returns the shortest number of edges from the document to any root.
181
+ def root_distance_for(document)
182
+ return nil unless active_document?(document)
183
+
184
+ root_distances_for_graph[document.object_id]
185
+ end
186
+
140
187
  # Adds one parent-child edge unless it would break tree guarantees.
141
188
  def add_edge(parent_document:, child_document:, tree_settings:, definition:, source_description:)
142
189
  return unless active_document?(parent_document) && active_document?(child_document)
@@ -220,7 +267,7 @@ class Graph
220
267
  tree_settings: tree_settings,
221
268
  source_description: source_description
222
269
  )
223
- clear_distance_cache
270
+ clear_traversal_cache
224
271
  debug_tree_event(
225
272
  document: child_document,
226
273
  definition: definition,
@@ -240,6 +287,32 @@ class Graph
240
287
  end
241
288
  end
242
289
 
290
+ # Removes one document from the graph along with every adjacent edge.
291
+ def remove_document!(document)
292
+ return false unless active_document?(document)
293
+
294
+ @active_document_ids.delete(document.object_id) unless @active_document_ids.nil?
295
+ @children[document].dup.each do |child_document|
296
+ remove_parent_edge(parent_document: document, child_document: child_document)
297
+ end
298
+ @parents[document].dup.each do |parent_document|
299
+ remove_parent_edge(parent_document: parent_document, child_document: document)
300
+ end
301
+ @children.delete(document)
302
+ @parents.delete(document)
303
+ @child_edge_entries.delete(document)
304
+ @parent_edge_entries.delete(document)
305
+ clear_traversal_cache
306
+ true
307
+ end
308
+
309
+ # Removes many documents from the graph.
310
+ def remove_documents!(documents)
311
+ Array(documents).each do |document|
312
+ remove_document!(document)
313
+ end
314
+ end
315
+
243
316
  # Returns every document participating in tree relationships.
244
317
  def tree_documents
245
318
  @tree_collections.to_a.flat_map { |collection| documents_for(collection) }.uniq
@@ -252,10 +325,11 @@ class Graph
252
325
  frontmatter = definition.tree_settings.frontmatter
253
326
 
254
327
  definition_documents(definition).each do |document|
255
- parent_values = parents_for(document, primary_path: definition.primary_path)
256
- child_values = children_for(document, primary_path: definition.primary_path)
257
- ancestor_values = ancestors_for(document, primary_path: definition.primary_path, min: 0)
258
- descendant_values = descendants_for(document, primary_path: definition.primary_path, min: 0)
328
+ parent_values = parents_for(document, primary_path: definition.primary_path, scope_fields: definition.scope_fields)
329
+ child_values = children_for(document, primary_path: definition.primary_path, scope_fields: definition.scope_fields)
330
+ ancestor_values = ancestors_for(document, primary_path: definition.primary_path, scope_fields: definition.scope_fields, min: 0)
331
+ descendant_values = descendants_for(document, primary_path: definition.primary_path, scope_fields: definition.scope_fields, min: 0)
332
+ depth_value = root_distance_for(document)
259
333
 
260
334
  if frontmatter.output_path
261
335
  @data_path.write(
@@ -268,6 +342,7 @@ class Graph
268
342
  children_value: child_values,
269
343
  ancestors_value: ancestor_values,
270
344
  descendants_value: descendant_values,
345
+ depth_value: depth_value,
271
346
  max_parents: definition.tree_settings.max_parents,
272
347
  max_children: definition.tree_settings.max_children
273
348
  )
@@ -300,6 +375,7 @@ class Graph
300
375
  )
301
376
  @data_path.write(document.data, frontmatter.ancestors_output_path, ancestor_values)
302
377
  @data_path.write(document.data, frontmatter.descendants_output_path, descendant_values)
378
+ @data_path.write(document.data, frontmatter.depth_output_path, depth_value)
303
379
  debug_tree_event(
304
380
  document: document,
305
381
  definition: definition,
@@ -312,7 +388,9 @@ class Graph
312
388
  ancestors_path: frontmatter.ancestors_output_path,
313
389
  ancestors_value: ancestor_values,
314
390
  descendants_path: frontmatter.descendants_output_path,
315
- descendants_value: descendant_values
391
+ descendants_value: descendant_values,
392
+ depth_path: frontmatter.depth_output_path,
393
+ depth_value: depth_value
316
394
  }
317
395
  )
318
396
  end
@@ -333,11 +411,30 @@ class Graph
333
411
  @primary_path_by_collection[document.collection.label]
334
412
  end
335
413
 
414
+ # Chooses explicit scope fields, or the collection's first tree scope scheme for public helper calls.
415
+ def resolve_scope_fields(document:, scope_fields:)
416
+ return scope_fields unless scope_fields.nil?
417
+
418
+ @scope_fields_by_collection[document.collection.label] || []
419
+ end
420
+
336
421
  # Returns true when one edge already exists.
337
422
  def edge_exists?(parent_document:, child_document:)
338
423
  @parents[child_document].include?(parent_document)
339
424
  end
340
425
 
426
+ # Removes one stored parent-child edge and its metadata in both directions.
427
+ def remove_parent_edge(parent_document:, child_document:)
428
+ @parents[child_document].delete(parent_document)
429
+ @children[parent_document].delete(child_document)
430
+ @parent_edge_entries[child_document].reject! do |entry|
431
+ entry.fetch(:document) == parent_document
432
+ end
433
+ @child_edge_entries[parent_document].reject! do |entry|
434
+ entry.fetch(:document) == child_document
435
+ end
436
+ end
437
+
341
438
  # Returns true when one collection has already reached its configured cap.
342
439
  def maximum_reached?(maximum:, items:)
343
440
  maximum != -1 && items.length >= maximum
@@ -384,21 +481,27 @@ class Graph
384
481
  cache[document] = ordered
385
482
  end
386
483
 
484
+ # Computes one shortest-root-distance map for the current graph.
485
+ def root_distances_for_graph
486
+ @root_distance_cache ||= Trees::RootDistances.for(graph: self)
487
+ end
488
+
387
489
  # Filters one distance map to the requested range and builds references.
388
- def filter_distances(distance_map, primary_path:, min:, max:)
490
+ def filter_distances(distance_map, primary_path:, scope_fields:, min:, max:)
389
491
  distance_map.each_with_object([]) do |(document, distance), references|
390
492
  next if distance < min
391
493
  next if max != -1 && distance > max
392
494
 
393
- references << build_reference(document, primary_path: primary_path, distance: distance)
495
+ references << build_reference(document, primary_path: primary_path, scope_fields: scope_fields, distance: distance)
394
496
  end
395
497
  end
396
498
 
397
499
  # Builds one canonical reference hash, optionally including distance.
398
- def build_reference(document, primary_path:, distance: nil)
500
+ def build_reference(document, primary_path:, scope_fields:, distance: nil)
399
501
  reference = @configuration.reference_template.build(
400
502
  document: document,
401
503
  key: @registry.key_for(document, primary_path: primary_path),
504
+ scope: @registry.scope_for(document, scope_fields: scope_fields),
402
505
  include_count: false
403
506
  )
404
507
  reference['distance'] = distance unless distance.nil?
@@ -414,10 +517,11 @@ class Graph
414
517
  end
415
518
  end
416
519
 
417
- # Clears cached breadth-first-search results after a new edge is added.
418
- def clear_distance_cache
520
+ # Clears cached graph traversals after the tree changes.
521
+ def clear_traversal_cache
419
522
  @ancestor_cache.clear
420
523
  @descendant_cache.clear
524
+ @root_distance_cache = nil
421
525
  end
422
526
 
423
527
  # Captures the metadata needed to understand one stored edge later on.
@@ -445,6 +549,20 @@ class Graph
445
549
  details: details
446
550
  )
447
551
  end
552
+
553
+ # Duplicates one adjacency hash keyed by document.
554
+ def duplicate_document_adjacency(adjacency)
555
+ adjacency.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(document, neighbours), duplicated_adjacency|
556
+ duplicated_adjacency[document] = neighbours.dup
557
+ end
558
+ end
559
+
560
+ # Duplicates one edge-entry hash keyed by document.
561
+ def duplicate_edge_entries(edge_entries)
562
+ edge_entries.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(document, entries), duplicated_edge_entries|
563
+ duplicated_edge_entries[document] = entries.map(&:dup)
564
+ end
565
+ end
448
566
  end
449
567
 
450
568
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Plugins
5
+
6
+ module Relationships
7
+ module Trees
8
+
9
+ # Calculates the shortest distance from every tree node to any current root.
10
+ #
11
+ # The supplied graph-like object must expose `tree_documents`,
12
+ # `parent_documents_for`, and `child_documents_for`. Keeping this traversal in
13
+ # one helper ensures tree write-back and tree pruning interpret depth the same
14
+ # way even though they operate on different graph wrappers.
15
+ class RootDistances
16
+ # Returns one map of document object id to shortest root distance.
17
+ def self.for(graph:)
18
+ root_distances = {}
19
+ queue = graph.tree_documents.select do |document|
20
+ graph.parent_documents_for(document).empty?
21
+ end.sort_by(&:relative_path).map do |root_document|
22
+ [root_document, 0]
23
+ end
24
+
25
+ until queue.empty?
26
+ document, distance = queue.shift
27
+ existing_distance = root_distances[document.object_id]
28
+ next if !existing_distance.nil? && existing_distance <= distance
29
+
30
+ root_distances[document.object_id] = distance
31
+ graph.child_documents_for(document).each do |child_document|
32
+ queue << [child_document, distance + 1]
33
+ end
34
+ end
35
+
36
+ root_distances
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -4,7 +4,7 @@ module Jekyll
4
4
  module Plugins
5
5
 
6
6
  module Relationships
7
- VERSION = '0.1.0.alpha'
7
+ VERSION = '0.2.0.alpha'
8
8
  end
9
9
 
10
10
  end
@@ -7,6 +7,7 @@ require 'jekyll-relationships/version'
7
7
  require 'jekyll-relationships/errors'
8
8
  require 'jekyll-relationships/support/string_array'
9
9
  require 'jekyll-relationships/support/frontmatter_path'
10
+ require 'jekyll-relationships/support/frontmatter_matcher'
10
11
  require 'jekyll-relationships/support/data_path'
11
12
  require 'jekyll-relationships/support/hash_deep_merge'
12
13
  require 'jekyll-relationships/support/placeholders'