1 require 'rdoc/markup/formatter'
2 require 'rdoc/markup/fragments'
3 require 'rdoc/markup/inline'
8 # Convert SimpleMarkup to basic LaTeX report format.
10 class RDoc::Markup::ToLaTeX < RDoc::Markup::Formatter
17 BACKSLASH = "#{BS}symbol#{OB}92#{CB}"
18 HAT = "#{BS}symbol#{OB}94#{CB}"
19 BACKQUOTE = "#{BS}symbol#{OB}0#{CB}"
20 TILDE = "#{DL}#{BS}sim#{DL}"
21 LESSTHAN = "#{DL}<#{DL}"
22 GREATERTHAN = "#{DL}>#{DL}"
25 str.tr('\\', BS).tr('{', OB).tr('}', CB).tr('$', DL)
29 RDoc::Markup::ToLaTeX.l(arg)
32 LIST_TYPE_TO_LATEX = {
33 :BULLET => [ l("\\begin{itemize}"), l("\\end{itemize}") ],
34 :NUMBER => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\arabic" ],
35 :UPPERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\Alph" ],
36 :LOWERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\alph" ],
37 :LABELED => [ l("\\begin{description}"), l("\\end{description}") ],
39 l("\\begin{tabularx}{\\linewidth}{@{} l X @{}}"),
40 l("\\end{tabularx}") ],
43 InlineTag = Struct.new(:bit, :on, :off)
52 # Set up the standard mapping of attributes to LaTeX
56 InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), l("\\textbf{"), l("}")),
57 InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), l("\\texttt{"), l("}")),
58 InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), l("\\emph{"), l("}")),
63 # Escape a LaTeX string
66 $stderr.print "FE: ", str if $DEBUG_RDOC
69 gsub(/([_\${}&%#])/, "#{BS}\\1").
70 gsub(/\\/, BACKSLASH).
74 gsub(/>/, GREATERTHAN).
77 $stderr.print "-> ", s, "\n" if $DEBUG_RDOC
82 # Add a new set of LaTeX tags for an attribute. We allow
83 # separate start and end tags for flexibility
85 def add_tag(name, start, stop)
86 @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop)
90 # Here's the client side of the visitor pattern
98 @res.tr(BS, '\\').tr(OB, '{').tr(CB, '}').tr(DL, '$')
101 def accept_paragraph(am, fragment)
102 @res << wrap(convert_flow(am.flow(fragment.txt)))
106 def accept_verbatim(am, fragment)
107 @res << "\n\\begin{code}\n"
108 @res << fragment.txt.sub(/[\n\s]+\Z/, '')
109 @res << "\n\\end{code}\n\n"
112 def accept_rule(am, fragment)
113 size = fragment.param
114 size = 10 if size > 10
115 @res << "\n\n\\rule{\\linewidth}{#{size}pt}\n\n"
118 def accept_list_start(am, fragment)
119 @res << list_name(fragment.type, true) << "\n"
120 @in_list_entry.push false
123 def accept_list_end(am, fragment)
124 if tag = @in_list_entry.pop
127 @res << list_name(fragment.type, false) << "\n"
130 def accept_list_item(am, fragment)
131 if tag = @in_list_entry.last
134 @res << list_item_start(am, fragment)
135 @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n"
136 @in_list_entry[-1] = list_end_for(fragment.type)
139 def accept_blank_line(am, fragment)
143 def accept_heading(am, fragment)
144 @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
148 # This is a higher speed (if messier) version of wrap
150 def wrap(txt, line_len = 76)
155 # scan back for a space
156 p = sp + line_len - 1
160 while p > sp and txt[p] != ?\s
165 while p < ep and txt[p] != ?\s
170 res << txt[sp...p] << "\n"
172 sp += 1 while sp < ep and txt[sp] == ?\s
179 def on_tags(res, item)
180 attr_mask = item.turn_on
181 return if attr_mask.zero?
183 @attr_tags.each do |tag|
184 if attr_mask & tag.bit != 0
190 def off_tags(res, item)
191 attr_mask = item.turn_off
192 return if attr_mask.zero?
194 @attr_tags.reverse_each do |tag|
195 if attr_mask & tag.bit != 0
201 def convert_flow(flow)
206 $stderr.puts "Converting '#{item}'" if $DEBUG_RDOC
207 res << convert_string(item)
212 res << convert_special(item)
214 raise "Unknown flow element: #{item.inspect}"
221 # some of these patterns are taken from SmartyPants...
223 def convert_string(item)
226 # convert ... to elipsis (and make sure .... becomes .<elipsis>)
227 gsub(/\.\.\.\./, '.\ldots{}').gsub(/\.\.\./, '\ldots{}').
229 # convert single closing quote
230 gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1\'').
231 gsub(%r{\'(?=\W|s\b)}, "'" ).
233 # convert single opening quote
236 # convert double closing quote
237 gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}, "\\1''").
239 # convert double opening quote
243 gsub(/\(c\)/, '\copyright{}')
247 def convert_special(special)
249 Attribute.each_name_of(special.type) do |name|
250 method_name = "handle_special_#{name}"
251 if self.respond_to? method_name
252 special.text = send(method_name, special)
256 raise "Unhandled special: #{special}" unless handled
260 def convert_heading(level, flow)
263 when 1 then "\\chapter{"
264 when 2 then "\\section{"
265 when 3 then "\\subsection{"
266 when 4 then "\\subsubsection{"
273 def list_name(list_type, is_open_tag)
274 tags = LIST_TYPE_TO_LATEX[list_type] || raise("Invalid list type: #{list_type.inspect}")
275 if tags[2] # enumerate
278 if @prev_list_types[@list_depth] != tags[2]
289 raise("Too deep list: level #{@list_depth}")
291 @prev_list_types[@list_depth] = tags[2]
292 return l("\\renewcommand{\\labelenum#{roman}}{#{tags[2]}{enum#{roman}}}") + "\n" + tags[0]
298 tags[ is_open_tag ? 0 : 1]
301 def list_item_start(am, fragment)
303 when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then
307 "\\item[" + convert_flow(am.flow(fragment.param)) + "] "
310 convert_flow(am.flow(fragment.param)) + " & "
312 raise "Invalid list type"
316 def list_end_for(fragment_type)
318 when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA, :LABELED then
323 raise "Invalid list type"