gitlab-gollum-lib 1.1.0 → 4.2.7

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -3
  3. data/HISTORY.md +25 -0
  4. data/LICENSE +1 -1
  5. data/README.md +24 -312
  6. data/Rakefile +32 -16
  7. data/gemspec.rb +110 -0
  8. data/gollum-lib.gemspec +8 -75
  9. data/gollum-lib_java.gemspec +4 -0
  10. data/lib/gollum-lib.rb +18 -6
  11. data/lib/gollum-lib/blob_entry.rb +10 -9
  12. data/lib/gollum-lib/committer.rb +37 -30
  13. data/lib/gollum-lib/file.rb +71 -15
  14. data/lib/gollum-lib/file_view.rb +53 -48
  15. data/lib/gollum-lib/filter.rb +78 -0
  16. data/lib/gollum-lib/filter/code.rb +145 -0
  17. data/lib/gollum-lib/filter/emoji.rb +39 -0
  18. data/lib/gollum-lib/filter/macro.rb +57 -0
  19. data/lib/gollum-lib/filter/metadata.rb +29 -0
  20. data/lib/gollum-lib/filter/plain_text.rb +16 -0
  21. data/lib/gollum-lib/filter/plantuml.rb +176 -0
  22. data/lib/gollum-lib/filter/remote_code.rb +63 -0
  23. data/lib/gollum-lib/filter/render.rb +20 -0
  24. data/lib/gollum-lib/filter/sanitize.rb +18 -0
  25. data/lib/gollum-lib/filter/tags.rb +327 -0
  26. data/lib/gollum-lib/filter/toc.rb +134 -0
  27. data/lib/gollum-lib/filter/wsd.rb +54 -0
  28. data/lib/gollum-lib/git_access.rb +30 -32
  29. data/lib/gollum-lib/gitcode.rb +16 -16
  30. data/lib/gollum-lib/helpers.rb +3 -3
  31. data/lib/gollum-lib/hook.rb +35 -0
  32. data/lib/gollum-lib/macro.rb +43 -0
  33. data/lib/gollum-lib/macro/all_pages.rb +11 -0
  34. data/lib/gollum-lib/macro/global_toc.rb +12 -0
  35. data/lib/gollum-lib/macro/navigation.rb +20 -0
  36. data/lib/gollum-lib/macro/series.rb +48 -0
  37. data/lib/gollum-lib/markup.rb +95 -572
  38. data/lib/gollum-lib/markups.rb +9 -3
  39. data/lib/gollum-lib/page.rb +109 -80
  40. data/lib/gollum-lib/pagination.rb +1 -1
  41. data/lib/gollum-lib/sanitization.rb +75 -75
  42. data/lib/gollum-lib/version.rb +5 -0
  43. data/lib/gollum-lib/wiki.rb +287 -129
  44. metadata +237 -92
  45. data/CHANGELOG +0 -2
  46. data/VERSION +0 -1
  47. data/lib/gollum-lib/grit_ext.rb +0 -20
  48. data/lib/gollum-lib/remote_code.rb +0 -39
  49. data/lib/gollum-lib/web_sequence_diagram.rb +0 -44
@@ -1,4 +1,5 @@
1
1
  # ~*~ encoding: utf-8 ~*~
2
+
2
3
  module Gollum
3
4
  class File
4
5
  Wiki.file_class = self
@@ -9,9 +10,11 @@ module Gollum
9
10
  #
10
11
  # Returns a newly initialized Gollum::File.
11
12
  def initialize(wiki)
12
- @wiki = wiki
13
- @blob = nil
14
- @path = nil
13
+ @wiki = wiki
14
+ @blob = nil
15
+ @path = nil
16
+ @on_disk = false
17
+ @on_disk_path = nil
15
18
  end
16
19
 
17
20
  # Public: The url path required to reach this page within the repo.
@@ -27,21 +30,24 @@ module Gollum
27
30
  #
28
31
  # Returns the String url_path
29
32
  def escaped_url_path
30
- CGI.escape(self.url_path).gsub('%2F','/')
33
+ CGI.escape(self.url_path).gsub('%2F', '/')
31
34
  end
32
35
 
33
36
  # Public: The on-disk filename of the file.
34
37
  #
35
38
  # Returns the String name.
36
39
  def name
40
+ return @path if on_disk?
37
41
  @blob && @blob.name
38
42
  end
43
+
39
44
  alias filename name
40
45
 
