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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/lib/jekyll-relationships/configuration/debug_setting.rb +117 -0
  3. data/lib/jekyll-relationships/configuration/defaults.rb +54 -0
  4. data/lib/jekyll-relationships/configuration/frontmatter.rb +102 -0
  5. data/lib/jekyll-relationships/configuration/hash_utilities.rb +55 -0
  6. data/lib/jekyll-relationships/configuration/multiple_settings.rb +101 -0
  7. data/lib/jekyll-relationships/configuration/parser.rb +551 -0
  8. data/lib/jekyll-relationships/configuration/prune_rule_settings.rb +101 -0
  9. data/lib/jekyll-relationships/configuration/prune_settings.rb +82 -0
  10. data/lib/jekyll-relationships/configuration/tree_frontmatter.rb +244 -0
  11. data/lib/jekyll-relationships/configuration/tree_settings.rb +74 -0
  12. data/lib/jekyll-relationships/configuration.rb +250 -0
  13. data/lib/jekyll-relationships/debug_logger.rb +104 -0
  14. data/lib/jekyll-relationships/definitions/configured_relationship.rb +61 -0
  15. data/lib/jekyll-relationships/definitions/normal_relationship.rb +51 -0
  16. data/lib/jekyll-relationships/definitions/prune_rule.rb +67 -0
  17. data/lib/jekyll-relationships/definitions/tree_relationship.rb +48 -0
  18. data/lib/jekyll-relationships/documents/registry.rb +136 -0
  19. data/lib/jekyll-relationships/engine/raw_path_state.rb +217 -0
  20. data/lib/jekyll-relationships/engine/relationship_state.rb +251 -0
  21. data/lib/jekyll-relationships/engine/session.rb +187 -0
  22. data/lib/jekyll-relationships/engine/write_back.rb +154 -0
  23. data/lib/jekyll-relationships/engine.rb +228 -0
  24. data/lib/jekyll-relationships/errors.rb +30 -0
  25. data/lib/jekyll-relationships/generators/relationships.rb +27 -0
  26. data/lib/jekyll-relationships/pruning/normal_graph.rb +119 -0
  27. data/lib/jekyll-relationships/pruning/rule_pruner.rb +67 -0
  28. data/lib/jekyll-relationships/pruning/tree_phase.rb +351 -0
  29. data/lib/jekyll-relationships/pruning/tree_provenance.rb +32 -0
  30. data/lib/jekyll-relationships/references/accumulator.rb +185 -0
  31. data/lib/jekyll-relationships/references/template.rb +222 -0
  32. data/lib/jekyll-relationships/resolvers/base.rb +207 -0
  33. data/lib/jekyll-relationships/run_logger.rb +101 -0
  34. data/lib/jekyll-relationships/support/data_path.rb +194 -0
  35. data/lib/jekyll-relationships/support/frontmatter_path.rb +217 -0
  36. data/lib/jekyll-relationships/support/hash_deep_merge.rb +63 -0
  37. data/lib/jekyll-relationships/support/placeholders.rb +21 -0
  38. data/lib/jekyll-relationships/support/string_array.rb +157 -0
  39. data/lib/jekyll-relationships/trees/edge_builder.rb +196 -0
  40. data/lib/jekyll-relationships/trees/graph.rb +454 -0
  41. data/lib/jekyll-relationships/version.rb +11 -0
  42. data/lib/jekyll-relationships.rb +34 -0
  43. data/readme.md +509 -0
  44. metadata +189 -0
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Plugins
5
+
6
+ module Relationships
7
+
8
+ class Configuration
9
+
10
+ # Encapsulates the global settings that control pruning behaviour.
11
+ #
12
+ # These settings determine whether expanded relationship entries should be
13
+ # combined into one prune rule per subject collection, how many prune rounds
14
+ # may run before the final rebuild pass, and how tree orphans should be
15
+ # handled after parent documents disappear.
16
+ class PruneSettings
17
+ DEFAULTS = {
18
+ 'combine' => true,
19
+ 'iterations' => 10,
20
+ 'tree' => {
21
+ 'orphans' => 'grandparents'
22
+ }.freeze
23
+ }.freeze
24
+
25
+ attr_reader :iterations, :tree_orphans
26
+
27
+ # Builds one prune-settings helper from raw configuration.
28
+ def initialize(raw_config:)
29
+ config = normalise_config(raw_config)
30
+ @combine = Configuration::HashUtilities.boolean_value(config.fetch('combine'), 'relationships.prune.combine')
31
+ @iterations = normalise_iterations(config.fetch('iterations'))
32
+ @tree_orphans = normalise_tree_orphans(Configuration::HashUtilities.fetch_hash_value(config, 'tree'))
33
+ end
34
+
35
+ # Returns true when expanded relationship members should combine by subject collection.
36
+ def combine?
37
+ @combine
38
+ end
39
+
40
+ # Returns the total number of prune rounds to run before the final rebuild pass.
41
+ def prune_rounds
42
+ @iterations + 1
43
+ end
44
+
45
+ private
46
+
47
+ # Converts the loose raw value into one normalised hash.
48
+ def normalise_config(raw_config)
49
+ return DEFAULTS if raw_config.nil?
50
+ raise ConfigurationError, '`relationships.prune` must be a hash.' unless raw_config.is_a?(Hash)
51
+
52
+ Configuration::HashUtilities.merge_hash(DEFAULTS, raw_config)
53
+ end
54
+
55
+ # Clamps the configured prune-round iteration count into the supported range.
56
+ def normalise_iterations(raw_iterations)
57
+ interpreted_iterations = Configuration::HashUtilities.integer_value(raw_iterations, 'relationships.prune.iterations')
58
+ return 0 if interpreted_iterations < 0
59
+ return 100 if interpreted_iterations > 100
60
+
61
+ interpreted_iterations
62
+ end
63
+
64
+ # Resolves the configured orphan-handling mode for tree pruning.
65
+ def normalise_tree_orphans(raw_tree_config)
66
+ tree_config = raw_tree_config.is_a?(Hash) ? raw_tree_config : {}
67
+ raw_mode = Configuration::HashUtilities.hash_key?(tree_config, 'orphans') ? Configuration::HashUtilities.fetch_hash_value(tree_config, 'orphans') : DEFAULTS.fetch('tree').fetch('orphans')
68
+ string_mode = raw_mode.to_s.strip.downcase
69
+ string_mode = DEFAULTS.fetch('tree').fetch('orphans') if string_mode.empty?
70
+ return 'grandparents' if %w[grandparent grandparents].include?(string_mode)
71
+ return 'grandparents required' if %w[grandparent\ required grandparents\ required].include?(string_mode)
72
+ return string_mode if %w[prune orphan].include?(string_mode)
73
+
74
+ raise ConfigurationError, "Unsupported relationships.prune.tree.orphans value `#{raw_mode}`."
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Plugins
5
+
6
+ module Relationships
7
+
8
+ class Configuration
9
+
10
+ # Resolves the tree-specific frontmatter locations for one effective config.
11
+ #
12
+ # Tree frontmatter inherits the active relationship frontmatter base at each
13
+ # override level, but may replace that base with its own `tree.frontmatter.base`.
14
+ # The resolved helper exposes explicit input and output paths so the tree
15
+ # graph never has to reimplement config merging rules.
16
+ class TreeFrontmatter
17
+ PATH_NAMES = %w[parent child parents children ancestors descendants].freeze
18
+
19
+ # Builds one tree-frontmatter helper from the resolved values.
20
+ def initialize(base:, raw_paths:, raw_output_path:, string_array:)
21
+ @base = normalise_base(base)
22
+ @raw_paths = stringify_hash(raw_paths)
23
+ @raw_output_path = raw_output_path
24
+ @string_array = string_array
25
+ validate_paths!
26
+ end
27
+
28
+ # Builds the built-in default tree frontmatter configuration.
29
+ def self.defaults(string_array:)
30
+ new(
31
+ base: Defaults::FRONTMATTER.fetch('base'),
32
+ raw_paths: Defaults::TREE.fetch('frontmatter'),
33
+ raw_output_path: nil,
34
+ string_array: string_array
35
+ )
36
+ end
37
+
38
+ # Returns one cloned helper with one override level merged in.
39
+ #
40
+ # Relationship `frontmatter.base` is applied first for the level, then
41
+ # `tree.frontmatter.base` may replace it for tree paths only.
42
+ def merge_level(frontmatter_override:, tree_frontmatter_override:)
43
+ override_hash = tree_frontmatter_override.is_a?(Hash) ? tree_frontmatter_override : {}
44
+ next_base = base_after_overrides(frontmatter_override: frontmatter_override, tree_frontmatter_override: override_hash)
45
+ next_paths = @raw_paths.dup
46
+ next_output_path = @raw_output_path
47
+
48
+ PATH_NAMES.each do |name|
49
+ next unless Configuration::HashUtilities.hash_key?(override_hash, name)
50
+
51
+ next_paths[name] = Configuration::HashUtilities.fetch_hash_value(override_hash, name)
52
+ end
53
+
54
+ if Configuration::HashUtilities.hash_key?(override_hash, 'output')
55
+ next_output_path = Configuration::HashUtilities.fetch_hash_value(override_hash, 'output')
56
+ end
57
+
58
+ self.class.new(
59
+ base: next_base,
60
+ raw_paths: next_paths,
61
+ raw_output_path: next_output_path,
62
+ string_array: @string_array
63
+ )
64
+ end
65
+
66
+ # Returns the effective base path applied to tree frontmatter.
67
+ def base
68
+ @base
69
+ end
70
+
71
+ # Returns every absolute input path used to read parent references.
72
+ #
73
+ # Single-parent trees only read the singular parent keys, matching the
74
+ # documented contract that plural keys are ignored once the maximum is 1.
75
+ def parent_input_paths(max_parents:)
76
+ return paths_for('parent') if max_parents == 1
77
+
78
+ (paths_for('parent') + paths_for('parents')).uniq
79
+ end
80
+
81
+ # Returns every absolute input path used to read child references.
82
+ #
83
+ # Single-child trees mirror the parent-side behaviour and only read the
84
+ # singular child keys when the configured maximum is 1.
85
+ def child_input_paths(max_children:)
86
+ return paths_for('child') if max_children == 1
87
+
88
+ (paths_for('child') + paths_for('children')).uniq
89
+ end
90
+
91
+ # Returns the absolute prevailing singular parent output path.
92
+ def parent_output_path
93
+ first_path_for('parent')
94
+ end
95
+
96
+ # Returns the absolute prevailing singular child output path.
97
+ def child_output_path
98
+ first_path_for('child')
99
+ end
100
+
101
+ # Returns the absolute prevailing plural parent output path.
102
+ def parents_output_path
103
+ first_path_for('parents')
104
+ end
105
+
106
+ # Returns the absolute prevailing plural child output path.
107
+ def children_output_path
108
+ first_path_for('children')
109
+ end
110
+
111
+ # Returns the absolute prevailing ancestors output path.
112
+ def ancestors_output_path
113
+ first_path_for('ancestors')
114
+ end
115
+
116
+ # Returns the absolute prevailing descendants output path.
117
+ def descendants_output_path
118
+ first_path_for('descendants')
119
+ end
120
+
121
+ # Returns the absolute output container path, if one is configured.
122
+ def output_path
123
+ return nil if blank_path?(@raw_output_path)
124
+ raise ConfigurationError, 'Tree frontmatter output must be one path, not an array.' if @raw_output_path.is_a?(Array)
125
+
126
+ apply_base(@raw_output_path.to_s.strip)
127
+ end
128
+
129
+ # Builds the final tree-output hash for one document.
130
+ #
131
+ # The configured relative tree paths become nested keys within the output
132
+ # hash, while the configured output container path becomes the single
133
+ # frontmatter location on which that hash is written.
134
+ def output_payload(parent_value:, parents_value:, child_value:, children_value:, ancestors_value:, descendants_value:, max_parents:, max_children:)
135
+ payload = {}
136
+ data_path = Jekyll::Plugins::Relationships::Support::DataPath.new
137
+
138
+ if max_parents == 1
139
+ data_path.write(payload, first_relative_path_for('parent'), parent_value)
140
+ else
141
+ data_path.write(payload, first_relative_path_for('parents'), parents_value)
142
+ end
143
+
144
+ if max_children == 1
145
+ data_path.write(payload, first_relative_path_for('child'), child_value)
146
+ else
147
+ data_path.write(payload, first_relative_path_for('children'), children_value)
148
+ end
149
+
150
+ data_path.write(payload, first_relative_path_for('ancestors'), ancestors_value)
151
+ data_path.write(payload, first_relative_path_for('descendants'), descendants_value)
152
+ payload
153
+ end
154
+
155
+ private
156
+
157
+ # Resolves the next effective base for one override level.
158
+ def base_after_overrides(frontmatter_override:, tree_frontmatter_override:)
159
+ next_base = @base
160
+
161
+ if Configuration::HashUtilities.hash_key?(frontmatter_override, 'base')
162
+ next_base = normalise_base(Configuration::HashUtilities.fetch_hash_value(frontmatter_override, 'base'))
163
+ end
164
+
165
+ if Configuration::HashUtilities.hash_key?(tree_frontmatter_override, 'base')
166
+ next_base = normalise_base(Configuration::HashUtilities.fetch_hash_value(tree_frontmatter_override, 'base'))
167
+ end
168
+
169
+ next_base
170
+ end
171
+
172
+ # Returns the configured absolute paths for one tree property.
173
+ def paths_for(name)
174
+ configured_paths_for(name).map { |path| apply_base(path) }
175
+ end
176
+
177
+ # Returns the first prevailing absolute path for one tree property.
178
+ def first_path_for(name)
179
+ paths_for(name).first
180
+ end
181
+
182
+ # Returns the first prevailing relative path for one tree property.
183
+ def first_relative_path_for(name)
184
+ configured_paths_for(name).first
185
+ end
186
+
187
+ # Returns the configured relative paths for one property.
188
+ def configured_paths_for(name)
189
+ raw_value = @raw_paths[name]
190
+ paths = @string_array.interpret(raw_value, split: true, flatten: true).map do |path|
191
+ string_path = path.to_s.strip
192
+ raise ConfigurationError, "Tree frontmatter path `#{name}` cannot be blank." if string_path.empty?
193
+
194
+ string_path
195
+ end
196
+ raise ConfigurationError, "Tree frontmatter must define at least one path for `#{name}`." if paths.empty?
197
+
198
+ paths.uniq
199
+ end
200
+
201
+ # Applies the current base to one relative path.
202
+ def apply_base(path)
203
+ return path if @base.empty?
204
+ return @base if path.to_s.empty?
205
+
206
+ "#{@base}.#{path}"
207
+ end
208
+
209
+ # Ensures the helper only stores string-keyed frontmatter fields.
210
+ def stringify_hash(hash)
211
+ return {} unless hash.is_a?(Hash)
212
+
213
+ hash.each_with_object({}) do |(key, value), stringified|
214
+ stringified[key.to_s] = value
215
+ end
216
+ end
217
+
218
+ # Normalises nil bases to the empty string for path resolution.
219
+ def normalise_base(raw_base)
220
+ return '' if raw_base.nil?
221
+
222
+ raw_base.to_s
223
+ end
224
+
225
+ # Returns true when one configured path is blank or missing.
226
+ def blank_path?(path)
227
+ path.nil? || path.to_s.strip.empty?
228
+ end
229
+
230
+ # Validates only the known path-bearing properties eagerly.
231
+ def validate_paths!
232
+ PATH_NAMES.each { |name| configured_paths_for(name) }
233
+ return if blank_path?(@raw_output_path)
234
+ return unless @raw_output_path.is_a?(Array)
235
+
236
+ raise ConfigurationError, 'Tree frontmatter output must be one path, not an array.'
237
+ end
238
+ end
239
+ end
240
+
241
+ end
242
+
243
+ end
244
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Plugins
5
+
6
+ module Relationships
7
+
8
+ class Configuration
9
+
10
+ # Encapsulates one effective tree configuration scope.
11
+ #
12
+ # Tree settings are merged level by level so relationship-level
13
+ # `frontmatter.base` overrides can replace higher-level tree bases before a
14
+ # lower-level `tree.frontmatter.base` optionally replaces them again.
15
+ class TreeSettings
16
+ attr_reader :frontmatter
17
+
18
+ # Builds one tree-settings helper from already-resolved values.
19
+ def initialize(frontmatter:, maximums:, url:)
20
+ @frontmatter = frontmatter
21
+ @maximums = Configuration::HashUtilities.merge_hash(Defaults::TREE.fetch('max'), maximums)
22
+ @url = !!url
23
+ end
24
+
25
+ # Builds the built-in default tree settings.
26
+ def self.defaults(string_array:)
27
+ new(
28
+ frontmatter: TreeFrontmatter.defaults(string_array: string_array),
29
+ maximums: Defaults::TREE.fetch('max'),
30
+ url: Defaults::TREE.fetch('url')
31
+ )
32
+ end
33
+
34
+ # Returns one cloned helper with one override level merged in.
35
+ def merge_level(frontmatter_override:, tree_override:)
36
+ override_hash = tree_override.is_a?(Hash) ? tree_override : {}
37
+ next_frontmatter = @frontmatter.merge_level(
38
+ frontmatter_override: frontmatter_override,
39
+ tree_frontmatter_override: Configuration::HashUtilities.fetch_hash_value(override_hash, 'frontmatter')
40
+ )
41
+ next_maximums = Configuration::HashUtilities.merge_hash(
42
+ @maximums,
43
+ Configuration::HashUtilities.fetch_hash_value(override_hash, 'max')
44
+ )
45
+ next_url = Configuration::HashUtilities.hash_key?(override_hash, 'url') ? !!Configuration::HashUtilities.fetch_hash_value(override_hash, 'url') : @url
46
+
47
+ self.class.new(
48
+ frontmatter: next_frontmatter,
49
+ maximums: next_maximums,
50
+ url: next_url
51
+ )
52
+ end
53
+
54
+ # Returns the configured maximum parent count.
55
+ def max_parents
56
+ Configuration::HashUtilities.integer_value(@maximums['parents'], 'tree.max.parents')
57
+ end
58
+
59
+ # Returns the configured maximum child count.
60
+ def max_children
61
+ Configuration::HashUtilities.integer_value(@maximums['children'], 'tree.max.children')
62
+ end
63
+
64
+ # Returns true when URL-derived tree inference is enabled.
65
+ def url?
66
+ @url
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jekyll-relationships/definitions/normal_relationship'
4
+ require 'jekyll-relationships/definitions/tree_relationship'
5
+ require 'jekyll-relationships/configuration/defaults'
6
+ require 'jekyll-relationships/configuration/debug_setting'
7
+ require 'jekyll-relationships/configuration/hash_utilities'
8
+ require 'jekyll-relationships/configuration/frontmatter'
9
+ require 'jekyll-relationships/configuration/multiple_settings'
10
+ require 'jekyll-relationships/configuration/prune_settings'
11
+ require 'jekyll-relationships/configuration/prune_rule_settings'
12
+ require 'jekyll-relationships/configuration/tree_frontmatter'
13
+ require 'jekyll-relationships/configuration/tree_settings'
14
+ require 'jekyll-relationships/configuration/parser'
15
+
16
+ module Jekyll
17
+ module Plugins
18
+
19
+ module Relationships
20
+
21
+ # Normalises the site configuration into explicit relationship definitions.
22
+ #
23
+ # The object resolves defaults once, exposes the final keyword and frontmatter
24
+ # rules, and indexes the concrete relationship definitions used by the engine.
25
+ class Configuration
26
+ attr_reader :keywords, :multiple_settings, :reference_template, :tree_settings, :global_frontmatter, :debug, :prune_settings
27
+
28
+ # Builds the full configuration model from `site.config`.
29
+ def initialize(site_config)
30
+ @string_array = Jekyll::Plugins::Relationships::Support::StringArray.new
31
+ @raw_config = Configuration::HashUtilities.fetch_hash_value(site_config, 'relationships') || {}
32
+ @enabled = build_enabled_setting
33
+
34
+ if !enabled?
35
+ initialise_disabled_configuration
36
+ return
37
+ end
38
+
39
+ @debug = build_debug_setting
40
+ global_frontmatter_override = Configuration::HashUtilities.fetch_hash_value(@raw_config, 'frontmatter')
41
+ global_tree_override = Configuration::HashUtilities.fetch_hash_value(@raw_config, 'tree')
42
+ @keywords = build_keywords(Configuration::HashUtilities.fetch_hash_value(@raw_config, 'keywords'))
43
+ @multiple_settings = MultipleSettings.new(
44
+ raw_config: Configuration::HashUtilities.fetch_hash_value(@raw_config, 'multiple')
45
+ )
46
+ @prune_settings = PruneSettings.new(
47
+ raw_config: Configuration::HashUtilities.fetch_hash_value(@raw_config, 'prune')
48
+ )
49
+ @global_frontmatter = Frontmatter.new(
50
+ raw_config: Configuration::HashUtilities.merge_hash(
51
+ Defaults::FRONTMATTER,
52
+ global_frontmatter_override
53
+ ),
54
+ string_array: @string_array
55
+ )
56
+ @reference_template = References::Template.new(
57
+ config: build_reference_config,
58
+ count_enabled: @multiple_settings.count?
59
+ )
60
+ @tree_settings = TreeSettings.defaults(
61
+ string_array: @string_array
62
+ ).merge_level(
63
+ frontmatter_override: global_frontmatter_override,
64
+ tree_override: global_tree_override
65
+ )
66
+
67
+ parsed_relationships = Parser.new(
68
+ raw_config: @raw_config,
69
+ string_array: @string_array,
70
+ global_debug: @debug,
71
+ global_frontmatter: @global_frontmatter,
72
+ global_tree_settings: @tree_settings,
73
+ keywords: @keywords,
74
+ prune_settings: @prune_settings
75
+ )
76
+
77
+ parsed_result = parsed_relationships.parse
78
+ @normal_relationships = parsed_result.fetch(:normal_relationships)
79
+ @tree_relationships = parsed_result.fetch(:tree_relationships)
80
+ @collections = parsed_result.fetch(:collections)
81
+ @configured_relationships = parsed_result.fetch(:configured_relationships)
82
+ @normal_prune_rules = parsed_result.fetch(:normal_prune_rules)
83
+ @tree_prune_rules = parsed_result.fetch(:tree_prune_rules)
84
+ attach_resolvers!(parser: parsed_relationships)
85
+ end
86
+
87
+ # Returns true when relationship processing is globally enabled.
88
+ def enabled?
89
+ @enabled
90
+ end
91
+
92
+ # Returns the concrete normal relationship definition for one pair.
93
+ def normal_relationship_for(from_collection:, to_collection:)
94
+ collection_relationships = @normal_relationships[from_collection]
95
+ return nil unless collection_relationships
96
+
97
+ collection_relationships[to_collection]
98
+ end
99
+
100
+ # Returns all normal relationship definitions for one source collection.
101
+ def normal_relationships_for(collection)
102
+ (@normal_relationships[collection] || {}).values.sort_by(&:sequence)
103
+ end
104
+
105
+ # Returns all configured tree relationships.
106
+ def tree_relationships
107
+ @tree_relationships.sort_by(&:sequence)
108
+ end
109
+
110
+ # Returns every collection that participates in any relationship.
111
+ def collections
112
+ @collections.dup
113
+ end
114
+
115
+ # Returns every primary-key path used anywhere in the configuration.
116
+ def primary_paths
117
+ normal_paths = @normal_relationships.values.flat_map(&:values).map(&:primary_path)
118
+ tree_paths = @tree_relationships.map(&:primary_path)
119
+ (normal_paths + tree_paths).uniq
120
+ end
121
+
122
+ private
123
+
124
+ # Builds the active global enabled setting.
125
+ def build_enabled_setting
126
+ return Defaults::ENABLED unless Configuration::HashUtilities.hash_key?(@raw_config, 'enabled')
127
+
128
+ Configuration::HashUtilities.boolean_value(
129
+ Configuration::HashUtilities.fetch_hash_value(@raw_config, 'enabled'),
130
+ 'relationships.enabled'
131
+ )
132
+ end
133
+
134
+ # Sets up the inert configuration used when the plugin is globally disabled.
135
+ def initialise_disabled_configuration
136
+ @debug = Configuration::DebugSetting.disabled
137
+ @keywords = build_keywords(nil)
138
+ @multiple_settings = MultipleSettings.new(raw_config: nil)
139
+ @prune_settings = PruneSettings.new(raw_config: nil)
140
+ @global_frontmatter = Frontmatter.new(
141
+ raw_config: Defaults::FRONTMATTER,
142
+ string_array: @string_array
143
+ )
144
+ @reference_template = References::Template.new(
145
+ config: default_reference_config,
146
+ count_enabled: @multiple_settings.count?
147
+ )
148
+ @tree_settings = TreeSettings.defaults(string_array: @string_array)
149
+ @normal_relationships = {}
150
+ @tree_relationships = []
151
+ @collections = []
152
+ @configured_relationships = []
153
+ @normal_prune_rules = []
154
+ @tree_prune_rules = []
155
+ end
156
+
157
+ public
158
+
159
+ # Returns every configured forward-facing relationship member in sequence order.
160
+ def configured_relationships
161
+ @configured_relationships.sort_by(&:sequence)
162
+ end
163
+
164
+ # Returns every configured prune rule for normal relationships.
165
+ def normal_prune_rules
166
+ @normal_prune_rules.sort_by do |rule|
167
+ [rule.subject_collection, rule.entry_index, rule.members.first.sequence]
168
+ end
169
+ end
170
+
171
+ # Returns every configured prune rule for tree relationships.
172
+ def tree_prune_rules
173
+ @tree_prune_rules.sort_by do |rule|
174
+ [rule.subject_collection, rule.entry_index, rule.members.first.sequence]
175
+ end
176
+ end
177
+
178
+ # Returns true when any relationship entry defined a prune rule.
179
+ def pruning_enabled?
180
+ !@normal_prune_rules.empty? || !@tree_prune_rules.empty?
181
+ end
182
+
183
+ # Builds the active keyword table.
184
+ def build_keywords(raw_keywords)
185
+ Configuration::HashUtilities.merge_hash(Defaults::KEYWORDS, raw_keywords)
186
+ end
187
+
188
+ # Builds the active global debug setting.
189
+ def build_debug_setting
190
+ Configuration::DebugSetting.build(
191
+ value: Configuration::HashUtilities.hash_key?(@raw_config, 'debug') ? Configuration::HashUtilities.fetch_hash_value(@raw_config, 'debug') : Defaults::DEBUG,
192
+ string_array: @string_array,
193
+ context: 'relationships.debug'
194
+ )
195
+ end
196
+
197
+ # Builds the resolved reference template config.
198
+ def build_reference_config
199
+ default_references = default_reference_config
200
+ explicit_config = Configuration::HashUtilities.fetch_hash_value(@raw_config, 'references')
201
+ return explicit_config if explicit_config.is_a?(Hash)
202
+
203
+ nested_frontmatter = Configuration::HashUtilities.fetch_hash_value(
204
+ Configuration::HashUtilities.fetch_hash_value(@raw_config, 'frontmatter'),
205
+ 'references'
206
+ )
207
+ if nested_frontmatter.is_a?(Hash)
208
+ raise ConfigurationError, '`relationships.references` must be configured directly under `relationships`, not under `relationships.frontmatter`.'
209
+ end
210
+
211
+ default_references
212
+ end
213
+
214
+ # Builds the default reference template shape for the active duplicate mode.
215
+ def default_reference_config
216
+ default_references = {
217
+ 'id' => Jekyll::Plugins::Relationships::Support::Placeholders::KEY,
218
+ 'collection' => Jekyll::Plugins::Relationships::Support::Placeholders::COLLECTION,
219
+ 'page' => Jekyll::Plugins::Relationships::Support::Placeholders::PAGE
220
+ }
221
+ if @multiple_settings.count?
222
+ default_references['count'] = Jekyll::Plugins::Relationships::Support::Placeholders::COUNT
223
+ end
224
+
225
+ default_references
226
+ end
227
+
228
+ # Attaches registered resolver classes to matching concrete relationships.
229
+ def attach_resolvers!(parser:)
230
+ Resolvers::Base.registered_subclasses.each do |resolver_class|
231
+ parser.pairs_for(
232
+ from_definition: resolver_class.from_definition,
233
+ to_definition: resolver_class.to_definition
234
+ ).each do |from_collection, to_collection|
235
+ definition = normal_relationship_for(
236
+ from_collection: from_collection,
237
+ to_collection: to_collection
238
+ )
239
+ next unless definition
240
+
241
+ definition.add_resolver(resolver_class)
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ end
248
+
249
+ end
250
+ end