~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to pybb/markups/postmarkup.py

  • Committer: franku
  • Author(s): GunChleoc
  • Date: 2016-12-13 18:30:38 UTC
  • mfrom: (438.1.6 pyformat_util)
  • Revision ID: somal@arcor.de-20161213183038-5cgmvfh2fkgmoc1s
adding a script to run pyformat over the code base

Show diffs side-by-side

added added

removed removed

Lines of Context:
5
5
Author: Will McGugan (http://www.willmcgugan.com)
6
6
"""
7
7
 
8
 
__version__ = "1.1.3"
 
8
__version__ = '1.1.3'
9
9
 
10
10
import re
11
11
from urllib import quote, unquote, quote_plus
21
21
    pygments_available = False
22
22
 
23
23
 
24
 
 
25
24
def annotate_link(domain):
26
 
    """This function is called by the url tag. Override to disable or change behaviour.
 
25
    """This function is called by the url tag. Override to disable or change
 
26
    behaviour.
27
27
 
28
28
    domain -- Domain parsed from url
29
29
 
30
30
    """
31
 
    return u" [%s]"%_escape(domain)
32
 
 
33
 
 
34
 
re_url = re.compile(r"((https?):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)", re.MULTILINE| re.UNICODE)
35
 
 
36
 
 
37
 
re_html=re.compile('<.*?>|\&.*?\;')
 
31
    return u" [%s]" % _escape(domain)
 
32
 
 
33
 
 
34
re_url = re.compile(
 
35
    r"((https?):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)", re.MULTILINE | re.UNICODE)
 
36
 
 
37
 
 
38
re_html = re.compile('<.*?>|\&.*?\;')
 
39
 
 
40
 
38
41
def textilize(s):
39
 
    """Remove markup from html"""
40
 
    return re_html.sub("", s)
 
42
    """Remove markup from html."""
 
43
    return re_html.sub('', s)
41
44
 
42
45
re_excerpt = re.compile(r'\[".*?\]+?.*?\[/".*?\]+?', re.DOTALL)
43
46
re_remove_markup = re.compile(r'\[.*?\]', re.DOTALL)
44
47
 
 
48
 
45
49
def remove_markup(post):
46
50
    """Removes html tags from a string."""
47
 
    return re_remove_markup.sub("", post)
 
51
    return re_remove_markup.sub('', post)
 
52
 
48
53
 
49
54
def get_excerpt(post):
50
55
    """Returns an excerpt between ["] and [/"]
51
56
 
52
 
    post -- BBCode string"""
 
57
    post -- BBCode string
 
58
 
 
59
    """
53
60
 
54
61
    match = re_excerpt.search(post)
55
62
    if match is None:
56
 
        return ""
 
63
        return ''
57
64
    excerpt = match.group(0)
58
65
    excerpt = excerpt.replace(u'\n', u"<br/>")
59
66
    return remove_markup(excerpt)
60
67
 
 
68
 
61
69
def strip_bbcode(bbcode):
62
 
 
63
 
    """ Strips bbcode tags from a string.
 
70
    """Strips bbcode tags from a string.
64
71
 
65
72
    bbcode -- A string to remove tags from
66
73
 
70
77
 
71
78
 
72
79
def create(include=None, exclude=None, use_pygments=True, **kwargs):
73
 
 
74
80
    """Create a postmarkup object that converts bbcode to XML snippets.
75
81
 
76
82
    include -- List or similar iterable containing the names of the tags to use
79
85
               If omitted, no tags will be excluded
80
86
    use_pygments -- If True, Pygments (http://pygments.org/) will be used for the code tag,
81
87
                    otherwise it will use <pre>code</pre>
 
88
 
82
89
    """
83
90
 
84
91
    postmarkup = PostMarkup()
90
97
                return
91
98
            postmarkup_add_tag(tag_class, name, *args, **kwargs)
92
99
 
93
 
 
94
 
 
95
100
    add_tag(SimpleTag, 'b', 'strong')
96
101
    add_tag(SimpleTag, 'i', 'em')
97
102
    add_tag(SimpleTag, 'u', 'u')
120
125
    add_tag(CenterTag, u"center")
121
126
 
122
127
    if use_pygments:
123
 
        assert pygments_available, "Install Pygments (http://pygments.org/) or call create with use_pygments=False"
 
128
        assert pygments_available, 'Install Pygments (http://pygments.org/) or call create with use_pygments=False'
124
129
        add_tag(PygmentsCodeTag, u'code', **kwargs)
125
130
    else:
126
131
        add_tag(CodeTag, u'code', **kwargs)
128
133
    return postmarkup
129
134
 
130
135
 
131
 
 
132
136
_postmarkup = None
133
 
def render_bbcode(bbcode, encoding="ascii", exclude_tags=None, auto_urls=True):
134
 
 
 
137
 
 
138
 
 
139
def render_bbcode(bbcode, encoding='ascii', exclude_tags=None, auto_urls=True):
135
140
    """Renders a bbcode string in to XHTML. This is a shortcut if you don't
136
141
    need to customize any tags.
137
142
 
173
178
        self.close_node_index = None
174
179
 
175
180
    def open(self, parser, params, open_pos, node_index):
176
 
        """ Called when the open tag is initially encountered. """
 
181
        """Called when the open tag is initially encountered."""
177
182
        self.params = params
178
183
        self.open_pos = open_pos
179
184
        self.open_node_index = node_index
180
185
 
181
186
    def close(self, parser, close_pos, node_index):
182
 
        """ Called when the close tag is initially encountered. """
 
187
        """Called when the close tag is initially encountered."""
183
188
        self.close_pos = close_pos
184
189
        self.close_node_index = node_index
185
190
 
186
191
    def render_open(self, parser, node_index):
187
 
        """ Called to render the open tag. """
 
192
        """Called to render the open tag."""
188
193
        pass
189
194
 
190
195
    def render_close(self, parser, node_index):
191
 
        """ Called to render the close tag. """
 
196
        """Called to render the close tag."""
192
197
        pass
193
198
 
194
199
    def get_contents(self, parser):
196
201
        return parser.markup[self.open_pos:self.close_pos]
197
202
 
198
203
    def get_contents_text(self, parser):
199
 
        """Returns the string between the the open and close tag, minus bbcode tags."""
200
 
        return u"".join( parser.get_text_nodes(self.open_node_index, self.close_node_index) )
 
204
        """Returns the string between the the open and close tag, minus bbcode
 
205
        tags."""
 
206
        return u"".join(parser.get_text_nodes(self.open_node_index, self.close_node_index))
201
207
 
202
208
    def skip_contents(self, parser):
203
209
        """Skips the contents of a tag while rendering."""
204
210
        parser.skip_to_node(self.close_node_index)
205
211
 
206
212
    def __str__(self):
207
 
        return '[%s]'%self.name
 
213
        return '[%s]' % self.name
208
214
 
209
215
 
210
216
class SimpleTag(TagBase):
211
217
 
212
 
    """A tag that can be rendered with a simple substitution. """
 
218
    """A tag that can be rendered with a simple substitution."""
213
219
 
214
220
    def __init__(self, name, html_name, **kwargs):
215
221
        """ html_name -- the html tag to substitute."""
217
223
        self.html_name = html_name
218
224
 
219
225
    def render_open(self, parser, node_index):
220
 
        return u"<%s>"%self.html_name
 
226
        return u"<%s>" % self.html_name
221
227
 
222
228
    def render_close(self, parser, node_index):
223
 
        return u"</%s>"%self.html_name
 
229
        return u"</%s>" % self.html_name
224
230
 
225
231
 
226
232
class DivStyleTag(TagBase):
246
252
 
247
253
        self.annotate_links = annotate_links
248
254
 
249
 
 
250
255
    def render_open(self, parser, node_index):
251
256
 
252
257
        self.domain = u''
253
258
        tag_data = parser.tag_data
254
 
        nest_level = tag_data['link_nest_level'] = tag_data.setdefault('link_nest_level', 0) + 1
 
259
        nest_level = tag_data['link_nest_level'] = tag_data.setdefault(
 
260
            'link_nest_level', 0) + 1
255
261
 
256
262
        if nest_level > 1:
257
263
            return u""
261
267
        else:
262
268
            url = self.get_contents_text(parser).strip()
263
269
 
264
 
        self.domain = ""
265
 
        #Unquote the url
 
270
        self.domain = ''
 
271
        # Unquote the url
266
272
        self.url = unquote(url)
267
273
 
268
 
        #Disallow javascript links
 
274
        # Disallow javascript links
269
275
        if u"javascript:" in self.url.lower():
270
 
            return ""
 
276
            return ''
271
277
 
272
 
        #Disallow non http: links
 
278
        # Disallow non http: links
273
279
        url_parsed = urlparse(self.url)
274
280
        if url_parsed[0] and not url_parsed[0].lower().startswith(u'http'):
275
 
            return ""
 
281
            return ''
276
282
 
277
 
        #Prepend http: if it is not present
 
283
        # Prepend http: if it is not present
278
284
        if not url_parsed[0]:
279
 
            self.url="http://"+self.url
 
285
            self.url = 'http://' + self.url
280
286
            url_parsed = urlparse(self.url)
281
287
 
282
 
        #Get domain
 
288
        # Get domain
283
289
        self.domain = url_parsed[1].lower()
284
290
 
285
 
        #Remove www for brevity
 
291
        # Remove www for brevity
286
292
        if self.domain.startswith(u'www.'):
287
293
            self.domain = self.domain[4:]
288
294
 
289
 
        #Quote the url
 
295
        # Quote the url
290
296
        #self.url="http:"+urlunparse( map(quote, (u"",)+url_parsed[1:]) )
291
 
        self.url= unicode( urlunparse([quote(component.encode("utf-8"), safe='/=&?:+') for component in url_parsed]) )
 
297
        self.url = unicode(urlunparse(
 
298
            [quote(component.encode('utf-8'), safe='/=&?:+') for component in url_parsed]))
292
299
 
293
300
        if not self.url:
294
301
            return u""
295
302
 
296
303
        if self.domain:
297
 
            return u'<a href="%s">'%self.url
 
304
            return u'<a href="%s">' % self.url
298
305
        else:
299
306
            return u""
300
307
 
307
314
            return u''
308
315
 
309
316
        if self.domain:
310
 
            return u'</a>'+self.annotate_link(self.domain)
 
317
            return u'</a>' + self.annotate_link(self.domain)
311
318
        else:
312
319
            return u''
313
320
 
332
339
 
333
340
    def render_open(self, parser, node_index):
334
341
        if self.params:
335
 
            return u'<blockquote><em>%s</em><br/>'%(PostMarkup.standard_replace(self.params))
 
342
            return u'<blockquote><em>%s</em><br/>' % (PostMarkup.standard_replace(self.params))
336
343
        else:
337
344
            return u'<blockquote>'
338
345
 
339
 
 
340
346
    def render_close(self, parser, node_index):
341
347
        return u"</blockquote>"
342
348
 
343
349
 
344
350
class SearchTag(TagBase):
345
351
 
346
 
    def __init__(self, name, url, label="", annotate_links=True, **kwargs):
 
352
    def __init__(self, name, url, label='', annotate_links=True, **kwargs):
347
353
        TagBase.__init__(self, name, inline=True)
348
354
        self.url = url
349
355
        self.label = label
352
358
    def render_open(self, parser, node_idex):
353
359
 
354
360
        if self.params:
355
 
            search=self.params
 
361
            search = self.params
356
362
        else:
357
 
            search=self.get_contents(parser)
 
363
            search = self.get_contents(parser)
358
364
        link = u'<a href="%s">' % self.url
359
365
        if u'%' in link:
360
 
            return link%quote_plus(search.encode("UTF-8"))
 
366
            return link % quote_plus(search.encode('UTF-8'))
361
367
        else:
362
368
            return link
363
369
 
389
395
            contents = _escape(contents)
390
396
            return '<div class="code"><pre>%s</pre></div>' % contents
391
397
 
392
 
        formatter = HtmlFormatter(linenos=self.line_numbers, cssclass="code")
 
398
        formatter = HtmlFormatter(linenos=self.line_numbers, cssclass='code')
393
399
        return highlight(contents, lexer, formatter)
394
400
 
395
401
 
396
 
 
397
402
class CodeTag(TagBase):
398
403
 
399
404
    def __init__(self, name, **kwargs):
416
421
        contents = self.get_contents(parser)
417
422
        self.skip_contents(parser)
418
423
 
419
 
        contents = strip_bbcode(contents).replace(u'"', "%22")
 
424
        contents = strip_bbcode(contents).replace(u'"', '%22')
420
425
 
421
426
        return u'<img src="%s"></img>' % contents
422
427
 
432
437
    def close(self, parser, close_pos, node_index):
433
438
        TagBase.close(self, parser, close_pos, node_index)
434
439
 
435
 
 
436
440
    def render_open(self, parser, node_index):
437
441
 
438
442
        self.close_tag = u""
439
443
 
440
444
        tag_data = parser.tag_data
441
 
        tag_data.setdefault("ListTag.count", 0)
 
445
        tag_data.setdefault('ListTag.count', 0)
442
446
 
443
 
        if tag_data["ListTag.count"]:
 
447
        if tag_data['ListTag.count']:
444
448
            return u""
445
449
 
446
 
        tag_data["ListTag.count"] += 1
447
 
 
448
 
        tag_data["ListItemTag.initial_item"]=True
449
 
 
450
 
        if self.params == "1":
 
450
        tag_data['ListTag.count'] += 1
 
451
 
 
452
        tag_data['ListItemTag.initial_item'] = True
 
453
 
 
454
        if self.params == '1':
451
455
            self.close_tag = u"</li></ol>"
452
456
            return u"<ol><li>"
453
 
        elif self.params == "a":
 
457
        elif self.params == 'a':
454
458
            self.close_tag = u"</li></ol>"
455
459
            return u'<ol style="list-style-type: lower-alpha;"><li>'
456
 
        elif self.params == "A":
 
460
        elif self.params == 'A':
457
461
            self.close_tag = u"</li></ol>"
458
462
            return u'<ol style="list-style-type: upper-alpha;"><li>'
459
463
        else:
463
467
    def render_close(self, parser, node_index):
464
468
 
465
469
        tag_data = parser.tag_data
466
 
        tag_data["ListTag.count"] -= 1
 
470
        tag_data['ListTag.count'] -= 1
467
471
 
468
472
        return self.close_tag
469
473
 
477
481
    def render_open(self, parser, node_index):
478
482
 
479
483
        tag_data = parser.tag_data
480
 
        if not tag_data.setdefault("ListTag.count", 0):
 
484
        if not tag_data.setdefault('ListTag.count', 0):
481
485
            return u""
482
486
 
483
 
        if tag_data["ListItemTag.initial_item"]:
484
 
            tag_data["ListItemTag.initial_item"] = False
 
487
        if tag_data['ListItemTag.initial_item']:
 
488
            tag_data['ListItemTag.initial_item'] = False
485
489
            return
486
490
 
487
491
        return u"</li><li>"
489
493
 
490
494
class SizeTag(TagBase):
491
495
 
492
 
    valid_chars = frozenset("0123456789")
 
496
    valid_chars = frozenset('0123456789')
493
497
 
494
498
    def __init__(self, name, **kwargs):
495
499
        TagBase.__init__(self, name, inline=True)
497
501
    def render_open(self, parser, node_index):
498
502
 
499
503
        try:
500
 
            self.size = int( "".join([c for c in self.params if c in self.valid_chars]) )
 
504
            self.size = int(
 
505
                ''.join([c for c in self.params if c in self.valid_chars]))
501
506
        except ValueError:
502
507
            self.size = None
503
508
 
524
529
 
525
530
class ColorTag(TagBase):
526
531
 
527
 
    valid_chars = frozenset("#0123456789abcdefghijklmnopqrstuvwxyz")
 
532
    valid_chars = frozenset('#0123456789abcdefghijklmnopqrstuvwxyz')
528
533
 
529
534
    def __init__(self, name, **kwargs):
530
535
        TagBase.__init__(self, name, inline=True)
533
538
 
534
539
        valid_chars = self.valid_chars
535
540
        color = self.params.split()[0:1][0].lower()
536
 
        self.color = "".join([c for c in color if c in valid_chars])
 
541
        self.color = ''.join([c for c in color if c in valid_chars])
537
542
 
538
543
        if not self.color:
539
544
            return u""
553
558
 
554
559
        return u'<div style="text-align:center">'
555
560
 
556
 
 
557
561
    def render_close(self, parser, node_index):
558
562
 
559
563
        return u'</div>'
560
564
 
561
565
# http://effbot.org/zone/python-replace.htm
 
566
 
 
567
 
562
568
class MultiReplace:
563
569
 
564
570
    def __init__(self, repl_dict):
565
571
 
566
572
        # string to string mapping; use a regular expression
567
573
        keys = repl_dict.keys()
568
 
        keys.sort() # lexical order
569
 
        keys.reverse() # use longest match first
 
574
        keys.sort()  # lexical order
 
575
        keys.reverse()  # use longest match first
570
576
        pattern = u"|".join([re.escape(key) for key in keys])
571
577
        self.pattern = re.compile(pattern)
572
578
        self.dict = repl_dict
585
591
def _escape(s):
586
592
    return PostMarkup.standard_replace(s.rstrip('\n'))
587
593
 
 
594
 
588
595
def _escape_no_breaks(s):
589
596
    return PostMarkup.standard_replace_no_break(s.rstrip('\n'))
590
597
 
 
598
 
591
599
class TagFactory(object):
592
600
 
593
601
    def __init__(self):
596
604
 
597
605
    @classmethod
598
606
    def tag_factory_callable(cls, tag_class, name, *args, **kwargs):
599
 
        """
600
 
        Returns a callable that returns a new tag instance.
601
 
        """
 
607
        """Returns a callable that returns a new tag instance."""
602
608
        def make():
603
609
            return tag_class(name, *args, **kwargs)
604
610
 
605
611
        return make
606
612
 
607
 
 
608
613
    def add_tag(self, cls, name, *args, **kwargs):
609
614
 
610
615
        self.tags[name] = self.tag_factory_callable(cls, name, *args, **kwargs)
627
632
 
628
633
class _Parser(object):
629
634
 
630
 
    """ This is an interface to the parser, used by Tag classes. """
 
635
    """This is an interface to the parser, used by Tag classes."""
631
636
 
632
637
    def __init__(self, post_markup):
633
638
 
636
641
        self.render_node_index = 0
637
642
 
638
643
    def skip_to_node(self, node_index):
639
 
 
640
 
        """ Skips to a node, ignoring intermediate nodes. """
641
 
        assert node_index is not None, "Node index must be non-None"
 
644
        """Skips to a node, ignoring intermediate nodes."""
 
645
        assert node_index is not None, 'Node index must be non-None'
642
646
        self.render_node_index = node_index
643
647
 
644
648
    def get_text_nodes(self, node1, node2):
645
 
 
646
 
        """ Retrieves the text nodes between two node indices. """
 
649
        """Retrieves the text nodes between two node indices."""
647
650
 
648
651
        if node2 is None:
649
 
            node2 = node1+1
 
652
            node2 = node1 + 1
650
653
 
651
654
        return [node for node in self.nodes[node1:node2] if not callable(node)]
652
655
 
653
656
    def begin_no_breaks(self):
 
657
        """Disables replacing of newlines with break tags at the start and end
 
658
        of text nodes.
654
659
 
655
 
        """Disables replacing of newlines with break tags at the start and end of text nodes.
656
660
        Can only be called from a tags 'open' method.
657
661
 
658
662
        """
659
 
        assert self.phase==1, "Can not be called from render_open or render_close"
 
663
        assert self.phase == 1, 'Can not be called from render_open or render_close'
660
664
        self.no_breaks_count += 1
661
665
 
662
666
    def end_no_breaks(self):
663
 
 
664
667
        """Re-enables auto-replacing of newlines with break tags (see begin_no_breaks)."""
665
668
 
666
 
        assert self.phase==1, "Can not be called from render_open or render_close"
 
669
        assert self.phase == 1, 'Can not be called from render_open or render_close'
667
670
        if self.no_breaks_count:
668
671
            self.no_breaks_count -= 1
669
672
 
670
673
 
671
674
class PostMarkup(object):
672
675
 
673
 
    standard_replace = MultiReplace({   u'<':u'&lt;',
674
 
                                        u'>':u'&gt;',
675
 
                                        u'&':u'&amp;',
676
 
                                        u'\n':u'<br/>'})
 
676
    standard_replace = MultiReplace({u'<': u'&lt;',
 
677
                                     u'>': u'&gt;',
 
678
                                     u'&': u'&amp;',
 
679
                                     u'\n': u'<br/>'})
677
680
 
678
 
    standard_replace_no_break = MultiReplace({  u'<':u'&lt;',
679
 
                                                u'>':u'&gt;',
680
 
                                                u'&':u'&amp;',})
 
681
    standard_replace_no_break = MultiReplace({u'<': u'&lt;',
 
682
                                              u'>': u'&gt;',
 
683
                                              u'&': u'&amp;', })
681
684
 
682
685
    TOKEN_TAG, TOKEN_PTAG, TOKEN_TEXT = range(3)
683
686
 
684
 
 
685
687
    # I tried to use RE's. Really I did.
686
688
    @classmethod
687
689
    def tokenize(cls, post):
702
704
 
703
705
            brace_pos = post.find(u'[', pos)
704
706
            if brace_pos == -1:
705
 
                if pos<len(post):
 
707
                if pos < len(post):
706
708
                    yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
707
709
                return
708
710
            if brace_pos - pos > 0:
709
711
                yield PostMarkup.TOKEN_TEXT, post[pos:brace_pos], pos, brace_pos
710
712
 
711
713
            pos = brace_pos
712
 
            end_pos = pos+1
 
714
            end_pos = pos + 1
713
715
 
714
716
            open_tag_pos = post.find(u'[', end_pos)
715
717
            end_pos = find_first(post, end_pos, u']=')
724
726
                continue
725
727
 
726
728
            if post[end_pos] == ']':
727
 
                yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
728
 
                pos = end_pos+1
 
729
                yield PostMarkup.TOKEN_TAG, post[pos:end_pos + 1], pos, end_pos + 1
 
730
                pos = end_pos + 1
729
731
                continue
730
732
 
731
733
            if post[end_pos] == '=':
734
736
                    while post[end_pos] == ' ':
735
737
                        end_pos += 1
736
738
                    if post[end_pos] != '"':
737
 
                        end_pos = post.find(u']', end_pos+1)
 
739
                        end_pos = post.find(u']', end_pos + 1)
738
740
                        if end_pos == -1:
739
741
                            return
740
 
                        yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
 
742
                        yield PostMarkup.TOKEN_TAG, post[pos:end_pos + 1], pos, end_pos + 1
741
743
                    else:
742
744
                        end_pos = find_first(post, end_pos, u'"]')
743
745
 
744
 
                        if end_pos==-1:
 
746
                        if end_pos == -1:
745
747
                            return
746
748
                        if post[end_pos] == '"':
747
 
                            end_pos = post.find(u'"', end_pos+1)
748
 
                            if end_pos == -1:
749
 
                                return
750
 
                            end_pos = post.find(u']', end_pos+1)
751
 
                            if end_pos == -1:
752
 
                                return
753
 
                            yield PostMarkup.TOKEN_PTAG, post[pos:end_pos+1], pos, end_pos+1
 
749
                            end_pos = post.find(u'"', end_pos + 1)
 
750
                            if end_pos == -1:
 
751
                                return
 
752
                            end_pos = post.find(u']', end_pos + 1)
 
753
                            if end_pos == -1:
 
754
                                return
 
755
                            yield PostMarkup.TOKEN_PTAG, post[pos:end_pos + 1], pos, end_pos + 1
754
756
                        else:
755
 
                            yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos
756
 
                    pos = end_pos+1
 
757
                            yield PostMarkup.TOKEN_TAG, post[pos:end_pos + 1], pos, end_pos
 
758
                    pos = end_pos + 1
757
759
                except IndexError:
758
760
                    return
759
761
 
760
 
    def tagify_urls(self, postmarkup ):
761
 
 
762
 
        """ Surrounds urls with url bbcode tags. """
 
762
    def tagify_urls(self, postmarkup):
 
763
        """Surrounds urls with url bbcode tags."""
763
764
 
764
765
        def repl(match):
765
766
            return u'[url]%s[/url]' % match.group(0)
774
775
 
775
776
        return u"".join(text_tokens)
776
777
 
777
 
 
778
778
    def __init__(self, tag_factory=None):
779
779
 
780
780
        self.tag_factory = tag_factory or TagFactory()
781
781
 
782
 
 
783
782
    def default_tags(self):
784
 
 
785
 
        """ Add some basic tags. """
 
783
        """Add some basic tags."""
786
784
 
787
785
        add_tag = self.tag_factory.add_tag
788
786
 
791
789
        add_tag(SimpleTag, u'u', u'u')
792
790
        add_tag(SimpleTag, u's', u's')
793
791
 
794
 
 
795
792
    def get_supported_tags(self):
796
 
 
797
 
        """ Returns a list of the supported tags. """
 
793
        """Returns a list of the supported tags."""
798
794
 
799
795
        return sorted(self.tag_factory.tags.keys())
800
796
 
801
 
 
802
 
 
803
797
    def render_to_html(self,
804
798
                       post_markup,
805
 
                       encoding="ascii",
 
799
                       encoding='ascii',
806
800
                       exclude_tags=None,
807
801
                       auto_urls=True):
808
 
 
809
802
        """Converts Post Markup to XHTML.
810
803
 
811
804
        post_markup -- String containing bbcode.
829
822
 
830
823
        tag_factory = self.tag_factory
831
824
 
832
 
 
833
825
        nodes = []
834
826
        parser.nodes = nodes
835
827
 
930
922
                end_tag = True
931
923
                tag_name = tag_name[1:]
932
924
 
933
 
 
934
925
            if enclosed_count and tag_stack[-1].name != tag_name:
935
926
                continue
936
927
 
957
948
 
958
949
                if tag.auto_close:
959
950
                    tag = tag_stack.pop()
960
 
                    tag.close(self, start_pos, len(nodes)-1)
 
951
                    tag.close(self, start_pos, len(nodes) - 1)
961
952
                    close_tag(tag)
962
953
 
963
954
            else:
1010
1001
    __call__ = render_to_html
1011
1002
 
1012
1003
 
1013
 
 
1014
 
 
1015
 
 
1016
1004
def _tests():
1017
1005
 
1018
1006
    import sys
1027
1015
    tests.append('[')
1028
1016
    tests.append(':-[ Hello, [b]World[/b]')
1029
1017
 
1030
 
    tests.append("[link=http://www.willmcgugan.com]My homepage[/link]")
 
1018
    tests.append('[link=http://www.willmcgugan.com]My homepage[/link]')
1031
1019
    tests.append('[link="http://www.willmcgugan.com"]My homepage[/link]')
1032
 
    tests.append("[link http://www.willmcgugan.com]My homepage[/link]")
1033
 
    tests.append("[link]http://www.willmcgugan.com[/link]")
 
1020
    tests.append('[link http://www.willmcgugan.com]My homepage[/link]')
 
1021
    tests.append('[link]http://www.willmcgugan.com[/link]')
1034
1022
 
1035
1023
    tests.append(u"[b]Hello AndrУЉ[/b]")
1036
1024
    tests.append(u"[google]AndrУЉ[/google]")
1037
 
    tests.append("[s]Strike through[/s]")
1038
 
    tests.append("[b]bold [i]bold and italic[/b] italic[/i]")
1039
 
    tests.append("[google]Will McGugan[/google]")
1040
 
    tests.append("[wiki Will McGugan]Look up my name in Wikipedia[/wiki]")
 
1025
    tests.append('[s]Strike through[/s]')
 
1026
    tests.append('[b]bold [i]bold and italic[/b] italic[/i]')
 
1027
    tests.append('[google]Will McGugan[/google]')
 
1028
    tests.append('[wiki Will McGugan]Look up my name in Wikipedia[/wiki]')
1041
1029
 
1042
 
    tests.append("[quote Will said...]BBCode is very cool[/quote]")
 
1030
    tests.append('[quote Will said...]BBCode is very cool[/quote]')
1043
1031
 
1044
1032
    tests.append("""[code python]
1045
1033
# A proxy object that calls a callback when converted to a string
1054
1042
        return self.__str__()
1055
1043
[/code]""")
1056
1044
 
1057
 
 
1058
 
    tests.append(u"[img]http://upload.wikimedia.org/wikipedia/commons"\
1059
 
                 "/6/61/Triops_longicaudatus.jpg[/img]")
1060
 
 
1061
 
    tests.append("[list][*]Apples[*]Oranges[*]Pears[/list]")
 
1045
    tests.append(u"[img]http://upload.wikimedia.org/wikipedia/commons"
 
1046
                 '/6/61/Triops_longicaudatus.jpg[/img]')
 
1047
 
 
1048
    tests.append('[list][*]Apples[*]Oranges[*]Pears[/list]')
1062
1049
    tests.append("""[list=1]
1063
1050
    [*]Apples
1064
1051
    [*]Oranges
1065
1052
    are not the only fruit
1066
1053
    [*]Pears
1067
1054
[/list]""")
1068
 
    tests.append("[list=a][*]Apples[*]Oranges[*]Pears[/list]")
1069
 
    tests.append("[list=A][*]Apples[*]Oranges[*]Pears[/list]")
 
1055
    tests.append('[list=a][*]Apples[*]Oranges[*]Pears[/list]')
 
1056
    tests.append('[list=A][*]Apples[*]Oranges[*]Pears[/list]')
1070
1057
 
1071
 
    long_test="""[b]Long test[/b]
 
1058
    long_test = """[b]Long test[/b]
1072
1059
 
1073
1060
New lines characters are converted to breaks."""\
1074
1061
"""Tags my be [b]ove[i]rl[/b]apped[/i].
1078
1065
 
1079
1066
    tests.append(long_test)
1080
1067
 
1081
 
    tests.append("[dict]Will[/dict]")
1082
 
 
1083
 
    tests.append("[code unknownlanguage]10 print 'In yr code'; 20 goto 10[/code]")
1084
 
 
1085
 
    tests.append("[url=http://www.google.com/coop/cse?cx=006850030468302103399%3Amqxv78bdfdo]CakePHP Google Groups[/url]")
1086
 
    tests.append("[url=http://www.google.com/search?hl=en&safe=off&client=opera&rls=en&hs=pO1&q=python+bbcode&btnG=Search]Search for Python BBCode[/url]")
 
1068
    tests.append('[dict]Will[/dict]')
 
1069
 
 
1070
    tests.append(
 
1071
        "[code unknownlanguage]10 print 'In yr code'; 20 goto 10[/code]")
 
1072
 
 
1073
    tests.append(
 
1074
        '[url=http://www.google.com/coop/cse?cx=006850030468302103399%3Amqxv78bdfdo]CakePHP Google Groups[/url]')
 
1075
    tests.append('[url=http://www.google.com/search?hl=en&safe=off&client=opera&rls=en&hs=pO1&q=python+bbcode&btnG=Search]Search for Python BBCode[/url]')
1087
1076
    #tests = []
1088
1077
    # Attempt to inject html in to unicode
1089
 
    tests.append("[url=http://www.test.com/sfsdfsdf/ter?t=\"></a><h1>HACK</h1><a>\"]Test Hack[/url]")
 
1078
    tests.append(
 
1079
        "[url=http://www.test.com/sfsdfsdf/ter?t=\"></a><h1>HACK</h1><a>\"]Test Hack[/url]")
1090
1080
 
1091
 
    tests.append('Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
 
1081
    tests.append(
 
1082
        'Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1092
1083
 
1093
1084
    tests.append(u'[google]ЩИЮВfvЮИУАsz[/google]')
1094
1085
 
1098
1089
    tests.append(u'[color #0f0]This should be green[/color]')
1099
1090
    tests.append(u"[center]This should be in the center!")
1100
1091
 
1101
 
    tests.append('Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
 
1092
    tests.append(
 
1093
        'Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1102
1094
 
1103
1095
    #tests = []
1104
1096
    tests.append('[b]Hello, [i]World[/b]! [/i]')
1107
1099
 
1108
1100
    tests.append('[list][*]Hello[i][*]World![/i][/list]')
1109
1101
 
1110
 
 
1111
1102
    tests.append("""[list=1]
1112
1103
    [*]Apples
1113
1104
    [*]Oranges
1115
1106
    [*]Pears
1116
1107
[/list]""")
1117
1108
 
1118
 
    tests.append("[b]urls such as http://www.willmcgugan.com are authomaticaly converted to links[/b]")
 
1109
    tests.append(
 
1110
        '[b]urls such as http://www.willmcgugan.com are authomaticaly converted to links[/b]')
1119
1111
 
1120
1112
    tests.append("""
1121
1113
[b]
1130
1122
[*]World
1131
1123
[/list]""")
1132
1124
 
1133
 
 
1134
 
 
1135
 
    #tests=["""[b]b[i]i[/b][/i]"""]
 
1125
    # tests=["""[b]b[i]i[/b][/i]"""]
1136
1126
 
1137
1127
    for test in tests:
1138
 
        print u"<pre>%s</pre>"%str(test.encode("ascii", "xmlcharrefreplace"))
1139
 
        print u"<p>%s</p>"%str(post_markup(test).encode("ascii", "xmlcharrefreplace"))
 
1128
        print u"<pre>%s</pre>" % str(test.encode('ascii', 'xmlcharrefreplace'))
 
1129
        print u"<p>%s</p>" % str(post_markup(test).encode('ascii', 'xmlcharrefreplace'))
1140
1130
        print u"<hr/>"
1141
1131
        print
1142
1132
 
1145
1135
    print repr(post_markup('http://www.google.com/search?as_q=bbcode&btnG=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA'))
1146
1136
 
1147
1137
    p = create(use_pygments=False)
1148
 
    print (p('[code]foo\nbar[/code]'))
 
1138
    print(p('[code]foo\nbar[/code]'))
1149
1139
 
1150
 
    #print render_bbcode("[b]For the lazy, use the http://www.willmcgugan.com render_bbcode function.[/b]")
 
1140
    # print render_bbcode("[b]For the lazy, use the http://www.willmcgugan.com
 
1141
    # render_bbcode function.[/b]")
1151
1142
 
1152
1143
 
1153
1144
def _run_unittests():
1162
1153
 
1163
1154
            postmarkup = create()
1164
1155
 
1165
 
            tests= [ ('[b]Hello[/b]', "<strong>Hello</strong>"),
1166
 
                     ('[i]Italic[/i]', "<em>Italic</em>"),
1167
 
                     ('[s]Strike[/s]', "<strike>Strike</strike>"),
1168
 
                     ('[u]underlined[/u]', "<u>underlined</u>"),
 
1156
            tests = [('[b]Hello[/b]', '<strong>Hello</strong>'),
 
1157
                     ('[i]Italic[/i]', '<em>Italic</em>'),
 
1158
                     ('[s]Strike[/s]', '<strike>Strike</strike>'),
 
1159
                     ('[u]underlined[/u]', '<u>underlined</u>'),
1169
1160
                     ]
1170
1161
 
1171
1162
            for test, result in tests:
1172
1163
                self.assertEqual(postmarkup(test), result)
1173
1164
 
1174
 
 
1175
1165
        def testoverlap(self):
1176
1166
 
1177
1167
            postmarkup = create()
1178
1168
 
1179
 
            tests= [ ('[i][b]Hello[/i][/b]', "<em><strong>Hello</strong></em>"),
1180
 
                     ('[b]bold [u]both[/b] underline[/u]', '<strong>bold <u>both</u></strong><u> underline</u>')
 
1169
            tests = [('[i][b]Hello[/i][/b]', '<em><strong>Hello</strong></em>'),
 
1170
                     ('[b]bold [u]both[/b] underline[/u]',
 
1171
                      '<strong>bold <u>both</u></strong><u> underline</u>')
1181
1172
                     ]
1182
1173
 
1183
1174
            for test, result in tests:
1187
1178
 
1188
1179
            postmarkup = create(annotate_links=False)
1189
1180
 
1190
 
            tests= [ ('[link=http://www.willmcgugan.com]blog1[/link]', '<a href="http://www.willmcgugan.com">blog1</a>'),
1191
 
                     ('[link="http://www.willmcgugan.com"]blog2[/link]', '<a href="http://www.willmcgugan.com">blog2</a>'),
1192
 
                     ('[link http://www.willmcgugan.com]blog3[/link]', '<a href="http://www.willmcgugan.com">blog3</a>'),
1193
 
                     ('[link]http://www.willmcgugan.com[/link]', '<a href="http://www.willmcgugan.com">http://www.willmcgugan.com</a>')
 
1181
            tests = [('[link=http://www.willmcgugan.com]blog1[/link]', '<a href="http://www.willmcgugan.com">blog1</a>'),
 
1182
                     ('[link="http://www.willmcgugan.com"]blog2[/link]',
 
1183
                      '<a href="http://www.willmcgugan.com">blog2</a>'),
 
1184
                     ('[link http://www.willmcgugan.com]blog3[/link]',
 
1185
                      '<a href="http://www.willmcgugan.com">blog3</a>'),
 
1186
                     ('[link]http://www.willmcgugan.com[/link]',
 
1187
                      '<a href="http://www.willmcgugan.com">http://www.willmcgugan.com</a>')
1194
1188
                     ]
1195
1189
 
1196
1190
            for test, result in tests:
1197
1191
                self.assertEqual(postmarkup(test), result)
1198
1192
 
1199
 
 
1200
1193
    suite = unittest.TestLoader().loadTestsFromTestCase(TestPostmarkup)
1201
1194
    unittest.TextTestRunner(verbosity=2).run(suite)
1202
1195
 
1203
1196
 
1204
 
 
1205
 
 
1206
 
if __name__ == "__main__":
 
1197
if __name__ == '__main__':
1207
1198
 
1208
1199
    _tests()
1209
1200
    _run_unittests()