jekyll-sheafy 0.1.0 → 0.2.0

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: 9d4d015eba5703cb9cc20923e78d52aa6e34581793348d3283e2652f375947d4
4
- data.tar.gz: 87eb2cde0952ff0887a711d6c00d93101f3b67072953946cdcdbfaa196f8a58a
3
+ metadata.gz: 3388e74b27cfab3e3fbdd846805e3c03c5326a7e91a30e6d1eb807f9d5619212
4
+ data.tar.gz: 0c9299c8e5cf165f0b8c84a66b5f7d9b51680c4b00453d999367aa5ea4d914b5
5
5
  SHA512:
6
- metadata.gz: e193c3aefb25940bb7d905bca9cb6090a9b2c64614f589eeaf7366299768ef79895dff96966c52f18abe3fb281f2404e207f89582eb05da173a30207d0ca0795
7
- data.tar.gz: 3f8a4517fd4c1e5dcc7fbb3a6cbe4c56e3b9512607756ca66f2f7d951a63185d4152266ec6c274eb69c11e935dd5a5e7aefac5e0d73c4b8ec6b0be9ddf9d044c
6
+ metadata.gz: b1b71ad0af49028612d9e60fe0544e13d941dfae46e81e135023da8f7004baeceff1b7d32571ffe05e0008a1d026446d2bc036ec653fc7b703ad432751e05322
7
+ data.tar.gz: 7b42013f49e76759e2fb80e5477bb562c28d9ebc88815afca2afe4c72968050c28163f2f9abe5b9002ac74d8a14e97947f455200cb350789816a79a45e553637
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2022-01-29
11
+
12
+ ### Added
13
+
14
+ - Configurable matchers for reference detection.
15
+
10
16
  ## [0.1.0] - 2022-01-28
11
17
 
12
18
  ### Added
data/README.md CHANGED
@@ -21,21 +21,78 @@
21
21
 
22
22
  ## Getting started
23
23
 
24
- TODO
24
+ Currently, the state of the art in using `jekyll-sheafy` is represented by [The Nursery][math-url]. Until I write a minimal guide, the bes way to get started is forking it and playing around with it.
25
25
 
26
26
  ## Usage
27
27
 
28
- TODO
28
+ > TODO: general usage notes.
29
+
30
+ ### Architecture
31
+
32
+ > TODO: explain the Directed Rooted Forest structure and the taxa mechanism.
33
+
34
+ ### Configuration
35
+
36
+ > TODO: fill in details for each parameter.
37
+
38
+ ```yaml
39
+ sheafy:
40
+ references:
41
+ matchers: [] # ...
42
+ taxa: {} # ...
43
+ ```
44
+
45
+ ### Node variables
46
+
47
+ > TODO: fill in details for each variable.
48
+
49
+ #### General
50
+
51
+ - `taxon`
52
+
53
+ #### Layouting
54
+
55
+ - `layout`
56
+ - `sublayout`
57
+
58
+ #### Dependencies
59
+
60
+ - `ancestors`
61
+ - `parent`
62
+ - `subroot`
63
+ - `children`
64
+
65
+ #### References
66
+
67
+ - `referrers`
68
+
69
+ #### Numbering
70
+
71
+ - `clicker`
72
+ - `clicks`
29
73
 
30
74
  ## Roadmap
31
75
 
32
- TODO
76
+ These are the features you can expect in the future:
77
+
78
+ - Enable `referents` variable w/ nodes references by the current one
79
+ - `root` variable w/ root node of the tree
80
+ - `siblings` variable or some variant of it to enable navigation between adjacent nodes at the same depth
81
+ - Prev/next node navigation
82
+ - Variable inheritance from parent/root node
83
+ - Search feature
84
+
85
+ Of course any feedback is welcome!
33
86
 
34
87
  ## Acknowledgements
35
88
 
