RTFMd 0.10301.1
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.
- data/HISTORY.md +93 -0
- data/LICENSE +21 -0
- data/README.md +387 -0
- data/RTFMd.gemspec +78 -0
- data/Rakefile +138 -0
- data/bin/rtfm +12 -0
- data/docs/sanitization.md +32 -0
- data/lib/gollum.rb +40 -0
- data/lib/gollum/blob_entry.rb +78 -0
- data/lib/gollum/committer.rb +217 -0
- data/lib/gollum/file.rb +64 -0
- data/lib/gollum/frontend/app.rb +94 -0
- data/lib/gollum/frontend/public/css/gollum.css +660 -0
- data/lib/gollum/frontend/public/css/ie7.css +69 -0
- data/lib/gollum/frontend/public/css/template.css +381 -0
- data/lib/gollum/frontend/public/images/icon-sprite.png +0 -0
- data/lib/gollum/frontend/public/javascript/gollum.js +137 -0
- data/lib/gollum/frontend/public/javascript/gollum.placeholder.js +54 -0
- data/lib/gollum/frontend/public/javascript/jquery.color.js +123 -0
- data/lib/gollum/frontend/public/javascript/jquery.js +7179 -0
- data/lib/gollum/frontend/templates/error.mustache +8 -0
- data/lib/gollum/frontend/templates/layout.mustache +28 -0
- data/lib/gollum/frontend/templates/page.mustache +32 -0
- data/lib/gollum/frontend/templates/pages.mustache +34 -0
- data/lib/gollum/frontend/views/error.rb +7 -0
- data/lib/gollum/frontend/views/layout.rb +24 -0
- data/lib/gollum/frontend/views/page.rb +53 -0
- data/lib/gollum/frontend/views/pages.rb +19 -0
- data/lib/gollum/git_access.rb +248 -0
- data/lib/gollum/markup.rb +389 -0
- data/lib/gollum/page.rb +374 -0
- data/lib/gollum/pagination.rb +61 -0
- data/lib/gollum/sanitization.rb +161 -0
- data/lib/gollum/wiki.rb +378 -0
- metadata +358 -0
@@ -0,0 +1,389 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'cgi'
|
3
|
+
require 'pygments'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Gollum
|
7
|
+
|
8
|
+
class Markup
|
9
|
+
# Initialize a new Markup object.
|
10
|
+
#
|
11
|
+
# page - The Gollum::Page.
|
12
|
+
#
|
13
|
+
# Returns a new Gollum::Markup object, ready for rendering.
|
14
|
+
def initialize(page)
|
15
|
+
@wiki = page.wiki
|
16
|
+
@name = page.filename
|
17
|
+
@data = page.text_data
|
18
|
+
@version = page.version.id if page.version
|
19
|
+
@format = page.format
|
20
|
+
@dir = ::File.dirname(page.path)
|
21
|
+
@tagmap = {}
|
22
|
+
@codemap = {}
|
23
|
+
@premap = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Render the content with Gollum wiki syntax on top of the file's own
|
27
|
+
# markup language.
|
28
|
+
#
|
29
|
+
# no_follow - Boolean that determines if rel="nofollow" is added to all
|
30
|
+
# <a> tags.
|
31
|
+
#
|
32
|
+
# Returns the formatted String content.
|
33
|
+
def render(no_follow = false)
|
34
|
+
sanitize = no_follow ?
|
35
|
+
@wiki.history_sanitizer :
|
36
|
+
@wiki.sanitizer
|
37
|
+
|
38
|
+
data = extract_code(@data.dup)
|
39
|
+
data = extract_tags(data)
|
40
|
+
begin
|
41
|
+
data = GitHub::Markup.render(@name, data)
|
42
|
+
if data.nil?
|
43
|
+
raise "There was an error converting #{@name} to HTML."
|
44
|
+
end
|
45
|
+
rescue Object => e
|
46
|
+
data = %{<p class="gollum-error">#{e.message}</p>}
|
47
|
+
end
|
48
|
+
data = process_tags(data)
|
49
|
+
data = process_code(data)
|
50
|
+
if sanitize || block_given?
|
51
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(data)
|
52
|
+
doc = sanitize.clean_node!(doc) if sanitize
|
53
|
+
yield doc if block_given?
|
54
|
+
data = doc.to_html
|
55
|
+
end
|
56
|
+
data.gsub!(/<p><\/p>/, '')
|
57
|
+
data
|
58
|
+
end
|
59
|
+
|
60
|
+
#########################################################################
|
61
|
+
#
|
62
|
+
# Tags
|
63
|
+
#
|
64
|
+
#########################################################################
|
65
|
+
|
66
|
+
# Extract all tags into the tagmap and replace with placeholders.
|
67
|
+
#
|
68
|
+
# data - The raw String data.
|
69
|
+
#
|
70
|
+
# Returns the placeholder'd String data.
|
71
|
+
def extract_tags(data)
|
72
|
+
data.gsub!(/(.?)\[\[(.+?)\]\]([^\[]?)/m) do
|
73
|
+
if $1 == "'" && $3 != "'"
|
74
|
+
"[[#{$2}]]#{$3}"
|
75
|
+
elsif $2.include?('][')
|
76
|
+
if $2[0..4] == 'file:'
|
77
|
+
pre = $1
|
78
|
+
post = $3
|
79
|
+
parts = $2.split('][')
|
80
|
+
parts[0][0..4] = ""
|
81
|
+
link = "#{parts[1]}|#{parts[0].sub(/\.org/,'')}"
|
82
|
+
id = Digest::SHA1.hexdigest(link)
|
83
|
+
@tagmap[id] = link
|
84
|
+
"#{pre}#{id}#{post}"
|
85
|
+
else
|
86
|
+
$&
|
87
|
+
end
|
88
|
+
else
|
89
|
+
id = Digest::SHA1.hexdigest($2)
|
90
|
+
@tagmap[id] = $2
|
91
|
+
"#{$1}#{id}#{$3}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
data
|
95
|
+
end
|
96
|
+
|
97
|
+
# Process all tags from the tagmap and replace the placeholders with the
|
98
|
+
# final markup.
|
99
|
+
#
|
100
|
+
# data - The String data (with placeholders).
|
101
|
+
#
|
102
|
+
# Returns the marked up String data.
|
103
|
+
def process_tags(data)
|
104
|
+
@tagmap.each do |id, tag|
|
105
|
+
data.gsub!(id, process_tag(tag))
|
106
|
+
end
|
107
|
+
data
|
108
|
+
end
|
109
|
+
|
110
|
+
# Process a single tag into its final HTML form.
|
111
|
+
#
|
112
|
+
# tag - The String tag contents (the stuff inside the double
|
113
|
+
# brackets).
|
114
|
+
#
|
115
|
+
# Returns the String HTML version of the tag.
|
116
|
+
def process_tag(tag)
|
117
|
+
if html = process_image_tag(tag)
|
118
|
+
html
|
119
|
+
elsif html = process_file_link_tag(tag)
|
120
|
+
html
|
121
|
+
else
|
122
|
+
process_page_link_tag(tag)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Attempt to process the tag as an image tag.
|
127
|
+
#
|
128
|
+
# tag - The String tag contents (the stuff inside the double brackets).
|
129
|
+
#
|
130
|
+
# Returns the String HTML if the tag is a valid image tag or nil
|
131
|
+
# if it is not.
|
132
|
+
def process_image_tag(tag)
|
133
|
+
parts = tag.split('|')
|
134
|
+
return if parts.size.zero?
|
135
|
+
|
136
|
+
name = parts[0].strip
|
137
|
+
path = if file = find_file(name)
|
138
|
+
::File.join @wiki.base_path, file.path
|
139
|
+
elsif name =~ /^https?:\/\/.+(jpg|png|gif|svg|bmp)$/i
|
140
|
+
name
|
141
|
+
end
|
142
|
+
|
143
|
+
if path
|
144
|
+
opts = parse_image_tag_options(tag)
|
145
|
+
|
146
|
+
containered = false
|
147
|
+
|
148
|
+
classes = [] # applied to whatever the outermost container is
|
149
|
+
attrs = [] # applied to the image
|
150
|
+
|
151
|
+
align = opts['align']
|
152
|
+
if opts['float']
|
153
|
+
containered = true
|
154
|
+
align ||= 'left'
|
155
|
+
if %w{left right}.include?(align)
|
156
|
+
classes << "float-#{align}"
|
157
|
+
end
|
158
|
+
elsif %w{top texttop middle absmiddle bottom absbottom baseline}.include?(align)
|
159
|
+
attrs << %{align="#{align}"}
|
160
|
+
elsif align
|
161
|
+
if %w{left center right}.include?(align)
|
162
|
+
containered = true
|
163
|
+
classes << "align-#{align}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
if width = opts['width']
|
168
|
+
if width =~ /^\d+(\.\d+)?(em|px)$/
|
169
|
+
attrs << %{width="#{width}"}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
if height = opts['height']
|
174
|
+
if height =~ /^\d+(\.\d+)?(em|px)$/
|
175
|
+
attrs << %{height="#{height}"}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
if alt = opts['alt']
|
180
|
+
attrs << %{alt="#{alt}"}
|
181
|
+
end
|
182
|
+
|
183
|
+
attr_string = attrs.size > 0 ? attrs.join(' ') + ' ' : ''
|
184
|
+
|
185
|
+
if opts['frame'] || containered
|
186
|
+
classes << 'frame' if opts['frame']
|
187
|
+
%{<span class="#{classes.join(' ')}">} +
|
188
|
+
%{<span>} +
|
189
|
+
%{<img src="#{path}" #{attr_string}/>} +
|
190
|
+
(alt ? %{<span>#{alt}</span>} : '') +
|
191
|
+
%{</span>} +
|
192
|
+
%{</span>}
|
193
|
+
else
|
194
|
+
%{<img src="#{path}" #{attr_string}/>}
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Parse any options present on the image tag and extract them into a
|
200
|
+
# Hash of option names and values.
|
201
|
+
#
|
202
|
+
# tag - The String tag contents (the stuff inside the double brackets).
|
203
|
+
#
|
204
|
+
# Returns the options Hash:
|
205
|
+
# key - The String option name.
|
206
|
+
# val - The String option value or true if it is a binary option.
|
207
|
+
def parse_image_tag_options(tag)
|
208
|
+
tag.split('|')[1..-1].inject({}) do |memo, attr|
|
209
|
+
parts = attr.split('=').map { |x| x.strip }
|
210
|
+
memo[parts[0]] = (parts.size == 1 ? true : parts[1])
|
211
|
+
memo
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Attempt to process the tag as a file link tag.
|
216
|
+
#
|
217
|
+
# tag - The String tag contents (the stuff inside the double
|
218
|
+
# brackets).
|
219
|
+
#
|
220
|
+
# Returns the String HTML if the tag is a valid file link tag or nil
|
221
|
+
# if it is not.
|
222
|
+
def process_file_link_tag(tag)
|
223
|
+
parts = tag.split('|')
|
224
|
+
return if parts.size.zero?
|
225
|
+
|
226
|
+
name = parts[0].strip
|
227
|
+
path = parts[1] && parts[1].strip
|
228
|
+
path = if path && file = find_file(path)
|
229
|
+
::File.join @wiki.base_path, file.path
|
230
|
+
elsif path =~ %r{^https?://}
|
231
|
+
path
|
232
|
+
else
|
233
|
+
nil
|
234
|
+
end
|
235
|
+
|
236
|
+
if name && path && file
|
237
|
+
%{<a href="#{::File.join @wiki.base_path, file.path}">#{name}</a>}
|
238
|
+
elsif name && path
|
239
|
+
%{<a href="#{path}">#{name}</a>}
|
240
|
+
else
|
241
|
+
nil
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Attempt to process the tag as a page link tag.
|
246
|
+
#
|
247
|
+
# tag - The String tag contents (the stuff inside the double
|
248
|
+
# brackets).
|
249
|
+
#
|
250
|
+
# Returns the String HTML if the tag is a valid page link tag or nil
|
251
|
+
# if it is not.
|
252
|
+
def process_page_link_tag(tag)
|
253
|
+
parts = tag.split('|')
|
254
|
+
parts.reverse! if @format == :mediawiki
|
255
|
+
|
256
|
+
name, page_name = *parts.compact.map(&:strip)
|
257
|
+
cname = @wiki.page_class.cname(page_name || name)
|
258
|
+
|
259
|
+
if name =~ %r{^https?://} && page_name.nil?
|
260
|
+
%{<a href="#{name}">#{name}</a>}
|
261
|
+
else
|
262
|
+
presence = "absent"
|
263
|
+
link_name = cname
|
264
|
+
page, extra = find_page_from_name(cname)
|
265
|
+
if page
|
266
|
+
link_name = @wiki.page_class.cname(page.name)
|
267
|
+
presence = "present"
|
268
|
+
end
|
269
|
+
link = ::File.join(@wiki.base_path, CGI.escape(link_name))
|
270
|
+
%{<a class="internal #{presence}" href="#{link}#{extra}">#{name}</a>}
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Find the given file in the repo.
|
275
|
+
#
|
276
|
+
# name - The String absolute or relative path of the file.
|
277
|
+
#
|
278
|
+
# Returns the Gollum::File or nil if none was found.
|
279
|
+
def find_file(name)
|
280
|
+
if name =~ /^\//
|
281
|
+
@wiki.file(name[1..-1], @version)
|
282
|
+
else
|
283
|
+
path = @dir == '.' ? name : ::File.join(@dir, name)
|
284
|
+
@wiki.file(path, @version)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Find a page from a given cname. If the page has an anchor (#) and has
|
289
|
+
# no match, strip the anchor and try again.
|
290
|
+
#
|
291
|
+
# cname - The String canonical page name.
|
292
|
+
#
|
293
|
+
# Returns a Gollum::Page instance if a page is found, or an Array of
|
294
|
+
# [Gollum::Page, String extra] if a page without the extra anchor data
|
295
|
+
# is found.
|
296
|
+
def find_page_from_name(cname)
|
297
|
+
if page = @wiki.page(cname)
|
298
|
+
return page
|
299
|
+
end
|
300
|
+
if pos = cname.index('#')
|
301
|
+
[@wiki.page(cname[0...pos]), cname[pos..-1]]
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
#########################################################################
|
306
|
+
#
|
307
|
+
# Code
|
308
|
+
#
|
309
|
+
#########################################################################
|
310
|
+
|
311
|
+
# Extract all code blocks into the codemap and replace with placeholders.
|
312
|
+
#
|
313
|
+
# data - The raw String data.
|
314
|
+
#
|
315
|
+
# Returns the placeholder'd String data.
|
316
|
+
def extract_code(data)
|
317
|
+
data.gsub!(/^``` ?([^\r\n]+)?\r?\n(.+?)\r?\n```\r?$/m) do
|
318
|
+
id = Digest::SHA1.hexdigest("#{$1}.#{$2}")
|
319
|
+
cached = check_cache(:code, id)
|
320
|
+
@codemap[id] = cached ?
|
321
|
+
{ :output => cached } :
|
322
|
+
{ :lang => $1, :code => $2 }
|
323
|
+
id
|
324
|
+
end
|
325
|
+
data
|
326
|
+
end
|
327
|
+
|
328
|
+
# Process all code from the codemap and replace the placeholders with the
|
329
|
+
# final HTML.
|
330
|
+
#
|
331
|
+
# data - The String data (with placeholders).
|
332
|
+
#
|
333
|
+
# Returns the marked up String data.
|
334
|
+
def process_code(data)
|
335
|
+
return data if data.nil? || data.size.zero? || @codemap.size.zero?
|
336
|
+
|
337
|
+
blocks = []
|
338
|
+
@codemap.each do |id, spec|
|
339
|
+
next if spec[:output] # cached
|
340
|
+
|
341
|
+
code = spec[:code]
|
342
|
+
if code.lines.all? { |line| line =~ /\A\r?\n\Z/ || line =~ /^( |\t)/ }
|
343
|
+
code.gsub!(/^( |\t)/m, '')
|
344
|
+
end
|
345
|
+
|
346
|
+
blocks << [spec[:lang], code]
|
347
|
+
end
|
348
|
+
|
349
|
+
highlighted = begin
|
350
|
+
blocks.map { |lang, code| Pygments.highlight(code, :lexer => lang) }
|
351
|
+
rescue ::RubyPython::PythonError
|
352
|
+
[]
|
353
|
+
end
|
354
|
+
|
355
|
+
@codemap.each do |id, spec|
|
356
|
+
body = spec[:output] || begin
|
357
|
+
if (body = highlighted.shift.to_s).size > 0
|
358
|
+
update_cache(:code, id, body)
|
359
|
+
body
|
360
|
+
else
|
361
|
+
"<pre><code>#{CGI.escapeHTML(spec[:code])}</code></pre>"
|
362
|
+
end
|
363
|
+
end
|
364
|
+
data.gsub!(id, body)
|
365
|
+
end
|
366
|
+
|
367
|
+
data
|
368
|
+
end
|
369
|
+
|
370
|
+
# Hook for getting the formatted value of extracted tag data.
|
371
|
+
#
|
372
|
+
# type - Symbol value identifying what type of data is being extracted.
|
373
|
+
# id - String SHA1 hash of original extracted tag data.
|
374
|
+
#
|
375
|
+
# Returns the String cached formatted data, or nil.
|
376
|
+
def check_cache(type, id)
|
377
|
+
end
|
378
|
+
|
379
|
+
# Hook for caching the formatted value of extracted tag data.
|
380
|
+
#
|
381
|
+
# type - Symbol value identifying what type of data is being extracted.
|
382
|
+
# id - String SHA1 hash of original extracted tag data.
|
383
|
+
# data - The String formatted value to be cached.
|
384
|
+
#
|
385
|
+
# Returns nothing.
|
386
|
+
def update_cache(type, id, data)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
data/lib/gollum/page.rb
ADDED
@@ -0,0 +1,374 @@
|
|
1
|
+
module Gollum
|
2
|
+
class Page
|
3
|
+
include Pagination
|
4
|
+
|
5
|
+
Wiki.page_class = self
|
6
|
+
|
7
|
+
VALID_PAGE_RE = /^(.+)\.(md|mkdn?|mdown|markdown|ronn)$/i
|
8
|
+
FORMAT_NAMES = { :markdown => "Markdown",
|
9
|
+
:ronn => "ronn" }
|
10
|
+
|
11
|
+
# Sets a Boolean determing whether this page is a historical version.
|
12
|
+
#
|
13
|
+
# Returns nothing.
|
14
|
+
attr_writer :historical
|
15
|
+
|
16
|
+
# Checks if a filename has a valid extension understood by GitHub::Markup.
|
17
|
+
#
|
18
|
+
# filename - String filename, like "Home.md".
|
19
|
+
#
|
20
|
+
# Returns the matching String basename of the file without the extension.
|
21
|
+
def self.valid_filename?(filename)
|
22
|
+
filename && filename.to_s =~ VALID_PAGE_RE && $1
|
23
|
+
end
|
24
|
+
|
25
|
+
# Checks if a filename has a valid extension understood by GitHub::Markup.
|
26
|
+
# Also, checks if the filename has no "_" in the front (such as
|
27
|
+
# _Footer.md).
|
28
|
+
#
|
29
|
+
# filename - String filename, like "Home.md".
|
30
|
+
#
|
31
|
+
# Returns the matching String basename of the file without the extension.
|
32
|
+
def self.valid_page_name?(filename)
|
33
|
+
match = valid_filename?(filename)
|
34
|
+
filename =~ /^_/ ? false : match
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: The format of a given filename.
|
38
|
+
#
|
39
|
+
# filename - The String filename.
|
40
|
+
#
|
41
|
+
# Returns the Symbol format of the page. One of:
|
42
|
+
# [ :markdown | :ronn ]
|
43
|
+
def self.format_for(filename)
|
44
|
+
case filename.to_s
|
45
|
+
when /\.(md|mkdn?|mdown|markdown)$/i
|
46
|
+
:markdown
|
47
|
+
when /\.ronn$/i
|
48
|
+
:ronn
|
49
|
+
else
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Reusable filter to turn a filename (without path) into a canonical name.
|
55
|
+
# Strips extension, converts spaces to dashes.
|
56
|
+
#
|
57
|
+
# Returns the filtered String.
|
58
|
+
def self.canonicalize_filename(filename)
|
59
|
+
filename.split('.')[0..-2].join('.').gsub('-', ' ')
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: Initialize a page.
|
63
|
+
#
|
64
|
+
# wiki - The Gollum::Wiki in question.
|
65
|
+
#
|
66
|
+
# Returns a newly initialized Gollum::Page.
|
67
|
+
def initialize(wiki)
|
68
|
+
@wiki = wiki
|
69
|
+
@blob = @footer = @sidebar = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: The on-disk filename of the page including extension.
|
73
|
+
#
|
74
|
+
# Returns the String name.
|
75
|
+
def filename
|
76
|
+
@blob && @blob.name
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public: The canonical page name without extension, and dashes converted
|
80
|
+
# to spaces.
|
81
|
+
#
|
82
|
+
# Returns the String name.
|
83
|
+
def name
|
84
|
+
self.class.canonicalize_filename(filename)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Public: If the first element of a formatted page is an <h1> tag it can
|
88
|
+
# be considered the title of the page and used in the display. If the
|
89
|
+
# first element is NOT an <h1> tag, the title will be constructed from the
|
90
|
+
# filename by stripping the extension and replacing any dashes with
|
91
|
+
# spaces.
|
92
|
+
#
|
93
|
+
# Returns the fully sanitized String title.
|
94
|
+
def title
|
95
|
+
doc = Nokogiri::HTML(%{<div id="gollum-root">} + self.formatted_data + %{</div>})
|
96
|
+
|
97
|
+
header =
|
98
|
+
case self.format
|
99
|
+
when :asciidoc
|
100
|
+
doc.css("div#gollum-root > div#header > h1:first-child")
|
101
|
+
when :org
|
102
|
+
doc.css("div#gollum-root > p.title:first-child")
|
103
|
+
when :pod
|
104
|
+
doc.css("div#gollum-root > a.dummyTopAnchor:first-child + h1")
|
105
|
+
when :rest
|
106
|
+
doc.css("div#gollum-root > div > div > h1:first-child")
|
107
|
+
else
|
108
|
+
doc.css("div#gollum-root > h1:first-child")
|
109
|
+
end
|
110
|
+
|
111
|
+
if !header.empty?
|
112
|
+
Sanitize.clean(header.to_html)
|
113
|
+
else
|
114
|
+
Sanitize.clean(name)
|
115
|
+
end.strip
|
116
|
+
end
|
117
|
+
|
118
|
+
# Public: The path of the page within the repo.
|
119
|
+
#
|
120
|
+
# Returns the String path.
|
121
|
+
attr_reader :path
|
122
|
+
|
123
|
+
# Public: The raw contents of the page.
|
124
|
+
#
|
125
|
+
# Returns the String data.
|
126
|
+
def raw_data
|
127
|
+
@blob && @blob.data
|
128
|
+
end
|
129
|
+
|
130
|
+
# Public: A text data encoded in specified encoding.
|
131
|
+
#
|
132
|
+
# encoding - An Encoding or nil
|
133
|
+
#
|
134
|
+
# Returns a character encoding aware String.
|
135
|
+
def text_data(encoding=nil)
|
136
|
+
if raw_data.respond_to?(:encoding)
|
137
|
+
raw_data.force_encoding(encoding || Encoding::UTF_8)
|
138
|
+
else
|
139
|
+
raw_data
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Public: The formatted contents of the page.
|
144
|
+
#
|
145
|
+
# Returns the String data.
|
146
|
+
def formatted_data(&block)
|
147
|
+
@blob && markup_class.render(historical?, &block)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Public: The format of the page.
|
151
|
+
#
|
152
|
+
# Returns the Symbol format of the page. One of:
|
153
|
+
# [ :markdown | : ronn ]
|
154
|
+
def format
|
155
|
+
self.class.format_for(@blob.name)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Gets the Gollum::Markup instance that will render this page's content.
|
159
|
+
#
|
160
|
+
# Returns a Gollum::Markup instance.
|
161
|
+
def markup_class
|
162
|
+
@markup_class ||= @wiki.markup_classes[format].new(self)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Public: The current version of the page.
|
166
|
+
#
|
167
|
+
# Returns the Grit::Commit.
|
168
|
+
attr_reader :version
|
169
|
+
|
170
|
+
# Public: All of the versions that have touched the Page.
|
171
|
+
#
|
172
|
+
# options - The options Hash:
|
173
|
+
# :page - The Integer page number (default: 1).
|
174
|
+
# :per_page - The Integer max count of items to return.
|
175
|
+
# :follow - Follow's a file across renames, but falls back
|
176
|
+
# to a slower Grit native call. (default: false)
|
177
|
+
#
|
178
|
+
# Returns an Array of Grit::Commit.
|
179
|
+
def versions(options = {})
|
180
|
+
if options[:follow]
|
181
|
+
options[:pretty] = 'raw'
|
182
|
+
options.delete :max_count
|
183
|
+
options.delete :skip
|
184
|
+
log = @wiki.repo.git.native "log", options, @wiki.ref, "--", @path
|
185
|
+
Grit::Commit.list_from_string(@wiki.repo, log)
|
186
|
+
else
|
187
|
+
@wiki.repo.log(@wiki.ref, @path, log_pagination_options(options))
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Public: The footer Page.
|
192
|
+
#
|
193
|
+
# Returns the footer Page or nil if none exists.
|
194
|
+
def footer
|
195
|
+
@footer ||= find_sub_page(:footer)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Public: The sidebar Page.
|
199
|
+
#
|
200
|
+
# Returns the sidebar Page or nil if none exists.
|
201
|
+
def sidebar
|
202
|
+
@sidebar ||= find_sub_page(:sidebar)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Gets a Boolean determining whether this page is a historical version.
|
206
|
+
# Historical pages are pulled using exact SHA hashes and format all links
|
207
|
+
# with rel="nofollow"
|
208
|
+
#
|
209
|
+
# Returns true if the page is pulled from a named branch or tag, or false.
|
210
|
+
def historical?
|
211
|
+
!!@historical
|
212
|
+
end
|
213
|
+
|
214
|
+
#########################################################################
|
215
|
+
#
|
216
|
+
# Class Methods
|
217
|
+
#
|
218
|
+
#########################################################################
|
219
|
+
|
220
|
+
# Convert a human page name into a canonical page name.
|
221
|
+
#
|
222
|
+
# name - The String human page name.
|
223
|
+
#
|
224
|
+
# Examples
|
225
|
+
#
|
226
|
+
# Page.cname("Bilbo Baggins")
|
227
|
+
# # => 'Bilbo-Baggins'
|
228
|
+
#
|
229
|
+
# Returns the String canonical name.
|
230
|
+
def self.cname(name)
|
231
|
+
name.respond_to?(:gsub) ?
|
232
|
+
name.gsub(%r{[ /<>]}, '-') :
|
233
|
+
''
|
234
|
+
end
|
235
|
+
|
236
|
+
# Convert a format Symbol into an extension String.
|
237
|
+
#
|
238
|
+
# format - The format Symbol.
|
239
|
+
#
|
240
|
+
# Returns the String extension (no leading period).
|
241
|
+
def self.format_to_ext(format)
|
242
|
+
case format
|
243
|
+
when :markdown then 'mkd'
|
244
|
+
when :ronn then 'ronn'
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
#########################################################################
|
249
|
+
#
|
250
|
+
# Internal Methods
|
251
|
+
#
|
252
|
+
#########################################################################
|
253
|
+
|
254
|
+
# The underlying wiki repo.
|
255
|
+
#
|
256
|
+
# Returns the Gollum::Wiki containing the page.
|
257
|
+
attr_reader :wiki
|
258
|
+
|
259
|
+
# Set the Grit::Commit version of the page.
|
260
|
+
#
|
261
|
+
# Returns nothing.
|
262
|
+
attr_writer :version
|
263
|
+
|
264
|
+
# Find a page in the given Gollum repo.
|
265
|
+
#
|
266
|
+
# name - The human or canonical String page name to find.
|
267
|
+
# version - The String version ID to find.
|
268
|
+
#
|
269
|
+
# Returns a Gollum::Page or nil if the page could not be found.
|
270
|
+
def find(name, version)
|
271
|
+
map = @wiki.tree_map_for(version.to_s)
|
272
|
+
if page = find_page_in_tree(map, name)
|
273
|
+
page.version = version.is_a?(Grit::Commit) ?
|
274
|
+
version : @wiki.commit_for(version)
|
275
|
+
page.historical = page.version.to_s == version.to_s
|
276
|
+
page
|
277
|
+
end
|
278
|
+
rescue Grit::GitRuby::Repository::NoSuchShaFound
|
279
|
+
end
|
280
|
+
|
281
|
+
# Find a page in a given tree.
|
282
|
+
#
|
283
|
+
# map - The Array tree map from Wiki#tree_map.
|
284
|
+
# name - The canonical String page name.
|
285
|
+
# checked_dir - Optional String of the directory a matching page needs
|
286
|
+
# to be in. The string should
|
287
|
+
#
|
288
|
+
# Returns a Gollum::Page or nil if the page could not be found.
|
289
|
+
def find_page_in_tree(map, name, checked_dir = nil)
|
290
|
+
return nil if !map || name.to_s.empty?
|
291
|
+
if checked_dir = BlobEntry.normalize_dir(checked_dir)
|
292
|
+
checked_dir.downcase!
|
293
|
+
end
|
294
|
+
|
295
|
+
map.each do |entry|
|
296
|
+
next if entry.name.to_s.empty?
|
297
|
+
next unless checked_dir.nil? || entry.dir.downcase == checked_dir
|
298
|
+
next unless page_match(name, entry.name)
|
299
|
+
return entry.page(@wiki, @version)
|
300
|
+
end
|
301
|
+
|
302
|
+
return nil # nothing was found
|
303
|
+
end
|
304
|
+
|
305
|
+
# Populate the Page with information from the Blob.
|
306
|
+
#
|
307
|
+
# blob - The Grit::Blob that contains the info.
|
308
|
+
# path - The String directory path of the page file.
|
309
|
+
#
|
310
|
+
# Returns the populated Gollum::Page.
|
311
|
+
def populate(blob, path=nil)
|
312
|
+
@blob = blob
|
313
|
+
@path = "#{path}/#{blob.name}"[1..-1]
|
314
|
+
self
|
315
|
+
end
|
316
|
+
|
317
|
+
# The full directory path for the given tree.
|
318
|
+
#
|
319
|
+
# treemap - The Hash treemap containing parentage information.
|
320
|
+
# tree - The Grit::Tree for which to compute the path.
|
321
|
+
#
|
322
|
+
# Returns the String path.
|
323
|
+
def tree_path(treemap, tree)
|
324
|
+
if ptree = treemap[tree]
|
325
|
+
tree_path(treemap, ptree) + '/' + tree.name
|
326
|
+
else
|
327
|
+
''
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Compare the canonicalized versions of the two names.
|
332
|
+
#
|
333
|
+
# name - The human or canonical String page name.
|
334
|
+
# filename - the String filename on disk (including extension).
|
335
|
+
#
|
336
|
+
# Returns a Boolean.
|
337
|
+
def page_match(name, filename)
|
338
|
+
if match = self.class.valid_filename?(filename)
|
339
|
+
Page.cname(name).downcase == Page.cname(match).downcase
|
340
|
+
else
|
341
|
+
false
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Loads a sub page. Sub page nanes (footers) are prefixed with
|
346
|
+
# an underscore to distinguish them from other Pages.
|
347
|
+
#
|
348
|
+
# name - String page name.
|
349
|
+
#
|
350
|
+
# Returns the Page or nil if none exists.
|
351
|
+
def find_sub_page(name)
|
352
|
+
return nil unless self.version
|
353
|
+
return nil if self.filename =~ /^_/
|
354
|
+
name = "_#{name.to_s.capitalize}"
|
355
|
+
return nil if page_match(name, self.filename)
|
356
|
+
|
357
|
+
dirs = self.path.split('/')
|
358
|
+
dirs.pop
|
359
|
+
map = @wiki.tree_map_for(self.version.id)
|
360
|
+
while !dirs.empty?
|
361
|
+
if page = find_page_in_tree(map, name, dirs.join('/'))
|
362
|
+
return page
|
363
|
+
end
|
364
|
+
dirs.pop
|
365
|
+
end
|
366
|
+
|
367
|
+
find_page_in_tree(map, name, '')
|
368
|
+
end
|
369
|
+
|
370
|
+
def inspect
|
371
|
+
%(#<#{self.class.name}:#{object_id} #{name} (#{format}) @wiki=#{@wiki.repo.path.inspect}>)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|