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(//, 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 .) 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