gollum-lib 1.0.9 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of gollum-lib might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 12c5ce0dd57482d2d3264712ac15839e67e6398d
4
- data.tar.gz: 3b0b498880df3a86d6fb8a3d09acab8ed629043c
3
+ metadata.gz: 2e62158d0795431631a6a845ac26a221535c8604
4
+ data.tar.gz: 35c5ad577b84b262fd492dfc2e5b30f92d5f97ee
5
5
  SHA512:
6
- metadata.gz: 3d8a845aadfde384b2322851058937d9eb0724d9098b072f604eec6f41054eb3faf86b7430604caa99fd7ea2298db3f8c94b03a53215603fd435eccba956af4a
7
- data.tar.gz: 25974065b8c080aa366aa345891aa51475d43aa424e73f5c08c57a807fcbaeccd07d7d2bdfcb530d682c321d6ce98b23eb5275c4746cef37aed0d293bf2a7fda
6
+ metadata.gz: 54dd123e3f0cc5c2dc7df860a289d1ca3a46edd360c4d8ecf0dabf3e9d6ee3b2e68383c4b21b3d87f82e1309df3cfd24515e07e7b219a518557e1043ea2dbaa2
7
+ data.tar.gz: 5bd2a239672e66b8a0364acdee7806e1be851b821dfb5b471328d8ffeb8e8581bfea27ee5641363138898e4a6c51530fd5c0a948277190cfe779f11f558716af
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
- gem 'rake', '~> 10.0.3'
data/README.md CHANGED
@@ -20,7 +20,7 @@ Gollum-lib follows the rules of [Semantic Versioning](http://semver.org/) and us
20
20
  ## SYSTEM REQUIREMENTS
21
21
 
22
22
  - Python 2.5+ (2.7.3 recommended)
23
- - Ruby 1.8.7+ (1.9.3 recommended)
23
+ - Ruby 1.9.3+ (1.9.3 recommended)
24
24
  - Unix like operating system (OS X, Ubuntu, Debian, and more)
25
25
  - Will not work on Windows (because of [grit](https://github.com/github/grit))
26
26
 
data/gollum-lib.gemspec CHANGED
@@ -5,8 +5,8 @@ Gem::Specification.new do |s|
5
5
  s.required_ruby_version = ">= 1.9"
6
6
 
7
7
  s.name = 'gollum-lib'
8
- s.version = '1.0.9'
9
- s.date = '2013-11-02'
8
+ s.version = '2.0.0'
9
+ s.date = '2014-02-20'
10
10
  s.rubyforge_project = 'gollum-lib'
11
11
  s.license = 'MIT'
12
12
 
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
 
25
25
  s.add_dependency('gitlab-grit', '2.6.0')
26
26
  s.add_dependency('github-markup', ['>= 0.7.5', '< 1.0.0'])
27
- s.add_dependency('pygments.rb', '~> 0.5.2')
27
+ s.add_dependency('rouge', '~> 1.3.1')
28
28
  s.add_dependency('sanitize', '~> 2.0.6')
29
29
  s.add_dependency('nokogiri', '~> 1.6.0')
30
30
  s.add_dependency('stringex', '~> 2.1.0')
@@ -62,6 +62,16 @@ Gem::Specification.new do |s|
62
62
  lib/gollum-lib/committer.rb
63
63
  lib/gollum-lib/file.rb
64
64
  lib/gollum-lib/file_view.rb
65
+ lib/gollum-lib/filter.rb
66
+ lib/gollum-lib/filter/code.rb
67
+ lib/gollum-lib/filter/metadata.rb
68
+ lib/gollum-lib/filter/plain_text.rb
69
+ lib/gollum-lib/filter/remote_code.rb
70
+ lib/gollum-lib/filter/render.rb
71
+ lib/gollum-lib/filter/sanitize.rb
72
+ lib/gollum-lib/filter/tags.rb
73
+ lib/gollum-lib/filter/toc.rb
74
+ lib/gollum-lib/filter/wsd.rb
65
75
  lib/gollum-lib/git_access.rb
66
76
  lib/gollum-lib/gitcode.rb
67
77
  lib/gollum-lib/grit_ext.rb
@@ -71,9 +81,7 @@ Gem::Specification.new do |s|
71
81
  lib/gollum-lib/markups.rb
72
82
  lib/gollum-lib/page.rb
73
83
  lib/gollum-lib/pagination.rb
74
- lib/gollum-lib/remote_code.rb
75
84
  lib/gollum-lib/sanitization.rb
76
- lib/gollum-lib/web_sequence_diagram.rb
77
85
  lib/gollum-lib/wiki.rb
78
86
  licenses/licenses.txt
79
87
  ]
data/lib/gollum-lib.rb CHANGED
@@ -23,7 +23,7 @@ require File.expand_path('../gollum-lib/file_view', __FILE__)
23
23
  require File.expand_path('../gollum-lib/markup', __FILE__)
24
24
  require File.expand_path('../gollum-lib/markups', __FILE__)
25
25
  require File.expand_path('../gollum-lib/sanitization', __FILE__)
26
- require File.expand_path('../gollum-lib/web_sequence_diagram', __FILE__)
26
+ require File.expand_path('../gollum-lib/filter', __FILE__)
27
27
 
28
28
  # Set ruby to UTF-8 mode
29
29
  # This is required for Ruby 1.8.7 which gollum still supports.
@@ -31,7 +31,7 @@ $KCODE = 'U' if RUBY_VERSION[0,3] == '1.8'
31
31
 
32
32
  module Gollum
33
33
  module Lib
34
- VERSION = '1.0.9'
34
+ VERSION = '2.0.0'
35
35
  end
36
36
 
37
37
  def self.assets_path
@@ -0,0 +1,78 @@
1
+ # A "filter", in Gollum-speak, is an object which extracts tokens from an
2
+ # input stream of an arbitrary markup language, then replaces them with a
3
+ # final form in a rendered form of the same document. Filters are composed
4
+ # into a "filter chain", which forms the order in which filters are applied
5
+ # in both the extraction and processing phases (processing happens in the
6
+ # reverse order to extraction). A single instance of a filter class is used
7
+ # for both the extraction and processing, so you can store internal state
8
+ # from the extraction phase for use in the processing phase.
9
+ #
10
+ # Any class which is to be used as a filter must have an `initialize` method
11
+ # which takes a single mandatory argument (the instance of the `Markup`
12
+ # class we're being called from), and must implement two methods: `extract`
13
+ # and `process`, both of which must take a string as input and return a
14
+ # (possibly modified) form of that string as output.
15
+ #
16
+ # An example rendering session: consider a filter chain with three filters
17
+ # in it, :A, :B, and :C (filter chains are specified as symbols, which are
18
+ # taken to be class names within the Gollum::Filter namespace). An
19
+ # equivalent of the following will take place:
20
+ #
21
+ # a = Gollum::Filter.const_get(:A).new
22
+ # b = Gollum::Filter.const_get(:B).new
23
+ # c = Gollum::Filter.const_get(:C).new
24
+ #
25
+ # data = "<some markup>"
26
+ #
27
+ # data = a.extract(data)
28
+ # data = b.extract(data)
29
+ # data = c.extract(data)
30
+ #
31
+ # data = c.process(data)
32
+ # data = b.process(data)
33
+ # data = a.process(data)
34
+ #
35
+ # # `data` now contains the rendered document, ready for processing
36
+ #
37
+ # Note how the extraction steps go in the order of the filter chain, while
38
+ # the processing steps go in the reverse order. There hasn't (yet) been a
39
+ # case where that is a huge problem.
40
+ #
41
+ # If your particular filter doesn't need to do something with either the
42
+ # original markup or rendered forms, you can simply define the relevant
43
+ # method to be a pass-through (`def extract(d) d; end`), but you *must*
44
+ # define both methods yourself.
45
+ #
46
+ module Gollum
47
+ class Filter
48
+ include Gollum::Helpers
49
+
50
+ # Setup the object. Sets `@markup` to be the instance of Gollum::Markup that
51
+ # is running this filter chain, and sets `@map` to be an empty hash (for use
52
+ # in your extract/process operations).
53
+ def initialize(markup)
54
+ @markup = markup
55
+ @map = {}
56
+ end
57
+
58
+ def extract(_d)
59
+ raise RuntimeError,
60
+ "#{self.class} has not implemented ##extract!"
61
+ end
62
+
63
+ def process(_d)
64
+ raise RuntimeError,
65
+ "#{self.class} has not implemented ##process!"
66
+ end
67
+
68
+ protected
69
+ # Render a (presumably) non-fatal error as HTML
70
+ #
71
+ def html_error(message)
72
+ "<p class=\"gollum-error\">#{message}</p>"
73
+ end
74
+ end
75
+ end
76
+
77
+ # Load all standard filters
78
+ Dir[File.expand_path('../filter/*.rb', __FILE__)].each { |f| require f }
@@ -0,0 +1,123 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ # Code
4
+ #
5
+ # Render a block of code using the Rouge syntax-highlighter.
6
+ class Gollum::Filter::Code < Gollum::Filter
7
+ def extract(data)
8
+ return data if @markup.format == :txt
9
+ data.gsub!(/^([ \t]*)(~~~+) ?([^\r\n]+)?\r?\n(.+?)\r?\n\1(~~~+)[ \t\r]*$/m) do
10
+ m_indent = $1
11
+ m_start = $2 # ~~~
12
+ m_lang = $3
13
+ m_code = $4
14
+ m_end = $5 # ~~~
15
+
16
+ # start and finish tilde fence must be the same length
17
+ next '' if m_start.length != m_end.length
18
+
19
+ lang = m_lang ? m_lang.strip : nil
20
+ id = Digest::SHA1.hexdigest("#{lang}.#{m_code}")
21
+ cached = @markup.check_cache(:code, id)
22
+
23
+ # extract lang from { .ruby } or { #stuff .ruby .indent }
24
+ # see http://johnmacfarlane.net/pandoc/README.html#delimited-code-blocks
25
+
26
+ if lang
27
+ lang = lang.match(/\.([^}\s]+)/)
28
+ lang = lang[1] unless lang.nil?
29
+ end
30
+
31
+ @map[id] = cached ?
32
+ { :output => cached } :
33
+ { :lang => lang, :code => m_code, :indent => m_indent }
34
+
35
+ "#{m_indent}#{id}" # print the SHA1 ID with the proper indentation
36
+ end
37
+
38
+ data.gsub!(/^([ \t]*)``` ?([^\r\n]+)?\r?\n(.+?)\r?\n\1```[ \t]*\r?$/m) do
39
+ lang = $2 ? $2.strip : nil
40
+ id = Digest::SHA1.hexdigest("#{lang}.#{$3}")
41
+ cached = @markup.check_cache(:code, id)
42
+ @map[id] = cached ?
43
+ { :output => cached } :
44
+ { :lang => lang, :code => $3, :indent => $1 }
45
+ "#{$1}#{id}" # print the SHA1 ID with the proper indentation
46
+ end
47
+
48
+ data
49
+ end
50
+
51
+ # Process all code from the codemap and replace the placeholders with the
52
+ # final HTML.
53
+ #
54
+ # data - The String data (with placeholders).
55
+ # encoding - Encoding Constant or String.
56
+ #
57
+ # Returns the marked up String data.
58
+ def process(data)
59
+ return data if data.nil? || data.size.zero? || @map.size.zero?
60
+
61
+ blocks = []
62
+
63
+ @map.each do |id, spec|
64
+ next if spec[:output] # cached
65
+
66
+ code = spec[:code]
67
+
68
+ remove_leading_space(code, /^#{spec[:indent]}/m)
69
+ remove_leading_space(code, /^( |\t)/m)
70
+
71
+ blocks << [spec[:lang], code]
72
+ end
73
+
74
+ highlighted = []
75
+ blocks.each do |lang, code|
76
+ encoding = @markup.encoding || 'utf-8'
77
+ begin
78
+ if Rouge::Lexer.find(lang).nil?
79
+ lexer = Rouge::Lexers::PlainText.new
80
+ formatter = Rouge::Formatters::HTML.new(:wrap => false)
81
+ hl_code = formatter.format(lexer.lex(code))
82
+ hl_code = "<pre class='highlight'><span class='err'>#{CGI.escapeHTML(hl_code)}</span></pre>"
83
+ else
84
+ hl_code = Rouge.highlight(code, lang, 'html')
85
+ end
86
+ rescue
87
+ hl_code = code
88
+ end
89
+ highlighted << hl_code
90
+ end
91
+
92
+ @map.each do |id, spec|
93
+ body = spec[:output] || begin
94
+ if (body = highlighted.shift.to_s).size > 0
95
+ @markup.update_cache(:code, id, body)
96
+ body
97
+ else
98
+ "<pre><code>#{CGI.escapeHTML(spec[:code])}</code></pre>"
99
+ end
100
+ end
101
+ data.gsub!(id) do
102
+ body
103
+ end
104
+ end
105
+
106
+ data
107
+ end
108
+
109
+ private
110
+ # Remove the leading space from a code block. Leading space
111
+ # is only removed if every single line in the block has leading
112
+ # whitespace.
113
+ #
114
+ # code - The code block to remove spaces from
115
+ # regex - A regex to match whitespace
116
+ def remove_leading_space(code, regex)
117
+ if code.lines.all? { |line| line =~ /\A\r?\n\Z/ || line =~ regex }
118
+ code.gsub!(regex) do
119
+ ''
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,27 @@
1
+ # Extract metadata for data and build metadata table. Metadata consists of
2
+ # key/value pairs in "key:value" format found between markers. Each
3
+ # key/value pair must be on its own line. Internal whitespace in keys and
4
+ # values is preserved, but external whitespace is ignored.
5
+ #
6
+ # Because ri and ruby 1.8.7 are awesome, the markers can't
7
+ # be included in this documentation without triggering
8
+ # `Unhandled special: Special: type=17`
9
+ # Please read the source code for the exact markers
10
+ class Gollum::Filter::Metadata < Gollum::Filter
11
+ def extract(data)
12
+ # The markers are `<!-- ---` and `-->`
13
+ data.gsub(/\<\!--+\s+---(.*?)--+\>/m) do
14
+ @markup.metadata ||= {}
15
+ # Split untrusted input on newlines, then remove bits that look like
16
+ # HTML elements before parsing each line.
17
+ $1.split("\n").each do |line|
18
+ line.gsub!(/<[^>]*>/, '')
19
+ k, v = line.split(':', 2)
20
+ @markup.metadata[k.strip] = (v ? v.strip : '') if k
21
+ end
22
+ ''
23
+ end
24
+ end
25
+
26
+ def process(d) d; end
27
+ end
@@ -0,0 +1,15 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ # Plain Text
4
+ #
5
+ # Render plain text documents in a <pre> block without any special markup.
6
+
7
+ class Gollum::Filter::PlainText < Gollum::Filter
8
+
9
+ def extract(data)
10
+ @markup.format == :txt ? "<pre>#{CGI.escapeHTML(data)}</pre>" : data
11
+ end
12
+
13
+ def process(data); data ; end
14
+
15
+ end
@@ -0,0 +1,61 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'net/http'
3
+ require 'net/https' # ruby 1.8.7 fix, remove at upgrade
4
+ require 'uri'
5
+ require 'open-uri'
6
+
7
+ # Remote code - fetch code from url and replace the contents to a
8
+ # code-block that gets run the next parse.
9
+ # Acceptable formats:
10
+ # ```language:local-file.ext```
11
+ # ```language:/abs/other-file.ext```
12
+ # ```language:https://example.com/somefile.txt```
13
+ #
14
+ class Gollum::Filter::RemoteCode < Gollum::Filter
15
+ def extract(data)
16
+ return data if @markup.format == :txt
17
+ data.gsub /^[ \t]*``` ?([^:\n\r]+):((http)?[^`\n\r]+)```/ do
18
+ language = $1
19
+ uri = $2
20
+ protocol = $3
21
+
22
+ # Detect local file
23
+ if protocol.nil?
24
+ if file = @markup.find_file(uri, @markup.wiki.ref)
25
+ contents = file.raw_data
26
+ else
27
+ # How do we communicate a render error?
28
+ next html_error("File not found: #{CGI::escapeHTML(uri)}")
29
+ end
30
+ else
31
+ contents = req(uri)
32
+ end
33
+
34
+ "```#{language}\n#{contents}\n```\n"
35
+ end
36
+ end
37
+
38
+ def process(d) d; end
39
+
40
+ private
41
+
42
+ def req uri, cut = 1
43
+ uri = URI(uri)
44
+ return "Too many redirects or retries" if cut >= 10
45
+ http = Net::HTTP.new uri.host, uri.port
46
+ http.use_ssl = true
47
+ resp = http.get uri.path, {
48
+ 'Accept' => 'text/plain',
49
+ 'Cache-Control' => 'no-cache',
50
+ 'Connection' => 'keep-alive',
51
+ 'User-Agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0'
52
+ }
53
+ code = resp.code.to_i
54
+ return resp.body if code == 200
55
+ return "Not Found" if code == 404
56
+ return "Unhandled Response Code #{code}" unless code == 304 or not resp.header['location'].nil?
57
+ loc = URI.parse resp.header['location']
58
+ uri2 = loc.relative? ? (uri + loc) : loc # overloads (+)
59
+ req uri2, (cut + 1)
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ class Gollum::Filter::Render < Gollum::Filter
4
+ def extract(data)
5
+ begin
6
+ data = GitHub::Markup.render(@markup.name, data)
7
+ if data.nil?
8
+ raise "There was an error converting #{@markup.name} to HTML."
9
+ end
10
+ rescue Object => e
11
+ data = html_error("Failed to render page: #{e.message}")
12
+ end
13
+
14
+ data
15
+ end
16
+
17
+ def process(d) d; end
18
+ end
@@ -0,0 +1,16 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ class Gollum::Filter::Sanitize < Gollum::Filter
4
+ def extract(d) d; end
5
+
6
+ def process(data)
7
+ if @markup.sanitize
8
+ doc = Nokogiri::HTML::DocumentFragment.parse(data)
9
+ doc = @markup.sanitize.clean_node!(doc)
10
+
11
+ doc.to_xml(@markup.to_xml_opts)
12
+ else
13
+ data
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,292 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+
3
+ # Render all tags (things in double-square-brackets). This one's a biggie.
4
+ class Gollum::Filter::Tags < Gollum::Filter
5
+ # Extract all tags into the tagmap and replace with placeholders.
6
+ def extract(data)
7
+ return data if @markup.format == :txt || @markup.format == :asciidoc
8
+ data.gsub!(/(.?)\[\[(.+?)\]\]([^\[]?)/m) do
9
+ if $1 == "'" && $3 != "'"
10
+ "[[#{$2}]]#{$3}"
11
+ elsif $2.include?('][')
12
+ if $2[0..4] == 'file:'
13
+ pre = $1
14
+ post = $3
15
+ parts = $2.split('][')
16
+ parts[0][0..4] = ""
17
+ link = "#{parts[1]}|#{parts[0].sub(/\.org/,'')}"
18
+ id = Digest::SHA1.hexdigest(link)
19
+ @map[id] = link
20
+ "#{pre}#{id}#{post}"
21
+ else
22
+ $&
23
+ end
24
+ else
25
+ id = Digest::SHA1.hexdigest($2)
26
+ @map[id] = $2
27
+ "#{$1}#{id}#{$3}"
28
+ end
29
+ end
30
+ data
31
+ end
32
+
33
+ # Process all tags from the tagmap and replace the placeholders with the
34
+ # final markup.
35
+ def process(data)
36
+ @map.each do |id, tag|
37
+ # If it's preformatted, just put the tag back
38
+ if is_preformatted?(data, id)
39
+ data.gsub!(id) do
40
+ "[[#{tag}]]"
41
+ end
42
+ else
43
+ data.gsub!(id) do
44
+ process_tag(tag).gsub('%2F', '/')
45
+ end
46
+ end
47
+ end
48
+
49
+ data
50
+ end
51
+
52
+ private
53
+ # Find `id` within `data` and determine if it's within
54
+ # preformatted tags.
55
+ #
56
+ # data - The String data (with placeholders).
57
+ # id - The String SHA1 hash.
58
+ PREFORMATTED_TAGS = %w(code tt)
59
+ def is_preformatted?(data, id)
60
+ doc = Nokogiri::HTML::DocumentFragment.parse(data)
61
+ node = doc.search("[text()*='#{id}']").first
62
+ node && (PREFORMATTED_TAGS.include?(node.name) ||
63
+ node.ancestors.any? { |a| PREFORMATTED_TAGS.include?(a.name) })
64
+ end
65
+
66
+ # Process a single tag into its final HTML form.
67
+ #
68
+ # tag - The String tag contents (the stuff inside the double
69
+ # brackets).
70
+ #
71
+ # Returns the String HTML version of the tag.
72
+ def process_tag(tag)
73
+ if tag =~ /^_TOC_$/
74
+ %{[[#{tag}]]}
75
+ elsif tag =~ /^_$/
76
+ %{<div class="clearfloats"></div>}
77
+ elsif html = process_include_tag(tag)
78
+ html
79
+ elsif html = process_image_tag(tag)
80
+ html
81
+ elsif html = process_file_link_tag(tag)
82
+ html
83
+ else
84
+ process_page_link_tag(tag)
85
+ end
86
+ end
87
+
88
+ # Attempt to process the tag as an include tag
89
+ #
90
+ # tag - The String tag contents (the stuff inside the double brackets).
91
+ #
92
+ # Returns the String HTML if the tag is a valid image tag or nil
93
+ # if it is not.
94
+ #
95
+ def process_include_tag(tag)
96
+ return unless /^include:/.match(tag)
97
+ page_name = tag[8..-1]
98
+ resolved_page_name = ::File.expand_path(page_name, "/"+@markup.dir)
99
+
100
+ if @markup.include_levels > 0
101
+ page = find_page_from_name(resolved_page_name)
102
+ if page
103
+ page.formatted_data(@markup.encoding, @markup.include_levels-1)
104
+ else
105
+ html_error("Cannot include #{process_page_link_tag(resolved_page_name)} - does not exist yet")
106
+ end
107
+ else
108
+ html_error("Too many levels of included pages, will not include #{process_page_link_tag(resolved_page_name)}")
109
+ end
110
+ end
111
+
112
+ # Attempt to process the tag as an image tag.
113
+ #
114
+ # tag - The String tag contents (the stuff inside the double brackets).
115
+ #
116
+ # Returns the String HTML if the tag is a valid image tag or nil
117
+ # if it is not.
118
+ def process_image_tag(tag)
119
+ parts = tag.split('|')
120
+ return if parts.size.zero?
121
+
122
+ name = parts[0].strip
123
+ path = if file = @markup.find_file(name)
124
+ ::File.join @markup.wiki.base_path, file.path
125
+ elsif name =~ /^https?:\/\/.+(jpg|png|gif|svg|bmp)$/i
126
+ name
127
+ end
128
+
129
+ if path
130
+ opts = parse_image_tag_options(tag)
131
+
132
+ containered = false
133
+
134
+ classes = [] # applied to whatever the outermost container is
135
+ attrs = [] # applied to the image
136
+
137
+ align = opts['align']
138
+ if opts['float']
139
+ containered = true
140
+ align ||= 'left'
141
+ if %w{left right}.include?(align)
142
+ classes << "float-#{align}"
143
+ end
144
+ elsif %w{top texttop middle absmiddle bottom absbottom baseline}.include?(align)
145
+ attrs << %{align="#{align}"}
146
+ elsif align
147
+ if %w{left center right}.include?(align)
148
+ containered = true
149
+ classes << "align-#{align}"
150
+ end
151
+ end
152
+
153
+ if width = opts['width']
154
+ if width =~ /^\d+(\.\d+)?(em|px)$/
155
+ attrs << %{width="#{width}"}
156
+ end
157
+ end
158
+
159
+ if height = opts['height']
160
+ if height =~ /^\d+(\.\d+)?(em|px)$/
161
+ attrs << %{height="#{height}"}
162
+ end
163
+ end
164
+
165
+ if alt = opts['alt']
166
+ attrs << %{alt="#{alt}"}
167
+ end
168
+
169
+ attr_string = attrs.size > 0 ? attrs.join(' ') + ' ' : ''
170
+
171
+ if opts['frame'] || containered
172
+ classes << 'frame' if opts['frame']
173
+ %{<span class="#{classes.join(' ')}">} +
174
+ %{<span>} +
175
+ %{<img src="#{path}" #{attr_string}/>} +
176
+ (alt ? %{<span>#{alt}</span>} : '') +
177
+ %{</span>} +
178
+ %{</span>}
179
+ else
180
+ %{<img src="#{path}" #{attr_string}/>}
181
+ end
182
+ end
183
+ end
184
+
185
+ # Parse any options present on the image tag and extract them into a
186
+ # Hash of option names and values.
187
+ #
188
+ # tag - The String tag contents (the stuff inside the double brackets).
189
+ #
190
+ # Returns the options Hash:
191
+ # key - The String option name.
192
+ # val - The String option value or true if it is a binary option.
193
+ def parse_image_tag_options(tag)
194
+ tag.split('|')[1..-1].inject({}) do |memo, attr|
195
+ parts = attr.split('=').map { |x| x.strip }
196
+ memo[parts[0]] = (parts.size == 1 ? true : parts[1])
197
+ memo
198
+ end
199
+ end
200
+
201
+ # Attempt to process the tag as a file link tag.
202
+ #
203
+ # tag - The String tag contents (the stuff inside the double
204
+ # brackets).
205
+ #
206
+ # Returns the String HTML if the tag is a valid file link tag or nil
207
+ # if it is not.
208
+ def process_file_link_tag(tag)
209
+ parts = tag.split('|')
210
+ return if parts.size.zero?
211
+
212
+ name = parts[0].strip
213
+ path = parts[1] && parts[1].strip
214
+ path = if path && file = @markup.find_file(path)
215
+ ::File.join @markup.wiki.base_path, file.path
216
+ elsif path =~ %r{^https?://}
217
+ path
218
+ else
219
+ nil
220
+ end
221
+
222
+ if name && path && file
223
+ %{<a href="#{::File.join @markup.wiki.base_path, file.path}">#{name}</a>}
224
+ elsif name && path
225
+ %{<a href="#{path}">#{name}</a>}
226
+ else
227
+ nil
228
+ end
229
+ end
230
+
231
+ # Attempt to process the tag as a page link tag.
232
+ #
233
+ # tag - The String tag contents (the stuff inside the double
234
+ # brackets).
235
+ #
236
+ # Returns the String HTML if the tag is a valid page link tag or nil
237
+ # if it is not.
238
+ def process_page_link_tag(tag)
239
+ parts = tag.split('|')
240
+ parts.reverse! if @markup.format == :mediawiki
241
+
242
+ name, page_name = *parts.compact.map(&:strip)
243
+ cname = @markup.wiki.page_class.cname(page_name || name)
244
+
245
+ if name =~ %r{^https?://} && page_name.nil?
246
+ %{<a href="#{name}">#{name}</a>}
247
+ else
248
+ presence = "absent"
249
+ link_name = cname
250
+ page, extra = find_page_from_name(cname)
251
+ if page
252
+ link_name = @markup.wiki.page_class.cname(page.name)
253
+ presence = "present"
254
+ end
255
+ link = ::File.join(@markup.wiki.base_path, page ? page.escaped_url_path : CGI.escape(link_name))
256
+
257
+ # //page is invalid
258
+ # strip all duplicate forward slashes using helpers.rb trim_leading_slash
259
+ # //page => /page
260
+ link = trim_leading_slash link
261
+
262
+ %{<a class="internal #{presence}" href="#{link}#{extra}">#{name}</a>}
263
+ end
264
+ end
265
+
266
+ # Find a page from a given cname. If the page has an anchor (#) and has
267
+ # no match, strip the anchor and try again.
268
+ #
269
+ # cname - The String canonical page name including path.
270
+ #
271
+ # Returns a Gollum::Page instance if a page is found, or an Array of
272
+ # [Gollum::Page, String extra] if a page without the extra anchor data
273
+ # is found.
274
+ def find_page_from_name(cname)
275
+ slash = cname.rindex('/')
276
+
277
+ unless slash.nil?
278
+ name = cname[slash+1..-1]
279
+ path = cname[0..slash]
280
+ page = @markup.wiki.paged(name, path)
281
+ else
282
+ page = @markup.wiki.paged(cname, '/') || @markup.wiki.page(cname)
283
+ end
284
+
285
+ if page
286
+ return page
287
+ end
288
+ if pos = cname.index('#')
289
+ [@markup.wiki.page(cname[0...pos]), cname[pos..-1]]
290
+ end
291
+ end
292
+ end