gollum-lib 5.0.a.4 → 5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -1
  3. data/README.md +12 -7
  4. data/Rakefile +5 -5
  5. data/adapter_dependencies.rb +7 -0
  6. data/gemspec.rb +18 -10
  7. data/gollum-lib.gemspec +2 -6
  8. data/gollum-lib_java.gemspec +2 -2
  9. data/lib/gollum-lib.rb +9 -9
  10. data/lib/gollum-lib/blob_entry.rb +2 -8
  11. data/lib/gollum-lib/committer.rb +22 -60
  12. data/lib/gollum-lib/file.rb +105 -82
  13. data/lib/gollum-lib/file_view.rb +8 -4
  14. data/lib/gollum-lib/filter.rb +12 -0
  15. data/lib/gollum-lib/filter/code.rb +9 -13
  16. data/lib/gollum-lib/filter/critic_markup.rb +97 -0
  17. data/lib/gollum-lib/filter/emoji.rb +10 -8
  18. data/lib/gollum-lib/filter/macro.rb +5 -2
  19. data/lib/gollum-lib/filter/plantuml.rb +1 -1
  20. data/lib/gollum-lib/filter/remote_code.rb +3 -2
  21. data/lib/gollum-lib/filter/render.rb +25 -2
  22. data/lib/gollum-lib/filter/sanitize.rb +1 -8
  23. data/lib/gollum-lib/filter/tags.rb +57 -47
  24. data/lib/gollum-lib/filter/toc.rb +17 -21
  25. data/lib/gollum-lib/filter/yaml.rb +1 -1
  26. data/lib/gollum-lib/git_access.rb +0 -25
  27. data/lib/gollum-lib/helpers.rb +13 -3
  28. data/lib/gollum-lib/macro/audio.rb +9 -0
  29. data/lib/gollum-lib/macro/global_toc.rb +2 -1
  30. data/lib/gollum-lib/macro/navigation.rb +8 -6
  31. data/lib/gollum-lib/macro/note.rb +19 -0
  32. data/lib/gollum-lib/macro/octicon.rb +12 -0
  33. data/lib/gollum-lib/macro/warn.rb +11 -0
  34. data/lib/gollum-lib/markup.rb +17 -32
  35. data/lib/gollum-lib/markups.rb +11 -7
  36. data/lib/gollum-lib/page.rb +79 -165
  37. data/lib/gollum-lib/pagination.rb +7 -6
  38. data/lib/gollum-lib/redirects.rb +38 -0
  39. data/lib/gollum-lib/sanitization.rb +32 -357
  40. data/lib/gollum-lib/version.rb +1 -1
  41. data/lib/gollum-lib/wiki.rb +216 -404
  42. metadata +73 -28
  43. data/ROADMAP +0 -6
@@ -10,12 +10,13 @@ module Gollum
10
10
  # set pages to wiki.pages + wiki.files and show_all to true
11
11
  def initialize(pages, options = {})
12
12
  @pages = pages
13
+ @wiki = @pages.first ? @pages.first.wiki : nil
13
14
  @show_all = options[:show_all] || false
14
15
  @checked = options[:collapse_tree] ? '' : "checked"
15
16
  end
16
17
 
17
18
  def enclose_tree(string)
18
- %Q(<ol class="tree">\n) + string + %Q(</ol>)
19
+ sanitize_html(%Q(<ol class="tree">\n) + string + %Q(</ol>))
19
20
  end
20
21
 
21
22
  def new_page(page)
@@ -96,7 +97,7 @@ module Gollum
96
97
  </li>
97
98
  HTML
98
99
 
99
- return enclose_tree html
100
+ return enclose_tree(html)
100
101
  end
101
102
 
102
103
  sorted_folders = []
@@ -153,8 +154,11 @@ module Gollum
153
154
  changed = false
154
155
  end
155
156
 
156
- # return the completed html
157
- enclose_tree html
157
+ enclose_tree(html)
158
158
  end # end render_files
