jekyll-sheafy 0.2.0 → 0.3.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: 3388e74b27cfab3e3fbdd846805e3c03c5326a7e91a30e6d1eb807f9d5619212
4
- data.tar.gz: 0c9299c8e5cf165f0b8c84a66b5f7d9b51680c4b00453d999367aa5ea4d914b5
3
+ metadata.gz: 8ec5d4f41ee3fca018fca579929acf04ad876ff525de337d6e385e8949226f62
4
+ data.tar.gz: 3e3feffb64ce77917d63fc53506bed3310b1dc3d4f52f0dd7dd66ba615f14e74
5
5
  SHA512:
6
- metadata.gz: b1b71ad0af49028612d9e60fe0544e13d941dfae46e81e135023da8f7004baeceff1b7d32571ffe05e0008a1d026446d2bc036ec653fc7b703ad432751e05322
7
- data.tar.gz: 7b42013f49e76759e2fb80e5477bb562c28d9ebc88815afca2afe4c72968050c28163f2f9abe5b9002ac74d8a14e97947f455200cb350789816a79a45e553637
6
+ metadata.gz: 25b689459a2f27e2399463f31a24220b76875ea97e0f37c533ae975ed1c43f0b6369618c959bcb6502062565c61bf9b1b1ddb5b803bc2207ad08251ade16126f
7
+ data.tar.gz: 1d0d6d01edf37590ec5b41529eddca5778743cbe3c899bd44ca6630ebe15b7fdf5a1f4b082caa534e1b64e733fb343721fc14093572092dfe7a490768c5e3983
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2022-02-01
11
+
12
+ ### Added
13
+
14
+ - `inheritable` config key (array of strings, validated) to define attributes which are inherited from parent node.
15
+ - `referents` variable containing list of referents (i.e. the targets of all references in the current node).
16
+ - `root` variable containing the root node of the tree the current node belongs to.
17
+ - `predecessors`/`successors` variables containing the siblings preceeding/following the current node.
18
+
19
+ ### Changed
20
+
21
+ - Error message are a bit more rational.
22
+ - `taxa` configuration key is now validated; it must be an hash valued in hashes.
23
+ - `references.matchers` configuration key is now validated; it must be an array of regexps containing a single named capture "slug".
24
+
10
25
  ## [0.2.0] - 2022-01-29
11
26
 
12
27
  ### Added
@@ -20,5 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
20
35
  - Structure to represent directed graphs.
21
36
  - Topological checks for rooted forests.
22
37
 
23
- [unreleased]: https://github.com/paolobrasolin/jekyll-sheafy/compare/0.1.0...HEAD
38
+ [unreleased]: https://github.com/paolobrasolin/jekyll-sheafy/compare/0.3.0...HEAD
39
+ [0.3.0]: https://github.com/paolobrasolin/jekyll-sheafy/compare/0.2.0...0.3.0
40
+ [0.2.0]: https://github.com/paolobrasolin/jekyll-sheafy/compare/0.1.0...0.2.0
24
41
  [0.1.0]: https://github.com/paolobrasolin/jekyll-sheafy/releases/tag/0.1.0
data/README.md CHANGED
@@ -40,6 +40,7 @@ sheafy:
40
40
  references:
41
41
  matchers: [] # ...
42
42
  taxa: {} # ...
