Chris@909: require 'set'
Chris@909:
Chris@909: module CodeRay
Chris@909: module Encoders
Chris@909:
Chris@909: # = HTML Encoder
Chris@909: #
Chris@909: # This is CodeRay's most important highlighter:
Chris@909: # It provides save, fast XHTML generation and CSS support.
Chris@909: #
Chris@909: # == Usage
Chris@909: #
Chris@909: # require 'coderay'
Chris@909: # puts CodeRay.scan('Some /code/', :ruby).html #-> a HTML page
Chris@909: # puts CodeRay.scan('Some /code/', :ruby).html(:wrap => :span)
Chris@909: # #-> Some /code/
Chris@909: # puts CodeRay.scan('Some /code/', :ruby).span #-> the same
Chris@909: #
Chris@909: # puts CodeRay.scan('Some code', :ruby).html(
Chris@909: # :wrap => nil,
Chris@909: # :line_numbers => :inline,
Chris@909: # :css => :style
Chris@909: # )
Chris@909: #
Chris@909: # == Options
Chris@909: #
Chris@909: # === :tab_width
Chris@909: # Convert \t characters to +n+ spaces (a number.)
Chris@909: #
Chris@909: # Default: 8
Chris@909: #
Chris@909: # === :css
Chris@909: # How to include the styles; can be :class or :style.
Chris@909: #
Chris@909: # Default: :class
Chris@909: #
Chris@909: # === :wrap
Chris@909: # Wrap in :page, :div, :span or nil.
Chris@909: #
Chris@909: # You can also use Encoders::Div and Encoders::Span.
Chris@909: #
Chris@909: # Default: nil
Chris@909: #
Chris@909: # === :title
Chris@909: #
Chris@909: # The title of the HTML page (works only when :wrap is set to :page.)
Chris@909: #
Chris@909: # Default: 'CodeRay output'
Chris@909: #
Chris@909: # === :line_numbers
Chris@909: # Include line numbers in :table, :inline, or nil (no line numbers)
Chris@909: #
Chris@909: # Default: nil
Chris@909: #
Chris@909: # === :line_number_anchors
Chris@909: # Adds anchors and links to the line numbers. Can be false (off), true (on),
Chris@909: # or a prefix string that will be prepended to the anchor name.
Chris@909: #
Chris@909: # The prefix must consist only of letters, digits, and underscores.
Chris@909: #
Chris@909: # Default: true, default prefix name: "line"
Chris@909: #
Chris@909: # === :line_number_start
Chris@909: # Where to start with line number counting.
Chris@909: #
Chris@909: # Default: 1
Chris@909: #
Chris@909: # === :bold_every
Chris@909: # Make every +n+-th number appear bold.
Chris@909: #
Chris@909: # Default: 10
Chris@909: #
Chris@909: # === :highlight_lines
Chris@909: #
Chris@909: # Highlights certain line numbers.
Chris@909: # Can be any Enumerable, typically just an Array or Range, of numbers.
Chris@909: #
Chris@909: # Bolding is deactivated when :highlight_lines is set. It only makes sense
Chris@909: # in combination with :line_numbers.
Chris@909: #
Chris@909: # Default: nil
Chris@909: #
Chris@909: # === :hint
Chris@909: # Include some information into the output using the title attribute.
Chris@909: # Can be :info (show token kind on mouse-over), :info_long (with full path)
Chris@909: # or :debug (via inspect).
Chris@909: #
Chris@909: # Default: false
Chris@909: class HTML < Encoder
Chris@909:
Chris@909: register_for :html
Chris@909:
Chris@909: FILE_EXTENSION = 'snippet.html'
Chris@909:
Chris@909: DEFAULT_OPTIONS = {
Chris@909: :tab_width => 8,
Chris@909:
Chris@909: :css => :class,
Chris@909: :style => :alpha,
Chris@909: :wrap => nil,
Chris@909: :title => 'CodeRay output',
Chris@909:
Chris@909: :line_numbers => nil,
Chris@909: :line_number_anchors => 'n',
Chris@909: :line_number_start => 1,
Chris@909: :bold_every => 10,
Chris@909: :highlight_lines => nil,
Chris@909:
Chris@909: :hint => false,
Chris@909: }
Chris@909:
Chris@909: autoload :Output, 'coderay/encoders/html/output'
Chris@909: autoload :CSS, 'coderay/encoders/html/css'
Chris@909: autoload :Numbering, 'coderay/encoders/html/numbering'
Chris@909:
Chris@909: attr_reader :css
Chris@909:
Chris@909: protected
Chris@909:
Chris@909: HTML_ESCAPE = { #:nodoc:
Chris@909: '&' => '&',
Chris@909: '"' => '"',
Chris@909: '>' => '>',
Chris@909: '<' => '<',
Chris@909: }
Chris@909:
Chris@909: # This was to prevent illegal HTML.
Chris@909: # Strange chars should still be avoided in codes.
Chris@909: evil_chars = Array(0x00...0x20) - [?\n, ?\t, ?\s]
Chris@909: evil_chars.each { |i| HTML_ESCAPE[i.chr] = ' ' }
Chris@909: #ansi_chars = Array(0x7f..0xff)
Chris@909: #ansi_chars.each { |i| HTML_ESCAPE[i.chr] = '%d;' % i }
Chris@909: # \x9 (\t) and \xA (\n) not included
Chris@909: #HTML_ESCAPE_PATTERN = /[\t&"><\0-\x8\xB-\x1f\x7f-\xff]/
Chris@909: HTML_ESCAPE_PATTERN = /[\t"&><\0-\x8\xB-\x1f]/
Chris@909:
Chris@909: TOKEN_KIND_TO_INFO = Hash.new do |h, kind|
Chris@909: h[kind] = kind.to_s.gsub(/_/, ' ').gsub(/\b\w/) { $&.capitalize }
Chris@909: end
Chris@909:
Chris@909: TRANSPARENT_TOKEN_KINDS = Set[
Chris@909: :delimiter, :modifier, :content, :escape, :inline_delimiter,
Chris@909: ]
Chris@909:
Chris@909: # Generate a hint about the given +kinds+ in a +hint+ style.
Chris@909: #
Chris@909: # +hint+ may be :info, :info_long or :debug.
Chris@909: def self.token_path_to_hint hint, kinds
Chris@909: kinds = Array kinds
Chris@909: title =
Chris@909: case hint
Chris@909: when :info
Chris@909: kinds = kinds[1..-1] if TRANSPARENT_TOKEN_KINDS.include? kinds.first
Chris@909: TOKEN_KIND_TO_INFO[kinds.first]
Chris@909: when :info_long
Chris@909: kinds.reverse.map { |kind| TOKEN_KIND_TO_INFO[kind] }.join('/')
Chris@909: when :debug
Chris@909: kinds.inspect
Chris@909: end
Chris@909: title ? " title=\"#{title}\"" : ''
Chris@909: end
Chris@909:
Chris@909: def setup options
Chris@909: super
Chris@909:
Chris@909: if options[:wrap] || options[:line_numbers]
Chris@909: @real_out = @out
Chris@909: @out = ''
Chris@909: end
Chris@909:
Chris@909: @HTML_ESCAPE = HTML_ESCAPE.dup
Chris@909: @HTML_ESCAPE["\t"] = ' ' * options[:tab_width]
Chris@909:
Chris@909: @opened = []
Chris@909: @last_opened = nil
Chris@909: @css = CSS.new options[:style]
Chris@909:
Chris@909: hint = options[:hint]
Chris@909: if hint && ![:debug, :info, :info_long].include?(hint)
Chris@909: raise ArgumentError, "Unknown value %p for :hint; \
Chris@909: expected :info, :info_long, :debug, false, or nil." % hint
Chris@909: end
Chris@909:
Chris@909: css_classes = TokenKinds
Chris@909: case options[:css]
Chris@909: when :class
Chris@909: @span_for_kind = Hash.new do |h, k|
Chris@909: if k.is_a? ::Symbol
Chris@909: kind = k_dup = k
Chris@909: else
Chris@909: kind = k.first
Chris@909: k_dup = k.dup
Chris@909: end
Chris@909: if kind != :space && (hint || css_class = css_classes[kind])
Chris@909: title = HTML.token_path_to_hint hint, k if hint
Chris@909: css_class ||= css_classes[kind]
Chris@909: h[k_dup] = ""
Chris@909: else
Chris@909: h[k_dup] = nil
Chris@909: end
Chris@909: end
Chris@909: when :style
Chris@909: @span_for_kind = Hash.new do |h, k|
Chris@909: kind = k.is_a?(Symbol) ? k : k.first
Chris@909: h[k.is_a?(Symbol) ? k : k.dup] =
Chris@909: if kind != :space && (hint || css_classes[kind])
Chris@909: title = HTML.token_path_to_hint hint, k if hint
Chris@909: style = @css.get_style Array(k).map { |c| css_classes[c] }
Chris@909: ""
Chris@909: end
Chris@909: end
Chris@909: else
Chris@909: raise ArgumentError, "Unknown value %p for :css." % options[:css]
Chris@909: end
Chris@909:
Chris@909: @set_last_opened = options[:hint] || options[:css] == :style
Chris@909: end
Chris@909:
Chris@909: def finish options
Chris@909: unless @opened.empty?
Chris@909: warn '%d tokens still open: %p' % [@opened.size, @opened] if $CODERAY_DEBUG
Chris@909: @out << '' while @opened.pop
Chris@909: @last_opened = nil
Chris@909: end
Chris@909:
Chris@909: @out.extend Output
Chris@909: @out.css = @css
Chris@909: if options[:line_numbers]
Chris@909: Numbering.number! @out, options[:line_numbers], options
Chris@909: end
Chris@909: @out.wrap! options[:wrap]
Chris@909: @out.apply_title! options[:title]
Chris@909:
Chris@909: if defined?(@real_out) && @real_out
Chris@909: @real_out << @out
Chris@909: @out = @real_out
Chris@909: end
Chris@909:
Chris@909: super
Chris@909: end
Chris@909:
Chris@909: public
Chris@909:
Chris@909: def text_token text, kind
Chris@909: if text =~ /#{HTML_ESCAPE_PATTERN}/o
Chris@909: text = text.gsub(/#{HTML_ESCAPE_PATTERN}/o) { |m| @HTML_ESCAPE[m] }
Chris@909: end
Chris@909: if style = @span_for_kind[@last_opened ? [kind, *@opened] : kind]
Chris@909: @out << style << text << ''
Chris@909: else
Chris@909: @out << text
Chris@909: end
Chris@909: end
Chris@909:
Chris@909: # token groups, eg. strings
Chris@909: def begin_group kind
Chris@909: @out << (@span_for_kind[@last_opened ? [kind, *@opened] : kind] || '')
Chris@909: @opened << kind
Chris@909: @last_opened = kind if @set_last_opened
Chris@909: end
Chris@909:
Chris@909: def end_group kind
Chris@909: if $CODERAY_DEBUG && (@opened.empty? || @opened.last != kind)
Chris@909: warn 'Malformed token stream: Trying to close a token (%p) ' \
Chris@909: 'that is not open. Open are: %p.' % [kind, @opened[1..-1]]
Chris@909: end
Chris@909: if @opened.pop
Chris@909: @out << ''
Chris@909: @last_opened = @opened.last if @last_opened
Chris@909: end
Chris@909: end
Chris@909:
Chris@909: # whole lines to be highlighted, eg. a deleted line in a diff
Chris@909: def begin_line kind
Chris@909: if style = @span_for_kind[@last_opened ? [kind, *@opened] : kind]
Chris@909: if style['class="']
Chris@909: @out << style.sub('class="', 'class="line ')
Chris@909: else
Chris@909: @out << style.sub('>', ' class="line">')
Chris@909: end
Chris@909: else
Chris@909: @out << ''
Chris@909: end
Chris@909: @opened << kind
Chris@909: @last_opened = kind if @options[:css] == :style
Chris@909: end
Chris@909:
Chris@909: def end_line kind
Chris@909: if $CODERAY_DEBUG && (@opened.empty? || @opened.last != kind)
Chris@909: warn 'Malformed token stream: Trying to close a line (%p) ' \
Chris@909: 'that is not open. Open are: %p.' % [kind, @opened[1..-1]]
Chris@909: end
Chris@909: if @opened.pop
Chris@909: @out << ''
Chris@909: @last_opened = @opened.last if @last_opened
Chris@909: end
Chris@909: end
Chris@909:
Chris@909: end
Chris@909:
Chris@909: end
Chris@909: end