159
+
160
+ def sanitize_html(data)
161
+ @wiki ? @wiki.sanitizer.clean(data) : data
162
+ end
159
163
  end # end FileView class
160
164
  end # end Gollum module
@@ -47,14 +47,21 @@ module Gollum
47
47
  class Filter
48
48
  include Gollum::Helpers
49
49
 
50
+ PLACEHOLDER_PATTERN = /%(\S+)%.+=\1=/
51
+
50
52
  # Setup the object. Sets `@markup` to be the instance of Gollum::Markup that
51
53
  # is running this filter chain, and sets `@map` to be an empty hash (for use
52
54
  # in your extract/process operations).
55
+
53
56
  def initialize(markup)
54
57
  @markup = markup
55
58
  @map = {}
59
+ @open_pattern = "%#{self.class.to_s.split('::').last}%"
60
+ @close_pattern = "=#{self.class.to_s.split('::').last}="
56
61
  end
57
62
 
63
+ attr_reader :open_pattern, :close_pattern
64
+
58
65
  def extract(data)
59
66
  raise RuntimeError,
60
67
  "#{self.class} has not implemented ##extract!"
@@ -66,6 +73,11 @@ module Gollum
66
73
  end
67
74
 
68
75
  protected
76
+
77
+ def sanitize(data)
78
+ @markup.wiki.sanitizer.clean(data, @markup.historical)
79
+ end
80
+
69
81
  # Render a (presumably) non-fatal error as HTML
70
82
  #
71
83
  def html_error(message)
