jekyll-semtree 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 +162 -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: 45680543ef4121289d217b25451ff65a73c14399018f98275b9d8c08dcf91b22
|
4
|
+
data.tar.gz: 1d99a3e6062a15c75414770ac22e4ca88ff9b9c566b57ca2e2871f1358200495
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e34f0d3b31cc635c6163bca4a7fdfbde0e23c6e98a458cd8310bc4a8d78a07877e26bf6206806d8d63a2a2530fb945350b746e4d0a88e9101f7e4c52dd8c3588
|
7
|
+
data.tar.gz: 646df65358f1ea451c8e597bc74a956e943e32d3bbf5601d229ecabe58d7e4b4d423f47f61477f8729bd3d6baede280fedee93c9144dca5fd54f0b4561edf372
|
@@ -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,170 @@
|
|
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
|
+
if root_doc.nil? || index_docs.empty? || tree_hash.empty?
|
74
|
+
Jekyll.logger.error("Jekyll-SemTree: Unable to build semantic tree.")
|
75
|
+
return
|
76
|
+
end
|
77
|
+
# build tree
|
78
|
+
@site.tree = Tree.new(tree_hash, root_doc, option_virtual_trunk)
|
79
|
+
|
80
|
+
# generate metadata
|
81
|
+
@site.tree.nodes.each do |n|
|
82
|
+
doc = @md_docs.detect { |d| n.text == File.basename(d.basename, File.extname(d.basename)) }
|
83
|
+
if !doc.nil?
|
84
|
+
n.doc = doc
|
85
|
+
ancestorNames, childrenNames = @site.tree.find_doc_ancestors_and_children_metadata(doc)
|
86
|
+
doc.data['ancestors'] = fnames_to_urls(ancestorNames)
|
87
|
+
doc.data['children'] = fnames_to_urls(childrenNames)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
map_doc = @md_docs.detect { |d| option_page == File.basename(d.basename, File.extname(d.basename)) }
|
91
|
+
# root_node = @site.tree.nodes.detect { |n| n.text == @site.tree.root }
|
92
|
+
map_doc.data['nodes'] = @site.tree.nodes.map { |n| {
|
93
|
+
'text' => n.text,
|
94
|
+
'url' => n.url,
|
95
|
+
'ancestors' => n.ancestors,
|
96
|
+
'children' => n.children,
|
97
|
+
}
|
98
|
+
}
|
99
|
+
dangling_entries = []
|
100
|
+
@site.collections['entries'].each do |d|
|
101
|
+
fname = File.basename(d.basename, File.extname(d.basename))
|
102
|
+
if !@site.tree.in_tree?(fname)
|
103
|
+
dangling_entries << fname
|
104
|
+
end
|
105
|
+
end
|
106
|
+
Jekyll.logger.warn("Jekyll-SemTree: entries not listed in the tree: #{dangling_entries}")
|
107
|
+
end
|
108
|
+
|
109
|
+
def fnames_to_urls(fnames)
|
110
|
+
docs = []
|
111
|
+
fnames.each do |fname|
|
112
|
+
doc = @md_docs.detect { |d| fname == File.basename(d.basename, File.extname(d.basename)) }
|
113
|
+
docs << (doc.nil? ? fname : doc.url)
|
114
|
+
end
|
115
|
+
return docs
|
116
|
+
end
|
117
|
+
|
118
|
+
def build_tree_hash(collection_doc)
|
119
|
+
tree_data = {}
|
120
|
+
collection_doc.each do |d|
|
121
|
+
if d.type.to_s == self.option_doctype_name
|
122
|
+
fname = File.basename(d.basename, File.extname(d.basename))
|
123
|
+
tree_data[fname] = d.content
|
124
|
+
end
|
125
|
+
end
|
126
|
+
return tree_data
|
127
|
+
end
|
128
|
+
|
129
|
+
# config helpers
|
130
|
+
|
131
|
+
def disabled?
|
132
|
+
option(ENABLED_KEY) == false
|
133
|
+
end
|
134
|
+
|
135
|
+
def excluded?(type)
|
136
|
+
return false unless option(EXCLUDE_KEY)
|
137
|
+
return option(EXCLUDE_KEY).include?(type.to_s)
|
138
|
+
end
|
139
|
+
|
140
|
+
def markdown_extension?(extension)
|
141
|
+
markdown_converter.matches(extension)
|
142
|
+
end
|
143
|
+
|
144
|
+
def markdown_converter
|
145
|
+
@markdown_converter ||= @site.find_converter_instance(CONVERTER_CLASS)
|
146
|
+
end
|
147
|
+
|
148
|
+
def option(key)
|
149
|
+
@config[CONFIG_KEY] && @config[CONFIG_KEY][key]
|
150
|
+
end
|
151
|
+
|
152
|
+
def option_root_name
|
153
|
+
return option(ROOT_KEY) ? @config[CONFIG_KEY][ROOT_KEY] : 'i.bonsai'
|
154
|
+
end
|
155
|
+
|
156
|
+
def option_doctype_name
|
157
|
+
return option(DOCTYPE_KEY) ? @config[CONFIG_KEY][DOCTYPE_KEY] : 'index'
|
158
|
+
end
|
159
|
+
|
160
|
+
def option_virtual_trunk
|
161
|
+
return option(VIRTUAL_TRUNK_KEY) ? @config[CONFIG_KEY][VIRTUAL_TRUNK_KEY] : false
|
162
|
+
end
|
163
|
+
|
164
|
+
def option_page
|
165
|
+
return option(PAGE_KEY) ? @config[CONFIG_KEY][PAGE_KEY] : 'map'
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
9
169
|
end
|
10
170
|
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.3
|
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: []
|