jekyll-graph 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ module Graph
6
+
7
+ class PageWithoutAFile < Page
8
+ # rubocop:disable Naming/MemoizedInstanceVariableName
9
+ def read_yaml(*)
10
+ @data ||= {}
11
+ end
12
+ # rubocop:enable Naming/MemoizedInstanceVariableName
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Graph
5
+
6
+ class ForceGraphTag < Liquid::Tag
7
+ def render(context)
8
+ [
9
+ "<script src=\"//unpkg.com/element-resize-detector/dist/element-resize-detector.min.js\"></script>",
10
+ "<script src=\"//unpkg.com/force-graph\"></script>",
11
+ "<script src=\"https://d3js.org/d3.v6.min.js\"></script>",
12
+ ].join("\n").gsub!("\n", "") # for long-string legibility
13
+ end
14
+ end
15
+
16
+ # from: https://github.com/jekyll/jekyll-feed/blob/6d4913fe5017c685d2437f328ab4a9138cea07a8/lib/jekyll-feed/meta-tag.rb
17
+ class GraphScriptTag < Liquid::Tag
18
+ # TODO: this tag is actually not being used right now --
19
+ # but it's still here in case it is desirable to
20
+ # allow users to access each graph via their own
21
+ # div and skip scripting entirely
22
+
23
+ # Use Jekyll's native relative_url filter
24
+ include Jekyll::Filters::URLFilters
25
+
26
+ def render(context)
27
+ @context = context
28
+ "<script type=\"module\" src=\"#{$graph_conf.baseurl}#{$graph_conf.path_scripts}/jekyll-graph.js\" /></script>"
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Graph
5
+
6
+ VERSION = "0.0.1"
7
+
8
+ end
9
+ end
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+ require "jekyll"
3
+
4
+ require_relative "jekyll-graph/context"
5
+ require_relative "jekyll-graph/page"
6
+ require_relative "jekyll-graph/version"
7
+
8
+ # setup config
9
+ require_relative "jekyll-graph/config"
10
+ Jekyll::Hooks.register :site, :after_init do |site|
11
+ # global '$graph_conf' to ensure that all local jekyll plugins
12
+ # are reading from the same configuration with the same helper methods
13
+ # (global var is not ideal, but is DRY)
14
+ $graph_conf = Jekyll::Graph::PluginConfig.new(site.config)
15
+ end
16
+
17
+ require_relative "jekyll-graph/tags"
18
+ Liquid::Template.register_tag "force_graph", Jekyll::Graph::ForceGraphTag
19
+ Liquid::Template.register_tag "graph_scripts", Jekyll::Graph::GraphScriptTag
20
+
21
+ module Jekyll
22
+ module Graph
23
+
24
+ class Generator < Jekyll::Generator
25
+ priority :lowest
26
+
27
+ # Use Jekyll's native relative_url filter
28
+ include Jekyll::Filters::URLFilters
29
+
30
+ CONVERTER_CLASS = Jekyll::Converters::Markdown
31
+
32
+ def generate(site)
33
+ return if $graph_conf.disabled?
34
+ if !$graph_conf.disabled_net_web? && site.link_index.nil?
35
+ Jekyll.logger.error("To generate the net-web graph, please add and enable the 'jekyll-wikilinks' plugin")
36
+ return
37
+ end
38
+ if !$graph_conf.disabled_tree? && site.tree.nil?
39
+ Jekyll.logger.error("To generate the tree graph, please add and enable the 'jekyll-namespaces' plugin")
40
+ return
41
+ end
42
+
43
+ # setup site
44
+ @site = site
45
+ @context ||= Context.new(site)
46
+
47
+ # setup markdown docs
48
+ docs = []
49
+ docs += @site.pages if !$graph_conf.excluded?(:pages)
50
+ docs += @site.docs_to_write.filter { |d| !$graph_conf.excluded?(d.type) }
51
+ @md_docs = docs.filter { |doc| markdown_extension?(doc.extname) }
52
+ if @md_docs.empty?
53
+ Jekyll.logger.debug("No documents to process.")
54
+ end
55
+
56
+ # write graph
57
+ if !$graph_conf.disabled_net_web?
58
+ # generate json data
59
+ json_net_web_nodes, json_net_web_links = self.generate_json_net_web()
60
+ self.set_neighbors(json_net_web_nodes, json_net_web_links)
61
+ net_web_graph_content = JSON.dump(
62
+ nodes: json_net_web_nodes,
63
+ links: json_net_web_links,
64
+ )
65
+ # create json file
66
+ json_net_web_graph_file = self.new_page($graph_conf.path_assets, "graph-net-web.json", net_web_graph_content)
67
+ self.register_static_file(json_net_web_graph_file)
68
+ end
69
+ if !$graph_conf.disabled_tree?
70
+ # generate json data
71
+ json_tree_nodes, json_tree_links = self.generate_json_tree(@site.tree.root)
72
+ self.set_relatives(json_tree_nodes, json_tree_links)
73
+ tree_graph_content = JSON.dump(
74
+ nodes: json_tree_nodes,
75
+ links: json_tree_links,
76
+ )
77
+ # create json file
78
+ json_tree_graph_file = self.new_page($graph_conf.path_assets, "graph-tree.json", tree_graph_content)
79
+ self.register_static_file(json_tree_graph_file)
80
+ end
81
+ # add graph drawing scripts
82
+ script_filename = "jekyll-graph.js"
83
+ graph_script_content = File.read(source_path(script_filename))
84
+ static_file = self.new_page($graph_conf.path_scripts, script_filename, graph_script_content)
85
+ self.register_static_file(static_file)
86
+ end
87
+
88
+ # helpers
89
+
90
+ # from: https://github.com/jekyll/jekyll-sitemap/blob/master/lib/jekyll/jekyll-sitemap.rb#L39
91
+ def source_path(file)
92
+ File.expand_path "jekyll-graph/#{file}", __dir__
93
+ end
94
+
95
+ # Checks if a file already exists in the site source
96
+ def file_exists?(file_path)
97
+ @site.static_files.any? { |p| p.url == "/#{file_path}" }
98
+ end
99
+
100
+ def markdown_extension?(extension)
101
+ markdown_converter.matches(extension)
102
+ end
103
+
104
+ def markdown_converter
105
+ @markdown_converter ||= @site.find_converter_instance(CONVERTER_CLASS)
106
+ end
107
+
108
+ # generator helpers
109
+
110
+ def new_page(path, filename, content)
111
+ new_file = PageWithoutAFile.new(@site, __dir__, "", filename)
112
+ new_file.content = content
113
+ new_file.data["layout"] = nil
114
+ new_file.data["permalink"] = File.join(path, filename)
115
+ @site.pages << new_file unless file_exists?(filename)
116
+ return new_file
117
+ end
118
+
119
+ def register_static_file(static_file)
120
+ # tests fail without manually adding the static file, but actual site builds seem to do ok
121
+ # ...although there does seem to be a race condition which causes a rebuild to be necessary in order to detect the graph data file
122
+ if $graph_conf.testing
123
+ @site.static_files << static_file if !@site.static_files.include?(static_file)
124
+ end
125
+ end
126
+
127
+ # json population helpers
128
+ # set ids here, full javascript objects are populated in client-side javascript.
129
+
130
+ def set_neighbors(json_nodes, json_links)
131
+ json_links.each do |json_link|
132
+ source_node = json_nodes.detect { |n| n[:id] == json_link[:source] }
133
+ target_node = json_nodes.detect { |n| n[:id] == json_link[:target] }
134
+
135
+ source_node[:neighbors][:nodes] << target_node[:id]
136
+ target_node[:neighbors][:nodes] << source_node[:id]
137
+
138
+ source_node[:neighbors][:links] << json_link
139
+ target_node[:neighbors][:links] << json_link
140
+ end
141
+ end
142
+
143
+ def set_relatives(json_nodes, json_links)
144
+ # TODO: json nodes have relative_url, but node.id's/urls are doc urls.
145
+ json_nodes.each do |json_node|
146
+ ancestor_node_ids, descendent_node_ids = @site.tree.get_all_relative_ids(json_node[:id])
147
+ relative_node_ids = ancestor_node_ids.concat(descendent_node_ids)
148
+ json_node[:relatives][:nodes] = relative_node_ids if !relative_node_ids.nil?
149
+
150
+ # include current node when filtering for links along entire relative lineage
151
+ lineage_ids = relative_node_ids.concat([json_node[:id]])
152
+
153
+ json_relative_links = json_links.select { |l| lineage_ids.include?(l[:source]) && lineage_ids.include?(l[:target]) }
154
+ json_node[:relatives][:links] = json_relative_links if !json_relative_links.nil?
155
+ end
156
+ end
157
+
158
+ # json generation helpers
159
+
160
+ def generate_json_net_web()
161
+ net_web_nodes, net_web_links = [], []
162
+
163
+ @md_docs.each do |doc|
164
+ if !$graph_conf.excluded?(doc.type)
165
+
166
+ Jekyll.logger.debug "Processing graph nodes for doc: ", doc.data['title']
167
+ #
168
+ # missing nodes
169
+ #
170
+ @site.link_index.index[doc.url].missing.each do |missing_link_name|
171
+ if net_web_nodes.none? { |node| node[:id] == missing_link_name }
172
+ Jekyll.logger.warn "Net-Web node missing: ", missing_link_name
173
+ Jekyll.logger.warn " in: ", doc.data['title']
174
+ net_web_nodes << {
175
+ id: missing_link_name, # an id is necessary for link targets
176
+ url: '',
177
+ label: missing_link_name,
178
+ neighbors: {
179
+ nodes: [],
180
+ links: [],
181
+ },
182
+ }
183
+ net_web_links << {
184
+ source: doc.url,
185
+ target: missing_link_name,
186
+ }
187
+ end
188
+ end
189
+ #
190
+ # existing nodes
191
+ #
192
+ net_web_nodes << {
193
+ # TODO: when using real ids, be sure to convert id to string (to_s)
194
+ id: doc.url,
195
+ url: relative_url(doc.url),
196
+ label: doc.data['title'],
197
+ neighbors: {
198
+ nodes: [],
199
+ links: [],
200
+ },
201
+ }
202
+ # TODO: this link calculation ends up with duplicates -- re-visit this later.
203
+ @site.link_index.index[doc.url].attributes.each do |link| # link = { 'type' => str, 'urls' => [str, str, ...] }
204
+ # TODO: Header + Block-level wikilinks
205
+ link['urls'].each do |lu|
206
+ link_no_anchor = lu.match(/([^#]+)/i)[0]
207
+ link_no_baseurl = @site.baseurl.nil? ? link_no_anchor : link_no_anchor.gsub(@site.baseurl, "")
208
+ linked_doc = @md_docs.select{ |d| d.url == link_no_baseurl }
209
+ if !linked_doc.nil? && linked_doc.size == 1 && !$graph_conf.excluded?(linked_doc.first.type)
210
+ # TODO: add link['type'] to d3 graph
211
+ net_web_links << {
212
+ source: doc.url,
213
+ target: linked_doc.first.url,
214
+ }
215
+ end
216
+ end
217
+ end
218
+ @site.link_index.index[doc.url].forelinks.each do |link| # link = { 'type' => str, 'url' => str }
219
+ # TODO: Header + Block-level wikilinks
220
+ link_no_anchor = link['url'].match(/([^#]+)/i)[0]
221
+ link_no_baseurl = @site.baseurl.nil? ? link_no_anchor : link_no_anchor.gsub(@site.baseurl, "")
222
+ linked_doc = @md_docs.select{ |d| d.url == link_no_baseurl }
223
+ if !linked_doc.nil? && linked_doc.size == 1 && !$graph_conf.excluded?(linked_doc.first.type)
224
+ # TODO: add link['type'] to d3 graph
225
+ net_web_links << {
226
+ source: doc.url,
227
+ target: linked_doc.first.url,
228
+ }
229
+ end
230
+ end
231
+
232
+ end
233
+ end
234
+
235
+ return net_web_nodes, net_web_links
236
+ end
237
+
238
+ def generate_json_tree(node, json_parent="", tree_nodes=[], tree_links=[])
239
+ #
240
+ # missing nodes
241
+ #
242
+ if node.missing
243
+ Jekyll.logger.warn("Document for tree node missing: ", node.namespace)
244
+
245
+ leaf = node.namespace.split('.').pop()
246
+ missing_node = {
247
+ id: node.namespace,
248
+ label: leaf.gsub('-', ' '),
249
+ namespace: node.namespace,
250
+ url: "",
251
+ relatives: {
252
+ nodes: [],
253
+ links: [],
254
+ },
255
+ }
256
+ tree_nodes << missing_node
257
+ if !json_parent.empty?
258
+ tree_links << {
259
+ source: json_parent[:id],
260
+ target: node.namespace,
261
+ }
262
+ end
263
+ json_parent = missing_node
264
+ #
265
+ # existing nodes
266
+ #
267
+ else
268
+ existing_node = {
269
+ id: node.url,
270
+ label: node.title,
271
+ namespace: node.namespace,
272
+ url: relative_url(node.url),
273
+ relatives: {
274
+ nodes: [],
275
+ links: [],
276
+ },
277
+ }
278
+ tree_nodes << existing_node
279
+ if !json_parent.empty?
280
+ tree_links << {
281
+ source: json_parent[:id],
282
+ target: node.url,
283
+ }
284
+ end
285
+ json_parent = existing_node
286
+ end
287
+ node.children.each do |child|
288
+ self.generate_json_tree(child, json_parent, tree_nodes, tree_links)
289
+ end
290
+ return tree_nodes, tree_links
291
+ end
292
+ end
293
+
294
+ end
295
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-graph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - manunamz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jekyll-namespaces
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: jekyll-wikilinks
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.6
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.6
41
+ description:
42
+ email:
43
+ - manunamz@pm.me
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".rspec"
50
+ - ".rubocop.yml"
51
+ - ".travis.yml"
52
+ - CHANGELOG.md
53
+ - Gemfile
54
+ - LICENSE
55
+ - README.md
56
+ - Rakefile
57
+ - bin/console
58
+ - bin/setup
59
+ - jekyll-graph.gemspec
60
+ - lib/jekyll-graph.rb
61
+ - lib/jekyll-graph/config.rb
62
+ - lib/jekyll-graph/context.rb
63
+ - lib/jekyll-graph/jekyll-graph.js
64
+ - lib/jekyll-graph/page.rb
65
+ - lib/jekyll-graph/tags.rb
66
+ - lib/jekyll-graph/version.rb
67
+ homepage: https://github.com/manunamz/jekyll-graph
68
+ licenses: []
69
+ metadata:
70
+ homepage_uri: https://github.com/manunamz/jekyll-graph
71
+ source_code_uri: https://github.com/manunamz/jekyll-graph
72
+ changelog_uri: https://github.com/manunamz/jekyll-graph/blob/main/CHANGELOG.md
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.4.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.2.17
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Add d3 graph generation to jekyll.
92
+ test_files: []