[18425] | 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
|
---|