Skip to content

Commit

Permalink
implement the tex formatter and TexThemeRenderer
Browse files Browse the repository at this point in the history
  • Loading branch information
http://jneen.net/ committed Jun 13, 2019
1 parent de91c27 commit ec08b52
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 2 deletions.
2 changes: 2 additions & 0 deletions lib/rouge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ def lexer_dir(path = '')
load_relative 'rouge/formatters/html_linewise'
load_relative 'rouge/formatters/html_inline'
load_relative 'rouge/formatters/terminal256'
load_relative 'rouge/formatters/tex'
load_relative 'rouge/formatters/null'

load_relative 'rouge/theme'
load_relative 'rouge/tex_theme_renderer'
load_relative 'rouge/themes/thankful_eyes'
load_relative 'rouge/themes/colorful'
load_relative 'rouge/themes/base16'
Expand Down
21 changes: 19 additions & 2 deletions lib/rouge/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def initialize(opts={})
when 'html-inline' then Formatters::HTMLInline.new(theme)
when 'html-table' then Formatters::HTMLTable.new(Formatters::HTML.new)
when 'null', 'raw', 'tokens' then Formatters::Null.new
when 'tex' then Formatters::Tex.new
else
error! "unknown formatter preset #{opts[:formatter]}"
end
Expand Down Expand Up @@ -334,18 +335,30 @@ def self.doc
yield %|respectively. Theme defaults to thankful_eyes.|
yield %||
yield %|options:|
yield %| --scope (default: .highlight) a css selector to scope by|
yield %| --scope (default: .highlight) a css selector to scope by|
yield %| --tex (default: false) render as TeX|
yield %| --tex-prefix (default: RG) a command prefix for TeX|
yield %| implies --tex if specified|
yield %||
yield %|available themes:|
yield %| #{Theme.registry.keys.sort.join(', ')}|
end

def self.parse(argv)
opts = { :theme_name => 'thankful_eyes' }
opts = {
:theme_name => 'thankful_eyes',
:tex => false,
:tex_prefix => 'RG'
}

until argv.empty?
arg = argv.shift
case arg
when '--tex'
opts[:tex] = true
when '--tex-prefix'
opts[:tex] = true
opts[:tex_prefix] = argv.shift
when /--(\w+)/
opts[$1.tr('-', '_').to_sym] = argv.shift
else
Expand All @@ -362,6 +375,10 @@ def initialize(opts)
or error! "unknown theme: #{theme_name}"

@theme = theme_class.new(opts)
if opts[:tex]
tex_prefix = opts[:tex_prefix]
@theme = TexThemeRenderer.new(@theme, prefix: tex_prefix)
end
end

def run
Expand Down
90 changes: 90 additions & 0 deletions lib/rouge/formatters/tex.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module Rouge
module Formatters
class Tex < Formatter
tag 'tex'

# A map of TeX escape characters.
# Newlines are handled specially by using #token_lines
# spaces are preserved as long as they aren't at the beginning
# of a line. see #tag_first for our initial-space strategy
ESCAPE = {
'&' => '\&',
'%' => '\%',
'$' => '\$',
'#' => '\#',
'_' => '\_',
'{' => '\{',
'}' => '\}',
'~' => '{\textasciitilde}',
'^' => '{\textasciicircum}',
'|' => '{\textbar}',
'\\' => '{\textbackslash}',
"\t" => '{\tab}',
}

