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