Added syntax highlightment for repository files (using CodeRay).

Supported languages: c, ruby, rhtml, yaml, html, xml.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@644 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang
2007-08-15 20:20:18 +00:00
parent a5849ee044
commit 889d50089d
53 changed files with 5813 additions and 13 deletions

View File

@@ -0,0 +1,9 @@
module CodeRay
module Encoders
map :stats => :statistic,
:plain => :text,
:tex => :latex
end
end

View File

@@ -0,0 +1,21 @@
module CodeRay
module Encoders
class Count < Encoder
include Streamable
register_for :count
protected
def setup options
@out = 0
end
def token text, kind
@out += 1
end
end
end
end

View File

@@ -0,0 +1,41 @@
module CodeRay
module Encoders
# = Debug Encoder
#
# Fast encoder producing simple debug output.
#
# It is readable and diff-able and is used for testing.
#
# You cannot fully restore the tokens information from the
# output, because consecutive :space tokens are merged.
# Use Tokens#dump for caching purposes.
class Debug < Encoder
include Streamable
register_for :debug
FILE_EXTENSION = 'raydebug'
protected
def text_token text, kind
if kind == :space
text
else
text = text.gsub(/[)\\]/, '\\\\\0') # escape ) and \
"#{kind}(#{text})"
end
end
def open_token kind
"#{kind}<"
end
def close_token kind
">"
end
end
end
end

View File

@@ -0,0 +1,20 @@
module CodeRay
module Encoders
load :html
class Div < HTML
FILE_EXTENSION = 'div.html'
register_for :div
DEFAULT_OPTIONS = HTML::DEFAULT_OPTIONS.merge({
:css => :style,
:wrap => :div,
})
end
end
end

View File