41
46
  # Public: The raw contents of the page.
42
47
  #
43
48
  # Returns the String data.
44
49
  def raw_data
50
+ return IO.read(@on_disk_path) if on_disk?
45
51
  return nil unless @blob
46
52
 
47
53
  if !@wiki.repo.bare && @blob.is_symlink
@@ -52,7 +58,21 @@ module Gollum
52
58
  @blob.data
53
59
  end
54
60
 
55
- # Public: The Grit::Commit version of the file.
61
+ # Public: Is this an on-disk file reference?
62
+ #
63
+ # Returns true if this is a pointer to an on-disk file
64
+ def on_disk?
65
+ @on_disk
66
+ end
67
+
68
+ # Public: The path to this file on disk
69
+ #
70
+ # Returns nil if on_disk? is false.
71
+ def on_disk_path
72
+ @on_disk_path
73
+ end
74
+
75
+ # Public: The Gollum::Git::Commit version of the file.
56
76
  attr_accessor :version
57
77
 
58
78
  # Public: The String path of the file.
@@ -60,18 +80,20 @@ module Gollum
60
80
 
61
81
  # Public: The String mime type of the file.
62
82
  def mime_type
63
- @blob.mime_type
83
+ @blob && @blob.mime_type
64
84
  end
65
85
 
66
86
  # Populate the File with information from the Blob.
67
87
  #
68
- # blob - The Grit::Blob that contains the info.
88
+ # blob - The Gollum::Git::Blob that contains the info.
69
89
  # path - The String directory path of the file.
70
90
  #
71
91
  # Returns the populated Gollum::File.
72
- def populate(blob, path=nil)
73
- @blob = blob
74
- @path = "#{path}/#{blob.name}"[1..-1]
92
+ def populate(blob, path = nil)
93
+ @blob = blob
94
+ @path = "#{path}/#{blob.name}"[1..-1]
95
+ @on_disk = false
96
+ @on_disk_path = nil
75
97
  self
76
98
  end
77
99
 
@@ -81,19 +103,53 @@ module Gollum
81
103
  #
82
104
  #########################################################################
83
105
 
106
+ # Return the file path to this file on disk, if available.
107
+ #
108
+ # Returns nil if the file isn't available on disk. This can occur if the
109
+ # repo is bare, if the commit isn't the HEAD, or if there are problems
110
+ # resolving symbolic links.
111
+ def get_disk_reference(name, commit)
112
+ return false if @wiki.repo.bare
113
+ return false if commit.sha != @wiki.repo.head.commit.sha
114
+
115
+ # This will try to resolve symbolic links, as well
116
+ pathname = Pathname.new(::File.expand_path(::File.join(@wiki.repo.path, '..', name)))
117
+ if pathname.symlink?
118
+ source = ::File.readlink(pathname.to_path)
119
+ realpath = ::File.join(::File.dirname(pathname.to_path), source)
120
+ return false unless realpath && ::File.exist?(realpath)
121
+ @on_disk_path = realpath.to_s
122
+ else
123
+ @on_disk_path = pathname.to_path
124
+ end
125
+ true
126
+ end
127
+
84
128
  # Find a file in the given Gollum repo.
85
129
  #
86
130
  # name - The full String path.
87
131
  # version - The String version ID to find.
132
+ # try_on_disk - If true, try to return just a reference to a file
133
+ # that exists on the disk.
88
134
  #
89
- # Returns a Gollum::File or nil if the file could not be found.
90
- def find(name, version)
135
+ # Returns a Gollum::File or nil if the file could not be found. Note
136
+ # that if you specify try_on_disk=true, you may or may not get a file
137
+ # for which on_disk? is actually true.
138
+ def find(name, version, try_on_disk = false)
91
139
  checked = name.downcase
92
140
  map = @wiki.tree_map_for(version)
93
- if entry = map.detect { |entry| entry.path.downcase == checked }
141
+ commit = version.is_a?(Gollum::Git::Commit) ? version : @wiki.commit_for(version)
142
+
143
+ if (result = map.detect { |entry| entry.path.downcase == checked })
94
144
  @path = name
95
- @blob = entry.blob(@wiki.repo)
96
- @version = version.is_a?(Grit::Commit) ? version : @wiki.commit_for(version)
145
+ @version = commit
146
+
147
+ if try_on_disk && get_disk_reference(name, commit)
148
+ @on_disk = true
149
+ else
150
+ @blob = result.blob(@wiki.repo)
151
+ end
152
+
97
153
  self
