gollum-lib 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of gollum-lib might be problematic. Click here for more details.
- checksums.yaml +15 -0
- data/Gemfile +4 -0
- data/HISTORY.md +106 -0
- data/LICENSE +21 -0
- data/README.md +617 -0
- data/Rakefile +171 -0
- data/config.rb +28 -0
- data/docs/sanitization.md +32 -0
- data/gollum-lib.gemspec +74 -0
- data/lib/gollum-lib.rb +53 -0
- data/lib/gollum-lib/blob_entry.rb +95 -0
- data/lib/gollum-lib/committer.rb +236 -0
- data/lib/gollum-lib/file.rb +101 -0
- data/lib/gollum-lib/file_view.rb +155 -0
- data/lib/gollum-lib/git_access.rb +249 -0
- data/lib/gollum-lib/gitcode.rb +48 -0
- data/lib/gollum-lib/grit_ext.rb +20 -0
- data/lib/gollum-lib/helpers.rb +13 -0
- data/lib/gollum-lib/markup.rb +688 -0
- data/lib/gollum-lib/markups.rb +13 -0
- data/lib/gollum-lib/page.rb +485 -0
- data/lib/gollum-lib/pagination.rb +62 -0
- data/lib/gollum-lib/sanitization.rb +176 -0
- data/lib/gollum-lib/web_sequence_diagram.rb +44 -0
- data/lib/gollum-lib/wiki.rb +833 -0
- data/licenses/licenses.txt +6 -0
- metadata +301 -0
@@ -0,0 +1,249 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Gollum
|
3
|
+
# Controls all access to the Git objects from Gollum. Extend this class to
|
4
|
+
# add custom caching for special cases.
|
5
|
+
class GitAccess
|
6
|
+
# Initializes the GitAccess instance.
|
7
|
+
#
|
8
|
+
# path - The String path to the Git repository that holds the
|
9
|
+
# Gollum site.
|
10
|
+
# page_file_dir - String the directory in which all page files reside
|
11
|
+
#
|
12
|
+
# Returns this instance.
|
13
|
+
def initialize(path, page_file_dir = nil, bare = false)
|
14
|
+
@page_file_dir = page_file_dir
|
15
|
+
@path = path
|
16
|
+
@repo = Grit::Repo.new(path, { :is_bare => bare })
|
17
|
+
clear
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Determines whether the Git repository exists on disk.
|
21
|
+
#
|
22
|
+
# Returns true if it exists, or false.
|
23
|
+
def exist?
|
24
|
+
@repo.git.exist?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Converts a given Git reference to a SHA, using the cache if
|
28
|
+
# available.
|
29
|
+
#
|
30
|
+
# ref - a String Git reference (ex: "master")
|
31
|
+
#
|
32
|
+
# Returns a String, or nil if the ref isn't found.
|
33
|
+
def ref_to_sha(ref)
|
34
|
+
ref = ref.to_s
|
35
|
+
return if ref.empty?
|
36
|
+
sha =
|
37
|
+
if sha?(ref)
|
38
|
+
ref
|
39
|
+
else
|
40
|
+
get_cache(:ref, ref) { ref_to_sha!(ref) }
|
41
|
+
end.to_s
|
42
|
+
sha.empty? ? nil : sha
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: Gets a recursive list of Git blobs for the whole tree at the
|
46
|
+
# given commit.
|
47
|
+
#
|
48
|
+
# ref - A String Git reference or Git SHA to a commit.
|
49
|
+
#
|
50
|
+
# Returns an Array of BlobEntry instances.
|
51
|
+
def tree(ref)
|
52
|
+
if sha = ref_to_sha(ref)
|
53
|
+
get_cache(:tree, sha) { tree!(sha) }
|
54
|
+
else
|
55
|
+
[]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Public: Fetches the contents of the Git blob at the given SHA.
|
60
|
+
#
|
61
|
+
# sha - A String Git SHA.
|
62
|
+
#
|
63
|
+
# Returns the String content of the blob.
|
64
|
+
def blob(sha)
|
65
|
+
cat_file!(sha)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Public: Looks up the Git commit using the given Git SHA or ref.
|
69
|
+
#
|
70
|
+
# ref - A String Git SHA or ref.
|
71
|
+
#
|
72
|
+
# Returns a Grit::Commit.
|
73
|
+
def commit(ref)
|
74
|
+
if sha?(ref)
|
75
|
+
get_cache(:commit, ref) { commit!(ref) }
|
76
|
+
else
|
77
|
+
if sha = get_cache(:ref, ref)
|
78
|
+
commit(sha)
|
79
|
+
else
|
80
|
+
if cm = commit!(ref)
|
81
|
+
set_cache(:ref, ref, cm.id)
|
82
|
+
set_cache(:commit, cm.id, cm)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Public: Clears all of the cached data that this GitAccess is tracking.
|
89
|
+
#
|
90
|
+
# Returns nothing.
|
91
|
+
def clear
|
92
|
+
@ref_map = {}
|
93
|
+
@tree_map = {}
|
94
|
+
@commit_map = {}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Public: Refreshes just the cached Git reference data. This should
|
98
|
+
# be called after every Gollum update.
|
99
|
+
#
|
100
|
+
# Returns nothing.
|
101
|
+
def refresh
|
102
|
+
@ref_map.clear
|
103
|
+
end
|
104
|
+
|
105
|
+
#########################################################################
|
106
|
+
#
|
107
|
+
# Internal Methods
|
108
|
+
#
|
109
|
+
#########################################################################
|
110
|
+
|
111
|
+
# Gets the String path to the Git repository.
|
112
|
+
attr_reader :path
|
113
|
+
|
114
|
+
# Gets the Grit::Repo instance for the Git repository.
|
115
|
+
attr_reader :repo
|
116
|
+
|
117
|
+
# Gets a Hash cache of refs to commit SHAs.
|
118
|
+
#
|
119
|
+
# {"master" => "abc123", ...}
|
120
|
+
#
|
121
|
+
attr_reader :ref_map
|
122
|
+
|
123
|
+
# Gets a Hash cache of commit SHAs to a recursive tree of blobs.
|
124
|
+
#
|
125
|
+
# {"abc123" => [<BlobEntry>, <BlobEntry>]}
|
126
|
+
#
|
127
|
+
attr_reader :tree_map
|
128
|
+
|
129
|
+
# Gets a Hash cache of commit SHAs to the Grit::Commit instance.
|
130
|
+
#
|
131
|
+
# {"abcd123" => <Grit::Commit>}
|
132
|
+
#
|
133
|
+
attr_reader :commit_map
|
134
|
+
|
135
|
+
# Checks to see if the given String is a 40 character hex SHA.
|
136
|
+
#
|
137
|
+
# str - Possible String SHA.
|
138
|
+
#
|
139
|
+
# Returns true if the String is a SHA, or false.
|
140
|
+
def sha?(str)
|
141
|
+
!!(str =~ /^[0-9a-f]{40}$/)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Looks up the Git SHA for the given Git ref.
|
145
|
+
#
|
146
|
+
# ref - String Git ref.
|
147
|
+
#
|
148
|
+
# Returns a String SHA.
|
149
|
+
def ref_to_sha!(ref)
|
150
|
+
@repo.git.rev_list({:max_count=>1}, ref)
|
151
|
+
rescue Grit::GitRuby::Repository::NoSuchShaFound
|
152
|
+
end
|
153
|
+
|
154
|
+
# Looks up the Git blobs for a given commit.
|
155
|
+
#
|
156
|
+
# sha - String commit SHA.
|
157
|
+
#
|
158
|
+
# Returns an Array of BlobEntry instances.
|
159
|
+
def tree!(sha)
|
160
|
+
tree = @repo.git.native(:ls_tree,
|
161
|
+
{:r => true, :l => true, :z => true}, sha)
|
162
|
+
if tree.respond_to?(:force_encoding)
|
163
|
+
tree.force_encoding("UTF-8")
|
164
|
+
end
|
165
|
+
items = tree.split("\0").inject([]) do |memo, line|
|
166
|
+
memo << parse_tree_line(line)
|
167
|
+
end
|
168
|
+
|
169
|
+
if dir = @page_file_dir
|
170
|
+
regex = /^#{dir}\//
|
171
|
+
items.select { |i| i.path =~ regex }
|
172
|
+
else
|
173
|
+
items
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Reads the content from the Git db at the given SHA.
|
178
|
+
#
|
179
|
+
# sha - The String SHA.
|
180
|
+
#
|
181
|
+
# Returns the String content of the Git object.
|
182
|
+
def cat_file!(sha)
|
183
|
+
@repo.git.cat_file({:p => true}, sha)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Reads a Git commit.
|
187
|
+
#
|
188
|
+
# sha - The string SHA of the Git commit.
|
189
|
+
#
|
190
|
+
# Returns a Grit::Commit.
|
191
|
+
def commit!(sha)
|
192
|
+
@repo.commit(sha)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Attempts to get the given data from a cache. If it doesn't exist, it'll
|
196
|
+
# pass the results of the yielded block to the cache for future accesses.
|
197
|
+
#
|
198
|
+
# name - The cache prefix used in building the full cache key.
|
199
|
+
# key - The unique cache key suffix, usually a String Git SHA.
|
200
|
+
#
|
201
|
+
# Yields a block to pass to the cache.
|
202
|
+
# Returns the cached result.
|
203
|
+
def get_cache(name, key)
|
204
|
+
cache = instance_variable_get("@#{name}_map")
|
205
|
+
value = cache[key]
|
206
|
+
if value.nil? && block_given?
|
207
|
+
set_cache(name, key, value = yield)
|
208
|
+
end
|
209
|
+
value == :_nil ? nil : value
|
210
|
+
end
|
211
|
+
|
212
|
+
# Writes some data to the internal cache.
|
213
|
+
#
|
214
|
+
# name - The cache prefix used in building the full cache key.
|
215
|
+
# key - The unique cache key suffix, usually a String Git SHA.
|
216
|
+
# value - The value to write to the cache.
|
217
|
+
#
|
218
|
+
# Returns nothing.
|
219
|
+
def set_cache(name, key, value)
|
220
|
+
cache = instance_variable_get("@#{name}_map")
|
221
|
+
cache[key] = value || :_nil
|
222
|
+
end
|
223
|
+
|
224
|
+
# Parses a line of output from the `ls-tree` command.
|
225
|
+
#
|
226
|
+
# line - A String line of output:
|
227
|
+
# "100644 blob 839c2291b30495b9a882c17d08254d3c90d8fb53 Home.md"
|
228
|
+
#
|
229
|
+
# Returns an Array of BlobEntry instances.
|
230
|
+
def parse_tree_line(line)
|
231
|
+
mode, type, sha, size, *name = line.split(/\s+/)
|
232
|
+
BlobEntry.new(sha, name.join(' '), size.to_i, mode.to_i(8))
|
233
|
+
end
|
234
|
+
|
235
|
+
# Decode octal sequences (\NNN) in tree path names.
|
236
|
+
#
|
237
|
+
# path - String path name.
|
238
|
+
#
|
239
|
+
# Returns a decoded String.
|
240
|
+
def decode_git_path(path)
|
241
|
+
if path[0] == ?" && path[-1] == ?"
|
242
|
+
path = path[1...-1]
|
243
|
+
path.gsub!(/\\\d{3}/) { |m| m[1..-1].to_i(8).chr }
|
244
|
+
end
|
245
|
+
path.gsub!(/\\[rn"\\]/) { |m| eval(%("#{m.to_s}")) }
|
246
|
+
path
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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
|
+
module Gollum
|
8
|
+
class Gitcode
|
9
|
+
def initialize path
|
10
|
+
raise(ArgumentError, 'path is nil or empty') if path.nil? or path.empty?
|
11
|
+
|
12
|
+
@uri = URI::HTTP.build({
|
13
|
+
:path => self.unchomp(path),
|
14
|
+
:host => 'raw.github.com',
|
15
|
+
:scheme => 'https',
|
16
|
+
:port => 443 })
|
17
|
+
end
|
18
|
+
|
19
|
+
def contents
|
20
|
+
@contents ||= self.req @uri
|
21
|
+
end
|
22
|
+
|
23
|
+
def unchomp p
|
24
|
+
return p if p.nil?
|
25
|
+
p[0] == '/' ? p : ('/' + p)
|
26
|
+
end
|
27
|
+
|
28
|
+
def req uri, cut = 1
|
29
|
+
return "Too many redirects or retries" if cut >= 10
|
30
|
+
http = Net::HTTP.new uri.host, uri.port
|
31
|
+
http.use_ssl = true
|
32
|
+
resp = http.get uri.path, {
|
33
|
+
'Accept' => 'text/plain',
|
34
|
+
'Cache-Control' => 'no-cache',
|
35
|
+
'Connection' => 'keep-alive',
|
36
|
+
'Host' => uri.host,
|
37
|
+
'User-Agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0'
|
38
|
+
}
|
39
|
+
code = resp.code.to_i
|
40
|
+
return resp.body if code == 200
|
41
|
+
return "Not Found" if code == 404
|
42
|
+
return "Unhandled Response Code #{code}" unless code == 304 or not resp.header['location'].nil?
|
43
|
+
loc = URI.parse resp.header['location']
|
44
|
+
uri2 = loc.relative?() ? (uri + loc) : loc # overloads (+)
|
45
|
+
return req uri2, (cut + 1)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
|
3
|
+
module Grit
|
4
|
+
class Blob
|
5
|
+
def is_symlink
|
6
|
+
self.mode == 0120000
|
7
|
+
end
|
8
|
+
|
9
|
+
def symlink_target(base_path = nil)
|
10
|
+
target = self.data
|
11
|
+
new_path = File.expand_path(File.join('..', target), base_path)
|
12
|
+
|
13
|
+
if File.file? new_path
|
14
|
+
return new_path
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,688 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'cgi'
|
4
|
+
require 'pygments'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
require File.expand_path '../helpers', __FILE__
|
8
|
+
require File.expand_path '../gitcode', __FILE__
|
9
|
+
|
10
|
+
# initialize Pygments
|
11
|
+
Pygments.start
|
12
|
+
|
13
|
+
module Gollum
|
14
|
+
|
15
|
+
class Markup
|
16
|
+
include Helpers
|
17
|
+
|
18
|
+
@formats = {}
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_reader :formats
|
22
|
+
|
23
|
+
# Register a file extension and associated markup type
|
24
|
+
#
|
25
|
+
# ext - The file extension
|
26
|
+
# name - The name of the markup type
|
27
|
+
# options - Hash of options:
|
28
|
+
# regexp - Regexp to match against.
|
29
|
+
# Defaults to exact match of ext.
|
30
|
+
#
|
31
|
+
# If given a block, that block will be registered with GitHub::Markup to
|
32
|
+
# render any matching pages
|
33
|
+
def register(ext, name, options = {}, &block)
|
34
|
+
regexp = options[:regexp] || Regexp.new(ext.to_s)
|
35
|
+
@formats[ext] = { :name => name, :regexp => regexp }
|
36
|
+
GitHub::Markup.add_markup(regexp, &block) if block_given?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_accessor :toc
|
41
|
+
attr_reader :metadata
|
42
|
+
|
43
|
+
# Initialize a new Markup object.
|
44
|
+
#
|
45
|
+
# page - The Gollum::Page.
|
46
|
+
#
|
47
|
+
# Returns a new Gollum::Markup object, ready for rendering.
|
48
|
+
def initialize(page)
|
49
|
+
@wiki = page.wiki
|
50
|
+
@name = page.filename
|
51
|
+
@data = page.text_data
|
52
|
+
@version = page.version.id if page.version
|
53
|
+
@format = page.format
|
54
|
+
@sub_page = page.sub_page
|
55
|
+
@parent_page = page.parent_page
|
56
|
+
@dir = ::File.dirname(page.path)
|
57
|
+
@tagmap = {}
|
58
|
+
@codemap = {}
|
59
|
+
@wsdmap = {}
|
60
|
+
@premap = {}
|
61
|
+
@toc = nil
|
62
|
+
@metadata = nil
|
63
|
+
@to_xml = { :save_with => Nokogiri::XML::Node::SaveOptions::DEFAULT_XHTML ^ 1, :indent => 0, :encoding => 'UTF-8' }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Render the content with Gollum wiki syntax on top of the file's own
|
67
|
+
# markup language.
|
68
|
+
#
|
69
|
+
# no_follow - Boolean that determines if rel="nofollow" is added to all
|
70
|
+
# <a> tags.
|
71
|
+
# encoding - Encoding Constant or String.
|
72
|
+
#
|
73
|
+
# Returns the formatted String content.
|
74
|
+
def render(no_follow = false, encoding = nil)
|
75
|
+
sanitize = no_follow ?
|
76
|
+
@wiki.history_sanitizer :
|
77
|
+
@wiki.sanitizer
|
78
|
+
|
79
|
+
data = @data.dup
|
80
|
+
data = extract_metadata(data)
|
81
|
+
data = extract_gitcode(data)
|
82
|
+
data = extract_code(data)
|
83
|
+
data = extract_wsd(data)
|
84
|
+
data = extract_tags(data)
|
85
|
+
begin
|
86
|
+
data = GitHub::Markup.render(@name, data)
|
87
|
+
if data.nil?
|
88
|
+
raise "There was an error converting #{@name} to HTML."
|
89
|
+
end
|
90
|
+
rescue Object => e
|
91
|
+
data = %{<p class="gollum-error">#{e.message}</p>}
|
92
|
+
end
|
93
|
+
data = process_tags(data)
|
94
|
+
data = process_code(data, encoding)
|
95
|
+
|
96
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(data)
|
97
|
+
doc = sanitize.clean_node!(doc) if sanitize
|
98
|
+
doc,toc = process_headers(doc)
|
99
|
+
@toc = @sub_page ? ( @parent_page ? @parent_page.toc_data : "[[_TOC_]]" ) : toc
|
100
|
+
yield doc if block_given?
|
101
|
+
# nokogiri's save options are ored together. FORMAT has a value of 1 so ^ 1 removes it.
|
102
|
+
# formatting will create extra spaces in pre tags.
|
103
|
+
# https://github.com/sparklemotion/nokogiri/issues/782
|
104
|
+
# DEFAULT_HTML encodes unicode so XHTML is used for proper unicode support in href.
|
105
|
+
data = doc.to_xml( @to_xml )
|
106
|
+
|
107
|
+
data = process_toc_tags(data)
|
108
|
+
data = process_wsd(data)
|
109
|
+
data.gsub!(/<p><\/p>/) do
|
110
|
+
''
|
111
|
+
end
|
112
|
+
|
113
|
+
data
|
114
|
+
end
|
115
|
+
|
116
|
+
# Inserts header anchors and creates TOC
|
117
|
+
#
|
118
|
+
# doc - Nokogiri parsed document
|
119
|
+
#
|
120
|
+
# Returns doc Document and toc String
|
121
|
+
def process_headers(doc)
|
122
|
+
toc = nil
|
123
|
+
doc.css('h1,h2,h3,h4,h5,h6').each do |h|
|
124
|
+
# must escape "
|
125
|
+
h_name = h.content.gsub(' ','-').gsub('"','%22')
|
126
|
+
|
127
|
+
level = h.name.gsub(/[hH]/,'').to_i
|
128
|
+
|
129
|
+
# Add anchors
|
130
|
+
h.add_child(%Q{<a class="anchor" id="#{h_name}" href="##{h_name}"></a>})
|
131
|
+
|
132
|
+
# Build TOC
|
133
|
+
toc ||= Nokogiri::XML::DocumentFragment.parse('<div class="toc"><div class="toc-title">Table of Contents</div></div>')
|
134
|
+
tail ||= toc.child
|
135
|
+
tail_level ||= 0
|
136
|
+
|
137
|
+
while tail_level < level
|
138
|
+
node = Nokogiri::XML::Node.new('ul', doc)
|
139
|
+
tail = tail.add_child(node)
|
140
|
+
tail_level += 1
|
141
|
+
end
|
142
|
+
while tail_level > level
|
143
|
+
tail = tail.parent
|
144
|
+
tail_level -= 1
|
145
|
+
end
|
146
|
+
node = Nokogiri::XML::Node.new('li', doc)
|
147
|
+
# % -> %25 so anchors work on Firefox. See issue #475
|
148
|
+
node.add_child(%Q{<a href="##{h_name}">#{h.content}</a>})
|
149
|
+
tail.add_child(node)
|
150
|
+
end
|
151
|
+
toc = toc.to_xml(@to_xml) if toc != nil
|
152
|
+
[doc, toc]
|
153
|
+
end
|
154
|
+
|
155
|
+
#########################################################################
|
156
|
+
#
|
157
|
+
# Tags
|
158
|
+
#
|
159
|
+
#########################################################################
|
160
|
+
|
161
|
+
# Extract all tags into the tagmap and replace with placeholders.
|
162
|
+
#
|
163
|
+
# data - The raw String data.
|
164
|
+
#
|
165
|
+
# Returns the placeholder'd String data.
|
166
|
+
def extract_tags(data)
|
167
|
+
if @format == :asciidoc
|
168
|
+
return data
|
169
|
+
end
|
170
|
+
data.gsub!(/(.?)\[\[(.+?)\]\]([^\[]?)/m) do
|
171
|
+
if $1 == "'" && $3 != "'"
|
172
|
+
"[[#{$2}]]#{$3}"
|
173
|
+
elsif $2.include?('][')
|
174
|
+
if $2[0..4] == 'file:'
|
175
|
+
pre = $1
|
176
|
+
post = $3
|
177
|
+
parts = $2.split('][')
|
178
|
+
parts[0][0..4] = ""
|
179
|
+
link = "#{parts[1]}|#{parts[0].sub(/\.org/,'')}"
|
180
|
+
id = Digest::SHA1.hexdigest(link)
|
181
|
+
@tagmap[id] = link
|
182
|
+
"#{pre}#{id}#{post}"
|
183
|
+
else
|
184
|
+
$&
|
185
|
+
end
|
186
|
+
else
|
187
|
+
id = Digest::SHA1.hexdigest($2)
|
188
|
+
@tagmap[id] = $2
|
189
|
+
"#{$1}#{id}#{$3}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
data
|
193
|
+
end
|
194
|
+
|
195
|
+
# Process all tags from the tagmap and replace the placeholders with the
|
196
|
+
# final markup.
|
197
|
+
#
|
198
|
+
# data - The String data (with placeholders).
|
199
|
+
#
|
200
|
+
# Returns the marked up String data.
|
201
|
+
def process_tags(data)
|
202
|
+
@tagmap.each do |id, tag|
|
203
|
+
# If it's preformatted, just put the tag back
|
204
|
+
if is_preformatted?(data, id)
|
205
|
+
data.gsub!(id) do
|
206
|
+
"[[#{tag}]]"
|
207
|
+
end
|
208
|
+
else
|
209
|
+
data.gsub!(id) do
|
210
|
+
process_tag(tag).gsub('%2F', '/')
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
data
|
215
|
+
end
|
216
|
+
|
217
|
+
# Find `id` within `data` and determine if it's within
|
218
|
+
# preformatted tags.
|
219
|
+
#
|
220
|
+
# data - The String data (with placeholders).
|
221
|
+
# id - The String SHA1 hash.
|
222
|
+
PREFORMATTED_TAGS = %w(code tt)
|
223
|
+
def is_preformatted?(data, id)
|
224
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(data)
|
225
|
+
node = doc.search("[text()*='#{id}']").first
|
226
|
+
node && (PREFORMATTED_TAGS.include?(node.name) ||
|
227
|
+
node.ancestors.any? { |a| PREFORMATTED_TAGS.include?(a.name) })
|
228
|
+
end
|
229
|
+
|
230
|
+
# Process a single tag into its final HTML form.
|
231
|
+
#
|
232
|
+
# tag - The String tag contents (the stuff inside the double
|
233
|
+
# brackets).
|
234
|
+
#
|
235
|
+
# Returns the String HTML version of the tag.
|
236
|
+
def process_tag(tag)
|
237
|
+
if tag =~ /^_TOC_$/
|
238
|
+
%{[[#{tag}]]}
|
239
|
+
elsif tag =~ /^_$/
|
240
|
+
%{<div class="clearfloats"></div>}
|
241
|
+
elsif html = process_image_tag(tag)
|
242
|
+
html
|
243
|
+
elsif html = process_file_link_tag(tag)
|
244
|
+
html
|
245
|
+
else
|
246
|
+
process_page_link_tag(tag)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Attempt to process the tag as an image tag.
|
251
|
+
#
|
252
|
+
# tag - The String tag contents (the stuff inside the double brackets).
|
253
|
+
#
|
254
|
+
# Returns the String HTML if the tag is a valid image tag or nil
|
255
|
+
# if it is not.
|
256
|
+
def process_image_tag(tag)
|
257
|
+
parts = tag.split('|')
|
258
|
+
return if parts.size.zero?
|
259
|
+
|
260
|
+
name = parts[0].strip
|
261
|
+
path = if file = find_file(name)
|
262
|
+
::File.join @wiki.base_path, file.path
|
263
|
+
elsif name =~ /^https?:\/\/.+(jpg|png|gif|svg|bmp)$/i
|
264
|
+
name
|
265
|
+
end
|
266
|
+
|
267
|
+
if path
|
268
|
+
opts = parse_image_tag_options(tag)
|
269
|
+
|
270
|
+
containered = false
|
271
|
+
|
272
|
+
classes = [] # applied to whatever the outermost container is
|
273
|
+
attrs = [] # applied to the image
|
274
|
+
|
275
|
+
align = opts['align']
|
276
|
+
if opts['float']
|
277
|
+
containered = true
|
278
|
+
align ||= 'left'
|
279
|
+
if %w{left right}.include?(align)
|
280
|
+
classes << "float-#{align}"
|
281
|
+
end
|
282
|
+
elsif %w{top texttop middle absmiddle bottom absbottom baseline}.include?(align)
|
283
|
+
attrs << %{align="#{align}"}
|
284
|
+
elsif align
|
285
|
+
if %w{left center right}.include?(align)
|
286
|
+
containered = true
|
287
|
+
classes << "align-#{align}"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
if width = opts['width']
|
292
|
+
if width =~ /^\d+(\.\d+)?(em|px)$/
|
293
|
+
attrs << %{width="#{width}"}
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
if height = opts['height']
|
298
|
+
if height =~ /^\d+(\.\d+)?(em|px)$/
|
299
|
+
attrs << %{height="#{height}"}
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
if alt = opts['alt']
|
304
|
+
attrs << %{alt="#{alt}"}
|
305
|
+
end
|
306
|
+
|
307
|
+
attr_string = attrs.size > 0 ? attrs.join(' ') + ' ' : ''
|
308
|
+
|
309
|
+
if opts['frame'] || containered
|
310
|
+
classes << 'frame' if opts['frame']
|
311
|
+
%{<span class="#{classes.join(' ')}">} +
|
312
|
+
%{<span>} +
|
313
|
+
%{<img src="#{path}" #{attr_string}/>} +
|
314
|
+
(alt ? %{<span>#{alt}</span>} : '') +
|
315
|
+
%{</span>} +
|
316
|
+
%{</span>}
|
317
|
+
else
|
318
|
+
%{<img src="#{path}" #{attr_string}/>}
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Parse any options present on the image tag and extract them into a
|
324
|
+
# Hash of option names and values.
|
325
|
+
#
|
326
|
+
# tag - The String tag contents (the stuff inside the double brackets).
|
327
|
+
#
|
328
|
+
# Returns the options Hash:
|
329
|
+
# key - The String option name.
|
330
|
+
# val - The String option value or true if it is a binary option.
|
331
|
+
def parse_image_tag_options(tag)
|
332
|
+
tag.split('|')[1..-1].inject({}) do |memo, attr|
|
333
|
+
parts = attr.split('=').map { |x| x.strip }
|
334
|
+
memo[parts[0]] = (parts.size == 1 ? true : parts[1])
|
335
|
+
memo
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# Attempt to process the tag as a file link tag.
|
340
|
+
#
|
341
|
+
# tag - The String tag contents (the stuff inside the double
|
342
|
+
# brackets).
|
343
|
+
#
|
344
|
+
# Returns the String HTML if the tag is a valid file link tag or nil
|
345
|
+
# if it is not.
|
346
|
+
def process_file_link_tag(tag)
|
347
|
+
parts = tag.split('|')
|
348
|
+
return if parts.size.zero?
|
349
|
+
|
350
|
+
name = parts[0].strip
|
351
|
+
path = parts[1] && parts[1].strip
|
352
|
+
path = if path && file = find_file(path)
|
353
|
+
::File.join @wiki.base_path, file.path
|
354
|
+
elsif path =~ %r{^https?://}
|
355
|
+
path
|
356
|
+
else
|
357
|
+
nil
|
358
|
+
end
|
359
|
+
|
360
|
+
if name && path && file
|
361
|
+
%{<a href="#{::File.join @wiki.base_path, file.path}">#{name}</a>}
|
362
|
+
elsif name && path
|
363
|
+
%{<a href="#{path}">#{name}</a>}
|
364
|
+
else
|
365
|
+
nil
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Attempt to process the tag as a page link tag.
|
370
|
+
#
|
371
|
+
# tag - The String tag contents (the stuff inside the double
|
372
|
+
# brackets).
|
373
|
+
#
|
374
|
+
# Returns the String HTML if the tag is a valid page link tag or nil
|
375
|
+
# if it is not.
|
376
|
+
def process_page_link_tag(tag)
|
377
|
+
parts = tag.split('|')
|
378
|
+
parts.reverse! if @format == :mediawiki
|
379
|
+
|
380
|
+
name, page_name = *parts.compact.map(&:strip)
|
381
|
+
cname = @wiki.page_class.cname(page_name || name)
|
382
|
+
|
383
|
+
if name =~ %r{^https?://} && page_name.nil?
|
384
|
+
%{<a href="#{name}">#{name}</a>}
|
385
|
+
else
|
386
|
+
presence = "absent"
|
387
|
+
link_name = cname
|
388
|
+
page, extra = find_page_from_name(cname)
|
389
|
+
if page
|
390
|
+
link_name = @wiki.page_class.cname(page.name)
|
391
|
+
presence = "present"
|
392
|
+
end
|
393
|
+
link = ::File.join(@wiki.base_path, page ? page.escaped_url_path : CGI.escape(link_name))
|
394
|
+
|
395
|
+
# //page is invalid
|
396
|
+
# strip all duplicate forward slashes using helpers.rb trim_leading_slash
|
397
|
+
# //page => /page
|
398
|
+
link = trim_leading_slash link
|
399
|
+
|
400
|
+
%{<a class="internal #{presence}" href="#{link}#{extra}">#{name}</a>}
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
|
405
|
+
# Process the special table of contents tag [[_TOC_]]
|
406
|
+
#
|
407
|
+
# data - The String data (with placeholders).
|
408
|
+
#
|
409
|
+
# Returns the marked up String data.
|
410
|
+
def process_toc_tags(data)
|
411
|
+
data.gsub!("[[_TOC_]]") do
|
412
|
+
@toc.nil? ? '' : @toc
|
413
|
+
end
|
414
|
+
data
|
415
|
+
end
|
416
|
+
|
417
|
+
# Find the given file in the repo.
|
418
|
+
#
|
419
|
+
# name - The String absolute or relative path of the file.
|
420
|
+
#
|
421
|
+
# Returns the Gollum::File or nil if none was found.
|
422
|
+
def find_file(name, version=@version)
|
423
|
+
if name =~ /^\//
|
424
|
+
@wiki.file(name[1..-1], version)
|
425
|
+
else
|
426
|
+
path = @dir == '.' ? name : ::File.join(@dir, name)
|
427
|
+
@wiki.file(path, version)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# Find a page from a given cname. If the page has an anchor (#) and has
|
432
|
+
# no match, strip the anchor and try again.
|
433
|
+
#
|
434
|
+
# cname - The String canonical page name including path.
|
435
|
+
#
|
436
|
+
# Returns a Gollum::Page instance if a page is found, or an Array of
|
437
|
+
# [Gollum::Page, String extra] if a page without the extra anchor data
|
438
|
+
# is found.
|
439
|
+
def find_page_from_name(cname)
|
440
|
+
slash = cname.rindex('/')
|
441
|
+
|
442
|
+
unless slash.nil?
|
443
|
+
name = cname[slash+1..-1]
|
444
|
+
path = cname[0..slash]
|
445
|
+
page = @wiki.paged(name, path)
|
446
|
+
else
|
447
|
+
page = @wiki.paged(cname, '/') || @wiki.page(cname)
|
448
|
+
end
|
449
|
+
|
450
|
+
if page
|
451
|
+
return page
|
452
|
+
end
|
453
|
+
if pos = cname.index('#')
|
454
|
+
[@wiki.page(cname[0...pos]), cname[pos..-1]]
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
#########################################################################
|
459
|
+
#
|
460
|
+
# Gitcode - fetch code from github search path and replace the contents
|
461
|
+
# to a code-block that gets run the next parse.
|
462
|
+
# Acceptable formats:
|
463
|
+
# ```language:local-file.ext```
|
464
|
+
# ```language:/abs/other-file.ext```
|
465
|
+
# ```language:gollum/gollum/master/somefile.txt```
|
466
|
+
#
|
467
|
+
#########################################################################
|
468
|
+
|
469
|
+
def extract_gitcode data
|
470
|
+
data.gsub /^[ \t]*``` ?([^:\n\r]+):([^`\n\r]+)```/ do
|
471
|
+
contents = ''
|
472
|
+
# Use empty string if $2 is nil.
|
473
|
+
uri = $2 || ''
|
474
|
+
# Detect local file.
|
475
|
+
if uri[0..6] != 'gollum/'
|
476
|
+
if file = self.find_file(uri, @wiki.ref)
|
477
|
+
contents = file.raw_data
|
478
|
+
else
|
479
|
+
# How do we communicate a render error?
|
480
|
+
next "File not found: #{CGI::escapeHTML(uri)}"
|
481
|
+
end
|
482
|
+
else
|
483
|
+
contents = Gollum::Gitcode.new(uri).contents
|
484
|
+
end
|
485
|
+
|
486
|
+
"```#{$1}\n#{contents}\n```\n"
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
#########################################################################
|
491
|
+
#
|
492
|
+
# Code
|
493
|
+
#
|
494
|
+
#########################################################################
|
495
|
+
|
496
|
+
# Extract all code blocks into the codemap and replace with placeholders.
|
497
|
+
#
|
498
|
+
# data - The raw String data.
|
499
|
+
#
|
500
|
+
# Returns the placeholder'd String data.
|
501
|
+
def extract_code(data)
|
502
|
+
data.gsub!(/^([ \t]*)(~~~+) ?([^\r\n]+)?\r?\n(.+?)\r?\n\1(~~~+)[ \t\r]*$/m) do
|
503
|
+
m_indent = $1
|
504
|
+
m_start = $2 # ~~~
|
505
|
+
m_lang = $3
|
506
|
+
m_code = $4
|
507
|
+
m_end = $5 # ~~~
|
508
|
+
|
509
|
+
# start and finish tilde fence must be the same length
|
510
|
+
return '' if m_start.length != m_end.length
|
511
|
+
|
512
|
+
lang = m_lang ? m_lang.strip : nil
|
513
|
+
id = Digest::SHA1.hexdigest("#{lang}.#{m_code}")
|
514
|
+
cached = check_cache(:code, id)
|
515
|
+
|
516
|
+
# extract lang from { .ruby } or { #stuff .ruby .indent }
|
517
|
+
# see http://johnmacfarlane.net/pandoc/README.html#delimited-code-blocks
|
518
|
+
|
519
|
+
if lang
|
520
|
+
lang = lang.match(/\.([^}\s]+)/)
|
521
|
+
lang = lang[1] unless lang.nil?
|
522
|
+
end
|
523
|
+
|
524
|
+
@codemap[id] = cached ?
|
525
|
+
{ :output => cached } :
|
526
|
+
{ :lang => lang, :code => m_code, :indent => m_indent }
|
527
|
+
|
528
|
+
"#{m_indent}#{id}" # print the SHA1 ID with the proper indentation
|
529
|
+
end
|
530
|
+
|
531
|
+
data.gsub!(/^([ \t]*)``` ?([^\r\n]+)?\r?\n(.+?)\r?\n\1```[ \t]*\r?$/m) do
|
532
|
+
lang = $2 ? $2.strip : nil
|
533
|
+
id = Digest::SHA1.hexdigest("#{lang}.#{$3}")
|
534
|
+
cached = check_cache(:code, id)
|
535
|
+
@codemap[id] = cached ?
|
536
|
+
{ :output => cached } :
|
537
|
+
{ :lang => lang, :code => $3, :indent => $1 }
|
538
|
+
"#{$1}#{id}" # print the SHA1 ID with the proper indentation
|
539
|
+
end
|
540
|
+
data
|
541
|
+
end
|
542
|
+
|
543
|
+
# Remove the leading space from a code block. Leading space
|
544
|
+
# is only removed if every single line in the block has leading
|
545
|
+
# whitespace.
|
546
|
+
#
|
547
|
+
# code - The code block to remove spaces from
|
548
|
+
# regex - A regex to match whitespace
|
549
|
+
def remove_leading_space(code, regex)
|
550
|
+
if code.lines.all? { |line| line =~ /\A\r?\n\Z/ || line =~ regex }
|
551
|
+
code.gsub!(regex) do
|
552
|
+
''
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
# Process all code from the codemap and replace the placeholders with the
|
558
|
+
# final HTML.
|
559
|
+
#
|
560
|
+
# data - The String data (with placeholders).
|
561
|
+
# encoding - Encoding Constant or String.
|
562
|
+
#
|
563
|
+
# Returns the marked up String data.
|
564
|
+
def process_code(data, encoding = nil)
|
565
|
+
return data if data.nil? || data.size.zero? || @codemap.size.zero?
|
566
|
+
|
567
|
+
blocks = []
|
568
|
+
@codemap.each do |id, spec|
|
569
|
+
next if spec[:output] # cached
|
570
|
+
|
571
|
+
code = spec[:code]
|
572
|
+
|
573
|
+
remove_leading_space(code, /^#{spec[:indent]}/m)
|
574
|
+
remove_leading_space(code, /^( |\t)/m)
|
575
|
+
|
576
|
+
blocks << [spec[:lang], code]
|
577
|
+
end
|
578
|
+
|
579
|
+
highlighted = []
|
580
|
+
blocks.each do |lang, code|
|
581
|
+
encoding ||= 'utf-8'
|
582
|
+
begin
|
583
|
+
# must set startinline to true for php to be highlighted without <?
|
584
|
+
# http://pygments.org/docs/lexers/
|
585
|
+
hl_code = Pygments.highlight(code, :lexer => lang, :options => {:encoding => encoding.to_s, :startinline => true})
|
586
|
+
rescue
|
587
|
+
hl_code = code
|
588
|
+
end
|
589
|
+
highlighted << hl_code
|
590
|
+
end
|
591
|
+
|
592
|
+
@codemap.each do |id, spec|
|
593
|
+
body = spec[:output] || begin
|
594
|
+
if (body = highlighted.shift.to_s).size > 0
|
595
|
+
update_cache(:code, id, body)
|
596
|
+
body
|
597
|
+
else
|
598
|
+
"<pre><code>#{CGI.escapeHTML(spec[:code])}</code></pre>"
|
599
|
+
end
|
600
|
+
end
|
601
|
+
data.gsub!(id) do
|
602
|
+
body
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
data
|
607
|
+
end
|
608
|
+
|
609
|
+
#########################################################################
|
610
|
+
#
|
611
|
+
# Sequence Diagrams
|
612
|
+
#
|
613
|
+
#########################################################################
|
614
|
+
|
615
|
+
# Extract all sequence diagram blocks into the wsdmap and replace with
|
616
|
+
# placeholders.
|
617
|
+
#
|
618
|
+
# data - The raw String data.
|
619
|
+
#
|
620
|
+
# Returns the placeholder'd String data.
|
621
|
+
def extract_wsd(data)
|
622
|
+
data.gsub(/^\{\{\{\{\{\{ ?(.+?)\r?\n(.+?)\r?\n\}\}\}\}\}\}\r?$/m) do
|
623
|
+
id = Digest::SHA1.hexdigest($2)
|
624
|
+
@wsdmap[id] = { :style => $1, :code => $2 }
|
625
|
+
id
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
# Process all diagrams from the wsdmap and replace the placeholders with
|
630
|
+
# the final HTML.
|
631
|
+
#
|
632
|
+
# data - The String data (with placeholders).
|
633
|
+
#
|
634
|
+
# Returns the marked up String data.
|
635
|
+
def process_wsd(data)
|
636
|
+
@wsdmap.each do |id, spec|
|
637
|
+
style = spec[:style]
|
638
|
+
code = spec[:code]
|
639
|
+
data.gsub!(id) do
|
640
|
+
Gollum::WebSequenceDiagram.new(code, style).to_tag
|
641
|
+
end
|
642
|
+
end
|
643
|
+
data
|
644
|
+
end
|
645
|
+
|
646
|
+
#########################################################################
|
647
|
+
#
|
648
|
+
# Metadata
|
649
|
+
#
|
650
|
+
#########################################################################
|
651
|
+
|
652
|
+
# Extract metadata for data and build metadata table. Metadata
|
653
|
+
# is content found between markers, and must
|
654
|
+
# be a valid YAML mapping.
|
655
|
+
#
|
656
|
+
# Because ri and ruby 1.8.7 are awesome, the markers can't
|
657
|
+
# be included in this documentation without triggering
|
658
|
+
# `Unhandled special: Special: type=17`
|
659
|
+
# Please read the source code for the exact markers
|
660
|
+
#
|
661
|
+
# Returns the String of formatted data with metadata removed.
|
662
|
+
def extract_metadata(data)
|
663
|
+
@metadata = {}
|
664
|
+
data
|
665
|
+
end
|
666
|
+
|
667
|
+
# Hook for getting the formatted value of extracted tag data.
|
668
|
+
#
|
669
|
+
# type - Symbol value identifying what type of data is being extracted.
|
670
|
+
# id - String SHA1 hash of original extracted tag data.
|
671
|
+
#
|
672
|
+
# Returns the String cached formatted data, or nil.
|
673
|
+
def check_cache(type, id)
|
674
|
+
end
|
675
|
+
|
676
|
+
# Hook for caching the formatted value of extracted tag data.
|
677
|
+
#
|
678
|
+
# type - Symbol value identifying what type of data is being extracted.
|
679
|
+
# id - String SHA1 hash of original extracted tag data.
|
680
|
+
# data - The String formatted value to be cached.
|
681
|
+
#
|
682
|
+
# Returns nothing.
|
683
|
+
def update_cache(type, id, data)
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
MarkupGFM = Markup
|
688
|
+
end
|