~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to pybb/markups/postmarkup.py

  • Committer: Holger Rapp
  • Date: 2010-01-01 21:35:23 UTC
  • mto: (173.3.2 widelands)
  • mto: This revision was merged to the branch mainline in revision 176.
  • Revision ID: rapp@mrt.uka.de-20100101213523-53rcapbemm69ep6u
Made the site compatible to django 1.1 and all the various packages

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: UTF-8 -*-
2
 
 
3
 
"""
4
 
Post Markup
5
 
Author: Will McGugan (http://www.willmcgugan.com)
6
 
"""
7
 
 
8
 
__version__ = "1.1.3"
9
 
 
10
 
import re
11
 
from urllib import quote, unquote, quote_plus
12
 
from urlparse import urlparse, urlunparse
13
 
 
14
 
pygments_available = True
15
 
try:
16
 
    from pygments import highlight
17
 
    from pygments.lexers import get_lexer_by_name, ClassNotFound
18
 
    from pygments.formatters import HtmlFormatter
19
 
except ImportError:
20
 
    # Make Pygments optional
21
 
    pygments_available = False
22
 
 
23
 
 
24
 
 
25
 
def annotate_link(domain):
26
 
    """This function is called by the url tag. Override to disable or change behaviour.
27
 
 
28
 
    domain -- Domain parsed from url
29
 
 
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('<.*?>|\&.*?\;')
38
 
def textilize(s):
39
 
    """Remove markup from html"""
40
 
    return re_html.sub("", s)
41
 
 
42
 
re_excerpt = re.compile(r'\[".*?\]+?.*?\[/".*?\]+?', re.DOTALL)
43
 
re_remove_markup = re.compile(r'\[.*?\]', re.DOTALL)
44
 
 
45
 
def remove_markup(post):
46
 
    """Removes html tags from a string."""
47
 
    return re_remove_markup.sub("", post)
48
 
 
49
 
def get_excerpt(post):
50
 
    """Returns an excerpt between ["] and [/"]
51
 
 
52
 
    post -- BBCode string"""
53
 
 
54
 
    match = re_excerpt.search(post)
55
 
    if match is None:
56
 
        return ""
57
 
    excerpt = match.group(0)
58
 
    excerpt = excerpt.replace(u'\n', u"<br/>")
59
 
    return remove_markup(excerpt)
60
 
 
61
 
def strip_bbcode(bbcode):
62
 
 
63
 
    """ Strips bbcode tags from a string.
64
 
 
65
 
    bbcode -- A string to remove tags from
66
 
 
67
 
    """
68
 
 
69
 
    return u"".join([t[1] for t in PostMarkup.tokenize(bbcode) if t[0] == PostMarkup.TOKEN_TEXT])
70
 
 
71
 
 
72
 
def create(include=None, exclude=None, use_pygments=True, **kwargs):
73
 
 
74
 
    """Create a postmarkup object that converts bbcode to XML snippets.
75
 
 
76
 
    include -- List or similar iterable containing the names of the tags to use
77
 
               If omitted, all tags will be used
78
 
    exclude -- List or similar iterable containing the names of the tags to exclude.
79
 
               If omitted, no tags will be excluded
