jekyll-relationships 0.1.1.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jekyll-relationships/configuration/defaults.rb +1 -0
  3. data/lib/jekyll-relationships/configuration/frontmatter.rb +28 -1
  4. data/lib/jekyll-relationships/configuration/parser.rb +20 -3
  5. data/lib/jekyll-relationships/configuration/prune_rule_settings.rb +33 -5
  6. data/lib/jekyll-relationships/configuration.rb +43 -8
  7. data/lib/jekyll-relationships/definitions/normal_relationship.rb +8 -2
  8. data/lib/jekyll-relationships/definitions/prune_rule.rb +19 -3
  9. data/lib/jekyll-relationships/definitions/tree_relationship.rb +3 -2
  10. data/lib/jekyll-relationships/documents/registry.rb +182 -33
  11. data/lib/jekyll-relationships/engine/normal_seed.rb +9 -4
  12. data/lib/jekyll-relationships/engine/raw_path_state.rb +44 -22
  13. data/lib/jekyll-relationships/engine/relationship_state.rb +12 -0
  14. data/lib/jekyll-relationships/engine/session.rb +89 -15
  15. data/lib/jekyll-relationships/engine/write_back.rb +1 -0
  16. data/lib/jekyll-relationships/engine.rb +1 -1
  17. data/lib/jekyll-relationships/pruning/rule_pruner.rb +7 -7
  18. data/lib/jekyll-relationships/pruning/tree_phase.rb +29 -11
  19. data/lib/jekyll-relationships/pruning/tree_provenance.rb +1 -1
  20. data/lib/jekyll-relationships/references/accumulator.rb +18 -6
  21. data/lib/jekyll-relationships/references/template.rb +46 -11
  22. data/lib/jekyll-relationships/resolvers/base.rb +3 -0
  23. data/lib/jekyll-relationships/support/frontmatter_matcher.rb +76 -0
  24. data/lib/jekyll-relationships/support/placeholders.rb +1 -0
  25. data/lib/jekyll-relationships/trees/edge_builder.rb +43 -11
  26. data/lib/jekyll-relationships/trees/graph.rb +30 -13
  27. data/lib/jekyll-relationships/version.rb +1 -1
  28. data/lib/jekyll-relationships.rb +1 -0
  29. data/readme.md +154 -23
  30. metadata +3 -2
data/readme.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Plugin for Jekyll that allows collection documents to specify their relationships with each other, via many-to-many links. These can form trees, graphs or relational-database-like structures. Exposes the relationships in each document's frontmatter.
4
4
 
5
+
5
6
  ## Configuration
6
7
 
7
8
  How documents link to each other is specified by configuration.
@@ -17,6 +18,7 @@ relationships:
17
18
  frontmatter: # see Frontmatter below
18
19
  base: relationships # prepend to other keys
19
20
  primary: nil # use Document path as primary key
21
+ scope: nil # optional paths that narrow where the primary key is unique
20
22
  foreign: <collection> # because of base, treated as `relationships.<foreign>`
21
23
  output: nil # see Output below
22
24
 
@@ -24,6 +26,7 @@ relationships:
24
26
  references: # see References below
25
27
  id: <key>
26
28
  collection: <collection>
29
+ scope: <scope>
27
30
  page: <page>
28
31
 
29
32
  # Settings for tree relationships
@@ -104,7 +107,7 @@ Each array item has:
104
107
  * `link` (default): `from` links to `to`.
