Edgewall Software

Ticket #2296: wikilatex.2.py

File wikilatex.2.py, 19.6 KB (added by Trent Apted <tapted@…>, 6 years ago)

/trunk/trac/wiki/wikilatex.py LaTeX formatter (wiki export)

Line 
1import re
2import os
3import urllib
4
5try:
6    from cStringIO import StringIO
7except ImportError:
8    from StringIO import StringIO
9
10from trac import util
11from trac.mimeview import *
12from trac.wiki.api import WikiSystem
13from trac.wiki.formatter import Formatter
14from trac.util import shorten_line, to_unicode
15
16__all__ = ['wiki_to_latex']
17
18def pretexfilter(s, boolparam=True):
19    """
20    Escape LaTeX seqeuences in \a s that do not affect Wiki formatting
21    """
22    s = s.replace('\\', '\textbackslash ') #<lbrace /><rbrace /> ??
23    s = s.replace('{', '\\{{<rbrace />').replace('}', '\\<rbrace />{<rbrace />').replace('<rbrace />', '}')
24    s = s.replace('$', '\\${}')
25    s = s.replace('&gt;', '\textgreater{}').replace('&lt;', '\textless{}').replace('&amp;', '\myamp{}')
26    s = s.replace('&', '\\&{}').replace('%', '\\mypercent{}') #etc
27    return s
28
29def gtotex(s, boolparam=True):
30    """
31    Escape LaTeX seqeuences in \a s that may still be present after processing.
32    Favour pretexfilter() because the LaTeX formatting added _during_ processing need to be kept
33    """
34    if not s:
35        return s
36    s = re.sub(r'"([^,. ])', r"``\1", s).replace('"', "''") #tex-ify quote character
37    s = s.replace('<', '\\textless{}').replace('>', '\\textgreater{}').replace('%', '\\mypercent{}') #should NEVER be any of these
38    s = s.replace('&gt;', '\textgreater{}').replace('&lt;', '\textless{}')
39    s = re.sub(r'^#', r'\\#{}', s)
40    s = re.sub(r'([^\\$])#', r'\1\\#{}', s) #replace hashes, if not already preceded by a backslash or $
41    s = re.sub(r'^_', r'\\_{}', s)
42    s = re.sub(r'([^\\$])_', r'\1\\_{}', s) #replace underscores, if not already preceded by a backslash or $
43    s = re.sub(r'^\^', '\\\\textasciicircum{}', s)
44    s = re.sub(r'([^$])^', r'\1\\\\textasciicircum{}', s)
45    return to_unicode(s)
46
47def gfilter_to_url(s, boolparam=True):
48    """
49    Filter out things escaped by pretexfilter that \url is able to cope with
50    """
51    if not s:
52        return s
53    s = s.replace('\\_{}', '_').replace('\\%{}', '%').replace('\\&{}amp;', '&')
54    return s
55
56def LatexURL(formatter, ns, target, text, boolparam=True):
57    """
58    Create a LaTeX \url with a given namespace, target and link text
59    """
60    if text == target:
61        return '\\url{' + (ns and ns + ':' or "") + gfilter_to_url(target) + '}';
62    return '\\anchortext{' + (gtotex(text) or gtotex(target) or "broken") + '}\\footnote{\\url{' + (ns and ns + ':' or "") + gfilter_to_url(target) + '}}';
63
64def WikiLatexURL(formatter, ns, target, text, boolparam=True):
65    """
66    Create a LaTeX \url with a given namespace, target and link text
67    """
68    if text == target:
69        return '\\anchortext{' + gtotex(target) + '} (\\S\\ref{sub:' + totexlabel(target) + '})'
70    return '\\anchortext{' + (gtotex(text) or gtotex(target) or "broken") + '} (\\S\\ref{sub:' + totexlabel(target) + '})\\footnote{\\url{' + (ns and ns + ':' or "") + gfilter_to_url(target) + '}}';
71
72def totexlabel(s, boolparam=True):
73    """
74    Create a valid LaTeX for \ref cross references
75    """
76    if not s:
77        return s
78    return s.replace('&', '').replace('$', '').replace(' ', '-').replace('"', '').replace("'", '').replace('#', ':').replace('{', '').replace('}', '').replace('~', '-')
79
80class LatexFormatter(Formatter):
81    flavor = 'latex'
82
83    def __init__(self, env, page, req=None, absurls=0, db=None):
84        Formatter.__init__(self, env, req, absurls, db)
85        self.pagename = page.name
86
87    def _get_link_resolvers(self):
88        return {'link':LatexURL, 'source':LatexURL, 'wiki':WikiLatexURL, 'ticket':LatexURL, 'changeset':LatexURL} #WikiSystem(self.env).link_resolvers
89    link_resolvers = property(_get_link_resolvers)
90
91    def _get_db(self):
92        if not self._db:
93            self._db = self.env.get_db_cnx()
94        return self._db
95    db = property(fget=_get_db)
96
97    def _get_rules(self):
98        return self.wiki.rules
99    rules = property(_get_rules)
100
101    def replace(self, fullmatch):
102        for itype, match in fullmatch.groupdict().items():
103            if match and not itype in self.wiki.helper_patterns:
104                # Check for preceding escape character '!'
105                if match[0] == '!':
106                    return gtotex(pretexfilter(match[1:]))
107                if match[0:4] == "http":
108                    return LatexURL(self, 'http', match, match)
109                elif match[0].isalpha():
110                    return WikiLatexURL(self, 'wiki', match, match)
111                if itype in self.wiki.external_handlers:
112                    return self.wiki.external_handlers[itype](self, match, fullmatch)
113                else:
114                    return getattr(self, '_' + itype + '_formatter')(match, fullmatch)
115
116    def _tickethref_formatter(self, match, fullmatch):
117        """
118        This is from a patch for Trac 0.8 \todo how to call for 0.9+ (via link resolvers?)
119        """
120        number = int(match[1:])
121        cursor = self.db.cursor ()
122        cursor.execute('SELECT summary,status FROM ticket WHERE id=%s', number)
123        row = cursor.fetchone ()
124        if not row:
125            return '\\#%d (missing)' % (number)
126        else:
127            summary =  gtotex(self.prefilter(util.shorten_line(row[0])))
128            if row[1] == 'new':
129                return '\\#%d*\\footnote{%s \\emph{(new)}}' % (number, summary)
130            elif row[1] == 'closed':
131                return '\\sout{\\#%d}\\footnote{%s \\emph{(closed)}}'% (number, summary)
132            else:
133                return '\\#%d\\footnote{%s}' % (number, summary)
134
135    def _bolditalic_formatter(self, match, fullmatch):
136        italic = ('\\emph{', '}')
137        italic_open = self.tag_open_p(italic)
138        tmp = ''
139        if italic_open:
140            tmp += italic[1]
141            self.close_tag(italic[1])
142        tmp += self._bold_formatter(match, fullmatch)
143        if not italic_open:
144            tmp += italic[0]
145            self.open_tag(*italic)
146        return tmp
147
148    def _unquote(self, text):
149        if text and text[0] in "'\"" and text[0] == text[-1]:
150            return text[1:-1]
151        else:
152            return text
153
154    def _shref_formatter(self, match, fullmatch):
155        ns = fullmatch.group('sns')
156        target = self._unquote(fullmatch.group('stgt'))
157        return self._make_link(ns, target, match, match)
158
159    def _make_link(self, ns, target, match, label):
160        if ns in self.link_resolvers:
161            return self.link_resolvers[ns](self, ns, target, label, False)
162        elif target.startswith('//') or ns == "mailto":
163            return self._make_ext_link(ns+':'+target, label)
164        else:
165            return gtotex(match)
166
167    def _make_ext_link(self, url, text, title=''):
168        same = text == url
169        url = gfilter_to_url(url)
170        ref = totexlabel(url)
171        text, title = gtotex(text), gtotex(title)
172        if Formatter.img_re.search(url) and self.flavor != 'oneliner':
173            return '\\url{%s} (extimage - todo: make figure float with caption %s)' % (
174                   url, title or text)
175        if not url.startswith(self._local):
176            if same:
177                return '\\url{' + url + '}'
178            return '\\anchortext{%s}\\footnote{\\url{%s}}' % (text, url)
179        else:
180            if same:
181                return '\\url{' + url + '}'
182            return '\\anchortext{%s}\\footnote{ext: \\url{%s}}' % (text, url)
183
184    def _make_relative_link(self, url, text):
185        same = text == url
186        url = gfilter_to_url(url)
187        ref = totexlabel(url)
188        text = gtotex(text)
189        if Formatter.img_re.search(url) and self.flavor != 'oneliner':
190            return '\\url{%s} (relimage - todo: make figure float with caption %s)' % (
191                   url, text)
192        if not url.startswith(self._local):
193            if same:
194                return '\\url{' + url + '}'
195            return '\\anchortext{%s}\\footnote{\\url{%s}}' % (text, url)
196        else:
197            if same:
198                return '\\url{' + url + '}'
199            return '\\anchortext{%s}\\footnote{\\url{%s}} or Subsection \\ref{sub:%s}' % (text, url, ref)
200
201    def _bold_formatter(self, match, fullmatch):
202        return self.simple_tag_handler('\\textbf{', '}')
203
204    def _italic_formatter(self, match, fullmatch):
205        return self.simple_tag_handler('\\textit{', '}')
206
207    def _underline_formatter(self, match, fullmatch):
208        if match[0] == '!':
209            return gtotex(pretexfilter(match[1:]))
210        else:
211            return self.simple_tag_handler('\\underbar{',
212                                           '}')
213
214    def _strike_formatter(self, match, fullmatch):
215        if match[0] == '!':
216            return gtotex(pretexfilter(match[1:]))
217        else:
218            return self.simple_tag_handler('\\sout{', '}')
219
220    def _subscript_formatter(self, match, fullmatch):
221        if match[0] == '!':
222            return gtotex(pretexfilter(match[1:]))
223        else:
224            return self.simple_tag_handler('$_{', '}$')
225
226    def _superscript_formatter(self, match, fullmatch):
227        if match[0] == '!':
228            return gtotex(pretexfilter(match[1:]))
229        else:
230            return self.simple_tag_handler('$^{', '}$')
231
232    def _inlinecode_formatter(self, match, fullmatch):
233        return '\\texttt{%s}' % gtotex(fullmatch.group('inline'))
234
235    def _inlinecode2_formatter(self, match, fullmatch):
236        return '\\texttt{%s}' % gtotex(fullmatch.group('inline2'))
237
238    def _htmlescape_formatter(self, match, fullmatch):
239        return match == "&" and "\\&{}" or \
240               match == "<" and "\\textless{}" or "\\textgreater{}"
241
242    def _macro_formatter(self, match, fullmatch):
243        name = fullmatch.group('macroname')
244        if name in ['br', 'BR']:
245            if len(self.current_line) < 7:
246                return '\\vspace{1ex}'
247            return '\\\\'
248        return '\\begin{verbatim}\n' + Formatter._macro_formatter(self, match, fullmatch) + '\\end{verbatim}\n'
249#        args = fullmatch.group('macroargs')
250#        try:
251#            macro = WikiProcessor(self.env, name)
252#            return macro.process(self.req, args, 1)
253#        except Exception, e:
254#            self.env.log.error('Macro %s(%s) failed' % (name, args),
255#                               exc_info=True)
256#            return system_message('Error: Macro %s(%s) failed' % (name, args), e)
257
258    def _heading_formatter(self, match, fullmatch):
259        match = match.strip()
260        self.close_table()
261        self.close_paragraph()
262        self.close_indentation()
263        self.close_list()
264        self.close_def_list()
265
266        depth = min(len(fullmatch.group('hdepth')), 5)
267        heading = match[depth + 1:len(match) - depth - 1]
268
269        anchor = text = heading
270        sans_markup = re.sub(r'</?\w+(?: .*?)?>', '', text)
271
272        #check if valid LaTeX label
273        i = 1
274        anchor = anchor_base = anchor.encode('utf-8')
275        while anchor in self._anchors:
276            anchor = anchor_base + str(i)
277            i += 1
278        self._anchors.append(anchor)
279        try:
280                self.out.write('\\subsubsection{\\label{anchor:%s}%s}' % (totexlabel(anchor), gtotex(text)))
281        except:
282                self.out.write('\\subsubsection{Bad Unicode}')
283
284    def _indent_formatter(self, match, fullmatch):
285        depth = int((len(fullmatch.group('idepth')) + 1) / 2)
286        list_depth = len(self._list_stack)
287        if list_depth > 0 and depth == list_depth + 1:
288            self.in_list_item = 1
289        else:
290            self.open_indentation(depth)
291        return ''
292
293    def _last_table_cell_formatter(self, match, fullmatch):
294        return ''
295
296    def _table_cell_formatter(self, match, fullmatch):
297        self.open_table()
298        self.open_table_row()
299        if self.in_table_cell:
300            return ' & '
301        else:
302            self.in_table_cell = 1
303            return ''
304
305    def close_indentation(self):
306        self.out.write(('\\end{quote}' + os.linesep) * self.indent_level)
307        self.indent_level = 0
308
309    def open_indentation(self, depth):
310        if self.in_def_list:
311            return
312        diff = depth - self.indent_level
313        if diff != 0:
314            self.close_paragraph()
315            self.close_indentation()
316            self.close_list()
317            self.indent_level = depth
318            self.out.write(('\\begin{quote}' + os.linesep) * depth)
319
320    def _list_formatter(self, match, fullmatch):
321        ldepth = len(fullmatch.group('ldepth'))
322        depth = int((len(fullmatch.group('ldepth')) + 1) / 2)
323        self.in_list_item = depth > 0
324        type_ = ['ol', 'ul'][match[ldepth] == '*']
325        self._set_list_depth(depth, type_)
326        return ''
327
328    def _definition_formatter(self, match, fullmatch):
329        tmp = self.in_def_list and '' or '\\begin{description}\n'
330        tmp += '\\item %s ' % gtotex(match[:-2]) #wiki_to_oneliner(match[:-2], self.env, self.db)
331        self.in_def_list = True
332        return tmp
333
334    def close_def_list(self):
335        if self.in_def_list:
336            self.out.write('\\end{description}\n')
337        self.in_def_list = False
338
339    def _hl_to_ll(self, l):
340        if l == 'ul':
341            return 'itemize'
342        else:
343            return 'enumerate'
344
345    def _set_list_depth(self, depth, type_):
346        current_depth = len(self._list_stack)
347        diff = depth - current_depth
348        self.close_table()
349        self.close_paragraph()
350        self.close_indentation()
351        if diff > 0:
352            for i in range(diff):
353                self._list_stack.append(type_)
354                self.out.write('\\begin{' + self._hl_to_ll(type_) + '}\n')
355                self.out.write('\n\\item ')
356        elif diff < 0:
357            for i in range(-diff):
358                tmp = self._list_stack.pop()
359                self.out.write('\\end{' + self._hl_to_ll(tmp) + '}\n')
360            if self._list_stack != [] and type_ != self._list_stack[-1]:
361                tmp = self._list_stack.pop()
362                self._list_stack.append(type_)
363                self.out.write('\n\\end{%s}\n\\begin{%s}\n\\item ' % (self._hl_to_ll(tmp), self._hl_to_ll(type_)))
364            if depth > 0:
365                self.out.write('\n\\item ')
366        # diff == 0
367        elif self._list_stack != [] and type_ != self._list_stack[-1]:
368            tmp = self._list_stack.pop()
369            self._list_stack.append(type_)
370            self.out.write('\n\\end{%s}\n\\begin{%s}\n\\item ' % (self._hl_to_ll(tmp), self._hl_to_ll(type_)))
371        elif depth > 0:
372            self.out.write('\n\\item ')
373
374    def close_list(self):
375        if self._list_stack != []:
376            self._set_list_depth(0, None)
377
378    def open_paragraph(self):
379        if not self.paragraph_open:
380            self.out.write(os.linesep)
381            self.paragraph_open = 1
382
383    def close_paragraph(self):
384        if self.paragraph_open:
385            while self._open_tags != []:
386                self.out.write(self._open_tags.pop()[1])
387            self.out.write(os.linesep)
388            self.paragraph_open = 0
389
390    def open_table(self):
391        if not self.in_table:
392            self.close_paragraph()
393            self.close_indentation()
394            self.close_list()
395            self.close_def_list()
396            self.in_table = 1
397            self.out.write('\\begin{tabular}{|l|l|l|l|l|l|l|l|l|} \\hline' + os.linesep)
398
399    def open_table_row(self):
400        if not self.in_table_row:
401            self.open_table()
402            self.in_table_row = 1
403            self.out.write(os.linesep)
404
405    def close_table_row(self):
406        if self.in_table_row:
407            self.in_table_row = 0
408            if self.in_table_cell:
409                self.in_table_cell = 0
410                self.out.write('~\\\\ \\hline' + os.linesep)
411
412    def close_table(self):
413        if self.in_table:
414            self.close_table_row()
415            self.out.write('\\end{tabular}' + os.linesep)
416            self.in_table = 0
417
418    def handle_code_block(self, line):
419        if line.strip() == '{{{':
420            self.in_code_block += 1
421            if self.in_code_block == 1:
422                self.out.write('\\begin{verbatim}' + os.linesep)
423            else:
424                self.out.write(line + os.linesep)
425        elif line.strip() == '}}}':
426            self.in_code_block -= 1
427            if self.in_code_block == 0:
428                self.out.write('\\end{verbatim}' + os.linesep)
429            else:
430                self.out.write(line + os.linesep)
431        else:
432            self.out.write(line + os.linesep)
433
434    def preamble(self):
435        self.secname = self.pagename
436        self.out.write('\\documentclass{article}' + os.linesep)
437        self.out.write('\\usepackage{url}' + os.linesep)
438        self.out.write('\\usepackage{ulem}' + os.linesep)
439#        self.out.write("""\def\dotuline{\\bgroup
440#                \\ifdim\\ULdepth=\\maxdimen  % Set depth based on font, if not set already
441#                \\settodepth\\ULdepth{(j}\\advance\\ULdepth.4pt\\fi
442#                \\markoverwith{\\begingroup
443#                \\advance\\ULdepth0.08ex
444#                \\lower\\ULdepth\\hbox{\\kern.15em .\\kern.1em}%
445#                \\endgroup}\\ULon}""")
446        self.out.write('\\newcommand{\\anchortext}[1]{\def\ULthickness{.2pt}\\underbar{#1}\def\ULthickness{.4pt}} %this does not appear to work properly' + os.linesep)
447        self.out.write('\\newcommand{\\mypercent}{\\%{}}')
448        self.out.write('\\newcommand{\\myamp}{\\&{}}')
449        self.out.write('\\begin{document}' + os.linesep)
450        self.out.write('\\subsection{\\label{sub:' + self.secname + '}' + self.secname + '}' + os.linesep)
451
452    def format(self, text, out, escape_newlines=False):
453        self.out = out
454        self._open_tags = []
455        self._list_stack = []
456
457        self.in_code_block = 0
458        self.in_table = 0
459        self.in_def_list = 0
460        self.in_table_row = 0
461        self.in_table_cell = 0
462        self.indent_level = 0
463        self.paragraph_open = 0
464
465        self.preamble()
466
467        for line in text.splitlines():
468            self.current_line = line
469            # Handle code block
470            if self.in_code_block or line.strip() == '{{{':
471                self.handle_code_block(line)
472                continue
473            # Handle Horizontal ruler
474            elif line[0:4] == '----':
475                self.close_paragraph()
476                self.close_indentation()
477                self.close_list()
478                self.close_def_list()
479                self.close_table()
480                self.out.write('{\\normalsize \\vspace{1ex} \\hrule width \\columnwidth \\vspace{1ex}}' + os.linesep)
481                continue
482            # Handle new paragraph
483            elif line == '':
484                self.close_paragraph()
485                self.close_indentation()
486                self.close_list()
487                self.close_def_list()
488                continue
489
490            if escape_newlines:
491                line += ' [[BR]]'
492            self.in_list_item = False
493            # Throw a bunch of regexps on the problem
494            result = re.sub(self.rules, self.replace, line)
495
496            if not self.in_list_item:
497                self.close_list()
498
499            if self.in_def_list and not line.startswith(' '):
500                self.close_def_list()
501
502            if self.in_table and line[0:2] != '||':
503                self.close_table()
504
505            if len(result) and not self.in_list_item and not self.in_def_list \
506                    and not self.in_table:
507                self.open_paragraph()
508            try:
509                out.write(gtotex(result) + os.linesep)
510            except:
511                out.write("\\textless Bad Unicode \textgreater" + os.linesep)
512            self.close_table_row()
513
514        self.close_table()
515        self.close_paragraph()
516        self.close_indentation()
517        self.close_list()
518        self.close_def_list()
519        self.out.write('\\end{document}')
520
521def wiki_to_latex(page, env, req, db=None, absurls=0, escape_newlines=False):
522    out = StringIO()
523    LatexFormatter(env, page, req, absurls, db).format(page.text, out, escape_newlines)
524    return util.Markup(out.getvalue())