jekyll-relationships 0.1.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.
- checksums.yaml +7 -0
- data/lib/jekyll-relationships/configuration/debug_setting.rb +117 -0
- data/lib/jekyll-relationships/configuration/defaults.rb +54 -0
- data/lib/jekyll-relationships/configuration/frontmatter.rb +102 -0
- data/lib/jekyll-relationships/configuration/hash_utilities.rb +55 -0
- data/lib/jekyll-relationships/configuration/multiple_settings.rb +101 -0
- data/lib/jekyll-relationships/configuration/parser.rb +551 -0
- data/lib/jekyll-relationships/configuration/prune_rule_settings.rb +101 -0
- data/lib/jekyll-relationships/configuration/prune_settings.rb +82 -0
- data/lib/jekyll-relationships/configuration/tree_frontmatter.rb +244 -0
- data/lib/jekyll-relationships/configuration/tree_settings.rb +74 -0
- data/lib/jekyll-relationships/configuration.rb +250 -0
- data/lib/jekyll-relationships/debug_logger.rb +104 -0
- data/lib/jekyll-relationships/definitions/configured_relationship.rb +61 -0
- data/lib/jekyll-relationships/definitions/normal_relationship.rb +51 -0
- data/lib/jekyll-relationships/definitions/prune_rule.rb +67 -0
- data/lib/jekyll-relationships/definitions/tree_relationship.rb +48 -0
- data/lib/jekyll-relationships/documents/registry.rb +136 -0
- data/lib/jekyll-relationships/engine/raw_path_state.rb +217 -0
- data/lib/jekyll-relationships/engine/relationship_state.rb +251 -0
- data/lib/jekyll-relationships/engine/session.rb +187 -0
- data/lib/jekyll-relationships/engine/write_back.rb +154 -0
- data/lib/jekyll-relationships/engine.rb +228 -0
- data/lib/jekyll-relationships/errors.rb +30 -0
- data/lib/jekyll-relationships/generators/relationships.rb +27 -0
- data/lib/jekyll-relationships/pruning/normal_graph.rb +119 -0
- data/lib/jekyll-relationships/pruning/rule_pruner.rb +67 -0
- data/lib/jekyll-relationships/pruning/tree_phase.rb +351 -0
- data/lib/jekyll-relationships/pruning/tree_provenance.rb +32 -0
- data/lib/jekyll-relationships/references/accumulator.rb +185 -0
- data/lib/jekyll-relationships/references/template.rb +222 -0
- data/lib/jekyll-relationships/resolvers/base.rb +207 -0
- data/lib/jekyll-relationships/run_logger.rb +101 -0
- data/lib/jekyll-relationships/support/data_path.rb +194 -0
- data/lib/jekyll-relationships/support/frontmatter_path.rb +217 -0
- data/lib/jekyll-relationships/support/hash_deep_merge.rb +63 -0
- data/lib/jekyll-relationships/support/placeholders.rb +21 -0
- data/lib/jekyll-relationships/support/string_array.rb +157 -0
- data/lib/jekyll-relationships/trees/edge_builder.rb +196 -0
- data/lib/jekyll-relationships/trees/graph.rb +454 -0
- data/lib/jekyll-relationships/version.rb +11 -0
- data/lib/jekyll-relationships.rb +34 -0
- data/readme.md +509 -0
- metadata +189 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Plugins
|
|
5
|
+
|
|
6
|
+
module Relationships
|
|
7
|
+
|
|
8
|
+
class Configuration
|
|
9
|
+
|
|
10
|
+
# Expands the raw relationship config into explicit definitions.
|
|
11
|
+
#
|
|
12
|
+
# This parser handles shorthand collection lists, target hashes, keyword
|
|
13
|
+
# expansion, duplicate detection, and frontmatter override precedence.
|
|
14
|
+
class Parser
|
|
15
|
+
# Builds one parser for the current configuration.
|
|
16
|
+
def initialize(raw_config:, string_array:, global_debug:, global_frontmatter:, global_tree_settings:, keywords:, prune_settings:)
|
|
17
|
+
@raw_config = raw_config
|
|
18
|
+
@string_array = string_array
|
|
19
|
+
@global_debug = global_debug
|
|
20
|
+
@global_frontmatter = global_frontmatter
|
|
21
|
+
@global_tree_settings = global_tree_settings
|
|
22
|
+
@keywords = keywords
|
|
23
|
+
@prune_settings = prune_settings
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Parses the full relationship config into concrete definitions.
|
|
27
|
+
def parse
|
|
28
|
+
entries = Configuration::HashUtilities.fetch_hash_value(@raw_config, 'relationships')
|
|
29
|
+
entries = [] if entries.nil?
|
|
30
|
+
raise ConfigurationError, '`relationships.relationships` must be an array.' unless entries.is_a?(Array)
|
|
31
|
+
|
|
32
|
+
normal_relationships = Hash.new { |hash, key| hash[key] = {} }
|
|
33
|
+
tree_relationships = []
|
|
34
|
+
collections = Set.new
|
|
35
|
+
occupancy = {}
|
|
36
|
+
configured_relationships = []
|
|
37
|
+
normal_prune_rules = []
|
|
38
|
+
tree_prune_rules = []
|
|
39
|
+
prune_rule_occupancy = Hash.new { |hash, key| hash[key] = [] }
|
|
40
|
+
sequence = 0
|
|
41
|
+
|
|
42
|
+
entries.each_with_index do |entry, entry_index|
|
|
43
|
+
raise ConfigurationError, "Relationship entry #{entry_index + 1} must be a hash." unless entry.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
from_collections = parse_collection_list(
|
|
46
|
+
value: Configuration::HashUtilities.fetch_hash_value(entry, 'from'),
|
|
47
|
+
context: "relationships[#{entry_index}].from"
|
|
48
|
+
)
|
|
49
|
+
raise ConfigurationError, "Relationship entry #{entry_index + 1} must define at least one `from` collection." if from_collections.empty?
|
|
50
|
+
|
|
51
|
+
targets = parse_targets(
|
|
52
|
+
value: Configuration::HashUtilities.fetch_hash_value(entry, 'to'),
|
|
53
|
+
context: "relationships[#{entry_index}].to"
|
|
54
|
+
)
|
|
55
|
+
raise ConfigurationError, "Relationship entry #{entry_index + 1} must define at least one `to` target." if targets.empty?
|
|
56
|
+
|
|
57
|
+
relationship_frontmatter = Configuration::HashUtilities.fetch_hash_value(entry, 'frontmatter')
|
|
58
|
+
relationship_tree = Configuration::HashUtilities.fetch_hash_value(entry, 'tree')
|
|
59
|
+
relationship_prune_configuration = relationship_prune_setting(entry: entry, entry_index: entry_index)
|
|
60
|
+
relationship_debug = relationship_debug_setting(entry: entry, entry_index: entry_index)
|
|
61
|
+
relationship_mode = normalise_mode(Configuration::HashUtilities.fetch_hash_value(entry, 'mode'))
|
|
62
|
+
effective_relationship_frontmatter = @global_frontmatter.merge(relationship_frontmatter)
|
|
63
|
+
effective_relationship_tree_settings = @global_tree_settings.merge_level(
|
|
64
|
+
frontmatter_override: relationship_frontmatter,
|
|
65
|
+
tree_override: relationship_tree
|
|
66
|
+
)
|
|
67
|
+
effective_relationship_debug = relationship_debug.nil? ? @global_debug : relationship_debug
|
|
68
|
+
prune_members_by_configuration = Hash.new { |hash, key| hash[key] = [] }
|
|
69
|
+
target_prune_configuration_cache = {}
|
|
70
|
+
resolved_targets = targets.map do |target|
|
|
71
|
+
{
|
|
72
|
+
descriptor: target,
|
|
73
|
+
prune_overridden: target.key?('prune'),
|
|
74
|
+
prune_configuration: target_prune_setting(target: target, entry_index: entry_index, cache: target_prune_configuration_cache),
|
|
75
|
+
debug_setting: target_debug_setting(target: target, entry_index: entry_index)
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
from_collections.each do |from_collection|
|
|
80
|
+
collections << from_collection
|
|
81
|
+
|
|
82
|
+
resolved_targets.each do |resolved_target|
|
|
83
|
+
target = resolved_target.fetch(:descriptor)
|
|
84
|
+
expanded_targets(
|
|
85
|
+
token: target.fetch('collection'),
|
|
86
|
+
from_collections: from_collections,
|
|
87
|
+
current_from_collection: from_collection
|
|
88
|
+
).each do |to_collection|
|
|
89
|
+
collections << to_collection
|
|
90
|
+
effective_mode = normalise_mode(target['mode'].nil? ? relationship_mode : target['mode'])
|
|
91
|
+
effective_frontmatter = effective_relationship_frontmatter.merge(target['frontmatter'])
|
|
92
|
+
effective_tree_settings = effective_relationship_tree_settings.merge_level(
|
|
93
|
+
frontmatter_override: target['frontmatter'],
|
|
94
|
+
tree_override: target['tree']
|
|
95
|
+
)
|
|
96
|
+
target_debug = resolved_target.fetch(:debug_setting)
|
|
97
|
+
effective_debug = target_debug.nil? ? effective_relationship_debug : target_debug
|
|
98
|
+
effective_prune_configuration = effective_prune_configuration(
|
|
99
|
+
target_prune_configuration: resolved_target.fetch(:prune_configuration),
|
|
100
|
+
target_overrides_prune: resolved_target.fetch(:prune_overridden),
|
|
101
|
+
relationship_prune_configuration: relationship_prune_configuration
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if tree_mode?(effective_mode)
|
|
105
|
+
tree_definition = register_tree_relationship(
|
|
106
|
+
tree_relationships: tree_relationships,
|
|
107
|
+
occupancy: occupancy,
|
|
108
|
+
from_collection: from_collection,
|
|
109
|
+
to_collection: to_collection,
|
|
110
|
+
mode: effective_mode,
|
|
111
|
+
frontmatter: effective_frontmatter,
|
|
112
|
+
tree_settings: effective_tree_settings,
|
|
113
|
+
debug: effective_debug,
|
|
114
|
+
sequence: sequence
|
|
115
|
+
)
|
|
116
|
+
configured_relationships << build_configured_relationship(
|
|
117
|
+
definition: tree_definition,
|
|
118
|
+
kind: :tree,
|
|
119
|
+
mode: effective_mode,
|
|
120
|
+
sequence: sequence
|
|
121
|
+
)
|
|
122
|
+
register_prune_member!(
|
|
123
|
+
prune_members_by_configuration: prune_members_by_configuration,
|
|
124
|
+
prune_configuration: effective_prune_configuration,
|
|
125
|
+
member: configured_relationships.last
|
|
126
|
+
)
|
|
127
|
+
sequence += 1
|
|
128
|
+
else
|
|
129
|
+
registration = register_normal_relationships(
|
|
130
|
+
normal_relationships: normal_relationships,
|
|
131
|
+
occupancy: occupancy,
|
|
132
|
+
from_collection: from_collection,
|
|
133
|
+
to_collection: to_collection,
|
|
134
|
+
mode: effective_mode,
|
|
135
|
+
frontmatter: effective_frontmatter,
|
|
136
|
+
debug: effective_debug,
|
|
137
|
+
sequence: sequence
|
|
138
|
+
)
|
|
139
|
+
configured_relationships << build_configured_relationship(
|
|
140
|
+
definition: registration.fetch(:definition),
|
|
141
|
+
kind: :normal,
|
|
142
|
+
mode: effective_mode,
|
|
143
|
+
sequence: sequence
|
|
144
|
+
)
|
|
145
|
+
register_prune_member!(
|
|
146
|
+
prune_members_by_configuration: prune_members_by_configuration,
|
|
147
|
+
prune_configuration: effective_prune_configuration,
|
|
148
|
+
member: configured_relationships.last
|
|
149
|
+
)
|
|
150
|
+
sequence = registration.fetch(:sequence)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
prune_members_by_configuration.each do |prune_configuration, prune_members|
|
|
157
|
+
register_prune_rules!(
|
|
158
|
+
entry_members: prune_members,
|
|
159
|
+
prune_configuration: prune_configuration,
|
|
160
|
+
normal_prune_rules: normal_prune_rules,
|
|
161
|
+
tree_prune_rules: tree_prune_rules,
|
|
162
|
+
prune_rule_occupancy: prune_rule_occupancy,
|
|
163
|
+
entry_index: entry_index
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
{
|
|
169
|
+
normal_relationships: normal_relationships,
|
|
170
|
+
tree_relationships: tree_relationships,
|
|
171
|
+
collections: collections.to_a.sort,
|
|
172
|
+
configured_relationships: configured_relationships,
|
|
173
|
+
normal_prune_rules: normal_prune_rules,
|
|
174
|
+
tree_prune_rules: tree_prune_rules
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Expands resolver `from` and `to` selectors into explicit pairs.
|
|
179
|
+
def pairs_for(from_definition:, to_definition:)
|
|
180
|
+
from_collections = parse_collection_list(value: from_definition, context: 'resolver.from')
|
|
181
|
+
|
|
182
|
+
from_collections.each_with_object([]) do |from_collection, pairs|
|
|
183
|
+
expanded_target_descriptors(to_definition).each do |target|
|
|
184
|
+
expanded_targets(
|
|
185
|
+
token: target.fetch('collection'),
|
|
186
|
+
from_collections: from_collections,
|
|
187
|
+
current_from_collection: from_collection
|
|
188
|
+
).each do |to_collection|
|
|
189
|
+
pairs << [from_collection, to_collection]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end.uniq
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
# Adds one concrete normal relationship pair.
|
|
198
|
+
def register_normal_relationships(normal_relationships:, occupancy:, from_collection:, to_collection:, mode:, frontmatter:, debug:, sequence:)
|
|
199
|
+
ensure_unoccupied_pair!(occupancy, from_collection, to_collection, "normal relationship #{from_collection} -> #{to_collection}")
|
|
200
|
+
forward_definition = build_normal_relationship(
|
|
201
|
+
from_collection: from_collection,
|
|
202
|
+
to_collection: to_collection,
|
|
203
|
+
frontmatter: frontmatter,
|
|
204
|
+
debug: debug,
|
|
205
|
+
sequence: sequence,
|
|
206
|
+
reads_frontmatter: true,
|
|
207
|
+
bidirectional: mode == 'bidirectional'
|
|
208
|
+
)
|
|
209
|
+
normal_relationships[from_collection][to_collection] = forward_definition
|
|
210
|
+
occupancy[[from_collection, to_collection]] = mode
|
|
211
|
+
sequence += 1
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
sequence: sequence,
|
|
215
|
+
definition: forward_definition
|
|
216
|
+
} unless mode == 'bidirectional'
|
|
217
|
+
|
|
218
|
+
ensure_unoccupied_pair!(occupancy, to_collection, from_collection, "bidirectional reverse relationship #{to_collection} -> #{from_collection}")
|
|
219
|
+
normal_relationships[to_collection][from_collection] = build_normal_relationship(
|
|
220
|
+
from_collection: to_collection,
|
|
221
|
+
to_collection: from_collection,
|
|
222
|
+
frontmatter: frontmatter,
|
|
223
|
+
debug: debug,
|
|
224
|
+
sequence: sequence,
|
|
225
|
+
reads_frontmatter: false,
|
|
226
|
+
bidirectional: true
|
|
227
|
+
)
|
|
228
|
+
occupancy[[to_collection, from_collection]] = mode
|
|
229
|
+
{
|
|
230
|
+
sequence: sequence + 1,
|
|
231
|
+
definition: forward_definition
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Adds one concrete tree relationship definition.
|
|
236
|
+
def register_tree_relationship(tree_relationships:, occupancy:, from_collection:, to_collection:, mode:, frontmatter:, tree_settings:, debug:, sequence:)
|
|
237
|
+
ensure_unoccupied_pair!(occupancy, from_collection, to_collection, "tree relationship #{from_collection} <-> #{to_collection}")
|
|
238
|
+
ensure_unoccupied_pair!(occupancy, to_collection, from_collection, "tree relationship #{to_collection} <-> #{from_collection}") unless from_collection == to_collection
|
|
239
|
+
|
|
240
|
+
definition = Definitions::TreeRelationship.new(
|
|
241
|
+
from_collection: from_collection,
|
|
242
|
+
to_collection: to_collection,
|
|
243
|
+
primary_path: frontmatter.primary_path,
|
|
244
|
+
parent_child_pairs: parent_child_pairs(from_collection: from_collection, to_collection: to_collection, mode: mode),
|
|
245
|
+
tree_settings: tree_settings,
|
|
246
|
+
debug: debug,
|
|
247
|
+
sequence: sequence
|
|
248
|
+
)
|
|
249
|
+
tree_relationships << definition
|
|
250
|
+
|
|
251
|
+
occupancy[[from_collection, to_collection]] = mode
|
|
252
|
+
occupancy[[to_collection, from_collection]] = mode
|
|
253
|
+
definition
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Builds one normal relationship definition.
|
|
257
|
+
def build_normal_relationship(from_collection:, to_collection:, frontmatter:, debug:, sequence:, reads_frontmatter:, bidirectional:)
|
|
258
|
+
Definitions::NormalRelationship.new(
|
|
259
|
+
from_collection: from_collection,
|
|
260
|
+
to_collection: to_collection,
|
|
261
|
+
primary_path: frontmatter.primary_path,
|
|
262
|
+
foreign_paths: frontmatter.foreign_paths_for(to_collection: to_collection),
|
|
263
|
+
output_path: frontmatter.output_path_for(to_collection: to_collection),
|
|
264
|
+
debug: debug,
|
|
265
|
+
sequence: sequence,
|
|
266
|
+
reads_frontmatter: reads_frontmatter,
|
|
267
|
+
bidirectional: bidirectional
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Builds one forward-facing configured relationship member.
|
|
272
|
+
def build_configured_relationship(definition:, kind:, mode:, sequence:)
|
|
273
|
+
Definitions::ConfiguredRelationship.new(
|
|
274
|
+
definition: definition,
|
|
275
|
+
kind: kind,
|
|
276
|
+
mode: mode,
|
|
277
|
+
sequence: sequence
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Registers every prune rule produced by one raw relationship entry.
|
|
282
|
+
def register_prune_rules!(entry_members:, prune_configuration:, normal_prune_rules:, tree_prune_rules:, prune_rule_occupancy:, entry_index:)
|
|
283
|
+
return unless prune_configuration
|
|
284
|
+
raise ConfigurationError, "Relationship entry #{entry_index + 1} defines `prune` but did not expand to any relationships." if entry_members.empty?
|
|
285
|
+
|
|
286
|
+
member_kinds = entry_members.map(&:kind).uniq
|
|
287
|
+
if member_kinds.length > 1
|
|
288
|
+
raise ConfigurationError, "Relationship entry #{entry_index + 1} cannot combine tree and normal relationships within one `prune` block."
|
|
289
|
+
end
|
|
290
|
+
if member_kinds.first == :tree
|
|
291
|
+
if prune_configuration.shortcut?
|
|
292
|
+
raise ConfigurationError, "Relationship entry #{entry_index + 1} cannot use `prune: <int>` on a tree relationship."
|
|
293
|
+
end
|
|
294
|
+
if prune_configuration.depth.nil?
|
|
295
|
+
raise ConfigurationError, "Relationship entry #{entry_index + 1} must define `prune.depth` when pruning a tree relationship."
|
|
296
|
+
end
|
|
297
|
+
elsif !prune_configuration.depth.nil?
|
|
298
|
+
raise ConfigurationError, "Relationship entry #{entry_index + 1} cannot define `prune.depth` on a normal relationship."
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
prune_rules_for_entry(
|
|
302
|
+
entry_members: entry_members,
|
|
303
|
+
prune_configuration: prune_configuration,
|
|
304
|
+
entry_index: entry_index
|
|
305
|
+
).each do |rule|
|
|
306
|
+
ensure_unoccupied_prune_rule!(prune_rule_occupancy, rule)
|
|
307
|
+
if rule.normal?
|
|
308
|
+
normal_prune_rules << rule
|
|
309
|
+
else
|
|
310
|
+
tree_prune_rules << rule
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Adds one configured member to the prune group that should govern it.
|
|
316
|
+
def register_prune_member!(prune_members_by_configuration:, prune_configuration:, member:)
|
|
317
|
+
return unless prune_configuration
|
|
318
|
+
|
|
319
|
+
prune_members_by_configuration[prune_configuration] << member
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Expands one raw relationship entry into one or more prune rules.
|
|
323
|
+
def prune_rules_for_entry(entry_members:, prune_configuration:, entry_index:)
|
|
324
|
+
return expanded_prune_rules_for_entry(entry_members: entry_members, prune_configuration: prune_configuration, entry_index: entry_index) unless @prune_settings.combine?
|
|
325
|
+
|
|
326
|
+
grouped_members = if prune_configuration.inverse?
|
|
327
|
+
entry_members.group_by(&:to_collection)
|
|
328
|
+
else
|
|
329
|
+
entry_members.group_by(&:from_collection)
|
|
330
|
+
end
|
|
331
|
+
grouped_members.map do |subject_collection, members|
|
|
332
|
+
build_prune_rule(
|
|
333
|
+
subject_collection: subject_collection,
|
|
334
|
+
members: members,
|
|
335
|
+
prune_configuration: prune_configuration,
|
|
336
|
+
entry_index: entry_index
|
|
337
|
+
)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Expands one raw relationship entry into one separate prune rule per member.
|
|
342
|
+
def expanded_prune_rules_for_entry(entry_members:, prune_configuration:, entry_index:)
|
|
343
|
+
entry_members.map do |member|
|
|
344
|
+
build_prune_rule(
|
|
345
|
+
subject_collection: prune_configuration.inverse? ? member.to_collection : member.from_collection,
|
|
346
|
+
members: [member],
|
|
347
|
+
prune_configuration: prune_configuration,
|
|
348
|
+
entry_index: entry_index
|
|
349
|
+
)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Builds one concrete prune rule for one subject collection.
|
|
354
|
+
def build_prune_rule(subject_collection:, members:, prune_configuration:, entry_index:)
|
|
355
|
+
Definitions::PruneRule.new(
|
|
356
|
+
kind: members.first.kind,
|
|
357
|
+
subject_collection: subject_collection,
|
|
358
|
+
members: members,
|
|
359
|
+
min: prune_configuration.min,
|
|
360
|
+
depth: prune_configuration.depth,
|
|
361
|
+
inverse: prune_configuration.inverse?,
|
|
362
|
+
entry_index: entry_index
|
|
363
|
+
)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Raises when two prune rules overlap on the same subject collection.
|
|
367
|
+
def ensure_unoccupied_prune_rule!(prune_rule_occupancy, rule)
|
|
368
|
+
occupancy_key = [rule.kind, rule.subject_collection]
|
|
369
|
+
existing_rules = prune_rule_occupancy[occupancy_key]
|
|
370
|
+
overlapping_rule = existing_rules.find do |existing_rule|
|
|
371
|
+
!(existing_rule.member_identifiers & rule.member_identifiers).empty?
|
|
372
|
+
end
|
|
373
|
+
if overlapping_rule
|
|
374
|
+
raise ConfigurationError, "Clashing prune rules target collection `#{rule.subject_collection}` more than once across overlapping #{rule.kind} relationships."
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
existing_rules << rule
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Parses one loose collection list into explicit collection labels.
|
|
381
|
+
def parse_collection_list(value:, context:)
|
|
382
|
+
@string_array.interpret(value, split: true, flatten: true).map do |entry|
|
|
383
|
+
string_value = entry.to_s.strip
|
|
384
|
+
raise ConfigurationError, "`#{context}` cannot contain blank collection names." if string_value.empty?
|
|
385
|
+
|
|
386
|
+
string_value
|
|
387
|
+
end.uniq
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Parses one loose target list into explicit target descriptors.
|
|
391
|
+
def parse_targets(value:, context:)
|
|
392
|
+
expanded_target_descriptors(value).each do |target|
|
|
393
|
+
raise ConfigurationError, "`#{context}` target collections cannot be blank." if target.fetch('collection').to_s.strip.empty?
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Expands strings and hashes into target descriptor hashes.
|
|
398
|
+
def expanded_target_descriptors(value)
|
|
399
|
+
target_entries = value.is_a?(Array) ? value : [value]
|
|
400
|
+
|
|
401
|
+
target_entries.flat_map do |target_entry|
|
|
402
|
+
case target_entry
|
|
403
|
+
when String
|
|
404
|
+
@string_array.interpret(target_entry, split: true, flatten: true).map do |collection|
|
|
405
|
+
{ 'collection' => collection.to_s.strip }
|
|
406
|
+
end
|
|
407
|
+
when Hash
|
|
408
|
+
build_target_descriptors(target_entry)
|
|
409
|
+
else
|
|
410
|
+
raise ConfigurationError, '`to` entries must be strings or hashes.'
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Builds one or more explicit target descriptors while preserving whether
|
|
416
|
+
# optional keys were actually present in the source config.
|
|
417
|
+
def build_target_descriptors(target_entry)
|
|
418
|
+
parse_collection_list(
|
|
419
|
+
value: Configuration::HashUtilities.fetch_hash_value(target_entry, 'collection'),
|
|
420
|
+
context: 'to.collection'
|
|
421
|
+
).map do |collection|
|
|
422
|
+
descriptor = {
|
|
423
|
+
'collection' => collection.to_s.strip
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
%w[frontmatter tree mode debug prune].each do |key|
|
|
427
|
+
next unless Configuration::HashUtilities.hash_key?(target_entry, key)
|
|
428
|
+
|
|
429
|
+
descriptor[key] = Configuration::HashUtilities.fetch_hash_value(target_entry, key)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
descriptor
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Expands keyword targets such as `self`, `others`, and `all`.
|
|
437
|
+
def expanded_targets(token:, from_collections:, current_from_collection:)
|
|
438
|
+
case token
|
|
439
|
+
when keyword('self')
|
|
440
|
+
[current_from_collection]
|
|
441
|
+
when keyword('others')
|
|
442
|
+
from_collections.reject { |collection| collection == current_from_collection }
|
|
443
|
+
when keyword('all')
|
|
444
|
+
from_collections.dup
|
|
445
|
+
else
|
|
446
|
+
[token]
|
|
447
|
+
end.uniq
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Returns true when one mode is a tree mode.
|
|
451
|
+
def tree_mode?(mode)
|
|
452
|
+
%w[parent child parent/child child/parent].include?(mode)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Converts one loose mode value into its canonical form.
|
|
456
|
+
def normalise_mode(mode)
|
|
457
|
+
string_mode = mode.nil? ? 'link' : mode.to_s.strip.downcase
|
|
458
|
+
string_mode = 'link' if string_mode.empty?
|
|
459
|
+
|
|
460
|
+
return string_mode if %w[link bidirectional parent child parent/child child/parent].include?(string_mode)
|
|
461
|
+
|
|
462
|
+
raise ConfigurationError, "Unsupported relationship mode `#{mode}`."
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Resolves one optional per-relationship prune block.
|
|
466
|
+
def relationship_prune_setting(entry:, entry_index:)
|
|
467
|
+
return nil unless Configuration::HashUtilities.hash_key?(entry, 'prune')
|
|
468
|
+
|
|
469
|
+
Configuration::PruneRuleSettings.build(
|
|
470
|
+
raw_config: Configuration::HashUtilities.fetch_hash_value(entry, 'prune'),
|
|
471
|
+
context: "relationships.relationships[#{entry_index}].prune"
|
|
472
|
+
)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Resolves one optional per-relationship debug override.
|
|
476
|
+
def relationship_debug_setting(entry:, entry_index:)
|
|
477
|
+
return nil unless Configuration::HashUtilities.hash_key?(entry, 'debug')
|
|
478
|
+
|
|
479
|
+
Configuration::DebugSetting.build(
|
|
480
|
+
value: Configuration::HashUtilities.fetch_hash_value(entry, 'debug'),
|
|
481
|
+
string_array: @string_array,
|
|
482
|
+
context: "relationships.relationships[#{entry_index}].debug"
|
|
483
|
+
)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Resolves one optional per-target debug override.
|
|
487
|
+
def target_debug_setting(target:, entry_index:)
|
|
488
|
+
return nil unless target.key?('debug')
|
|
489
|
+
|
|
490
|
+
Configuration::DebugSetting.build(
|
|
491
|
+
value: target.fetch('debug'),
|
|
492
|
+
string_array: @string_array,
|
|
493
|
+
context: "relationships.relationships[#{entry_index}].to.debug"
|
|
494
|
+
)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Resolves one optional per-target prune override.
|
|
498
|
+
def target_prune_setting(target:, entry_index:, cache:)
|
|
499
|
+
return nil unless target.key?('prune')
|
|
500
|
+
|
|
501
|
+
raw_config = target.fetch('prune')
|
|
502
|
+
return cache[raw_config.object_id] if cache.key?(raw_config.object_id)
|
|
503
|
+
|
|
504
|
+
cache[raw_config.object_id] = Configuration::PruneRuleSettings.build(
|
|
505
|
+
raw_config: raw_config,
|
|
506
|
+
context: "relationships.relationships[#{entry_index}].to.prune"
|
|
507
|
+
)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Resolves the effective prune config for one expanded target.
|
|
511
|
+
#
|
|
512
|
+
# Targets may either inherit the relationship-level rule, replace it with a
|
|
513
|
+
# target-level rule, or explicitly disable it with `prune: false`.
|
|
514
|
+
def effective_prune_configuration(target_prune_configuration:, target_overrides_prune:, relationship_prune_configuration:)
|
|
515
|
+
return target_prune_configuration if target_overrides_prune
|
|
516
|
+
|
|
517
|
+
relationship_prune_configuration
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Builds the allowed parent-child collection directions for one tree mode.
|
|
521
|
+
def parent_child_pairs(from_collection:, to_collection:, mode:)
|
|
522
|
+
case mode
|
|
523
|
+
when 'parent'
|
|
524
|
+
[[to_collection, from_collection]]
|
|
525
|
+
when 'child'
|
|
526
|
+
[[from_collection, to_collection]]
|
|
527
|
+
when 'parent/child', 'child/parent'
|
|
528
|
+
[[to_collection, from_collection], [from_collection, to_collection]].uniq
|
|
529
|
+
else
|
|
530
|
+
raise ConfigurationError, "Mode `#{mode}` is not a tree mode."
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Raises when one concrete collection pair has already been defined.
|
|
535
|
+
def ensure_unoccupied_pair!(occupancy, from_collection, to_collection, description)
|
|
536
|
+
return unless occupancy.key?([from_collection, to_collection])
|
|
537
|
+
|
|
538
|
+
raise ConfigurationError, "Duplicate or clashing relationship definition for #{description}."
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Returns one active keyword string.
|
|
542
|
+
def keyword(name)
|
|
543
|
+
@keywords.fetch(name.to_s)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
end
|
|
551
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Plugins
|
|
5
|
+
|
|
6
|
+
module Relationships
|
|
7
|
+
|
|
8
|
+
class Configuration
|
|
9
|
+
|
|
10
|
+
# Normalises one relationship-level or target-level prune configuration block.
|
|
11
|
+
#
|
|
12
|
+
# Relationship entries and hash-form targets may opt into pruning by defining
|
|
13
|
+
# a minimum neighbour count and, optionally, switching the subject of the rule
|
|
14
|
+
# to the inverse side of the relationship. Tree prune rules may also
|
|
15
|
+
# constrain which depths are eligible for pruning.
|
|
16
|
+
class PruneRuleSettings
|
|
17
|
+
attr_reader :mode, :min, :depth
|
|
18
|
+
|
|
19
|
+
# Interprets one loose prune config value.
|
|
20
|
+
def self.build(raw_config:, context:)
|
|
21
|
+
return false if raw_config == false
|
|
22
|
+
return new(raw_config: raw_config, context: context, shortcut: true) if raw_config.is_a?(Integer)
|
|
23
|
+
|
|
24
|
+
new(raw_config: raw_config, context: context, shortcut: false)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Builds one immutable prune-rule helper from raw configuration.
|
|
28
|
+
def initialize(raw_config:, context:, shortcut:)
|
|
29
|
+
@shortcut = shortcut
|
|
30
|
+
if @shortcut
|
|
31
|
+
@mode = 'direct'
|
|
32
|
+
@min = normalise_min(raw_config, context: context)
|
|
33
|
+
@depth = nil
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
raise ConfigurationError, "`#{context}` must be a hash." unless raw_config.is_a?(Hash)
|
|
38
|
+
|
|
39
|
+
@mode = normalise_mode(
|
|
40
|
+
Configuration::HashUtilities.hash_key?(raw_config, 'mode') ? Configuration::HashUtilities.fetch_hash_value(raw_config, 'mode') : nil,
|
|
41
|
+
context: context
|
|
42
|
+
)
|
|
43
|
+
@min = normalise_min(
|
|
44
|
+
Configuration::HashUtilities.fetch_hash_value(raw_config, 'min'),
|
|
45
|
+
context: context
|
|
46
|
+
)
|
|
47
|
+
@depth = normalise_depth(
|
|
48
|
+
Configuration::HashUtilities.hash_key?(raw_config, 'depth') ? Configuration::HashUtilities.fetch_hash_value(raw_config, 'depth') : nil,
|
|
49
|
+
context: context
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns true when the rule prunes the inverse side of the relationship.
|
|
54
|
+
def inverse?
|
|
55
|
+
@mode == 'inverse'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns true when the rule came from the integer shorthand form.
|
|
59
|
+
def shortcut?
|
|
60
|
+
@shortcut
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Resolves the configured prune mode.
|
|
66
|
+
def normalise_mode(raw_mode, context:)
|
|
67
|
+
return 'direct' if raw_mode.nil?
|
|
68
|
+
|
|
69
|
+
string_mode = raw_mode.to_s.strip.downcase
|
|
70
|
+
return 'direct' if string_mode.empty?
|
|
71
|
+
return 'inverse' if string_mode == 'inverse'
|
|
72
|
+
|
|
73
|
+
raise ConfigurationError, "Unsupported `#{context}.mode` value `#{raw_mode}`."
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Resolves the configured minimum count and rejects non-positive values.
|
|
77
|
+
def normalise_min(raw_min, context:)
|
|
78
|
+
raise ConfigurationError, "`#{context}` must define `min`." if raw_min.nil?
|
|
79
|
+
|
|
80
|
+
interpreted_min = Configuration::HashUtilities.integer_value(raw_min, "#{context}.min")
|
|
81
|
+
raise ConfigurationError, "`#{context}.min` must be at least 1." if interpreted_min < 1
|
|
82
|
+
|
|
83
|
+
interpreted_min
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Resolves the optional tree depth selector.
|
|
87
|
+
def normalise_depth(raw_depth, context:)
|
|
88
|
+
return nil if raw_depth.nil?
|
|
89
|
+
|
|
90
|
+
interpreted_depth = Configuration::HashUtilities.integer_value(raw_depth, "#{context}.depth")
|
|
91
|
+
raise ConfigurationError, "`#{context}.depth` cannot be 0." if interpreted_depth == 0
|
|
92
|
+
|
|
93
|
+
interpreted_depth
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
101
|
+
end
|