80
 
    use_pygments -- If True, Pygments (http://pygments.org/) will be used for the code tag,
81
 
                    otherwise it will use <pre>code</pre>
82
 
    """
83
 
 
84
 
    postmarkup = PostMarkup()
85
 
    postmarkup_add_tag = postmarkup.tag_factory.add_tag
86
 
 
87
 
    def add_tag(tag_class, name, *args, **kwargs):
88
 
        if include is None or name in include:
89
 
            if exclude is not None and name in exclude:
90
 
                return
91
 
            postmarkup_add_tag(tag_class, name, *args, **kwargs)
92
 
 
93
 
 
94
 
 
95
 
    add_tag(SimpleTag, 'b', 'strong')
96
 
    add_tag(SimpleTag, 'i', 'em')
97
 
    add_tag(SimpleTag, 'u', 'u')
98
 
    add_tag(SimpleTag, 's', 'strike')
99
 
 
100
 
    add_tag(LinkTag, 'link', **kwargs)
101
 
    add_tag(LinkTag, 'url', **kwargs)
102
 
 
103
 
    add_tag(QuoteTag, 'quote')
104
 
 
105
 
    add_tag(SearchTag, u'wiki',
106
 
            u"http://en.wikipedia.org/wiki/Special:Search?search=%s", u'wikipedia.com', **kwargs)
107
 
    add_tag(SearchTag, u'google',
108
 
            u"http://www.google.com/search?hl=en&q=%s&btnG=Google+Search", u'google.com', **kwargs)
109
 
    add_tag(SearchTag, u'dictionary',
110
 
            u"http://dictionary.reference.com/browse/%s", u'dictionary.com', **kwargs)
111
 
    add_tag(SearchTag, u'dict',
112
 
            u"http://dictionary.reference.com/browse/%s", u'dictionary.com', **kwargs)
113
 
 
114
 
    add_tag(ImgTag, u'img')
115
 
    add_tag(ListTag, u'list')
116
 
    add_tag(ListItemTag, u'*')
117
 
 
118
 
    add_tag(SizeTag, u"size")
119
 
    add_tag(ColorTag, u"color")
120
 
    add_tag(CenterTag, u"center")
121
 
 
122
 
    if use_pygments:
123
 
        assert pygments_available, "Install Pygments (http://pygments.org/) or call create with use_pygments=False"
124
 
        add_tag(PygmentsCodeTag, u'code', **kwargs)
125
 
    else:
126
 
        add_tag(CodeTag, u'code', **kwargs)
127
 
 
128
 
    return postmarkup
129
 
 
130
 
 
131
 
 
132
 
_postmarkup = None
133
 
def render_bbcode(bbcode, encoding="ascii", exclude_tags=None, auto_urls=True):
134
 
 
135
 
    """Renders a bbcode string in to XHTML. This is a shortcut if you don't
136
 
    need to customize any tags.
137
 
 
138
 
    bbcode -- A string containing the bbcode
139
 
    encoding -- If bbcode is not unicode, then then it will be encoded with
140
 
    this encoding (defaults to 'ascii'). Ignore the encoding if you already have
141
 
    a unicode string
142
 
 
143
 
    """
144
 
 
145
 
    global _postmarkup
146
 
    if _postmarkup is None:
147
 
        _postmarkup = create(use_pygments=pygments_available)
148
 
 
149
 
    return _postmarkup(bbcode, encoding, exclude_tags=exclude_tags, auto_urls=auto_urls)
150
 
 
151
 
 
152
 
class TagBase(object):
153
 
 
154
 
    def __init__(self, name, enclosed=False, auto_close=False, inline=False, strip_first_newline=False, **kwargs):
155
 
        """Base class for all tags.
156
 
 
157
 
        name -- The name of the bbcode tag
158
 
        enclosed -- True if the contents of the tag should not be bbcode processed.
159
 
        auto_close -- True if the tag is standalone and does not require a close tag.
160
 
        inline -- True if the tag generates an inline html tag.
161
 
 
162
 
        """
163
 
 
164
 
        self.name = name
165
 
        self.enclosed = enclosed
166
 
        self.auto_close = auto_close
167
 
        self.inline = inline
168
 
        self.strip_first_newline = strip_first_newline
169
 
 
170
 
        self.open_pos = None
171
 
        self.close_pos = None
172
 
        self.open_node_index = None
173
 
        self.close_node_index = None
174
 
 
175
 
    def open(self, parser, params, open_pos, node_index):
176
 
        """ Called when the open tag is initially encountered. """
177
 
        self.params = params
178
 
        self.open_pos = open_pos
179
 
        self.open_node_index = node_index
180
 
 
181
 
    def close(self, parser, close_pos, node_index):
182
 
        """ Called when the close tag is initially encountered. """
183
 
        self.close_pos = close_pos
184
 
        self.close_node_index = node_index
185
 
 
186
 
    def render_open(self, parser, node_index):
187
 
        """ Called to render the open tag. """
188
 
        pass
189
 
 
190
 
    def render_close(self, parser, node_index):
191
 
        """ Called to render the close tag. """
192
 
        pass
193
 
 
194
 
    def get_contents(self, parser):
195
 
        """Returns the string between the open and close tag."""
196
 
        return parser.markup[self.open_pos:self.close_pos]
197
 
 
198
 
    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) )
201
 
 
202
 
    def skip_contents(self, parser):
203
 
        """Skips the contents of a tag while rendering."""
204
 
        parser.skip_to_node(self.close_node_index)
205
 
 
206
 
    def __str__(self):
207
 
        return '[%s]'%self.name
208
 
 
209
 
 
210
 
class SimpleTag(TagBase):
211
 
 
212
 
    """A tag that can be rendered with a simple substitution. """
213
 
 
214
 
    def __init__(self, name, html_name, **kwargs):
215
 
        """ html_name -- the html tag to substitute."""
216
 
        TagBase.__init__(self, name, inline=True)
217
 
        self.html_name = html_name
218
 
 
219
 
    def render_open(self, parser, node_index):
220
 
        return u"<%s>"%self.html_name
221
 
 
222
 
    def render_close(self, parser, node_index):
223
 
        return u"</%s>"%self.html_name
224
 
 
225
 
 
226
 
class DivStyleTag(TagBase):
227
 
 
228
 
    """A simple tag that is replaces with a div and a style."""
229
 
 
230
 
    def __init__(self, name, style, value, **kwargs):
231
 
        TagBase.__init__(self, name)
232
 
        self.style = style
233
 
        self.value = value
234
 
 
235
 
    def render_open(self, parser, node_index):
236
 
        return u'<div style="%s:%s;">' % (self.style, self.value)
237
 
 
238
 
    def render_close(self, parser, node_index):
239
 
        return u'</div>'
240
 
 
241
 
 
242
 
class LinkTag(TagBase):
243
 
 
244
 
    def __init__(self, name, annotate_links=True, **kwargs):
245
 
        TagBase.__init__(self, name, inline=True)
246
 
 
247
 
        self.annotate_links = annotate_links
248
 
 
249
 
 
250
 
    def render_open(self, parser, node_index):
251
 
 
252
 
        self.domain = u''
253
 
        tag_data = parser.tag_data
254
 
        nest_level = tag_data['link_nest_level'] = tag_data.setdefault('link_nest_level', 0) + 1
255
 
 
256
 
        if nest_level > 1:
257
 
            return u""
258
 
 
259
 
        if self.params:
260
 
            url = self.params.strip()
261
 
        else:
262
 
            url = self.get_contents_text(parser).strip()
263
 
 
264
 
        self.domain = ""
265
 
        #Unquote the url
266
 
        self.url = unquote(url)
267
 
 
268
 
        #Disallow javascript links
269
 
        if u"javascript:" in self.url.lower():
270
 
            return ""
271
 
 
272
 
        #Disallow non http: links
273
 
        url_parsed = urlparse(self.url)
274
 
        if url_parsed[0] and not url_parsed[0].lower().startswith(u'http'):
275
 
            return ""
276
 
 
277
 
        #Prepend http: if it is not present
278
 
        if not url_parsed[0]:
279
 
            self.url="http://"+self.url
280
 
            url_parsed = urlparse(self.url)
281
 
 
282
 
        #Get domain
283
 
        self.domain = url_parsed[1].lower()
284
 
 
285
 
        #Remove www for brevity
286
 
        if self.domain.startswith(u'www.'):
287
 
            self.domain = self.domain[4:]
288
 
 
289
 
        #Quote the url
290
 
        #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]) )
292
 
 
293
 
        if not self.url:
294
 
            return u""
295
 
 
296
 
        if self.domain:
297
 
            return u'<a href="%s">'%self.url
298
 
        else:
299
 
            return u""
300
 
 
301
 
    def render_close(self, parser, node_index):
302
 
 
303
 
        tag_data = parser.tag_data
304
 
        tag_data['link_nest_level'] -= 1
305
 
 
306
 
        if tag_data['link_nest_level'] > 0:
307
 
            return u''
308
 
 
309
 
        if self.domain:
310
 
            return u'</a>'+self.annotate_link(self.domain)
311
 
        else:
312
 
            return u''
313
 
 
314
 
    def annotate_link(self, domain=None):
315
 
 
316
 
        if domain and self.annotate_links:
317
 
            return annotate_link(domain)
318
 
        else:
319
 
            return u""
320
 
 
321
 
 
322
 
class QuoteTag(TagBase):
323
 
 
324
 
    def __init__(self, name, **kwargs):
325
 
        TagBase.__init__(self, name, strip_first_newline=True)
326
 
 
327
 
    def open(self, parser, *args):
328
 
        TagBase.open(self, parser, *args)
329
 
 
330
 
    def close(self, parser, *args):
331
 
        TagBase.close(self, parser, *args)
332
 
 
333
 
    def render_open(self, parser, node_index):
334
 
        if self.params:
335
 
            return u'<blockquote><em>%s</em><br/>'%(PostMarkup.standard_replace(self.params))
336
 
        else:
337
 
            return u'<blockquote>'
338
 
 
339
 
 
340
 
    def render_close(self, parser, node_index):
341
 
        return u"</blockquote>"
342
 
 
343
 
 
344
 
class SearchTag(TagBase):
345
 
 
346
 
    def __init__(self, name, url, label="", annotate_links=True, **kwargs):
347
 
        TagBase.__init__(self, name, inline=True)
348
 
        self.url = url
349
 
        self.label = label
350
 
        self.annotate_links = annotate_links
351
 
 
352
 
    def render_open(self, parser, node_idex):
353
 
 
354
 
        if self.params:
355
 
            search=self.params
356
 
        else:
357
 
            search=self.get_contents(parser)
358
 
        link = u'<a href="%s">' % self.url
359
 
        if u'%' in link:
360
 
            return link%quote_plus(search.encode("UTF-8"))
361
 
        else:
362
 
            return link
363
 
 
364
 
    def render_close(self, parser, node_index):
365
 
 
366
 
        if self.label:
367
 
            ret = u'</a>'
368
 
            if self.annotate_links:
369
 
                ret += annotate_link(self.label)
370
 
            return ret
371
 
        else:
372
 
            return u''
373
 
 
374
 
 
375
 
class PygmentsCodeTag(TagBase):
376
 
 
377
 
    def __init__(self, name, pygments_line_numbers=False, **kwargs):
378
 
        TagBase.__init__(self, name, enclosed=True, strip_first_newline=True)
379
 
        self.line_numbers = pygments_line_numbers
380
 
 
381
 
    def render_open(self, parser, node_index):
382
 
 
383
 
        contents = self.get_contents(parser)
384
 
        self.skip_contents(parser)
385
 
 
386
 
        try:
387
 
            lexer = get_lexer_by_name(self.params, stripall=True)
388
 
        except ClassNotFound:
389
 
            contents = _escape(contents)
390
 
            return '<div class="code"><pre>%s</pre></div>' % contents
391
 
 
392
 
        formatter = HtmlFormatter(linenos=self.line_numbers, cssclass="code")
393
 
        return highlight(contents, lexer, formatter)
394
 
 
395
 
 
396
 
 
397
 
class CodeTag(TagBase):
398
 
 
399
 
    def __init__(self, name, **kwargs):
400
 
        TagBase.__init__(self, name, enclosed=True, strip_first_newline=True)
401
 
 
402
 
    def render_open(self, parser, node_index):
403
 
 
404
 
        contents = _escape_no_breaks(self.get_contents(parser))
405
 
        self.skip_contents(parser)
406
 
        return '<div class="code"><pre>%s</pre></div>' % contents
407
 
 
408
 
 
409
 
class ImgTag(TagBase):
410
 
 
411
 
    def __init__(self, name, **kwargs):
412
 
        TagBase.__init__(self, name, inline=True)
413
 
 
414
 
    def render_open(self, parser, node_index):
415
 
 
416
 
        contents = self.get_contents(parser)
417
 
        self.skip_contents(parser)
418
 
 
419
 
        contents = strip_bbcode(contents).replace(u'"', "%22")
420
 
 
421
 
        return u'<img src="%s"></img>' % contents
422
 
 
423
 
 
424
 
class ListTag(TagBase):
425
 
 
426
 
    def __init__(self, name,  **kwargs):
427
 
        TagBase.__init__(self, name, strip_first_newline=True)
428
 
 
429
 
    def open(self, parser, params, open_pos, node_index):
430
 
        TagBase.open(self, parser, params, open_pos, node_index)
431
 
 
432
 
    def close(self, parser, close_pos, node_index):
433
 
        TagBase.close(self, parser, close_pos, node_index)
434
 
 
435
 
 
436
 
    def render_open(self, parser, node_index):
437
 
 
438
 
        self.close_tag = u""
439
 
 
440
 
        tag_data = parser.tag_data
441
 
        tag_data.setdefault("ListTag.count", 0)
442
 
 
443
 
        if tag_data["ListTag.count"]:
444
 
            return u""
445
 
 
446
 
        tag_data["ListTag.count"] += 1
447
 
 
448
 
        tag_data["ListItemTag.initial_item"]=True
449
 
 
450
 
        if self.params == "1":
451
 
            self.close_tag = u"</li></ol>"
452
 
            return u"<ol><li>"
453
 
        elif self.params == "a":
454
 
            self.close_tag = u"</li></ol>"
455
 
            return u'<ol style="list-style-type: lower-alpha;"><li>'
456
 
        elif self.params == "A":
457
 
            self.close_tag = u"</li></ol>"
458
 
            return u'<ol style="list-style-type: upper-alpha;"><li>'
459
 
        else:
460
 
            self.close_tag = u"</li></ul>"
461
 
            return u"<ul><li>"
462
 
 
463
 
    def render_close(self, parser, node_index):
464
 
 
465
 
        tag_data = parser.tag_data
466
 
        tag_data["ListTag.count"] -= 1
467
 
 
468
 
        return self.close_tag
469
 
 
470
 
 
471
 
class ListItemTag(TagBase):
472
 
 
473
 
    def __init__(self, name, **kwargs):
474
 
        TagBase.__init__(self, name)
475
 
        self.closed = False
476
 
 
477
 
    def render_open(self, parser, node_index):
478
 
 
479
 
        tag_data = parser.tag_data
480
 
        if not tag_data.setdefault("ListTag.count", 0):
481
 
            return u""
482
 
 
483
 
        if tag_data["ListItemTag.initial_item"]:
484
 
            tag_data["ListItemTag.initial_item"] = False
485
 
            return
486
 
 
487
 
        return u"</li><li>"
488
 
 
489
 
 
490
 
class SizeTag(TagBase):
491
 
 
492
 
    valid_chars = frozenset("0123456789")
493
 
 
494
 
    def __init__(self, name, **kwargs):
495
 
        TagBase.__init__(self, name, inline=True)
496
 
 
497
 
    def render_open(self, parser, node_index):
498
 
 
499
 
        try:
500
 
            self.size = int( "".join([c for c in self.params if c in self.valid_chars]) )
501
 
        except ValueError:
502
 
            self.size = None
503
 
 
504
 
        if self.size is None:
505
 
            return u""
506
 
 
507
 
        self.size = self.validate_size(self.size)
508
 
 
509
 
        return u'<span style="font-size:%spx">' % self.size
510
 
 
511
 
    def render_close(self, parser, node_index):
512
 
 
513
 
        if self.size is None:
514
 
            return u""
515
 
 
516
 
        return u'</span>'
517
 
 
518
 
    def validate_size(self, size):
519
 
 
520
 
        size = min(64, size)
521
 
        size = max(4, size)
522
 
        return size
523
 
 
524
 
 
525
 
class ColorTag(TagBase):
526
 
 
527
 
    valid_chars = frozenset("#0123456789abcdefghijklmnopqrstuvwxyz")
528
 
 
529
 
    def __init__(self, name, **kwargs):
530
 
        TagBase.__init__(self, name, inline=True)
531
 
 
532
 
    def render_open(self, parser, node_index):
533
 
 
534
 
        valid_chars = self.valid_chars
535
 
        color = self.params.split()[0:1][0].lower()
536
 
        self.color = "".join([c for c in color if c in valid_chars])
537
 
 
538
 
        if not self.color:
539
 
            return u""
540
 
 
541
 
        return u'<span style="color:%s">' % self.color
542
 
 
543
 
    def render_close(self, parser, node_index):
544
 
 
545
 
        if not self.color:
546
 
            return u''
547
 
        return u'</span>'
548
 
 
549
 
 
550
 
class CenterTag(TagBase):
551
 
 
552
 
    def render_open(self, parser, node_index, **kwargs):
553
 
 
554
 
        return u'<div style="text-align:center">'
555
 
 
556
 
 
557
 
    def render_close(self, parser, node_index):
558
 
 
559
 
        return u'</div>'
560
 
 
561
 
# http://effbot.org/zone/python-replace.htm
562
 
class MultiReplace:
563
 
 
564
 
    def __init__(self, repl_dict):
565
 
 
566
 
        # string to string mapping; use a regular expression
567
 
        keys = repl_dict.keys()
568
 
        keys.sort() # lexical order
569
 
        keys.reverse() # use longest match first
570
 
        pattern = u"|".join([re.escape(key) for key in keys])
571
 
        self.pattern = re.compile(pattern)
572
 
        self.dict = repl_dict
573
 
 
574
 
    def replace(self, s):
575
 
        # apply replacement dictionary to string
576
 
 
577
 
        def repl(match, get=self.dict.get):
578
 
            item = match.group(0)
579
 
            return get(item, item)
580
 
        return self.pattern.sub(repl, s)
581
 
 
582
 
    __call__ = replace
583
 
 
584
 
 
585
 
def _escape(s):
586
 
    return PostMarkup.standard_replace(s.rstrip('\n'))
587
 
 
588
 
def _escape_no_breaks(s):
589
 
    return PostMarkup.standard_replace_no_break(s.rstrip('\n'))
590
 
 
591
 
class TagFactory(object):
592
 
 
593
 
    def __init__(self):
594
 
 
595
 
        self.tags = {}
596
 
 
597
 
    @classmethod
598
 
    def tag_factory_callable(cls, tag_class, name, *args, **kwargs):
599
 
        """
600
 
        Returns a callable that returns a new tag instance.
601
 
        """
602
 
        def make():
603
 
            return tag_class(name, *args, **kwargs)
604
 
 
605
 
        return make
606
 
 
607
 
 
608
 
    def add_tag(self, cls, name, *args, **kwargs):
609
 
 
610
 
        self.tags[name] = self.tag_factory_callable(cls, name, *args, **kwargs)
611
 
 
612
 
    def __getitem__(self, name):
613
 
 
614
 
        return self.tags[name]()
615
 
 
616
 
    def __contains__(self, name):
617
 
 
618
 
        return name in self.tags
619
 
 
620
 
    def get(self, name, default=None):
621
 
 
622
 
        if name in self.tags:
623
 
            return self.tags[name]()
624
 
 
625
 
        return default
626
 
 
627
 
 
628
 
class _Parser(object):
629
 
 
630
 
    """ This is an interface to the parser, used by Tag classes. """
631
 
 
632
 
    def __init__(self, post_markup):
633
 
 
634
 
        self.pm = post_markup
635
 
        self.tag_data = {}
636
 
        self.render_node_index = 0
637
 
 
638
 
    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"
642
 
        self.render_node_index = node_index
643
 
 
644
 
    def get_text_nodes(self, node1, node2):
645
 
 
646
 
        """ Retrieves the text nodes between two node indices. """
647
 
 
648
 
        if node2 is None:
649
 
            node2 = node1+1
650
 
 
651
 
        return [node for node in self.nodes[node1:node2] if not callable(node)]
652
 
 
653
 
    def begin_no_breaks(self):
654
 
 
655
 
        """Disables replacing of newlines with break tags at the start and end of text nodes.
656
 
        Can only be called from a tags 'open' method.
657
 
 
658
 
        """
659
 
        assert self.phase==1, "Can not be called from render_open or render_close"
660
 
        self.no_breaks_count += 1
661
 
 
662
 
    def end_no_breaks(self):
663
 
 
664
 
        """Re-enables auto-replacing of newlines with break tags (see begin_no_breaks)."""
665
 
 
666
 
        assert self.phase==1, "Can not be called from render_open or render_close"
667
 
        if self.no_breaks_count:
668
 
            self.no_breaks_count -= 1
669
 
 
670
 
 
671
 
class PostMarkup(object):
672
 
 
673
 
    standard_replace = MultiReplace({   u'<':u'&lt;',
674
 
                                        u'>':u'&gt;',
675
 
                                        u'&':u'&amp;',
676
 
                                        u'\n':u'<br/>'})
677
 
 
678
 
    standard_replace_no_break = MultiReplace({  u'<':u'&lt;',
679
 
                                                u'>':u'&gt;',
680
 
                                                u'&':u'&amp;',})
681
 
 
682
 
    TOKEN_TAG, TOKEN_PTAG, TOKEN_TEXT = range(3)
683
 
 
684
 
 
685
 
    # I tried to use RE's. Really I did.
686
 
    @classmethod
687
 
    def tokenize(cls, post):
688
 
 
689
 
        text = True
690
 
        pos = 0
691
 
 
692
 
        def find_first(post, pos, c):
693
 
            f1 = post.find(c[0], pos)
694
 
            f2 = post.find(c[1], pos)
695
 
            if f1 == -1:
696
 
                return f2
697
 
            if f2 == -1:
698
 
                return f1
699
 
            return min(f1, f2)
700
 
 
701
 
        while True:
702
 
 
703
 
            brace_pos = post.find(u'[', pos)
704
 
            if brace_pos == -1:
705
 
                if pos<len(post):
706
 
                    yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
707
 
                return
708
 
            if brace_pos - pos > 0:
709
 
                yield PostMarkup.TOKEN_TEXT, post[pos:brace_pos], pos, brace_pos
710
 
 
711
 
            pos = brace_pos
712
 
            end_pos = pos+1
713
 
 
714
 
            open_tag_pos = post.find(u'[', end_pos)
715
 
            end_pos = find_first(post, end_pos, u']=')
716
 
            if end_pos == -1:
717
 
                yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
718
 
                return
719
 
 
720
 
            if open_tag_pos != -1 and open_tag_pos < end_pos:
721
 
                yield PostMarkup.TOKEN_TEXT, post[pos:open_tag_pos], pos, open_tag_pos
722
 
                end_pos = open_tag_pos
723
 
                pos = end_pos
724
 
                continue
725
 
 
726
 
            if post[end_pos] == ']':
727
 
                yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
728
 
                pos = end_pos+1
729
 
                continue
730
 
 
731
 
            if post[end_pos] == '=':
732
 
                try:
733
 
                    end_pos += 1
734
 
                    while post[end_pos] == ' ':
735
 
                        end_pos += 1
736
 
                    if post[end_pos] != '"':
737
 
                        end_pos = post.find(u']', end_pos+1)
738
 
                        if end_pos == -1:
739
 
                            return
740
 
                        yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
741
 
                    else:
742
 
                        end_pos = find_first(post, end_pos, u'"]')
743
 
 
744
 
                        if end_pos==-1:
745
 
                            return
746
 
                        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
754
 
                        else:
755
 
                            yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos
756
 
                    pos = end_pos+1
757
 
                except IndexError:
758
 
                    return
759
 
 
760
 
    def tagify_urls(self, postmarkup ):
761
 
 
762
 
        """ Surrounds urls with url bbcode tags. """
763
 
 
764
 
        def repl(match):
765
 
            return u'[url]%s[/url]' % match.group(0)
766
 
 
767
 
        text_tokens = []
768
 
        for tag_type, tag_token, start_pos, end_pos in self.tokenize(postmarkup):
769
 
 
770
 
            if tag_type == PostMarkup.TOKEN_TEXT:
771
 
                text_tokens.append(re_url.sub(repl, tag_token))
772
 
            else:
773
 
                text_tokens.append(tag_token)
774
 
 
775
 
        return u"".join(text_tokens)
776
 
 
777
 
 
778
 
    def __init__(self, tag_factory=None):
779
 
 
780
 
        self.tag_factory = tag_factory or TagFactory()
781
 
 
782
 
 
783
 
    def default_tags(self):
784
 
 
785
 
        """ Add some basic tags. """
786
 
 
787
 
        add_tag = self.tag_factory.add_tag
788
 
 
789
 
        add_tag(SimpleTag, u'b', u'strong')
790
 
        add_tag(SimpleTag, u'i', u'em')
791
 
        add_tag(SimpleTag, u'u', u'u')
792
 
        add_tag(SimpleTag, u's', u's')
793
 
 
794
 
 
795
 
    def get_supported_tags(self):
796
 
 
797
 
        """ Returns a list of the supported tags. """
798
 
 
799
 
        return sorted(self.tag_factory.tags.keys())
800
 
 
801
 
 
802
 
 
803
 
    def render_to_html(self,
804
 
                       post_markup,
805
 
                       encoding="ascii",
806
 
                       exclude_tags=None,
807
 
                       auto_urls=True):
808
 
 
809
 
        """Converts Post Markup to XHTML.
810
 
 
811
 
        post_markup -- String containing bbcode.
812
 
        encoding -- Encoding of string, defaults to "ascii".
813
 
        exclude_tags -- A collection of tag names to ignore.
814
 
        auto_urls -- If True, then urls will be wrapped with url bbcode tags.
815
 
 
816
 
        """
817
 
 
818
 
        if not isinstance(post_markup, unicode):
819
 
            post_markup = unicode(post_markup, encoding, 'replace')
820
 
 
821
 
        if auto_urls:
822
 
            post_markup = self.tagify_urls(post_markup)
823
 
 
824
 
        parser = _Parser(self)
825
 
        parser.markup = post_markup
826
 
 
827
 
        if exclude_tags is None:
828
 
            exclude_tags = []
829
 
 
830
 
        tag_factory = self.tag_factory
831
 
 
832
 
 
833
 
        nodes = []
834
 
        parser.nodes = nodes
835
 
 
836
 
        parser.phase = 1
837
 
        parser.no_breaks_count = 0
838
 
        enclosed_count = 0
839
 
        open_stack = []
840
 
        tag_stack = []
841
 
        break_stack = []
842
 
        remove_next_newline = False
843
 
 
844
 
        def check_tag_stack(tag_name):
845
 
 
846
 
            for tag in reversed(tag_stack):
847
 
                if tag_name == tag.name:
848
 
                    return True
849
 
            return False
850
 
 
851
 
        def redo_break_stack():
852
 
 
853
 
            while break_stack:
854
 
                tag = break_stack.pop()
855
 
                open_tag(tag)
856
 
                tag_stack.append(tag)
857
 
 
858
 
        def break_inline_tags():
859
 
 
860
 
            while tag_stack:
861
 
                if tag_stack[-1].inline:
862
 
                    tag = tag_stack.pop()
863
 
                    close_tag(tag)
864
 
                    break_stack.append(tag)
865
 
                else:
866
 
                    break
867
 
 
868
 
        def open_tag(tag):
869
 
            def call(node_index):
870
 
                return tag.render_open(parser, node_index)
871
 
            nodes.append(call)
872
 
 
873
 
        def close_tag(tag):
874
 
            def call(node_index):
875
 
                return tag.render_close(parser, node_index)
876
 
            nodes.append(call)
877
 
 
878
 
        # Pass 1
879
 
        for tag_type, tag_token, start_pos, end_pos in self.tokenize(post_markup):
880
 
 
881
 
            raw_tag_token = tag_token
882
 
 
883
 
            if tag_type == PostMarkup.TOKEN_TEXT:
884
 
                if parser.no_breaks_count:
885
 
                    tag_token = tag_token.strip()
886
 
                    if not tag_token:
887
 
                        continue
888
 
                if remove_next_newline:
889
 
                    tag_token = tag_token.lstrip(' ')
890
 
                    if tag_token.startswith('\n'):
891
 
                        tag_token = tag_token.lstrip(' ')[1:]
892
 
                        if not tag_token:
893
 
                            continue
894
 
                    remove_next_newline = False
895
 
 
896
 
                if tag_stack and tag_stack[-1].strip_first_newline:
897
 
                    tag_token = tag_token.lstrip()
898
 
                    tag_stack[-1].strip_first_newline = False
899
 
                    if not tag_stack[-1]:
900
 
                        tag_stack.pop()
901
 
                        continue
902
 
 
903
 
                if not enclosed_count:
904
 
                    redo_break_stack()
905
 
 
906
 
                nodes.append(self.standard_replace(tag_token))
907
 
                continue
908
 
 
909
 
            elif tag_type == PostMarkup.TOKEN_TAG:
910
 
                tag_token = tag_token[1:-1].lstrip()
911
 
                if ' ' in tag_token:
912
 
                    tag_name, tag_attribs = tag_token.split(u' ', 1)
913
 
                    tag_attribs = tag_attribs.strip()
914
 
                else:
915
 
                    if '=' in tag_token:
916
 
                        tag_name, tag_attribs = tag_token.split(u'=', 1)
917
 
                        tag_attribs = tag_attribs.strip()
918
 
                    else:
919
 
                        tag_name = tag_token
920
 
                        tag_attribs = u""
921
 
            else:
922
 
                tag_token = tag_token[1:-1].lstrip()
923
 
                tag_name, tag_attribs = tag_token.split(u'=', 1)
924
 
                tag_attribs = tag_attribs.strip()[1:-1]
925
 
 
926
 
            tag_name = tag_name.strip().lower()
927
 
 
928
 
            end_tag = False
929
 
            if tag_name.startswith(u'/'):
930
 
                end_tag = True
931
 
                tag_name = tag_name[1:]
932
 
 
933
 
 
934
 
            if enclosed_count and tag_stack[-1].name != tag_name:
935
 
                continue
936
 
 
937
 
            if tag_name in exclude_tags:
938
 
                continue
939
 
 
940
 
            if not end_tag:
941
 
 
942
 
                tag = tag_factory.get(tag_name, None)
943
 
                if tag is None:
944
 
                    continue
945
 
 
946
 
                redo_break_stack()
947
 
 
948
 
                if not tag.inline:
949
 
                    break_inline_tags()
950
 
 
951
 
                tag.open(parser, tag_attribs, end_pos, len(nodes))
952
 
                if tag.enclosed:
953
 
                    enclosed_count += 1
954
 
                tag_stack.append(tag)
955
 
 
956
 
                open_tag(tag)
957
 
 
958
 
                if tag.auto_close:
959
 
                    tag = tag_stack.pop()
960
 
                    tag.close(self, start_pos, len(nodes)-1)
961
 
                    close_tag(tag)
962
 
 
963
 
            else:
964
 
 
965
 
                if break_stack and break_stack[-1].name == tag_name:
966
 
                    break_stack.pop()
967
 
                    tag.close(parser, start_pos, len(nodes))
968
 
                elif check_tag_stack(tag_name):
969
 
                    while tag_stack[-1].name != tag_name:
970
 
                        tag = tag_stack.pop()
971
 
                        break_stack.append(tag)
972
 
                        close_tag(tag)
973
 
 
974
 
                    tag = tag_stack.pop()
975
 
                    tag.close(parser, start_pos, len(nodes))
976
 
                    if tag.enclosed:
977
 
                        enclosed_count -= 1
978
 
 
979
 
                    close_tag(tag)
980
 
 
981
 
                    if not tag.inline:
982
 
                        remove_next_newline = True
983
 
 
984
 
        if tag_stack:
985
 
            redo_break_stack()
986
 
            while tag_stack:
987
 
                tag = tag_stack.pop()
988
 
                tag.close(parser, len(post_markup), len(nodes))
989
 
                if tag.enclosed:
990
 
                    enclosed_count -= 1
991
 
                close_tag(tag)
992
 
 
993
 
        parser.phase = 2
994
 
        # Pass 2
995
 
        parser.nodes = nodes
996
 
 
997
 
        text = []
998
 
        parser.render_node_index = 0
999
 
        while parser.render_node_index < len(parser.nodes):
1000
 
            i = parser.render_node_index
1001
 
            node_text = parser.nodes[i]
1002
 
            if callable(node_text):
1003
 
                node_text = node_text(i)
1004
 
            if node_text is not None:
1005
 
                text.append(node_text)
1006
 
            parser.render_node_index += 1
1007
 
 
1008
 
        return u"".join(text)
1009
 
 
1010
 
    __call__ = render_to_html
1011
 
 
1012
 
 
1013
 
 
1014
 
 
1015
 
 
1016
 
def _tests():
1017
 
 
1018
 
    import sys
1019
 
    #sys.stdout=open('test.htm', 'w')
1020
 
 
1021
 
    post_markup = create(use_pygments=True)
1022
 
 
1023
 
    tests = []
1024
 
    print """<link rel="stylesheet" href="code.css" type="text/css" />\n"""
1025
 
 
1026
 
    tests.append(']')
1027
 
    tests.append('[')
1028
 
    tests.append(':-[ Hello, [b]World[/b]')
1029
 
 
1030
 
    tests.append("[link=http://www.willmcgugan.com]My homepage[/link]")
1031
 
    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]")
1034
 
 
1035
 
    tests.append(u"[b]Hello AndrУЉ[/b]")
1036
 
    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]")
1041
 
 
1042
 
    tests.append("[quote Will said...]BBCode is very cool[/quote]")
1043
 
 
1044
 
    tests.append("""[code python]
1045
 
# A proxy object that calls a callback when converted to a string
1046
 
class TagStringify(object):
1047
 
    def __init__(self, callback, raw):
1048
 
        self.callback = callback
1049
 
        self.raw = raw
1050
 
        r[b]=3
1051
 
    def __str__(self):
1052
 
        return self.callback()
1053
 
    def __repr__(self):
1054
 
        return self.__str__()
1055
 
[/code]""")
1056
 
 
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]")
1062
 
    tests.append("""[list=1]
1063
 
    [*]Apples
1064
 
    [*]Oranges
1065
 
    are not the only fruit
1066
 
    [*]Pears
1067
 
[/list]""")
1068
 
    tests.append("[list=a][*]Apples[*]Oranges[*]Pears[/list]")
1069
 
    tests.append("[list=A][*]Apples[*]Oranges[*]Pears[/list]")
1070
 
 
1071
 
    long_test="""[b]Long test[/b]
1072
 
 
1073
 
New lines characters are converted to breaks."""\
1074
 
"""Tags my be [b]ove[i]rl[/b]apped[/i].
1075
 
 
1076
 
[i]Open tags will be closed.
1077
 
[b]Test[/b]"""
1078
 
 
1079
 
    tests.append(long_test)
1080
 
 
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]")
1087
 
    #tests = []
1088
 
    # 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]")
1090
 
 
1091
 
    tests.append('Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1092
 
 
1093
 
    tests.append(u'[google]ЩИЮВfvЮИУАsz[/google]')
1094
 
 
1095
 
    tests.append(u'[size 30]Hello, World![/size]')
1096
 
 
1097
 
    tests.append(u'[color red]This should be red[/color]')
1098
 
    tests.append(u'[color #0f0]This should be green[/color]')
1099
 
    tests.append(u"[center]This should be in the center!")
1100
 
 
1101
 
    tests.append('Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1102
 
 
1103
 
    #tests = []
1104
 
    tests.append('[b]Hello, [i]World[/b]! [/i]')
1105
 
 
1106
 
    tests.append('[b][center]This should be centered![/center][/b]')
1107
 
 
1108
 
    tests.append('[list][*]Hello[i][*]World![/i][/list]')
1109
 
 
1110
 
 
1111
 
    tests.append("""[list=1]
1112
 
    [*]Apples
1113
 
    [*]Oranges
1114
 
    are not the only fruit
1115
 
    [*]Pears
1116
 
[/list]""")
1117
 
 
1118
 
    tests.append("[b]urls such as http://www.willmcgugan.com are authomaticaly converted to links[/b]")
1119
 
 
1120
 
    tests.append("""
1121
 
[b]
1122
 
[code python]
1123
 
parser.markup[self.open_pos:self.close_pos]
1124
 
[/code]
1125
 
asdasdasdasdqweqwe
1126
 
""")
1127
 
 
1128
 
    tests.append("""[list 1]
1129
 
[*]Hello
1130
 
[*]World
1131
 
[/list]""")
1132
 
 
1133
 
 
1134
 
 
1135
 
    #tests=["""[b]b[i]i[/b][/i]"""]
1136
 
 
1137
 
    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"))
1140
 
        print u"<hr/>"
1141
 
        print
1142
 
 
1143
 
    print repr(post_markup('[url=<script>Attack</script>]Attack[/url]'))
1144
 
 
1145
 
    print repr(post_markup('http://www.google.com/search?as_q=bbcode&btnG=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA'))
1146
 
 
1147
 
    p = create(use_pygments=False)
1148
 
    print (p('[code]foo\nbar[/code]'))
1149
 
 
1150
 
    #print render_bbcode("[b]For the lazy, use the http://www.willmcgugan.com render_bbcode function.[/b]")
1151
 
 
1152
 
 
1153
 
def _run_unittests():
1154
 
 
1155
 
    # TODO: Expand tests for better coverage!
1156
 
 
1157
 
    import unittest
1158
 
 
1159
 
    class TestPostmarkup(unittest.TestCase):
1160
 
 
1161
 
        def testsimpletag(self):
1162
 
 
1163
 
            postmarkup = create()
1164
 
 
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>"),
1169
 
                     ]
1170
 
 
1171
 
            for test, result in tests:
1172
 
                self.assertEqual(postmarkup(test), result)
1173
 
 
1174
 
 
1175
 
        def testoverlap(self):
1176
 
 
1177
 
            postmarkup = create()
1178
 
 
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>')
1181
 
                     ]
1182
 
 
1183
 
            for test, result in tests:
1184
 
                self.assertEqual(postmarkup(test), result)
1185
 
 
1186
 
        def testlinks(self):
1187
 
 
1188
 
            postmarkup = create(annotate_links=False)
1189
 
 
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>')
1194
 
                     ]
1195
 
 
1196
 
            for test, result in tests:
1197
 
                self.assertEqual(postmarkup(test), result)
1198
 
 
1199
 
 
1200
 
    suite = unittest.TestLoader().loadTestsFromTestCase(TestPostmarkup)
1201
 
    unittest.TextTestRunner(verbosity=2).run(suite)
1202
 
 
1203
 
 
1204
 
 
1205
 
 
1206
 
if __name__ == "__main__":
1207
 
 
1208
 
    _tests()
1209
 
    _run_unittests()
 
1
# -*- coding: UTF-8 -*-
 
2
 
 
3
"""
 
4
Post Markup
 
5
Author: Will McGugan (http://www.willmcgugan.com)
 
6
"""
 
7
 
 
8
__version__ = "1.1.3"
 
9
 
 
10
import re
 
11
from urllib import quote, unquote, quote_plus
 
12
from urlparse import urlparse, urlunparse
 
13
 
 
14
pygments_available = True
 
15
try:
 
16
    from pygments import highlight
 
17
    from pygments.lexers import get_lexer_by_name, ClassNotFound
 
18
    from pygments.formatters import HtmlFormatter
 
19
except ImportError:
 
20
    # Make Pygments optional
 
21
    pygments_available = False
 
22
 
 
23
 
 
24
 
 
25
def annotate_link(domain):
 
26
    """This function is called by the url tag. Override to disable or change behaviour.
 
27
 
 
28
    domain -- Domain parsed from url
 
29
 
 
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('<.*?>|\&.*?\;')
 
38
def textilize(s):
 
39
    """Remove markup from html"""
 
40
    return re_html.sub("", s)
 
41
 
 
42
re_excerpt = re.compile(r'\[".*?\]+?.*?\[/".*?\]+?', re.DOTALL)
 
43
re_remove_markup = re.compile(r'\[.*?\]', re.DOTALL)
 
44
 
 
45
def remove_markup(post):
 
46
    """Removes html tags from a string."""
 
47
    return re_remove_markup.sub("", post)
 
48
 
 
49
def get_excerpt(post):
 
50
    """Returns an excerpt between ["] and [/"]
 
51
 
 
52
    post -- BBCode string"""
 
53
 
 
54
    match = re_excerpt.search(post)
 
55
    if match is None:
 
56
        return ""
 
57
    excerpt = match.group(0)
 
58
    excerpt = excerpt.replace(u'\n', u"<br/>")
 
59
    return remove_markup(excerpt)
 
60
 
 
61
def strip_bbcode(bbcode):
 
62
 
 
63
    """ Strips bbcode tags from a string.
 
64
 
 
65
    bbcode -- A string to remove tags from
 
66
 
 
67
    """
 
68
 
 
69
    return u"".join([t[1] for t in PostMarkup.tokenize(bbcode) if t[0] == PostMarkup.TOKEN_TEXT])
 
70
 
 
71
 
 
72
def create(include=None, exclude=None, use_pygments=True, **kwargs):
 
73
 
 
74
    """Create a postmarkup object that converts bbcode to XML snippets.
 
75
 
 
76
    include -- List or similar iterable containing the names of the tags to use
 
77
               If omitted, all tags will be used
 
78
    exclude -- List or similar iterable containing the names of the tags to exclude.
 
79
               If omitted, no tags will be excluded
 
80
    use_pygments -- If True, Pygments (http://pygments.org/) will be used for the code tag,
 
81
                    otherwise it will use <pre>code</pre>
 
82
    """
 
83
 
 
84
    postmarkup = PostMarkup()
 
85
    postmarkup_add_tag = postmarkup.tag_factory.add_tag
 
86
 
 
87
    def add_tag(tag_class, name, *args, **kwargs):
 
88
        if include is None or name in include:
 
89
            if exclude is not None and name in exclude:
 
90
                return
 
91
            postmarkup_add_tag(tag_class, name, *args, **kwargs)
 
92
 
 
93
 
 
94
 
 
95
    add_tag(SimpleTag, 'b', 'strong')
 
96
    add_tag(SimpleTag, 'i', 'em')
 
97
    add_tag(SimpleTag, 'u', 'u')
 
98
    add_tag(SimpleTag, 's', 'strike')
 
99
 
 
100
    add_tag(LinkTag, 'link', **kwargs)
 
101
    add_tag(LinkTag, 'url', **kwargs)
 
102
 
 
103
    add_tag(QuoteTag, 'quote')
 
104
 
 
105
    add_tag(SearchTag, u'wiki',
 
106
            u"http://en.wikipedia.org/wiki/Special:Search?search=%s", u'wikipedia.com', **kwargs)
 
107
    add_tag(SearchTag, u'google',
 
108
            u"http://www.google.com/search?hl=en&q=%s&btnG=Google+Search", u'google.com', **kwargs)
 
109
    add_tag(SearchTag, u'dictionary',
 
110
            u"http://dictionary.reference.com/browse/%s", u'dictionary.com', **kwargs)
 
111
    add_tag(SearchTag, u'dict',
 
112
            u"http://dictionary.reference.com/browse/%s", u'dictionary.com', **kwargs)
 
113
 
 
114
    add_tag(ImgTag, u'img')
 
115
    add_tag(ListTag, u'list')
 
116
    add_tag(ListItemTag, u'*')
 
117
 
 
118
    add_tag(SizeTag, u"size")
 
119
    add_tag(ColorTag, u"color")
 
120
    add_tag(CenterTag, u"center")
 
121
 
 
122
    if use_pygments:
 
123
        assert pygments_available, "Install Pygments (http://pygments.org/) or call create with use_pygments=False"
 
124
        add_tag(PygmentsCodeTag, u'code', **kwargs)
 
125
    else:
 
126
        add_tag(CodeTag, u'code', **kwargs)
 
127
 
 
128
    return postmarkup
 
129
 
 
130
 
 
131
 
 
132
_postmarkup = None
 
133
def render_bbcode(bbcode, encoding="ascii", exclude_tags=None, auto_urls=True):
 
134
 
 
135
    """Renders a bbcode string in to XHTML. This is a shortcut if you don't
 
136
    need to customize any tags.
 
137
 
 
138
    bbcode -- A string containing the bbcode
 
139
    encoding -- If bbcode is not unicode, then then it will be encoded with
 
140
    this encoding (defaults to 'ascii'). Ignore the encoding if you already have
 
141
    a unicode string
 
142
 
 
143
    """
 
144
 
 
145
    global _postmarkup
 
146
    if _postmarkup is None:
 
147
        _postmarkup = create(use_pygments=pygments_available)
 
148
 
 
149
    return _postmarkup(bbcode, encoding, exclude_tags=exclude_tags, auto_urls=auto_urls)
 
150
 
 
151
 
 
152
class TagBase(object):
 
153
 
 
154
    def __init__(self, name, enclosed=False, auto_close=False, inline=False, strip_first_newline=False, **kwargs):
 
155
        """Base class for all tags.
 
156
 
 
157
        name -- The name of the bbcode tag
 
158
        enclosed -- True if the contents of the tag should not be bbcode processed.
 
159
        auto_close -- True if the tag is standalone and does not require a close tag.
 
160
        inline -- True if the tag generates an inline html tag.
 
161
 
 
162
        """
 
163
 
 
164
        self.name = name
 
165
        self.enclosed = enclosed
 
166
        self.auto_close = auto_close
 
167
        self.inline = inline
 
168
        self.strip_first_newline = strip_first_newline
 
169
 
 
170
        self.open_pos = None
 
171
        self.close_pos = None
 
172
        self.open_node_index = None
 
173
        self.close_node_index = None
 
174
 
 
175
    def open(self, parser, params, open_pos, node_index):
 
176
        """ Called when the open tag is initially encountered. """
 
177
        self.params = params
 
178
        self.open_pos = open_pos
 
179
        self.open_node_index = node_index
 
180
 
 
181
    def close(self, parser, close_pos, node_index):
 
182
        """ Called when the close tag is initially encountered. """
 
183
        self.close_pos = close_pos
 
184
        self.close_node_index = node_index
 
185
 
 
186
    def render_open(self, parser, node_index):
 
187
        """ Called to render the open tag. """
 
188
        pass
 
189
 
 
190
    def render_close(self, parser, node_index):
 
191
        """ Called to render the close tag. """
 
192
        pass
 
193
 
 
194
    def get_contents(self, parser):
 
195
        """Returns the string between the open and close tag."""
 
196
        return parser.markup[self.open_pos:self.close_pos]
 
197
 
 
198
    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) )
 