@@ -0,0 +1,262 @@
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)
# #-> <span class="CodeRay"><span class="co">Some</span> /code/</span>
# puts CodeRay.scan('Some /code/', :ruby).span #-> the same
#
# puts CodeRay.scan('Some code', :ruby).html(
# :wrap => nil,
# :line_numbers => :inline,
# :css => :style
# )
# #-> <span class="no">1</span> <span style="color:#036; font-weight:bold;">Some</span> 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
#
# === :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
#
# === :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,
:css => :class,
:style => :cycnus,
:wrap => nil,
:line_numbers => nil,
:line_number_start => 1,
:bold_every => 10,
:hint => false,
}
helper :output, :css
attr_reader :css
protected
HTML_ESCAPE = { #:nodoc:
'&' => '&amp;',
'"' => '&quot;',
'>' => '&gt;',
'<' => '&lt;',
}
# 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}\""
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 = 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] = '<span%s>' % [title]
else
h[k.dup] = '<span%s class="%s">' % [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
'<span%s style="%s">' % [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 << '</span>' * @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]
super
end
def token text, type
if text.is_a? ::String
if text =~ /#{HTML_ESCAPE_PATTERN}/o
text = text.gsub(/#{HTML_ESCAPE_PATTERN}/o) { |m| @HTML_ESCAPE[m] }
end
@opened[0] = type
if style = @css_style[@opened]
@out << style << text << '</span>'
else
@out << text
end
else
case text
when :open
@opened[0] = type
@out << (@css_style[@opened] || '<span>')
@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 << '</span>'
@opened.pop
end
when nil
raise 'Token with nil as text was given: %p' % [[text, type]]
else
raise 'unknown token kind: %p' % text
end
end
end
end
end
end

View File

@@ -0,0 +1,65 @@
module CodeRay
module Encoders
class HTML
class CSS
attr :stylesheet
def CSS.load_stylesheet style = nil
CodeRay::Styles[style]
end
def initialize style = :default
@classes = Hash.new
style = CSS.load_stylesheet style
@stylesheet = [
style::CSS_MAIN_STYLES,
style::TOKEN_COLORS.gsub(/^(?!$)/, '.CodeRay ')
].join("\n")
parse style::TOKEN_COLORS
end
def [] *styles
cl = @classes[styles.first]
return '' unless cl
style = ''
1.upto(styles.size) do |offset|
break if style = cl[styles[offset .. -1]]
end
raise 'Style not found: %p' % [styles] if $DEBUG and style.empty?
return style
end
private
CSS_CLASS_PATTERN = /
( (?: # $1 = classes
\s* \. [-\w]+
)+ )
\s* \{ \s*
( [^\}]+ )? # $2 = style
\s* \} \s*
|
( . ) # $3 = error
/mx
def parse stylesheet
stylesheet.scan CSS_CLASS_PATTERN do |classes, style, error|
raise "CSS parse error: '#{error.inspect}' not recognized" if error
styles = classes.scan(/[-\w]+/)
cl = styles.pop
@classes[cl] ||= Hash.new
@classes[cl][styles] = style.to_s.strip
end
end
end
end
end
end
if $0 == __FILE__
require 'pp'
pp CodeRay::Encoders::HTML::CSS.new
end

View File

@@ -0,0 +1,122 @@
module CodeRay
module Encoders
class HTML
module Output
def numerize *args
clone.numerize!(*args)
end
=begin NUMERIZABLE_WRAPPINGS = {
:table => [:div, :page, nil],
:inline => :all,
:list => [:div, :page, nil]
}
NUMERIZABLE_WRAPPINGS.default = :all
=end
def numerize! mode = :table, options = {}
return self unless mode
options = DEFAULT_OPTIONS.merge options
start = options[:line_number_start]
unless start.is_a? Integer
raise ArgumentError, "Invalid value %p for :line_number_start; Integer expected." % start
end
#allowed_wrappings = NUMERIZABLE_WRAPPINGS[mode]
#unless allowed_wrappings == :all or allowed_wrappings.include? options[:wrap]
# raise ArgumentError, "Can't numerize, :wrap must be in %p, but is %p" % [NUMERIZABLE_WRAPPINGS, options[:wrap]]
#end
bold_every = options[:bold_every]
bolding =
if bold_every == false
proc { |line| line.to_s }
elsif bold_every.is_a? Integer
raise ArgumentError, ":bolding can't be 0." if bold_every == 0
proc do |line|
if line % bold_every == 0
"<strong>#{line}</strong>" # every bold_every-th number in bold
else
line.to_s
end
end
else
raise ArgumentError, 'Invalid value %p for :bolding; false or Integer expected.' % bold_every
end
case mode
when :inline
max_width = (start + line_count).to_s.size
line = start
gsub!(/^/) do
line_number = bolding.call line
indent = ' ' * (max_width - line.to_s.size)
res = "<span class=\"no\">#{indent}#{line_number}</span> "
line += 1
res
end
when :table
# This is really ugly.
# Because even monospace fonts seem to have different heights when bold,
# I make the newline bold, both in the code and the line numbers.
# FIXME Still not working perfect for Mr. Internet Exploder
# FIXME Firefox struggles with very long codes (> 200 lines)
line_numbers = (start ... start + line_count).to_a.map(&bolding).join("\n")
line_numbers << "\n" # also for Mr. MS Internet Exploder :-/
line_numbers.gsub!(/\n/) { "<tt>\n</tt>" }
line_numbers_table_tpl = TABLE.apply('LINE_NUMBERS', line_numbers)
gsub!(/\n/) { "<tt>\n</tt>" }
wrap_in! line_numbers_table_tpl
@wrapped_in = :div
when :list
opened_tags = []
gsub!(/^.*$\n?/) do |line|
line.chomp!
open = opened_tags.join
line.scan(%r!<(/)?span[^>]*>?!) do |close,|
if close
opened_tags.pop
else
opened_tags << $&
end
end
close = '</span>' * opened_tags.size
"<li>#{open}#{line}#{close}</li>"
end
wrap_in! LIST
@wrapped_in = :div
else
raise ArgumentError, 'Unknown value %p for mode: expected one of %p' %
[mode, [:table, :list, :inline]]
end
self
end
def line_count
line_count = count("\n")
position_of_last_newline = rindex(?\n)
if position_of_last_newline
after_last_newline = self[position_of_last_newline + 1 .. -1]
ends_with_newline = after_last_newline[/\A(?:<\/span>)*\z/]
line_count += 1 if not ends_with_newline
end
line_count
end
end
end
end
end

View File

@@ -0,0 +1,195 @@
module CodeRay
module Encoders
class HTML
# This module is included in the output String from thew HTML Encoder.
#
# It provides methods like wrap, div, page etc.
#
# Remember to use #clone instead of #dup to keep the modules the object was
# extended with.
#
# TODO: more doc.
module Output
require 'coderay/encoders/html/numerization.rb'
attr_accessor :css
class << self
# This makes Output look like a class.
#
# Example:
#
# a = Output.new '<span class="co">Code</span>'
# a.wrap! :page
def new string, css = CSS.new, element = nil
output = string.clone.extend self
output.wrapped_in = element
output.css = css
output
end
# Raises an exception if an object that doesn't respond to to_str is extended by Output,
# to prevent users from misuse. Use Module#remove_method to disable.
def extended o
warn "The Output module is intended to extend instances of String, not #{o.class}." unless o.respond_to? :to_str
end
def make_stylesheet css, in_tag = false
sheet = css.stylesheet
sheet = <<-CSS if in_tag
<style type="text/css">
#{sheet}
</style>
CSS
sheet
end
def page_template_for_css css
sheet = make_stylesheet css
PAGE.apply 'CSS', sheet
end
# Define a new wrapper. This is meta programming.
def wrapper *wrappers
wrappers.each do |wrapper|
define_method wrapper do |*args|
wrap wrapper, *args
end
define_method "#{wrapper}!".to_sym do |*args|
wrap! wrapper, *args
end
end
end
end
wrapper :div, :span, :page
def wrapped_in? element
wrapped_in == element
end
def wrapped_in
@wrapped_in ||= nil
end
attr_writer :wrapped_in
def wrap_in template
clone.wrap_in! template
end
def wrap_in! template
Template.wrap! self, template, 'CONTENT'
self
end
def wrap! element, *args
return self if not element or element == wrapped_in
case element
when :div
raise "Can't wrap %p in %p" % [wrapped_in, element] unless wrapped_in? nil
wrap_in! DIV
when :span
raise "Can't wrap %p in %p" % [wrapped_in, element] unless wrapped_in? nil
wrap_in! SPAN
when :page
wrap! :div if wrapped_in? nil
raise "Can't wrap %p in %p" % [wrapped_in, element] unless wrapped_in? :div
wrap_in! Output.page_template_for_css(@css)
when nil
return self
else
raise "Unknown value %p for :wrap" % element
end
@wrapped_in = element
self
end
def wrap *args
clone.wrap!(*args)
end
def stylesheet in_tag = false
Output.make_stylesheet @css, in_tag
end
class Template < String
def self.wrap! str, template, target
target = Regexp.new(Regexp.escape("<%#{target}%>"))
if template =~ target
str[0,0] = $`
str << $'
else
raise "Template target <%%%p%%> not found" % target
end
end
def apply target, replacement
target = Regexp.new(Regexp.escape("<%#{target}%>"))
if self =~ target
Template.new($` + replacement + $')
else
raise "Template target <%%%p%%> not found" % target
end
end
module Simple
def ` str #` <-- for stupid editors
Template.new str
end
end
end
extend Template::Simple
#-- don't include the templates in docu
SPAN = `<span class="CodeRay"><%CONTENT%></span>`
DIV = <<-`DIV`
<div class="CodeRay">
<div class="code"><pre><%CONTENT%></pre></div>
</div>
DIV
TABLE = <<-`TABLE`
<table class="CodeRay"><tr>
<td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre><%LINE_NUMBERS%></pre></td>
<td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><%CONTENT%></pre></td>
</tr></table>
TABLE
# title="double click to expand"
LIST = <<-`LIST`
<ol class="CodeRay"><%CONTENT%></ol>
LIST
PAGE = <<-`PAGE`
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="de">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>CodeRay HTML Encoder Example</title>
<style type="text/css">
<%CSS%>
</style>
</head>
<body style="background-color: white;">
<%CONTENT%>
</body>
</html>
PAGE
end
end
end
end

View File

@@ -0,0 +1,26 @@
module CodeRay
module Encoders
# = Null Encoder
#
# Does nothing and returns an empty string.
class Null < Encoder
include Streamable
register_for :null
# Defined for faster processing
def to_proc
proc {}
end
protected
def token(*)
# do nothing
end
end
end
end

View File

@@ -0,0 +1,21 @@
module CodeRay
module Encoders
load :html
class Page < HTML
FILE_EXTENSION = 'html'
register_for :page
DEFAULT_OPTIONS = HTML::DEFAULT_OPTIONS.merge({
:css => :class,
:wrap => :page,
:line_numbers => :table
})
end
end
end

View File

@@ -0,0 +1,20 @@
module CodeRay
module Encoders
load :html
class Span < HTML
FILE_EXTENSION = 'span.html'
register_for :span
DEFAULT_OPTIONS = HTML::DEFAULT_OPTIONS.merge({
:css => :style,
:wrap => :span,
})
end
end
end

View File

@@ -0,0 +1,77 @@
module CodeRay
module Encoders
# Makes a statistic for the given tokens.
class Statistic < Encoder
include Streamable
register_for :stats, :statistic
attr_reader :type_stats, :real_token_count
protected
TypeStats = Struct.new :count, :size
def setup options
@type_stats = Hash.new { |h, k| h[k] = TypeStats.new 0, 0 }
@real_token_count = 0
end
def generate tokens, options
@tokens = tokens
super
end
def text_token text, kind
@real_token_count += 1 unless kind == :space
@type_stats[kind].count += 1
@type_stats[kind].size += text.size
@type_stats['TOTAL'].size += text.size
@type_stats['TOTAL'].count += 1
end
# TODO Hierarchy handling
def block_token action, kind
@type_stats['TOTAL'].count += 1
@type_stats['open/close'].count += 1
end
STATS = <<-STATS
Code Statistics
Tokens %8d
Non-Whitespace %8d
Bytes Total %8d
Token Types (%d):
type count ratio size (average)
-------------------------------------------------------------
%s
STATS
# space 12007 33.81 % 1.7
TOKEN_TYPES_ROW = <<-TKR
%-20s %8d %6.2f %% %5.1f
TKR
def finish options
all = @type_stats['TOTAL']
all_count, all_size = all.count, all.size
@type_stats.each do |type, stat|
stat.size /= stat.count.to_f
end
types_stats = @type_stats.sort_by { |k, v| [-v.count, k.to_s] }.map do |k, v|
TOKEN_TYPES_ROW % [k, v.count, 100.0 * v.count / all_count, v.size]
end.join
STATS % [
all_count, @real_token_count, all_size,
@type_stats.delete_if { |k, v| k.is_a? String }.size,
types_stats
]
end
end
end
end

View File

@@ -0,0 +1,32 @@
module CodeRay
module Encoders
class Text < Encoder
include Streamable
register_for :text
FILE_EXTENSION = 'txt'
DEFAULT_OPTIONS = {
:separator => ''
}
protected
def setup options
@out = ''
@sep = options[:separator]
end
def token text, kind
@out << text + @sep if text.is_a? ::String
end
def finish options
@out.chomp @sep
end
end
end
end

View File

@@ -0,0 +1,44 @@
module CodeRay
module Encoders
# The Tokens encoder converts the tokens to a simple
# readable format. It doesn't use colors and is mainly
# intended for console output.
#
# The tokens are converted with Tokens.write_token.
#
# The format is:
#
# <token-kind> \t <escaped token-text> \n
#
# Example:
#
# require 'coderay'
# puts CodeRay.scan("puts 3 + 4", :ruby).tokens
#
# prints:
#
# ident puts
# space
# integer 3
# space
# operator +
# space
# integer 4
#
class Tokens < Encoder
include Streamable
register_for :tokens
FILE_EXTENSION = 'tok'
protected
def token text, kind
@out << CodeRay::Tokens.write_token(text, kind)
end
end
end
end

View File

@@ -0,0 +1,70 @@
module CodeRay
module Encoders
# = XML Encoder
#
# Uses REXML. Very slow.
class XML < Encoder
include Streamable
register_for :xml
FILE_EXTENSION = 'xml'
require 'rexml/document'
DEFAULT_OPTIONS = {
:tab_width => 8,
:pretty => -1,
:transitive => false,
}
protected
def setup options
@doc = REXML::Document.new
@doc << REXML::XMLDecl.new
@tab_width = options[:tab_width]
@root = @node = @doc.add_element('coderay-tokens')
end
def finish options
@doc.write @out, options[:pretty], options[:transitive], true
@out
end
def text_token text, kind
if kind == :space
token = @node
else
token = @node.add_element kind.to_s
end
text.scan(/(\x20+)|(\t+)|(\n)|[^\x20\t\n]+/) do |space, tab, nl|
case
when space
token << REXML::Text.new(space, true)
when tab
token << REXML::Text.new(tab, true)
when nl
token << REXML::Text.new(nl, true)
else
token << REXML::Text.new($&)
end
end
end
def open_token kind
@node = @node.add_element kind.to_s
end
def close_token kind
if @node == @root
raise 'no token to close!'
end
@node = @node.parent
end
end
end
end

View File

@@ -0,0 +1,22 @@
module CodeRay
module Encoders
# = YAML Encoder
#
# Slow.
class YAML < Encoder
register_for :yaml
FILE_EXTENSION = 'yaml'
protected
def compile tokens, options
require 'yaml'
@out = tokens.to_a.to_yaml
end
end
end
end