gitlab-gollum-lib 1.1.0 → 4.2.7

Sign up to get free protection for your applications and to get access to all the features.
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