201
 
 
202
    def skip_contents(self, parser):
 
203
        """Skips the contents of a tag while rendering."""
 
204
        parser.skip_to_node(self.close_node_index)
 
205
 
 
206
    def __str__(self):
 
207
        return '[%s]'%self.name
 
208
 
 
209
 
 
210
class SimpleTag(TagBase):
 
211
 
 
212
    """A tag that can be rendered with a simple substitution. """
 
213
 
 
214
    def __init__(self, name, html_name, **kwargs):
 
215
        """ html_name -- the html tag to substitute."""
 
216
        TagBase.__init__(self, name, inline=True)
 
217
        self.html_name = html_name
 
218
 
 
219
    def render_open(self, parser, node_index):
 
220
        return u"<%s>"%self.html_name
 
221
 
 
222
    def render_close(self, parser, node_index):
 
223
        return u"</%s>"%self.html_name
 
224
 
 
225
 
 
226
class DivStyleTag(TagBase):
 
227
 
 
228
    """A simple tag that is replaces with a div and a style."""
 
229
 
 
230
    def __init__(self, name, style, value, **kwargs):
 
231
        TagBase.__init__(self, name)
 
232
        self.style = style
 
233
        self.value = value
 
234
 
 
235
    def render_open(self, parser, node_index):
 
