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