jekyll-semtree 0.0.1 → 0.0.2
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 +4 -4
- data/lib/jekyll-semtree/patch/context.rb +13 -0
- data/lib/jekyll-semtree/patch/doc_manager.rb +120 -0
- data/lib/jekyll-semtree/patch/site.rb +13 -0
- data/lib/jekyll-semtree/tree.rb +478 -0
- data/lib/jekyll-semtree/version.rb +1 -1
- data/lib/jekyll-semtree.rb +158 -2
- metadata +12 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec2c16946b942b0e6466e9fff5d94e1109fdd7eb6c7b2330ee760cd8e9cb1c5f
|
4
|
+
data.tar.gz: 2831eeca5e0722a33ac0d17eff83952245da02a9153173f9d6a8080f8ef27b3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e34ef808a05bab7f70619b733bc96e5885b25ac1c223ec9290835cdca48e46b704586b6c18d57db77c10032babfbce459b60c45c8675b6ed0959fe3e72d6ce3
|
7
|
+
data.tar.gz: ef22fa79b8e9e3bb6d0af9e63f48abdcd4c1e6b20628d452cb75d765e56dba90f3e0e999accaa9359df69e5feff942558f123537832666e54386663e8f0dbf7b
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "../util/regex"
|
3
|
+
|
4
|
+
module Jekyll
|
5
|
+
module SemTree
|
6
|
+
|
7
|
+
# note: this is a copy/paste from Jekyll-SemTree;
|
8
|
+
# if that plugin is installed, this is all redundant code.
|
9
|
+
#
|
10
|
+
# this class is responsible for answering any questions
|
11
|
+
# related to jekyll markdown documents
|
12
|
+
# that are meant to be processed by the wikirefs plugin.
|
13
|
+
#
|
14
|
+
# the following methods are specifically to address two things:
|
15
|
+
# 1. ruby's 'find' / 'detect' function does not throw errors if
|
16
|
+
# there are multiple matches. fail fast, i want to know if there
|
17
|
+
# are duplicates.
|
18
|
+
# (not using sets because i don't want to clobber existing documents)
|
19
|
+
# 2. handle all jekyll documents in one place. i don't want to
|
20
|
+
# have to filter all documents for target markdown documents
|
21
|
+
# every time i need to check if a file exists.
|
22
|
+
#
|
23
|
+
# there is probably a better way to do this...i would prefer to have
|
24
|
+
# a plugin-wide function that just wraps all of this and can be called
|
25
|
+
# from anywhere in the plugin...but ruby is not a functional language...
|
26
|
+
# gotta have classes...
|
27
|
+
#
|
28
|
+
class DocManager
|
29
|
+
CONVERTER_CLASS = Jekyll::Converters::Markdown
|
30
|
+
|
31
|
+
def initialize(site)
|
32
|
+
return if $wiki_conf.disabled?
|
33
|
+
|
34
|
+
markdown_converter = site.find_converter_instance(CONVERTER_CLASS)
|
35
|
+
# filter docs based on configs
|
36
|
+
docs = []
|
37
|
+
docs += site.pages if !$wiki_conf.exclude?(:pages)
|
38
|
+
docs += site.docs_to_write.filter { |d| !$wiki_conf.exclude?(d.type) }
|
39
|
+
@md_docs = docs.filter { |doc| markdown_converter.matches(doc.extname) }
|
40
|
+
if @md_docs.nil? || @md_docs.empty?
|
41
|
+
Jekyll.logger.warn("Jekyll-SemTree: No documents to process.")
|
42
|
+
end
|
43
|
+
|
44
|
+
@static_files ||= site.static_files
|
45
|
+
end
|
46
|
+
|
47
|
+
# accessors
|
48
|
+
|
49
|
+
def all
|
50
|
+
return @md_docs
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_doc_by_fname(filename)
|
54
|
+
Jekyll.logger.error("Jekyll-SemTree: Must provide a 'filename'") if filename.nil? || filename.empty?
|
55
|
+
docs = @md_docs.select{ |d| File.basename(d.basename, File.extname(d.basename)) == filename }
|
56
|
+
return nil if docs.nil? || docs.empty? || docs.size > 1
|
57
|
+
return docs[0]
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_doc_by_fpath(file_path)
|
61
|
+
Jekyll.logger.error("Jekyll-SemTree: Must provide a 'file_path'") if file_path.nil? || file_path.empty?
|
62
|
+
docs = @md_docs.select{ |d| d.relative_path == (file_path + ".md") }
|
63
|
+
return nil if docs.nil? || docs.empty? || docs.size > 1
|
64
|
+
return docs[0]
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_doc_by_url(url)
|
68
|
+
Jekyll.logger.error("Jekyll-SemTree: Must provide a 'url'") if url.nil? || url.empty?
|
69
|
+
docs = @md_docs.select{ |d| d.url == url }
|
70
|
+
return nil if docs.nil? || docs.empty? || docs.size > 1
|
71
|
+
return docs[0]
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_doc_content(filename)
|
75
|
+
doc = self.get_doc_by_fname(filename)
|
76
|
+
return nil if docs.nil?
|
77
|
+
return doc.content
|
78
|
+
end
|
79
|
+
|
80
|
+
def get_image_by_fname(filename)
|
81
|
+
Jekyll.logger.error("Jekyll-SemTree: Must provide a 'filename'") if filename.nil? || filename.empty?
|
82
|
+
return nil if @static_files.size == 0 || !SUPPORTED_IMG_FORMATS.any?{ |ext| ext == File.extname(filename).downcase }
|
83
|
+
docs = @static_files.select{ |d| File.basename(d.relative_path) == filename }
|
84
|
+
return nil if docs.nil? || docs.empty? || docs.size > 1
|
85
|
+
return docs[0]
|
86
|
+
end
|
87
|
+
|
88
|
+
# validators
|
89
|
+
|
90
|
+
def file_exists?(filename, file_path=nil)
|
91
|
+
Jekyll.logger.error("Jekyll-SemTree: Must provide a 'filename'") if filename.nil? || filename.empty?
|
92
|
+
if file_path.nil?
|
93
|
+
return false if get_doc_by_fname(filename).nil? && get_image_by_fname(filename).nil?
|
94
|
+
return true
|
95
|
+
else
|
96
|
+
return false if get_doc_by_fpath(file_path).nil?
|
97
|
+
return true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# todo: wiki headers/blocks
|
102
|
+
|
103
|
+
# def doc_has_header?(doc, header)
|
104
|
+
# Jekyll.logger.error("Jekyll-SemTree: Must provide a 'header'") if header.nil? || header.empty?
|
105
|
+
# # leading + trailing whitespace is ignored when matching headers
|
106
|
+
# header_results = doc.content.scan(REGEX_ATX_HEADER).flatten.map { |htxt| htxt.downcase.strip }
|
107
|
+
# setext_header_results = doc.content.scan(REGEX_SETEXT_HEADER).flatten.map { |htxt| htxt.downcase.strip }
|
108
|
+
# return header_results.include?(header.downcase.strip) || setext_header_results.include?(header.downcase.strip)
|
109
|
+
# end
|
110
|
+
|
111
|
+
# def doc_has_block_id?(doc, block_id)
|
112
|
+
# Jekyll.logger.error("Jekyll-SemTree: Must provide a 'block_id'") if block_id.nil? || block_id.empty?
|
113
|
+
# # leading + trailing whitespace is ignored when matching blocks
|
114
|
+
# block_id_results = doc.content.scan(REGEX_BLOCK).flatten.map { |bid| bid.strip }
|
115
|
+
# return block_id_results.include?(block_id)
|
116
|
+
# end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "jekyll"
|
3
|
+
|
4
|
+
# appending to built-in jekyll site object to pass data to jekyll-d3
|
5
|
+
|
6
|
+
module Jekyll
|
7
|
+
|
8
|
+
class Site
|
9
|
+
# 'doc_mngr' only necessary if 'jekyll-wikirefs' not installed
|
10
|
+
attr_accessor :doc_mngr, :tree
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,478 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "jekyll"
|
3
|
+
|
4
|
+
module Jekyll
|
5
|
+
|
6
|
+
class Tree
|
7
|
+
|
8
|
+
OPEN_BRACKETS = '[['
|
9
|
+
CLOSE_BRACKETS = ']]'
|
10
|
+
|
11
|
+
MARKDOWN_BULLET_ASTERISK = '* '
|
12
|
+
MARKDOWN_BULLET_DASH = '- '
|
13
|
+
MARKDOWN_BULLET_PLUS = '+ '
|
14
|
+
|
15
|
+
REGEX = {
|
16
|
+
LEVEL: /^[ \t]*/, # TODO: link
|
17
|
+
TEXT_WITH_LOC: /([^\\:^|\[\]]+)-(\d+)-(\d+)/i,
|
18
|
+
TEXT_WITH_ID: /([^\\:^|\[\]]+)-\(([A-Za-z0-9]{5})\)/i,
|
19
|
+
WIKITEXT_WITH_ID: /([+*-]) \[\[([^\\:\\^|\[\]]+)-\(([A-Za-z0-9]{5})\)\]\]/i,
|
20
|
+
WHITESPACE: /^\s*$/,
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
def is_markdown_bullet(text)
|
24
|
+
return [
|
25
|
+
MARKDOWN_BULLET_ASTERISK,
|
26
|
+
MARKDOWN_BULLET_DASH,
|
27
|
+
MARKDOWN_BULLET_PLUS,
|
28
|
+
].include?(text)
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_accessor :chunk_size # size of indentation for each tree level (set by the first indentation found)
|
32
|
+
attr_accessor :duplicates # duplicate node names in the tree
|
33
|
+
attr_accessor :level_max #
|
34
|
+
attr_accessor :nodes # the tree nodes
|
35
|
+
attr_accessor :petiole_map # a hash where each key is each node in the tree and the value is the index file that contains that node/doc
|
36
|
+
attr_accessor :root # name of the root node/document
|
37
|
+
attr_accessor :trunk # list of index doc fnames
|
38
|
+
attr_accessor :virtual_trunk # whether or not the trunk/index documents should be included in the tree data
|
39
|
+
|
40
|
+
def initialize(content, root_doc, virtual_trunk = false)
|
41
|
+
# init
|
42
|
+
# tree properties
|
43
|
+
@chunk_size = -1
|
44
|
+
@level_max = -1
|
45
|
+
@duplicates = []
|
46
|
+
@mkdn_list = true
|
47
|
+
@virtual_trunk = virtual_trunk
|
48
|
+
# tree nodes
|
49
|
+
@nodes = []
|
50
|
+
@petiole_map = {}
|
51
|
+
@root = ''
|
52
|
+
@trunk = []
|
53
|
+
|
54
|
+
# go
|
55
|
+
root_fname = File.basename(root_doc.basename, File.extname(root_doc.basename))
|
56
|
+
# tree_data.each do |data|
|
57
|
+
# if doc != root_doc
|
58
|
+
# # jekyll pages don't have the slug attribute: https://github.com/jekyll/jekyll/blob/master/lib/jekyll/page.rb#L8
|
59
|
+
# if doc.type == :pages
|
60
|
+
# page_basename = File.basename(doc.name, File.extname(doc.name))
|
61
|
+
# doc.data['slug'] = Jekyll::Utils.slugify(page_basename)
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
|
66
|
+
# prep
|
67
|
+
lines = []
|
68
|
+
# single file
|
69
|
+
if content.is_a?(String)
|
70
|
+
lines = content.split("\n")
|
71
|
+
set_units(lines)
|
72
|
+
return build_tree('root', { 'root' => lines })
|
73
|
+
# multiple files
|
74
|
+
elsif content.is_a?(Hash)
|
75
|
+
unless root_fname
|
76
|
+
puts 'Cannot parse multiple files without a "root" defined'
|
77
|
+
return
|
78
|
+
end
|
79
|
+
unless content.keys.include?(root_fname)
|
80
|
+
raise "content hash does not contain: '#{root_fname}'; keys are: #{content.keys.join(', ')}"
|
81
|
+
end
|
82
|
+
lines = content[root_fname].split("\n")
|
83
|
+
set_units(lines)
|
84
|
+
content_hash = {}
|
85
|
+
content.each do |filename, file_content|
|
86
|
+
content_hash[filename] = file_content.split("\n")
|
87
|
+
end
|
88
|
+
self.clear
|
89
|
+
return build_tree(root_fname, deepcopy(content_hash))
|
90
|
+
else
|
91
|
+
raise "content is not a string or hash: #{content}"
|
92
|
+
end
|
93
|
+
# print_tree(root)
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_tree(cur_key, content, ancestors = [], total_level = 0)
|
97
|
+
@trunk = content.keys
|
98
|
+
# if the trunk isn't virtual, handle index/trunk file
|
99
|
+
unless @virtual_trunk
|
100
|
+
node = TreeNode.new(
|
101
|
+
cur_key,
|
102
|
+
ancestors.map { |n| raw_text(n.text) },
|
103
|
+
total_level,
|
104
|
+
)
|
105
|
+
if total_level == 0
|
106
|
+
add_root(cur_key)
|
107
|
+
else
|
108
|
+
add_branch(cur_key, node.ancestors)
|
109
|
+
end
|
110
|
+
ancestors << node
|
111
|
+
total_level += 1
|
112
|
+
end
|
113
|
+
# handle file...
|
114
|
+
lines = content[cur_key]
|
115
|
+
lines.each_with_index do |line, i|
|
116
|
+
text = line.gsub(REGEX[:LEVEL], '')
|
117
|
+
next if text.nil? || text.empty?
|
118
|
+
if @nodes.map(&:text).include?(raw_text(text))
|
119
|
+
@duplicates << raw_text(text)
|
120
|
+
next
|
121
|
+
end
|
122
|
+
# calculate numbers
|
123
|
+
line_num = i + 1
|
124
|
+
level_match = line.match(REGEX[:LEVEL])
|
125
|
+
# number of spaces
|
126
|
+
next if level_match.nil?
|
127
|
+
size = get_whitespace_size(level_match[0])
|
128
|
+
level = get_level(size) + total_level
|
129
|
+
@chunk_size = 2 if @chunk_size < 0
|
130
|
+
# root
|
131
|
+
if total_level == 0 && i == 0
|
132
|
+
node = TreeNode.new(
|
133
|
+
text,
|
134
|
+
[],
|
135
|
+
level,
|
136
|
+
line_num,
|
137
|
+
)
|
138
|
+
add_root(raw_text(node.text))
|
139
|
+
ancestors << node
|
140
|
+
# node
|
141
|
+
else
|
142
|
+
# connect subtree via 'virtual' semantic-tree node
|
143
|
+
# TODO: if cur_key == raw_text(text), print a warning: don't do that.
|
144
|
+
if cur_key != raw_text(text) && content.keys.include?(raw_text(text))
|
145
|
+
# virtual_levels += @chunk_size # This line is commented out as in the original TypeScript
|
146
|
+
ancestors = calc_ancestry(level, ancestors)
|
147
|
+
build_tree(raw_text(text), content, deepcopy(ancestors), get_level(size))
|
148
|
+
next
|
149
|
+
end
|
150
|
+
node = TreeNode.new(
|
151
|
+
text,
|
152
|
+
[],
|
153
|
+
level,
|
154
|
+
line_num,
|
155
|
+
)
|
156
|
+
node.text = raw_text(node.text)
|
157
|
+
ancestors = calc_ancestry(level, ancestors)
|
158
|
+
node.ancestors = ancestors.map { |p| raw_text(p.text) }
|
159
|
+
ancestors << node
|
160
|
+
add_branch(node.text, node.ancestors, cur_key)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
content.delete(cur_key)
|
164
|
+
if content.any? && total_level == 0
|
165
|
+
return "Some files were not processed: #{content.keys.join(', ')}"
|
166
|
+
end
|
167
|
+
if content.empty?
|
168
|
+
if @duplicates.any?
|
169
|
+
duplicates = @duplicates.uniq
|
170
|
+
error_msg = "Tree did not build, duplicate nodes found:\n\n"
|
171
|
+
error_msg += duplicates.join(', ') + "\n\n"
|
172
|
+
clear
|
173
|
+
return error_msg
|
174
|
+
end
|
175
|
+
return @nodes.dup
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# helper methods
|
180
|
+
|
181
|
+
def add_root(text)
|
182
|
+
@root = text
|
183
|
+
@nodes << TreeNode.new(text)
|
184
|
+
@petiole_map[text] = text
|
185
|
+
end
|
186
|
+
|
187
|
+
def add_branch(text, ancestry_titles, trnk_fname = nil)
|
188
|
+
trnk_fname ||= text
|
189
|
+
ancestry_titles.each_with_index do |ancestry_title, i|
|
190
|
+
if i < (ancestry_titles.length - 1)
|
191
|
+
node = @nodes.find { |n| n.text == ancestry_title }
|
192
|
+
if node && !node.children.include?(ancestry_titles[i + 1])
|
193
|
+
node.children << ancestry_titles[i + 1]
|
194
|
+
end
|
195
|
+
else
|
196
|
+
node = @nodes.find { |n| n.text == ancestry_title }
|
197
|
+
if node && !node.children.include?(text)
|
198
|
+
node.children << text
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
@nodes << TreeNode.new(text, ancestry_titles)
|
203
|
+
@petiole_map[text] = trnk_fname
|
204
|
+
end
|
205
|
+
|
206
|
+
def calc_ancestry(level, ancestors)
|
207
|
+
parent = ancestors.last
|
208
|
+
is_child = (parent.level == (level - 1))
|
209
|
+
is_sibling = (parent.level == level)
|
210
|
+
# child:
|
211
|
+
# - [[parent]]
|
212
|
+
# - [[child]]
|
213
|
+
if is_child
|
214
|
+
# continue...
|
215
|
+
# sibling:
|
216
|
+
# - [[sibling]]
|
217
|
+
# - [[sibling]]
|
218
|
+
elsif is_sibling
|
219
|
+
# we can safely throw away the last node name because
|
220
|
+
# it can't have children if we've already decreased the level
|
221
|
+
ancestors.pop
|
222
|
+
# unrelated (great+) (grand)parent:
|
223
|
+
# - [[descendent]]
|
224
|
+
# - [[great-grandparent]]
|
225
|
+
else # (parent.level < level)
|
226
|
+
level_diff = parent.level - level
|
227
|
+
(1..(level_diff + 1)).each do
|
228
|
+
ancestors.pop
|
229
|
+
end
|
230
|
+
end
|
231
|
+
return ancestors
|
232
|
+
end
|
233
|
+
|
234
|
+
# util methods
|
235
|
+
|
236
|
+
def raw_text(full_text)
|
237
|
+
# strip markdown list marker if it exists
|
238
|
+
if @mkdn_list && is_markdown_bullet(full_text[0..1])
|
239
|
+
full_text = full_text[2..-1]
|
240
|
+
end
|
241
|
+
# strip wikistring special chars and line breaks
|
242
|
+
# using gsub to replace substrings in Ruby
|
243
|
+
full_text.gsub!(OPEN_BRACKETS, '')
|
244
|
+
full_text.gsub!(CLOSE_BRACKETS, '')
|
245
|
+
full_text.gsub!(/\r?\n|\r/, '')
|
246
|
+
return full_text
|
247
|
+
end
|
248
|
+
|
249
|
+
def define_level_size(whitespace)
|
250
|
+
if whitespace[0] == ' '
|
251
|
+
return whitespace.length
|
252
|
+
elsif whitespace[0] == "\t"
|
253
|
+
tab_size = 4
|
254
|
+
return tab_size
|
255
|
+
else
|
256
|
+
# puts "defineLevelSize: unknown whitespace: #{whitespace}"
|
257
|
+
return -1
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def get_whitespace_size(whitespace)
|
262
|
+
if whitespace.include?(' ')
|
263
|
+
return whitespace.length
|
264
|
+
elsif whitespace.include?("\t")
|
265
|
+
tab_size = 4
|
266
|
+
return whitespace.length * tab_size
|
267
|
+
else
|
268
|
+
# puts "getWhitespaceSize: unknown whitespace: #{whitespace}"
|
269
|
+
return whitespace.length
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def get_level(size)
|
274
|
+
(size / @chunk_size) + 1
|
275
|
+
end
|
276
|
+
|
277
|
+
def clear
|
278
|
+
@root = ''
|
279
|
+
@nodes = []
|
280
|
+
@petiole_map = {}
|
281
|
+
@duplicates = []
|
282
|
+
end
|
283
|
+
|
284
|
+
def deepcopy(obj)
|
285
|
+
Marshal.load(Marshal.dump(obj))
|
286
|
+
end
|
287
|
+
|
288
|
+
def set_units(lines)
|
289
|
+
# calculate number of spaces per level and size of deepest level
|
290
|
+
lines.each do |line|
|
291
|
+
level_match = line.match(REGEX[:LEVEL])
|
292
|
+
# calculates number of spaces
|
293
|
+
if level_match
|
294
|
+
if @chunk_size < 0
|
295
|
+
@chunk_size = define_level_size(level_match[0])
|
296
|
+
end
|
297
|
+
level = get_level(level_match[0].length)
|
298
|
+
else
|
299
|
+
next
|
300
|
+
end
|
301
|
+
@level_max = level > @level_max ? level : @level_max
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# metadata methods
|
306
|
+
|
307
|
+
def get_all_lineage_ids(target_node_id, node=@nodes.detect { |n| n.text == @root }, ancestors=[], descendents=[], found=false)
|
308
|
+
# found target node, stop adding ancestors and build descendents
|
309
|
+
if target_node_id == node.id || target_node_id == node.text || found
|
310
|
+
node.children.each do |child|
|
311
|
+
child_node = @nodes.detect { |n| n.text == child }
|
312
|
+
# if the child document is an empty string, it is a missing node
|
313
|
+
if child_node.missing
|
314
|
+
descendents << child_node.text
|
315
|
+
else
|
316
|
+
descendents << child_node.id
|
317
|
+
end
|
318
|
+
self.get_all_lineage_ids(target_node_id, child_node, ancestors.clone, descendents, found=true)
|
319
|
+
end
|
320
|
+
return ancestors, descendents
|
321
|
+
# target node not yet found, build ancestors
|
322
|
+
else
|
323
|
+
# if the node document is an empty string, it is a missing node
|
324
|
+
if node.missing
|
325
|
+
ancestors << node.text
|
326
|
+
else
|
327
|
+
ancestors << node.id
|
328
|
+
end
|
329
|
+
results = []
|
330
|
+
node.children.each do |child|
|
331
|
+
child_node = @nodes.detect { |n| n.text == child }
|
332
|
+
results.concat(self.get_all_lineage_ids(target_node_id, child_node, ancestors.clone))
|
333
|
+
end
|
334
|
+
return results.select { |r| !r.nil? }
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def get_sibling_ids(target_node_id, node=@nodes.detect { |n| n.text == @root }, parent=nil)
|
339
|
+
return [] if node.text === @root
|
340
|
+
# found target node
|
341
|
+
if target_node_id == node.id || target_node_id == node.text
|
342
|
+
return parent.children.select { |c| c.id }
|
343
|
+
# target node not yet found
|
344
|
+
else
|
345
|
+
node.children.each do |child|
|
346
|
+
child_node = @nodes.detect { |n| n.text == child }
|
347
|
+
self.get_sibling_ids(target_node_id, child_node, node)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# find the parent and children of the 'target_doc'.
|
353
|
+
def find_doc_ancestors_and_children_metadata(target_doc)
|
354
|
+
fname = File.basename(target_doc.basename, File.extname(target_doc.basename))
|
355
|
+
node = @nodes.detect { |n| n.text == fname }
|
356
|
+
return node.ancestors, node.children
|
357
|
+
end
|
358
|
+
|
359
|
+
def in_tree?(fname)
|
360
|
+
return @nodes.map(&:text).include?(fname)
|
361
|
+
end
|
362
|
+
|
363
|
+
# ...for debugging
|
364
|
+
|
365
|
+
def to_s
|
366
|
+
puts build_tree_str
|
367
|
+
end
|
368
|
+
|
369
|
+
def print_nodes
|
370
|
+
puts "# Tree Nodes: "
|
371
|
+
@nodes.each do |node|
|
372
|
+
puts "# #{node.to_s}"
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def build_tree_str(cur_node_name = @root, prefix = '')
|
377
|
+
output = "#{cur_node_name}\n"
|
378
|
+
node = @nodes.find { |n| n.text == cur_node_name }
|
379
|
+
if node.nil?
|
380
|
+
puts `SemTree.build_tree_str: error: nil node for name '#{cur_node_name}'`
|
381
|
+
return output
|
382
|
+
end
|
383
|
+
node.children.each_with_index do |child, index|
|
384
|
+
is_last_child = index == node.children.length - 1
|
385
|
+
child_prefix = prefix + (is_last_child ? '└── ' : '├── ')
|
386
|
+
grandchild_prefix = prefix + (is_last_child ? ' ' : '| ')
|
387
|
+
subtree = build_tree_str(child, grandchild_prefix)
|
388
|
+
output += "#{child_prefix}#{subtree}"
|
389
|
+
end
|
390
|
+
return output
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# class TreeNode
|
395
|
+
# attr_accessor :ancestors # array of strings for the text of each node from root to current (leaf)
|
396
|
+
# attr_accessor :children # array of strings for the text of each child node
|
397
|
+
# attr_accessor :doc # the associated jekyll document
|
398
|
+
# attr_accessor :text # text of the node -- used for primary identification in tree and must be uniquely named
|
399
|
+
# attr_accessor :url # jekyll blog url for the given node/page
|
400
|
+
|
401
|
+
# def initialize(text, ancestors=[], children=[], url="", doc="")
|
402
|
+
# @text = text
|
403
|
+
# @ancestors = ancestors
|
404
|
+
# # optional
|
405
|
+
# @children = children
|
406
|
+
# @url = url.nil? ? "" : url
|
407
|
+
# @doc = doc
|
408
|
+
# end
|
409
|
+
|
410
|
+
# def missing
|
411
|
+
# return @doc == ""
|
412
|
+
# end
|
413
|
+
|
414
|
+
# def type
|
415
|
+
# return @doc.type
|
416
|
+
# end
|
417
|
+
|
418
|
+
# def to_s
|
419
|
+
# "<Node text: '#{@text}', ancestors: #{@ancestors}, children: #{@children}"
|
420
|
+
# end
|
421
|
+
# end
|
422
|
+
|
423
|
+
class TreeNode
|
424
|
+
attr_accessor :id # node id -- should be unique
|
425
|
+
attr_accessor :ancestors # array of strings for the text of each node from root to current (leaf)
|
426
|
+
attr_accessor :children # array of strings for the text of each child node
|
427
|
+
attr_accessor :doc # the associated jekyll document
|
428
|
+
attr_accessor :level # level in the tree
|
429
|
+
attr_accessor :line # line of the index file content
|
430
|
+
# does not include stripped content (e.g. yaml);
|
431
|
+
# value for index docs is -1
|
432
|
+
attr_accessor :text # text of the node -- used for primary identification in tree and must be uniquely named
|
433
|
+
attr_accessor :url # jekyll blog url for the given node/page
|
434
|
+
|
435
|
+
def initialize(text, ancestors=[], level=-1, line=-1, children=[], doc="")
|
436
|
+
# mandatory
|
437
|
+
@text = text
|
438
|
+
@ancestors = ancestors
|
439
|
+
# optional
|
440
|
+
@children = children
|
441
|
+
@level = level
|
442
|
+
@line = line
|
443
|
+
@doc = doc
|
444
|
+
end
|
445
|
+
|
446
|
+
def missing
|
447
|
+
return @doc == ''
|
448
|
+
# return (@doc.nil? || (@doc.class == String))
|
449
|
+
end
|
450
|
+
|
451
|
+
def to_s
|
452
|
+
"<Node text: '#{@text}', ancestors: #{@ancestors}, children: #{@children}"
|
453
|
+
end
|
454
|
+
|
455
|
+
# doc properties
|
456
|
+
|
457
|
+
def id
|
458
|
+
return (self.missing) ? @text : @doc.url
|
459
|
+
end
|
460
|
+
|
461
|
+
def url
|
462
|
+
return (self.missing) ? @text : @doc.url
|
463
|
+
end
|
464
|
+
|
465
|
+
def title
|
466
|
+
return (self.missing) ? @text : @doc.data['title']
|
467
|
+
end
|
468
|
+
|
469
|
+
def type
|
470
|
+
return (self.missing) ? 'zombie' : @doc.type
|
471
|
+
end
|
472
|
+
|
473
|
+
# for legacy 'jekyll-namespaces' calls
|
474
|
+
def namespace
|
475
|
+
return false
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
data/lib/jekyll-semtree.rb
CHANGED
@@ -1,10 +1,166 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "jekyll"
|
2
3
|
|
4
|
+
require_relative "jekyll-semtree/patch/context"
|
5
|
+
require_relative "jekyll-semtree/patch/site"
|
6
|
+
require_relative "jekyll-semtree/tree"
|
3
7
|
require_relative "jekyll-semtree/version"
|
4
8
|
|
9
|
+
|
5
10
|
module Jekyll
|
6
11
|
module SemTree
|
7
|
-
|
8
|
-
|
12
|
+
|
13
|
+
class Generator < Jekyll::Generator
|
14
|
+
# for testing
|
15
|
+
attr_reader :config
|
16
|
+
|
17
|
+
CONVERTER_CLASS = Jekyll::Converters::Markdown
|
18
|
+
# config keys
|
19
|
+
CONFIG_KEY = "semtree"
|
20
|
+
ENABLED_KEY = "enabled"
|
21
|
+
EXCLUDE_KEY = "exclude"
|
22
|
+
DOCTYPE_KEY = "doctype"
|
23
|
+
PAGE_KEY = "map"
|
24
|
+
ROOT_KEY = "root"
|
25
|
+
VIRTUAL_TRUNK_KEY = "virtual_trunk"
|
26
|
+
|
27
|
+
def initialize(config)
|
28
|
+
@config ||= config
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate(site)
|
32
|
+
return if disabled?
|
33
|
+
|
34
|
+
# setup site
|
35
|
+
@site = site
|
36
|
+
@context ||= Context.new(site)
|
37
|
+
|
38
|
+
# setup docs (based on configs)
|
39
|
+
# unless @site.keys.include('doc_mngr') # may have been installed by jekyll-wikirefs or jekyll-wikilinks already
|
40
|
+
# require_relative "jekyll-semtree/patch/doc_manager"
|
41
|
+
# Jekyll::Hooks.register :site, :post_read do |site|
|
42
|
+
# if !self.disabled?
|
43
|
+
# site.doc_mngr = Jekyll::SemTree::DocManager.new(site)
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
|
48
|
+
# setup markdown docs
|
49
|
+
docs = []
|
50
|
+
docs += site.pages # if !$wiki_conf.exclude?(:pages)
|
51
|
+
docs += site.docs_to_write.filter { |d| !self.excluded?(d.type) }
|
52
|
+
@md_docs = docs.filter { |doc| self.markdown_extension?(doc.extname) }
|
53
|
+
if @md_docs.empty?
|
54
|
+
Jekyll.logger.warn("Jekyll-SemTree: No semtree files to process.")
|
55
|
+
end
|
56
|
+
|
57
|
+
# tree setup
|
58
|
+
# root
|
59
|
+
root_doc = @md_docs.detect { |d| d.data['slug'] == self.option_root_name }
|
60
|
+
if root_doc.nil?
|
61
|
+
Jekyll.logger.error("Jekyll-SemTree: No root doc detected.")
|
62
|
+
end
|
63
|
+
# trunk / index docs
|
64
|
+
index_docs = @site.docs_to_write.filter { |d| d.type.to_s == self.option_doctype_name }
|
65
|
+
if index_docs.empty?
|
66
|
+
Jekyll.logger.error("Jekyll-SemTree: No trunk (index docs) detected.")
|
67
|
+
end
|
68
|
+
# tree content hash
|
69
|
+
tree_hash = build_tree_hash(@site.collections[self.option_doctype_name])
|
70
|
+
if tree_hash.empty?
|
71
|
+
Jekyll.logger.error("Jekyll-SemTree: No trunk (index docs) detected.")
|
72
|
+
end
|
73
|
+
# build tree
|
74
|
+
@site.tree = Tree.new(tree_hash, root_doc, option_virtual_trunk)
|
75
|
+
|
76
|
+
# generate metadata
|
77
|
+
@site.tree.nodes.each do |n|
|
78
|
+
doc = @md_docs.detect { |d| n.text == File.basename(d.basename, File.extname(d.basename)) }
|
79
|
+
if !doc.nil?
|
80
|
+
n.doc = doc
|
81
|
+
ancestorNames, childrenNames = @site.tree.find_doc_ancestors_and_children_metadata(doc)
|
82
|
+
doc.data['ancestors'] = fnames_to_urls(ancestorNames)
|
83
|
+
doc.data['children'] = fnames_to_urls(childrenNames)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
map_doc = @md_docs.detect { |d| option_page == File.basename(d.basename, File.extname(d.basename)) }
|
87
|
+
# root_node = @site.tree.nodes.detect { |n| n.text == @site.tree.root }
|
88
|
+
map_doc.data['nodes'] = @site.tree.nodes.map { |n| {
|
89
|
+
'text' => n.text,
|
90
|
+
'url' => n.url,
|
91
|
+
'ancestors' => n.ancestors,
|
92
|
+
'children' => n.children,
|
93
|
+
}
|
94
|
+
}
|
95
|
+
dangling_entries = []
|
96
|
+
@site.collections['entries'].each do |d|
|
97
|
+
fname = File.basename(d.basename, File.extname(d.basename))
|
98
|
+
if !@site.tree.in_tree?(fname)
|
99
|
+
dangling_entries << fname
|
100
|
+
end
|
101
|
+
end
|
102
|
+
Jekyll.logger.warn("Jekyll-SemTree: entries not listed in the tree: #{dangling_entries}")
|
103
|
+
end
|
104
|
+
|
105
|
+
def fnames_to_urls(fnames)
|
106
|
+
docs = []
|
107
|
+
fnames.each do |fname|
|
108
|
+
doc = @md_docs.detect { |d| fname == File.basename(d.basename, File.extname(d.basename)) }
|
109
|
+
docs << (doc.nil? ? fname : doc.url)
|
110
|
+
end
|
111
|
+
return docs
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_tree_hash(collection_doc)
|
115
|
+
tree_data = {}
|
116
|
+
collection_doc.each do |d|
|
117
|
+
if d.type.to_s == self.option_doctype_name
|
118
|
+
fname = File.basename(d.basename, File.extname(d.basename))
|
119
|
+
tree_data[fname] = d.content
|
120
|
+
end
|
121
|
+
end
|
122
|
+
return tree_data
|
123
|
+
end
|
124
|
+
|
125
|
+
# config helpers
|
126
|
+
|
127
|
+
def disabled?
|
128
|
+
option(ENABLED_KEY) == false
|
129
|
+
end
|
130
|
+
|
131
|
+
def excluded?(type)
|
132
|
+
return false unless option(EXCLUDE_KEY)
|
133
|
+
return option(EXCLUDE_KEY).include?(type.to_s)
|
134
|
+
end
|
135
|
+
|
136
|
+
def markdown_extension?(extension)
|
137
|
+
markdown_converter.matches(extension)
|
138
|
+
end
|
139
|
+
|
140
|
+
def markdown_converter
|
141
|
+
@markdown_converter ||= @site.find_converter_instance(CONVERTER_CLASS)
|
142
|
+
end
|
143
|
+
|
144
|
+
def option(key)
|
145
|
+
@config[CONFIG_KEY] && @config[CONFIG_KEY][key]
|
146
|
+
end
|
147
|
+
|
148
|
+
def option_root_name
|
149
|
+
return option(ROOT_KEY) ? @config[CONFIG_KEY][ROOT_KEY] : 'i.bonsai'
|
150
|
+
end
|
151
|
+
|
152
|
+
def option_doctype_name
|
153
|
+
return option(DOCTYPE_KEY) ? @config[CONFIG_KEY][DOCTYPE_KEY] : 'index'
|
154
|
+
end
|
155
|
+
|
156
|
+
def option_virtual_trunk
|
157
|
+
return option(VIRTUAL_TRUNK_KEY) ? @config[CONFIG_KEY][VIRTUAL_TRUNK_KEY] : false
|
158
|
+
end
|
159
|
+
|
160
|
+
def option_page
|
161
|
+
return option(PAGE_KEY) ? @config[CONFIG_KEY][PAGE_KEY] : 'map'
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
9
165
|
end
|
10
166
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jekyll-semtree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- manunamz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-09-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jekyll
|
@@ -32,14 +32,18 @@ extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
34
|
- lib/jekyll-semtree.rb
|
35
|
+
- lib/jekyll-semtree/patch/context.rb
|
36
|
+
- lib/jekyll-semtree/patch/doc_manager.rb
|
37
|
+
- lib/jekyll-semtree/patch/site.rb
|
38
|
+
- lib/jekyll-semtree/tree.rb
|
35
39
|
- lib/jekyll-semtree/version.rb
|
36
|
-
homepage: https://github.com/
|
40
|
+
homepage: https://github.com/wikibonsai/jekyll-semtree
|
37
41
|
licenses:
|
38
42
|
- MIT
|
39
43
|
metadata:
|
40
|
-
homepage_uri: https://github.com/
|
41
|
-
source_code_uri: https://github.com/
|
42
|
-
changelog_uri: https://github.com/
|
44
|
+
homepage_uri: https://github.com/wikibonsai/jekyll-semtree
|
45
|
+
source_code_uri: https://github.com/wikibonsai/jekyll-semtree
|
46
|
+
changelog_uri: https://github.com/wikibonsai/jekyll-semtree/blob/main/CHANGELOG.md
|
43
47
|
post_install_message:
|
44
48
|
rdoc_options: []
|
45
49
|
require_paths:
|
@@ -55,8 +59,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
59
|
- !ruby/object:Gem::Version
|
56
60
|
version: '0'
|
57
61
|
requirements: []
|
58
|
-
rubygems_version: 3.4.
|
62
|
+
rubygems_version: 3.4.10
|
59
63
|
signing_key:
|
60
64
|
specification_version: 4
|
61
|
-
summary:
|
65
|
+
summary: Add jekyll support for a semantic tree (in markdown).
|
62
66
|
test_files: []
|