236
        return u'<div style="%s:%s;">' % (self.style, self.value)
 
237
 
 
238
    def render_close(self, parser, node_index):
 
239
        return u'</div>'
 
240
 
 
241
 
 
242
class LinkTag(TagBase):
 
243
 
 
244
    def __init__(self, name, annotate_links=True, **kwargs):
 
245
        TagBase.__init__(self, name, inline=True)
 
246
 
 
247
        self.annotate_links = annotate_links
 
248
 
 
249
 
 
250
    def render_open(self, parser, node_index):
 
251
 
 
252
        self.domain = u''
 
253
        tag_data = parser.tag_data
 
254
        nest_level = tag_data['link_nest_level'] = tag_data.setdefault('link_nest_level', 0) + 1
 
255
 
 
256
        if nest_level > 1:
 
257
            return u""
 
258
 
 
259
        if self.params:
 
260
            url = self.params.strip()
 
261
        else:
 
262
            url = self.get_contents_text(parser).strip()
 
263
 
 
264
        self.domain = ""
 
265
        #Unquote the url
 
266
        self.url = unquote(url)
 
267
 
 
268
        #Disallow javascript links
 
269
        if u"javascript:" in self.url.lower():
 
270
            return ""
 
271
 
 
272
        #Disallow non http: links
 
