diff --git a/README.md b/README.md
index a052a6ddb6..3e6f06bd87 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ First, take a look at the [pretty colors][].
``` ruby
# make some nice lexed html
source = File.read('/etc/bashrc')
-formatter = Rouge::Formatters::HTML.new(css_class: 'highlight')
+formatter = Rouge::Formatters::HTML.new
lexer = Rouge::Lexers::Shell.new
formatter.format(lexer.lex(source))
@@ -32,26 +32,37 @@ Rouge::Theme.find('base16.light').render(scope: '.highlight')
```
### Full options
-#### Formatter options
-##### css_class: 'highlight'
-Apply a class to the syntax-highlighted output. Set to false to not apply any css class.
-##### line_numbers: false
-Generate line numbers.
-
-##### start_line: 1
-Index to start line numbers.
-
-##### inline_theme: nil
-A `Rouge::CSSTheme` used to highlight the output with inline styles instead of classes. Allows string inputs (separate mode with a dot):
-
-```
-%w[colorful github monokai monokai.sublime thankful_eyes base16
- base16.dark base16.light base16.solarized base16.monokai]
-```
-
-##### wrap: true
-Wrap the highlighted content in a container. Defaults to `
`, or `` if line numbers are enabled.
+#### Formatters
+
+As of Rouge 2.0, you are encouraged to write your own formatter for custom formatting needs.
+Builtin formatters include:
+
+* `Rouge::Formatters::HTML.new` - will render your code with standard class names for tokens,
+ with no div-wrapping or other bells or whistles.
+* `Rouge::Formatters::HTMLInline.new(theme)` - will render your code with no class names, but
+ instead inline the styling options into the `style=` attribute. This is good for emails and
+ other systems where CSS support is minimal.
+* `Rouge::Formatters::HTMLLinewise.new(formatter, class_format: 'line-%i')`
+ This formatter will split your code into lines, each contained in its own div. The
+ `class_format` option will be used to add a class name to the div, given the line
+ number.
+* `Rouge::Formatters::HTMLPygments.new(formatter, css_class='codehilite')`
+ wraps the given formatter with div wrappers generally expected by stylesheets designed for
+ Pygments.
+* `Rouge::Formatters::HTMLTable.new(formatter, opts={})` will output an HTML table containing
+ numbered lines. Options are:
+ * `start_line: 1` - the number of the first line
+ * `line_format: '%i'` - a `sprintf` template for the line number itself
+ * `table_class: 'rouge-table'` - a CSS class for the table
+ * `gutter_class: 'rouge-gutter'` - a CSS class for the gutter
+ * `code_class: 'rouge-code'` - a CSS class for the code column
+* `Rouge::Formatters::HTMLLegacy.new(opts={})` is a backwards-compatibility class intended
+ for users of rouge 1.x, with options that were supported then. Options are:
+ * `inline_theme: nil` - use an HTMLInline formatter with the given theme
+ * `line_numbers: false` - use an HTMLTable formatter
+ * `wrap: true` - use an HTMLPygments wrapper
+ * `css_class: 'codehilite'` - a CSS class to use for the pygments wrapper
#### Lexer options
##### debug: false
diff --git a/lib/rouge.rb b/lib/rouge.rb
index 2d6f7305f4..d1e9b9f6e2 100644
--- a/lib/rouge.rb
+++ b/lib/rouge.rb
@@ -54,6 +54,11 @@ def highlight(text, lexer, formatter, &b)
load load_dir.join('rouge/formatter.rb')
load load_dir.join('rouge/formatters/html.rb')
+load load_dir.join('rouge/formatters/html_table.rb')
+load load_dir.join('rouge/formatters/html_pygments.rb')
+load load_dir.join('rouge/formatters/html_legacy.rb')
+load load_dir.join('rouge/formatters/html_linewise.rb')
+load load_dir.join('rouge/formatters/html_inline.rb')
load load_dir.join('rouge/formatters/terminal256.rb')
load load_dir.join('rouge/formatters/null.rb')
diff --git a/lib/rouge/cli.rb b/lib/rouge/cli.rb
index 0071400358..3e80190312 100644
--- a/lib/rouge/cli.rb
+++ b/lib/rouge/cli.rb
@@ -181,6 +181,8 @@ def self.doc
def self.parse(argv)
opts = {
:formatter => 'terminal256',
+ :theme => 'thankful_eyes',
+ :css_class => 'codehilite',
:input_file => '-',
:lexer_opts => {},
:formatter_opts => {},
@@ -195,12 +197,14 @@ def self.parse(argv)
opts[:mimetype] = argv.shift
when '--lexer', '-l'
opts[:lexer] = argv.shift
- when '--formatter', '-f'
+ when '--formatter-preset', '-f'
opts[:formatter] = argv.shift
+ when '--theme', '-t'
+ opts[:theme] = argv.shift
+ when '--css-class', '-c'
+ opts[:css_class] = argv.shift
when '--lexer-opts', '-L'
opts[:lexer_opts] = parse_cgi(argv.shift)
- when '--formatter-opts', '-F'
- opts[:formatter_opts] = parse_cgi(argv.shift)
when /^--/
error! "unknown option #{arg.inspect}"
else
@@ -246,10 +250,17 @@ def initialize(opts={})
@lexer_opts = opts[:lexer_opts]
- formatter_class = Formatter.find(opts[:formatter]) \
- or error! "unknown formatter #{opts[:formatter]}"
+ theme = Theme.find(opts[:theme]).new or error! "unknown theme #{opts[:theme]}"
- @formatter = formatter_class.new(opts[:formatter_opts])
+ @formatter = case opts[:formatter]
+ when 'terminal256' then Formatters::Terminal256.new(theme)
+ when 'html' then Formatters::HTML.new
+ when 'html-pygments' then Formatters::HTMLPygments.new(Formatters::HTML.new, opts[:css_class])
+ when 'html-inline' then Formatters::HTMLInline.new(theme)
+ when 'html-table' then Formatters::HTMLTable.new(Formatters::HTML.new)
+ else
+ error! "unknown formatter preset #{opts[:formatter]}"
+ end
end
def run
diff --git a/lib/rouge/formatter.rb b/lib/rouge/formatter.rb
index 646ae8d291..54c096be22 100644
--- a/lib/rouge/formatter.rb
+++ b/lib/rouge/formatter.rb
@@ -25,6 +25,10 @@ def self.format(tokens, opts={}, &b)
new(opts).format(tokens, &b)
end
+ def initialize(opts={})
+ # pass
+ end
+
# Format a token stream.
def format(tokens, &b)
return stream(tokens, &b) if block_given?
@@ -46,5 +50,26 @@ def render(tokens)
def stream(tokens, &b)
raise 'abstract'
end
+
+ protected
+ def token_lines(tokens, &b)
+ return enum_for(:lines, tokens) unless block_given?
+
+ out = []
+ tokens.each do |tok, val|
+ val.scan /\n|[^\n]+/ do |s|
+ if s == "\n"
+ yield out
+ out = []
+ else
+ out << [tok, s]
+ end
+ end
+ end
+
+ # for inputs not ending in a newline
+ yield out if out.any?
+ end
+
end
end
diff --git a/lib/rouge/formatters/html.rb b/lib/rouge/formatters/html.rb
index cdc8a2d946..88a66ac1f2 100644
--- a/lib/rouge/formatters/html.rb
+++ b/lib/rouge/formatters/html.rb
@@ -9,109 +9,32 @@ module Formatters
class HTML < Formatter
tag 'html'
- # @option opts [String] :css_class ('highlight')
- # @option opts [true/false] :line_numbers (false)
- # @option opts [Rouge::CSSTheme] :inline_theme (nil)
- # @option opts [true/false] :wrap (true)
- #
- # Initialize with options.
- #
- # If `:inline_theme` is given, then instead of rendering the
- # tokens as
tags with CSS classes, the styles according to
- # the given theme will be inlined in "style" attributes. This is
- # useful for formats in which stylesheets are not available.
- #
- # Content will be wrapped in a tag (`div` if tableized, `pre` if
- # not) with the given `:css_class` unless `:wrap` is set to `false`.
- def initialize(opts={})
- @css_class = opts.fetch(:css_class, 'highlight')
- @css_class = " class=#{@css_class.inspect}" if @css_class
-
- @line_numbers = opts.fetch(:line_numbers, false)
- @start_line = opts.fetch(:start_line, 1)
- @inline_theme = opts.fetch(:inline_theme, nil)
- @inline_theme = Theme.find(@inline_theme).new if @inline_theme.is_a? String
-
- @wrap = opts.fetch(:wrap, true)
- end
-
# @yield the html output.
def stream(tokens, &b)
- if @line_numbers
- stream_tableized(tokens, &b)
- else
- stream_untableized(tokens, &b)
- end
+ tokens.each { |tok, val| yield span(tok, val) }
end
- private
- def stream_untableized(tokens, &b)
- yield "" if @wrap
- tokens.each{ |tok, val| span(tok, val, &b) }
- yield "
\n" if @wrap
+ def span(tok, val)
+ safe_span(tok, val.gsub(/[&<>]/, TABLE_FOR_ESCAPE_HTML))
end
- def stream_tableized(tokens)
- num_lines = 0
- last_val = ''
- formatted = ''
-
- tokens.each do |tok, val|
- last_val = val
- num_lines += val.scan(/\n/).size
- span(tok, val) { |str| formatted << str }
- end
+ def safe_span(tok, safe_val)
+ if tok == Token::Tokens::Text
+ safe_val
+ else
+ shortname = tok.shortname \
+ or raise "unknown token: #{tok.inspect} for #{safe_val.inspect}"
- # add an extra line for non-newline-terminated strings
- if last_val[-1] != "\n"
- num_lines += 1
- span(Token::Tokens::Text::Whitespace, "\n") { |str| formatted << str }
+ "#{safe_val}"
end
-
- # generate a string of newline-separated line numbers for the gutter>
- numbers = %<#{(@start_line..num_lines+@start_line-1)
- .to_a.join("\n")}
>
-
- yield "" if @wrap
- yield '
'
-
- # the "gl" class applies the style for Generic.Lineno
- yield ''
- yield numbers
- yield ' | '
-
- yield ''
- yield ''
- yield formatted
- yield ' '
- yield ' | '
-
- yield "
\n"
- yield "
\n" if @wrap
end
+ private
TABLE_FOR_ESCAPE_HTML = {
'&' => '&',
'<' => '<',
'>' => '>',
}
-
- def span(tok, val)
- val = val.gsub(/[&<>]/, TABLE_FOR_ESCAPE_HTML)
- shortname = tok.shortname or raise "unknown token: #{tok.inspect} for #{val.inspect}"
-
- if shortname.empty?
- yield val
- else
- if @inline_theme
- rules = @inline_theme.style_for(tok).rendered_rules
-
- yield "#{val}"
- else
- yield "#{val}"
- end
- end
- end
end
end
end
diff --git a/lib/rouge/formatters/html_inline.rb b/lib/rouge/formatters/html_inline.rb
new file mode 100644
index 0000000000..a6a0a2f116
--- /dev/null
+++ b/lib/rouge/formatters/html_inline.rb
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*- #
+
+module Rouge
+ module Formatters
+ class HTMLInline < HTML
+ tag 'html_inline'
+
+ def initialize(theme)
+ @theme = theme
+ end
+
+ def safe_span(tok, safe_val)
+ return safe_val if tok == Token::Tokens::Text
+
+ rules = @theme.style_for(tok).rendered_rules
+
+ "#{safe_val}"
+ end
+ end
+ end
+end
+
diff --git a/lib/rouge/formatters/html_legacy.rb b/lib/rouge/formatters/html_legacy.rb
new file mode 100644
index 0000000000..813cefb81f
--- /dev/null
+++ b/lib/rouge/formatters/html_legacy.rb
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*- #
+
+# stdlib
+require 'cgi'
+
+module Rouge
+ module Formatters
+ # Transforms a token stream into HTML output.
+ class HTMLLegacy < Formatter
+ tag 'html_legacy'
+
+ # @option opts [String] :css_class ('highlight')
+ # @option opts [true/false] :line_numbers (false)
+ # @option opts [Rouge::CSSTheme] :inline_theme (nil)
+ # @option opts [true/false] :wrap (true)
+ #
+ # Initialize with options.
+ #
+ # If `:inline_theme` is given, then instead of rendering the
+ # tokens as tags with CSS classes, the styles according to
+ # the given theme will be inlined in "style" attributes. This is
+ # useful for formats in which stylesheets are not available.
+ #
+ # Content will be wrapped in a tag (`div` if tableized, `pre` if
+ # not) with the given `:css_class` unless `:wrap` is set to `false`.
+ def initialize(opts={})
+ @formatter = opts[:inline_theme] ? HTMLInline.new(opts[:inline_theme])
+ : HTML.new
+
+
+ @formatter = HTMLTable.new(@formatter, opts) if opts[:line_numbers]
+
+ if opts.fetch(:wrap, true)
+ @formatter = HTMLPygments.new(@formatter, opts.fetch(:css_class, 'codehilite'))
+ end
+ end
+
+ # @yield the html output.
+ def stream(tokens, &b)
+ @formatter.stream(tokens, &b)
+ end
+ end
+ end
+end
diff --git a/lib/rouge/formatters/html_linewise.rb b/lib/rouge/formatters/html_linewise.rb
new file mode 100644
index 0000000000..55c8e72ae9
--- /dev/null
+++ b/lib/rouge/formatters/html_linewise.rb
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*- #
+
+module Rouge
+ module Formatters
+ class HTMLLinewise < Formatter
+ def initialize(formatter, opts={})
+ @formatter = formatter
+ @class_format = opts.fetch(:class, 'line-%i')
+ end
+
+ def stream(tokens, &b)
+ token_lines(tokens) do |line|
+ yield ""
+ line.each do |tok, val|
+ yield @formatter.span(tok, val)
+ end
+ yield '
'
+ end
+ end
+
+ def next_line_class
+ @lineno ||= 0
+ sprintf(@class_format, @lineno += 1).inspect
+ end
+ end
+ end
+end
diff --git a/lib/rouge/formatters/html_pygments.rb b/lib/rouge/formatters/html_pygments.rb
new file mode 100644
index 0000000000..38c88c7fce
--- /dev/null
+++ b/lib/rouge/formatters/html_pygments.rb
@@ -0,0 +1,16 @@
+module Rouge
+ module Formatters
+ class HTMLPygments < Formatter
+ def initialize(inner, css_class='codehilite')
+ @inner = inner
+ @css_class = css_class
+ end
+
+ def stream(tokens, &b)
+ yield %<>
+ @inner.stream(tokens, &b)
+ yield "
"
+ end
+ end
+ end
+end
diff --git a/lib/rouge/formatters/html_table.rb b/lib/rouge/formatters/html_table.rb
new file mode 100644
index 0000000000..c440e34f80
--- /dev/null
+++ b/lib/rouge/formatters/html_table.rb
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*- #
+
+module Rouge
+ module Formatters
+ class HTMLTable < Formatter
+ tag 'html_table'
+
+ def initialize(inner, opts={})
+ @inner = inner
+ @start_line = opts.fetch(:start_line, 1)
+ @line_format = opts.fetch(:line_format, '%i')
+ @table_class = opts.fetch(:table_class, 'rouge-table')
+ @gutter_class = opts.fetch(:gutter_class, 'rouge-gutter')
+ @code_class = opts.fetch(:code_class, 'rouge-code')
+ end
+
+ def style(scope)
+ yield "#{scope} .rouge-table { border-spacing: 0 }"
+ yield "#{scope} .rouge-gutter { text-align: right }"
+ end
+
+ def stream(tokens, &b)
+ num_lines = 0
+ last_val = ''
+ formatted = ''
+
+ tokens.each do |tok, val|
+ last_val = val
+ num_lines += val.scan(/\n/).size
+ formatted << @inner.span(tok, val)
+ end
+
+ # add an extra line for non-newline-terminated strings
+ if last_val[-1] != "\n"
+ num_lines += 1
+ @inner.span(Token::Tokens::Text::Whitespace, "\n") { |str| formatted << str }
+ end
+
+ # generate a string of newline-separated line numbers for the gutter>
+ formatted_line_numbers = (@start_line..num_lines+@start_line-1).map do |i|
+ sprintf("#{@line_format}", i) << "\n"
+ end.join('')
+
+ numbers = %<#{formatted_line_numbers}
>
+
+ yield %<>
+
+ # the "gl" class applies the style for Generic.Lineno
+ yield %<>
+ yield numbers
+ yield ' | '
+
+ yield %<>
+ yield formatted
+ yield ' | '
+
+ yield "
\n"
+ end
+ end
+ end
+end
diff --git a/lib/rouge/formatters/terminal256.rb b/lib/rouge/formatters/terminal256.rb
index 165120d86f..b89d5244bb 100644
--- a/lib/rouge/formatters/terminal256.rb
+++ b/lib/rouge/formatters/terminal256.rb
@@ -4,17 +4,13 @@ module Rouge
module Formatters
# A formatter for 256-color terminals
class Terminal256 < Formatter
- tag 'terminal256'
-
# @private
attr_reader :theme
-
- # @option opts :theme
- # (default is thankful_eyes) the theme to render with.
- def initialize(opts={})
- @theme = opts[:theme] || 'thankful_eyes'
- @theme = Theme.find(@theme) if @theme.is_a? String
+ # @argument theme
+ # the theme to render with.
+ def initialize(theme=nil)
+ @theme = theme || Themes::ThankfulEyes
end
def stream(tokens, &b)
diff --git a/lib/rouge/plugins/redcarpet.rb b/lib/rouge/plugins/redcarpet.rb
index f27511b05e..4ae513bc13 100644
--- a/lib/rouge/plugins/redcarpet.rb
+++ b/lib/rouge/plugins/redcarpet.rb
@@ -23,7 +23,7 @@ def block_code(code, language)
# override this method for custom formatting behavior
def rouge_formatter(lexer)
- Formatters::HTML.new(:css_class => "highlight #{lexer.tag}")
+ Formatters::HTMLLegacy.new(:css_class => "highlight #{lexer.tag}")
end
end
end
diff --git a/spec/formatters/html_linewise_spec.rb b/spec/formatters/html_linewise_spec.rb
new file mode 100644
index 0000000000..1954a02d31
--- /dev/null
+++ b/spec/formatters/html_linewise_spec.rb
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*- #
+
+describe Rouge::Formatters::HTMLLinewise do
+ let(:subject) { Rouge::Formatters::HTMLLinewise.new(formatter, options) }
+ let(:formatter) { Rouge::Formatters::HTML.new }
+
+ let(:options) { {} }
+ let(:output) { subject.format(input_stream) }
+ Token = Rouge::Token
+
+ describe 'a simple token stream' do
+ let(:input_stream) { [[Token['Name'], 'foo']] }
+
+ it 'formats' do
+ assert { output == %(foo
) }
+ end
+ end
+
+ describe 'final newlines' do
+ let(:input_stream) { [[Token['Text'], "foo\n"], [Token['Name'], "bar\n"]] }
+
+ it 'formats' do
+ assert { output == %(foo
bar
) }
+ end
+ end
+
+ describe 'intermediate newlines' do
+ let(:input_stream) { [[Token['Name'], "foo\nbar"]] }
+
+ it 'formats' do
+ assert { output == %(foo
bar
) }
+ end
+ end
+end
diff --git a/spec/formatters/html_spec.rb b/spec/formatters/html_spec.rb
index 0f7b91ec0c..d3c4b7b6ff 100644
--- a/spec/formatters/html_spec.rb
+++ b/spec/formatters/html_spec.rb
@@ -1,18 +1,14 @@
# -*- coding: utf-8 -*- #
describe Rouge::Formatters::HTML do
- let(:subject) { Rouge::Formatters::HTML.new(options) }
+ let(:subject) { Rouge::Formatters::HTMLLegacy.new(options) }
let(:options) { {} }
Token = Rouge::Token
- it 'formats a simple token stream' do
- out = subject.format([[Token['Name'], 'foo']])
- assert { out == %(foo
\n) }
- end
-
describe 'skipping the wrapper' do
- let(:options) { { :wrap => false } }
+ let(:subject) { Rouge::Formatters::HTML.new }
let(:output) { subject.format([[Token['Name'], 'foo']]) }
+ let(:options) { { :wrap => false } }
it 'skips the wrapper' do
assert { output == 'foo' }
@@ -38,21 +34,32 @@ class InlineTheme < Rouge::CSSTheme
describe 'tableized line numbers' do
let(:options) { { :line_numbers => true } }
- let(:text) { Rouge::Lexers::Clojure.demo }
let(:tokens) { Rouge::Lexers::Clojure.lex(text) }
let(:output) { subject.format(tokens) }
let(:line_numbers) { output[%r[]m].scan(/\d+/m).size }
let(:output_code) {
- output =~ %r((.*?) | )m
+ output =~ %r((.*?))m
$1
}
let(:code_lines) { output_code.scan(/\n/).size }
- it 'preserves the number of lines' do
- assert { code_lines == line_numbers }
+ describe 'newline-terminated text' do
+ let(:text) { Rouge::Lexers::Clojure.demo }
+
+ it 'preserves the number of lines' do
+ assert { code_lines == line_numbers }
+ end
+ end
+
+ describe 'non-newline-terminated text' do
+ let(:text) { Rouge::Lexers::Clojure.demo.chomp }
+
+ it 'preserves the number of lines' do
+ assert { code_lines == line_numbers }
+ end
end
end
end
diff --git a/spec/visual/app.rb b/spec/visual/app.rb
index 337223b34b..72898ed98d 100644
--- a/spec/visual/app.rb
+++ b/spec/visual/app.rb
@@ -29,12 +29,12 @@ def reload_source!
theme_class = Rouge::Theme.find(params[:theme] || 'thankful_eyes')
halt 404 unless theme_class
- @theme = theme_class.new
+ @theme = theme_class.new(scope: '.codehilite')
formatter_opts = { :line_numbers => params[:line_numbers] }
formatter_opts[:inline_theme] = @theme if params[:inline]
- @formatter = Rouge::Formatters::HTML.new(formatter_opts)
+ @formatter = Rouge::Formatters::HTMLLegacy.new(formatter_opts)
end
get '/:lexer' do |lexer_name|
diff --git a/spec/visual/templates/index.erb b/spec/visual/templates/index.erb
index 5ef33123fc..9c20a34ac4 100644
--- a/spec/visual/templates/index.erb
+++ b/spec/visual/templates/index.erb
@@ -16,6 +16,5 @@
<%= Rouge.highlight(sample.demo, sample, @formatter) %>
-
<% end %>