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