gitlab-gollum-lib 1.1.0 → 4.2.7

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -3
  3. data/HISTORY.md +25 -0
  4. data/LICENSE +1 -1
  5. data/README.md +24 -312
  6. data/Rakefile +32 -16
  7. data/gemspec.rb +110 -0
  8. data/gollum-lib.gemspec +8 -75
  9. data/gollum-lib_java.gemspec +4 -0
  10. data/lib/gollum-lib.rb +18 -6
  11. data/lib/gollum-lib/blob_entry.rb +10 -9
  12. data/lib/gollum-lib/committer.rb +37 -30
  13. data/lib/gollum-lib/file.rb +71 -15
  14. data/lib/gollum-lib/file_view.rb +53 -48
  15. data/lib/gollum-lib/filter.rb +78 -0
  16. data/lib/gollum-lib/filter/code.rb +145 -0
  17. data/lib/gollum-lib/filter/emoji.rb +39 -0
  18. data/lib/gollum-lib/filter/macro.rb +57 -0
  19. data/lib/gollum-lib/filter/metadata.rb +29 -0
  20. data/lib/gollum-lib/filter/plain_text.rb +16 -0
  21. data/lib/gollum-lib/filter/plantuml.rb +176 -0
  22. data/lib/gollum-lib/filter/remote_code.rb +63 -0
  23. data/lib/gollum-lib/filter/render.rb +20 -0
  24. data/lib/gollum-lib/filter/sanitize.rb +18 -0
  25. data/lib/gollum-lib/filter/tags.rb +327 -0
  26. data/lib/gollum-lib/filter/toc.rb +134 -0
  27. data/lib/gollum-lib/filter/wsd.rb +54 -0
  28. data/lib/gollum-lib/git_access.rb +30 -32
  29. data/lib/gollum-lib/gitcode.rb +16 -16
  30. data/lib/gollum-lib/helpers.rb +3 -3
  31. data/lib/gollum-lib/hook.rb +35 -0
  32. data/lib/gollum-lib/macro.rb +43 -0
  33. data/lib/gollum-lib/macro/all_pages.rb +11 -0
  34. data/lib/gollum-lib/macro/global_toc.rb +12 -0
  35. data/lib/gollum-lib/macro/navigation.rb +20 -0
  36. data/lib/gollum-lib/macro/series.rb +48 -0
  37. data/lib/gollum-lib/markup.rb +95 -572
  38. data/lib/gollum-lib/markups.rb +9 -3
  39. data/lib/gollum-lib/page.rb +109 -80
  40. data/lib/gollum-lib/pagination.rb +1 -1
  41. data/lib/gollum-lib/sanitization.rb +75 -75
  42. data/lib/gollum-lib/version.rb +5 -0
  43. data/lib/gollum-lib/wiki.rb +287 -129
  44. metadata +237 -92
  45. data/CHANGELOG +0 -2
  46. data/VERSION +0 -1
  47. data/lib/gollum-lib/grit_ext.rb +0 -20
  48. data/lib/gollum-lib/remote_code.rb +0 -39
  49. data/lib/gollum-lib/web_sequence_diagram.rb +0 -44