273
        url_parsed = urlparse(self.url)
 
274
        if url_parsed[0] and not url_parsed[0].lower().startswith(u'http'):
 
275
            return ""
 
276
 
 
277
        #Prepend http: if it is not present
 
278
        if not url_parsed[0]:
 
279
            self.url="http://"+self.url
 
280
            url_parsed = urlparse(self.url)
 
281
 
 
282
        #Get domain
 
283
        self.domain = url_parsed[1].lower()
 
284
 
 
285
        #Remove www for brevity
 
286
        if self.domain.startswith(u'www.'):
 
287
            self.domain = self.domain[4:]
 
288
 
 
289
        #Quote the url
 
290
        #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]) )
 
292
 
 
293
        if not self.url:
 
294
            return u""
 
295
 
 
296
        if self.domain:
 
297
            return u'<a href="%s">'%self.url
 
298
        else:
 
299
            return u""
 
300
 
 
301
    def render_close(self, parser, node_index):
 
302
 
 
303
        tag_data = parser.tag_data
 
304
        tag_data['link_nest_level'] -= 1
 
305
 
 
306
        if tag_data['link_nest_level'] > 0:
 
307
            return u''
 
308
 
 
309
        if self.domain:
 
310
            return u'</a>'+self.annotate_link(self.domain)
 