@@ -14,31 +14,27 @@ class Gollum::Filter::Code < Gollum::Filter
14
14
  org_headers = %r{([ \t]*#\+HEADER[S]?:[^\r\n]*\n)*}
15
15
  org_name = %r{([ \t]*#\+NAME:[^\r\n]*\n)?}
16
16
  org_lang = %r{[ ]*([^\n \r]*)[ ]*[^\r\n]*}
17
- org_begin = %r{[ \t]*#\+BEGIN_SRC#{org_lang}\n}
18
- org_end = %r{\n[ \t]*#\+END_SRC[ \t]*}
17
+ org_begin = %r{([ \t]*)#\+BEGIN_SRC#{org_lang}\r?\n}
18
+ org_end = %r{\r?\n[ \t]*#\+END_SRC[ \t\r]*}
19
19
  data.gsub!(/^#{org_headers}#{org_name}#{org_begin}(.+?)#{org_end}$/mi) do
20
- cache_codeblock(Regexp.last_match[3], Regexp.last_match[4])
20
+ "#{Regexp.last_match[3]}#{cache_codeblock(Regexp.last_match[4], Regexp.last_match[5])}"
21
21
  end
22
22
  when :markdown
23
- data.gsub!(/^([ \t]*)(~~~+) ?([^\r\n]+)?\r?\n(.+?)\r?\n\1(~~~+)[ \t\r]*$/m) do
23
+ data.gsub!(/^([ ]{0,3})(~~~+) ?([^\r\n]+)?\r?\n(.+?)\r?\n[ ]{0,3}(~~~+)[ \t\r]*$/m) do
24
24
  m_indent = Regexp.last_match[1]
25
25
  m_start = Regexp.last_match[2] # ~~~
26
26
  m_lang = Regexp.last_match[3]
27
27
  m_code = Regexp.last_match[4]
28
28
  m_end = Regexp.last_match[5] # ~~~
29
- # start and finish tilde fence must be the same length
30
- next '' if m_start.length != m_end.length
31
- lang = m_lang ? m_lang.strip : nil
32
- if lang
33
- lang = lang.match(/\.([^}\s]+)/)
34
- lang = lang[1] unless lang.nil?
35
- end
29
+ # The closing code fence must be at least as long as the opening fence
30
+ next '' if m_end.length < m_start.length
31
+ lang = m_lang ? m_lang.strip.split.first : nil
36
32
  "#{m_indent}#{cache_codeblock(lang, m_code, m_indent)}"
37
33
  end
38
34
  end
39
35
 
40
36
 
41
- data.gsub!(/^([ \t]*)``` ?([^\r\n]+)?\r?\n(.+?)\r?\n\1```[ \t]*\r?$/m) do
37
+ data.gsub!(/^([ ]{0,3})``` ?([^\r\n]+)?\r?\n(.+?)\r?\n[ ]{0,3}```[ \t]*\r?$/m) do
42
38
  "#{Regexp.last_match[1]}#{cache_codeblock(Regexp.last_match[2].to_s.strip, Regexp.last_match[3], Regexp.last_match[1])}" # print the SHA1 ID with the proper indentation
43
39
  end
44
40
  data
@@ -133,7 +129,7 @@ class Gollum::Filter::Code < Gollum::Filter
133
129
 
134
130
  def cache_codeblock(language, code, indent = "")
135
131
  language = language.to_s.empty? ? nil : language
136
- id = Digest::SHA1.hexdigest("#{language}.#{code}")
132
+ id = "#{open_pattern}#{Digest::SHA1.hexdigest("#{language}.#{code}")}#{close_pattern}"
137
133
  cached = @markup.check_cache(:code, id)
138
134
  @map[id] = cached ?
139
135
  { :output => cached } :
@@ -0,0 +1,97 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ # CriticMarkup
4
+ #
5
+ # Render CriticMarkup
6
+
7
+ class Gollum::Filter::CriticMarkup < Gollum::Filter
8
+
9
+ # Patterns inspired by https://github.com/DivineDominion/criticmarkup.tmbundle/blob/master/Syntaxes/criticmarkup.tmLanguage
10
+ # All patterns use multiline matching (m flag)
11
+ # Logic inspired by https://github.com/CriticMarkup/CriticMarkup-toolkit/blob/master/CLI/criticParser_CLI.py
12
+
13
+ ADDITION_PATTERN = %r|{\+\+(?<content>.*?)\+\+[ \t]*(\[(.*?)\])?[ \t]*\}|m
14
+ DELETION_PATTERN = %r|{--(?<content>.*?)--[ \t]*(\[(.*?)\])?[ \t]*\}|m
15
+ SUBSTITUTION_PATTERN = %r|{~~(?<oldcontent>.*?)~>(?<newcontent>.*?)~~}|m
16
+ HIGHLIGHT_PATTERN = %r|{\=\=(?<content>.*?)[ \t]*(\[(.*?)\])?[ \t]*\=\=\}{>>(?<comment>.*?)<<}|m
17
+ COMMENT_PATTERN = %r|{>>(?<content>.*?)<<}|m
18
+
19
+ def extract(data)
20
+ data.gsub! ADDITION_PATTERN do
21
+ content = $~[:content]
22
+ placeholder = generate_placeholder("#{content}#{@map.size}")
23
+ # Is there a new paragraph followed by new text
24
+ if content.start_with?("\n\n") && content != "\n\n"
25
+ html = "\n\n<ins class='critic break'>&nbsp;</ins>\n\n<ins>#{content.gsub('\n', ' ')}</ins>"
26
+ # Is the addition just a single new paragraph
27
+ elsif content == "\n\n"
28
+ html = "\n\n<ins class='critic break'>&nbsp;</ins>\n\n"
29
+ # Is it added text followed by a new paragraph?
30
+ elsif content.end_with?("\n\n") && content != "\n\n"
31
+ html = "<ins>#{content.gsub('\n', ' ')}</ins>\n\n<ins class='critic break'>&nbsp;</ins>\n\n"
32
+ else
33
+ html = "<ins>#{content.gsub('\n', ' ')}</ins>"
34
+ end
35
+ @map[placeholder] = html
36
+ placeholder
37
+ end
38
+
39
+ data.gsub! DELETION_PATTERN do
40
+ content = $~[:content]
41
+ placeholder = generate_placeholder("#{content}#{@map.size}")
42
+ if content == "\n\n"
43
+ html = "<del>&nbsp;</del>"
44
+ else
45
+ html = "<del>#{content.gsub('\n\n', ' ')}</del>"
46
+ end
47
+ @map[placeholder] = html
48
+ placeholder
49
+ end
50
+
51
+ data.gsub! SUBSTITUTION_PATTERN do
52
+ oldcontent = $~[:oldcontent]
53
+ newcontent = $~[:newcontent]
54
+ placeholder = generate_placeholder("#{oldcontent}#{newcontent}#{@map.size}")
55
+ html = "<del>#{oldcontent}</del><ins>#{newcontent}</ins>"
56
+ @map[placeholder] = html
57
+ placeholder
58
+ end
59
+
60
+ data.gsub! HIGHLIGHT_PATTERN do
61
+ content = $~[:content]
62
+ comment = $~[:comment]
63
+ placeholder = generate_placeholder("#{content}#{@map.size}")
64
+ html = "<mark>#{content}</mark><span class='critic comment'>#{comment}</span>"
65
+ @map[placeholder] = html
66
+ placeholder
67
+ end
68
+
69
+ data.gsub! COMMENT_PATTERN do
70
+ content = $~[:content]
71
+ placeholder = generate_placeholder("#{content}#{@map.size}")
72
+ html = "<span class='critic comment'>#{content}</span>"
73
+ @map[placeholder] = html
74
+ placeholder
75
+ end
76
+
77
+ data
78
+ end
79
+
80
+ def process(data)
81
+ data.gsub! process_pattern do
82
+ @map[$~[:placeholder]]
83
+ end
84
+ data
85
+ end
86
+
87
+ private
88
+
89
+ def process_pattern
90
+ /(?<placeholder>#{open_pattern}\h{40}#{close_pattern})/
91
+ end
92
+
93
+ def generate_placeholder(content)
94
+ "#{open_pattern}#{Digest::SHA1.hexdigest(content)}#{close_pattern}"
95
+ end
96
+
97
+ end
@@ -14,17 +14,11 @@ class Gollum::Filter::Emoji < Gollum::Filter
14
14
  (?!\]{^2})
15
15
  }ix
16
16
 
17
- PROCESS_PATTERN = %r{
18
- =EEMMOOJJII=
19
- (?<name>[\w-]+)
20
- =IIJJOOMMEE=
21
- }ix
22
-
23
17
  def extract(data)
24
18
  data.gsub! EXTRACT_PATTERN do
25
19
  case
26
20
  when $~[:escape] then $&[1..-1]
27
- when emoji_exists?($~[:name]) then "=EEMMOOJJII=#{$~[:name]}=IIJJOOMMEE="
21
+ when emoji_exists?($~[:name]) then "#{open_pattern}#{$~[:name]}#{close_pattern}"
28
22
  else $&
29
23
  end
30
24
  end
@@ -32,12 +26,20 @@ class Gollum::Filter::Emoji < Gollum::Filter
32
26
  end
33
27
 
34
28
  def process(data)
35
- data.gsub! PROCESS_PATTERN, %q(<img src="/emoji/\k<name>" alt="\k<name>" class="emoji">)
29
+ data.gsub! process_pattern, %q(<img src="/gollum/emoji/\k<name>" alt="\k<name>" class="emoji">)
36
30
  data
37
31
  end
38
32
 
39
33
  private
40
34
 
35
+ def process_pattern
36
+ %r{
37
+ #{open_pattern}
38
+ (?<name>[\w-]+)
39
+ #{close_pattern}
40
+ }ix
41
+ end
42
+
41
43
  def emoji_exists?(name)
42
44
  @index ||= Gemojione::Index.new
43
45
  !!@index.find_by_name(name)
@@ -1,4 +1,5 @@
1
1
  # ~*~ encoding: utf-8 ~*~
2
+ require 'octicons'
2
3
 
3
4
  # Replace specified tokens with dynamically generated content.
4
5
  class Gollum::Filter::Macro < Gollum::Filter
@@ -12,7 +13,7 @@ class Gollum::Filter::Macro < Gollum::Filter
12
13
 
13
14
  data.gsub(/('?)\<\<\s*([A-Z][A-Za-z0-9]*)\s*\(#{arg_list}\)\s*\>\>/) do
14
15
  next CGI.escape_html($&[1..-1]) unless Regexp.last_match[1].empty?
15
- id = Digest::SHA1.hexdigest(Regexp.last_match[2] + Regexp.last_match[3])
16
+ id = "#{open_pattern}#{Digest::SHA1.hexdigest(Regexp.last_match[2] + Regexp.last_match[3])}#{close_pattern}"
16
17
  macro = Regexp.last_match[2]
17
18
  argstr = Regexp.last_match[3]
18
19
  args = []
@@ -47,7 +48,9 @@ class Gollum::Filter::Macro < Gollum::Filter
47
48
  begin
48
49
  Gollum::Macro.instance(macro, @markup.wiki, @markup.page).render(*args)
49
50
  rescue StandardError => e
50
- "!!!Macro Error: #{e.message}!!!"
51
+ icon = Octicons::Octicon.new('zap', {width: 24, height: 24})
52
+ icon.options[:class] << ' mr-2'
53
+ "<div class='flash flash-error'>#{icon.to_svg}Macro Error for #{macro}: #{e.message}</div>"
51
54
  end
52
55
  end
53
56
  end
@@ -70,7 +70,7 @@ class Gollum::Filter::PlantUML < Gollum::Filter
70
70
  # placeholders.
71
71
  def extract(data)
72
72
  data.gsub(/(@startuml\r?\n.+?\r?\n@enduml\r?$)/m) do
73
- id = Digest::SHA1.hexdigest($1)
73
+ id = "#{open_pattern}#{Digest::SHA1.hexdigest($1)}#{close_pattern}"
74
74
  @map[id] = { :code => $1 }
75
75
  id
76
76
  end
@@ -20,7 +20,7 @@ class Gollum::Filter::RemoteCode < Gollum::Filter
20
20
 
21
21
  # Detect local file
22
22
  if protocol.nil?
23
- if (file = @markup.find_file(uri, @markup.wiki.ref))
23
+ if (file = @markup.wiki.file(uri, @markup.wiki.ref))
24
24
  contents = file.raw_data
25
25
  else
26
26
  # How do we communicate a render error?
@@ -45,7 +45,8 @@ class Gollum::Filter::RemoteCode < Gollum::Filter
45
45
  return "Too many redirects or retries" if cut >= 10
46
46
  http = Net::HTTP.new uri.host, uri.port
47
47
  http.use_ssl = true
48
- resp = http.get uri.path, {
48
+ path = uri.path.empty? ? '/' : uri.path
49
+ resp = http.get path, {
49
50
  'Accept' => 'text/plain',
50
51
  'Cache-Control' => 'no-cache',
51
52
  'Connection' => 'keep-alive',
@@ -3,7 +3,11 @@
3
3
  class Gollum::Filter::Render < Gollum::Filter
4
4
  def extract(data)
5
5
  begin
6
- data = GitHub::Markup.render(@markup.name, data)
6
+ working_dir = Pathname.new(@markup.wiki.path).join(@markup.dir)
7
+ working_dir = working_dir.exist? ? working_dir.to_s : '.'
8
+ Dir.chdir(working_dir) do
9
+ data = GitHub::Markup.render_s(@markup.format, data)
10
+ end
7
11
  if data.nil?
8
12
  raise "There was an error converting #{@markup.name} to HTML."
9
13
  end
@@ -15,6 +19,25 @@ class Gollum::Filter::Render < Gollum::Filter
15
19
  end
16
20
 
17
21
  def process(data)
22
+ data = add_editable_header_class(data)
18
23
  data
19
24
  end
20
- end
25
+
26
+ private
27
+
28
+ def add_editable_header_class(data)
29
+ doc = Nokogiri::HTML::DocumentFragment.parse(data)
30
+ doc.css('h1,h2,h3,h4,h5,h6').each_with_index do |header, i|
31
+ next if header.content.empty?
32
+ next if header.inner_html.match(PLACEHOLDER_PATTERN)
33
+ klass = header['class']
34
+ if klass
35
+ header['class'] = klass << ' editable'
36
+ else
37
+ header['class'] = 'editable'
38
+ end
39
+ end
40
+ doc.to_xml(@markup.class.to_xml_opts)
41
+ end
42
+
43
+ end
@@ -6,13 +6,6 @@ class Gollum::Filter::Sanitize < Gollum::Filter
6
6
  end
7
7
 
8
8
  def process(data)
9
- if @markup.sanitize
10
- doc = Nokogiri::HTML::DocumentFragment.parse(data)
11
- doc = @markup.sanitize.clean_node!(doc)
12
-
13
- doc.to_xml(@markup.to_xml_opts).gsub(/<p><\/p>/, '')
14
- else
15
- data
16
- end
9
+ sanitize(data)
17
10
  end
18
11
  end
@@ -34,7 +34,7 @@ class Gollum::Filter::Tags < Gollum::Filter
34
34
  doc.traverse do |node|
35
35
  if node.text? then
36
36
  content = node.content
37
- content.gsub!(/TAG[a-f0-9]+TAG/) do |id|
37
+ content.gsub!(%r{#{open_pattern}[a-f0-9]+#{close_pattern}}) do |id|
38
38
  if (tag = @map[id]) then
39
39
  if is_preformatted?(node) then
40
40
  "[[#{tag}]]"
@@ -56,7 +56,7 @@ class Gollum::Filter::Tags < Gollum::Filter
56
56
  INCLUDE_TAG = 'include:'
57
57
 
58
58
  def register_tag(tag)
59
- id = "TAG#{Digest::SHA1.hexdigest(tag)}TAG"
59
+ id = "#{open_pattern}#{Digest::SHA1.hexdigest(tag)}#{close_pattern}"
60
60
  @map[id] = tag
61
61
  id
62
62
  end
@@ -77,7 +77,6 @@ class Gollum::Filter::Tags < Gollum::Filter
77
77
  return generate_link('', nil, nil, :page_absent) if link_part.nil?
78
78
  img_args = extra ? [extra, link_part] : [link_part]
79
79
  mime = MIME::Types.type_for(::File.extname(img_args.first.to_s)).first
80
-
81
80
  result = if tag =~ /^_TOC_/
82
81
  %{[[#{tag}]]}
83
82
  elsif link_part =~ /^_$/
@@ -122,9 +121,9 @@ class Gollum::Filter::Tags < Gollum::Filter
122
121
  len = INCLUDE_TAG.length
123
122
  return html_error('Cannot process include directive: no page name given') if tag.length <= len
124
123
  page_name = tag[len..-1]
125
- resolved_page_name = ::File.expand_path(page_name, "#{::File::SEPARATOR}#{@markup.dir}")
124
+ resolved_page_name = ::File.join(@markup.dir, page_name)
126
125
  if @markup.include_levels > 0
127
- page = find_page_from_path(resolved_page_name)
126
+ page = find_page_or_file_from_path(resolved_page_name)
128
127
  if page
129
128
  page.formatted_data(@markup.encoding, @markup.include_levels-1)
130
129
  else
@@ -146,8 +145,8 @@ class Gollum::Filter::Tags < Gollum::Filter
146
145
  opts = parse_image_tag_options(options)
147
146
  if path =~ /^https?:\/\/.+$/i
148
147
  generate_image(path, opts)
149
- elsif file = @markup.find_file(path)
150
- generate_image(generate_href_for_path(file.path), opts)
148
+ elsif file = find_page_or_file_from_path(path, :file)
149
+ generate_image(generate_href_for_path(file.url_path), opts)
151
150
  else
152
151
  generate_image('', opts)
153
152
  end
@@ -173,14 +172,8 @@ class Gollum::Filter::Tags < Gollum::Filter
173
172
  # Return the String HTML if the tag is a valid external link tag or
174
173
  # nil if it is not.
175
174
  def process_external_link_tag(url, pretty_name = nil)
176
- accepted_protocols = @markup.wiki.sanitization.protocols['a']['href'].dup
177
- if accepted_protocols.include?(:relative)
178
- accepted_protocols.select!{|protocol| protocol != :relative}
179
- regexp = %r{^((#{accepted_protocols.join("|")}):)?(//)}
180
- else
181
- regexp = %r{^((#{accepted_protocols.join("|")}):)}
182
- end
183
- if url =~ regexp
175
+ @accepted_protocols_regex ||= %r{^((#{::Gollum::Sanitization.accepted_protocols.join('|')}):)?(//)}
176
+ if url =~ @accepted_protocols_regex
184
177
  generate_link(url, pretty_name, nil, :external)
185
178
  else
186
179
  nil
@@ -195,8 +188,9 @@ class Gollum::Filter::Tags < Gollum::Filter
195
188
  # Returns the String HTML if the tag is a valid file link tag or nil
196
189
  # if it is not.
197
190
  def process_file_link_tag(link_part, pretty_name)
198
- if file = @markup.find_file(link_part)
199
- generate_link(file.path, pretty_name, nil, :file)
191
+ return nil if ::Gollum::Page.valid_extension?(link_part)
192
+ if file = find_page_or_file_from_path(link_part, :file)
193
+ generate_link(file.url_path, pretty_name, nil, :file)
200
194
  else
201
195
  nil
202
196
  end
@@ -212,18 +206,31 @@ class Gollum::Filter::Tags < Gollum::Filter
212
206
  def process_page_link_tag(link_part, pretty_name = nil)
213
207
  presence = :page_absent
214
208
  link = link_part
215
- page = find_page_from_path(link)
209
+ page = find_page_or_file_from_path(link)
216
210
 
217
211
  # If no match yet, try finding page with anchor removed
218
- if (page.nil? && pos = link.rindex('#'))
219
- extra = link[pos..-1]
220
- link = link[0...pos]
221
- page = find_page_from_path(link)
222
- end
223
- presence = :page_present if page
212
+ if page.nil?
213
+ if pos = link.rindex('#')
214
+ extra = link[pos..-1]
215
+ link = link[0...pos]
216
+ else
217
+ extra = nil
218
+ end
219
+
220
+ if link.empty? && extra # Internal anchor link, don't search for the page but return immediately
221
+ return generate_link(nil, pretty_name, extra, :internal_anchor)
222
+ end
224
223
 
225
- name = pretty_name ? pretty_name : link
226
- link = page ? page.escaped_url_path : CGI.escape(link)
224
+ page = find_page_or_file_from_path(link)
225
+ end
226
+ presence = :page_present if page
227
+
228
+ if pretty_name
229
+ name = pretty_name
230
+ else
231
+ name = page ? path_to_link_text(link) : link
232
+ end
233
+ link = page ? page.escaped_url_path : ERB::Util.url_encode(link).force_encoding('utf-8')
227
234
  generate_link(link, name, extra, presence)
228
235
  end
229
236
 
@@ -232,15 +239,15 @@ class Gollum::Filter::Tags < Gollum::Filter
232
239
  # path - The String path to search for.
233
240
  #
234
241
  # Returns a Gollum::Page instance if a page is found, or nil otherwise
235
- def find_page_from_path(path)
236
- slash = path.rindex('/')
237
-
238
- unless slash.nil?
239
- name = path[slash+1..-1]
240
- path = path[0..slash]
241
- @markup.wiki.paged(name, path)
242
+ def find_page_or_file_from_path(path, kind = :page)
243
+ if Pathname.new(path).relative?
244
+ result = @markup.wiki.send(kind, ::File.join(@markup.dir, path))
245
+ if result.nil? && @markup.wiki.global_tag_lookup # 4.x link compatibility option. Slow!
246
+ result = @markup.wiki.send(kind, path, nil, true)
247
+ end
248
+ result
242
249
  else
243
- @markup.wiki.page(path)
250
+ @markup.wiki.send(kind, path)
244
251
  end
245
252
  end
246
253
 
@@ -264,7 +271,8 @@ class Gollum::Filter::Tags < Gollum::Filter
264
271
  #
265
272
  # Returns a String href.
266
273
  def generate_href_for_path(path, extra = nil)
267
- "#{trim_leading_slash(::File.join(@markup.wiki.base_path, path))}#{extra}"
274
+ return extra if !path && extra # Internal anchor link
275
+ "#{trim_leading_slashes(::File.join(@markup.wiki.base_path, path))}#{extra}"
268
276
  end
269
277
 
270
278
  # Construct a CSS class and attribute string for different kinds of links: internal Pages (absent or present) and Files, and External links.
@@ -278,6 +286,8 @@ class Gollum::Filter::Tags < Gollum::Filter
278
286
  'class="internal absent"'
279
287
  when :page_present
280
288
  'class="internal present"'
289
+ when :internal_anchor
290
+ 'class="internal anchorlink"'
281
291
  when :file
282
292
  nil
283
293
  when :external
@@ -299,10 +309,10 @@ class Gollum::Filter::Tags < Gollum::Filter
299
309
  attr_string = attrs.map {|key, value| "#{key}=\"#{value}\""}.join(' ')
300
310
 
301
311
  if containered
302
- %{<span class="#{classes.join(' ')}">} +
303
- %{<span>} +
312
+ %{<span class="d-flex #{classes[:container].join(' ')}">} +
313
+ %{<span class="#{classes[:nested].join(' ')}">} +
304
314
  %{<img src="#{path}" #{attr_string}/>} +
305
- (attrs[:alt] ? %{<span>#{attrs[:alt]}</span>} : '') +
315
+ (options[:frame] && attrs[:alt] ? %{<span class="clearfix">#{attrs[:alt]}</span>} : '') +
306
316
  %{</span>} +
307
317
  %{</span>}
308
318
  else
@@ -314,31 +324,31 @@ class Gollum::Filter::Tags < Gollum::Filter
314
324
  #
315
325
  # options - The Hash of parsed image options.
316
326
  #
317
- # Returns an Array of CSS classes, a Hash of CSS attributes, and a Boolean indicating whether or not the image is containered.
327
+ # Returns a Hash containing CSS class Arrays, a Hash of CSS attributes, and a Boolean indicating whether or not the image is containered.
318
328
  def generate_image_attributes(options)
319
329
  containered = false
320
- classes = [] # applied to whatever the outermost container is
330
+ classes = {container: [], nested: []} # applied to the container(s)
321
331
  attrs = {} # applied to the image
322
332
 
323
333
  align = options[:align]
324
334
  if options[:float]
325
335
  containered = true
326
- align ||= 'left'
327
- if %w{left right}.include?(align)
328
- classes << "float-#{align}"
329
- end
336
+ align = 'left' unless align == 'right'
337
+ classes[:container] << "float-#{align} pb-4"
330
338
  elsif %w{top texttop middle absmiddle bottom absbottom baseline}.include?(align)
331
339
  attrs[:align] = align
332
340
  elsif align
333
341
  if %w{left center right}.include?(align)
334
342
  containered = true
335
- classes << "align-#{align}"
343
+ text_align = "text-#{align}"
344
+ align = 'end' if align == 'right'
345
+ classes[:container] << "flex-justify-#{align} #{text_align}"
336
346
  end
337
347
  end
338
348
 
339
- if options[:frame]
349
+ if options[:frame]
340
350
  containered = true
341
- classes << 'frame'
351
+ classes[:nested] << 'border p-4'
342
352
  end
343
353
 
344
354
  attrs[:alt] = options[:alt] if options[:alt]