jekyll-relationships 0.1.0.alpha → 0.1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9656850968f173e997fe9e9a0b5500f12682c94d613f84444827bc5e52aa955a
4
- data.tar.gz: e56a5ec1a9a44b58d2aa03e2066e99f7a4b39df1335c449e5325f6a5599b6871
3
+ metadata.gz: f4f49c60e8c57f7661b7fdec54b0d4d8ce2e9564dca858e86126ed2f810bfdec
4
+ data.tar.gz: f0c22961ff04cc0493e923676d45a9de3c69ff6720f72160e7897bf921272687
5
5
  SHA512:
6
- metadata.gz: 010f090f1083c9f8ede2f8a99bd23e05a68b474f6c90b1519a4002c2446bf97eee6256b8cb7858cbd9a7e9aa9def901885fed07290e8a83c995656f71e47afd4
7
- data.tar.gz: 1f0c0c85af8fd938f7d6d66a15e4dfb2941615057656867702a01b146e10426901969a2498e10d9fe08427d2226fe9d0f3968e50e262560223fff439ebfcda0f
6
+ metadata.gz: d4b352ce61991bf340b239a9aa881e1314274371637a96e24f317597a6343fb5bebb763fbe6b60f31d8877b3e01f1e4bfc5076994382940f08a757906f632933
7
+ data.tar.gz: 1ec40c78e1bf65e5781b580c4effc37e5510424031267c120a08be6bb6a0c5ffc1f36ccffa1fc4817b6c1ddc451fe996947b4403af48e36db7112d875dcd0bc2
@@ -23,12 +23,32 @@ class Configuration
23
23
  'resolvers' => 'resolver execution and helper calls',
24
24
  'trees' => 'tree discovery, edge handling, and tree write-back'
25
25
  }.freeze
26
+ HASH_KEYS = %w[ids log].freeze
26
27
 
27
28
  ALL_AREAS = AREAS.keys.freeze
28
29
 
29
30
  class << self
30
31
  # Builds one explicit debug setting from a loose config value.
31
32
  def build(value:, string_array:, context:)
33
+ return build_hash_value(value: value, string_array: string_array, context: context) if value.is_a?(Hash)
34
+
35
+ build_scalar_value(value: value, string_array: string_array, context: context)
36
+ end
37
+
38
+ # Returns one disabled debug setting.
39
+ def disabled
40
+ new([])
41
+ end
42
+
43
+ # Returns one debug setting that enables every area.
44
+ def all
45
+ new(ALL_AREAS)
46
+ end
47
+
48
+ private
49
+
50
+ # Builds one debug setting from the legacy scalar forms.
51
+ def build_scalar_value(value:, string_array:, context:)
32
52
  return disabled if value.nil? || false_value?(value)
33
53
  return all if true_value?(value)
34
54
 
@@ -45,18 +65,30 @@ class Configuration
45
65
  new(areas.uniq)
46
66
  end
47
67
 
48
- # Returns one disabled debug setting.
49
- def disabled
50
- new([])
51
- end
68
+ # Builds one debug setting from the new hash form with optional ID filtering.
69
+ def build_hash_value(value:, string_array:, context:)
70
+ hash_value = stringify_hash(value)
71
+ unknown_keys = hash_value.keys - HASH_KEYS
72
+ unless unknown_keys.empty?
73
+ raise ConfigurationError, "`#{context}` contains unsupported keys #{unknown_keys.sort.join(', ')}. Supported keys: #{HASH_KEYS.join(', ')}."
74
+ end
52
75
 
53
- # Returns one debug setting that enables every area.
54
- def all
55
- new(ALL_AREAS)
76
+ log_context = hash_value.key?('log') ? "#{context}.log" : context
77
+ scalar_setting = build_scalar_value(
78
+ value: hash_value.key?('log') ? hash_value.fetch('log') : true,
79
+ string_array: string_array,
80
+ context: log_context
81
+ )
82
+ new(
83
+ scalar_setting.areas,
84
+ parse_ids(
85
+ value: hash_value['ids'],
86
+ string_array: string_array,
87
+ context: "#{context}.ids"
88
+ )
89
+ )
56
90
  end
57
91
 
58
- private
59
-
60
92
  # Returns true when a loose value means "debug everything".
61
93
  def true_value?(value)
62
94
  value == true || value.to_s.strip.casecmp('true').zero?
@@ -83,13 +115,34 @@ class Configuration
83
115
  raise ConfigurationError,
