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 '' - - yield "
' - yield numbers - yield '' - yield '
'
-        yield formatted
-        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 %<' + + yield "
> + yield numbers + yield '
>
+        yield formatted
+        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 @@
 

<%= sample.tag %>

<%= Rouge.highlight(sample.demo, sample, @formatter) %> -
<% end %>