require "set" module CodeRay module Encoders # = HTML Encoder # # This is CodeRay's most important highlighter: # It provides save, fast XHTML generation and CSS support. # # == Usage # # require 'coderay' # puts CodeRay.scan('Some /code/', :ruby).html #-> a HTML page # puts CodeRay.scan('Some /code/', :ruby).html(:wrap => :span) # #-> Some /code/ # puts CodeRay.scan('Some /code/', :ruby).span #-> the same # # puts CodeRay.scan('Some code', :ruby).html( # :wrap => nil, # :line_numbers => :inline, # :css => :style # ) # #-> 1 Some code # # == Options # # === :tab_width # Convert \t characters to +n+ spaces (a number.) # Default: 8 # # === :css # How to include the styles; can be :class or :style. # # Default: :class # # === :wrap # Wrap in :page, :div, :span or nil. # # You can also use Encoders::Div and Encoders::Span. # # Default: nil # # === :title # # The title of the HTML page (works only when :wrap is set to :page.) # # Default: 'CodeRay output' # # === :line_numbers # Include line numbers in :table, :inline, :list or nil (no line numbers) # # Default: nil # # === :line_number_start # Where to start with line number counting. # # Default: 1 # # === :bold_every # Make every +n+-th number appear bold. # # Default: 10 # # === :highlight_lines # # Highlights certain line numbers now by using the :highlight_lines option. # Can be any Enumerable, typically just an Array or Range, of numbers. # # Bolding is deactivated when :highlight_lines is set. It only makes sense # in combination with :line_numbers. # # Default: nil # # === :hint # Include some information into the output using the title attribute. # Can be :info (show token type on mouse-over), :info_long (with full path) # or :debug (via inspect). # # Default: false class HTML < Encoder include Streamable register_for :html FILE_EXTENSION = 'html' DEFAULT_OPTIONS = { :tab_width => 8, # :level => :xhtml, # reserved for future use :css => :class, :style => :cycnus, :wrap => nil, :title => 'CodeRay output', :line_numbers => nil, :line_number_start => 1, :bold_every => 10, :highlight_lines => nil, :hint => false, } helper :output, :css attr_reader :css protected HTML_ESCAPE = { #:nodoc: '&' => '&', '"' => '"', '>' => '>', '<' => '<', } # This was to prevent illegal HTML. # Strange chars should still be avoided in codes. evil_chars = Array(0x00...0x20) - [?\n, ?\t, ?\s] evil_chars.each { |i| HTML_ESCAPE[i.chr] = ' ' } #ansi_chars = Array(0x7f..0xff) #ansi_chars.each { |i| HTML_ESCAPE[i.chr] = '&#%d;' % i } # \x9 (\t) and \xA (\n) not included #HTML_ESCAPE_PATTERN = /[\t&"><\0-\x8\xB-\x1f\x7f-\xff]/ HTML_ESCAPE_PATTERN = /[\t"&><\0-\x8\xB-\x1f]/ TOKEN_KIND_TO_INFO = Hash.new { |h, kind| h[kind] = case kind when :pre_constant 'Predefined constant' else kind.to_s.gsub(/_/, ' ').gsub(/\b\w/) { $&.capitalize } end } TRANSPARENT_TOKEN_KINDS = [ :delimiter, :modifier, :content, :escape, :inline_delimiter, ].to_set # Generate a hint about the given +classes+ in a +hint+ style. # # +hint+ may be :info, :info_long or :debug. def self.token_path_to_hint hint, classes title = case hint when :info TOKEN_KIND_TO_INFO[classes.first] when :info_long classes.reverse.map { |kind| TOKEN_KIND_TO_INFO[kind] }.join('/') when :debug classes.inspect end title ? " title=\"#{title}\"" : '' end def setup options super @HTML_ESCAPE = HTML_ESCAPE.dup @HTML_ESCAPE["\t"] = ' ' * options[:tab_width] @opened = [nil] @css = CSS.new options[:style] hint = options[:hint] if hint and not [:debug, :info, :info_long].include? hint raise ArgumentError, "Unknown value %p for :hint; \ expected :info, :debug, false, or nil." % hint end case options[:css] when :class @css_style = Hash.new do |h, k| c = CodeRay::Tokens::ClassOfKind[k.first] if c == :NO_HIGHLIGHT and not hint h[k.dup] = false else title = if hint HTML.token_path_to_hint(hint, k[1..-1] << k.first) else '' end if c == :NO_HIGHLIGHT h[k.dup] = '' % [title] else h[k.dup] = '' % [title, c] end end end when :style @css_style = Hash.new do |h, k| if k.is_a? ::Array styles = k.dup else styles = [k] end type = styles.first classes = styles.map { |c| Tokens::ClassOfKind[c] } if classes.first == :NO_HIGHLIGHT and not hint h[k] = false else styles.shift if TRANSPARENT_TOKEN_KINDS.include? styles.first title = HTML.token_path_to_hint hint, styles style = @css[*classes] h[k] = if style '' % [title, style] else false end end end else raise ArgumentError, "Unknown value %p for :css." % options[:css] end end def finish options not_needed = @opened.shift @out << '' * @opened.size unless @opened.empty? warn '%d tokens still open: %p' % [@opened.size, @opened] end @out.extend Output @out.css = @css @out.numerize! options[:line_numbers], options @out.wrap! options[:wrap] @out.apply_title! options[:title] super end def token text, type = :plain case text when nil # raise 'Token with nil as text was given: %p' % [[text, type]] when String if text =~ /#{HTML_ESCAPE_PATTERN}/o text = text.gsub(/#{HTML_ESCAPE_PATTERN}/o) { |m| @HTML_ESCAPE[m] } end @opened[0] = type if text != "\n" && style = @css_style[@opened] @out << style << text << '' else @out << text end # token groups, eg. strings when :open @opened[0] = type @out << (@css_style[@opened] || '') @opened << type when :close if @opened.empty? # nothing to close else if $DEBUG and (@opened.size == 1 or @opened.last != type) raise 'Malformed token stream: Trying to close a token (%p) \ that is not open. Open are: %p.' % [type, @opened[1..-1]] end @out << '' @opened.pop end # whole lines to be highlighted, eg. a deleted line in a diff when :begin_line @opened[0] = type if style = @css_style[@opened] @out << style.sub('' end @opened << type when :end_line if @opened.empty? # nothing to close else if $DEBUG and (@opened.size == 1 or @opened.last != type) raise 'Malformed token stream: Trying to close a line (%p) \ that is not open. Open are: %p.' % [type, @opened[1..-1]] end @out << '' @opened.pop end else raise 'unknown token kind: %p' % [text] end end end end end