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.
- checksums.yaml +4 -4
- data/Gemfile +1 -3
- data/HISTORY.md +25 -0
- data/LICENSE +1 -1
- data/README.md +24 -312
- data/Rakefile +32 -16
- data/gemspec.rb +110 -0
- data/gollum-lib.gemspec +8 -75
- data/gollum-lib_java.gemspec +4 -0
- data/lib/gollum-lib.rb +18 -6
- data/lib/gollum-lib/blob_entry.rb +10 -9
- data/lib/gollum-lib/committer.rb +37 -30
- data/lib/gollum-lib/file.rb +71 -15
- data/lib/gollum-lib/file_view.rb +53 -48
- data/lib/gollum-lib/filter.rb +78 -0
- data/lib/gollum-lib/filter/code.rb +145 -0
- data/lib/gollum-lib/filter/emoji.rb +39 -0
- data/lib/gollum-lib/filter/macro.rb +57 -0
- data/lib/gollum-lib/filter/metadata.rb +29 -0
- data/lib/gollum-lib/filter/plain_text.rb +16 -0
- data/lib/gollum-lib/filter/plantuml.rb +176 -0
- data/lib/gollum-lib/filter/remote_code.rb +63 -0
- data/lib/gollum-lib/filter/render.rb +20 -0
- data/lib/gollum-lib/filter/sanitize.rb +18 -0
- data/lib/gollum-lib/filter/tags.rb +327 -0
- data/lib/gollum-lib/filter/toc.rb +134 -0
- data/lib/gollum-lib/filter/wsd.rb +54 -0
- data/lib/gollum-lib/git_access.rb +30 -32
- data/lib/gollum-lib/gitcode.rb +16 -16
- data/lib/gollum-lib/helpers.rb +3 -3
- data/lib/gollum-lib/hook.rb +35 -0
- data/lib/gollum-lib/macro.rb +43 -0
- data/lib/gollum-lib/macro/all_pages.rb +11 -0
- data/lib/gollum-lib/macro/global_toc.rb +12 -0
- data/lib/gollum-lib/macro/navigation.rb +20 -0
- data/lib/gollum-lib/macro/series.rb +48 -0
- data/lib/gollum-lib/markup.rb +95 -572
- data/lib/gollum-lib/markups.rb +9 -3
- data/lib/gollum-lib/page.rb +109 -80
- data/lib/gollum-lib/pagination.rb +1 -1
- data/lib/gollum-lib/sanitization.rb +75 -75
- data/lib/gollum-lib/version.rb +5 -0
- data/lib/gollum-lib/wiki.rb +287 -129
- metadata +237 -92
- data/CHANGELOG +0 -2
- data/VERSION +0 -1
- data/lib/gollum-lib/grit_ext.rb +0 -20
- data/lib/gollum-lib/remote_code.rb +0 -39
- 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
|