Chris@0
|
1 require 'set'
|
Chris@0
|
2
|
Chris@0
|
3 module CodeRay
|
Chris@0
|
4 module Encoders
|
Chris@0
|
5
|
Chris@0
|
6 # = HTML Encoder
|
Chris@0
|
7 #
|
Chris@0
|
8 # This is CodeRay's most important highlighter:
|
Chris@0
|
9 # It provides save, fast XHTML generation and CSS support.
|
Chris@0
|
10 #
|
Chris@0
|
11 # == Usage
|
Chris@0
|
12 #
|
Chris@0
|
13 # require 'coderay'
|
Chris@0
|
14 # puts CodeRay.scan('Some /code/', :ruby).html #-> a HTML page
|
Chris@0
|
15 # puts CodeRay.scan('Some /code/', :ruby).html(:wrap => :span)
|
Chris@0
|
16 # #-> <span class="CodeRay"><span class="co">Some</span> /code/</span>
|
Chris@0
|
17 # puts CodeRay.scan('Some /code/', :ruby).span #-> the same
|
Chris@0
|
18 #
|
Chris@0
|
19 # puts CodeRay.scan('Some code', :ruby).html(
|
Chris@0
|
20 # :wrap => nil,
|
Chris@0
|
21 # :line_numbers => :inline,
|
Chris@0
|
22 # :css => :style
|
Chris@0
|
23 # )
|
Chris@0
|
24 # #-> <span class="no">1</span> <span style="color:#036; font-weight:bold;">Some</span> code
|
Chris@0
|
25 #
|
Chris@0
|
26 # == Options
|
Chris@0
|
27 #
|
Chris@0
|
28 # === :tab_width
|
Chris@0
|
29 # Convert \t characters to +n+ spaces (a number.)
|
Chris@0
|
30 # Default: 8
|
Chris@0
|
31 #
|
Chris@0
|
32 # === :css
|
Chris@0
|
33 # How to include the styles; can be :class or :style.
|
Chris@0
|
34 #
|
Chris@0
|
35 # Default: :class
|
Chris@0
|
36 #
|
Chris@0
|
37 # === :wrap
|
Chris@0
|
38 # Wrap in :page, :div, :span or nil.
|
Chris@0
|
39 #
|
Chris@0
|
40 # You can also use Encoders::Div and Encoders::Span.
|
Chris@0
|
41 #
|
Chris@0
|
42 # Default: nil
|
Chris@0
|
43 #
|
Chris@0
|
44 # === :title
|
Chris@0
|
45 #
|
Chris@0
|
46 # The title of the HTML page (works only when :wrap is set to :page.)
|
Chris@0
|
47 #
|
Chris@0
|
48 # Default: 'CodeRay output'
|
Chris@0
|
49 #
|
Chris@0
|
50 # === :line_numbers
|
Chris@0
|
51 # Include line numbers in :table, :inline, :list or nil (no line numbers)
|
Chris@0
|
52 #
|
Chris@0
|
53 # Default: nil
|
Chris@0
|
54 #
|
Chris@0
|
55 # === :line_number_start
|
Chris@0
|
56 # Where to start with line number counting.
|
Chris@0
|
57 #
|
Chris@0
|
58 # Default: 1
|
Chris@0
|
59 #
|
Chris@0
|
60 # === :bold_every
|
Chris@0
|
61 # Make every +n+-th number appear bold.
|
Chris@0
|
62 #
|
Chris@0
|
63 # Default: 10
|
Chris@0
|
64 #
|
Chris@0
|
65 # === :highlight_lines
|
Chris@0
|
66 #
|
Chris@0
|
67 # Highlights certain line numbers.
|
Chris@0
|
68 # Can be any Enumerable, typically just an Array or Range, of numbers.
|
Chris@0
|
69 #
|
Chris@0
|
70 # Bolding is deactivated when :highlight_lines is set. It only makes sense
|
Chris@0
|
71 # in combination with :line_numbers.
|
Chris@0
|
72 #
|
Chris@0
|
73 # Default: nil
|
Chris@0
|
74 #
|
Chris@0
|
75 # === :hint
|
Chris@0
|
76 # Include some information into the output using the title attribute.
|
Chris@0
|
77 # Can be :info (show token type on mouse-over), :info_long (with full path)
|
Chris@0
|
78 # or :debug (via inspect).
|
Chris@0
|
79 #
|
Chris@0
|
80 # Default: false
|
Chris@0
|
81 class HTML < Encoder
|
Chris@0
|
82
|
Chris@0
|
83 include Streamable
|
Chris@0
|
84 register_for :html
|
Chris@0
|
85
|
Chris@0
|
86 FILE_EXTENSION = 'html'
|
Chris@0
|
87
|
Chris@0
|
88 DEFAULT_OPTIONS = {
|
Chris@0
|
89 :tab_width => 8,
|
Chris@0
|
90
|
Chris@0
|
91 :css => :class,
|
Chris@0
|
92
|
Chris@0
|
93 :style => :cycnus,
|
Chris@0
|
94 :wrap => nil,
|
Chris@0
|
95 :title => 'CodeRay output',
|
Chris@0
|
96
|
Chris@0
|
97 :line_numbers => nil,
|
Chris@0
|
98 :line_number_start => 1,
|
Chris@0
|
99 :bold_every => 10,
|
Chris@0
|
100 :highlight_lines => nil,
|
Chris@0
|
101
|
Chris@0
|
102 :hint => false,
|
Chris@0
|
103 }
|
Chris@0
|
104
|
Chris@0
|
105 helper :output, :css
|
Chris@0
|
106
|
Chris@0
|
107 attr_reader :css
|
Chris@0
|
108
|
Chris@0
|
109 protected
|
Chris@0
|
110
|
Chris@0
|
111 HTML_ESCAPE = { #:nodoc:
|
Chris@0
|
112 '&' => '&',
|
Chris@0
|
113 '"' => '"',
|
Chris@0
|
114 '>' => '>',
|
Chris@0
|
115 '<' => '<',
|
Chris@0
|
116 }
|
Chris@0
|
117
|
Chris@0
|
118 # This was to prevent illegal HTML.
|
Chris@0
|
119 # Strange chars should still be avoided in codes.
|
Chris@0
|
120 evil_chars = Array(0x00...0x20) - [?\n, ?\t, ?\s]
|
Chris@0
|
121 evil_chars.each { |i| HTML_ESCAPE[i.chr] = ' ' }
|
Chris@0
|
122 #ansi_chars = Array(0x7f..0xff)
|
Chris@0
|
123 #ansi_chars.each { |i| HTML_ESCAPE[i.chr] = '&#%d;' % i }
|
Chris@0
|
124 # \x9 (\t) and \xA (\n) not included
|
Chris@0
|
125 #HTML_ESCAPE_PATTERN = /[\t&"><\0-\x8\xB-\x1f\x7f-\xff]/
|
Chris@0
|
126 HTML_ESCAPE_PATTERN = /[\t"&><\0-\x8\xB-\x1f]/
|
Chris@0
|
127
|
Chris@0
|
128 TOKEN_KIND_TO_INFO = Hash.new { |h, kind|
|
Chris@0
|
129 h[kind] =
|
Chris@0
|
130 case kind
|
Chris@0
|
131 when :pre_constant
|
Chris@0
|
132 'Predefined constant'
|
Chris@0
|
133 else
|
Chris@0
|
134 kind.to_s.gsub(/_/, ' ').gsub(/\b\w/) { $&.capitalize }
|
Chris@0
|
135 end
|
Chris@0
|
136 }
|
Chris@0
|
137
|
Chris@0
|
138 TRANSPARENT_TOKEN_KINDS = [
|
Chris@0
|
139 :delimiter, :modifier, :content, :escape, :inline_delimiter,
|
Chris@0
|
140 ].to_set
|
Chris@0
|
141
|
Chris@0
|
142 # Generate a hint about the given +classes+ in a +hint+ style.
|
Chris@0
|
143 #
|
Chris@0
|
144 # +hint+ may be :info, :info_long or :debug.
|
Chris@0
|
145 def self.token_path_to_hint hint, classes
|
Chris@0
|
146 title =
|
Chris@0
|
147 case hint
|
Chris@0
|
148 when :info
|
Chris@0
|
149 TOKEN_KIND_TO_INFO[classes.first]
|
Chris@0
|
150 when :info_long
|
Chris@0
|
151 classes.reverse.map { |kind| TOKEN_KIND_TO_INFO[kind] }.join('/')
|
Chris@0
|
152 when :debug
|
Chris@0
|
153 classes.inspect
|
Chris@0
|
154 end
|
Chris@0
|
155 title ? " title=\"#{title}\"" : ''
|
Chris@0
|
156 end
|
Chris@0
|
157
|
Chris@0
|
158 def setup options
|
Chris@0
|
159 super
|
Chris@0
|
160
|
Chris@0
|
161 @HTML_ESCAPE = HTML_ESCAPE.dup
|
Chris@0
|
162 @HTML_ESCAPE["\t"] = ' ' * options[:tab_width]
|
Chris@0
|
163
|
Chris@0
|
164 @opened = [nil]
|
Chris@0
|
165 @css = CSS.new options[:style]
|
Chris@0
|
166
|
Chris@0
|
167 hint = options[:hint]
|
Chris@0
|
168 if hint and not [:debug, :info, :info_long].include? hint
|
Chris@0
|
169 raise ArgumentError, "Unknown value %p for :hint; \
|
Chris@0
|
170 expected :info, :debug, false, or nil." % hint
|
Chris@0
|
171 end
|
Chris@0
|
172
|
Chris@0
|
173 case options[:css]
|
Chris@0
|
174
|
Chris@0
|
175 when :class
|
Chris@0
|
176 @css_style = Hash.new do |h, k|
|
Chris@0
|
177 c = CodeRay::Tokens::ClassOfKind[k.first]
|
Chris@0
|
178 if c == :NO_HIGHLIGHT and not hint
|
Chris@0
|
179 h[k.dup] = false
|
Chris@0
|
180 else
|
Chris@0
|
181 title = if hint
|
Chris@0
|
182 HTML.token_path_to_hint(hint, k[1..-1] << k.first)
|
Chris@0
|
183 else
|
Chris@0
|
184 ''
|
Chris@0
|
185 end
|
Chris@0
|
186 if c == :NO_HIGHLIGHT
|
Chris@0
|
187 h[k.dup] = '<span%s>' % [title]
|
Chris@0
|
188 else
|
Chris@0
|
189 h[k.dup] = '<span%s class="%s">' % [title, c]
|
Chris@0
|
190 end
|
Chris@0
|
191 end
|
Chris@0
|
192 end
|
Chris@0
|
193
|
Chris@0
|
194 when :style
|
Chris@0
|
195 @css_style = Hash.new do |h, k|
|
Chris@0
|
196 if k.is_a? ::Array
|
Chris@0
|
197 styles = k.dup
|
Chris@0
|
198 else
|
Chris@0
|
199 styles = [k]
|
Chris@0
|
200 end
|
Chris@0
|
201 type = styles.first
|
Chris@0
|
202 classes = styles.map { |c| Tokens::ClassOfKind[c] }
|
Chris@0
|
203 if classes.first == :NO_HIGHLIGHT and not hint
|
Chris@0
|
204 h[k] = false
|
Chris@0
|
205 else
|
Chris@0
|
206 styles.shift if TRANSPARENT_TOKEN_KINDS.include? styles.first
|
Chris@0
|
207 title = HTML.token_path_to_hint hint, styles
|
Chris@0
|
208 style = @css[*classes]
|
Chris@0
|
209 h[k] =
|
Chris@0
|
210 if style
|
Chris@0
|
211 '<span%s style="%s">' % [title, style]
|
Chris@0
|
212 else
|
Chris@0
|
213 false
|
Chris@0
|
214 end
|
Chris@0
|
215 end
|
Chris@0
|
216 end
|
Chris@0
|
217
|
Chris@0
|
218 else
|
Chris@0
|
219 raise ArgumentError, "Unknown value %p for :css." % options[:css]
|
Chris@0
|
220
|
Chris@0
|
221 end
|
Chris@0
|
222 end
|
Chris@0
|
223
|
Chris@0
|
224 def finish options
|
Chris@0
|
225 not_needed = @opened.shift
|
Chris@0
|
226 @out << '</span>' * @opened.size
|
Chris@0
|
227 unless @opened.empty?
|
Chris@0
|
228 warn '%d tokens still open: %p' % [@opened.size, @opened]
|
Chris@0
|
229 end
|
Chris@0
|
230
|
Chris@0
|
231 @out.extend Output
|
Chris@0
|
232 @out.css = @css
|
Chris@0
|
233 @out.numerize! options[:line_numbers], options
|
Chris@0
|
234 @out.wrap! options[:wrap]
|
Chris@0
|
235 @out.apply_title! options[:title]
|
Chris@0
|
236
|
Chris@0
|
237 super
|
Chris@0
|
238 end
|
Chris@0
|
239
|
Chris@0
|
240 def token text, type = :plain
|
Chris@0
|
241 case text
|
Chris@0
|
242
|
Chris@0
|
243 when nil
|
Chris@0
|
244 # raise 'Token with nil as text was given: %p' % [[text, type]]
|
Chris@0
|
245
|
Chris@0
|
246 when String
|
Chris@0
|
247 if text =~ /#{HTML_ESCAPE_PATTERN}/o
|
Chris@0
|
248 text = text.gsub(/#{HTML_ESCAPE_PATTERN}/o) { |m| @HTML_ESCAPE[m] }
|
Chris@0
|
249 end
|
Chris@0
|
250 @opened[0] = type
|
Chris@0
|
251 if text != "\n" && style = @css_style[@opened]
|
Chris@0
|
252 @out << style << text << '</span>'
|
Chris@0
|
253 else
|
Chris@0
|
254 @out << text
|
Chris@0
|
255 end
|
Chris@0
|
256
|
Chris@0
|
257
|
Chris@0
|
258 # token groups, eg. strings
|
Chris@0
|
259 when :open
|
Chris@0
|
260 @opened[0] = type
|
Chris@0
|
261 @out << (@css_style[@opened] || '<span>')
|
Chris@0
|
262 @opened << type
|
Chris@0
|
263 when :close
|
Chris@0
|
264 if @opened.empty?
|
Chris@0
|
265 # nothing to close
|
Chris@0
|
266 else
|
Chris@0
|
267 if $CODERAY_DEBUG and (@opened.size == 1 or @opened.last != type)
|
Chris@0
|
268 raise 'Malformed token stream: Trying to close a token (%p) \
|
Chris@0
|
269 that is not open. Open are: %p.' % [type, @opened[1..-1]]
|
Chris@0
|
270 end
|
Chris@0
|
271 @out << '</span>'
|
Chris@0
|
272 @opened.pop
|
Chris@0
|
273 end
|
Chris@0
|
274
|
Chris@0
|
275 # whole lines to be highlighted, eg. a deleted line in a diff
|
Chris@0
|
276 when :begin_line
|
Chris@0
|
277 @opened[0] = type
|
Chris@0
|
278 if style = @css_style[@opened]
|
Chris@0
|
279 @out << style.sub('<span', '<div')
|
Chris@0
|
280 else
|
Chris@0
|
281 @out << '<div>'
|
Chris@0
|
282 end
|
Chris@0
|
283 @opened << type
|
Chris@0
|
284 when :end_line
|
Chris@0
|
285 if @opened.empty?
|
Chris@0
|
286 # nothing to close
|
Chris@0
|
287 else
|
Chris@0
|
288 if $CODERAY_DEBUG and (@opened.size == 1 or @opened.last != type)
|
Chris@0
|
289 raise 'Malformed token stream: Trying to close a line (%p) \
|
Chris@0
|
290 that is not open. Open are: %p.' % [type, @opened[1..-1]]
|
Chris@0
|
291 end
|
Chris@0
|
292 @out << '</div>'
|
Chris@0
|
293 @opened.pop
|
Chris@0
|
294 end
|
Chris@0
|
295
|
Chris@0
|
296 else
|
Chris@0
|
297 raise 'unknown token kind: %p' % [text]
|
Chris@0
|
298
|
Chris@0
|
299 end
|
Chris@0
|
300 end
|
Chris@0
|
301
|
Chris@0
|
302 end
|
Chris@0
|
303
|
Chris@0
|
304 end
|
Chris@0
|
305 end
|