ruby--ruby/lib/rdoc/markup/to_latex.rb

329 lines
7.6 KiB
Ruby

require 'rdoc/markup/formatter'
require 'rdoc/markup/fragments'
require 'rdoc/markup/inline'
require 'cgi'
##
# Convert SimpleMarkup to basic LaTeX report format.
class RDoc::Markup::ToLaTeX < RDoc::Markup::Formatter
BS = "\020" # \
OB = "\021" # {
CB = "\022" # }
DL = "\023" # Dollar
BACKSLASH = "#{BS}symbol#{OB}92#{CB}"
HAT = "#{BS}symbol#{OB}94#{CB}"
BACKQUOTE = "#{BS}symbol#{OB}0#{CB}"
TILDE = "#{DL}#{BS}sim#{DL}"
LESSTHAN = "#{DL}<#{DL}"
GREATERTHAN = "#{DL}>#{DL}"
def self.l(str)
str.tr('\\', BS).tr('{', OB).tr('}', CB).tr('$', DL)
end
def l(arg)
RDoc::Markup::ToLaTeX.l(arg)
end
LIST_TYPE_TO_LATEX = {
:BULLET => [ l("\\begin{itemize}"), l("\\end{itemize}") ],
:NUMBER => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\arabic" ],
:UPPERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\Alph" ],
:LOWERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\alph" ],
:LABELED => [ l("\\begin{description}"), l("\\end{description}") ],
:NOTE => [
l("\\begin{tabularx}{\\linewidth}{@{} l X @{}}"),
l("\\end{tabularx}") ],
}
InlineTag = Struct.new(:bit, :on, :off)
def initialize
init_tags
@list_depth = 0
@prev_list_types = []
end
##
# Set up the standard mapping of attributes to LaTeX
def init_tags
@attr_tags = [
InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), l("\\textbf{"), l("}")),
InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), l("\\texttt{"), l("}")),
InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), l("\\emph{"), l("}")),
]
end
##
# Escape a LaTeX string
def escape(str)
$stderr.print "FE: ", str if $DEBUG_RDOC
s = str.
sub(/\s+$/, '').
gsub(/([_\${}&%#])/, "#{BS}\\1").
gsub(/\\/, BACKSLASH).
gsub(/\^/, HAT).
gsub(/~/, TILDE).
gsub(/</, LESSTHAN).
gsub(/>/, GREATERTHAN).
gsub(/,,/, ",{},").
gsub(/\`/, BACKQUOTE)
$stderr.print "-> ", s, "\n" if $DEBUG_RDOC
s
end
##
# Add a new set of LaTeX tags for an attribute. We allow
# separate start and end tags for flexibility
def add_tag(name, start, stop)
@attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop)
end
##
# Here's the client side of the visitor pattern
def start_accepting
@res = ""
@in_list_entry = []
end
def end_accepting
@res.tr(BS, '\\').tr(OB, '{').tr(CB, '}').tr(DL, '$')
end
def accept_paragraph(am, fragment)
@res << wrap(convert_flow(am.flow(fragment.txt)))
@res << "\n"
end
def accept_verbatim(am, fragment)
@res << "\n\\begin{code}\n"
@res << fragment.txt.sub(/[\n\s]+\Z/, '')
@res << "\n\\end{code}\n\n"
end
def accept_rule(am, fragment)
size = fragment.param
size = 10 if size > 10
@res << "\n\n\\rule{\\linewidth}{#{size}pt}\n\n"
end
def accept_list_start(am, fragment)
@res << list_name(fragment.type, true) << "\n"
@in_list_entry.push false
end
def accept_list_end(am, fragment)
if tag = @in_list_entry.pop
@res << tag << "\n"
end
@res << list_name(fragment.type, false) << "\n"
end
def accept_list_item(am, fragment)
if tag = @in_list_entry.last
@res << tag << "\n"
end
@res << list_item_start(am, fragment)
@res << wrap(convert_flow(am.flow(fragment.txt))) << "\n"
@in_list_entry[-1] = list_end_for(fragment.type)
end
def accept_blank_line(am, fragment)
# @res << "\n"
end
def accept_heading(am, fragment)
@res << convert_heading(fragment.head_level, am.flow(fragment.txt))
end
##
# This is a higher speed (if messier) version of wrap
def wrap(txt, line_len = 76)
res = ""
sp = 0
ep = txt.length
while sp < ep
# scan back for a space
p = sp + line_len - 1
if p >= ep
p = ep
else
while p > sp and txt[p] != ?\s
p -= 1
end
if p <= sp
p = sp + line_len
while p < ep and txt[p] != ?\s
p += 1
end
end
end
res << txt[sp...p] << "\n"
sp = p
sp += 1 while sp < ep and txt[sp] == ?\s
end
res
end
private
def on_tags(res, item)
attr_mask = item.turn_on
return if attr_mask.zero?
@attr_tags.each do |tag|
if attr_mask & tag.bit != 0
res << tag.on
end
end
end
def off_tags(res, item)
attr_mask = item.turn_off
return if attr_mask.zero?
@attr_tags.reverse_each do |tag|
if attr_mask & tag.bit != 0
res << tag.off
end
end
end
def convert_flow(flow)
res = ""
flow.each do |item|
case item
when String
$stderr.puts "Converting '#{item}'" if $DEBUG_RDOC
res << convert_string(item)
when AttrChanger
off_tags(res, item)
on_tags(res, item)
when Special
res << convert_special(item)
else
raise "Unknown flow element: #{item.inspect}"
end
end
res
end
##
# some of these patterns are taken from SmartyPants...
def convert_string(item)
escape(item).
# convert ... to elipsis (and make sure .... becomes .<elipsis>)
gsub(/\.\.\.\./, '.\ldots{}').gsub(/\.\.\./, '\ldots{}').
# convert single closing quote
gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1\'').
gsub(%r{\'(?=\W|s\b)}, "'" ).
# convert single opening quote
gsub(/'/, '`').
# convert double closing quote
gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}, "\\1''").
# convert double opening quote
gsub(/"/, "``").
# convert copyright
gsub(/\(c\)/, '\copyright{}')
end
def convert_special(special)
handled = false
Attribute.each_name_of(special.type) do |name|
method_name = "handle_special_#{name}"
if self.respond_to? method_name
special.text = send(method_name, special)
handled = true
end
end
raise "Unhandled special: #{special}" unless handled
special.text
end
def convert_heading(level, flow)
res =
case level
when 1 then "\\chapter{"
when 2 then "\\section{"
when 3 then "\\subsection{"
when 4 then "\\subsubsection{"
else "\\paragraph{"
end +
convert_flow(flow) +
"}\n"
end
def list_name(list_type, is_open_tag)
tags = LIST_TYPE_TO_LATEX[list_type] || raise("Invalid list type: #{list_type.inspect}")
if tags[2] # enumerate
if is_open_tag
@list_depth += 1
if @prev_list_types[@list_depth] != tags[2]
case @list_depth
when 1
roman = "i"
when 2
roman = "ii"
when 3
roman = "iii"
when 4
roman = "iv"
else
raise("Too deep list: level #{@list_depth}")
end
@prev_list_types[@list_depth] = tags[2]
return l("\\renewcommand{\\labelenum#{roman}}{#{tags[2]}{enum#{roman}}}") + "\n" + tags[0]
end
else
@list_depth -= 1
end
end
tags[ is_open_tag ? 0 : 1]
end
def list_item_start(am, fragment)
case fragment.type
when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then
"\\item "
when :LABELED then
"\\item[" + convert_flow(am.flow(fragment.param)) + "] "
when :NOTE then
convert_flow(am.flow(fragment.param)) + " & "
else
raise "Invalid list type"
end
end
def list_end_for(fragment_type)
case fragment_type
when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA, :LABELED then
""
when :NOTE
"\\\\\n"
else
raise "Invalid list type"
end
end
end