311
        else:
 
312
            return u''
 
313
 
 
314
    def annotate_link(self, domain=None):
 
315
 
 
316
        if domain and self.annotate_links:
 
317
            return annotate_link(domain)
 
318
        else:
 
319
            return u""
 
320
 
 
321
 
 
322
class QuoteTag(TagBase):
 
323
 
 
324
    def __init__(self, name, **kwargs):
 
325
        TagBase.__init__(self, name, strip_first_newline=True)
 
326
 
 
327
    def open(self, parser, *args):
 
328
        TagBase.open(self, parser, *args)
 
329
 
 
330
    def close(self, parser, *args):
 
331
        TagBase.close(self, parser, *args)
 
332
 
 
333
    def render_open(self, parser, node_index):
 
334
        if self.params:
 
335
            return u'<blockquote><em>%s</em><br/>'%(PostMarkup.standard_replace(self.params))
 
336
        else:
 
337
            return u'<blockquote>'
 
338
 
 
339
 
 
340
    def render_close(self, parser, node_index):
 
341
        return u"</blockquote>"
 
342
 
 
343
 
 
344
class SearchTag(TagBase):
 
345
 
 
346
    def __init__(self, name, url, label="", annotate_links=True, **kwargs):
 
347
        TagBase.__init__(self, name, inline=True)
 
348
        self.url = url
 
349
        self.label = label
 
350
        self.annotate_links = annotate_links
 
351
 
 
352
    def render_open(self, parser, node_idex):
 
353
 
 
354
        if self.params:
 
355
            search=self.params
 
356
        else:
 
357
            search=self.get_contents(parser)
 
358
        link = u'<a href="%s">' % self.url
 
359
        if u'%' in link:
 
360
            return link%quote_plus(search.encode("UTF-8"))
 
361
        else:
 
362
            return link
 
363
 
 
364
    def render_close(self, parser, node_index):
 
365
 
 
366
        if self.label:
 
367
            ret = u'</a>'
 
368
            if self.annotate_links:
 
369
                ret += annotate_link(self.label)
 
370
            return ret
 
371
        else:
 
372
            return u''
 
373
 
 
374
 
 
375
class PygmentsCodeTag(TagBase):
 
376
 
 
377
    def __init__(self, name, pygments_line_numbers=False, **kwargs):
 
378
        TagBase.__init__(self, name, enclosed=True, strip_first_newline=True)
 
379
        self.line_numbers = pygments_line_numbers
 
380
 
 
381
    def render_open(self, parser, node_index):
 
382
 
 
383
        contents = self.get_contents(parser)
 
384
        self.skip_contents(parser)
 
385
 
 
386
        try:
 
387
            lexer = get_lexer_by_name(self.params, stripall=True)
 
388
        except ClassNotFound:
 
389
            contents = _escape(contents)
 
390
            return '<div class="code"><pre>%s</pre></div>' % contents
 
391
 
 
392
        formatter = HtmlFormatter(linenos=self.line_numbers, cssclass="code")
 
393
        return highlight(contents, lexer, formatter)
 
394
 
 
395
 
 
396
 
 
397
class CodeTag(TagBase):
 
398
 
 
399
    def __init__(self, name, **kwargs):
 
400
        TagBase.__init__(self, name, enclosed=True, strip_first_newline=True)
 
401
 
 
402
    def render_open(self, parser, node_index):
 
403
 
 
404
        contents = _escape_no_breaks(self.get_contents(parser))
 
405
        self.skip_contents(parser)
 
406
        return '<div class="code"><pre>%s</pre></div>' % contents
 
407
 
 
408
 
 
409
class ImgTag(TagBase):
 
410
 
 
411
    def __init__(self, name, **kwargs):
 
412
        TagBase.__init__(self, name, inline=True)
 
413
 
 
414
    def render_open(self, parser, node_index):
 
415
 
 
416
        contents = self.get_contents(parser)
 
417
        self.skip_contents(parser)
 
418
 
 
419
        contents = strip_bbcode(contents).replace(u'"', "%22")
 
420
 
 
421
        return u'<img src="%s"></img>' % contents
 
422
 
 
423
 
 
424
class ListTag(TagBase):
 
425
 
 
426
    def __init__(self, name,  **kwargs):
 
427
        TagBase.__init__(self, name, strip_first_newline=True)
 
428
 
 
429
    def open(self, parser, params, open_pos, node_index):
 
430
        TagBase.open(self, parser, params, open_pos, node_index)
 
431
 
 
432
    def close(self, parser, close_pos, node_index):
 
433
        TagBase.close(self, parser, close_pos, node_index)
 
434
 
 
435
 
 
436
    def render_open(self, parser, node_index):
 
437
 
 
438
        self.close_tag = u""
 
439
 
 
440
        tag_data = parser.tag_data
 
441
        tag_data.setdefault("ListTag.count", 0)
 
442
 
 
443
        if tag_data["ListTag.count"]:
 
444
            return u""
 
445
 
 
446
        tag_data["ListTag.count"] += 1
 
447
 
 
448
        tag_data["ListItemTag.initial_item"]=True
 
449
 
 
450
        if self.params == "1":
 
451
            self.close_tag = u"</li></ol>"
 
452
            return u"<ol><li>"
 
453
        elif self.params == "a":
 
454
            self.close_tag = u"</li></ol>"
 
455
            return u'<ol style="list-style-type: lower-alpha;"><li>'
 
456
        elif self.params == "A":
 
457
            self.close_tag = u"</li></ol>"
 
458
            return u'<ol style="list-style-type: upper-alpha;"><li>'
 
459
        else:
 
460
            self.close_tag = u"</li></ul>"
 
461
            return u"<ul><li>"
 
462
 
 
463
    def render_close(self, parser, node_index):
 
464
 
 
465
        tag_data = parser.tag_data
 
466
        tag_data["ListTag.count"] -= 1
 
467
 
 
468
        return self.close_tag
 
469
 
 
470
 
 
471
class ListItemTag(TagBase):
 
472
 
 
473
    def __init__(self, name, **kwargs):
 
474
        TagBase.__init__(self, name)
 
475
        self.closed = False
 
476
 
 
477
    def render_open(self, parser, node_index):
 
478
 
 
479
        tag_data = parser.tag_data
 
480
        if not tag_data.setdefault("ListTag.count", 0):
 
481
            return u""
 
482
 
 
483
        if tag_data["ListItemTag.initial_item"]:
 
484
            tag_data["ListItemTag.initial_item"] = False
 
485
            return
 
486
 
 
487
        return u"</li><li>"
 
488
 
 
489
 
 
490
class SizeTag(TagBase):
 
491
 
 
492
    valid_chars = frozenset("0123456789")
 
493
 
 
494
    def __init__(self, name, **kwargs):
 
495
        TagBase.__init__(self, name, inline=True)
 
496
 
 
497
    def render_open(self, parser, node_index):
 
498
 
 
499
        try:
 
500
            self.size = int( "".join([c for c in self.params if c in self.valid_chars]) )
 
501
        except ValueError:
 
502
            self.size = None
 
503
 
 
504
        if self.size is None:
 
505
            return u""
 
506
 
 
507
        self.size = self.validate_size(self.size)
 
508
 
 
509
        return u'<span style="font-size:%spx">' % self.size
 
510
 
 
511
    def render_close(self, parser, node_index):
 
512
 
 
513
        if self.size is None:
 
514
            return u""
 
515
 
 
516
        return u'</span>'
 
517
 
 
518
    def validate_size(self, size):
 
519
 
 
520
        size = min(64, size)
 
521
        size = max(4, size)
 
522
        return size
 
523
 
 
524
 
 
525
class ColorTag(TagBase):
 
526
 
 
527
    valid_chars = frozenset("#0123456789abcdefghijklmnopqrstuvwxyz")
 
528
 
 
529
    def __init__(self, name, **kwargs):
 
530
        TagBase.__init__(self, name, inline=True)
 
531
 
 
532
    def render_open(self, parser, node_index):
 
533
 
 
534
        valid_chars = self.valid_chars
 
535
        color = self.params.split()[0:1][0].lower()
 
536
        self.color = "".join([c for c in color if c in valid_chars])
 
537
 
 
538
        if not self.color:
 
539
            return u""
 
540
 
 
541
        return u'<span style="color:%s">' % self.color
 
542
 
 
543
    def render_close(self, parser, node_index):
 
544
 
 
545
        if not self.color:
 
546
            return u''
 
547
        return u'</span>'
 
548
 
 
549
 
 
550
class CenterTag(TagBase):
 
551
 
 
552
    def render_open(self, parser, node_index, **kwargs):
 
553
 
 
554
        return u'<div style="text-align:center">'
 
555
 
 
556
 
 
557
    def render_close(self, parser, node_index):
 
558
 
 
559
        return u'</div>'
 
560
 
 
561
# http://effbot.org/zone/python-replace.htm
 
562
class MultiReplace:
 
563
 
 
564
    def __init__(self, repl_dict):
 
565
 
 
566
        # string to string mapping; use a regular expression
 
567
        keys = repl_dict.keys()
 
568
        keys.sort() # lexical order
 
569
        keys.reverse() # use longest match first
 
570
        pattern = u"|".join([re.escape(key) for key in keys])
 
571
        self.pattern = re.compile(pattern)
 
572
        self.dict = repl_dict
 
573
 
 
574
    def replace(self, s):
 
575
        # apply replacement dictionary to string
 
576
 
 
577
        def repl(match, get=self.dict.get):
 