@@ -0,0 +1,39 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ # Emoji
4
+ #
5
+ # Render emoji such as :smile:
6
+ class Gollum::Filter::Emoji < Gollum::Filter
7
+
8
+ EXTRACT_PATTERN = %r{
9
+ (?<!\[{2})
10
+ :(?<name>[\w-]+):
11
+ (?!\]{^2})
12
+ }ix
13
+
14
+ PROCESS_PATTERN = %r{
15
+ =EEMMOOJJII=
16
+ (?<name>[\w-]+)
17
+ =IIJJOOMMEE=
18
+ }ix
19
+
20
+ def extract(data)
21
+ data.gsub! EXTRACT_PATTERN do
22
+ emoji_exists?($~[:name]) ? "=EEMMOOJJII=#{$~[:name]}=IIJJOOMMEE=" : $&
23
+ end
24
+ data
25
+ end
26
+
27
+ def process(data)
28
+ data.gsub! PROCESS_PATTERN, %q(<img src="/emoji/\k<name>" alt="\k<name>" class="emoji">)
29
+ data
30
+ end
31
+
32
+ private
33
+
34
+ def emoji_exists?(name)
35
+ @index ||= Gemojione::Index.new
36
+ !!@index.find_by_name(name)
37
+ end
38
+
39
+ end
@@ -0,0 +1,57 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ # Replace specified tokens with dynamically generated content.
4
+ class Gollum::Filter::Macro < Gollum::Filter
5
+ def extract(data)
6
+ quoted_arg = %r{".*?"}
7
+ unquoted_arg = %r{[^,)]+}
8
+ named_arg = %r{[a-z0-9_]+=".*?"}
9
+
10
+ arg = %r{(?:#{quoted_arg}|#{unquoted_arg}|#{named_arg})}
11
+ arg_list = %r{(\s*|#{arg}(?:\s*,\s*#{arg})*)}
12
+
13
+ data.gsub(/('?)\<\<\s*([A-Z][A-Za-z0-9]*)\s*\(#{arg_list}\)\s*\>\>/) do
14
+ 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
+ macro = Regexp.last_match[2]
17
+ argstr = Regexp.last_match[3]
18
+ args = []
19
+ opts = {}
20
+
21
+ argstr.scan(/,?\s*(#{arg})\s*/) do |arguments|
22
+ # Stabstabstab
23
+ argument = arguments.first
24
+
25
+ if argument =~ /^([a-z0-9_]+)="(.*?)"/
26
+ opts[Regexp.last_match[1]] = Regexp.last_match[2]
27
+ elsif argument =~ /^"(.*)"$/
28
+ args << Regexp.last_match[1]
29
+ else
30
+ args << argument
31
+ end
32
+ end
33
+
34
+ args << opts unless opts.empty?
35
+
36
+ @map[id] = { :macro => macro, :args => args }
37
+ id
38
+ end
39
+ end
40
+
41
+ def process(data)
42
+ @map.each do |id, spec|
43
+ macro = spec[:macro]
44
+ args = spec[:args]
45
+
46
+ data.gsub!(id) do
47
+ begin
48
+ Gollum::Macro.instance(macro, @markup.wiki, @markup.page).render(*args)
49
+ rescue StandardError => e
50
+ "!!!Macro Error: #{e.message}!!!"
51
+ end
52
+ end
53
+ end
54
+
55
+ data
56
+ end
57
+ end
@@ -0,0 +1,29 @@
1
+ # Extract metadata for data and build metadata table. Metadata consists of
2
+ # key/value pairs in "key:value" format found between markers. Each
3
+ # key/value pair must be on its own line. Internal whitespace in keys and
4
+ # values is preserved, but external whitespace is ignored.
5
+ #
6
+ # Because ri and ruby 1.8.7 are awesome, the markers can't
7
+ # be included in this documentation without triggering
8
+ # `Unhandled special: Special: type=17`
9
+ # Please read the source code for the exact markers
10
+ class Gollum::Filter::Metadata < Gollum::Filter
11
+ def extract(data)
12
+ # The markers are `<!-- ---` and `-->`
13
+ data.gsub(/\<\!--+\s+---(.*?)--+\>/m) do
14
+ @markup.metadata ||= {}
15
+ # Split untrusted input on newlines, then remove bits that look like
16
+ # HTML elements before parsing each line.
17
+ Regexp.last_match[1].split("\n").each do |line|
18
+ line.gsub!(/<[^>]*>/, '')
19
+ k, v = line.split(':', 2)
20
+ @markup.metadata[k.strip] = (v ? v.strip : '') if k
21
+ end
22
+ ''
23
+ end
24
+ end
25
+
26
+ def process(data)
27
+ data
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ # Plain Text
4
+ #
5
+ # Render plain text documents in a <pre> block without any special markup.
6
+
7
+ class Gollum::Filter::PlainText < Gollum::Filter
8
+
9
+ def extract(data)
10
+ @markup.format == :txt ? "<pre>#{CGI.escapeHTML(data)}</pre>" : data
11
+ end
12
+
13
+ def process(data)
14
+ data
15
+ end
16
+ end
@@ -0,0 +1,176 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'open-uri'
5
+ require 'zlib'
6
+
7
+ # PlantUML Diagrams
8
+ #
9
+ # This filter replaces PlantUML blocks with HTML img tags. These img tags
10
+ # point to a PlantUML web service that converts the UML text blocks into nice
11
+ # diagrams.
12
+ #
13
+ # For this to work you must have your own PlantUML server running somewhere.
14
+ # Just follow the instructions on the github page to run your own server:
15
+ #
16
+ # https://github.com/plantuml/plantuml-server
17
+ #
18
+ # Once you start you own plantuml server you need to configure this filter to
19
+ # point to it:
20
+ #
21
+ # Gollum::Filter::PlantUML.configure do |config|
22
+ # config.url = "http://localhost:8080/plantuml/png"
23
+ # end
24
+ #
25
+ # Then in your wiki pages simply add PlantUML blocks anywhere you want a
26
+ # diagram:
27
+ #
28
+ # @startuml
29
+ # Alice -> Bob: Authentication Request
30
+ # Bob --> Alice: Authentication Response
31
+ # Alice -> Bob: Another authentication Request
32
+ # Alice <-- Bob: another authentication Response
33
+ # @enduml
34
+ #
35
+ # To learn more about how to create cool PlantUML diagrams check the examples
36
+ # at: http://plantuml.sourceforge.net/
37
+ #
38
+ class Gollum::Filter::PlantUML < Gollum::Filter
39
+
40
+ DEFAULT_URL = "http://localhost:8080/plantuml/png"
41
+
42
+ # Configuration class used to change the behaviour of the PlatnUML filter.
43
+ #
44
+ # url: PlantUML server URL (e.g. http://localhost:8080)
45
+ # test: Set to true when running tests to skip the server check.
46
+ #
47
+ class Configuration
48
+ attr_accessor :url, :test, :verify_ssl
49
+
50
+ def initialize
51
+ @url = DEFAULT_URL
52
+ @verify_ssl = true
53
+ @test = false
54
+ end
55
+ end
56
+
57
+ class << self
58
+ attr_writer :configuration
59
+ end
60
+
61
+ def self.configuration
62
+ @configuration ||= Configuration.new
63
+ end
64
+
65
+ def self.configure
66
+ yield(configuration)
67
+ end
68
+
69
+ # Extract all sequence diagram blocks into the map and replace with
70
+ # placeholders.
71
+ def extract(data)
72
+ return data if @markup.format == :txt
73
+ data.gsub(/(@startuml\r?\n.+?\r?\n@enduml\r?$)/m) do
74
+ id = Digest::SHA1.hexdigest($1)
75
+ @map[id] = { :code => $1 }
76
+ id
77
+ end
78
+ end
79
+
80
+ # Process all diagrams from the map and replace the placeholders with
81
+ # the final HTML.
82
+ def process(data)
83
+ @map.each do |id, spec|
84
+ data.gsub!(id) do
85
+ render_plantuml(id, spec[:code])
86
+ end
87
+ end
88
+ data
89
+ end
90
+
91
+ private
92
+
93
+ def server_url
94
+ PlantUML::configuration.url
95
+ end
96
+
97
+ def test?
98
+ PlantUML::configuration.test
99
+ end
100
+
101
+ def verify_ssl?
102
+ PlantUML::configuration.verify_ssl
103
+ end
104
+
105
+ def render_plantuml(id, code)
106
+ if check_server
107
+ plantuml_url = gen_url(code)
108
+ "<img src=\"#{gen_url(code)}\" />"
109
+ else
110
+ html_error("Sorry, unable to render PlantUML diagram at this time")
111
+ end
112
+ end
113
+
114
+ # Compression code used to generate PlantUML URLs. Taken directly from the
115
+ # Transcoder class in the PlantUML java code.
116
+ def gen_url(text)
117
+ result = ""
118
+ compressedData = Zlib::Deflate.deflate(text)
119
+ compressedData.chars.each_slice(3) do |bytes|
120
+ #print bytes[0], ' ' , bytes[1] , ' ' , bytes[2]
121
+ b1 = bytes[0].nil? ? 0 : (bytes[0].ord & 0xFF)
122
+ b2 = bytes[1].nil? ? 0 : (bytes[1].ord & 0xFF)
123
+ b3 = bytes[2].nil? ? 0 : (bytes[2].ord & 0xFF)
124
+ result += append3bytes(b1, b2, b3)
125
+ end
126
+ "#{server_url}/#{result}"
127
+ end
128
+
129
+ def encode6bit(b)
130
+ if b < 10
131
+ return ('0'.ord + b).chr
132
+ end
133
+ b = b - 10
134
+ if b < 26
135
+ return ('A'.ord + b).chr
136
+ end
137
+ b = b - 26
138
+ if b < 26
139
+ return ('a'.ord + b).chr
140
+ end
141
+ b = b - 26
142
+ if b == 0
143
+ return '-'
144
+ end
145
+ if b == 1
146
+ return '_'
147
+ end
148
+ return '?'
149
+ end
150
+
151
+ def append3bytes(b1, b2, b3)
152
+ c1 = b1 >> 2
153
+ c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
154
+ c3 = ((b2 & 0xF) << 2) | (b3 >> 6)
155
+ c4 = b3 & 0x3F
156
+ return encode6bit(c1 & 0x3F).chr +
157
+ encode6bit(c2 & 0x3F).chr +
158
+ encode6bit(c3 & 0x3F).chr +
159
+ encode6bit(c4 & 0x3F).chr
160
+ end
161
+
162
+ # Make a call to the PlantUML server with the simplest diagram possible to
163
+ # check if the server is available or not.
164
+ def check_server
165
+ return true if test?
166
+ check_url = "#{server_url}/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000"
167
+ uri = URI.parse(check_url)
168
+ http = Net::HTTP.new(uri.host, uri.port)
169
+ http.use_ssl = true if uri.scheme == 'https'
170
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless verify_ssl?
171
+ response = http.request_get(uri.request_uri)
172
+ return response.is_a?(Net::HTTPSuccess)
173
+ rescue
174
+ return false
175
+ end
176
+ end
@@ -0,0 +1,63 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'net/http'
3
+ require 'net/https' # ruby 1.8.7 fix, remove at upgrade
4
+ require 'uri'
5
+ require 'open-uri'
6
+
7
+ # Remote code - fetch code from url and replace the contents to a
8
+ # code-block that gets run the next parse.
9
+ # Acceptable formats:
10
+ # ```language:local-file.ext```
11
+ # ```language:/abs/other-file.ext```
12
+ # ```language:https://example.com/somefile.txt```
13
+ #
14
+ class Gollum::Filter::RemoteCode < Gollum::Filter
15
+ def extract(data)
16
+ return data if @markup.format == :txt
17
+ data.gsub(/^[ \t]*``` ?([^:\n\r]+):((http)?[^`\n\r]+)```/) do
18
+ language = Regexp.last_match[1]
19
+ uri = Regexp.last_match[2]
20
+ protocol = Regexp.last_match[3]
21
+
22
+ # Detect local file
23
+ if protocol.nil?
24
+ if (file = @markup.find_file(uri, @markup.wiki.ref))
25
+ contents = file.raw_data
26
+ else
27
+ # How do we communicate a render error?
28
+ next html_error("File not found: #{CGI::escapeHTML(uri)}")
29
+ end
30
+ else
31
+ contents = req(uri)
32
+ end
33
+
34
+ "```#{language}\n#{contents}\n```\n"
35
+ end
36
+ end
37
+
38
+ def process(data)
39
+ data
40
+ end
41
+
42
+ private
43
+
44
+ def req(uri, cut = 1)
45
+ uri = URI(uri)
46
+ return "Too many redirects or retries" if cut >= 10
47
+ http = Net::HTTP.new uri.host, uri.port
48
+ http.use_ssl = true
49
+ resp = http.get uri.path, {
50
+ 'Accept' => 'text/plain',
51
+ 'Cache-Control' => 'no-cache',
52
+ 'Connection' => 'keep-alive',
53
+ 'User-Agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0'
54
+ }
55
+ code = resp.code.to_i
56
+ return resp.body if code == 200
57
+ return "Not Found" if code == 404
58
+ return "Unhandled Response Code #{code}" unless code == 304 or not resp.header['location'].nil?
59
+ loc = URI.parse resp.header['location']
60
+ uri2 = loc.relative? ? (uri + loc) : loc # overloads (+)
61
+ req uri2, (cut + 1)
62
+ end
63
+ end
@@ -0,0 +1,20 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ class Gollum::Filter::Render < Gollum::Filter
4
+ def extract(data)
5
+ begin
6
+ data = GitHub::Markup.render(@markup.name, data)
7
+ if data.nil?
8
+ raise "There was an error converting #{@markup.name} to HTML."
9
+ end
10
+ rescue Object => e
11
+ data = html_error("Failed to render page: #{e.message}")
12
+ end
13
+
14
+ data
15
+ end
16
+
17
+ def process(data)
18
+ data
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ class Gollum::Filter::Sanitize < Gollum::Filter
4
+ def extract(data)
5
+ data
6
+ end
7
+
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)
14
+ else
15
+ data
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,327 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ # Render all tags (things in double-square-brackets). This one's a biggie.
4
+ class Gollum::Filter::Tags < Gollum::Filter
5
+ # Extract all tags into the tagmap and replace with placeholders.
6
+ def extract(data)
7
+ return data if @markup.format == :txt || @markup.format == :asciidoc
8
+ data.gsub!(/(.?)\[\[(.+?)\]\]([^\[]?)/m) do
9
+ if Regexp.last_match[1] == "'" && Regexp.last_match[3] != "'"
10
+ "[[#{Regexp.last_match[2]}]]#{Regexp.last_match[3]}"
11
+ elsif Regexp.last_match[2].include?('][')
12
+ if Regexp.last_match[2][0..4] == 'file:'
13
+ pre = Regexp.last_match[1]
14
+ post = Regexp.last_match[3]
15
+ parts = Regexp.last_match[2].split('][')
16
+ parts[0][0..4] = ""
17
+ link = "#{parts[1]}|#{parts[0].sub(/\.org/, '')}"
18
+ id = register_tag(link)
19
+ "#{pre}#{id}#{post}"
20
+ else
21
+ Regexp.last_match[0]
22
+ end
23
+ else
24
+ id = register_tag(Regexp.last_match[2])
25
+ "#{Regexp.last_match[1]}#{id}#{Regexp.last_match[3]}"
26
+ end
27
+ end
28
+ data
29
+ end
30
+
31
+ def register_tag(tag)
32
+ id = "TAG#{Digest::SHA1.hexdigest(tag)}TAG"
33
+ @map[id] = tag
34
+ id
35
+ end
36
+
37
+ # Process all text nodes from the doc and replace the placeholders with the
38
+ # final markup.
39
+ def process(rendered_data)
40
+ doc = Nokogiri::HTML::DocumentFragment.parse(rendered_data)
41
+ doc.traverse do |node|
42
+ if node.text? then
43
+ content = node.content
44
+ content.gsub!(/TAG[a-f0-9]+TAG/) do |id|
45
+ if (tag = @map[id]) then
46
+ if is_preformatted?(node) then
47
+ "[[#{tag}]]"
48
+ else
49
+ process_tag(tag).gsub('%2f', '/')
50
+ end
51
+ end
52
+ end
53
+ node.replace(content) if content != node.content
54
+ end
55
+ end
56
+
57
+ doc.to_html
58
+ end
59
+
60
+ private
61
+
62
+ PREFORMATTED_TAGS = %w(code tt)
63
+
64
+ def is_preformatted?(node)
65
+ node && (PREFORMATTED_TAGS.include?(node.name) ||
66
+ node.ancestors.any? { |a| PREFORMATTED_TAGS.include?(a.name) })
67
+ end
68
+
69
+ # Process a single tag into its final HTML form.
70
+ #
71
+ # tag - The String tag contents (the stuff inside the double
72
+ # brackets).
73
+ #
74
+ # Returns the String HTML version of the tag.
75
+ def process_tag(tag)
76
+ if tag =~ /^_TOC_/
77
+ %{[[#{tag}]]}
78
+ elsif tag =~ /^_$/
79
+ %{<div class="clearfloats"></div>}
80
+ elsif (html = process_include_tag(tag))
81
+ html
82
+ elsif (html = process_image_tag(tag))
83
+ html
84
+ elsif (html = process_external_link_tag(tag))
85
+ html
86
+ elsif (html = process_file_link_tag(tag))
87
+ html
88
+ else
89
+ process_page_link_tag(tag)
90
+ end
91
+ end
92
+
93
+ # Attempt to process the tag as an include tag
94
+ #
95
+ # tag - The String tag contents (the stuff inside the double brackets).
96
+ #
97
+ # Returns the String HTML if the tag is a valid image tag or nil
98
+ # if it is not.
99
+ #
100
+ def process_include_tag(tag)
101
+ return unless /^include:/.match(tag)
102
+ page_name = tag[8..-1]
103
+ resolved_page_name = ::File.expand_path(page_name, "/"+@markup.dir)
104
+
105
+ if @markup.include_levels > 0
106
+ page = find_page_from_name(resolved_page_name)
107
+ if page
108
+ page.formatted_data(@markup.encoding, @markup.include_levels-1)
109
+ else
110
+ html_error("Cannot include #{process_page_link_tag(resolved_page_name)} - does not exist yet")
111
+ end
112
+ else
113
+ html_error("Too many levels of included pages, will not include #{process_page_link_tag(resolved_page_name)}")
114
+ end
115
+ end
116
+
117
+ # Attempt to process the tag as an image tag.
118
+ #
119
+ # tag - The String tag contents (the stuff inside the double brackets).
120
+ #
121
+ # Returns the String HTML if the tag is a valid image tag or nil
122
+ # if it is not.
123
+ def process_image_tag(tag)
124
+ parts = tag.split('|')
125
+ return if parts.size.zero?
126
+
127
+ name = parts[0].strip
128
+ if (file = @markup.find_file(name))
129
+ path = ::File.join @markup.wiki.base_path, file.path
130
+ elsif name =~ /^https?:\/\/.+(jpg|png|gif|svg|bmp)$/i
131
+ path = name
132
+ elsif name =~ /.+(jpg|png|gif|svg|bmp)$/i
133
+ # If is image, file not found and no link, then populate with empty String
134
+ # We can than add an image not found alt attribute for this later
135
+ path = ""
136
+ end
137
+
138
+ if path
139
+ opts = parse_image_tag_options(tag)
140
+
141
+ containered = false
142
+
143
+ classes = [] # applied to whatever the outermost container is
144
+ attrs = [] # applied to the image
145
+
146
+ align = opts['align']
147
+ if opts['float']
148
+ containered = true
149
+ align ||= 'left'
150
+ if %w{left right}.include?(align)
151
+ classes << "float-#{align}"
152
+ end
153
+ elsif %w{top texttop middle absmiddle bottom absbottom baseline}.include?(align)
154
+ attrs << %{align="#{align}"}
155
+ elsif align
156
+ if %w{left center right}.include?(align)
157
+ containered = true
158
+ classes << "align-#{align}"
159
+ end
160
+ end
161
+
162
+ if (width = opts['width'])
163
+ if width =~ /^\d+(\.\d+)?(em|px)$/
164
+ attrs << %{width="#{width}"}
165
+ end
166
+ end
167
+
168
+ if (height = opts['height'])
169
+ if height =~ /^\d+(\.\d+)?(em|px)$/
170
+ attrs << %{height="#{height}"}
171
+ end
172
+ end
173
+
174
+ if path != "" && (alt = opts['alt'])
175
+ attrs << %{alt="#{alt}"}
176
+ elsif path == ""
177
+ attrs << %{alt="Image not found"}
178
+ end
179
+
180
+ attr_string = attrs.size > 0 ? attrs.join(' ') + ' ' : ''
181
+
182
+ if opts['frame'] || containered
183
+ classes << 'frame' if opts['frame']
184
+ %{<span class="#{classes.join(' ')}">} +
185
+ %{<span>} +
186
+ %{<img src="#{path}" #{attr_string}/>} +
187
+ (alt ? %{<span>#{alt}</span>} : '') +
188
+ %{</span>} +
189
+ %{</span>}
190
+ else
191
+ %{<img src="#{path}" #{attr_string}/>}
192
+ end
193
+ end
194
+ end
195
+
196
+ # Parse any options present on the image tag and extract them into a
197
+ # Hash of option names and values.
198
+ #
199
+ # tag - The String tag contents (the stuff inside the double brackets).
200
+ #
201
+ # Returns the options Hash:
202
+ # key - The String option name.
203
+ # val - The String option value or true if it is a binary option.
204
+ def parse_image_tag_options(tag)
205
+ tag.split('|')[1..-1].inject({}) do |memo, attr|
206
+ parts = attr.split('=').map { |x| x.strip }
207
+ memo[parts[0]] = (parts.size == 1 ? true : parts[1])
208
+ memo
209
+ end
210
+ end
211
+
212
+ # Return the String HTML if the tag is a valid external link tag or
213
+ # nil if it is not.
214
+ def process_external_link_tag(tag)
215
+ parts = tag.split('|')
216
+ parts.reverse! if @markup.reverse_links?
217
+ return if parts.size.zero?
218
+ if parts.size == 1
219
+ url = parts[0].strip
220
+ else
221
+ name, url = *parts.compact.map(&:strip)
222
+ end
223
+ accepted_protocols = @markup.wiki.sanitization.protocols['a']['href'].dup
224
+ if accepted_protocols.include?(:relative)
225
+ accepted_protocols.select!{|protocol| protocol != :relative}
226
+ regexp = %r{^((#{accepted_protocols.join("|")}):)?(//)}
227
+ else
228
+ regexp = %r{^((#{accepted_protocols.join("|")}):)}
229
+ end
230
+ if url =~ regexp
231
+ if name.nil?
232
+ %{<a href="#{url}">#{url}</a>}
233
+ else
234
+ %{<a href="#{url}">#{name}</a>}
235
+ end
236
+ else
237
+ nil
238
+ end
239
+
240
+ end
241
+
242
+ # Attempt to process the tag as a file link tag.
243
+ #
244
+ # tag - The String tag contents (the stuff inside the double
245
+ # brackets).
246
+ #
247
+ # Returns the String HTML if the tag is a valid file link tag or nil
248
+ # if it is not.
249
+ def process_file_link_tag(tag)
250
+ parts = tag.split('|')
251
+ return if parts.size.zero?
252
+
253
+ name = parts[0].strip
254
+ path = parts[1] && parts[1].strip
255
+ if path && file = @markup.find_file(path)
256
+ path = ::File.join @markup.wiki.base_path, file.path
257
+ else
258
+ path = nil
259
+ end
260
+
261
+ if name && path && file
262
+ %{<a href="#{::File.join @markup.wiki.base_path, file.path}">#{name}</a>}
263
+ elsif name && path
264
+ %{<a href="#{path}">#{name}</a>}
265
+ else
266
+ nil
267
+ end
268
+ end
269
+
270
+ # Attempt to process the tag as a page link tag.
271
+ #
272
+ # tag - The String tag contents (the stuff inside the double
273
+ # brackets).
274
+ #
275
+ # Returns the String HTML if the tag is a valid page link tag or nil
276
+ # if it is not.
277
+ def process_page_link_tag(tag)
278
+ parts = tag.split('|')
279
+ parts.reverse! if @markup.reverse_links?
280
+
281
+ name, page_name = *parts.compact.map(&:strip)
282
+ cname = @markup.wiki.page_class.cname(page_name || name)
283
+
284
+ presence = "absent"
285
+ link_name = cname
286
+ page, extra = find_page_from_name(cname)
287
+ if page
288
+ link_name = @markup.wiki.page_class.cname(page.name)
289
+ presence = "present"
290
+ end
291
+ link = ::File.join(@markup.wiki.base_path, page ? page.escaped_url_path : CGI.escape(link_name))
292
+
293
+ # //page is invalid
294
+ # strip all duplicate forward slashes using helpers.rb trim_leading_slash
295
+ # //page => /page
296
+ link = trim_leading_slash link
297
+
298
+ %{<a class="internal #{presence}" href="#{link}#{extra}">#{name}</a>}
299
+ end
300
+
301
+ # Find a page from a given cname. If the page has an anchor (#) and has
302
+ # no match, strip the anchor and try again.
303
+ #
304
+ # cname - The String canonical page name including path.
305
+ #
306
+ # Returns a Gollum::Page instance if a page is found, or an Array of
307
+ # [Gollum::Page, String extra] if a page without the extra anchor data
308
+ # is found.
309
+ def find_page_from_name(cname)
310
+ slash = cname.rindex('/')
311
+
312
+ unless slash.nil?
313
+ name = cname[slash+1..-1]
314
+ path = cname[0..slash]
315
+ page = @markup.wiki.paged(name, path)
316
+ else
317
+ page = @markup.wiki.paged(cname, '/') || @markup.wiki.page(cname)
318
+ end
319
+
320
+ if page
321
+ return page
322
+ end
323
+ if (pos = cname.index('#'))
324
+ [@markup.wiki.page(cname[0...pos]), cname[pos..-1]]
325
+ end
326
+ end
327
+ end