jekyll-sheafy 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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!