98
154
  end
99
155
  end
@@ -1,35 +1,38 @@
1
1
  # ~*~ encoding: utf-8 ~*~
2
2
  module Gollum
3
- =begin
4
- FileView requires that:
5
- - All files in root dir are processed first
6
- - Then all the folders are sorted and processed
7
- =end
3
+ # FileView requires that:
4
+ # - All files in root dir are processed first
5
+ # - Then all the folders are sorted and processed
6
+
8
7
  class FileView
9
8
  # common use cases:
10
9
  # set pages to wiki.pages and show_all to false
11
10
  # set pages to wiki.pages + wiki.files and show_all to true
12
- def initialize pages, options = {}
13
- @pages = pages
11
+ def initialize(pages, options = {})
12
+ @pages = pages
14
13
  @show_all = options[:show_all] || false
15
- @checked = options[:collapse_tree] ? '' : "checked"
14
+ @checked = options[:collapse_tree] ? '' : "checked"
16
15
  end
17
16
 
18
- def enclose_tree string
17
+ def enclose_tree(string)
19
18
  %Q(<ol class="tree">\n) + string + %Q(</ol>)
20
19
  end
21
20
 
22
- def new_page page
21
+ def new_page(page)
23
22
  name = page.name
24
- url = url_for_page page
25
- %Q( <li class="file"><a href="#{url}"><span class="icon"></span>#{name}</a></li>)
23
+ url, valid_page = url_for_page page
24
+ %Q( <li class="file"><a href="#{url}"><span class="icon"></span>#{name}</a>#{valid_page ? "" : delete_file(url, valid_page)}</li>)
25
+ end
26
+
27
+ def delete_file(url, valid_page)
28
+ %Q(<form method="POST" action="/deleteFile/#{url}" onsubmit="return confirm('Do you really want to delete the file #{url}?');"><button type="submit" name="delete" value="true"></button></form>)
26
29
  end
27
30
 
28
- def new_folder folder_path
31
+ def new_folder(folder_path)
29
32
  new_sub_folder folder_path
30
33
  end
31
34
 
32
- def new_sub_folder path
35
+ def new_sub_folder(path)
33
36
  <<-HTML
34
37
  <li>
35
38
  <label>#{path}</label> <input type="checkbox" #{@checked} />
@@ -41,29 +44,31 @@ module Gollum
41
44
  "</ol></li>\n"
42
45
  end
43
46
 
44
- def url_for_page page
47
+ def url_for_page(page)
45
48
  url = ''
49
+ valid_page_name = false
46
50
  if @show_all
47
51
  # Remove ext for valid pages.
48
52
  filename = page.filename
49
- filename = Page::valid_page_name?(filename) ? filename.chomp(::File.extname(filename)) : filename
53
+ valid_page_name = Page::valid_page_name?(filename)
54
+ filename = valid_page_name ? filename.chomp(::File.extname(filename)) : filename
50
55
 
51
56
  url = ::File.join(::File.dirname(page.path), filename)
52
57
  else
53
58
  url = ::File.join(::File.dirname(page.path), page.filename_stripped)
54
59
  end
55
- url = url[2..-1] if url[0,2] == './'
56
- url
60
+ url = url[2..-1] if url[0, 2] == './'
61
+ return url, valid_page_name
57
62
  end
58
63
 
59
64
  def render_files
60
- html = ''
61
- count = @pages.size
65
+ html = ''
66
+ count = @pages.size
62
67
  folder_start = -1
63
68
 
64
69
  # Process all pages until folders start
65
- count.times do | index |
66
- page = @pages[ index ]
70
+ count.times do |index|
71
+ page = @pages[index]
67
72
  path = page.path
68
73
 
69
74
  unless path.include? '/'
@@ -81,7 +86,7 @@ module Gollum
81
86
 
82
87
  # Handle special case of only one folder.
83
88
  if (count - folder_start == 1)
84
- page = @pages[ folder_start ]
89
+ page = @pages[folder_start]
85
90
  html += <<-HTML
86
91
  <li>
87
92
  <label>#{::File.dirname(page.path)}</label> <input type="checkbox" #{@checked} />
@@ -95,14 +100,14 @@ module Gollum
95
100
  end
96
101
 
97
102
  sorted_folders = []
98
- (folder_start).upto count - 1 do | index |
99
- sorted_folders += [[ @pages[ index ].path, index ]]
103
+ (folder_start).upto count - 1 do |index|
104
+ sorted_folders += [[@pages[index].path, index]]
100
105
  end
101
106
 
102
107
  # http://stackoverflow.com/questions/3482814/sorting-list-of-string-paths-in-vb-net
103
- sorted_folders.sort! do |first,second|
104
- a = first[0]
105
- b = second[0]
108
+ sorted_folders.sort! do |first, second|
109
+ a = first[0]
110
+ b = second[0]
106
111
 
107
112
  # use :: operator because gollum defines its own conflicting File class
108
113
  dir_compare = ::File.dirname(a) <=> ::File.dirname(b)
@@ -118,34 +123,34 @@ module Gollum
118
123
 
119
124
  # keep track of folder depth, 0 = at root.
120
125
  cwd_array = []
121
- changed = false
126
+ changed = false
122
127
 
123
128
  # process rest of folders
124
- (0...sorted_folders.size).each do | index |
125
- page = @pages[ sorted_folders[ index ][ 1 ] ]
126
- path = page.path
127
- folder = ::File.dirname path
129
+ (0...sorted_folders.size).each do |i|
130
+ page = @pages[sorted_folders[i][1]]
131
+ path = page.path
132
+ folder = ::File.dirname path
128
133
 
129
- tmp_array = folder.split '/'
134
+ tmp_array = folder.split '/'
130
135
 
131
- (0...tmp_array.size).each do | index |
132
- if cwd_array[ index ].nil? || changed
133
- html += new_sub_folder tmp_array[ index ]
134
- next
135
- end
136
+ (0...tmp_array.size).each do |index|
137
+ if cwd_array[index].nil? || changed
138
+ html += new_sub_folder tmp_array[index]
139
+ next
140
+ end
136
141
 
137
- if cwd_array[ index ] != tmp_array[ index ]
138
- changed = true
139
- (cwd_array.size - index).times do
140
- html += end_folder
141
- end
142
- html += new_sub_folder tmp_array[ index ]
142
+ if cwd_array[index] != tmp_array[index]
143
+ changed = true
144
+ (cwd_array.size - index).times do
145
+ html += end_folder
143
146
  end
147
+ html += new_sub_folder tmp_array[index]
144
148
  end
149
+ end
145
150
 
146
- html += new_page page
147
- cwd_array = tmp_array
148
- changed = false
151
+ html += new_page page
152
+ cwd_array = tmp_array
153
+ changed = false
149
154
  end
150
155
 
151
156
  # return the completed html
@@ -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,145 @@
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
+ case @markup.format
9
+ when :txt
10
+ return data
11
+ when :asciidoc
12
+ data.gsub!(/^(\[source,([^\r\n]*)\]\n)?----\n(.+?)\n----$/m) do
13
+ cache_codeblock(Regexp.last_match[2], Regexp.last_match[3])
14
+ end
15
+ when :org
16
+ org_headers = %r{([ \t]*#\+HEADER[S]?:[^\r\n]*\n)*}
17
+ org_name = %r{([ \t]*#\+NAME:[^\r\n]*\n)?}
18
+ org_lang = %r{[ ]*([^\n \r]*)[ ]*[^\r\n]*}
19
+ org_begin = %r{[ \t]*#\+BEGIN_SRC#{org_lang}\n}
20
+ org_end = %r{\n[ \t]*#\+END_SRC[ \t]*}
21
+ data.gsub!(/^#{org_headers}#{org_name}#{org_begin}(.+?)#{org_end}$/mi) do
22
+ cache_codeblock(Regexp.last_match[3], Regexp.last_match[4])
23
+ end
24
+ when :markdown
25
+ data.gsub!(/^([ \t]*)(~~~+) ?([^\r\n]+)?\r?\n(.+?)\r?\n\1(~~~+)[ \t\r]*$/m) do
26
+ m_indent = Regexp.last_match[1]
27
+ m_start = Regexp.last_match[2] # ~~~
28
+ m_lang = Regexp.last_match[3]
29
+ m_code = Regexp.last_match[4]
30
+ m_end = Regexp.last_match[5] # ~~~
31
+ # start and finish tilde fence must be the same length
32
+ next '' if m_start.length != m_end.length
33
+ lang = m_lang ? m_lang.strip : nil
34
+ if lang
35
+ lang = lang.match(/\.([^}\s]+)/)
36
+ lang = lang[1] unless lang.nil?
37
+ end
38
+ "#{m_indent}#{cache_codeblock(lang, m_code, m_indent)}"
39
+ end
40
+ end
41
+
42
+
43
+ data.gsub!(/^([ \t]*)``` ?([^\r\n]+)?\r?\n(.+?)\r?\n\1```[ \t]*\r?$/m) do
44
+ "#{Regexp.last_match[1]}#{cache_codeblock(Regexp.last_match[2].to_s.strip, Regexp.last_match[3], Regexp.last_match[1])}" # print the SHA1 ID with the proper indentation
45
+ end
46
+ data
47
+ end
48
+
49
+ # Process all code from the codemap and replace the placeholders with the
50
+ # final HTML.
51
+ #
52
+ # data - The String data (with placeholders).
53
+ # encoding - Encoding Constant or String.
54
+ #
55
+ # Returns the marked up String data.
56
+ def process(data)
57
+ return data if data.nil? || data.size.zero? || @map.size.zero?
58
+
59
+ blocks = []
60
+
61
+ @map.each do |_id, spec|
62
+ next if spec[:output] # cached
63
+
64
+ code = spec[:code]
65
+
66
+ remove_leading_space(code, /^#{spec[:indent]}/m)
67
+ remove_leading_space(code, /^( |\t)/m)
68
+
69
+ blocks << [spec[:lang], code]
70
+ end
71
+
72
+ highlighted = []
73
+ blocks.each do |lang, code|
74
+ encoding = @markup.encoding || 'utf-8'
75
+
76
+ if defined? Pygments
77
+ # Set the default lexer to 'text' to prevent #153 and #154
78
+ lang = lang || 'text'
79
+ lexer = Pygments::Lexer[(lang)] || Pygments::Lexer['text']
80
+
81
+ # must set startinline to true for php to be highlighted without <?
82
+ hl_code = lexer.highlight(code, :options => { :encoding => encoding.to_s, :startinline => true })
83
+ else # Rouge
84
+ begin
85
+ # if `lang` was not defined then assume plaintext
86
+ lexer = Rouge::Lexer.find_fancy(lang || 'plaintext')
87
+ formatter = Rouge::Formatters::HTML.new
88
+ wrap_template = '<pre class="highlight"><code>%s</code></pre>'
89
+
90
+ # if `lang` is defined but cannot be found then wrap it with an error
91
+ if lexer.nil?
92
+ lexer = Rouge::Lexers::PlainText
93
+ wrap_template = '<pre class="highlight"><span class="err">%s</span></pre>'
94
+ end
95
+
96
+ formatted = formatter.format(lexer.lex(code))
97
+
98
+ hl_code = Kernel.sprintf(wrap_template, formatted)
99
+ rescue
100
+ hl_code = code
101
+ end
102
+ end
103
+
104
+ highlighted << hl_code
105
+ end
106
+
107
+ @map.each do |id, spec|
108
+ body = spec[:output] || begin
109
+ if (body = highlighted.shift.to_s).size > 0
110
+ @markup.update_cache(:code, id, body)
111
+ body
112
+ else
113
+ "<pre><code>#{CGI.escapeHTML(spec[:code])}</code></pre>"
114
+ end
115
+ end
116
+ # Removes paragraph tags surrounding <pre> blocks, see issue https://github.com/gollum/gollum-lib/issues/97
117
+ data.gsub!(/(<p>#{id}<\/p>|#{id})/) { body }
118
+ end
119
+
120
+ data
121
+ end
122
+
123
+ private
124
+ # Remove the leading space from a code block. Leading space
125
+ # is only removed if every single line in the block has leading
126
+ # whitespace.
127
+ #
128
+ # code - The code block to remove spaces from
129
+ # regex - A regex to match whitespace
130
+ def remove_leading_space(code, regex)
131
+ if code.lines.all? { |line| line =~ /\A\r?\n\Z/ || line =~ regex }
132
+ code.gsub!(regex) { '' }
133
+ end
134
+ end
135
+
136
+ def cache_codeblock(language, code, indent = "")
137
+ language = language.to_s.empty? ? nil : language
138
+ id = Digest::SHA1.hexdigest("#{language}.#{code}")
139
+ cached = @markup.check_cache(:code, id)
140
+ @map[id] = cached ?
141
+ { :output => cached } :
142
+ { :lang => language, :code => code, :indent => indent }
143
+ id
144
+ end
145
+ end