36
- - Thanks to [@jonsterling](https://github.com/jonsterling) for [having the idea][math-url] and letting me collaborate and spin it off.
89
+ - Thanks to [@jonsterling](https://github.com/jonsterling) for
90
+ - using [`krater`][krater-url] to setup [The Nursery][math-url],
91
+ - having the "brew your own Kerodon" idea, and
92
+ - letting me collaborate to spin it off into `jekyll-sheafy`.
37
93
 
38
94
  [jekyll-url]: https://jekyllrb.com/
95
+ [krater-url]: https://github.com/paolobrasolin/krater/
39
96
  [math-url]: https://github.com/jonsterling/math
40
97
  [gerby-url]: https://gerby-project.github.io/
41
98
  [stacks-url]: https://stacks.math.columbia.edu/
@@ -0,0 +1,138 @@
1
+ require "jekyll/sheafy/directed_graph"
2
+
3
+ module Jekyll
4
+ module Sheafy
5
+ module Dependencies
6
+ RE_INCLUDE_TAG = /^@include{(?<slug>.+?)}$/
7
+ SUBLAYOUT_KEY = "sublayout"
8
+ SUBLAYOUT_DEFAULT_VALUE = "sheafy/node/default"
9
+ SUBROOT_KEY = "subroot"
10
+
11
+ def self.process(nodes)
12
+ adjacency_list = build_adjacency_list(nodes)
13
+ graph = build_rooted_forest!(adjacency_list)
14
+ denormalize_adjacency_list!(graph, nodes)
15
+ attribute_neighbors!(graph)
16
+
17
+ # NOTE: top.ord. is good to denormalize from leaves up to roots, while
18
+ # rev.top.ord is good to denormalize data from roots down to leaves.
19
+ # I.e.: for destructive procedures which need the altered children use
20
+ # tsorted_nodes.each { |node| ... }
21
+ # and for destructive procedures which need the original children use
22
+ # tsorted_nodes.reverse.each { |node| ... }
23
+ tsorted_nodes = graph.topologically_sorted
24
+
25
+ tsorted_nodes.reverse.each do |node|
26
+ attribute_ancestors!(node)
27
+ attribute_depth!(node)
28
+ attribute_clicks!(node)
29
+ end
30
+
31
+ # NOTE: this pass is separate to have data available in layouts.
32
+ tsorted_nodes.reverse.each do |node|
33
+ flatten_subtree!(node, nodes)
34
+ end
35
+ end
36
+
37
+ def self.scan_includes(node)
38
+ node.content.scan(RE_INCLUDE_TAG).flatten
39
+ end
40
+
41
+ def self.build_adjacency_list(nodes_index)
42
+ nodes_index.transform_values(&method(:scan_includes))
43
+ end
44
+
45
+ def self.denormalize_adjacency_list!(list, index)
46
+ # TODO: handle missing nodes
47
+ list.transform_keys!(&index)
48
+ list.values.each { |children| children.map!(&index) }
49
+ end
50
+
51
+ def self.attribute_neighbors!(list)
52
+ list.each do |parent, children|
53
+ parent.data["children"] = children
54
+ children.each { |child| child.data["parent"] ||= parent }
55
+ end
56
+ end
57
+
58
+ def self.attribute_ancestors!(node)
59
+ node.data["ancestors"] = []
60
+ parent = node.data["parent"]
61
+ if parent
62
+ ancestors = [*parent.data["ancestors"], parent]
63
+ node.data["ancestors"] = ancestors
64
+ end
65
+ end
66
+
67
+ def self.attribute_depth!(node)
68
+ parent = node.data["parent"]
69
+ node.data["depth"] = 1 + (parent&.data&.[]("depth") || -1)
70
+ end
71
+
72
+ def self.attribute_clicks!(node)
73
+ node.data["clicks"] ||= [
74
+ { "clicker" => node.data["clicker"], "value" => 0 },
75
+ ]
76
+ node.data["children"].
77
+ group_by { |child| child.data["clicker"] }.
78
+ each do |clicker, children|
79
+ children.each_with_index do |child, index|
80
+ clicks = node.data["clicks"].dup
81
+ clicks << { "clicker" => clicker, "value" => index }
82
+ child.data["clicks"] = clicks
83
+ end
84
+ end
85
+ end
86
+
87
+ def self.build_rooted_forest!(adjacency_list)
88
+ Sheafy::DirectedGraph[adjacency_list].
89
+ tap(&:ensure_rooted_forest!)
90
+ rescue Sheafy::DirectedGraph::PayloadError => error
91
+ message = case error
92
+ when Sheafy::DirectedGraph::MultipleEdgesError then "node reuse"
93
+ when Sheafy::DirectedGraph::LoopsError then "self reference"
94
+ when Sheafy::DirectedGraph::CyclesError then "cyclic reference"
95
+ when Sheafy::DirectedGraph::IndegreeError then "node reuse"
96
+ else raise StandardError.new("Malformed dependency graph!")
97
+ end
98
+ raise StandardError.new(<<~MESSAGE)
99
+ Error in dependency graph topology, #{message} detected: #{error.payload}
100
+ MESSAGE
101
+ end
102
+
103
+ def self.apply_sublayout(resource, content, subroot)
104
+ sublayout = resource.data.fetch(SUBLAYOUT_KEY, SUBLAYOUT_DEFAULT_VALUE)
105
+ # NOTE: all this mess is just to adhere to Jekyll's internals
106
+ site = resource.site
107
+ payload = site.site_payload
108
+ payload["page"] = resource.to_liquid
109
+ payload["page"].merge!(SUBROOT_KEY => subroot)
110
+ payload["content"] = content
111
+ info = {
112
+ :registers => { :site => site, :page => payload["page"] },
113
+ :strict_filters => site.config["liquid"]["strict_filters"],
114
+ :strict_variables => site.config["liquid"]["strict_variables"],
115
+ }
116
+ layout = site.layouts[sublayout]
117
+ # TODO add_regenerator_dependencies(layout)
118
+ template = site.liquid_renderer.file(layout.path).parse(layout.content)
119
+ # TODO: handle warnings like https://github.com/jekyll/jekyll/blob/0b12fd26aed1038f69169b665818f5245e4f4b6d/lib/jekyll/renderer.rb#L126
120
+ template.render!(payload, info)
121
+ # TODO: handle exceptions like https://github.com/jekyll/jekyll/blob/0b12fd26aed1038f69169b665818f5245e4f4b6d/lib/jekyll/renderer.rb#L131
122
+ end
123
+
124
+ def self.flatten_subtree!(node, nodes)
125
+ node.content = flatten_subtree(node, nodes)
126
+ end
127
+
128
+ def self.flatten_subtree(resource, resources, subroot = resource)
129
+ content = resource.content.gsub(RE_INCLUDE_TAG) do
130
+ doc = resources[Regexp.last_match[:slug]]
131
+ # TODO: handle missing references
132
+ flatten_subtree(doc, resources, subroot)
133
+ end
134
+ apply_sublayout(resource, content, subroot)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,70 @@
1
+ require "safe_yaml"
2
+ # NOTE: allows for rexexp entries in mathers array in config
3
+ SafeYAML::OPTIONS[:whitelisted_tags].push("!ruby/regexp")
4
+
5
+ module Jekyll
6
+ module Sheafy
7
+ module References
8
+ class Error < StandardError; end
9
+ class InvalidMatcher < Error; end
10
+
11
+ CONFIG_KEY = "references"
12
+ DEFAULT_CONFIG = {
13
+ "matchers" => [
14
+ /{%\s*ref (?<slug>.+?)\s*%}/,
15
+ ],
16
+ }
17
+ @@config = DEFAULT_CONFIG
18
+ REFERRERS_KEY = "referrers"
19
+
20
+ def self.validate_matcher!(matcher)
21
+ return if matcher.names.include?("slug")
22
+ raise InvalidMatcher.new(<<~ERROR)
23
+ Sheafy configuration error: the matcher #{matcher} is missing a capturing group named "slug".
24
+ ERROR
25
+ end
26
+
27
+ def self.load_config(config)
28
+ overrides = config.fetch(CONFIG_KEY, {})
29
+ overrides["matchers"]&.each(&method(:validate_matcher!))
30
+ @@config = Jekyll::Utils.deep_merge_hashes(DEFAULT_CONFIG, overrides)
31
+ end
32
+
33
+ def self.process(nodes_index, config = {})
34
+ load_config(config)
35
+ adjacency_list = build_adjacency_list(nodes_index)
36
+ denormalize_adjacency_list!(adjacency_list, nodes_index)
37
+ # NOTE: topology is arbitrary so no single pass technique is possible.
38
+ nodes_index.values.each { |node| node.data[REFERRERS_KEY] = [] }
39
+ attribute_neighbors!(adjacency_list)
40
+ end
41
+
42
+ def self.scan_references(node)
43
+ @@config["matchers"].flat_map do |matcher|
44
+ node.content.scan(matcher).flatten
45
+ end
46
+ end
47
+
48
+ def self.build_adjacency_list(nodes_index)
49
+ nodes_index.transform_values(&method(:scan_references))
50
+ end
51
+
52
+ def self.denormalize_adjacency_list!(list, index)
53
+ # TODO: handle missing nodes
54
+ list.transform_keys!(&index)
55
+ list.values.each { |children| children.map!(&index) }
56
+ end
57
+
58
+ def self.attribute_neighbors!(list)
59
+ list.each do |referrer, referents|
60
+ # TODO: can referents be useful?
61
+ # referrer.data["referents"] = referents.uniq
62
+ referents.each do |referent|
63
+ next if referent.data[REFERRERS_KEY].include?(referrer)
64
+ referent.data[REFERRERS_KEY] << referrer
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,18 @@
1
+ module Jekyll
2
+ module Sheafy
3
+ module Taxa
4
+ TAXON_KEY = "taxon"
5
+
6
+ def self.process(nodes_index)
7
+ nodes_index.values.each(&method(:apply_taxon!))
8
+ end
9
+
10
+ def self.apply_taxon!(node)
11
+ taxon_name = node.data[TAXON_KEY]
12
+ taxon_data = node.site.config.dig("sheafy", "taxa", taxon_name) || {}
13
+ # TODO: emit warning on undefined taxon
14
+ node.data.merge!(taxon_data) { |key, left, right| left }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  module Jekyll
2
2
  module Sheafy
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/jekyll/sheafy.rb CHANGED
@@ -1 +1,29 @@
1
+ require "jekyll"
2
+ require "jekyll/sheafy/dependencies"
3
+ require "jekyll/sheafy/references"
4
+ require "jekyll/sheafy/taxa"
1
5
  require "jekyll/sheafy/version"
6
+
7
+ module Jekyll
8
+ module Sheafy
9
+ CONFIG_KEY = "sheafy"
10
+
11
+ def self.process(site)
12
+ config = site.config.fetch(CONFIG_KEY, {})
13
+ nodes_index = build_nodes_index(site)
14
+ Jekyll::Sheafy::Taxa.process(nodes_index)
15
+ Jekyll::Sheafy::References.process(nodes_index, config)
16
+ Jekyll::Sheafy::Dependencies.process(nodes_index)
17
+ end
18
+
19
+ def self.build_nodes_index(site)
20
+ site.collections.values.flat_map(&:docs).
21
+ filter { |doc| doc.data.key?(Jekyll::Sheafy::Taxa::TAXON_KEY) }.
22
+ map { |doc| [doc.data["slug"], doc] }.to_h
23
+ end
24
+ end
25
+ end
26
+
27
+ Jekyll::Hooks.register :site, :post_read, priority: 30 do |site|
28
+ Jekyll::Sheafy.process(site)
29
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-sheafy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paolo Brasolin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-28 00:00:00.000000000 Z
11
+ date: 2022-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -99,7 +99,10 @@ files:
99
99
  - LICENSE
100
100
  - README.md
101
101
  - lib/jekyll/sheafy.rb
102
+ - lib/jekyll/sheafy/dependencies.rb
102
103
  - lib/jekyll/sheafy/directed_graph.rb
104
+ - lib/jekyll/sheafy/references.rb
105
+ - lib/jekyll/sheafy/taxa.rb
103
106
  - lib/jekyll/sheafy/version.rb
104
107
  homepage: https://github.com/paolobrasolin/jekyll-sheafy
105
108
  licenses:
@@ -125,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
128
  - !ruby/object:Gem::Version
126
129
  version: '0'
127
130
  requirements: []
128
- rubygems_version: 3.1.4
131
+ rubygems_version: 3.1.6
129
132
  signing_key:
130
133
  specification_version: 4
131
134
  summary: Brew your own Stacks Project with Jekyll!