Skip to content

Commit

Permalink
Merge pull request rouge-ruby#450 from jneen/refactor.split-html
Browse files Browse the repository at this point in the history
[Rouge 2.0] split Formatters::HTML into different classes
  • Loading branch information
jneen authored Jun 14, 2016
2 parents e83a35a + efdb9ae commit 444738e
Show file tree
Hide file tree
Showing 16 changed files with 318 additions and 137 deletions.
51 changes: 31 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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 `<pre><code>`, or `<div>` 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
Expand Down
5 changes: 5 additions & 0 deletions lib/rouge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
23 changes: 17 additions & 6 deletions lib/rouge/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {},
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions lib/rouge/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
99 changes: 11 additions & 88 deletions lib/rouge/formatters/html.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span> 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 "<pre#@css_class><code>" if @wrap
tokens.each{ |tok, val| span(tok, val, &b) }
yield "</code></pre>\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 }
"<span class=\"#{shortname}\">#{safe_val}</span>"
end

# generate a string of newline-separated line numbers for the gutter>
numbers = %<<pre class="lineno">#{(@start_line..num_lines+@start_line-1)
.to_a.join("\n")}</pre>>

yield "<div#@css_class>" if @wrap
yield '<table style="border-spacing: 0"><tbody><tr>'

# the "gl" class applies the style for Generic.Lineno
yield '<td class="gutter gl" style="text-align: right">'
yield numbers
yield '</td>'

yield '<td class="code">'
yield '<pre>'
yield formatted
yield '</pre>'
yield '</td>'

yield "</tr></tbody></table>\n"
yield "</div>\n" if @wrap
end

private
TABLE_FOR_ESCAPE_HTML = {
'&' => '&amp;',
'<' => '&lt;',
'>' => '&gt;',
}

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 "<span style=\"#{rules.to_a.join(';')}\">#{val}</span>"
else
yield "<span class=\"#{shortname}\">#{val}</span>"
end
end
end
end
end
end
22 changes: 22 additions & 0 deletions lib/rouge/formatters/html_inline.rb
Original file line number Diff line number Diff line change
@@ -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

"<span style=\"#{rules.to_a.join(';')}\">#{safe_val}</span>"
end
end
end
end

44 changes: 44 additions & 0 deletions lib/rouge/formatters/html_legacy.rb
Original file line number Diff line number Diff line change
@@ -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 <span> 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
27 changes: 27 additions & 0 deletions lib/rouge/formatters/html_linewise.rb
Original file line number Diff line number Diff line change
@@ -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 "<div class=#{next_line_class}>"
line.each do |tok, val|
yield @formatter.span(tok, val)
end
yield '</div>'
end
end

def next_line_class
@lineno ||= 0
sprintf(@class_format, @lineno += 1).inspect
end
end
end
end
16 changes: 16 additions & 0 deletions lib/rouge/formatters/html_pygments.rb
Original file line number Diff line number Diff line change
@@ -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 %<<pre class="#@css_class"><code>>
@inner.stream(tokens, &b)
yield "</code></pre>"
end
end
end
end
Loading

0 comments on commit 444738e

Please sign in to comment.