43
+ inheritable: [] # ...
43
44
  ```
44
45
 
45
46
  ### Node variables
@@ -57,14 +58,18 @@ sheafy:
57
58
 
58
59
  #### Dependencies
59
60
 
61
+ - `root`
60
62
  - `ancestors`
61
63
  - `parent`
62
64
  - `subroot`
63
65
  - `children`
66
+ - `predecessors`
67
+ - `successors`
64
68
 
65
69
  #### References
66
70
 
67
71
  - `referrers`
72
+ - `referents`
68
73
 
69
74
  #### Numbering
70
75
 
@@ -75,11 +80,6 @@ sheafy:
75
80
 
76
81
  These are the features you can expect in the future:
77
82
 
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
83
  - Search feature
84
84
 
85
85
  Of course any feedback is welcome!
@@ -4,9 +4,10 @@ module Jekyll
4
4
  module Sheafy
5
5
  module Dependencies
6
6
  RE_INCLUDE_TAG = /^@include{(?<slug>.+?)}$/
7
- SUBLAYOUT_KEY = "sublayout"
8
- SUBLAYOUT_DEFAULT_VALUE = "sheafy/node/default"
9
- SUBROOT_KEY = "subroot"
7
+
8
+ class Error < TemplateError; end
9
+
10
+ InvalidConfig = Error.build("Invalid config: %s is not an %s.")
10
11
 
11
12
  def self.process(nodes)
12
13
  adjacency_list = build_adjacency_list(nodes)
@@ -26,6 +27,7 @@ module Jekyll
26
27
  attribute_ancestors!(node)
27
28
  attribute_depth!(node)
28
29
  attribute_clicks!(node)
30
+ attribute_inheritable!(node)
29
31
  end
30
32
 
31
33
  # NOTE: this pass is separate to have data available in layouts.
@@ -34,6 +36,8 @@ module Jekyll
34
36
  end
35
37
  end
36
38
 
39
+ #==[ Graph building ]=====================================================
40
+
37
41
  def self.scan_includes(node)
38
42
  node.content.scan(RE_INCLUDE_TAG).flatten
39
43
  end
@@ -42,16 +46,31 @@ module Jekyll
42
46
  nodes_index.transform_values(&method(:scan_includes))
43
47
  end
44
48
 
49
+ def self.build_rooted_forest!(adjacency_list)
50
+ Sheafy::DirectedGraph[adjacency_list].
51
+ tap(&:ensure_rooted_forest!)
52
+ rescue Jekyll::Sheafy::DirectedGraph::Error => error
53
+ raise StandardError.new(<<~MESSAGE)
54
+ Sheafy processing error! The nodes dependency graph MUST be a directed rooted forest. #{error.message}
55
+ MESSAGE
56
+ end
57
+
45
58
  def self.denormalize_adjacency_list!(list, index)
46
59
  # TODO: handle missing nodes
47
60
  list.transform_keys!(&index)
48
61
  list.values.each { |children| children.map!(&index) }
49
62
  end
50
63
 
64
+ #==[ Data generation ]====================================================
65
+
51
66
  def self.attribute_neighbors!(list)
52
67
  list.each do |parent, children|
53
68
  parent.data["children"] = children
54
- children.each { |child| child.data["parent"] ||= parent }
69
+ children.each_with_index do |child, index|
70
+ child.data["parent"] ||= parent
71
+ child.data["predecessors"] = children.take(index)
72
+ child.data["successors"] = children.drop(index.succ)
73
+ end
55
74
  end
56
75
  end
57
76
 
@@ -62,6 +81,7 @@ module Jekyll
62
81
  ancestors = [*parent.data["ancestors"], parent]
63
82
  node.data["ancestors"] = ancestors
64
83
  end
84
+ node.data["root"] = parent&.data&.[]("root") || node
65
85
  end
66
86
 
67
87
  def self.attribute_depth!(node)
@@ -73,8 +93,8 @@ module Jekyll
73
93
  node.data["clicks"] ||= [
74
94
  { "clicker" => node.data["clicker"], "value" => 0 },
75
95
  ]
76
- node.data["children"].
77
- group_by { |child| child.data["clicker"] }.
96
+ node.data["children"]&.
97
+ group_by { |child| child.data["clicker"] }&.
78
98
  each do |clicker, children|
79
99
  children.each_with_index do |child, index|
80
100
  clicks = node.data["clicks"].dup
@@ -84,22 +104,22 @@ module Jekyll
84
104
  end
85
105
  end
86
106
 
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
107
+ INHERITABLE_PATH = ["sheafy", "inheritable"]
108
+
109
+ def self.attribute_inheritable!(node)
110
+ inheritable = node.site.config.dig(*INHERITABLE_PATH) || []
111
+ return unless (parent = node.data["parent"])
112
+ inherited = parent.data.slice(*inheritable)
113
+ # TODO: how/when do we need to clone/dup here?
114
+ node.data.merge!(**inherited) { |key, left, right| left }
101
115
  end
102
116
 
117
+ #==[ Layout flattening ]==================================================
118
+
119
+ SUBLAYOUT_KEY = "sublayout"
120
+ SUBLAYOUT_DEFAULT_VALUE = "sheafy/node/default"
121
+ SUBROOT_KEY = "subroot"
122
+
103
123
  def self.apply_sublayout(resource, content, subroot)
104
124
  sublayout = resource.data.fetch(SUBLAYOUT_KEY, SUBLAYOUT_DEFAULT_VALUE)
105
125
  # NOTE: all this mess is just to adhere to Jekyll's internals
@@ -133,6 +153,21 @@ module Jekyll
133
153
  end
134
154
  apply_sublayout(resource, content, subroot)
135
155
  end
156
+
157
+ #==[ Config validation ]==================================================
158
+
159
+ def self.validate_config!(config)
160
+ case (inheritables = config.dig(*INHERITABLE_PATH))
161
+ when nil
162
+ when Array
163
+ inheritables.each_with_index do |inheritable, index|
164
+ next if inheritable.is_a?(String)
165
+ raise InvalidConfig.new([*INHERITABLE_PATH, index].join("."), "string")
166
+ end
167
+ else
168
+ raise InvalidConfig.new(INHERITABLE_PATH.join("."), "array")
169
+ end
170
+ end
136
171
  end
137
172
  end
138
173
  end
@@ -1,23 +1,17 @@
1
1
  require "tsort"
2
+ require "jekyll/sheafy/template_error"
2
3
 
3
4
  module Jekyll
4
5
  module Sheafy
5
6
  class DirectedGraph < Hash
6
- class PayloadError < StandardError
7
- attr_reader :payload
7
+ class Error < TemplateError; end
8
8
 
9
- def initialize(payload)
10
- @payload = payload
11
- super
12
- end
13
- end
14
-
15
- class MissingKeysError < PayloadError; end
16
- class InvalidValuesError < PayloadError; end
17
- class MultipleEdgesError < PayloadError; end
18
- class LoopsError < PayloadError; end
19
- class CyclesError < PayloadError; end
20
- class IndegreeError < PayloadError; end
9
+ MissingKeysError = Error.build("Missing keys detected: %s.")
10
+ InvalidValuesError = Error.build("Invalid values detected: %s.")
11
+ MultipleEdgesError = Error.build("Parallel edges detected: %s.")
12
+ LoopsError = Error.build("Loops detected: %s.")
13
+ CyclesError = Error.build("Cycles detected: %s.")
14
+ IndegreeError = Error.build("Nodes with indegree > 1 detected: %s.")
21
15
 
22
16
  def ensure_rooted_forest!
23
17
  ensure_valid!
@@ -1,47 +1,37 @@
1
1
  require "safe_yaml"
2
+ require "jekyll/sheafy/template_error"
3
+
2
4
  # NOTE: allows for rexexp entries in mathers array in config
3
5
  SafeYAML::OPTIONS[:whitelisted_tags].push("!ruby/regexp")
4
6
 
5
7
  module Jekyll
6
8
  module Sheafy
7
9
  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
10
+ MATCHERS_PATH = ["sheafy", "references", "matchers"]
11
+ DEFAULT_MATCHERS = [
12
+ /{%\s*ref (?<slug>.+?)\s*%}/,
13
+ ]
14
+ SLUG_CAPTURE_NAME = "slug"
18
15
  REFERRERS_KEY = "referrers"
16
+ REFERENTS_KEY = "referents"
19
17
 
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
18
+ class Error < TemplateError; end
26
19
 
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
20
+ InvalidMatcher = Error.build("Invalid reference matcher: %s should have one capture group named '#{SLUG_CAPTURE_NAME}'.")
32
21
 
33
- def self.process(nodes_index, config = {})
34
- load_config(config)
22
+ def self.process(nodes_index)
35
23
  adjacency_list = build_adjacency_list(nodes_index)
36
24
  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
25
  attribute_neighbors!(adjacency_list)
40
26
  end
41
27
 
28
+ #==[ Graph building ]=====================================================
29
+
42
30
  def self.scan_references(node)
43
- @@config["matchers"].flat_map do |matcher|
44
- node.content.scan(matcher).flatten
31
+ matchers = node.site.config.dig(*MATCHERS_PATH) || DEFAULT_MATCHERS
32
+ matchers.flat_map do |matcher|
33
+ index = matcher.named_captures.fetch(SLUG_CAPTURE_NAME).fetch(0).pred
34
+ node.content.scan(matcher).map { |captures| captures.fetch(index) }
45
35
  end
46
36
  end
47
37
 
@@ -55,16 +45,31 @@ module Jekyll
55
45
  list.values.each { |children| children.map!(&index) }
56
46
  end
57
47
 
48
+ #==[ Data generation ]====================================================
49
+
58
50
  def self.attribute_neighbors!(list)
51
+ # NOTE: topology is arbitrary so no single pass technique is possible.
52
+ list.keys.each { |node| node.data[REFERRERS_KEY] = [] }
59
53
  list.each do |referrer, referents|
60
- # TODO: can referents be useful?
61
- # referrer.data["referents"] = referents.uniq
54
+ referrer.data[REFERENTS_KEY] = referents.uniq
62
55
  referents.each do |referent|
63
56
  next if referent.data[REFERRERS_KEY].include?(referrer)
64
57
  referent.data[REFERRERS_KEY] << referrer
65
58
  end
66
59
  end
67
60
  end
61
+
62
+ #==[ Config validation ]==================================================
63
+
64
+ def self.validate_config!(config)
65
+ config.dig(*MATCHERS_PATH)&.each(&method(:validate_matcher!))
66
+ end
67
+
68
+ def self.validate_matcher!(matcher)
69
+ valid = matcher.named_captures.key?(SLUG_CAPTURE_NAME)
70
+ valid &&= matcher.named_captures.fetch(SLUG_CAPTURE_NAME).one?
71
+ raise InvalidMatcher.new(matcher) unless valid
72
+ end
68
73
  end
69
74
  end
70
75
  end
@@ -1,18 +1,39 @@
1
+ require "jekyll/sheafy/template_error"
2
+
1
3
  module Jekyll
2
4
  module Sheafy
3
5
  module Taxa
6
+ TAXA_PATH = ["sheafy", "taxa"]
4
7
  TAXON_KEY = "taxon"
5
8
 
9
+ class Error < TemplateError; end
10
+
11
+ InvalidTaxon = Error.build("Invalid taxon: %s should be an hash.")
12
+
6
13
  def self.process(nodes_index)
7
14
  nodes_index.values.each(&method(:apply_taxon!))
8
15
  end
9
16
 
17
+ #==[ Data generation ]====================================================
18
+
10
19
  def self.apply_taxon!(node)
11
20
  taxon_name = node.data[TAXON_KEY]
12
- taxon_data = node.site.config.dig("sheafy", "taxa", taxon_name) || {}
21
+ taxon_data = node.site.config.dig(*TAXA_PATH, taxon_name) || {}
13
22
  # TODO: emit warning on undefined taxon
14
23
  node.data.merge!(taxon_data) { |key, left, right| left }
15
24
  end
25
+
26
+ #==[ Config validation ]==================================================
27
+
28
+ def self.validate_config!(config)
29
+ config.dig(*TAXA_PATH)&.each_pair do |taxon_key, taxon_value|
30
+ validate_taxon!(taxon_key, taxon_value)
31
+ end
32
+ end
33
+
34
+ def self.validate_taxon!(taxon_key, taxon_value)
35
+ raise InvalidTaxon.new(taxon_key) unless taxon_value.is_a?(Hash)
36
+ end
16
37
  end
17
38
  end
18
39
  end
@@ -0,0 +1,25 @@
1
+ module Jekyll
2
+ module Sheafy
3
+ # NOTE: be careful in wielding magick!
4
+ class TemplateError < StandardError
5
+ attr_reader :payload
6
+
7
+ def initialize(*payload)
8
+ @payload = payload
9
+ end
10
+
11
+ def to_s
12
+ self.class.instance_variable_get(:@template) % payload
13
+ end
14
+
15
+ def self.build(template)
16
+ raise ArgumentError.new(<<~MESSAGE) unless template.is_a?(String)
17
+ wrong type of argument (given #{template.class}, expected String)
18
+ MESSAGE
19
+ Class.new(self) do
20
+ @template = template
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  module Jekyll
2
2
  module Sheafy
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
data/lib/jekyll/sheafy.rb CHANGED
@@ -2,17 +2,25 @@ require "jekyll"
2
2
  require "jekyll/sheafy/dependencies"
3
3
  require "jekyll/sheafy/references"
4
4
  require "jekyll/sheafy/taxa"
5
+ require "jekyll/sheafy/template_error"
5
6
  require "jekyll/sheafy/version"
6
7
 
7
8
  module Jekyll
8
9
  module Sheafy
9
- CONFIG_KEY = "sheafy"
10
+ def self.validate_config!(site)
11
+ Jekyll::Sheafy::Taxa.validate_config!(site.config)
12
+ Jekyll::Sheafy::References.validate_config!(site.config)
13
+ Jekyll::Sheafy::Dependencies.validate_config!(site.config)
14
+ rescue Jekyll::Sheafy::TemplateError => error
15
+ raise StandardError.new(<<~MESSAGE)
16
+ Sheafy configuration error! #{error.message}
17
+ MESSAGE
18
+ end
10
19
 
11
20
  def self.process(site)
12
- config = site.config.fetch(CONFIG_KEY, {})
13
21
  nodes_index = build_nodes_index(site)
14
22
  Jekyll::Sheafy::Taxa.process(nodes_index)
15
- Jekyll::Sheafy::References.process(nodes_index, config)
23
+ Jekyll::Sheafy::References.process(nodes_index)
16
24
  Jekyll::Sheafy::Dependencies.process(nodes_index)
17
25
  end
18
26
 
@@ -24,6 +32,10 @@ module Jekyll
24
32
  end
25
33
  end
26
34
 
35
+ Jekyll::Hooks.register :site, :after_init do |site|
36
+ Jekyll::Sheafy::validate_config!(site)
37
+ end
38
+
27
39
  Jekyll::Hooks.register :site, :post_read, priority: 30 do |site|
28
40
  Jekyll::Sheafy.process(site)
29
41
  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.2.0
4
+ version: 0.3.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-29 00:00:00.000000000 Z
11
+ date: 2022-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -44,6 +44,34 @@ dependencies:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: 11.1.3
47
+ - !ruby/object:Gem::Dependency
48
+ name: guard-rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 4.7.3
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 4.7.3
61
+ - !ruby/object:Gem::Dependency
62
+ name: guard
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 2.18.0
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 2.18.0
47
75
  - !ruby/object:Gem::Dependency
48
76
  name: rspec
49
77
  requirement: !ruby/object:Gem::Requirement
@@ -72,6 +100,20 @@ dependencies:
72
100
  - - "~>"
73
101
  - !ruby/object:Gem::Version
74
102
  version: 0.13.0
103
+ - !ruby/object:Gem::Dependency
104
+ name: simplecov-lcov
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 0.8.0
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 0.8.0
75
117
  - !ruby/object:Gem::Dependency
76
118
  name: simplecov
77
119
  requirement: !ruby/object:Gem::Requirement
@@ -103,6 +145,7 @@ files:
103
145
  - lib/jekyll/sheafy/directed_graph.rb
104
146
  - lib/jekyll/sheafy/references.rb
105
147
  - lib/jekyll/sheafy/taxa.rb
148
+ - lib/jekyll/sheafy/template_error.rb
106
149
  - lib/jekyll/sheafy/version.rb
107
150
  homepage: https://github.com/paolobrasolin/jekyll-sheafy
108
151
  licenses:
@@ -128,7 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
171
  - !ruby/object:Gem::Version
129
172
  version: '0'
130
173
  requirements: []
131
- rubygems_version: 3.1.6
174
+ rubygems_version: 3.1.4
132
175
  signing_key:
133
176
  specification_version: 4
134
177
  summary: Brew your own Stacks Project with Jekyll!