Edgewall Software

Ticket #2296: wikilatex.py

File wikilatex.py, 16.5 KB (added by Trent Apted <tapted@…>, 3 years ago)

site-packages/trac/wiki/wikilatex.py patch for LaTeX export (first cut)

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
14
15__all__ = ['wiki_to_latex']
16
17def gtotex(s, boolparam=True):
18    return s.replace('&', '\\&{}').replace('$', '\\${}') #etc
19
20def LatexURL(formatter, ns, target, text):
21    return gtotex(text) + '\\footnote{\\url{' + ns + ':' + gtotex(target) + '}}';
22
23class LatexFormatter(Formatter):
24    flavor = 'latex'
25
26    def __init__(self, env, page, req=None, absurls=0, db=None):
27        self.env = env
28        self.req = req
29        self._db = db
30        self._absurls = absurls
31        self._anchors = []
32        self._open_tags = []
33        self.href = absurls and env.abs_href or env.href
34        self._local = env.config.get('project', 'url', '') or env.abs_href.base
35        self.pagename = page.name
36
37    def totex(self, s, boolparam=True):
38        return s.replace('&', '\\&{}').replace('$', '\\${}') #etc
39
40    def tolabel(self, s, boolparam=True):
41        return s.replace('&', '').replace('$', '').replace(' ', '-') #etc
42
43    def _get_db(self):
44        if not self._db:
45            self._db = self.env.get_db_cnx()
46        return self._db
47    db = property(fget=_get_db)
48
49    def _get_rules(self):
50        return WikiSystem(self.env).rules
51    rules = property(_get_rules)
52
53    def _get_link_resolvers(self):
54        return {'source':LatexURL, 'wiki':LatexURL, 'ticket':LatexURL, 'changeset':LatexURL} #WikiSystem(self.env).link_resolvers
55    link_resolvers = property(_get_link_resolvers)
56
57    def replace(self, fullmatch):
58        wiki = WikiSystem(self.env)       
59        for itype, match in fullmatch.groupdict().items():
60            if match and not itype in wiki.helper_patterns:
61                # Check for preceding escape character '!'
62                if match[0] == '!':
63                    return match[1:]
64                elif match[0].isalpha():
65                    return LatexURL(self, 'wiki', match, match)
66                if itype in wiki.external_handlers:
67                    return wiki.external_handlers[itype](self, match, fullmatch)
68                else:
69                    return getattr(self, '_' + itype + '_formatter')(match, fullmatch)
70
71    def tag_open_p(self, tag):
72        """Do we currently have any open tag with @tag as end-tag"""
73        return tag in self._open_tags
74
75    def close_tag(self, tag):
76        tmp =  ''
77        for i in xrange(len(self._open_tags)-1, -1, -1):
78            tmp += self._open_tags[i][1]
79            if self._open_tags[i][1] == tag:
80                del self._open_tags[i]
81                for j in xrange(i, len(self._open_tags)):
82                    tmp += self._open_tags[j][0]
83                break
84        return tmp
85
86    def open_tag(self, open, close):
87        self._open_tags.append((open, close))
88
89    def simple_tag_handler(self, open_tag, close_tag):
90        """Generic handler for simple binary style tags"""
91        if self.tag_open_p((open_tag, close_tag)):
92            return self.close_tag(close_tag)
93        else:
94            self.open_tag(open_tag, close_tag)
95        return open_tag
96
97    def _bolditalic_formatter(self, match, fullmatch):
98        italic = ('\\emph{', '}')
99        italic_open = self.tag_open_p(italic)
100        tmp = ''
101        if italic_open:
102            tmp += italic[1]
103            self.close_tag(italic[1])
104        tmp += self._bold_formatter(match, fullmatch)
105        if not italic_open:
106            tmp += italic[0]
107            self.open_tag(*italic)
108        return tmp
109
110    def _unquote(self, text):
111        if text and text[0] in "'\"" and text[0] == text[-1]:
112            return text[1:-1]
113        else:
114            return text
115
116    def _shref_formatter(self, match, fullmatch):
117        ns = fullmatch.group('sns')
118        target = self._unquote(fullmatch.group('stgt'))
119        return self._make_link(ns, target, match, match)
120
121    def _lhref_formatter(self, match, fullmatch):
122        ns = fullmatch.group('lns')
123        target = self._unquote(fullmatch.group('ltgt'))
124        label = fullmatch.group('label')
125        if not label: # e.g. `[http://target]` or `[wiki:target]`
126            if target:
127                if target.startswith('//'): # for `[http://target]`
128                    label = ns+':'+target   # use `http://target`
129                else:                       # for `wiki:target`
130                    label = target          # use only `target`
131            else: # e.g. `[search:]`
132                label = ns
133        label = self._unquote(label)
134        rel = fullmatch.group('rel')
135        if rel:
136            return self._make_relative_link(rel, label or rel)
137        else:
138            return self._make_link(ns, target, match, label)
139
140    def _make_link(self, ns, target, match, label):
141        if ns in self.link_resolvers:
142            return self.link_resolvers[ns](self, ns, target,
143                                           self.totex(label, False))
144        elif target.startswith('//') or ns == "mailto":
145            return self._make_ext_link(ns+':'+target, label)
146        else:
147            return self.totex(match)
148
149    def _make_ext_link(self, url, text, title=''):
150        url = self.totex(url)
151        text, title = self.totex(text), self.totex(title)
152        title_attr = title and ' title="%s"' % title or ''
153        if Formatter.img_re.search(url) and self.flavor != 'oneliner':
154            return '\\url{%s} (extimage - todo: make figure float for %s)' % (url, title or text)
155        if not url.startswith(self._local):
156            return '%s\\footnote{\\url{%s}}' % (text, url)
157        else:
158            return '\\url{%s} (ext: %s)' % (url, text)
159
160    def _make_relative_link(self, url, text):
161        url, text = self.totex(url), self.totex(text)
162        if Formatter.img_re.search(url) and self.flavor != 'oneliner':
163            return '\\url{%s} (image - todo: make figure float for %s)' % (url, title or text)
164        if not url.startswith(self._local):
165            return '\\url{%s} (local: %s)' % (url, text)
166        else:
167            return '\\url{%s} (%s) or Subsection \\ref{url}' % (url, text)
168
169    def _bold_formatter(self, match, fullmatch):
170        return self.simple_tag_handler('\\textbf{', '}')
171
172    def _italic_formatter(self, match, fullmatch):
173        return self.simple_tag_handler('\\textit{', '}')
174
175    def _underline_formatter(self, match, fullmatch):
176        if match[0] == '!':
177            return match[1:]
178        else:
179            return self.simple_tag_handler('\\underbar{',
180                                           '}')
181
182    def _strike_formatter(self, match, fullmatch):
183        if match[0] == '!':
184            return match[1:]
185        else:
186            return self.simple_tag_handler('\\underbar{', '} (strike)')
187
188    def _subscript_formatter(self, match, fullmatch):
189        if match[0] == '!':
190            return match[1:]
191        else:
192            return self.simple_tag_handler('$_{', '}$')
193
194    def _superscript_formatter(self, match, fullmatch):
195        if match[0] == '!':
196            return match[1:]
197        else:
198            return self.simple_tag_handler('$^{', '}$')
199
200    def _inlinecode_formatter(self, match, fullmatch):
201        return '\\texttt{%s}' % self.totex(fullmatch.group('inline'))
202
203    def _inlinecode2_formatter(self, match, fullmatch):
204        return '\\texttt{%s}' % self.totex(fullmatch.group('inline2'))
205
206    def _htmlescape_formatter(self, match, fullmatch):
207        return match == "&" and "\\&{}" or \
208               match == "<" and "\\textless{}" or "\\textgreater{}"
209
210    def _macro_formatter(self, match, fullmatch):
211        name = fullmatch.group('macroname')
212        if name in ['br', 'BR']:
213            return '\\\\'
214        args = fullmatch.group('macroargs')
215        try:
216            macro = WikiProcessor(self.env, name)
217            return macro.process(self.req, args, 1)
218        except Exception, e:
219            self.env.log.error('Macro %s(%s) failed' % (name, args),
220                               exc_info=True)
221            return system_message('Error: Macro %s(%s) failed' % (name, args), e)
222
223    def _heading_formatter(self, match, fullmatch):
224        match = match.strip()
225        self.close_table()
226        self.close_paragraph()
227        self.close_indentation()
228        self.close_list()
229        self.close_def_list()
230
231        depth = min(len(fullmatch.group('hdepth')), 5)
232        heading = match[depth + 1:len(match) - depth - 1]
233
234        anchor = text = heading
235        sans_markup = re.sub(r'</?\w+(?: .*?)?>', '', text)
236
237        #check if valid LaTeX label
238
239        anchor = anchor_base = anchor.encode('utf-8')
240        while anchor in self._anchors:
241            anchor = anchor_base + str(i)
242            i += 1
243        self._anchors.append(anchor)
244        self.out.write('\\subsubsection{\\label{anchor:%s}%s}' % (self.tolabel(anchor), self.totex(text)))
245
246    def _indent_formatter(self, match, fullmatch):
247        depth = int((len(fullmatch.group('idepth')) + 1) / 2)
248        list_depth = len(self._list_stack)
249        if list_depth > 0 and depth == list_depth + 1:
250            self.in_list_item = 1
251        else:
252            self.open_indentation(depth)
253        return ''
254
255    def _last_table_cell_formatter(self, match, fullmatch):
256        return ''
257
258    def _table_cell_formatter(self, match, fullmatch):
259        self.open_table()
260        self.open_table_row()
261        if self.in_table_cell:
262            return ' & '
263        else:
264            self.in_table_cell = 1
265            return '\\\\'
266
267    def close_indentation(self):
268        self.out.write(('\\end{quote}' + os.linesep) * self.indent_level)
269        self.indent_level = 0
270
271    def open_indentation(self, depth):
272        if self.in_def_list:
273            return
274        diff = depth - self.indent_level
275        if diff != 0:
276            self.close_paragraph()
277            self.close_indentation()
278            self.close_list()
279            self.indent_level = depth
280            self.out.write(('\\begin{quote}' + os.linesep) * depth)
281
282    def _list_formatter(self, match, fullmatch):
283        ldepth = len(fullmatch.group('ldepth'))
284        depth = int((len(fullmatch.group('ldepth')) + 1) / 2)
285        self.in_list_item = depth > 0
286        type_ = ['ol', 'ul'][match[ldepth] == '*']
287        self._set_list_depth(depth, type_)
288        return ''
289
290    def _definition_formatter(self, match, fullmatch):
291        tmp = self.in_def_list and '' or '\\begin{definition}\n'
292        tmp += '\\item %s ' % wiki_to_oneliner(match[:-2], self.env,
293                                                    self.db)
294        self.in_def_list = True
295        return tmp
296
297    def close_def_list(self):
298        if self.in_def_list:
299            self.out.write('\\end{definition}\n')
300        self.in_def_list = False
301
302    def _hl_to_ll(self, l):
303        if l == 'ul':
304            return 'itemize'
305        else:
306            return 'enumerate'
307
308    def _set_list_depth(self, depth, type_):
309        current_depth = len(self._list_stack)
310        diff = depth - current_depth
311        self.close_table()
312        self.close_paragraph()
313        self.close_indentation()
314        if diff > 0:
315            for i in range(diff):
316                self._list_stack.append(type_)
317                self.out.write('\\begin{' + self._hl_to_ll(type_) + '}\n')
318                self.out.write('\n\\item ')
319        elif diff < 0:
320            for i in range(-diff):
321                tmp = self._list_stack.pop()
322                self.out.write('\\end{' + self._hl_to_ll(type_) + '}\n')
323            if self._list_stack != [] and type_ != self._list_stack[-1]:
324                tmp = self._list_stack.pop()
325                self._list_stack.append(type_)
326                self.out.write('\n\\end{%s}\n\\begin{%s}\n\\item ' % (self._hl_to_ll(tmp), self._hl_to_ll(type_)))
327            if depth > 0:
328                self.out.write('\n\\item ')
329        # diff == 0
330        elif self._list_stack != [] and type_ != self._list_stack[-1]:
331            tmp = self._list_stack.pop()
332            self._list_stack.append(type_)
333            self.out.write('\n\\end{%s}\n\\begin{%s}\n\\item ' % (self._hl_to_ll(tmp), self._hl_to_ll(type_)))
334        elif depth > 0:
335            self.out.write('\n\\item ')
336
337    def close_list(self):
338        if self._list_stack != []:
339            self._set_list_depth(0, None)
340
341    def open_paragraph(self):
342        if not self.paragraph_open:
343            self.out.write(os.linesep)
344            self.paragraph_open = 1
345
346    def close_paragraph(self):
347        if self.paragraph_open:
348            while self._open_tags != []:
349                self.out.write(self._open_tags.pop()[1])
350            self.out.write(os.linesep)
351            self.paragraph_open = 0
352
353    def open_table(self):
354        if not self.in_table:
355            self.close_paragraph()
356            self.close_indentation()
357            self.close_list()
358            self.close_def_list()
359            self.in_table = 1
360            self.out.write('\\begin{tablular}' + os.linesep)
361
362    def open_table_row(self):
363        if not self.in_table_row:
364            self.open_table()
365            self.in_table_row = 1
366            self.out.write(os.linesep)
367
368    def close_table_row(self):
369        if self.in_table_row:
370            self.in_table_row = 0
371            if self.in_table_cell:
372                self.in_table_cell = 0
373                self.out.write('')
374
375            self.out.write('\\\\')
376
377    def close_table(self):
378        if self.in_table:
379            self.close_table_row()
380            self.out.write('\\end{tablular}' + os.linesep)
381            self.in_table = 0
382
383    def handle_code_block(self, line):
384        if line.strip() == '{{{':
385            self.in_code_block += 1
386            if self.in_code_block == 1:
387                self.out.write('\\begin{verbatim}' + os.linesep)
388            else:
389                self.out.write(line + os.linesep)
390        elif line.strip() == '}}}':
391            self.in_code_block -= 1
392            if self.in_code_block == 0:
393                self.out.write('\\end{verbatim}' + os.linesep)
394            else:
395                self.out.write(line + os.linesep)
396        else:
397            self.out.write(line + os.linesep)
398
399    def preamble(self):
400        self.secname = self.pagename
401        self.out.write('\\documentclass{article}' + os.linesep)
402        self.out.write('\\usepackage{url}' + os.linesep)
403        self.out.write('\\begin{document}' + os.linesep)
404        self.out.write('\\subsection{\\label{sub:' + self.secname + '}' + self.secname + '}' + os.linesep)
405
406    def format(self, text, out, escape_newlines=False):
407        self.out = out
408        self._open_tags = []
409        self._list_stack = []
410
411        self.in_code_block = 0
412        self.in_table = 0
413        self.in_def_list = 0
414        self.in_table_row = 0
415        self.in_table_cell = 0
416        self.indent_level = 0
417        self.paragraph_open = 0
418
419        self.preamble()
420
421        for line in text.splitlines():
422            # Handle code block
423            if self.in_code_block or line.strip() == '{{{':
424                self.handle_code_block(line)
425                continue
426            # Handle Horizontal ruler
427            elif line[0:4] == '----':
428                self.close_paragraph()
429                self.close_indentation()
430                self.close_list()
431                self.close_def_list()
432                self.close_table()
433                self.out.write('{\\normalsize \\vspace{1ex} \\hrule width \\columnwidth \\vspace{1ex}}' + os.linesep)
434                continue
435            # Handle new paragraph
436            elif line == '':
437                self.close_paragraph()
438                self.close_indentation()
439                self.close_list()
440                self.close_def_list()
441                continue
442
443            if escape_newlines:
444                line += ' [[BR]]'
445            self.in_list_item = False
446            # Throw a bunch of regexps on the problem
447            result = re.sub(self.rules, self.replace, line)
448
449            if not self.in_list_item:
450                self.close_list()
451
452            if self.in_def_list and not line.startswith(' '):
453                self.close_def_list()
454
455            if self.in_table and line[0:2] != '||':
456                self.close_table()
457
458            if len(result) and not self.in_list_item and not self.in_def_list \
459                    and not self.in_table:
460                self.open_paragraph()
461            out.write(result + os.linesep)
462            self.close_table_row()
463
464        self.close_table()
465        self.close_paragraph()
466        self.close_indentation()
467        self.close_list()
468        self.close_def_list()
469        self.out.write('\\end{document}')
470
471def wiki_to_latex(page, env, req, db=None, absurls=0, escape_newlines=False):
472    out = StringIO()
473    LatexFormatter(env, page, req, absurls, db).format(page.text, out, escape_newlines)
474    return util.Markup(out.getvalue())