Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / rdoc / markup / to_latex.rb
blobbbf958f2edc83e4f92cfce0db013ba3eb5f35206
1 require 'rdoc/markup/formatter'
2 require 'rdoc/markup/fragments'
3 require 'rdoc/markup/inline'
5 require 'cgi'
7 ##
8 # Convert SimpleMarkup to basic LaTeX report format.
10 class RDoc::Markup::ToLaTeX < RDoc::Markup::Formatter
12   BS = "\020"   # \
13   OB = "\021"   # {
14   CB = "\022"   # }
15   DL = "\023"   # Dollar
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}"
24   def self.l(str)
25     str.tr('\\', BS).tr('{', OB).tr('}', CB).tr('$', DL)
26   end
28   def l(arg)
29     RDoc::Markup::ToLaTeX.l(arg)
30   end
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}") ],
38     :NOTE    => [
39       l("\\begin{tabularx}{\\linewidth}{@{} l X @{}}"),
40       l("\\end{tabularx}") ],
41   }
43   InlineTag = Struct.new(:bit, :on, :off)
45   def initialize
46     init_tags
47     @list_depth = 0
48     @prev_list_types = []
49   end
51   ##
52   # Set up the standard mapping of attributes to LaTeX
54   def init_tags
55     @attr_tags = [
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("}")),
59     ]
60   end
62   ##
63   # Escape a LaTeX string
65   def escape(str)
66     $stderr.print "FE: ", str if $DEBUG_RDOC
67     s = str.
68        sub(/\s+$/, '').
69       gsub(/([_\${}&%#])/, "#{BS}\\1").
70       gsub(/\\/, BACKSLASH).
71       gsub(/\^/, HAT).
72       gsub(/~/,  TILDE).
73       gsub(/</,  LESSTHAN).
74       gsub(/>/,  GREATERTHAN).
75       gsub(/,,/, ",{},").
76       gsub(/\`/,  BACKQUOTE)
77     $stderr.print "-> ", s, "\n" if $DEBUG_RDOC
78     s
79   end
81   ##
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)
87   end
89   ##
90   # Here's the client side of the visitor pattern
92   def start_accepting
93     @res = ""
94     @in_list_entry = []
95   end
97   def end_accepting
98     @res.tr(BS, '\\').tr(OB, '{').tr(CB, '}').tr(DL, '$')
99   end
101   def accept_paragraph(am, fragment)
102     @res << wrap(convert_flow(am.flow(fragment.txt)))
103     @res << "\n"
104   end
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"
110   end
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"
116   end
118   def accept_list_start(am, fragment)
119     @res << list_name(fragment.type, true) << "\n"
120     @in_list_entry.push false
121   end
123   def accept_list_end(am, fragment)
124     if tag = @in_list_entry.pop
125       @res << tag << "\n"
126     end
127     @res << list_name(fragment.type, false) << "\n"
128   end
130   def accept_list_item(am, fragment)
131     if tag = @in_list_entry.last
132       @res << tag << "\n"
133     end
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)
137   end
139   def accept_blank_line(am, fragment)
140     # @res << "\n"
141   end
143   def accept_heading(am, fragment)
144     @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
145   end
147   ##
148   # This is a higher speed (if messier) version of wrap
150   def wrap(txt, line_len = 76)
151     res = ""
152     sp = 0
153     ep = txt.length
154     while sp < ep
155       # scan back for a space
156       p = sp + line_len - 1
157       if p >= ep
158         p = ep
159       else
160         while p > sp and txt[p] != ?\s
161           p -= 1
162         end
163         if p <= sp
164           p = sp + line_len
165           while p < ep and txt[p] != ?\s
166             p += 1
167           end
168         end
169       end
170       res << txt[sp...p] << "\n"
171       sp = p
172       sp += 1 while sp < ep and txt[sp] == ?\s
173     end
174     res
175   end
177   private
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
185         res << tag.on
186       end
187     end
188   end
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
196         res << tag.off
197       end
198     end
199   end
201   def convert_flow(flow)
202     res = ""
203     flow.each do |item|
204       case item
205       when String
206         $stderr.puts "Converting '#{item}'" if $DEBUG_RDOC
207         res << convert_string(item)
208       when AttrChanger
209         off_tags(res, item)
210         on_tags(res,  item)
211       when Special
212         res << convert_special(item)
213       else
214         raise "Unknown flow element: #{item.inspect}"
215       end
216     end
217     res
218   end
220   ##
221   # some of these patterns are taken from SmartyPants...
223   def convert_string(item)
224     escape(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
234       gsub(/'/, '`').
236     # convert double closing quote
237       gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}, "\\1''").
239     # convert double opening quote
240       gsub(/"/, "``").
242     # convert copyright
243       gsub(/\(c\)/, '\copyright{}')
245   end
247   def convert_special(special)
248     handled = false
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)
253         handled = true
254       end
255     end
256     raise "Unhandled special: #{special}" unless handled
257     special.text
258   end
260   def convert_heading(level, flow)
261     res =
262       case level
263       when 1 then "\\chapter{"
264       when 2 then "\\section{"
265       when 3 then "\\subsection{"
266       when 4 then "\\subsubsection{"
267       else  "\\paragraph{"
268       end +
269       convert_flow(flow) +
270       "}\n"
271   end
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
276       if is_open_tag
277         @list_depth += 1
278         if @prev_list_types[@list_depth] != tags[2]
279           case @list_depth
280           when 1
281             roman = "i"
282           when 2
283             roman = "ii"
284           when 3
285             roman = "iii"
286           when 4
287             roman = "iv"
288           else
289             raise("Too deep list: level #{@list_depth}")
290           end
291           @prev_list_types[@list_depth] = tags[2]
292           return l("\\renewcommand{\\labelenum#{roman}}{#{tags[2]}{enum#{roman}}}") + "\n" + tags[0]
293         end
294       else
295         @list_depth -= 1
296       end
297     end
298     tags[ is_open_tag ? 0 : 1]
299   end
301   def list_item_start(am, fragment)
302     case fragment.type
303     when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then
304       "\\item "
306     when :LABELED then
307       "\\item[" + convert_flow(am.flow(fragment.param)) + "] "
309     when :NOTE then
310         convert_flow(am.flow(fragment.param)) + " & "
311     else
312       raise "Invalid list type"
313     end
314   end
316   def list_end_for(fragment_type)
317     case fragment_type
318     when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA, :LABELED then
319       ""
320     when :NOTE
321       "\\\\\n"
322     else
323       raise "Invalid list type"
324     end
325   end