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