jekyll-wikilinks 0.0.5 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+ require "nokogiri"
2
+ require_relative "regex"
3
+ require_relative "wikilink"
4
+
5
+ module Jekyll
6
+ module WikiLinks
7
+
8
+ # more of a "parser" than a parser
9
+ class Parser
10
+ attr_accessor :doc_manager, :markdown_converter, :wikilink_inlines, :wikilink_blocks
11
+
12
+ # Use Jekyll's native relative_url filter
13
+ include Jekyll::Filters::URLFilters
14
+
15
+ CONVERTER_CLASS = Jekyll::Converters::Markdown
16
+
17
+ def initialize(site)
18
+ @context ||= Jekyll::WikiLinks::Context.new(site)
19
+ # do not use @dm in parser -- it is only meant to be passed down into wikilink classes.
20
+ @doc_manager ||= site.doc_mngr
21
+ @markdown_converter ||= site.find_converter_instance(CONVERTER_CLASS)
22
+ @wikilink_blocks, @wikilink_inlines = [], []
23
+ end
24
+
25
+ # parsing
26
+
27
+ def parse(doc_filename, doc_content)
28
+ @wikilink_blocks, @wikilink_inlines = [], []
29
+ if !$wiki_conf.disabled_attributes?
30
+ self.parse_blocks(doc_filename, doc_content)
31
+ end
32
+ self.parse_inlines(doc_filename, doc_content)
33
+ end
34
+
35
+ def parse_blocks(doc_filename, doc_content)
36
+ block_matches = doc_content.scan(REGEX_WIKI_LINK_BLOCKS)
37
+ if !block_matches.nil? && block_matches.size != 0
38
+ block_matches.each do |wl_match|
39
+ # init block wikilink
40
+ wikilink_block = WikiLinkBlock.new(
41
+ @doc_manager,
42
+ doc_filename,
43
+ wl_match[0], # link_type
44
+ wl_match[2], # bullet_type
45
+ )
46
+ # extract + add filenames
47
+ items = wl_match[1]
48
+ filename_matches = items.scan(/#{REGEX_LINK_LEFT}#{REGEX_FILENAME}#{REGEX_LINK_RIGHT}/i)
49
+ filename_matches.each do |match|
50
+ match.each do |fname|
51
+ wikilink_block.add_item(fname)
52
+ end
53
+ end
54
+ # replace text
55
+ doc_content.gsub!(wikilink_block.md_regex, "\n")
56
+ @wikilink_blocks << wikilink_block
57
+ end
58
+ end
59
+ end
60
+
61
+ def parse_inlines(doc_filename, doc_content)
62
+ inline_matches = doc_content.scan(REGEX_WIKI_LINK_INLINES)
63
+ if !inline_matches.nil? && inline_matches.size != 0
64
+ inline_matches.each do |wl_match|
65
+ @wikilink_inlines << WikiLinkInline.new(
66
+ @doc_manager,
67
+ doc_filename,
68
+ wl_match[0],
69
+ wl_match[1],
70
+ wl_match[2],
71
+ wl_match[3],
72
+ wl_match[4],
73
+ wl_match[5],
74
+ )
75
+ end
76
+ end
77
+ # replace text
78
+ return if @wikilink_inlines.nil?
79
+ self.sort_typed_first
80
+ @wikilink_inlines.each do |wikilink|
81
+ doc_content.gsub!(
82
+ wikilink.md_regex,
83
+ self.build_html(wikilink)
84
+ )
85
+ end
86
+ end
87
+
88
+ # building/converting
89
+
90
+ def build_html_embed(title, content, url)
91
+ # multi-line for readability
92
+ return [
93
+ "<div class=\"#{$wiki_conf.css_name("embed_wrapper")}\">",
94
+ "<div class=\"#{$wiki_conf.css_name("embed_title")}\">",
95
+ "#{title}",
96
+ "</div>",
97
+ "<div class=\"#{$wiki_conf.css_name("embed_content")}\">",
98
+ "#{@markdown_converter.convert(content)}",
99
+ "</div>",
100
+ "<a class=\"#{$wiki_conf.css_name("embed_wiki_link")}\" href=\"#{url}\"></a>",
101
+ "</div>",
102
+ ].join("\n").gsub!("\n", "")
103
+ end
104
+
105
+ def build_html_img_embed(static_doc, is_svg=false)
106
+ svg_content = ""
107
+ if is_svg
108
+ File.open(static_doc.path, "r") do |svg_img|
109
+ svg_content = svg_img.read
110
+ end
111
+ return "<p><span class=\"#{$wiki_conf.css_name("embed_image_wrapper")}\">#{svg_content}</span></p>"
112
+ else
113
+ return "<p><span class=\"#{$wiki_conf.css_name("embed_image_wrapper")}\"><img class=\"#{$wiki_conf.css_name("embed_image")}\" src=\"#{relative_url(static_doc.relative_path)}\"></span></p>"
114
+ end
115
+ end
116
+
117
+ def build_html(wikilink)
118
+ if !wikilink.is_valid?
119
+ return '<span class="' + $wiki_conf.css_name("invalid_wiki") + '">' + wikilink.md_str + '</span>'
120
+ end
121
+ # image processing
122
+ if wikilink.embedded? && wikilink.is_img?
123
+ return build_html_img_embed(wikilink.linked_img, is_svg=wikilink.is_img_svg?)
124
+ end
125
+ # markdown file processing
126
+ linked_doc = wikilink.linked_doc
127
+ link_type_txt = wikilink.is_typed? ? " #{$wiki_conf.css_name("typed")} #{wikilink.link_type}" : ""
128
+
129
+ inner_txt = wikilink.label_txt if wikilink.labelled?
130
+ lnk_doc_rel_url = relative_url(linked_doc.url)
131
+
132
+ if (wikilink.level == "file")
133
+ inner_txt = "#{linked_doc['title'].downcase}" if inner_txt.nil?
134
+ return build_html_embed(
135
+ linked_doc['title'],
136
+ linked_doc.content,
137
+ lnk_doc_rel_url
138
+ ) if wikilink.embedded?
139
+ elsif (wikilink.level == "header")
140
+ # from: https://github.com/jekyll/jekyll/blob/6855200ebda6c0e33f487da69e4e02ec3d8286b7/Rakefile#L74
141
+ lnk_doc_rel_url += "\#" + Jekyll::Utils.slugify(wikilink.header_txt)
142
+ inner_txt = "#{linked_doc['title'].downcase} > #{wikilink.header_txt.downcase}" if inner_txt.nil?
143
+ elsif (wikilink.level == "block")
144
+ lnk_doc_rel_url += "\#" + wikilink.block_id
145
+ inner_txt = "#{linked_doc['title'].downcase} > ^#{wikilink.block_id}" if inner_txt.nil?
146
+ else
147
+ Jekyll.logger.error("Jekyll-Wikilinks: Invalid wikilink level")
148
+ end
149
+ return '<a class="' + $wiki_conf.css_name("wiki") + link_type_txt + '" href="' + lnk_doc_rel_url + '">' + inner_txt + '</a>'
150
+ end
151
+
152
+ # helpers
153
+
154
+ def sort_typed_first
155
+ # sorting inline wikilinks is necessary so when wikilinks are replaced,
156
+ # longer strings are replaced first so as not to accidentally overwrite
157
+ # substrings
158
+ # (this is especially likely if there is a matching wikilink that
159
+ # appears as both untyped and typed in a document)
160
+ temp = @wikilink_inlines.dup
161
+ @wikilink_inlines.clear()
162
+ typed_wikilinks = temp.select { |wl| wl.is_typed? }
163
+ untyped_wikilinks = temp.select { |wl| !wl.is_typed? }
164
+ @wikilink_inlines = typed_wikilinks.concat(untyped_wikilinks)
165
+ end
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,72 @@
1
+ # regex.rb
2
+ # regex constants defining supported file types and valid names for files, variables, or text
3
+ #
4
+
5
+ module Jekyll
6
+ module WikiLinks
7
+ # <regex_variables> only work with 'match' function, not with 'scan' function. :/
8
+ # oh well...they are there for easier debugging...
9
+
10
+ # supported image formats
11
+ # from: https://docs.github.com/en/github/managing-files-in-a-repository/working-with-non-code-files/rendering-and-diffing-images
12
+ SUPPORTED_IMG_FORMATS = Set.new(['.png', '.jpg', '.gif', '.psd', '.svg'])
13
+
14
+ # wikilink constants
15
+ REGEX_LINK_LEFT = /\[\[/
16
+ REGEX_LINK_RIGHT = /\]\]/
17
+ REGEX_LINK_EMBED = /(?<embed>\!)/
18
+ REGEX_LINK_TYPE = /\s*::\s*/
19
+ REGEX_LINK_HEADER = /\#/
20
+ REGEX_LINK_BLOCK = /\#\^/
21
+ REGEX_LINK_LABEL = /\|/
22
+
23
+ # wikitext usable char requirements
24
+ REGEX_LINK_TYPE_CHARS = /[^\n\s\!\#\^\|\]]+/i
25
+ REGEX_FILENAME_CHARS = /[^\\\/:\#\^\|\[\]]+/i
26
+ REGEX_HEADER_CHARS = /[^\!\#\^\|\[\]]+/i
27
+ REGEX_BLOCK_ID_CHARS = /[^\\\/:\!\#\^\|\[\]^\n]+/i
28
+ REGEX_LABEL_CHARS = /(.+?)(?=\]{2}[^\]])/i
29
+
30
+ # capture groups
31
+ REGEX_LINK_TYPE_TXT = /(?<link-type-txt>#{REGEX_LINK_TYPE_CHARS})/i
32
+ REGEX_FILENAME = /(?<filename>#{REGEX_FILENAME_CHARS})/i
33
+ REGEX_HEADER_TXT = /(?<header-txt>#{REGEX_HEADER_CHARS})/i
34
+ REGEX_BLOCK_ID_TXT = /(?<block-id>#{REGEX_BLOCK_ID_CHARS})/i
35
+ REGEX_LABEL_TXT = /(?<label-txt>#{REGEX_LABEL_CHARS})/i
36
+
37
+ # target markdown text (headers, lists, and blocks)
38
+ ## kramdown regexes
39
+ ### atx header: https://github.com/gettalong/kramdown/blob/master/lib/kramdown/parser/kramdown/header.rb#L29
40
+ REGEX_ATX_HEADER = /^\#{1,6}[\t ]*([^ \t].*)\n/i
41
+ ### setext header: https://github.com/gettalong/kramdown/blob/master/lib/kramdown/parser/kramdown/header.rb#L17
42
+ REGEX_SETEXT_HEADER = /^\s{0,3}([^ \t].*)\n[-=][-=]*[ \t\r\f\v]*\n/i
43
+ ## list item: https://github.com/gettalong/kramdown/blob/master/lib/kramdown/parser/kramdown/list.rb#L49
44
+ REGEX_BULLET = /(?<bullet>[+*-])/i
45
+ ## markdown-style block-reference
46
+ REGEX_BLOCK = /.*\s\^#{REGEX_BLOCK_ID_TXT}/i
47
+
48
+ # wikilinks
49
+
50
+ ## inline
51
+ REGEX_WIKI_LINK_INLINES = %r{ # capture indeces
52
+ (#{REGEX_LINK_EMBED})? # 0
53
+ (#{REGEX_LINK_TYPE_TXT}#{REGEX_LINK_TYPE})? # 1
54
+ #{REGEX_LINK_LEFT}
55
+ #{REGEX_FILENAME} # 2
56
+ (#{REGEX_LINK_HEADER}#{REGEX_HEADER_TXT})? # 3
57
+ (#{REGEX_LINK_BLOCK}#{REGEX_BLOCK_ID_TXT})? # 4
58
+ (#{REGEX_LINK_LABEL}#{REGEX_LABEL_TXT})? # 5
59
+ #{REGEX_LINK_RIGHT}
60
+ }x
61
+
62
+ ## block
63
+ ### single
64
+ REGEX_SINGLE = /#{REGEX_LINK_LEFT}#{REGEX_FILENAME_CHARS}#{REGEX_LINK_RIGHT}/i
65
+ ### list (comma is responsible for catching the single case)
66
+ REGEX_LIST_COMMA = /((?:\s*#{REGEX_SINGLE}\s*)(?:,\s*#{REGEX_SINGLE}\s*)*)/i
67
+ REGEX_LIST_MKDN = /((?<=\n)\s{0,3}#{REGEX_BULLET}\s#{REGEX_SINGLE}\s*)+/i # (see REGEX_LIST_ITEM)
68
+ ### process
69
+ REGEX_BLOCK_TYPES = /((?<!\n)(?:#{REGEX_LIST_COMMA})|#{REGEX_LIST_MKDN})/i
70
+ REGEX_WIKI_LINK_BLOCKS = /^\s{0,3}#{REGEX_LINK_TYPE_TXT}#{REGEX_LINK_TYPE}(?:\s*|\G)(?<items>#{REGEX_BLOCK_TYPES})\n/i
71
+ end
72
+ end
@@ -0,0 +1,278 @@
1
+ # wiki data structures
2
+ require_relative "regex"
3
+
4
+ module Jekyll
5
+ module WikiLinks
6
+
7
+ # wikilink classes know everything about the original markdown syntax and its semantic meaning
8
+
9
+ class WikiLinkBlock
10
+ attr_accessor :link_type, :filenames
11
+
12
+ # parameters ordered by appearance in regex
13
+ def initialize(doc_mngr, context_filename, link_type, bullet_type=nil)
14
+ @doc_mngr ||= doc_mngr
15
+ @context_filename ||= context_filename
16
+ @link_type ||= link_type
17
+ @bullet_type ||= bullet_type
18
+ @filenames = []
19
+ end
20
+
21
+ def add_item(filename)
22
+ Jekyll.logger.error("Jekyll-Wikilinks: 'filename' required") if filename.nil? || filename.empty?
23
+ @filenames << filename
24
+ end
25
+
26
+ # data
27
+
28
+ def md_regex
29
+ if !is_typed? || !has_filenames?
30
+ Jekyll.logger.error("Jekyll-Wikilinks: WikiLinkBlock.md_regex error -- type: #{@link_type}, fnames: #{@filenames.inspect}, for: #{@context_filename}")
31
+ end
32
+ # comma (including singles)
33
+ if @bullet_type.nil?
34
+ link_type = /#{@link_type}#{REGEX_LINK_TYPE}/i
35
+ tmp_filenames = @filenames.dup
36
+ first_filename = /\s*#{REGEX_LINK_LEFT}#{tmp_filenames.shift()}#{REGEX_LINK_RIGHT}\s*/i
37
+ filename_strs = tmp_filenames.map { |f| /,\s*#{REGEX_LINK_LEFT}#{f}#{REGEX_LINK_RIGHT}\s*/i }
38
+ md_regex = /#{link_type}#{first_filename}#{filename_strs.join('')}\n/i
39
+ # mkdn
40
+ elsif !@bullet_type.match(REGEX_BULLET).nil?
41
+ link_type = /#{@link_type}#{REGEX_LINK_TYPE}\n/i
42
+ filename_strs = @filenames.map { |f| /\s{0,3}#{Regexp.escape(@bullet_type)}\s#{REGEX_LINK_LEFT}#{f}#{REGEX_LINK_RIGHT}\n/i }
43
+ md_regex = /#{link_type}#{filename_strs.join("")}/i
44
+ else
45
+ Jekyll.logger.error("Jekyll-Wikilinks: WikiLinkBlock.bullet_type error: #{@bullet_type}")
46
+ end
47
+ return md_regex
48
+ end
49
+
50
+ def md_str
51
+ if !is_typed? || !has_filenames?
52
+ Jekyll.logger.error("Jekyll-Wikilinks: WikiLinkBlockList.md_str error -- type: #{@link_type}, fnames: #{@filenames.inspect}, for: #{@context_filename}")
53
+ end
54
+ # comma (including singles)
55
+ if @bullet_type.nil?
56
+ link_type = "#{@link_type}::"
57
+ filename_strs = @filenames.map { |f| "\[\[#{f}\]\]," }
58
+ md_str = (link_type + filename_strs.join('')).delete_suffix(",")
59
+ # mkdn
60
+ elsif !@bullet_type.match(REGEX_BULLET).nil?
61
+ link_type = "#{@link_type}::\n"
62
+ filename_strs = @filenames.map { |f| li[0] + " \[\[#{li[1]}\]\]\n" }
63
+ md_str = link_type + filename_strs.join('')
64
+ else
65
+ Jekyll.logger.error("Jekyll-Wikilinks: 'bullet_type' invalid: #{@bullet_type}")
66
+ end
67
+ return md_str
68
+ end
69
+
70
+ def urls
71
+ # return @filenames.map { |f| @doc_mngr.get_doc_by_fname(f) }
72
+ urls = []
73
+ @filenames.each do |f|
74
+ doc = @doc_mngr.get_doc_by_fname(f)
75
+ urls << doc.url if !doc.nil?
76
+ end
77
+ return urls
78
+ end
79
+
80
+ # 'fm' -> frontmatter
81
+
82
+ def context_fm_data
83
+ return {
84
+ 'type' => @link_type,
85
+ 'urls' => [self.context_doc.url],
86
+ }
87
+ end
88
+
89
+ def linked_fm_data
90
+ return {
91
+ 'type' => @link_type,
92
+ 'urls' => self.urls,
93
+ }
94
+ end
95
+
96
+ def context_doc
97
+ return @doc_mngr.get_doc_by_fname(@context_filename)
98
+ end
99
+
100
+ def linked_docs
101
+ docs = []
102
+ @filenames.each do |f|
103
+ doc = @doc_mngr.get_doc_by_fname(f)
104
+ docs << doc if !doc.nil?
105
+ end
106
+ return docs
107
+ end
108
+
109
+ # descriptor methods
110
+
111
+ def has_filenames?
112
+ return !@filenames.nil? && !@filenames.empty?
113
+ end
114
+
115
+ def is_typed?
116
+ return !@link_type.nil? && !@link_type.empty?
117
+ end
118
+
119
+ # validation methods
120
+
121
+ def is_valid?
122
+ return false if !is_typed?
123
+ return false if !has_filenames?
124
+ @filenames.each do |f|
125
+ return false if !@doc_mngr.file_exists?(f)
126
+ end
127
+ return true
128
+ end
129
+ end
130
+
131
+ class WikiLinkInline
132
+ attr_accessor :context_filename, :embed, :link_type, :filename, :header_txt, :block_id, :label_txt
133
+
134
+ FILENAME = "filename"
135
+ HEADER_TXT = "header_txt"
136
+ BLOCK_ID = "block_id"
137
+
138
+ # parameters ordered by appearance in regex
139
+ def initialize(doc_mngr, context_filename, embed, link_type, filename, header_txt, block_id, label_txt)
140
+ @doc_mngr ||= doc_mngr
141
+ @context_filename ||= context_filename
142
+ @embed ||= embed
143
+ @link_type ||= link_type
144
+ @filename ||= filename
145
+ @header_txt ||= header_txt
146
+ @block_id ||= block_id
147
+ @label_txt ||= label_txt
148
+ end
149
+
150
+ # escape square brackets if they appear in label text
151
+ def label_txt
152
+ return @label_txt.sub("[", "\\[").sub("]", "\\]")
153
+ end
154
+
155
+ # data
156
+
157
+ def md_regex
158
+ regex_embed = embedded? ? REGEX_LINK_EMBED : %r{}
159
+ regex_link_type = is_typed? ? %r{#{@link_type}#{REGEX_LINK_TYPE}} : %r{}
160
+ filename = described?(FILENAME) ? @filename : ""
161
+ if described?(HEADER_TXT)
162
+ header = %r{#{REGEX_LINK_HEADER}#{@header_txt}}
163
+ block = %r{}
164
+ elsif described?(BLOCK_ID)
165
+ header = %r{}
166
+ block = %r{#{REGEX_LINK_BLOCK}#{@block_id}}
167
+ elsif !described?(FILENAME)
168
+ Jekyll.logger.error("Jekyll-Wikilinks: WikiLinkInline.md_regex error")
169
+ end
170
+ label_ = labelled? ? %r{#{REGEX_LINK_LABEL}#{label_txt}} : %r{}
171
+ return %r{#{regex_embed}#{regex_link_type}#{REGEX_LINK_LEFT}#{filename}#{header}#{block}#{label_}#{REGEX_LINK_RIGHT}}
172
+ end
173
+
174
+ def md_str
175
+ embed = embedded? ? "!" : ""
176
+ link_type = is_typed? ? "#{@link_type}::" : ""
177
+ filename = described?(FILENAME) ? @filename : ""
178
+ if described?(HEADER_TXT)
179
+ header = "\##{@header_txt}"
180
+ block = ""
181
+ elsif described?(BLOCK_ID)
182
+ header = ""
183
+ block = "\#\^#{@block_id}"
184
+ elsif !described?(FILENAME)
185
+ Jekyll.logger.error("Jekyll-Wikilinks: WikiLinkInline.md_str error")
186
+ end
187
+ label_ = labelled? ? "\|#{@label_txt}" : ""
188
+ return "#{embed}#{link_type}\[\[#{filename}#{header}#{block}#{label_}\]\]"
189
+ end
190
+
191
+ # 'fm' -> frontmatter
192
+
193
+ def context_fm_data
194
+ return {
195
+ 'type' => @link_type,
196
+ 'url' => self.context_doc.url,
197
+ }
198
+ end
199
+
200
+ def linked_fm_data
201
+ return {
202
+ 'type' => @link_type,
203
+ 'url' => self.linked_doc.url,
204
+ }
205
+ end
206
+
207
+ def context_doc
208
+ return @doc_mngr.get_doc_by_fname(@context_filename)
209
+ end
210
+
211
+ def linked_doc
212
+ return @doc_mngr.get_doc_by_fname(@filename)
213
+ end
214
+
215
+ def linked_img
216
+ return @doc_mngr.get_image_by_fname(@filename) if self.is_img?
217
+ return nil
218
+ end
219
+
220
+ # descriptor methods
221
+
222
+ # def describe
223
+ # return {
224
+ # 'level' => level,
225
+ # 'labelled' => labelled?,
226
+ # 'embedded' => embedded?,
227
+ # 'typed_link' => is_typed?,
228
+ # }
229
+ # end
230
+
231
+ def labelled?
232
+ return !@label_txt.nil? && !@label_txt.empty?
233
+ end
234
+
235
+ def is_typed?
236
+ return !@link_type.nil? && !@link_type.empty?
237
+ end
238
+
239
+ def embedded?
240
+ return !@embed.nil? && @embed == "!"
241
+ end
242
+
243
+ def is_img?
244
+ # github supported image formats: https://docs.github.com/en/github/managing-files-in-a-repository/working-with-non-code-files/rendering-and-diffing-images
245
+ return SUPPORTED_IMG_FORMATS.any?{ |ext| ext == File.extname(@filename).downcase }
246
+ end
247
+
248
+ def is_img_svg?
249
+ return File.extname(@filename).downcase == ".svg"
250
+ end
251
+
252
+ # this method helps to make the 'WikiLinkInline.level' code read like a clean truth table.
253
+ def described?(chunk)
254
+ return (!@filename.nil? && !@filename.empty?) if chunk == FILENAME
255
+ return (!@header_txt.nil? && !@header_txt.empty?) if chunk == HEADER_TXT
256
+ return (!@block_id.nil? && !@block_id.empty?) if chunk == BLOCK_ID
257
+ Jekyll.logger.error("Jekyll-Wikilinks: There is no link level '#{chunk}' in the WikiLink Class")
258
+ end
259
+
260
+ def level
261
+ return "file" if described?(FILENAME) && !described?(HEADER_TXT) && !described?(BLOCK_ID)
262
+ return "header" if described?(FILENAME) && described?(HEADER_TXT) && !described?(BLOCK_ID)
263
+ return "block" if described?(FILENAME) && !described?(HEADER_TXT) && described?(BLOCK_ID)
264
+ return "invalid"
265
+ end
266
+
267
+ # validation methods
268
+
269
+ def is_valid?
270
+ return false if !@doc_mngr.file_exists?(@filename)
271
+ return false if (self.level == "header") && !@doc_mngr.doc_has_header?(self.linked_doc, @header_txt)
272
+ return false if (self.level == "block") && !@doc_mngr.doc_has_block_id?(self.linked_doc, @block_id)
273
+ return true
274
+ end
275
+ end
276
+
277
+ end
278
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module JekyllWikiLinks
4
- VERSION = "0.0.5"
5
- end
3
+ module Jekyll
4
+ module WikiLinks
5
+
6
+ VERSION = "0.0.9"
7
+
8
+ end
9
+ end