rtfmd 0.10301.17
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.md +93 -0
- data/LICENSE +21 -0
- data/README.md +393 -0
- data/bin/rtfm +0 -0
- data/bin/rtfmd +4 -0
- data/config/rtfmd.ru +33 -0
- data/docs/sanitization.md +32 -0
- data/lib/gollum.rb +39 -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 +96 -0
- data/lib/gollum/frontend/indexapp.rb +17 -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/ronn.css +40 -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/index.mustache +25 -0
- data/lib/gollum/frontend/templates/layout.mustache +28 -0
- data/lib/gollum/frontend/templates/page.mustache +34 -0
- data/lib/gollum/frontend/templates/pages.mustache +31 -0
- data/lib/gollum/frontend/views/error.rb +7 -0
- data/lib/gollum/frontend/views/index.rb +21 -0
- data/lib/gollum/frontend/views/layout.rb +28 -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
- data/rtfmd.gemspec +47 -0
- metadata +291 -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
|