84
116
  "`#{context}` must be true, false, `all`, or a comma-delimited string/array of debug areas: #{ALL_AREAS.join(', ')}."
85
117
  end
118
+
119
+ # Parses one optional string-or-array ID filter.
120
+ def parse_ids(value:, string_array:, context:)
121
+ return [] if value.nil?
122
+
123
+ raw_ids = string_array.interpret(value, split: true, flatten: true)
124
+ raise ConfigurationError, "`#{context}` must be a string or array of strings." if raw_ids.empty?
125
+
126
+ ids = raw_ids.map { |raw_id| raw_id.to_s.strip }.reject(&:empty?).uniq
127
+ raise ConfigurationError, "`#{context}` must contain at least one non-blank ID." if ids.empty?
128
+
129
+ ids
130
+ end
131
+
132
+ # Converts one config hash to a shallow string-keyed copy.
133
+ def stringify_hash(hash)
134
+ hash.each_with_object({}) do |(key, value), stringified|
135
+ stringified[key.to_s] = value
136
+ end
137
+ end
86
138
  end
87
139
 
88
- attr_reader :areas
140
+ attr_reader :areas, :ids
89
141
 
90
- # Captures one immutable set of enabled debug areas.
91
- def initialize(areas)
142
+ # Captures one immutable set of enabled debug areas and optional ID filters.
143
+ def initialize(areas, ids = [])
92
144
  @areas = areas.freeze
145
+ @ids = Array(ids).map(&:to_s).uniq.freeze
93
146
  end
94
147
 
95
148
  # Returns true when any debug area is enabled, or when one named area is enabled.
@@ -99,6 +152,13 @@ class Configuration
99
152
  @areas.include?(normalise_runtime_area(area))
100
153
  end
101
154
 
155
+ # Returns true when this setting either has no ID filter or overlaps one.
156
+ def matches_ids?(related_ids)
157
+ return true if @ids.empty?
158
+
159
+ Array(related_ids).map(&:to_s).any? { |related_id| @ids.include?(related_id) }
160
+ end
161
+
102
162
  private
103
163
 
104
164
  # Normalises one runtime area name and raises on internal typos.
@@ -26,7 +26,8 @@ class Configuration
26
26
  'parents' => 'parents',
27
27
  'children' => 'children',
28
28
  'ancestors' => 'ancestors',
29
- 'descendants' => 'descendants'
29
+ 'descendants' => 'descendants',
30
+ 'depth' => 'depth'
30
31
  }.freeze,