578
            item = match.group(0)
 
579
            return get(item, item)
 
580
        return self.pattern.sub(repl, s)
 
581
 
 
582
    __call__ = replace
 
583
 
 
584
 
 
585
def _escape(s):
 
586
    return PostMarkup.standard_replace(s.rstrip('\n'))
 
587
 
 
588
def _escape_no_breaks(s):
 
589
    return PostMarkup.standard_replace_no_break(s.rstrip('\n'))
 
590
 
 
591
class TagFactory(object):
 
592
 
 
593
    def __init__(self):
 
594
 
 
595
        self.tags = {}
 
596
 
 
597
    @classmethod
 
598
    def tag_factory_callable(cls, tag_class, name, *args, **kwargs):
 
599
        """
 
600
        Returns a callable that returns a new tag instance.
 
601
        """
 
602
        def make():
 
603
            return tag_class(name, *args, **kwargs)
 
604
 
 
605
        return make
 
606
 
 
607
 
 
608
    def add_tag(self, cls, name, *args, **kwargs):
 
609
 
 
610
        self.tags[name] = self.tag_factory_callable(cls, name, *args, **kwargs)
 
611
 
 
612
    def __getitem__(self, name):
 
613
 
 
614
        return self.tags[name]()
 
615
 
 
616
    def __contains__(self, name):
 
617
 
 
618
        return name in self.tags
 
619
 
 
620
    def get(self, name, default=None):
 
621
 
 
622
        if name in self.tags:
 
623
            return self.tags[name]()
 
624
 
 
625
        return default
 
626
 
 
627
 
 
628
class _Parser(object):
 
629
 
 
630
    """ This is an interface to the parser, used by Tag classes. """
 
631
 
 
632
    def __init__(self, post_markup):
 
633
 
 
634
        self.pm = post_markup
 
635
        self.tag_data = {}
 
636
        self.render_node_index = 0
 
637
 
 
638
    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"
 
642
        self.render_node_index = node_index
 
643
 
 
644
    def get_text_nodes(self, node1, node2):
 
645
 
 
646
        """ Retrieves the text nodes between two node indices. """
 
647
 
 
648
        if node2 is None:
 
649
            node2 = node1+1
 
650
 
 
651
        return [node for node in self.nodes[node1:node2] if not callable(node)]
 
652
 
 
653
    def begin_no_breaks(self):
 
654
 
 
655
        """Disables replacing of newlines with break tags at the start and end of text nodes.
 
656
        Can only be called from a tags 'open' method.
 
657
 
 
658
        """
 
659
        assert self.phase==1, "Can not be called from render_open or render_close"
 
660
        self.no_breaks_count += 1
 
661
 
 
662
    def end_no_breaks(self):
 
663
 
 
664
        """Re-enables auto-replacing of newlines with break tags (see begin_no_breaks)."""
 
665
 
 
666
        assert self.phase==1, "Can not be called from render_open or render_close"
 
667
        if self.no_breaks_count:
 
668
            self.no_breaks_count -= 1
 
669
 
 
670
 
 
671
class PostMarkup(object):
 
672
 
 
673
    standard_replace = MultiReplace({   u'<':u'&lt;',
 
674
                                        u'>':u'&gt;',
 
675
                                        u'&':u'&amp;',
 
676
                                        u'\n':u'<br/>'})
 
677
 
 
678
    standard_replace_no_break = MultiReplace({  u'<':u'&lt;',
 
679
                                                u'>':u'&gt;',
 
680
                                                u'&':u'&amp;',})
 
681
 
 
682
    TOKEN_TAG, TOKEN_PTAG, TOKEN_TEXT = range(3)
 
683
 
 
684
 
 
685
    # I tried to use RE's. Really I did.
 
686
    @classmethod
 
687
    def tokenize(cls, post):
 
688
 
 
689
        text = True
 
690
        pos = 0
 
691
 
 
692
        def find_first(post, pos, c):
 
693
            f1 = post.find(c[0], pos)
 
694
            f2 = post.find(c[1], pos)
 
695
            if f1 == -1:
 
696
                return f2
 
697
            if f2 == -1:
 
698
                return f1
 
699
            return min(f1, f2)
 
700
 
 
701
        while True:
 
702
 
 
703
            brace_pos = post.find(u'[', pos)
 
704
            if brace_pos == -1:
 
705
                if pos<len(post):
 
706
                    yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
 
707
                return
 
708
            if brace_pos - pos > 0:
 
709
                yield PostMarkup.TOKEN_TEXT, post[pos:brace_pos], pos, brace_pos
 
710
 
 
711
            pos = brace_pos
 
712
            end_pos = pos+1
 
713
 
 
714
            open_tag_pos = post.find(u'[', end_pos)
 
715
            end_pos = find_first(post, end_pos, u']=')
 
716
            if end_pos == -1:
 
717
                yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
 
718
                return
 
719
 
 
720
            if open_tag_pos != -1 and open_tag_pos < end_pos:
 
721
                yield PostMarkup.TOKEN_TEXT, post[pos:open_tag_pos], pos, open_tag_pos
 
722
                end_pos = open_tag_pos
 
723
                pos = end_pos
 
724
                continue
 
725
 
 
726
            if post[end_pos] == ']':
 
727
                yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
 
728
                pos = end_pos+1
 
729
                continue
 
730
 
 
731
            if post[end_pos] == '=':
 
732
                try:
 
733
                    end_pos += 1
 
734
                    while post[end_pos] == ' ':
 
735
                        end_pos += 1
 
736
                    if post[end_pos] != '"':
 
737
                        end_pos = post.find(u']', end_pos+1)
 
738
                        if end_pos == -1:
 
739
                            return
 
740
                        yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
 
741
                    else:
 
742
                        end_pos = find_first(post, end_pos, u'"]')
 
743
 
 
744
                        if end_pos==-1:
 
745
                            return
 
746
                        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
 
754
                        else:
 
755
                            yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos
 
756
                    pos = end_pos+1
 
757
                except IndexError:
 
758
                    return
 
759
 
 
760
    def tagify_urls(self, postmarkup ):
 
761
 
 
762
        """ Surrounds urls with url bbcode tags. """
 
763
 
 
764
        def repl(match):
 
765
            return u'[url]%s[/url]' % match.group(0)
 
766
 
 
767
        text_tokens = []
 
768
        for tag_type, tag_token, start_pos, end_pos in self.tokenize(postmarkup):
 
769
 
 
770
            if tag_type == PostMarkup.TOKEN_TEXT:
 
771
                text_tokens.append(re_url.sub(repl, tag_token))
 
772
            else:
 
773
                text_tokens.append(tag_token)
 
774
 
 
775
        return u"".join(text_tokens)
 
776
 
 
777
 
 
778
    def __init__(self, tag_factory=None):
 
779
 
 
780
        self.tag_factory = tag_factory or TagFactory()
 
781
 
 
782
 
 
783
    def default_tags(self):
 
784
 
 
785
        """ Add some basic tags. """
 
786
 
 
787
        add_tag = self.tag_factory.add_tag
 
788
 
 
789
        add_tag(SimpleTag, u'b', u'strong')
 
790
        add_tag(SimpleTag, u'i', u'em')
 
791
        add_tag(SimpleTag, u'u', u'u')
 
792
        add_tag(SimpleTag, u's', u's')
 
793
 
 
794
 
 
795
    def get_supported_tags(self):
 
796
 
 
797
        """ Returns a list of the supported tags. """
 
798
 
 
799
        return sorted(self.tag_factory.tags.keys())
 
800
 
 
801
 
 
802
 
 
803
    def render_to_html(self,
 
804
                       post_markup,
 
805
                       encoding="ascii",
 
806
                       exclude_tags=None,
 
807
                       auto_urls=True):
 
808
 
 
809
        """Converts Post Markup to XHTML.
 
810
 
 
811
        post_markup -- String containing bbcode.
 
812
        encoding -- Encoding of string, defaults to "ascii".
 
813
        exclude_tags -- A collection of tag names to ignore.
 
814
        auto_urls -- If True, then urls will be wrapped with url bbcode tags.
 
815
 
 
816
        """
 
817
 
 
818
        if not isinstance(post_markup, unicode):
 
819
            post_markup = unicode(post_markup, encoding, 'replace')
 
820
 
 
821
        if auto_urls:
 
822
            post_markup = self.tagify_urls(post_markup)
 
823
 
 
824
        parser = _Parser(self)
 
825
        parser.markup = post_markup
 
826
 
 
827
        if exclude_tags is None:
 
828
            exclude_tags = []
 
829
 
 
830
        tag_factory = self.tag_factory
 
831
 
 
832
 
 
833
        nodes = []
 
834
        parser.nodes = nodes
 
835
 
 
836
        parser.phase = 1
 
837
        parser.no_breaks_count = 0
 
838
        enclosed_count = 0
 
839
        open_stack = []
 
840
        tag_stack = []
 
841
        break_stack = []
 
842
        remove_next_newline = False
 
843
 
 
844
        def check_tag_stack(tag_name):
 
845
 
 
846
            for tag in reversed(tag_stack):
 
847
                if tag_name == tag.name:
 
848
                    return True
 
849
            return False
 
850
 
 
851
        def redo_break_stack():
 
852
 
 
853
            while break_stack:
 
854
                tag = break_stack.pop()
 
855
                open_tag(tag)
 
856
                tag_stack.append(tag)
 
857
 
 
858
        def break_inline_tags():
 
859
 
 
860
            while tag_stack:
 
861
                if tag_stack[-1].inline:
 
862
                    tag = tag_stack.pop()
 
863
                    close_tag(tag)
 
864
                    break_stack.append(tag)
 
865
                else:
 
866
                    break
 
867
 
 
868
        def open_tag(tag):
 
869
            def call(node_index):
 
870
                return tag.render_open(parser, node_index)
 
871
            nodes.append(call)
 
872
 
 
873
        def close_tag(tag):
 
874
            def call(node_index):
 
875
                return tag.render_close(parser, node_index)
 
876
            nodes.append(call)
 
877
 
 
878
        # Pass 1
 
879
        for tag_type, tag_token, start_pos, end_pos in self.tokenize(post_markup):
 
880
 
 
881
            raw_tag_token = tag_token
 
882
 
 
883
            if tag_type == PostMarkup.TOKEN_TEXT:
 
884
                if parser.no_breaks_count:
 
885
                    tag_token = tag_token.strip()
 
886
                    if not tag_token:
 
887
                        continue
 
888
                if remove_next_newline:
 
889
                    tag_token = tag_token.lstrip(' ')
 
890
                    if tag_token.startswith('\n'):
 
891
                        tag_token = tag_token.lstrip(' ')[1:]
 
892
                        if not tag_token:
 