ESCAPE_REGEX = /[#{ESCAPE.keys.map(&Regexp.method(:escape)).join}]/om

def initialize(opts={})
@prefix = opts.fetch(:prefix) { 'RG' }
end

def escape_tex(str)
str.gsub(ESCAPE_REGEX, ESCAPE)
end

def stream(tokens, &b)
# surround the output with \begin{RG*}...\end{RG*}
yield "\\begin{#{@prefix}*}%\n"

# we strip the newline off the last line to avoid
# an extra line being rendered. we do this by yielding
# the \newline tag *before* every line group except
# the first.
first = true

token_lines tokens do |line|
if first
first = false
else
yield "\\newline%\n"
end

render_line(line, &b)
end

yield "%\n\\end{#{@prefix}*}%\n"
end

def render_line(line, &b)
head, *rest = line
return unless head

tag_first(*head, &b)
rest.each do |(tok, val)|
yield tag(tok, val)
end
end

# special handling for the first token
# of a line. we replace all initial spaces
# with \phantom{xxxx}, which renders an
# empty space equal to the size of the x's.
def tag_first(tok, val)
leading = nil
val.sub!(/^[ ]+/) { leading = $&.size; '' }
yield "\\phantom{#{'x' * leading}}" if leading
yield tag(tok, val)
end

def tag(tok, val)
if escape?(tok)
val
elsif tok == Token::Tokens::Text
escape_tex(val)
else
"\\#@prefix{#{tok.shortname}}{#{escape_tex(val)}}"
end
end
end
end
end
128 changes: 128 additions & 0 deletions lib/rouge/tex_theme_renderer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
module Rouge
class TexThemeRenderer
def initialize(theme, opts={})
@theme = theme
@prefix = opts.fetch(:prefix) { 'RG' }
end

# Our general strategy is this:
#
# * First, define the \RG{tokname}{content} command, which will
# expand into \RG@tok@tokname{content}. We use \csname...\endcsname
# to interpolate into a command.
#
# * Define the default RG* environment, which will enclose the whole
# thing. By default this will simply set \ttfamily (select monospace font)
# but it can be overridden with \renewcommand by the user to be
# any other formatting.
#
# * Define all the colors using xcolors \definecolor command. First we define
# every palette color with a name such as rouge@palette@themneame@colorname.
# Then we find all foreground and background colors that have literal html
# colors embedded in them and define them with names such as
# rouge@palette@themename@000000. While html allows three-letter colors such
# as #FFF, xcolor requires all six characters to be present, so we make sure
# to normalize that as well as the case convention in #inline_name.
#
# * Define the token commands RG@tok@xx. These will take the content as the
# argument and format it according to the theme, referring to the color
# in the palette.
def render(&b)
yield <<'END'.gsub('RG', @prefix)
\makeatletter
\def\RG#1#2{\csname RG@tok@#1\endcsname{#2}}%
\newenvironment{RG*}{\ttfamily}{\relax}%
END

base = @theme.class.base_style
yield "\\definecolor{#{@prefix}@fgcolor}{HTML}{#{inline_name(base.fg)}}"
yield "\\definecolor{#{@prefix}@bgcolor}{HTML}{#{inline_name(base.bg)}}"

render_palette(@theme.palette, &b)

@theme.styles.each do |tok, style|
render_inline_pallete(style, &b)
end

Token.each_token do |tok|
style = @theme.class.get_own_style(tok)
style ? render_style(tok, style, &b) : render_blank(tok, &b)
end
yield '\makeatother'
end

def render_palette(palette, &b)
palette.each do |name, color|
hex = inline_name(color)

yield "\\definecolor{#{palette_name(name)}}{HTML}{#{hex}}%"
end
end

def render_inline_pallete(style, &b)
gen_inline(style[:fg], &b)
gen_inline(style[:bg], &b)
end

def inline_name(color)
color =~ /^#(\h+)/ or return nil

# xcolor does not support 3-character HTML colors,
# so we convert them here
case $1.size
when 6
$1
when 3
# duplicate every character: abc -> aabbcc
$1.gsub(/\h/, '\0\0')
else
raise "invalid HTML color: #{$1}"
end.upcase
end

def gen_inline(name, &b)
# detect inline colors
hex = inline_name(name)
return unless hex

@gen_inline ||= {}
@gen_inline[hex] ||= begin
yield "\\definecolor{#{palette_name(hex)}}{HTML}{#{hex}}%"
end
end

def camelize(name)
name.gsub(/_(.)/) { $1.upcase }
end

def palette_name(name)
name = inline_name(name) || name.to_s

"rouge@palette@#{camelize(@theme.name)}@#{camelize(name.to_s)}"
end

def token_name(tok)
"\\csname #@prefix@tok@#{tok.shortname}\\endcsname"
end

def render_blank(tok, &b)
out = "\\expandafter\\def#{token_name(tok)}#1{#1}"
end

def render_style(tok, style, &b)
out = "\\expandafter\\def#{token_name(tok)}#1{"
out << "\\fboxsep=0pt\\colorbox{#{palette_name(style[:bg])}}{" if style[:bg]
out << '\\textbf{' if style[:bold]
out << '\\textit{' if style[:italic]
out << "\\textcolor{#{palette_name(style[:fg])}}{" if style[:fg]
out << "#1"
# close the right number of curlies
out << "}" if style[:bold]
out << "}" if style[:italic]
out << "}" if style[:fg]
out << "}" if style[:bg]
out << "}%"
yield out
end
end
end
4 changes: 4 additions & 0 deletions lib/rouge/theme.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ def get_style(token)
self.class.get_style(token)
end

def name
self.class.name
end

class << self
def style(*tokens)
style = tokens.last.is_a?(Hash) ? tokens.pop : {}
Expand Down
62 changes: 62 additions & 0 deletions spec/formatters/tex_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*- #
# frozen_string_literal: true

describe Rouge::Formatters::Tex do
let(:subject) { Rouge::Formatters::Tex.new }
let(:options) { {} }
let(:tokens) { [] }
let(:output) { subject.format(tokens) }
let(:expected) { '' }

describe 'a basic example' do
let(:tokens) { [[Token['Name'], 'foo']] }
let(:options) { { :wrap => false } }

let(:expected) do
<<-'OUT'
\begin{RG*}%
\RG{n}{foo}%
\end{RG*}%
OUT
end

it 'renders' do
assert { output == expected }
end
end

describe "escaping strategies" do
# some fake code that might look like:
#
# foo {
# ~100%
# }
#
# we must escape the braces, the percent sign, the tilde,
# and the initial space on the second line.
let(:tokens) do
[[Token['Keyword'], 'foo'],
[Token['Text'], ' '],
[Token['Punctuation'], '{'],
[Token['Text'], "\n "],
[Token['Name.Constant'], '~100%'],
[Token['Text'], "\n"],
[Token['Punctuation'], '}'],
[Token['Text'], "\n"]]
end

let(:expected) do
<<-'OUT'
\begin{RG*}%
\RG{k}{foo} \RG{p}{\{}\newline%
\phantom{xx}\RG{no}{{\textasciitilde}100\%}\newline%
\RG{p}{\}}%
\end{RG*}%
OUT
end

it 'renders' do
assert { output == expected }
end
end
end

0 comments on commit ec08b52

Please sign in to comment.