31
32
  'max' => {
32
33
  'parents' => -1,
@@ -14,7 +14,7 @@ class Configuration
14
14
  # The resolved helper exposes explicit input and output paths so the tree
15
15
  # graph never has to reimplement config merging rules.
16
16
  class TreeFrontmatter
17
- PATH_NAMES = %w[parent child parents children ancestors descendants].freeze
17
+ PATH_NAMES = %w[parent child parents children ancestors descendants depth].freeze
18
18
 
19
19
  # Builds one tree-frontmatter helper from the resolved values.
20
20
  def initialize(base:, raw_paths:, raw_output_path:, string_array:)
@@ -118,6 +118,11 @@ class Configuration
118
118
  first_path_for('descendants')
119
119
  end
120
120
 
121
+ # Returns the absolute prevailing depth output path.
122
+ def depth_output_path
123
+ first_path_for('depth')
124
+ end
125
+
121
126
  # Returns the absolute output container path, if one is configured.
122
127
  def output_path
123
128
  return nil if blank_path?(@raw_output_path)
@@ -131,7 +136,7 @@ class Configuration
131
136
  # The configured relative tree paths become nested keys within the output
132
137
  # hash, while the configured output container path becomes the single
133
138
  # 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:)
139
+ def output_payload(parent_value:, parents_value:, child_value:, children_value:, ancestors_value:, descendants_value:, depth_value:, max_parents:, max_children:)
135
140
  payload = {}
136
141
  data_path = Jekyll::Plugins::Relationships::Support::DataPath.new
137
142
 
@@ -149,6 +154,7 @@ class Configuration
149
154
 
150
155
  data_path.write(payload, first_relative_path_for('ancestors'), ancestors_value)
151
156
  data_path.write(payload, first_relative_path_for('descendants'), descendants_value)
157
+ data_path.write(payload, first_relative_path_for('depth'), depth_value)
152
158
  payload
153
159
  end
154
160
 
@@ -11,10 +11,30 @@ module Relationships
11
11
  # Jekyll logger. Callers should pass only already-resolved runtime state.
12
12
  class DebugLogger
13
13
  MAX_VALUE_LENGTH = 500
14
+ STRING_ID_PROPERTIES = %w[
15
+ id
16
+ key
17
+ target_key
18
+ reference
19
+ result
20
+ references
21
+ entries
22
+ value
23
+ parent_value
24
+ child_value
25
+ ancestors_value
26
+ descendants_value
27
+ ].freeze
28
+
29
+ # Builds one logger with the active reference key property for hash parsing.
30
+ def initialize(reference_key_property:)
31
+ @reference_key_property = reference_key_property.to_s
32
+ @data_path = Jekyll::Plugins::Relationships::Support::DataPath.new
33
+ end
14
34
 
15
35
  # Emits one debug line for one normal relationship state.
16
36
  def relationship_event(document:, definition:, area:, event:, details: {})
17
- return unless definition.debug?(area)
37
+ return unless should_log?(definition: definition, area: area, document: document, details: details)
18
38
 
19
39
  log(
20
40
  area: area,
@@ -25,7 +45,9 @@ class DebugLogger
25
45
 
26
46
  # Emits one debug line for a document-level write-back event.
27
47
  def document_event(document:, definitions:, area:, event:, details: {})
28
- debug_definitions = Array(definitions).select { |definition| definition.debug?(area) }
48
+ debug_definitions = Array(definitions).select do |definition|
49
+ should_log?(definition: definition, area: area, document: document, details: details)
50
+ end
29
51
  return if debug_definitions.empty?
30
52
 
31
53
  log(
@@ -41,7 +63,7 @@ class DebugLogger
41
63
 
42
64
  # Emits one debug line for one tree relationship event.
43
65
  def tree_event(document:, definition:, area:, event:, details: {})
44
- return unless definition.debug?(area)
66
+ return unless should_log?(definition: definition, area: area, document: document, details: details)
45
67
 
46
68
  log(
47
69
  area: area,
@@ -62,6 +84,21 @@ class DebugLogger
62
84
 
63
85
  private
64
86
 
87
+ # Returns true when one definition wants this area and its ID filter matches.
88
+ def should_log?(definition:, area:, document:, details:)
89
+ return false unless definition.debug?(area)
90
+
91
+ definition.debug_ids_match?(related_ids_for(definition: definition, document: document, details: details))
92
+ end
93
+
94
+ # Collects every document or reference ID that one event obviously touches.
95
+ def related_ids_for(definition:, document:, details:)
96
+ ([document_key(document, primary_path: definition.primary_path)] + extract_related_ids(
97
+ details,
98
+ primary_path: definition.primary_path
99
+ )).compact.uniq
100
+ end
101
+
65
102
  # Emits one line through the standard Jekyll logger.
66
103
  def log(area:, prefix:, details:)
67
104
  detail_text = details.each_with_object([]) do |(key, value), parts|
@@ -72,6 +109,60 @@ class DebugLogger
72
109
  Jekyll.logger.info('Relationships:', "[debug:#{area}] #{message}")
73
110
  end
74
111
 
112
+ # Resolves one document back to the primary key used by the active definition.
113
+ def document_key(document, primary_path:)
114
+ return nil unless document.is_a?(Jekyll::Document)
115
+
116
+ if primary_path.nil?
117
+ document.relative_path.sub(/\A_/, '').sub(/#{Regexp.escape(document.extname)}\z/, '')
118
+ else
119
+ value = @data_path.read(document.data, primary_path)
120
+ value.nil? ? nil : value.to_s
121
+ end
122
+ end
123
+
124
+ # Walks one debug payload and extracts any obvious relationship IDs from it.
125
+ def extract_related_ids(value, primary_path:, property_name: nil)
126
+ case value
127
+ when Jekyll::Document
128
+ [document_key(value, primary_path: primary_path)].compact
129
+ when Array
130
+ value.flat_map do |item|
131
+ extract_related_ids(item, primary_path: primary_path, property_name: property_name)
132
+ end
133
+ when Hash
134
+ extract_related_ids_from_hash(value, primary_path: primary_path)
135
+ when String
136
+ return [] unless property_name && STRING_ID_PROPERTIES.include?(property_name.to_s)
137
+
138
+ trimmed_value = value.strip
139
+ trimmed_value.empty? ? [] : [trimmed_value]
140
+ else
141
+ []
142
+ end
143
+ end
144
+
145
+ # Reads likely ID-bearing properties from one debug hash before recurring.
146
+ def extract_related_ids_from_hash(hash, primary_path:)
147
+ ids = []
148
+ hash.each do |key, value|
149
+ string_key = key.to_s
150
+ if string_key == @reference_key_property || string_key == 'id'
151
+ trimmed_value = value.to_s.strip
152
+ ids << trimmed_value unless trimmed_value.empty?
153
+ end
154
+
155
+ ids.concat(
156
+ extract_related_ids(
157
+ value,
158
+ primary_path: primary_path,
159
+ property_name: string_key
160
+ )
161
+ )
162
+ end
163
+ ids.uniq
164
+ end
165
+
75
166
  # Normalises values so large document objects stay readable in logs.
76
167
  def normalise_value(value)
77
168
  case value
@@ -38,6 +38,11 @@ class NormalRelationship
38
38
  @debug.enabled?(area)
39
39
  end
40
40
 
41
+ # Returns true when one event's related IDs satisfy this pair's debug filter.
42
+ def debug_ids_match?(ids)
43
+ @debug.matches_ids?(ids)
44
+ end
45
+
41
46
  # Registers one resolver class against this relationship pair.
42
47
  def add_resolver(resolver_class)
43
48
  @resolver_classes << resolver_class
@@ -35,6 +35,11 @@ class TreeRelationship
35
35
  @debug.enabled?(area)
36
36
  end
37
37
 
38
+ # Returns true when one event's related IDs satisfy this definition's debug filter.
39
+ def debug_ids_match?(ids)
40
+ @debug.matches_ids?(ids)
41
+ end
42
+
38
43
  # Returns true when one parent-child direction is permitted.
39
44
  def allows_parent_child?(parent_collection:, child_collection:)
40
45
  @parent_child_pairs.include?([parent_collection, child_collection])
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Plugins
5
+
6
+ module Relationships
7
+
8
+ class Engine
9
+
10
+ # Stores the source-derived normal relationship graph used to seed each round.
11
+ #
12
+ # The seed graph is built once from frontmatter, including deterministic
13
+ # bidirectional mirrors, and is later reduced only by document removals.
14
+ # Resolver output never flows back into this structure.
15
+ class NormalSeed
16
+ # Builds one normal seed graph for the current active document set.
17
+ def initialize(engine:, active_document_ids:)
18
+ @engine = engine
19
+ @configuration = engine.configuration
20
+ @registry = engine.registry
21
+ @data_path = engine.data_path
22
+ @debug_logger = engine.debug_logger
23
+ @active_document_ids = active_document_ids.each_with_object({}) do |document_id, active_ids|
24
+ active_ids[document_id] = true
25
+ end
26
+ @string_array = Jekyll::Plugins::Relationships::Support::StringArray.new
27
+ @raw_path_states = {}
28
+ @entries_by_state = {}
29
+ build!
30
+ end
31
+
32
+ # Returns the seed entries for one concrete document-to-collection pair.
33
+ def entries_for(document, to_collection)
34
+ duplicate_entries(@entries_by_state[[document.object_id, to_collection]])
35
+ end
36
+
37
+ # Permanently removes documents from the seed graph in both source and target positions.
38
+ def remove_documents!(documents)
39
+ removed_document_ids = Array(documents).each_with_object({}) do |document, lookup|
40
+ lookup[document.object_id] = true
41
+ end
42
+ return if removed_document_ids.empty?
43
+
44
+ @entries_by_state.delete_if do |(source_document_id, _to_collection), _entries|
45
+ removed_document_ids.key?(source_document_id)
46
+ end
47
+ @entries_by_state.each_value do |entries|
48
+ entries.reject! do |entry|
49
+ removed_document_ids.key?(entry.fetch(:document).object_id)
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Builds the full source-derived seed graph once.
57
+ def build!
58
+ accumulators = {}
59
+
60
+ @configuration.collections.each do |collection|
61
+ @configuration.normal_relationships_for(collection).each do |definition|
62
+ next unless definition.reads_frontmatter
63
+
64
+ documents_for(collection).each do |document|
65
+ seed_definition_from_frontmatter(
66
+ accumulators: accumulators,
67
+ document: document,
68
+ definition: definition
69
+ )
70
+ end
71
+ end
72
+ end
73
+
74
+ @entries_by_state = accumulators.each_with_object({}) do |(state_key, accumulator), entries_by_state|
75
+ entries_by_state[state_key] = accumulator.encounter_entries
76
+ end
77
+ end
78
+
79
+ # Reads and stores one concrete direct relationship definition from frontmatter.
80
+ def seed_definition_from_frontmatter(accumulators:, document:, definition:)
81
+ definition.foreign_paths.each do |path|
82
+ raw_state = raw_path_state(document, path)
83
+ debug_relationship_event(
84
+ document: document,
85
+ definition: definition,
86
+ event: 'raw_path',
87
+ details: {
88
+ path: path,
89
+ present: raw_state.present?,
90
+ value: raw_state.raw_value
91
+ }
92
+ )
93
+ next unless raw_state.present?
94
+
95
+ resolved_entries = raw_state.resolved_entries_for(
96
+ primary_path: definition.primary_path,
97
+ registry: @registry,
98
+ active_document_checker: proc { |resolved_document| active_document?(resolved_document) }
99
+ )
100
+ debug_relationship_event(
101
+ document: document,
102
+ definition: definition,
103
+ event: 'raw_path_resolved',
104
+ details: {
105
+ path: path,
106
+ entries: resolved_entries.compact
107
+ }
108
+ )
109
+
110
+ resolved_entries.each do |entry|
111
+ next unless entry
112
+ if entry.fetch(:document).collection.label != definition.to_collection
113
+ debug_relationship_event(
114
+ document: document,
115
+ definition: definition,
116
+ event: 'raw_path_skipped',
117
+ details: {
118
+ path: path,
119
+ target: entry.fetch(:document),
120
+ target_key: entry.fetch(:key),
121
+ actual_collection: entry.fetch(:document).collection.label,
122
+ expected_collection: definition.to_collection
123
+ }
124
+ )
125
+ next
126
+ end
127
+
128
+ add_direct_entry(
129
+ accumulators: accumulators,
130
+ document: document,
131
+ definition: definition,
132
+ entry: entry
133
+ )
134
+ end
135
+ end
136
+ end
137
+
138
+ # Adds one direct link and its deterministic bidirectional mirror when configured.
139
+ def add_direct_entry(accumulators:, document:, definition:, entry:)
140
+ accumulator_for(
141
+ accumulators: accumulators,
142
+ document: document,
143
+ to_collection: definition.to_collection
144
+ ).add(
145
+ document: entry.fetch(:document),
146
+ key: entry.fetch(:key),
147
+ metadata: entry.fetch(:metadata),
148
+ count: entry.fetch(:count)
149
+ )
150
+
151
+ return unless definition.bidirectional
152
+
153
+ accumulator_for(
154
+ accumulators: accumulators,
155
+ document: entry.fetch(:document),
156
+ to_collection: definition.from_collection
157
+ ).add(
158
+ document: document,
159
+ key: @registry.key_for(document, primary_path: definition.primary_path),
160
+ metadata: entry.fetch(:metadata),
161
+ count: entry.fetch(:count)
162
+ )
163
+ end
164
+
165
+ # Returns one reusable accumulator for one concrete source pair.
166
+ def accumulator_for(accumulators:, document:, to_collection:)
167
+ accumulators[[document.object_id, to_collection]] ||= Jekyll::Plugins::Relationships::References::Accumulator.new(
168
+ reference_template: @configuration.reference_template,
169
+ multiple_settings: @configuration.multiple_settings
170
+ )
171
+ end
172
+
173
+ # Returns every active document for one collection.
174
+ def documents_for(collection)
175
+ @registry.documents_for(collection).select do |document|
176
+ active_document?(document)
177
+ end
178
+ end
179
+
180
+ # Returns true when one document is still active in the seed graph.
181
+ def active_document?(document)
182
+ @active_document_ids.key?(document.object_id)
183
+ end
184
+
185
+ # Returns the cached raw-path parser for one document/path pair.
186
+ def raw_path_state(document, path)
187
+ @raw_path_states[[document.object_id, path]] ||= RawPathState.new(
188
+ document: document,
189
+ path: path,
190
+ data_path: @data_path,
191
+ string_array: @string_array,
192
+ reference_template: @configuration.reference_template
193
+ )
194
+ end
195
+
196
+ # Duplicates one stored seed-entry array defensively.
197
+ def duplicate_entries(entries)
198
+ Array(entries).map(&:dup)
199
+ end
200
+
201
+ # Emits one debug event while the seed graph is being built.
202
+ def debug_relationship_event(document:, definition:, event:, details:)
203
+ @debug_logger.relationship_event(
204
+ document: document,
205
+ definition: definition,
206
+ area: 'upgrading',
207
+ event: event,
208
+ details: details
209
+ )
210
+ end
211
+ end
212
+ end
213
+
214
+ end
215
+
216
+ end
217
+ end