1 | require 'rdoc/markup/simple_markup/fragments'
|
---|
2 | require 'rdoc/markup/simple_markup/inline'
|
---|
3 |
|
---|
4 | require 'cgi'
|
---|
5 |
|
---|
6 | module SM
|
---|
7 |
|
---|
8 | # Convert SimpleMarkup to basic LaTeX report format
|
---|
9 |
|
---|
10 | class ToLaTeX
|
---|
11 |
|
---|
12 | BS = "\020" # \
|
---|
13 | OB = "\021" # {
|
---|
14 | CB = "\022" # }
|
---|
15 | DL = "\023" # Dollar
|
---|
16 |
|
---|
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}"
|
---|
23 |
|
---|
24 | def self.l(str)
|
---|
25 | str.tr('\\', BS).tr('{', OB).tr('}', CB).tr('$', DL)
|
---|
26 | end
|
---|
27 |
|
---|
28 | def l(arg)
|
---|
29 | SM::ToLaTeX.l(arg)
|
---|
30 | end
|
---|
31 |
|
---|
32 | LIST_TYPE_TO_LATEX = {
|
---|
33 | ListBase::BULLET => [ l("\\begin{itemize}"), l("\\end{itemize}") ],
|
---|
34 | ListBase::NUMBER => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\arabic" ],
|
---|
35 | ListBase::UPPERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\Alph" ],
|
---|
36 | ListBase::LOWERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\alph" ],
|
---|
37 | ListBase::LABELED => [ l("\\begin{description}"), l("\\end{description}") ],
|
---|
38 | ListBase::NOTE => [
|
---|
39 | l("\\begin{tabularx}{\\linewidth}{@{} l X @{}}"),
|
---|
40 | l("\\end{tabularx}") ],
|
---|
41 | }
|
---|
42 |
|
---|
43 | InlineTag = Struct.new(:bit, :on, :off)
|
---|
44 |
|
---|
45 | def initialize
|
---|
46 | init_tags
|
---|
47 | @list_depth = 0
|
---|
48 | @prev_list_types = []
|
---|
49 | end
|
---|
50 |
|
---|
51 | ##
|
---|
52 | # Set up the standard mapping of attributes to LaTeX
|
---|
53 | #
|
---|
54 | def init_tags
|
---|
55 | @attr_tags = [
|
---|
56 | InlineTag.new(SM::Attribute.bitmap_for(:BOLD), l("\\textbf{"), l("}")),
|
---|
57 | InlineTag.new(SM::Attribute.bitmap_for(:TT), l("\\texttt{"), l("}")),
|
---|
58 | InlineTag.new(SM::Attribute.bitmap_for(:EM), l("\\emph{"), l("}")),
|
---|
59 | ]
|
---|
60 | end
|
---|
61 |
|
---|
62 | ##
|
---|
63 | # Escape a LaTeX string
|
---|
64 | def escape(str)
|
---|
65 | # $stderr.print "FE: ", str
|
---|
66 | s = str.
|
---|
67 | # sub(/\s+$/, '').
|
---|
68 | gsub(/([_\${}&%#])/, "#{BS}\\1").
|
---|
69 | gsub(/\\/, BACKSLASH).
|
---|
70 | gsub(/\^/, HAT).
|
---|
71 | gsub(/~/, TILDE).
|
---|
72 | gsub(/</, LESSTHAN).
|
---|
73 | gsub(/>/, GREATERTHAN).
|
---|
74 | gsub(/,,/, ",{},").
|
---|
75 | gsub(/\`/, BACKQUOTE)
|
---|
76 | # $stderr.print "-> ", s, "\n"
|
---|
77 | s
|
---|
78 | end
|
---|
79 |
|
---|
80 | ##
|
---|
81 | # Add a new set of LaTeX tags for an attribute. We allow
|
---|
82 | # separate start and end tags for flexibility
|
---|
83 | #
|
---|
84 | def add_tag(name, start, stop)
|
---|
85 | @attr_tags << InlineTag.new(SM::Attribute.bitmap_for(name), start, stop)
|
---|
86 | end
|
---|
87 |
|
---|
88 |
|
---|
89 | ##
|
---|
90 | # Here's the client side of the visitor pattern
|
---|
91 |
|
---|
92 | def start_accepting
|
---|
93 | @res = ""
|
---|
94 | @in_list_entry = []
|
---|
95 | end
|
---|
96 |
|
---|
97 | def end_accepting
|
---|
98 | @res.tr(BS, '\\').tr(OB, '{').tr(CB, '}').tr(DL, '$')
|
---|
99 | end
|
---|
100 |
|
---|
101 | def accept_paragraph(am, fragment)
|
---|
102 | @res << wrap(convert_flow(am.flow(fragment.txt)))
|
---|
103 | @res << "\n"
|
---|
104 | end
|
---|
105 |
|
---|
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
|
---|
111 |
|
---|
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
|
---|
117 |
|
---|
118 | def accept_list_start(am, fragment)
|
---|
119 | @res << list_name(fragment.type, true) <<"\n"
|
---|
120 | @in_list_entry.push false
|
---|
121 | end
|
---|
122 |
|
---|
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
|
---|
129 |
|
---|
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
|
---|
138 |
|
---|
139 | def accept_blank_line(am, fragment)
|
---|
140 | # @res << "\n"
|
---|
141 | end
|
---|
142 |
|
---|
143 | def accept_heading(am, fragment)
|
---|
144 | @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
|
---|
145 | end
|
---|
146 |
|
---|
147 | # This is a higher speed (if messier) version of wrap
|
---|
148 |
|
---|
149 | def wrap(txt, line_len = 76)
|
---|
150 | res = ""
|
---|
151 | sp = 0
|
---|
152 | ep = txt.length
|
---|
153 | while sp < ep
|
---|
154 | # scan back for a space
|
---|
155 | p = sp + line_len - 1
|
---|
156 | if p >= ep
|
---|
157 | p = ep
|
---|
158 | else
|
---|
159 | while p > sp and txt[p] != ?\s
|
---|
160 | p -= 1
|
---|
161 | end
|
---|
162 | if p <= sp
|
---|
163 | p = sp + line_len
|
---|
164 | while p < ep and txt[p] != ?\s
|
---|
165 | p += 1
|
---|
166 | end
|
---|
167 | end
|
---|
168 | end
|
---|
169 | res << txt[sp...p] << "\n"
|
---|
170 | sp = p
|
---|
171 | sp += 1 while sp < ep and txt[sp] == ?\s
|
---|
172 | end
|
---|
173 | res
|
---|
174 | end
|
---|
175 |
|
---|
176 | #######################################################################
|
---|
177 |
|
---|
178 | private
|
---|
179 |
|
---|
180 | #######################################################################
|
---|
181 |
|
---|
182 | def on_tags(res, item)
|
---|
183 | attr_mask = item.turn_on
|
---|
184 | return if attr_mask.zero?
|
---|
185 |
|
---|
186 | @attr_tags.each do |tag|
|
---|
187 | if attr_mask & tag.bit != 0
|
---|
188 | res << tag.on
|
---|
189 | end
|
---|
190 | end
|
---|
191 | end
|
---|
192 |
|
---|
193 | def off_tags(res, item)
|
---|
194 | attr_mask = item.turn_off
|
---|
195 | return if attr_mask.zero?
|
---|
196 |
|
---|
197 | @attr_tags.reverse_each do |tag|
|
---|
198 | if attr_mask & tag.bit != 0
|
---|
199 | res << tag.off
|
---|
200 | end
|
---|
201 | end
|
---|
202 | end
|
---|
203 |
|
---|
204 | def convert_flow(flow)
|
---|
205 | res = ""
|
---|
206 | flow.each do |item|
|
---|
207 | case item
|
---|
208 | when String
|
---|
209 | # $stderr.puts "Converting '#{item}'"
|
---|
210 | res << convert_string(item)
|
---|
211 | when AttrChanger
|
---|
212 | off_tags(res, item)
|
---|
213 | on_tags(res, item)
|
---|
214 | when Special
|
---|
215 | res << convert_special(item)
|
---|
216 | else
|
---|
217 | raise "Unknown flow element: #{item.inspect}"
|
---|
218 | end
|
---|
219 | end
|
---|
220 | res
|
---|
221 | end
|
---|
222 |
|
---|
223 | # some of these patterns are taken from SmartyPants...
|
---|
224 |
|
---|
225 | def convert_string(item)
|
---|
226 |
|
---|
227 | escape(item).
|
---|
228 |
|
---|
229 |
|
---|
230 | # convert ... to elipsis (and make sure .... becomes .<elipsis>)
|
---|
231 | gsub(/\.\.\.\./, '.\ldots{}').gsub(/\.\.\./, '\ldots{}').
|
---|
232 |
|
---|
233 | # convert single closing quote
|
---|
234 | gsub(%r{([^ \t\r\n\[\{\(])\'}) { "#$1'" }.
|
---|
235 | gsub(%r{\'(?=\W|s\b)}) { "'" }.
|
---|
236 |
|
---|
237 | # convert single opening quote
|
---|
238 | gsub(/'/, '`').
|
---|
239 |
|
---|
240 | # convert double closing quote
|
---|
241 | gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}) { "#$1''" }.
|
---|
242 |
|
---|
243 | # convert double opening quote
|
---|
244 | gsub(/"/, "``").
|
---|
245 |
|
---|
246 | # convert copyright
|
---|
247 | gsub(/\(c\)/, '\copyright{}')
|
---|
248 |
|
---|
249 | end
|
---|
250 |
|
---|
251 | def convert_special(special)
|
---|
252 | handled = false
|
---|
253 | Attribute.each_name_of(special.type) do |name|
|
---|
254 | method_name = "handle_special_#{name}"
|
---|
255 | if self.respond_to? method_name
|
---|
256 | special.text = send(method_name, special)
|
---|
257 | handled = true
|
---|
258 | end
|
---|
259 | end
|
---|
260 | raise "Unhandled special: #{special}" unless handled
|
---|
261 | special.text
|
---|
262 | end
|
---|
263 |
|
---|
264 | def convert_heading(level, flow)
|
---|
265 | res =
|
---|
266 | case level
|
---|
267 | when 1 then "\\chapter{"
|
---|
268 | when 2 then "\\section{"
|
---|
269 | when 3 then "\\subsection{"
|
---|
270 | when 4 then "\\subsubsection{"
|
---|
271 | else "\\paragraph{"
|
---|
272 | end +
|
---|
273 | convert_flow(flow) +
|
---|
274 | "}\n"
|
---|
275 | end
|
---|
276 |
|
---|
277 | def list_name(list_type, is_open_tag)
|
---|
278 | tags = LIST_TYPE_TO_LATEX[list_type] || raise("Invalid list type: #{list_type.inspect}")
|
---|
279 | if tags[2] # enumerate
|
---|
280 | if is_open_tag
|
---|
281 | @list_depth += 1
|
---|
282 | if @prev_list_types[@list_depth] != tags[2]
|
---|
283 | case @list_depth
|
---|
284 | when 1
|
---|
285 | roman = "i"
|
---|
286 | when 2
|
---|
287 | roman = "ii"
|
---|
288 | when 3
|
---|
289 | roman = "iii"
|
---|
290 | when 4
|
---|
291 | roman = "iv"
|
---|
292 | else
|
---|
293 | raise("Too deep list: level #{@list_depth}")
|
---|
294 | end
|
---|
295 | @prev_list_types[@list_depth] = tags[2]
|
---|
296 | return l("\\renewcommand{\\labelenum#{roman}}{#{tags[2]}{enum#{roman}}}") + "\n" + tags[0]
|
---|
297 | end
|
---|
298 | else
|
---|
299 | @list_depth -= 1
|
---|
300 | end
|
---|
301 | end
|
---|
302 | tags[ is_open_tag ? 0 : 1]
|
---|
303 | end
|
---|
304 |
|
---|
305 | def list_item_start(am, fragment)
|
---|
306 | case fragment.type
|
---|
307 | when ListBase::BULLET, ListBase::NUMBER, ListBase::UPPERALPHA, ListBase::LOWERALPHA
|
---|
308 | "\\item "
|
---|
309 |
|
---|
310 | when ListBase::LABELED
|
---|
311 | "\\item[" + convert_flow(am.flow(fragment.param)) + "] "
|
---|
312 |
|
---|
313 | when ListBase::NOTE
|
---|
314 | convert_flow(am.flow(fragment.param)) + " & "
|
---|
315 | else
|
---|
316 | raise "Invalid list type"
|
---|
317 | end
|
---|
318 | end
|
---|
319 |
|
---|
320 | def list_end_for(fragment_type)
|
---|
321 | case fragment_type
|
---|
322 | when ListBase::BULLET, ListBase::NUMBER, ListBase::UPPERALPHA, ListBase::LOWERALPHA, ListBase::LABELED
|
---|
323 | ""
|
---|
324 | when ListBase::NOTE
|
---|
325 | "\\\\\n"
|
---|
326 | else
|
---|
327 | raise "Invalid list type"
|
---|
328 | end
|
---|
329 | end
|
---|
330 |
|
---|
331 | end
|
---|
332 |
|
---|
333 | end
|
---|