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.
- 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
|