105
108
  * `parent`/`child`: items in `from` can have items in `to` as their parent/child (which also implies the reverse relationship). See [Trees](#trees) below.
106
109
  * `bidirectional`: like `link`, but whenever a link is added, it is also added to the target in the reverse direction automatically.
107
- * `prune` (optional): remove documents whose finally-resolved relationship count for this entry is too low. See [Pruning](#pruning) below.
110
+ * `prune` (optional): remove documents whose finally-resolved relationship count is too low, or select tree documents by frontmatter values. See [Pruning](#pruning) below.
108
111
 
109
112
  Duplicate or clashing relationship definitions will throw an error. A bidirectional relationship "uses up" the reverse definition, so "from A to B, bidirectional" followed by a "from B to A, link/bidirectional" definition is a duplicate and throws an error.
110
113
 
@@ -120,17 +123,19 @@ The defined relationships will be processed and resolved. This will:
120
123
  Relationships between documents are specified by values in each document's frontmatter. Somewhere in the frontmatter of documents you must have:
121
124
 
122
125
  * A "Primary Key": a value that identifies this document
126
+ * A "Scope" (optional): one or more frontmatter keys that narrow where the primary key is unique (see more below)
123
127
  * A "Foreign Key": a value that identifies another document's primary key
124
128
 
125
- You specify where in the frontmatter these primary/foreign keys exist, using:
129
+ You specify where in the frontmatter these values exist, using:
126
130
 
127
131
  ```yaml
128
132
  frontmatter:
129
133
  primary: id # the frontmatter key `id` holds the primary key value
134
+ scope: locale, meta.region # the complete scope is read from both paths
130
135
  foreign: <collection> # <collection> is replaced by the label of a `to` collection. The frontmatter key that matches this label holds a reference to a document in that collection.
131
136
  ```
132
137
 
133
- * Both keys can be specified with dot-notation to access deeply nested keys, e.g. `meta.details.relationships.id`.
138
+ * `primary`, every `scope` entry, and `foreign` can use dot-notation to access deeply nested keys, e.g. `meta.details.relationships.id`.
134
139
  * `<collection>` is valid only within `foreign`. Example:
135
140
 
136
141
  ```yaml
@@ -144,6 +149,35 @@ frontmatter:
144
149
  In `products` documents, this would find links to `categories` at `links.categories_links` and links to `tags` at `links.tags_links`.
145
150
  * `foreign` can be an array of frontmatter locations where references will be read. These will all be accumulated. Unless `output` is set, the first location will become the output location and others will be untouched.
146
151
 
152
+ ### Scope
153
+
154
+ `frontmatter.scope` is optional. When configured, a document is identified by its collection, primary key and complete scope. This allows the same primary key to occur in different contexts, e.g. where `locale` is `en` or `fr`, while requiring it to remain unique within each exact scope combination.
155
+
156
+ Scope accepts one path, a comma-delimited list of paths, or an array of paths:
157
+
158
+ ```yaml
159
+ frontmatter:
160
+ scope: locale
161
+
162
+ # Equivalent multi-field forms:
163
+ frontmatter:
164
+ scope: locale, meta.region
165
+
166
+ frontmatter:
167
+ scope:
168
+ - locale
169
+ - meta.region
170
+ ```
171
+
172
+ Each entry is a non-empty frontmatter path and may use dot notation. The configured path remains the literal field name in reference scope hashes, so `meta.region` is represented by the key `meta.region`, not a nested hash.
173
+
174
+ By default, every scope value is inherited from the referring document. For instance, with `scope: locale`, document A can refer to document B by `id` only. If A's `locale` is `en`, B is resolved using both the given `id` and `locale: en`.
175
+
176
+ Every configured field is required. A target must contain every field and its values must match the effective scope exactly. Missing and `null` values are invalid rather than wildcards, and lookup does not fall back to a target in another scope.
177
+
178
+ Scope follows the normal global, relationship-level and `to`-target override chain. If scope is configured globally, relationships inherit it unless they redefine it or explicitly set `scope: null`. If no global scope exists, scope may be introduced for only one relationship or target; relationships outside that override remain unscoped. An unscoped relationship is therefore one whose effective configuration has no scope fields, rather than necessarily one belonging to an entirely unscoped site.
179
+
180
+
147
181
  ### Output
148
182
 
149
183
  Relationships are read from the keys you give, and processed. Processing may modify the relationships, add/removing some. This will be written back into the frontmatter at the first location defined in `foreign`. However, if you want to leave this alone, or if the foreign location spans across an array and so can't be written back to, a separate key `output` gives the frontmatter location where you want the final set of relationships to be written. It may contain `<collection>`. Example:
@@ -159,12 +193,13 @@ relationships:
159
193
 
160
194
  ### Base
161
195
 
162
- Any definition of `frontmatter` may specify a `base` which will be prepended to both the `primary` and `foreign` keys within that `frontmatter` definition.
196
+ Any definition of `frontmatter` may specify a `base` which will be prepended to the `primary`, `scope`, `foreign`, and `output` paths within that `frontmatter` definition. Scope keys in reference hashes remain the literal configured names without the base prefix.
163
197
 
164
198
  ```yaml
165
199
  frontmatter:
166
200
  base: links
167
201
  primary: id # treated as `links.id`
202
+ scope: locale # read from `links.locale`, represented as `locale` in reference hashes
168
203
  foreign: <collection> # treated as `links.<collection>`
169
204
  ```
170
205
 
@@ -174,14 +209,14 @@ You can unset `base` with an empty string.
174
209
 
175
210
  ### Defaults and Overrides
176
211
 
177
- `frontmatter` settings for any given relationship is determined by a series of overrides:
212
+ `frontmatter` settings for any given relationship are determined by a series of overrides:
178
213
 
179
214
  1. Built-in defaults
180
215
  2. Global config
181
216
  3. Override at relationship level
182
217
  4. Override at `to` level
183
218
 
184
- For instance if `base` is defined higher in the chain, it will apply by default to any `frontmatter` given lower down, unless it is unset at that level.
219
+ For instance if `base` or `scope` is defined higher in the chain, it will apply by default to any `frontmatter` given lower down, unless it is overridden or unset at that level.
185
220
 
186
221
 
187
222
  ## References
@@ -208,8 +243,22 @@ The `references` config lets you modify the shape of reference hashes. Its keys
208
243
 
209
244
  * `<key>` exactly (required): this key gives the foreign key, and must be present.
210
245
  * `<collection>`: this key gives the collection in which the foreign document exists.
246
+ * `<scope>`: this key gives a hash of scope overrides and receives the complete effective scope after resolution. Despite supporting multiple fields, the placeholder is always the singular `<scope>`.
211
247
  * `<page>`: this key will be set to the actual `Jekyll::Document` instance for the foreign document.
212
248
 
249
+ Only one property may map to `<scope>`. When any relationship has an effective scope, the reference template must define this placeholder. Its property name is arbitrary:
250
+
251
+ ```yaml
252
+ relationships:
253
+ references:
254
+ id: <key>
255
+ collection: <collection>
256
+ context: <scope>
257
+ page: <page>
258
+ ```
259
+
260
+ Here, `context` is the reserved scope property in reference input and resolved output.
261
+
213
262
  Example:
214
263
 
215
264
  ```yaml
@@ -221,14 +270,47 @@ relationships:
221
270
  page: <page>
222
271
  ```
223
272
 
224
- When relationships are processed, references are read loosely: strings are foreign keys, hashes look at the foreign key/collection keys only, ignoring others. Following resolution, all references are upgraded to the defined hash form (merging over any other keys on an existing hash).
273
+ When relationships are processed, string references inherit their complete scope from the referring document. Hash references also inherit scope by default, but may override selected fields through the reserved scope property:
274
+
275
+ ```yaml
276
+ parent:
277
+ id: B
278
+ collection: pages
279
+ scope:
280
+ locale: fr
281
+ ```
282
+
283
+ The reserved scope property must be a hash. Omitted fields inherit from the referring document. Supplying a field with a `null` value is invalid rather than equivalent to omitting it. Every override key must exactly name a configured scope field, exactly as it was defined in `frontmatter`:
284
+
285
+ ```yaml
286
+ parent:
287
+ id: B
288
+ scope:
289
+ locale: en
290
+ meta.region: uk # literal key for the configured `meta.region` path
291
+ ```
292
+
293
+ Following resolution, a scoped reference contains its complete effective scope, including inherited fields:
294
+
295
+ ```yaml
296
+ id: B
297
+ collection: pages
298
+ scope:
299
+ locale: en
300
+ meta.region: uk
301
+ page: <Jekyll::Document>
302
+ ```
303
+
304
+ Unscoped relationships omit the reserved scope property. Other properties on an input hash remain free-form metadata, but the reserved scope property is kept separately for lookup and cannot be overwritten by resolver metadata.
225
305
 
226
306
 
227
307
  ## Primary Keys
228
308
 
229
- **Primary keys must be unique** within a collection. However, if you set things up so that primary keys are unique across *all* collections, then you no longer need to know the collection of a document when linking to it: just the foreign key is needed.
309
+ **Primary keys must be unique** within a collection and exact complete scope. For example, `B` may occur once in scope `{ locale: en }` and once in `{ locale: fr }`, but may not occur twice in either scope.
230
310
 
231
- If the collection isn't given by the reference, all collections will be searched to find the foreign key. If non-unique keys are found this will raise an error.
311
+ If the collection isn't given by the reference, the existing eligible collections are searched for the foreign key and then constrained by the effective scope. Scope does not change collection selection or cross-collection ambiguity behaviour.
312
+
313
+ When a relationship is unscoped, primary-key parsing, uniqueness and lookup behave as before, and resolved references do not gain a scope property.
232
314
 
233
315
  When the `primary` config is `nil`, (which is the default), the document "path" is used as the primary key. This guarantees uniqueness across the whole site. Document path is `Document#relative_path` with leading `_` and trailing `Document.extname` removed, giving strings like `products/shoes` for `shoes.md` in the `products` collection.
234
316
 
@@ -320,7 +402,7 @@ This document's links are modified with the `link` and `unlink` methods:
320
402
 
321
403
  * `link(reference)`
322
404
  * `reference` gives the document to link to, from this document. It must be in the `@to` collection.
323
- * `reference:` (optional named parameter): gives a hash that the created reference hash will merge over, providing arbitrary addititional properties to the hash.
405
+ * `reference:` (optional named parameter): gives a hash that the created reference hash will merge over, providing arbitrary additional properties to the hash. Engine-reserved key, collection, scope, page, and count properties cannot be overwritten through this metadata.
324
406
  * `persist:` (optional named parameter, `true` or `false`): whether to remember this resolver-added link so it can be restored in later prune rounds even if the original route by which it was discovered disappears (see [Pruning](#pruning)).
325
407
  * `unlink(reference)`
326
408
  * Remove the link to the `reference`d document.
@@ -353,6 +435,8 @@ The `reference` parameter in all the above:
353
435
  * Can be a `Jekyll::Document` object. Its primary key and collection are read from the object.
354
436
  * Can be omitted in the helpers, in which case the method operates on *this* document.
355
437
 
438
+ For a scoped relationship, resolver strings and hashes use the same scope inheritance and partial-override rules as frontmatter references. Passing a `Jekyll::Document` uses that target document's complete scope.
439
+
356
440
  ### Example
357
441
 
358
442
  ```ruby
@@ -433,7 +517,7 @@ references:
433
517
 
434
518
  ## Pruning
435
519
 
436
- You can remove certain pages from your site ("prune" them) according to the number of finally-resolved relationships on them. This is controlled with a `prune` key which you add to the relationship definition:
520
+ You can remove certain pages from your site ("prune" them) according to their finally-resolved relationship count. Tree relationships can also prune pages selected by exact frontmatter values. This is controlled with a `prune` key which you add to the relationship definition:
437
521
 
438
522
  ```yaml
439
523
  relationships:
@@ -454,6 +538,12 @@ relationships:
454
538
  prune:
455
539
  min: 2 # prune a category if it has fewer than 2 parents
456
540
  depth: -1 # only prune non-root nodes
541
+ - from: pages
542
+ to: self
543
+ mode: parent
544
+ prune:
545
+ where:
546
+ exists: false # prune pages whose exists value is the boolean false
457
547
  - from: products
458
548
  mode: parent
459
549
  to:
@@ -463,30 +553,70 @@ relationships:
463
553
  min: 2 # prune a product if it has fewer than 2 children
464
554
  depth: 1 # only prune root notes
465
555
  prune:
466
- combine: true # default
467
- iterations: 10 # default
556
+ combine: true # default; count expanded prune targets together per collection
557
+ iterations: 10 # default; extra prune rounds after the first pass
468
558
  tree:
469
559
  orphans: grandparents # default
470
560
  ```
471
561
 
472
- `prune` on each relationship/target entry allows:
562
+ Normal relationship pruning requires `min`. Tree pruning supports a relationship mode, a frontmatter mode, or both:
473
563
 
474
- * `min` (required): the minimum number of related documents needed to survive.
564
+ * Relationship mode requires both `min` and `depth`:
565
+ * `min`: the minimum number of related documents needed to survive.
566
+ * `depth`: which tree nodes are eligible for relationship-count pruning:
567
+ * `1` selects roots, `2` selects roots and their children, etc.
568
+ * `-1` and negative integers are the negation of their positive counterparts. Thus `-2` selects everything beyond the first two levels.
569
+ * Frontmatter mode requires a non-empty `where` hash of dot-separated frontmatter paths to expected values.
475
570
  * `mode: inverse` (optional): prune the `to` side instead of the `from` side.
476
- * `depth` (required if pruning a tree): only for tree relationships, determines which tree nodes can be pruned:
477
- * `1` selects roots, `2` would be roots and their chlidren, etc.
478
- * `-1` and negative integers are the negation of their positive counterparts. So `-2` means all but the first two levels of the tree are eligible for pruning.
479
571
 
480
- Pruning is iterative. E.g. if pruning causes more nodes to trigger pruning rules, they will also be pruned. Each round fully resolves the tree first, then fully resolves normal relationships on top of that tree.
572
+ When both tree modes are present, a document is pruned only when `where` matches and the `min`/`depth` mode also selects it. `where` is not supported on normal relationships.
573
+
574
+ `prune` can also be `false` to disable pruning at that level. Normal relationships accept an integer as a shortcut for `prune: min: int`; tree relationships do not.
575
+
576
+ ### Frontmatter matching
577
+
578
+ Each `where` entry is matched against the pruning subject: the `from` collection normally, or the inverse subject when `mode: inverse` is configured.
579
+
580
+ ```yaml
581
+ prune:
582
+ where:
583
+ exists: false
584
+ meta.status: hidden
585
+ ```
586
+
587
+ All entries must match. Paths traverse nested hashes only; arrays and hashes can instead be matched as complete terminal values. Matching is exact and type-sensitive, so `false` does not match `"false"`, and `1` does not match `1.0`. A present `null` matches an expected `null`, while a missing path never matches.
588
+
589
+ A where-only rule prunes every matching subject regardless of its relationship count.
590
+
591
+ ### Combine
592
+
593
+ Consider:
594
+
595
+ ```yaml
596
+ - from: products
597
+ to: categories, services
598
+ prune:
599
+ min: 2
600
+ ```
601
+
602
+ With `prune.combine: true`, the total count of links from `products` to `categories` *or* `services` would be considered. Only if this total count is below `min` would the product be pruned.
603
+
604
+ With `prune.combine: false` each relationship is checked separately. If either `products` → `categories` or `products` → `services` has fewer links than `min`, the product will be pruned.
605
+
606
+ ### Iteration
607
+
608
+ Pruning is iterative. E.g. if pruning causes more nodes to trigger pruning rules, they will also be pruned. Each round fully resolves the tree first, then fully resolves normal relationships on top of that tree. The maximum iterations can be controlled with `prune.iterations`.
609
+
610
+ ### Trees
481
611
 
482
612
  If pruning removes a node from a tree, any children that lose all parents become orphans. `relationships.prune.tree.orphans` controls what happens:
483
613
 
484
- * `grandparents` (default): reconnect to the pruned node's original grandparents, if any.
485
- * `grandparents required`: as above, but also remove the orphan if there is no grandparent to connect to.
614
+ * `grandparents` (default): follow each original parent lineage through consecutively removed nodes and reconnect to its nearest surviving ancestor. Leave the document parentless if no ancestor survives.
615
+ * `grandparents required`: reconnect as above, but remove the orphan if no original ancestor survives.
486
616
  * `prune`: prune all orphans recursively.
487
- * `orphan`: leave them parentless.
617
+ * `orphan`: leave them parentless without attempting reconnection.
488
618
 
489
- `prune` can also be `false` to disable pruning at that level, or an integer as a shortcut for `prune: min: int`.
619
+ Ancestor candidates retain their original order, are deduplicated, and continue to respect configured parent limits and cycle protection. Pruning does not change document URLs or permalinks.
490
620
 
491
621
 
492
622
  ## Keywords
@@ -532,6 +662,7 @@ relationships:
532
662
  foreign: data.client
533
663
  ```
534
664
 
665
+
535
666
  ## Notes
536
667
 
537
668
  **This gem is in an alpha release.** Breaking changes may occur between 0.x minor versions, and the gem overall has not been fully tested. If you encounter any issues please report them.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-relationships
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1.alpha
4
+ version: 0.2.0.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Convincible
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-31 00:00:00.000000000 Z
11
+ date: 2026-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -157,6 +157,7 @@ files:
157
157
  - lib/jekyll-relationships/resolvers/base.rb
158
158
  - lib/jekyll-relationships/run_logger.rb
159
159
  - lib/jekyll-relationships/support/data_path.rb
160
+ - lib/jekyll-relationships/support/frontmatter_matcher.rb
160
161
  - lib/jekyll-relationships/support/frontmatter_path.rb
161
162
  - lib/jekyll-relationships/support/hash_deep_merge.rb
162
163
  - lib/jekyll-relationships/support/placeholders.rb