893
                            continue
 
894
                    remove_next_newline = False
 
895
 
 
896
                if tag_stack and tag_stack[-1].strip_first_newline:
 
897
                    tag_token = tag_token.lstrip()
 
898
                    tag_stack[-1].strip_first_newline = False
 
899
                    if not tag_stack[-1]:
 
900
                        tag_stack.pop()
 
901
                        continue
 
902
 
 
903
                if not enclosed_count:
 
904
                    redo_break_stack()
 
905
 
 
906
                nodes.append(self.standard_replace(tag_token))
 
907
                continue
 
908
 
 
909
            elif tag_type == PostMarkup.TOKEN_TAG:
 
910
                tag_token = tag_token[1:-1].lstrip()
 
911
                if ' ' in tag_token:
 
912
                    tag_name, tag_attribs = tag_token.split(u' ', 1)
 
913
                    tag_attribs = tag_attribs.strip()
 
914
                else:
 
915
                    if '=' in tag_token:
 
916
                        tag_name, tag_attribs = tag_token.split(u'=', 1)
 
917
                        tag_attribs = tag_attribs.strip()
 
918
                    else:
 
919
                        tag_name = tag_token
 
920
                        tag_attribs = u""
 
921
            else:
 
922
                tag_token = tag_token[1:-1].lstrip()
 
923
                tag_name, tag_attribs = tag_token.split(u'=', 1)
 
924
                tag_attribs = tag_attribs.strip()[1:-1]
 
925
 
 
926
            tag_name = tag_name.strip().lower()
 
927
 
 
928
            end_tag = False
 
929
            if tag_name.startswith(u'/'):
 
930
                end_tag = True
 
931
                tag_name = tag_name[1:]
 
932
 
 
933
 
 
934
            if enclosed_count and tag_stack[-1].name != tag_name:
 
935
                continue
 
936
 
 
937
            if tag_name in exclude_tags:
 
938
                continue
 
939
 
 
940
            if not end_tag:
 
941
 
 
942
                tag = tag_factory.get(tag_name, None)
 
943
                if tag is None:
 
944
                    continue
 
945
 
 
946
                redo_break_stack()
 
947
 
 
948
                if not tag.inline:
 
949
                    break_inline_tags()
 
950
 
 
951
                tag.open(parser, tag_attribs, end_pos, len(nodes))
 
952
                if tag.enclosed:
 
953
                    enclosed_count += 1
 
954
                tag_stack.append(tag)
 
955
 
 
956
                open_tag(tag)
 
957
 
 
958
                if tag.auto_close:
 
959
                    tag = tag_stack.pop()
 
960
                    tag.close(self, start_pos, len(nodes)-1)
 
961
                    close_tag(tag)
 
962
 
 
963
            else:
 
964
 
 
965
                if break_stack and break_stack[-1].name == tag_name:
 
966
                    break_stack.pop()
 
967
                    tag.close(parser, start_pos, len(nodes))
 
968
                elif check_tag_stack(tag_name):
 
969
                    while tag_stack[-1].name != tag_name:
 
970
                        tag = tag_stack.pop()
 
971
                        break_stack.append(tag)
 
972
                        close_tag(tag)
 
973
 
 
974
                    tag = tag_stack.pop()
 
975
                    tag.close(parser, start_pos, len(nodes))
 
976
                    if tag.enclosed:
 
977
                        enclosed_count -= 1
 
978
 
 
979
                    close_tag(tag)
 
980
 
 
981
                    if not tag.inline:
 
982
                        remove_next_newline = True
 
983
 
 
984
        if tag_stack:
 
985
            redo_break_stack()
 
986
            while tag_stack:
 
987
                tag = tag_stack.pop()
 
988
                tag.close(parser, len(post_markup), len(nodes))
 
989
                if tag.enclosed:
 
990
                    enclosed_count -= 1
 
991
                close_tag(tag)
 
992
 
 
993
        parser.phase = 2
 
994
        # Pass 2
 
995
        parser.nodes = nodes
 
996
 
 
997
        text = []
 
998
        parser.render_node_index = 0
 
999
        while parser.render_node_index < len(parser.nodes):
 
1000
            i = parser.render_node_index
 
1001
            node_text = parser.nodes[i]
 
1002
            if callable(node_text):
 
1003
                node_text = node_text(i)
 
1004
            if node_text is not None:
 
1005
                text.append(node_text)
 
1006
            parser.render_node_index += 1
 
1007
 
 
1008
        return u"".join(text)
 
1009
 
 
1010
    __call__ = render_to_html
 
1011
 
 
1012
 
 
1013
 
 
1014
 
 
1015
 
 
1016
def _tests():
 
1017
 
 
1018
    import sys
 
1019
    #sys.stdout=open('test.htm', 'w')
 
1020
 
 
1021
    post_markup = create(use_pygments=True)
 
1022
 
 
1023
    tests = []
 
1024
    print """<link rel="stylesheet" href="code.css" type="text/css" />\n"""
 
1025
 
 
1026
    tests.append(']')
 
1027
    tests.append('[')
 
1028
    tests.append(':-[ Hello, [b]World[/b]')
 
1029
 
 
1030
    tests.append("[link=http://www.willmcgugan.com]My homepage[/link]")
 
1031
    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]")
 
1034
 
 
1035
    tests.append(u"[b]Hello AndrУЉ[/b]")
 
1036
    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]")
 
1041
 
 
1042
    tests.append("[quote Will said...]BBCode is very cool[/quote]")
 
1043
 
 
1044
    tests.append("""[code python]
 
1045
# A proxy object that calls a callback when converted to a string
 
1046
class TagStringify(object):
 
1047
    def __init__(self, callback, raw):
 
1048
        self.callback = callback
 
1049
        self.raw = raw
 
1050
        r[b]=3
 
1051
    def __str__(self):
 
1052
        return self.callback()
 
1053
    def __repr__(self):
 
1054
        return self.__str__()
 
1055
[/code]""")
 
1056
 
 
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]")
 
1062
    tests.append("""[list=1]
 
1063
    [*]Apples
 
1064
    [*]Oranges
 
1065
    are not the only fruit
 
1066
    [*]Pears
 
1067
[/list]""")
 
1068
    tests.append("[list=a][*]Apples[*]Oranges[*]Pears[/list]")
 
1069
    tests.append("[list=A][*]Apples[*]Oranges[*]Pears[/list]")
 
1070
 
 
1071
    long_test="""[b]Long test[/b]
 
1072
 
 
1073
New lines characters are converted to breaks."""\
 
1074
"""Tags my be [b]ove[i]rl[/b]apped[/i].
 
1075
 
 
1076
[i]Open tags will be closed.
 
1077
[b]Test[/b]"""
 
1078
 
 
1079
    tests.append(long_test)
 
1080
 
 
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]")
 
1087
    #tests = []
 
1088
    # 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]")
 
1090
 
 
1091
    tests.append('Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
 
1092
 
 
1093
    tests.append(u'[google]ЩИЮВfvЮИУАsz[/google]')
 
1094
 
 
1095
    tests.append(u'[size 30]Hello, World![/size]')
 
1096
 
 
1097
    tests.append(u'[color red]This should be red[/color]')
 
1098
    tests.append(u'[color #0f0]This should be green[/color]')
 
1099
    tests.append(u"[center]This should be in the center!")
 
1100
 
 
1101
    tests.append('Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
 
1102
 
 
1103
    #tests = []
 
1104
    tests.append('[b]Hello, [i]World[/b]! [/i]')
 
1105
 
 
1106
    tests.append('[b][center]This should be centered![/center][/b]')
 
1107
 
 
1108
    tests.append('[list][*]Hello[i][*]World![/i][/list]')
 
1109
 
 
1110
 
 
1111
    tests.append("""[list=1]
 
1112
    [*]Apples
 
1113
    [*]Oranges
 
1114
    are not the only fruit
 
1115
    [*]Pears
 
1116
[/list]""")
 
1117
 
 
1118
    tests.append("[b]urls such as http://www.willmcgugan.com are authomaticaly converted to links[/b]")
 
1119
 
 
1120
    tests.append("""
 
1121
[b]
 
1122
[code python]
 
1123
parser.markup[self.open_pos:self.close_pos]
 
1124
[/code]
 
1125
asdasdasdasdqweqwe
 
1126
""")
 
1127
 
 
1128
    tests.append("""[list 1]
 
1129
[*]Hello
 
1130
[*]World
 
1131
[/list]""")
 
1132
 
 
1133
 
 
1134
 
 
1135
    #tests=["""[b]b[i]i[/b][/i]"""]
 
1136
 
 
1137
    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"))
 
1140
        print u"<hr/>"
 
1141
        print
 
1142
 
 
1143
    print repr(post_markup('[url=<script>Attack</script>]Attack[/url]'))
 
1144
 
 
1145
    print repr(post_markup('http://www.google.com/search?as_q=bbcode&btnG=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA'))
 
1146
 
 
1147
    p = create(use_pygments=False)
 
1148
    print (p('[code]foo\nbar[/code]'))
 
1149
 
 
1150
    #print render_bbcode("[b]For the lazy, use the http://www.willmcgugan.com render_bbcode function.[/b]")
 
1151
 
 
1152
 
 
1153
def _run_unittests():
 
1154
 
 
1155
    # TODO: Expand tests for better coverage!
 
1156
 
 
1157
    import unittest
 
1158
 
 
1159
    class TestPostmarkup(unittest.TestCase):
 
1160
 
 
1161
        def testsimpletag(self):
 
1162
 
 
1163
            postmarkup = create()
 
1164
 
 
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>"),
 
1169
                     ]
 
1170
 
 
1171
            for test, result in tests:
 
1172
                self.assertEqual(postmarkup(test), result)
 
1173
 
 
1174
 
 
1175
        def testoverlap(self):
 
1176
 
 
1177
            postmarkup = create()
 
1178
 
 
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>')
 
1181
                     ]
 
1182
 
 
1183
            for test, result in tests:
 
1184
                self.assertEqual(postmarkup(test), result)
 
1185
 
 
1186
        def testlinks(self):
 
1187
 
 
1188
            postmarkup = create(annotate_links=False)
 
1189
 
 
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>')
 
1194
                     ]
 
1195
 
 
1196
            for test, result in tests:
 
1197
                self.assertEqual(postmarkup(test), result)
 
1198
 
 
1199
 
 
1200
    suite = unittest.TestLoader().loadTestsFromTestCase(TestPostmarkup)
 
1201
    unittest.TextTestRunner(verbosity=2).run(suite)
 
1202
 
 
1203
 
 
1204
 
 
1205
 
 
1206
if __name__ == "__main__":
 
1207
 
 
1208
    _tests()
 
1209
    _run_unittests()