Edgewall Software

Ticket #4270: notifyformatter.py

File notifyformatter.py, 20.0 KB (added by d_dorothy@…, 2 years ago)

proposed NotifyFormatter? class (and related one line class and convenience function)

Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2003-2006 Edgewall Software
4# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
5# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
6# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
7# All rights reserved.
8#
9# This software is licensed as described in the file COPYING, which
10# you should have received as part of this distribution. The terms
11# are also available at http://trac.edgewall.org/wiki/TracLicense.
12#
13# This software consists of voluntary contributions made by many
14# individuals. For the exact contribution history, see the revision
15# history and logs, available at http://trac.edgewall.org/log/.
16#
17# Author: Jonas Borgström <jonas@edgewall.com>
18#         Christopher Lenz <cmlenz@gmx.de>
19#         Christian Boos <cboos@neuf.fr>
20
21import re
22import os
23import urllib
24from urlparse import urlsplit
25
26from StringIO import StringIO
27
28from trac.core import *
29from trac.mimeview import *
30from trac.wiki.api import WikiSystem
31from trac.util.html import escape, plaintext, Markup, Element, html
32from trac.util.text import shorten_line, to_unicode
33from trac.wiki.formatter import Formatter, WikiProcessor
34
35class NotifyFormatter(Formatter):
36    flavor = 'notify'
37   
38    # -- Pre- IWikiSyntaxProvider rules (Font styles)
39
40    def simple_tag_handler(self, match, open_tag, close_tag):
41        """Generic handler for simple binary style tags"""
42        if self.tag_open_p((open_tag, close_tag)):
43            self.close_tag(close_tag)
44            return '' #simple tags are ignored
45        else:
46            self.open_tag(open_tag, close_tag)
47        return '' #simple tags are ignored
48
49    def _bolditalic_formatter(self, match, fullmatch):
50        italic = ('<i>', '</i>')
51        italic_open = self.tag_open_p(italic)
52        tmp = ''
53        if italic_open:
54            tmp += '' #italic[1]
55            self.close_tag(italic[1])
56        tmp += self._bold_formatter(match, fullmatch)
57        if not italic_open:
58            tmp += '' #italic[0]
59            self.open_tag(*italic)
60        return tmp
61
62   
63    def _inlinecode_formatter(self, match, fullmatch):
64        return '[code: ' + fullmatch.group('inline') + ']'
65        #return html.TT(fullmatch.group('inline'))
66
67    def _inlinecode2_formatter(self, match, fullmatch):
68        return '[code: ' + fullmatch.group('inline2') + ']'
69        #return html.TT(fullmatch.group('inline2'))
70
71    # -- Post- IWikiSyntaxProvider rules
72
73    # HTML escape of &, < and >
74    def _htmlescape_formatter(self, match, fullmatch):
75        return match
76
77    # Short form (shref) and long form (lhref) of TracLinks
78
79    def _make_link(self, ns, target, match, label):
80        # first check for an alias defined in trac.ini
81        ns = self.env.config.get('intertrac', ns) or ns
82        if ns in self.wiki.link_resolvers:
83            tmp = self.wiki.link_resolvers[ns](self, ns, target, escape(label, False))
84            r = re.compile("<a .*?</a>")
85            return r.sub(self._link_replace, '%s' % tmp)
86        elif target.startswith('//') or ns == "mailto":
87            return self._make_ext_link(ns+':'+target, label)
88        else:
89            return self._make_intertrac_link(ns, target, label) or \
90                   self._make_interwiki_link(ns, target, label) or \
91                   match
92
93    def _make_ext_link(self, url, text, title=''):
94        self.links.append(('%d: ' % (len(self.links) + 1)) + url )
95        # if title == '':
96            # return url + (' (%d)' % len(self.links))
97        return text + (' (%d)' % len(self.links))
98           
99
100    def _make_relative_link(self, url, text):
101        #relative links are absolute now (req or env).abs_href.base
102        if url.startswith('//'): # only the protocol will be kept
103            return url #what is this one?
104            #return html.A(text, class_="ext-link", href=url)
105        else:
106            self.links.append(('%d:' % (len(self.links) +1)) + url )
107            return text + (' (%d)' % len(self.links))
108            #return html.A(text, href=url)
109
110    # WikiMacros
111   
112    def _macro_formatter(self, match, fullmatch):
113        name = fullmatch.group('macroname')
114        if name.lower() == 'br':
115            return os.linesep
116        return '' # ignore wiki macros - rendering would be difficult and normally these are special cases
117
118    # Headings
119
120    def _link_replace(self, fullmatch):
121        tmp = fullmatch.group()
122        r = re.compile("(<a .*?)(href=)(?P<href>'[^']*'|\"[^\"]*\"|[^ ]*)(.*?>)(?P<text>.*(?=</a>))")
123        m = r.match('%s' % tmp)
124        if m:
125            href = self._unquote(m.group('href'))
126            #make relative links absolute
127            if href[0] == "/":
128                href = urlsplit((req or env).abs_href.base)[0] + "://" + urlsplit((req or env).abs_href.base)[1] + href
129            self.links.append("%d: " % (len(self.links) + 1) + href)
130            return m.group('text') + " (%d)" % len(self.links)
131        return tmp
132   
133    def _parse_heading(self, match, fullmatch, shorten):
134        match = match.strip()
135
136        depth = min(len(fullmatch.group('hdepth')), 5)
137        anchor = fullmatch.group('hanchor') or ''
138        heading_text = match[depth+1:-depth-1-len(anchor)]
139        heading = wiki_to_notifyoneliner(heading_text, self.env, self.db, False,
140                                   self._absurls, self.req, self.links)
141        if anchor:
142            anchor = anchor[1:]
143        else:
144            sans_markup = plaintext(heading, keeplinebreaks=False)
145            anchor = self._anchor_re.sub('', sans_markup)
146            if not anchor or anchor[0].isdigit() or anchor[0] in '.-':
147                # an ID must start with a Name-start character in XHTML
148                anchor = 'a' + anchor # keeping 'a' for backward compat
149        i = 1
150        anchor_base = anchor
151        while anchor in self._anchors:
152            anchor = anchor_base + str(i)
153            i += 1
154        self._anchors[anchor] = True
155        if shorten:
156            heading = wiki_to_notifyoneliner(heading_text, self.env, self.db, True,
157                                       self._absurls, self.req, self.links)
158        return (depth, heading, anchor)
159   
160    def _heading_formatter(self, match, fullmatch):
161        self.close_table()
162        self.close_paragraph()
163        self.close_indentation()
164        self.close_list()
165        self.close_def_list()
166        depth, heading, anchor = self._parse_heading(match, fullmatch, False)
167        self.out.write('%s %s %s' % ('='*depth, heading, '='*depth))
168
169    # Lists
170
171    def _set_list_depth(self, depth, new_type, list_class, start):
172        def open_list():
173            self.close_table()
174            self.close_paragraph()
175            self.close_indentation() # FIXME: why not lists in quotes?
176            self._list_stack.append((new_type, depth))
177            self._set_tab(depth)
178            class_attr = list_class and ' class="%s"' % list_class or ''
179            start_attr = start and ' start="%s"' % start or ''
180            self.out.write('  '*depth + '* ') # + '<'+new_type+class_attr+start_attr+'><li>')
181        def close_list(tp):
182            self._list_stack.pop()
183            self.out.write('') #'</li></%s>' % tp)
184
185        # depending on the indent/dedent, open or close lists
186        if depth > self._get_list_depth():
187            open_list()
188        else:
189            while self._list_stack:
190                deepest_type, deepest_offset = self._list_stack[-1]
191                if depth >= deepest_offset:
192                    break
193                close_list(deepest_type)
194            if depth > 0:
195                if self._list_stack:
196                    old_type, old_offset = self._list_stack[-1]
197                    if new_type and old_type != new_type:
198                        close_list(old_type)
199                        open_list()
200                    else:
201                        if old_offset != depth: # adjust last depth
202                            self._list_stack[-1] = (old_type, depth)
203                        self.out.write('  '*depth + '* ') #'</li><li>')
204                else:
205                    open_list()
206
207    # Definition Lists
208
209    def _definition_formatter(self, match, fullmatch):
210        #tmp = self.in_def_list and '</dd>' or '<dl>'
211        definition = match[:match.find('::')]
212        tmp = '%s: ' % wiki_to_notifyoneliner(definition, self.env,
213                                                    self.db, req=self.req)
214        self.in_def_list = True
215        return tmp
216
217    def close_def_list(self):
218        if self.in_def_list:
219            self.out.write('\n')
220        self.in_def_list = False
221
222    # Blockquote
223
224    def _indent_formatter(self, match, fullmatch):
225        idepth = len(fullmatch.group('idepth'))
226        if self._list_stack:
227            ltype, ldepth = self._list_stack[-1]
228            if idepth < ldepth:
229                for _, ldepth in self._list_stack:
230                    if idepth > ldepth:
231                        self.in_list_item = True
232                        self._set_list_depth(idepth, None, None, None)
233                        return ''
234            elif idepth <= ldepth + (ltype == 'ol' and 3 or 2):
235                self.in_list_item = True
236                return ''
237        if self.in_def_list:
238            return '  '
239        return '  '
240
241    def _citation_formatter(self, match, fullmatch):
242        cdepth = len(fullmatch.group('cdepth').replace(' ', ''))
243        self._set_quote_depth(cdepth, True)
244        return match #''
245
246    def close_indentation(self):
247        self._set_quote_depth(0)
248
249    def _get_quote_depth(self):
250        """Return the space offset associated to the deepest opened quote."""
251        return self._quote_stack and self._quote_stack[-1] or 0
252
253    def _set_quote_depth(self, depth, citation=False):
254        return '' #may want to revisit this
255
256    # Table
257   
258    def _last_table_cell_formatter(self, match, fullmatch):
259        return match #''
260
261    def _table_cell_formatter(self, match, fullmatch):
262        return match
263        self.open_table()
264        self.open_table_row()
265        if self.in_table_cell:
266            return '</td><td>'
267        else:
268            self.in_table_cell = 1
269            return '<td>'
270
271    def open_table(self):
272        if not self.in_table:
273            self.close_paragraph()
274            self.close_list()
275            self.close_def_list()
276            self.in_table = 1
277            self.out.write('<table class="wiki">' + os.linesep)
278
279    def open_table_row(self):
280        if not self.in_table_row:
281            self.open_table()
282            self.in_table_row = 1
283            self.out.write('<tr>')
284
285    def close_table_row(self):
286        if self.in_table_row:
287            self.in_table_row = 0
288            if self.in_table_cell:
289                self.in_table_cell = 0
290                self.out.write('</td>')
291
292            self.out.write('</tr>')
293
294    def close_table(self):
295        if self.in_table:
296            self.close_table_row()
297            self.out.write('</table>' + os.linesep)
298            self.in_table = 0
299
300    # Paragraphs
301
302    def open_paragraph(self):
303        if not self.paragraph_open:
304            self.out.write(os.linesep)
305            self.paragraph_open = 1
306
307    def close_paragraph(self):
308        if self.paragraph_open:
309            while self._open_tags != []:
310                self.out.write(self._open_tags.pop()[1])
311            self.out.write(os.linesep)
312            self.paragraph_open = 0
313
314    # Code blocks
315    #this was overridden
316    def handle_code_block(self, line):
317        if line.strip() == Formatter.STARTBLOCK:
318            self.in_code_block += 1
319            if self.in_code_block == 1:
320                self.code_processor = None
321                self.code_text = 'Code'
322            else:
323                self.code_text += ' ' + line + os.linesep
324                if not self.code_processor:
325                    self.code_processor = WikiProcessor(self.env, 'default')
326        elif line.strip() == Formatter.ENDBLOCK:
327            self.in_code_block -= 1
328            if self.in_code_block == 0 and self.code_processor:
329                self.close_table()
330                self.close_paragraph()
331                self.out.write(to_unicode(self.code_text))
332            else:
333                self.code_text += ' ' + line + os.linesep
334        elif not self.code_processor:
335            match = Formatter._processor_re.search(line)
336            if match:
337                name = match.group(1)
338                self.code_text += '(' + name + '):' + os.linesep
339                #we set it so that we do not end up back here but we never use it
340                self.code_processor = WikiProcessor(self.env, 'default')
341            else:
342                self.code_text += ':' +  os.linesep + ' ' + line + os.linesep
343                #we set it so that we do not end up back here but we never use it
344                self.code_processor = WikiProcessor(self.env, 'default')
345        else:
346            self.code_text += ' ' + line + os.linesep
347
348    # -- Wiki engine
349   
350    def handle_match(self, fullmatch):
351        for itype, match in fullmatch.groupdict().items():
352            if match and not itype in self.wiki.helper_patterns:
353                # Check for preceding escape character '!'
354                if match[0] == '!':
355                    return escape(match[1:])
356                if itype in self.wiki.external_handlers: # don't know what all to do here?
357                    external_handler = self.wiki.external_handlers[itype]
358                    tmp = external_handler(self, match, fullmatch)
359                    #handle links that are returned (CamelCase)
360                    r = re.compile("<a .*?</a>")
361                    return r.sub(self._link_replace, '%s' % tmp)
362                else:
363                    internal_handler = getattr(self, '_%s_formatter' % itype)
364                    return internal_handler(match, fullmatch)
365
366    def format(self, text, out=None, escape_newlines=False):
367        self.reset(out)
368        self.links = []
369        for line in text.splitlines():
370            # Handle code block
371            if self.in_code_block or line.strip() == Formatter.STARTBLOCK:
372                self.handle_code_block(line)
373                continue
374            # Handle Horizontal ruler
375            elif line[0:4] == '----':
376                self.close_table()
377                self.close_paragraph()
378                self.close_indentation()
379                self.close_list()
380                self.close_def_list()
381                self.out.write('----------------------------------------------------------------------------' + os.linesep)
382                continue
383            # Handle new paragraph
384            elif line == '':
385                self.close_paragraph()
386                self.close_indentation()
387                self.close_list()
388                self.close_def_list()
389                self.out.write(os.linesep) #maybe?
390                continue
391
392            # Tab expansion and clear tabstops if no indent
393            line = line.replace('\t', ' '*2)
394            if not line.startswith(' '):
395                self._tabstops = []
396
397            if escape_newlines:
398                line += ' [[BR]]'
399            self.in_list_item = False
400            self.in_quote = False
401            # Throw a bunch of regexps on the problem
402            result = re.sub(self.wiki.rules, self.replace, line)
403
404            if not self.in_list_item:
405                self.close_list()
406
407            if not self.in_quote:
408                self.close_indentation()
409
410            if self.in_def_list and not line.startswith(' '):
411                self.close_def_list()
412
413            if self.in_table and line.strip()[0:2] != '||':
414                self.close_table()
415
416            if len(result) and not self.in_list_item and not self.in_def_list \
417                    and not self.in_table:
418                self.open_paragraph()
419            self.out.write(result + os.linesep)
420            self.close_table_row()
421
422        self.close_table()
423        self.close_paragraph()
424        self.close_indentation()
425        self.close_list()
426        self.close_def_list()
427        self.close_code_blocks()
428       
429        #print links as foot notes
430        self.out.write('----------------------------------------------------------------------------' + os.linesep)
431        for link in self.links:
432            self.out.write(link + os.linesep)
433
434class NotifyOneLinerFormatter(NotifyFormatter):
435    """
436    A special version of the wiki formatter that only implement a
437    subset of the wiki formatting functions. This version is useful
438    for rendering short wiki-formatted messages on a single line
439    """
440    flavor = 'oneliner'
441
442    def __init__(self, env, absurls=False, db=None, req=None):
443        NotifyFormatter.__init__(self, env, req, absurls, db)
444   
445    # Override a few formatters to disable some wiki syntax in "oneliner"-mode
446    def _list_formatter(self, match, fullmatch): return match
447    def _indent_formatter(self, match, fullmatch): return match
448    def _citation_formatter(self, match, fullmatch):
449        return escape(match, False)
450    def _heading_formatter(self, match, fullmatch):
451        return escape(match, False)
452    def _definition_formatter(self, match, fullmatch):
453        return escape(match, False)
454    def _table_cell_formatter(self, match, fullmatch): return match
455    def _last_table_cell_formatter(self, match, fullmatch): return match
456
457    def _macro_formatter(self, match, fullmatch):
458        name = fullmatch.group('macroname')
459        if name.lower() == 'br':
460            return ' '
461        else:
462            return '' #'[[%s%s]]' % (name,  args and '(...)' or '')
463   
464    def format(self, text, out, shorten=False):
465        if not text:
466            return
467        self.out = out
468        self._open_tags = []
469
470        # Simplify code blocks
471        in_code_block = 0
472        processor = None
473        buf = StringIO()
474        for line in text.strip().splitlines():
475            if line.strip() == Formatter.STARTBLOCK:
476                in_code_block += 1
477            elif line.strip() == Formatter.ENDBLOCK:
478                if in_code_block:
479                    in_code_block -= 1
480                    if in_code_block == 0:
481                        if processor != 'comment':
482                            buf.write(' ![...]' + os.linesep)
483                        processor = None
484            elif in_code_block:
485                if not processor:
486                    if line.startswith('#!'):
487                        processor = line[2:].strip()
488            else:
489                buf.write(line + os.linesep)
490        result = buf.getvalue()[:-1]
491
492        if shorten:
493            result = shorten_line(result)
494
495        result = re.sub(self.wiki.rules, self.replace, result)
496        result = result.replace('[...]', '[&hellip;]')
497        if result.endswith('...'):
498            result = result[:-3] + '&hellip;'
499
500        # Close all open 'one line'-tags
501        result += self.close_tag(None)
502        # Flush unterminated code blocks
503        if in_code_block > 0:
504            result += '[&hellip;]'
505        out.write(result)
506
507#convenience function
508def wiki_to_notifyoneliner(wikitext, env, db=None, shorten=False, absurls=False,
509                     req=None, links=None):
510    if not wikitext:
511        return Markup()
512    out = StringIO()
513    nolf = NotifyOneLinerFormatter(env, absurls, db, req=req)
514    nolf.links = links
515    nolf.format(wikitext, out, shorten)
516    return Markup(out.getvalue())
517
518
519if __name__ == '__main__':
520    import trac.env
521    from trac.wiki.model import WikiPage
522    from trac.web.href import Href
523    req = None
524    env = trac.env.open_environment('/var/trac/WAdmin')
525    #request usually contains these but I do not have one
526    env.abs_href = Href('http://192.168.80.129/trac/WAdmin/')
527    env.href = Href(urlsplit(env.abs_href.base)[2])
528    page = WikiPage(env, 'WikiFormatting')
529    out = StringIO()
530    NotifyFormatter(env).format(page.text, out)
531    print out.getvalue()
532   
533