jekyll-wikilinks 0.0.3 → 0.0.4
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-wikilinks.rb +105 -184
- data/lib/jekyll-wikilinks/doc_manager.rb +68 -0
- data/lib/jekyll-wikilinks/filter.rb +22 -11
- data/lib/jekyll-wikilinks/jekyll_patch.rb +24 -0
- data/lib/jekyll-wikilinks/link_index.rb +81 -0
- data/lib/jekyll-wikilinks/naming_const.rb +21 -0
- data/lib/jekyll-wikilinks/parser.rb +236 -0
- data/lib/jekyll-wikilinks/version.rb +1 -1
- metadata +13 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f7349631b7b0ce9484512d06a368ac2808d3d3c64bdf7ccd66e845dc5f41380
|
4
|
+
data.tar.gz: 87c6d80c73e79c260d3b58acb8cc2e3c62dc0c29c2574a9c54bd5f476e70e8cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 05a261827c04023b8072f85a566c88abad259885e6af6fd85909d61f93200849994de360076023b383f2abab7f3ad4d92e4e755f0ea9cd487e1c59692786d3bf
|
7
|
+
data.tar.gz: 3b3862a5e00356d5227e39a346405e238255fe4cc4275c1f095a2ca828e62d7e1f3468035c99278c95dcbe2a340521588dd2ebc64571a9bec963affc19e9328e
|
data/lib/jekyll-wikilinks.rb
CHANGED
@@ -1,145 +1,133 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "jekyll"
|
3
|
+
|
3
4
|
require_relative "jekyll-wikilinks/context"
|
5
|
+
require_relative "jekyll-wikilinks/doc_manager"
|
6
|
+
require_relative "jekyll-wikilinks/link_index"
|
7
|
+
require_relative "jekyll-wikilinks/parser"
|
8
|
+
|
4
9
|
require_relative "jekyll-wikilinks/filter"
|
5
10
|
require_relative "jekyll-wikilinks/version"
|
6
11
|
|
12
|
+
Liquid::Template.register_filter(JekyllWikiLinks::TypeFilters)
|
7
13
|
|
8
14
|
module JekyllWikiLinks
|
9
|
-
|
10
|
-
attr_accessor :site, :config, :md_docs, :graph_nodes, :graph_links
|
15
|
+
class Generator < Jekyll::Generator
|
16
|
+
attr_accessor :site, :config, :md_docs, :doc_manager, :link_index, :parser, :graph_nodes, :graph_links
|
11
17
|
|
12
18
|
# Use Jekyll's native relative_url filter
|
13
19
|
include Jekyll::Filters::URLFilters
|
14
20
|
|
15
|
-
CONFIG_KEY = "wikilinks"
|
16
21
|
CONVERTER_CLASS = Jekyll::Converters::Markdown
|
22
|
+
# config
|
23
|
+
CONFIG_KEY = "wikilinks"
|
17
24
|
ENABLED_KEY = "enabled"
|
18
|
-
ENABLED_GRAPH_DATA_KEY = "enabled"
|
19
25
|
EXCLUDE_KEY = "exclude"
|
20
|
-
|
26
|
+
# graph config
|
21
27
|
GRAPH_DATA_KEY = "d3_graph_data"
|
28
|
+
ENABLED_GRAPH_DATA_KEY = "enabled"
|
29
|
+
EXCLUDE_GRAPH_KEY = "exclude"
|
30
|
+
GRAPH_ASSETS_LOCATION_KEY = "assets_rel_path"
|
31
|
+
|
32
|
+
# identify missing links in doc via .invalid-wiki-link class and nested doc-text.
|
33
|
+
REGEX_INVALID_WIKI_LINK = /invalid-wiki-link#{REGEX_NOT_GREEDY}\[\[(#{REGEX_NOT_GREEDY})\]\]/i
|
22
34
|
|
23
35
|
def initialize(config)
|
24
|
-
@config
|
36
|
+
@config ||= config
|
37
|
+
@testing ||= config['testing'] if config.keys.include?('testing')
|
25
38
|
end
|
26
39
|
|
27
40
|
def generate(site)
|
28
41
|
return if disabled?
|
42
|
+
self.old_config_warn()
|
29
43
|
Jekyll.logger.debug "Excluded jekyll types: ", option(EXCLUDE_KEY)
|
30
44
|
Jekyll.logger.debug "Excluded jekyll types in graph: ", option_graph(EXCLUDE_GRAPH_KEY)
|
31
|
-
|
45
|
+
|
46
|
+
# setup site
|
32
47
|
@site = site
|
33
|
-
@context
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
@md_docs =
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
48
|
+
@context ||= JekyllWikiLinks::Context.new(site)
|
49
|
+
|
50
|
+
# setup markdown docs
|
51
|
+
docs = []
|
52
|
+
docs += site.pages if !exclude?(:pages)
|
53
|
+
docs += site.docs_to_write.filter { |d| !exclude?(d.type) }
|
54
|
+
@md_docs = docs.filter {|doc| markdown_extension?(doc.extname) }
|
55
|
+
|
56
|
+
# setup helper classes
|
57
|
+
@doc_manager = DocManager.new(@md_docs, @site.static_files)
|
58
|
+
@parser = Parser.new(@context, @markdown_converter, @doc_manager)
|
59
|
+
@link_index = LinkIndex.new(@site, @doc_manager)
|
60
|
+
|
61
|
+
# parse + populate index
|
62
|
+
@md_docs.each do |doc|
|
63
|
+
@parser.parse(doc.content)
|
64
|
+
@link_index.populate_attributes(doc, @parser.typed_link_blocks)
|
65
|
+
end
|
66
|
+
@link_index.process
|
67
|
+
|
68
|
+
# handle graph data
|
49
69
|
@graph_nodes, @graph_links = [], []
|
50
|
-
md_docs.each do |
|
51
|
-
|
52
|
-
|
53
|
-
generate_graph_data(document)
|
70
|
+
@md_docs.each do |doc|
|
71
|
+
if !disabled_graph_data? && !self.excluded_in_graph?(doc.type)
|
72
|
+
self.generate_graph_data(doc)
|
54
73
|
end
|
55
74
|
end
|
56
|
-
|
57
75
|
if !disabled_graph_data?
|
58
|
-
write_graph_data()
|
76
|
+
self.write_graph_data()
|
59
77
|
end
|
60
78
|
end
|
61
79
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
80
|
+
# config helpers
|
81
|
+
|
82
|
+
def disabled?
|
83
|
+
option(ENABLED_KEY) == false
|
84
|
+
end
|
85
|
+
|
86
|
+
def exclude?(type)
|
87
|
+
return false unless option(EXCLUDE_KEY)
|
88
|
+
return option(EXCLUDE_KEY).include?(type.to_s)
|
66
89
|
end
|
67
90
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
md_docs.each do |note_potentially_linked_to|
|
72
|
-
title_from_filename = File.basename(
|
73
|
-
note_potentially_linked_to.basename,
|
74
|
-
File.extname(note_potentially_linked_to.basename)
|
75
|
-
)
|
76
|
-
|
77
|
-
note_url = relative_url(note_potentially_linked_to.url) if note_potentially_linked_to&.url
|
91
|
+
def has_custom_assets_path?
|
92
|
+
return !!option_graph(GRAPH_ASSETS_LOCATION_KEY)
|
93
|
+
end
|
78
94
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
render_txt = note_potentially_linked_to.data['title'].downcase
|
83
|
-
note.content = note.content.gsub(
|
84
|
-
regex_wl,
|
85
|
-
"<a class='wiki-link' href='#{note_url}'>#{render_txt}</a>"
|
86
|
-
)
|
95
|
+
def markdown_extension?(extension)
|
96
|
+
markdown_converter.matches(extension)
|
97
|
+
end
|
87
98
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
note.content = note.content.gsub(
|
92
|
-
regex_wl,
|
93
|
-
"<a class='wiki-link' href='#{note_url}'>#{cap_gr}</a>"
|
94
|
-
)
|
99
|
+
def markdown_converter
|
100
|
+
@markdown_converter ||= site.find_converter_instance(CONVERTER_CLASS)
|
101
|
+
end
|
95
102
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
"<a class='wiki-link' href='#{note_url}'>#{cap_gr}</a>"
|
102
|
-
)
|
103
|
-
end
|
103
|
+
def option(key)
|
104
|
+
config[CONFIG_KEY] && config[CONFIG_KEY][key]
|
105
|
+
end
|
106
|
+
|
107
|
+
# graph config helpers
|
104
108
|
|
105
|
-
|
106
|
-
|
107
|
-
# links by greying them out and changing the cursor
|
108
|
-
# vanilla wiki-links
|
109
|
-
regex_wl, cap_gr = regex_wiki_link()
|
110
|
-
note.content = note.content.gsub(
|
111
|
-
regex_wl,
|
112
|
-
"<span title='There is no note that matches this link.' class='invalid-wiki-link'>[[#{cap_gr}]]</span>"
|
113
|
-
)
|
114
|
-
# aliases -- both kinds
|
115
|
-
regex_wl, cap_gr = regex_wiki_link_w_alias()
|
116
|
-
note.content = note.content.gsub(
|
117
|
-
regex_wl,
|
118
|
-
"<span title='There is no note that matches this link.' class='invalid-wiki-link'>[[#{cap_gr}]]</span>"
|
119
|
-
)
|
109
|
+
def disabled_graph_data?
|
110
|
+
option_graph(ENABLED_GRAPH_DATA_KEY) == false
|
120
111
|
end
|
121
112
|
|
122
|
-
def
|
123
|
-
|
124
|
-
|
125
|
-
if backlinked_doc.content.include?(doc.url)
|
126
|
-
backlinks << backlinked_doc
|
127
|
-
end
|
128
|
-
end
|
129
|
-
return backlinks
|
113
|
+
def excluded_in_graph?(type)
|
114
|
+
return false unless option_graph(EXCLUDE_KEY)
|
115
|
+
return option_graph(EXCLUDE_KEY).include?(type.to_s)
|
130
116
|
end
|
131
117
|
|
132
|
-
def
|
133
|
-
|
134
|
-
return doc.content.scan(regex)[0]
|
118
|
+
def option_graph(key)
|
119
|
+
config[GRAPH_DATA_KEY] && config[GRAPH_DATA_KEY][key]
|
135
120
|
end
|
136
121
|
|
122
|
+
# graph helpers
|
123
|
+
|
137
124
|
def generate_graph_data(doc)
|
138
125
|
Jekyll.logger.debug "Processing graph nodes for doc: ", doc.data['title']
|
139
126
|
# missing nodes
|
140
|
-
missing_node_names =
|
127
|
+
missing_node_names = doc.content.scan(REGEX_INVALID_WIKI_LINK)
|
141
128
|
if !missing_node_names.nil?
|
142
|
-
missing_node_names.each do |
|
129
|
+
missing_node_names.each do |missing_node_name_captures|
|
130
|
+
missing_node_name = missing_node_name_captures[0]
|
143
131
|
if graph_nodes.none? { |node| node[:id] == missing_node_name }
|
144
132
|
Jekyll.logger.warn "Net-Web node missing: ", missing_node_name
|
145
133
|
Jekyll.logger.warn " in: ", doc.data['slug']
|
@@ -161,10 +149,13 @@ module JekyllWikiLinks
|
|
161
149
|
url: relative_url(doc.url),
|
162
150
|
label: doc.data['title'],
|
163
151
|
}
|
164
|
-
|
165
|
-
|
152
|
+
# TODO: this link calculation ends up with duplicates -- re-visit this later.
|
153
|
+
all_links = doc.data['attributes'] + doc.data['backlinks']
|
154
|
+
all_links.each do |link|
|
155
|
+
linked_doc = link['doc']
|
156
|
+
if !excluded_in_graph?(linked_doc.type)
|
166
157
|
graph_links << {
|
167
|
-
source: relative_url(
|
158
|
+
source: relative_url(linked_doc.url),
|
168
159
|
target: relative_url(doc.url),
|
169
160
|
}
|
170
161
|
end
|
@@ -172,100 +163,30 @@ module JekyllWikiLinks
|
|
172
163
|
end
|
173
164
|
|
174
165
|
def write_graph_data()
|
166
|
+
assets_path = has_custom_assets_path? ? option_graph(GRAPH_ASSETS_LOCATION_KEY) : "/assets"
|
167
|
+
if !File.directory?(File.join(site.source, assets_path))
|
168
|
+
Jekyll.logger.error "Assets location does not exist, please create required directories for path: ", assets_path
|
169
|
+
end
|
175
170
|
# from: https://github.com/jekyll/jekyll/issues/7195#issuecomment-415696200
|
176
|
-
static_file = Jekyll::StaticFile.new(site, site.source,
|
171
|
+
static_file = Jekyll::StaticFile.new(site, site.source, assets_path, "graph-net-web.json")
|
172
|
+
# TODO: make write file location more flexible -- requiring a write location configuration feels messy...
|
177
173
|
File.write(@site.source + static_file.relative_path, JSON.dump({
|
178
174
|
links: graph_links,
|
179
175
|
nodes: graph_nodes,
|
180
176
|
}))
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
end
|
186
|
-
|
187
|
-
|
188
|
-
def exclude?(type)
|
189
|
-
return false unless option(EXCLUDE_KEY)
|
190
|
-
return option(EXCLUDE_KEY).include?(type.to_s)
|
191
|
-
end
|
192
|
-
|
193
|
-
def exclude_graph?(type)
|
194
|
-
return false unless option_graph(EXCLUDE_KEY)
|
195
|
-
return option_graph(EXCLUDE_KEY).include?(type.to_s)
|
196
|
-
end
|
197
|
-
|
198
|
-
def markdown_extension?(extension)
|
199
|
-
markdown_converter.matches(extension)
|
200
|
-
end
|
201
|
-
|
202
|
-
def markdown_converter
|
203
|
-
@markdown_converter ||= site.find_converter_instance(CONVERTER_CLASS)
|
204
|
-
end
|
205
|
-
|
206
|
-
def option(key)
|
207
|
-
config[CONFIG_KEY] && config[CONFIG_KEY][key]
|
208
|
-
end
|
209
|
-
|
210
|
-
def option_graph(key)
|
211
|
-
config[GRAPH_DATA_KEY] && config[GRAPH_DATA_KEY][key]
|
212
|
-
end
|
213
|
-
|
214
|
-
def disabled?
|
215
|
-
option(ENABLED_KEY) == false
|
216
|
-
end
|
217
|
-
|
218
|
-
def disabled_graph_data?
|
219
|
-
option_graph(ENABLED_GRAPH_DATA_KEY) == false
|
220
|
-
end
|
221
|
-
|
222
|
-
# regex
|
223
|
-
# returns two items: regex and a target capture group (text to be rendered)
|
224
|
-
# using functions instead of constants because of the need to access 'wiki_link_text'
|
225
|
-
# -- esp. when aliasing.
|
226
|
-
|
227
|
-
def regex_invalid_wiki_link()
|
228
|
-
# identify missing links in note via .invalid-wiki-link class and nested note-name.
|
229
|
-
regex = /invalid-wiki-link[^\]]+\[\[([^\]]+)\]\]/i
|
230
|
-
cap_gr = "\\1" # this is mostly just to remain consistent with other regex functions
|
231
|
-
return regex, cap_gr
|
232
|
-
end
|
233
|
-
|
234
|
-
def regex_wiki_link(wiki_link_text='')
|
235
|
-
if wiki_link_text.empty?
|
236
|
-
regex = /(\[\[)([^\|\]]+)(\]\])/i
|
237
|
-
cap_gr = "\\2"
|
238
|
-
return regex, cap_gr
|
239
|
-
else
|
240
|
-
regex = /\[\[#{wiki_link_text}\]\]/i
|
241
|
-
cap_gr = wiki_link_text
|
242
|
-
return regex, cap_gr
|
177
|
+
# tests fail without manually adding the static file, but actual site builds seem to do ok
|
178
|
+
# ...although there does seem to be a race condition which causes a rebuild to be necessary in order to detect the graph data file
|
179
|
+
if @testing
|
180
|
+
@site.static_files << static_file if !@site.static_files.include?(static_file)
|
243
181
|
end
|
244
182
|
end
|
245
183
|
|
246
|
-
|
247
|
-
regex = /(\[\[)([^\]\|]+)(\|)([^\]]+)(\]\])/i
|
248
|
-
cap_gr = "\\2|\\4"
|
249
|
-
return regex, cap_gr
|
250
|
-
end
|
251
|
-
|
252
|
-
def regex_wiki_link_w_alias_left(wiki_link_text)
|
253
|
-
raise ArgumentError.new(
|
254
|
-
"Expected a value for 'wiki_link_text'"
|
255
|
-
) if wiki_link_text.nil?
|
256
|
-
regex = /(\[\[)([^\]\|]+)(\|)(#{wiki_link_text})(\]\])/i
|
257
|
-
cap_gr = "\\2"
|
258
|
-
return regex, cap_gr
|
259
|
-
end
|
260
|
-
|
261
|
-
def regex_wiki_link_w_alias_right(wiki_link_text)
|
262
|
-
raise ArgumentError.new(
|
263
|
-
"Expected a value for 'wiki_link_text'"
|
264
|
-
) if wiki_link_text.nil?
|
265
|
-
regex = /(\[\[)(#{wiki_link_text})(\|)([^\]]+)(\]\])/i
|
266
|
-
cap_gr = "\\4"
|
267
|
-
return regex, cap_gr
|
268
|
-
end
|
184
|
+
# !! deprecated !!
|
269
185
|
|
186
|
+
def old_config_warn()
|
187
|
+
if config.include?("wikilinks_collection")
|
188
|
+
Jekyll.logger.warn "As of 0.0.3, 'wikilinks_collection' is no longer used for configs. jekyll-wikilinks will scan all markdown files by default. Check README for details."
|
189
|
+
end
|
190
|
+
end
|
270
191
|
end
|
271
|
-
end
|
192
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require_relative "naming_const"
|
2
|
+
|
3
|
+
module JekyllWikiLinks
|
4
|
+
class DocManager
|
5
|
+
attr_accessor :md_docs, :static_files
|
6
|
+
|
7
|
+
# kramdown header regexes
|
8
|
+
# atx header: https://github.com/gettalong/kramdown/blob/master/lib/kramdown/parser/kramdown/header.rb#L29
|
9
|
+
REGEX_ATX_HEADER = /^\#{1,6}[\t ]*([^ \t].*)\n/i
|
10
|
+
# setext header: https://github.com/gettalong/kramdown/blob/master/lib/kramdown/parser/kramdown/header.rb#L17
|
11
|
+
REGEX_SETEXT_HEADER = /^ {0,3}([^ \t].*)\n[-=][-=]*[ \t\r\f\v]*\n/i
|
12
|
+
# obsidian-style
|
13
|
+
REGEX_BLOCK = /.*\s\^#{REGEX_BLOCK_ID_TXT}^\n/i
|
14
|
+
|
15
|
+
def initialize(md_docs, static_files)
|
16
|
+
@md_docs ||= md_docs
|
17
|
+
@static_files ||= static_files
|
18
|
+
end
|
19
|
+
|
20
|
+
def all
|
21
|
+
return @md_docs
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_doc_by_fname(filename)
|
25
|
+
return nil if filename.nil? || @md_docs.size == 0
|
26
|
+
docs = @md_docs.select{ |d| File.basename(d.basename, File.extname(d.basename)) == filename }
|
27
|
+
return nil if docs.nil? || docs.size > 1
|
28
|
+
return docs[0]
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_doc_by_url(url)
|
32
|
+
return nil if url.nil? || @md_docs.size == 0
|
33
|
+
docs = @md_docs.select{ |d| d.url == url }
|
34
|
+
return nil if docs.nil? || docs.size > 1
|
35
|
+
return docs[0]
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_doc_content(filename)
|
39
|
+
return nil if filename.nil? || @md_docs.size == 0
|
40
|
+
docs = @md_docs.select{ |d| File.basename(d.basename, File.extname(d.basename)) == filename }
|
41
|
+
return docs[0].content if docs.size == 1
|
42
|
+
return nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# 'bname' -> 'basename' (filename with extension)
|
46
|
+
def get_image_by_bname(filename)
|
47
|
+
return nil if filename.nil? || @static_files.size == 0 || !SUPPORTED_IMG_FORMATS.any?{ |ext| ext == File.extname(filename).downcase }
|
48
|
+
docs = @static_files.select{ |d| d.basename == filename[...-4] }
|
49
|
+
return nil if docs.nil? || docs.size > 1
|
50
|
+
return docs[0]
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.doc_has_header?(doc, header)
|
54
|
+
return nil if header.nil?
|
55
|
+
# leading + trailing whitespace is ignored when matching headers
|
56
|
+
header_results = doc.content.scan(REGEX_ATX_HEADER).flatten.map { |htxt| htxt.strip }
|
57
|
+
setext_header_results = doc.content.scan(REGEX_SETEXT_HEADER).flatten.map { |htxt| htxt.strip }
|
58
|
+
return header_results.include?(header.strip) || setext_header_results.include?(header.strip)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.doc_has_block_id?(doc, block_id)
|
62
|
+
return nil if block_id.nil?
|
63
|
+
# leading + trailing whitespace is ignored when matching blocks
|
64
|
+
block_id_results = doc.content.scan(REGEX_BLOCK).flatten.map { |bid| bid.strip }
|
65
|
+
return block_id_results.include?(block_id)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -1,16 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JekyllWikiLinks
|
4
|
-
module
|
5
|
-
#
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
module TypeFilters
|
5
|
+
# 'links' accepts both untyped links, typed links, and attributes; fore and back.
|
6
|
+
|
7
|
+
# usage: {% assign note_links = page.links | doc_type = "notes" %}
|
8
|
+
# "doc_type" is the jekyll type ("pages", "posts", "<collection-name>")
|
9
|
+
def doc_type(links, doc_type)
|
10
|
+
return if links.nil?
|
11
|
+
target_links = []
|
12
|
+
links.each do |l|
|
13
|
+
target_links << l if self.to_string(l['doc'].type) == doc_type.to_str
|
12
14
|
end
|
13
|
-
return
|
15
|
+
return target_links.uniq
|
16
|
+
end
|
17
|
+
|
18
|
+
# usage: {% assign author_links = page.links | link_type = "author" %}
|
19
|
+
# "link_type" is the wikilink's type, the string that appears before the link in `link-type::[[wikilink]]`.
|
20
|
+
def link_type(links, link_type)
|
21
|
+
return if links.nil?
|
22
|
+
target_links = []
|
23
|
+
link.each do |l|
|
24
|
+
target_links << l if self.to_string(l['type'].to_str) == link_type.to_str
|
25
|
+
end
|
26
|
+
return target_links.uniq
|
14
27
|
end
|
15
28
|
|
16
29
|
def to_string(type)
|
@@ -24,5 +37,3 @@ module JekyllWikiLinks
|
|
24
37
|
end
|
25
38
|
end
|
26
39
|
end
|
27
|
-
|
28
|
-
Liquid::Template.register_filter(JekyllWikiLinks::BackLinkTypeFilters)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# TODO: Would be nice to put LinkIndex-related data into real objects as opposed to sticking tons and tons of data into frontmatter...I think...
|
2
|
+
|
3
|
+
# modelling off of 'related_posts': https://github.com/jekyll/jekyll/blob/6855200ebda6c0e33f487da69e4e02ec3d8286b7/lib/jekyll/document.rb#L402
|
4
|
+
# module LinkLogic
|
5
|
+
# attr_accessor :attributed, :attributes, :backlinks, :forelinks
|
6
|
+
|
7
|
+
# # 'links' 'type' is 'nil' for untyped links.
|
8
|
+
# # 'attributes' are block-level typed forelinks; their 'type' may not be 'nil'.
|
9
|
+
# # 'attributed' are block-level typed backlinks; their 'type' may not be 'nil'.
|
10
|
+
# # [{ 'type': str, 'doc': doc }, ...]
|
11
|
+
# end
|
12
|
+
|
13
|
+
# module Jekyll
|
14
|
+
# class Page
|
15
|
+
# # it would be nice if these would only exist if the page is guaranteed to be a markdown file.
|
16
|
+
# include LinkLogic
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
|
20
|
+
# module Jekyll
|
21
|
+
# class Document
|
22
|
+
# include LinkLogic
|
23
|
+
# end
|
24
|
+
# end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module JekyllWikiLinks
|
2
|
+
class LinkIndex
|
3
|
+
attr_accessor :index
|
4
|
+
|
5
|
+
# Use Jekyll's native relative_url filter
|
6
|
+
include Jekyll::Filters::URLFilters
|
7
|
+
|
8
|
+
REGEX_LINK_TYPE = /<a\sclass="wiki-link(\slink-type\s(?<link-type>([^"]+)))?"\shref="(?<link-url>([^"]+))">/i
|
9
|
+
|
10
|
+
def initialize(site, doc_manager)
|
11
|
+
@context ||= JekyllWikiLinks::Context.new(site)
|
12
|
+
@doc_manager ||= doc_manager
|
13
|
+
@index = {}
|
14
|
+
@doc_manager.all.each do |doc|
|
15
|
+
@index[doc.url] = LinksInfo.new()
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def process
|
20
|
+
self.populate_links()
|
21
|
+
# apply index info to each document
|
22
|
+
@doc_manager.all.each do |doc|
|
23
|
+
doc.data['attributed'] = @index[doc.url].attributed
|
24
|
+
doc.data['backlinks'] = @index[doc.url].backlinks
|
25
|
+
doc.data['attributes'] = @index[doc.url].attributes
|
26
|
+
doc.data['forelinks'] = @index[doc.url].forelinks
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def populate_attributes(doc, typed_link_blocks)
|
31
|
+
typed_link_blocks.each do |tl|
|
32
|
+
attr_doc = @doc_manager.get_doc_by_fname(tl.filename)
|
33
|
+
@index[doc.url].attributes << {
|
34
|
+
'type' => tl.link_type,
|
35
|
+
'doc' => attr_doc,
|
36
|
+
}
|
37
|
+
@index[attr_doc.url].attributed << {
|
38
|
+
'type' => tl.link_type,
|
39
|
+
'doc' => doc,
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def populate_links()
|
45
|
+
# for each document...
|
46
|
+
@doc_manager.all.each do |doc|
|
47
|
+
# ...process its forelinks
|
48
|
+
doc.content.scan(REGEX_LINK_TYPE).each do |m|
|
49
|
+
ltype, lurl = m[0], m[1]
|
50
|
+
@index[doc.url].forelinks << {
|
51
|
+
'type' => ltype,
|
52
|
+
'doc' => @doc_manager.get_doc_by_url(lurl),
|
53
|
+
}
|
54
|
+
end
|
55
|
+
# ...process its backlinks
|
56
|
+
@doc_manager.all.each do |doc_to_backlink|
|
57
|
+
doc_to_backlink.content.scan(REGEX_LINK_TYPE).each do |m|
|
58
|
+
ltype, lurl = m[0], m[1]
|
59
|
+
if lurl == relative_url(doc.url)
|
60
|
+
@index[doc.url].backlinks << {
|
61
|
+
'type' => ltype,
|
62
|
+
'doc' => doc_to_backlink,
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class LinksInfo
|
71
|
+
attr_accessor :attributed, :attributes, :backlinks, :forelinks
|
72
|
+
|
73
|
+
def initialize
|
74
|
+
@attributed = [] # (block-level typed forelinks)
|
75
|
+
@attributes = [] # (block-level typed backlinks)
|
76
|
+
@backlinks = []
|
77
|
+
@forelinks = []
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# naming_const.rb
|
2
|
+
# regex constants defining supported file types and valid names for files, variables, or text
|
3
|
+
#
|
4
|
+
|
5
|
+
module JekyllWikiLinks
|
6
|
+
# TODO: Fix REGEX_NOT_GREEDY
|
7
|
+
# REGEX_NOT_GREEDY = /[^(?!\]\])]+/i
|
8
|
+
# REGEX_NOT_GREEDY = /(?!\]\]).*/i
|
9
|
+
REGEX_NOT_GREEDY = /[^\]]+/i
|
10
|
+
# <variables> only work with 'match' function, not with 'scan' function. :/
|
11
|
+
# oh well...they are there for easier debugging...
|
12
|
+
# valid naming conventions # capture indeces for WikiLinks class (0 is 'embed')
|
13
|
+
REGEX_LINK_TYPE_TXT = /(?<link-type-txt>([^\n\s\!\#\^\|\]]+))/i # 1
|
14
|
+
REGEX_FILENAME = /(?<filename>([^\\\/:\#\^\|\[\]]+))/i # 2
|
15
|
+
REGEX_HEADER_TXT = /(?<header-txt>([^\!\#\^\|\[\]]+))/i # 3
|
16
|
+
REGEX_BLOCK_ID_TXT = /(?<block-id>([^\\\/:\!\#\^\|\[\]]+))/i # 4
|
17
|
+
REGEX_LABEL_TXT = /(?<label-txt>(#{REGEX_NOT_GREEDY}))/i # 5
|
18
|
+
|
19
|
+
# from: https://docs.github.com/en/github/managing-files-in-a-repository/working-with-non-code-files/rendering-and-diffing-images
|
20
|
+
SUPPORTED_IMG_FORMATS = Set.new(['.png', '.jpg', '.gif', '.psd', '.svg'])
|
21
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require_relative "naming_const"
|
2
|
+
|
3
|
+
module JekyllWikiLinks
|
4
|
+
REGEX_LINK_EMBED = /(?<embed>(\!))/i # 0 (capture index for WikiLinks class)
|
5
|
+
REGEX_LINK_TYPE = /::/
|
6
|
+
REGEX_LINK_HEADER = /\#/
|
7
|
+
REGEX_LINK_BLOCK = /\#\^/
|
8
|
+
REGEX_LINK_LABEL = /\|/
|
9
|
+
REGEX_WIKI_LINKS = %r{
|
10
|
+
(#{REGEX_LINK_EMBED})?
|
11
|
+
(#{REGEX_LINK_TYPE_TXT}#{REGEX_LINK_TYPE})?
|
12
|
+
\[\[
|
13
|
+
#{REGEX_FILENAME}
|
14
|
+
(#{REGEX_LINK_HEADER}#{REGEX_HEADER_TXT})?
|
15
|
+
(#{REGEX_LINK_BLOCK}#{REGEX_BLOCK_ID_TXT})?
|
16
|
+
(#{REGEX_LINK_LABEL}#{REGEX_LABEL_TXT})?
|
17
|
+
\]\]
|
18
|
+
}x
|
19
|
+
REGEX_TYPED_LINK_BLOCK = /#{REGEX_LINK_TYPE_TXT}#{REGEX_LINK_TYPE}\[\[#{REGEX_FILENAME}\]\]\n/i
|
20
|
+
|
21
|
+
# more of a "parser" than a parser
|
22
|
+
class Parser
|
23
|
+
attr_accessor :doc_manager, :markdown_converter, :wikilinks, :typed_link_blocks
|
24
|
+
|
25
|
+
# Use Jekyll's native relative_url filter
|
26
|
+
include Jekyll::Filters::URLFilters
|
27
|
+
|
28
|
+
def initialize(context, markdown_converter, doc_manager)
|
29
|
+
@context ||= context
|
30
|
+
@doc_manager ||= doc_manager
|
31
|
+
@markdown_converter ||= markdown_converter
|
32
|
+
@wikilinks, @typed_link_blocks = [], []
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse(doc_content)
|
36
|
+
@typed_link_blocks, @wikilinks = [], []
|
37
|
+
# process blocks
|
38
|
+
typed_link_block_matches = doc_content.scan(REGEX_TYPED_LINK_BLOCK)
|
39
|
+
if !typed_link_block_matches.nil? && typed_link_block_matches.size != 0
|
40
|
+
typed_link_block_matches.each do |wl_match|
|
41
|
+
typed_link_block_wikilink = WikiLink.new(
|
42
|
+
nil,
|
43
|
+
wl_match[0],
|
44
|
+
wl_match[1],
|
45
|
+
nil,
|
46
|
+
nil,
|
47
|
+
nil,
|
48
|
+
)
|
49
|
+
doc_content.gsub!(typed_link_block_wikilink.md_link_str, "")
|
50
|
+
@typed_link_blocks << typed_link_block_wikilink
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# process inlines
|
54
|
+
wikilink_matches = doc_content.scan(REGEX_WIKI_LINKS)
|
55
|
+
if !wikilink_matches.nil? && wikilink_matches.size != 0
|
56
|
+
wikilink_matches.each do |wl_match|
|
57
|
+
@wikilinks << WikiLink.new(
|
58
|
+
wl_match[0],
|
59
|
+
wl_match[1],
|
60
|
+
wl_match[2],
|
61
|
+
wl_match[3],
|
62
|
+
wl_match[4],
|
63
|
+
wl_match[5],
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
# replace text
|
68
|
+
return if @wikilinks.nil?
|
69
|
+
@wikilinks.each do |wikilink|
|
70
|
+
doc_content.sub!(
|
71
|
+
wikilink.md_link_regex,
|
72
|
+
self.build_html(wikilink)
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_html_embed(title, content, url)
|
78
|
+
# multi-line for readability
|
79
|
+
return [
|
80
|
+
"<div class=\"wiki-link-embed\">",
|
81
|
+
"<div class=\"wiki-link-embed-title\">",
|
82
|
+
"#{title}",
|
83
|
+
"</div>",
|
84
|
+
"<div class=\"wiki-link-embed-content\">",
|
85
|
+
"#{@markdown_converter.convert(content)}",
|
86
|
+
"</div>",
|
87
|
+
"<a class=\"wiki-link-embed-link\" href=\"#{url}\"></a>",
|
88
|
+
"</div>",
|
89
|
+
].join("\n").gsub!("\n", "")
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_html_img_embed(img_file)
|
93
|
+
"<p><span class=\"wiki-link-embed-image\"><img class=\"wiki-link-img\" src=\"#{relative_url(img_file.relative_path)}\"/></span></p>"
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_html(wikilink)
|
97
|
+
if wikilink.is_img?
|
98
|
+
linked_doc = @doc_manager.get_image_by_bname(wikilink.filename)
|
99
|
+
if wikilink.embedded? && wikilink.is_img?
|
100
|
+
return build_html_img_embed(linked_doc)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
linked_doc = @doc_manager.get_doc_by_fname(wikilink.filename)
|
104
|
+
if !linked_doc.nil?
|
105
|
+
link_type = wikilink.typed? ? " link-type #{wikilink.link_type}" : ""
|
106
|
+
|
107
|
+
# label
|
108
|
+
wikilink_inner_txt = wikilink.clean_label_txt if wikilink.labelled?
|
109
|
+
|
110
|
+
lnk_doc_rel_url = relative_url(linked_doc.url) if linked_doc&.url
|
111
|
+
# TODO not sure about downcase
|
112
|
+
fname_inner_txt = linked_doc['title'].downcase if wikilink_inner_txt.nil?
|
113
|
+
|
114
|
+
link_lvl = wikilink.describe['level']
|
115
|
+
if (link_lvl == "file")
|
116
|
+
wikilink_inner_txt = "#{fname_inner_txt}" if wikilink_inner_txt.nil?
|
117
|
+
return build_html_embed(
|
118
|
+
linked_doc['title'],
|
119
|
+
@doc_manager.get_doc_content(wikilink.filename),
|
120
|
+
lnk_doc_rel_url
|
121
|
+
) if wikilink.embedded?
|
122
|
+
elsif (link_lvl == "header" && DocManager.doc_has_header?(linked_doc, wikilink.header_txt))
|
123
|
+
lnk_doc_rel_url += "\#" + wikilink.header_txt.downcase
|
124
|
+
wikilink_inner_txt = "#{fname_inner_txt} > #{wikilink.header_txt}" if wikilink_inner_txt.nil?
|
125
|
+
elsif (link_lvl == "block" && DocManager.doc_has_block_id?(linked_doc, wikilink.block_id))
|
126
|
+
lnk_doc_rel_url += "\#" + wikilink.block_id.downcase
|
127
|
+
wikilink_inner_txt = "#{fname_inner_txt} > ^#{wikilink.block_id}" if wikilink_inner_txt.nil?
|
128
|
+
else
|
129
|
+
return '<span title="Content not found." class="invalid-wiki-link">' + wikilink.md_link_str + '</span>'
|
130
|
+
end
|
131
|
+
return '<a class="wiki-link' + link_type + '" href="' + lnk_doc_rel_url + '">' + wikilink_inner_txt + '</a>'
|
132
|
+
else
|
133
|
+
return '<span title="Content not found." class="invalid-wiki-link">' + wikilink.md_link_str + '</span>'
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# the wikilink class knows everything about the original markdown syntax and its semantic meaning
|
139
|
+
class WikiLink
|
140
|
+
attr_accessor :embed, :link_type, :filename, :header_txt, :block_id, :label_txt
|
141
|
+
|
142
|
+
FILENAME = "filename"
|
143
|
+
HEADER_TXT = "header_txt"
|
144
|
+
BLOCK_ID = "block_id"
|
145
|
+
|
146
|
+
# parameters ordered by appearance in regex
|
147
|
+
def initialize(embed, link_type, filename, header_txt, block_id, label_txt)
|
148
|
+
# super(embed, link_type, filename, header_txt, block_id, label_txt)
|
149
|
+
@embed ||= embed
|
150
|
+
@link_type ||= link_type
|
151
|
+
@filename ||= filename
|
152
|
+
@header_txt ||= header_txt
|
153
|
+
@block_id ||= block_id
|
154
|
+
@label_txt ||= label_txt
|
155
|
+
end
|
156
|
+
|
157
|
+
# labeles are really flexible, so we need to handle them with a bit more care
|
158
|
+
def clean_label_txt
|
159
|
+
return @label_txt.sub("[", "\\[").sub("]", "\\]")
|
160
|
+
end
|
161
|
+
|
162
|
+
def md_link_str
|
163
|
+
embed = embedded? ? "!" : ""
|
164
|
+
link_type = typed? ? "#{@link_type}::" : ""
|
165
|
+
filename = described?(FILENAME) ? @filename : ""
|
166
|
+
if described?(HEADER_TXT)
|
167
|
+
header = "\##{@header_txt}"
|
168
|
+
block = ""
|
169
|
+
elsif described?(BLOCK_ID)
|
170
|
+
header = ""
|
171
|
+
block = "\#\^#{@block_id}"
|
172
|
+
elsif !described?(FILENAME)
|
173
|
+
Jekyll.logger.error "Invalid link level in 'md_link_str'. See WikiLink's 'md_link_str' for details"
|
174
|
+
end
|
175
|
+
label_ = labelled? ? "\|#{@label_txt}" : ""
|
176
|
+
return "#{embed}#{link_type}\[\[#{filename}#{header}#{block}#{label_}\]\]"
|
177
|
+
end
|
178
|
+
|
179
|
+
def md_link_regex
|
180
|
+
regex_embed = embedded? ? REGEX_LINK_EMBED : %r{}
|
181
|
+
regex_link_type = typed? ? %r{#{@link_type}#{REGEX_LINK_TYPE}} : %r{}
|
182
|
+
filename = described?(FILENAME) ? @filename : ""
|
183
|
+
if described?(HEADER_TXT)
|
184
|
+
header = %r{#{REGEX_LINK_HEADER}#{@header_txt}}
|
185
|
+
block = %r{}
|
186
|
+
elsif described?(BLOCK_ID)
|
187
|
+
header = %r{}
|
188
|
+
block = %r{#{REGEX_LINK_BLOCK}#{@block_id}}
|
189
|
+
elsif !described?(FILENAME)
|
190
|
+
Jekyll.logger.error "Invalid link level in regex. See WikiLink's 'md_link_regex' for details"
|
191
|
+
end
|
192
|
+
label_ = labelled? ? %r{#{REGEX_LINK_LABEL}#{clean_label_txt}} : %r{}
|
193
|
+
return %r{#{regex_embed}#{regex_link_type}\[\[#{filename}#{header}#{block}#{label_}\]\]}
|
194
|
+
end
|
195
|
+
|
196
|
+
def describe
|
197
|
+
return {
|
198
|
+
'level' => level,
|
199
|
+
'labelled' => labelled?,
|
200
|
+
'embedded' => embedded?,
|
201
|
+
'typed_link' => typed?,
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
def labelled?
|
206
|
+
return !@label_txt.nil? && !@label_txt.empty?
|
207
|
+
end
|
208
|
+
|
209
|
+
def typed?
|
210
|
+
return !@link_type.nil? && !@link_type.empty?
|
211
|
+
end
|
212
|
+
|
213
|
+
def embedded?
|
214
|
+
return !@embed.nil? && @embed == "!"
|
215
|
+
end
|
216
|
+
|
217
|
+
def is_img?
|
218
|
+
# github supported image formats: https://docs.github.com/en/github/managing-files-in-a-repository/working-with-non-code-files/rendering-and-diffing-images
|
219
|
+
return SUPPORTED_IMG_FORMATS.any?{ |ext| ext == File.extname(@filename).downcase }
|
220
|
+
end
|
221
|
+
|
222
|
+
def described?(chunk)
|
223
|
+
return (!@filename.nil? && !@filename.empty?) if chunk == FILENAME
|
224
|
+
return (!@header_txt.nil? && !@header_txt.empty?) if chunk == HEADER_TXT
|
225
|
+
return (!@block_id.nil? && !@block_id.empty?) if chunk == BLOCK_ID
|
226
|
+
Jekyll.logger.error "There is no link level '#{chunk}' in WikiLink Struct"
|
227
|
+
end
|
228
|
+
|
229
|
+
def level
|
230
|
+
return "file" if described?(FILENAME) && !described?(HEADER_TXT) && !described?(BLOCK_ID)
|
231
|
+
return "header" if described?(FILENAME) && described?(HEADER_TXT) && !described?(BLOCK_ID)
|
232
|
+
return "block" if described?(FILENAME) && !described?(HEADER_TXT) && described?(BLOCK_ID)
|
233
|
+
return "invalid"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
metadata
CHANGED
@@ -1,32 +1,37 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jekyll-wikilinks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- manunamz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
15
|
-
-
|
15
|
+
- manunamz@pm.me
|
16
16
|
executables: []
|
17
17
|
extensions: []
|
18
18
|
extra_rdoc_files: []
|
19
19
|
files:
|
20
20
|
- lib/jekyll-wikilinks.rb
|
21
21
|
- lib/jekyll-wikilinks/context.rb
|
22
|
+
- lib/jekyll-wikilinks/doc_manager.rb
|
22
23
|
- lib/jekyll-wikilinks/filter.rb
|
24
|
+
- lib/jekyll-wikilinks/jekyll_patch.rb
|
25
|
+
- lib/jekyll-wikilinks/link_index.rb
|
26
|
+
- lib/jekyll-wikilinks/naming_const.rb
|
27
|
+
- lib/jekyll-wikilinks/parser.rb
|
23
28
|
- lib/jekyll-wikilinks/version.rb
|
24
|
-
homepage: https://github.com/
|
29
|
+
homepage: https://github.com/manunamz/jekyll-wikilinks
|
25
30
|
licenses:
|
26
31
|
- MIT
|
27
32
|
metadata:
|
28
|
-
homepage_uri: https://github.com/
|
29
|
-
source_code_uri: https://github.com/
|
33
|
+
homepage_uri: https://github.com/manunamz/jekyll-wikilinks
|
34
|
+
source_code_uri: https://github.com/manunamz/jekyll-wikilinks
|
30
35
|
post_install_message:
|
31
36
|
rdoc_options: []
|
32
37
|
require_paths:
|
@@ -45,5 +50,5 @@ requirements: []
|
|
45
50
|
rubygems_version: 3.2.17
|
46
51
|
signing_key:
|
47
52
|
specification_version: 4
|
48
|
-
summary: Add support for [[wikilinks]] (in markdown).
|
53
|
+
summary: Add jekyll support for [[wikilinks]] (in markdown).
|
49
54
|
test_files: []
|