~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to pybb/markups/postmarkup.py

  • Committer: Holger Rapp
  • Date: 2009-02-25 16:55:36 UTC
  • Revision ID: sirver@kallisto.local-20090225165536-3abfhjx8qsgtzyru
- Added my hacked version of pybb. Remerging new versions is very difficult at this point :(

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()