RTFMd 0.10301.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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