1
# -*- coding: UTF-8 -*-
5
Author: Will McGugan (http://www.willmcgugan.com)
11
from urllib import quote, unquote, quote_plus
12
from urlparse import urlparse, urlunparse
14
pygments_available = True
16
from pygments import highlight
17
from pygments.lexers import get_lexer_by_name, ClassNotFound
18
from pygments.formatters import HtmlFormatter
20
# Make Pygments optional
21
pygments_available = False
25
def annotate_link(domain):
26
"""This function is called by the url tag. Override to disable or change behaviour.
28
domain -- Domain parsed from url
31
return u" [%s]"%_escape(domain)
34
re_url = re.compile(r"((https?):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)", re.MULTILINE| re.UNICODE)
37
re_html=re.compile('<.*?>|\&.*?\;')
39
"""Remove markup from html"""
40
return re_html.sub("", s)
42
re_excerpt = re.compile(r'\[".*?\]+?.*?\[/".*?\]+?', re.DOTALL)
43
re_remove_markup = re.compile(r'\[.*?\]', re.DOTALL)
45
def remove_markup(post):
46
"""Removes html tags from a string."""
47
return re_remove_markup.sub("", post)
49
def get_excerpt(post):
50
"""Returns an excerpt between ["] and [/"]
52
post -- BBCode string"""
54
match = re_excerpt.search(post)
57
excerpt = match.group(0)
58
excerpt = excerpt.replace(u'\n', u"<br/>")
59
return remove_markup(excerpt)
61
def strip_bbcode(bbcode):
63
""" Strips bbcode tags from a string.
65
bbcode -- A string to remove tags from
69
return u"".join([t[1] for t in PostMarkup.tokenize(bbcode) if t[0] == PostMarkup.TOKEN_TEXT])
72
def create(include=None, exclude=None, use_pygments=True, **kwargs):
74
"""Create a postmarkup object that converts bbcode to XML snippets.
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>
84
postmarkup = PostMarkup()
85
postmarkup_add_tag = postmarkup.tag_factory.add_tag
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:
91
postmarkup_add_tag(tag_class, name, *args, **kwargs)
95
add_tag(SimpleTag, 'b', 'strong')
96
add_tag(SimpleTag, 'i', 'em')
97
add_tag(SimpleTag, 'u', 'u')
98
add_tag(SimpleTag, 's', 'strike')
100
add_tag(LinkTag, 'link', **kwargs)
101
add_tag(LinkTag, 'url', **kwargs)
103
add_tag(QuoteTag, 'quote')
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)
114
add_tag(ImgTag, u'img')
115
add_tag(ListTag, u'list')
116
add_tag(ListItemTag, u'*')
118
add_tag(SizeTag, u"size")
119
add_tag(ColorTag, u"color")
120
add_tag(CenterTag, u"center")
123
assert pygments_available, "Install Pygments (http://pygments.org/) or call create with use_pygments=False"
124
add_tag(PygmentsCodeTag, u'code', **kwargs)
126
add_tag(CodeTag, u'code', **kwargs)
133
def render_bbcode(bbcode, encoding="ascii", exclude_tags=None, auto_urls=True):
135
"""Renders a bbcode string in to XHTML. This is a shortcut if you don't
136
need to customize any tags.
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
146
if _postmarkup is None:
147
_postmarkup = create(use_pygments=pygments_available)
149
return _postmarkup(bbcode, encoding, exclude_tags=exclude_tags, auto_urls=auto_urls)
152
class TagBase(object):
154
def __init__(self, name, enclosed=False, auto_close=False, inline=False, strip_first_newline=False, **kwargs):
155
"""Base class for all tags.
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.
165
self.enclosed = enclosed
166
self.auto_close = auto_close
168
self.strip_first_newline = strip_first_newline
171
self.close_pos = None
172
self.open_node_index = None
173
self.close_node_index = None
175
def open(self, parser, params, open_pos, node_index):
176
""" Called when the open tag is initially encountered. """
178
self.open_pos = open_pos
179
self.open_node_index = node_index
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
186
def render_open(self, parser, node_index):
187
""" Called to render the open tag. """
190
def render_close(self, parser, node_index):
191
""" Called to render the close tag. """
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]
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) )
202
def skip_contents(self, parser):
203
"""Skips the contents of a tag while rendering."""
204
parser.skip_to_node(self.close_node_index)
207
return '[%s]'%self.name
210
class SimpleTag(TagBase):
212
"""A tag that can be rendered with a simple substitution. """
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
219
def render_open(self, parser, node_index):
220
return u"<%s>"%self.html_name
222
def render_close(self, parser, node_index):
223
return u"</%s>"%self.html_name
226
class DivStyleTag(TagBase):
228
"""A simple tag that is replaces with a div and a style."""
230
def __init__(self, name, style, value, **kwargs):
231
TagBase.__init__(self, name)
235
def render_open(self, parser, node_index):
236
return u'<div style="%s:%s;">' % (self.style, self.value)
238
def render_close(self, parser, node_index):
242
class LinkTag(TagBase):
244
def __init__(self, name, annotate_links=True, **kwargs):
245
TagBase.__init__(self, name, inline=True)
247
self.annotate_links = annotate_links
250
def render_open(self, parser, node_index):
253
tag_data = parser.tag_data
254
nest_level = tag_data['link_nest_level'] = tag_data.setdefault('link_nest_level', 0) + 1
260
url = self.params.strip()
262
url = self.get_contents_text(parser).strip()
266
self.url = unquote(url)
268
#Disallow javascript links
269
if u"javascript:" in self.url.lower():
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'):
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)
283
self.domain = url_parsed[1].lower()
285
#Remove www for brevity
286
if self.domain.startswith(u'www.'):
287
self.domain = self.domain[4:]
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]) )
297
return u'<a href="%s">'%self.url
301
def render_close(self, parser, node_index):
303
tag_data = parser.tag_data
304
tag_data['link_nest_level'] -= 1
306
if tag_data['link_nest_level'] > 0:
310
return u'</a>'+self.annotate_link(self.domain)
314
def annotate_link(self, domain=None):
316
if domain and self.annotate_links:
317
return annotate_link(domain)
322
class QuoteTag(TagBase):
324
def __init__(self, name, **kwargs):
325
TagBase.__init__(self, name, strip_first_newline=True)
327
def open(self, parser, *args):
328
TagBase.open(self, parser, *args)
330
def close(self, parser, *args):
331
TagBase.close(self, parser, *args)
333
def render_open(self, parser, node_index):
335
return u'<blockquote><em>%s</em><br/>'%(PostMarkup.standard_replace(self.params))
337
return u'<blockquote>'
340
def render_close(self, parser, node_index):
341
return u"</blockquote>"
344
class SearchTag(TagBase):
346
def __init__(self, name, url, label="", annotate_links=True, **kwargs):
347
TagBase.__init__(self, name, inline=True)
350
self.annotate_links = annotate_links
352
def render_open(self, parser, node_idex):
357
search=self.get_contents(parser)
358
link = u'<a href="%s">' % self.url
360
return link%quote_plus(search.encode("UTF-8"))
364
def render_close(self, parser, node_index):
368
if self.annotate_links:
369
ret += annotate_link(self.label)
375
class PygmentsCodeTag(TagBase):
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
381
def render_open(self, parser, node_index):
383
contents = self.get_contents(parser)
384
self.skip_contents(parser)
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
392
formatter = HtmlFormatter(linenos=self.line_numbers, cssclass="code")
393
return highlight(contents, lexer, formatter)
397
class CodeTag(TagBase):
399
def __init__(self, name, **kwargs):
400
TagBase.__init__(self, name, enclosed=True, strip_first_newline=True)
402
def render_open(self, parser, node_index):
404
contents = _escape_no_breaks(self.get_contents(parser))
405
self.skip_contents(parser)
406
return '<div class="code"><pre>%s</pre></div>' % contents
409
class ImgTag(TagBase):
411
def __init__(self, name, **kwargs):
412
TagBase.__init__(self, name, inline=True)
414
def render_open(self, parser, node_index):
416
contents = self.get_contents(parser)
417
self.skip_contents(parser)
419
contents = strip_bbcode(contents).replace(u'"', "%22")
421
return u'<img src="%s"></img>' % contents
424
class ListTag(TagBase):
426
def __init__(self, name, **kwargs):
427
TagBase.__init__(self, name, strip_first_newline=True)
429
def open(self, parser, params, open_pos, node_index):
430
TagBase.open(self, parser, params, open_pos, node_index)
432
def close(self, parser, close_pos, node_index):
433
TagBase.close(self, parser, close_pos, node_index)
436
def render_open(self, parser, node_index):
440
tag_data = parser.tag_data
441
tag_data.setdefault("ListTag.count", 0)
443
if tag_data["ListTag.count"]:
446
tag_data["ListTag.count"] += 1
448
tag_data["ListItemTag.initial_item"]=True
450
if self.params == "1":
451
self.close_tag = u"</li></ol>"
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>'
460
self.close_tag = u"</li></ul>"
463
def render_close(self, parser, node_index):
465
tag_data = parser.tag_data
466
tag_data["ListTag.count"] -= 1
468
return self.close_tag
471
class ListItemTag(TagBase):
473
def __init__(self, name, **kwargs):
474
TagBase.__init__(self, name)
477
def render_open(self, parser, node_index):
479
tag_data = parser.tag_data
480
if not tag_data.setdefault("ListTag.count", 0):
483
if tag_data["ListItemTag.initial_item"]:
484
tag_data["ListItemTag.initial_item"] = False
490
class SizeTag(TagBase):
492
valid_chars = frozenset("0123456789")
494
def __init__(self, name, **kwargs):
495
TagBase.__init__(self, name, inline=True)
497
def render_open(self, parser, node_index):
500
self.size = int( "".join([c for c in self.params if c in self.valid_chars]) )
504
if self.size is None:
507
self.size = self.validate_size(self.size)
509
return u'<span style="font-size:%spx">' % self.size
511
def render_close(self, parser, node_index):
513
if self.size is None:
518
def validate_size(self, size):
525
class ColorTag(TagBase):
527
valid_chars = frozenset("#0123456789abcdefghijklmnopqrstuvwxyz")
529
def __init__(self, name, **kwargs):
530
TagBase.__init__(self, name, inline=True)
532
def render_open(self, parser, node_index):
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])
541
return u'<span style="color:%s">' % self.color
543
def render_close(self, parser, node_index):
550
class CenterTag(TagBase):
552
def render_open(self, parser, node_index, **kwargs):
554
return u'<div style="text-align:center">'
557
def render_close(self, parser, node_index):
561
# http://effbot.org/zone/python-replace.htm
564
def __init__(self, repl_dict):
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
574
def replace(self, s):
575
# apply replacement dictionary to string
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)
586
return PostMarkup.standard_replace(s.rstrip('\n'))
588
def _escape_no_breaks(s):
589
return PostMarkup.standard_replace_no_break(s.rstrip('\n'))
591
class TagFactory(object):
598
def tag_factory_callable(cls, tag_class, name, *args, **kwargs):
600
Returns a callable that returns a new tag instance.
603
return tag_class(name, *args, **kwargs)
608
def add_tag(self, cls, name, *args, **kwargs):
610
self.tags[name] = self.tag_factory_callable(cls, name, *args, **kwargs)
612
def __getitem__(self, name):
614
return self.tags[name]()
616
def __contains__(self, name):
618
return name in self.tags
620
def get(self, name, default=None):
622
if name in self.tags:
623
return self.tags[name]()
628
class _Parser(object):
630
""" This is an interface to the parser, used by Tag classes. """
632
def __init__(self, post_markup):
634
self.pm = post_markup
636
self.render_node_index = 0
638
def skip_to_node(self, node_index):
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
644
def get_text_nodes(self, node1, node2):
646
""" Retrieves the text nodes between two node indices. """
651
return [node for node in self.nodes[node1:node2] if not callable(node)]
653
def begin_no_breaks(self):
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.
659
assert self.phase==1, "Can not be called from render_open or render_close"
660
self.no_breaks_count += 1
662
def end_no_breaks(self):
664
"""Re-enables auto-replacing of newlines with break tags (see begin_no_breaks)."""
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
671
class PostMarkup(object):
673
standard_replace = MultiReplace({ u'<':u'<',
678
standard_replace_no_break = MultiReplace({ u'<':u'<',
682
TOKEN_TAG, TOKEN_PTAG, TOKEN_TEXT = range(3)
685
# I tried to use RE's. Really I did.
687
def tokenize(cls, post):
692
def find_first(post, pos, c):
693
f1 = post.find(c[0], pos)
694
f2 = post.find(c[1], pos)
703
brace_pos = post.find(u'[', pos)
706
yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
708
if brace_pos - pos > 0:
709
yield PostMarkup.TOKEN_TEXT, post[pos:brace_pos], pos, brace_pos
714
open_tag_pos = post.find(u'[', end_pos)
715
end_pos = find_first(post, end_pos, u']=')
717
yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
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
726
if post[end_pos] == ']':
727
yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
731
if post[end_pos] == '=':
734
while post[end_pos] == ' ':
736
if post[end_pos] != '"':
737
end_pos = post.find(u']', end_pos+1)
740
yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
742
end_pos = find_first(post, end_pos, u'"]')
746
if post[end_pos] == '"':
747
end_pos = post.find(u'"', end_pos+1)
750
end_pos = post.find(u']', end_pos+1)
753
yield PostMarkup.TOKEN_PTAG, post[pos:end_pos+1], pos, end_pos+1
755
yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos
760
def tagify_urls(self, postmarkup ):
762
""" Surrounds urls with url bbcode tags. """
765
return u'[url]%s[/url]' % match.group(0)
768
for tag_type, tag_token, start_pos, end_pos in self.tokenize(postmarkup):
770
if tag_type == PostMarkup.TOKEN_TEXT:
771
text_tokens.append(re_url.sub(repl, tag_token))
773
text_tokens.append(tag_token)
775
return u"".join(text_tokens)
778
def __init__(self, tag_factory=None):
780
self.tag_factory = tag_factory or TagFactory()
783
def default_tags(self):
785
""" Add some basic tags. """
787
add_tag = self.tag_factory.add_tag
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')
795
def get_supported_tags(self):
797
""" Returns a list of the supported tags. """
799
return sorted(self.tag_factory.tags.keys())
803
def render_to_html(self,
809
"""Converts Post Markup to XHTML.
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.
818
if not isinstance(post_markup, unicode):
819
post_markup = unicode(post_markup, encoding, 'replace')
822
post_markup = self.tagify_urls(post_markup)
824
parser = _Parser(self)
825
parser.markup = post_markup
827
if exclude_tags is None:
830
tag_factory = self.tag_factory
837
parser.no_breaks_count = 0
842
remove_next_newline = False
844
def check_tag_stack(tag_name):
846
for tag in reversed(tag_stack):
847
if tag_name == tag.name:
851
def redo_break_stack():
854
tag = break_stack.pop()
856
tag_stack.append(tag)
858
def break_inline_tags():
861
if tag_stack[-1].inline:
862
tag = tag_stack.pop()
864
break_stack.append(tag)
869
def call(node_index):
870
return tag.render_open(parser, node_index)
874
def call(node_index):
875
return tag.render_close(parser, node_index)
879
for tag_type, tag_token, start_pos, end_pos in self.tokenize(post_markup):
881
raw_tag_token = tag_token
883
if tag_type == PostMarkup.TOKEN_TEXT:
884
if parser.no_breaks_count:
885
tag_token = tag_token.strip()
888
if remove_next_newline:
889
tag_token = tag_token.lstrip(' ')
890
if tag_token.startswith('\n'):
891
tag_token = tag_token.lstrip(' ')[1:]
894
remove_next_newline = False
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]:
903
if not enclosed_count:
906
nodes.append(self.standard_replace(tag_token))
909
elif tag_type == PostMarkup.TOKEN_TAG:
910
tag_token = tag_token[1:-1].lstrip()
912
tag_name, tag_attribs = tag_token.split(u' ', 1)
913
tag_attribs = tag_attribs.strip()
916
tag_name, tag_attribs = tag_token.split(u'=', 1)
917
tag_attribs = tag_attribs.strip()
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]
926
tag_name = tag_name.strip().lower()
929
if tag_name.startswith(u'/'):
931
tag_name = tag_name[1:]
934
if enclosed_count and tag_stack[-1].name != tag_name:
937
if tag_name in exclude_tags:
942
tag = tag_factory.get(tag_name, None)
951
tag.open(parser, tag_attribs, end_pos, len(nodes))
954
tag_stack.append(tag)
959
tag = tag_stack.pop()
960
tag.close(self, start_pos, len(nodes)-1)
965
if break_stack and break_stack[-1].name == tag_name:
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)
974
tag = tag_stack.pop()
975
tag.close(parser, start_pos, len(nodes))
982
remove_next_newline = True
987
tag = tag_stack.pop()
988
tag.close(parser, len(post_markup), len(nodes))
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
1008
return u"".join(text)
1010
__call__ = render_to_html
1019
#sys.stdout=open('test.htm', 'w')
1021
post_markup = create(use_pygments=True)
1024
print """<link rel="stylesheet" href="code.css" type="text/css" />\n"""
1028
tests.append(':-[ Hello, [b]World[/b]')
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]")
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]")
1042
tests.append("[quote Will said...]BBCode is very cool[/quote]")
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
1052
return self.callback()
1054
return self.__str__()
1058
tests.append(u"[img]http://upload.wikimedia.org/wikipedia/commons"\
1059
"/6/61/Triops_longicaudatus.jpg[/img]")
1061
tests.append("[list][*]Apples[*]Oranges[*]Pears[/list]")
1062
tests.append("""[list=1]
1065
are not the only fruit
1068
tests.append("[list=a][*]Apples[*]Oranges[*]Pears[/list]")
1069
tests.append("[list=A][*]Apples[*]Oranges[*]Pears[/list]")
1071
long_test="""[b]Long test[/b]
1073
New lines characters are converted to breaks."""\
1074
"""Tags my be [b]ove[i]rl[/b]apped[/i].
1076
[i]Open tags will be closed.
1079
tests.append(long_test)
1081
tests.append("[dict]Will[/dict]")
1083
tests.append("[code unknownlanguage]10 print 'In yr code'; 20 goto 10[/code]")
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]")
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]")
1091
tests.append('Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1093
tests.append(u'[google]ЩИЮВfvЮИУАsz[/google]')
1095
tests.append(u'[size 30]Hello, World![/size]')
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!")
1101
tests.append('Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1104
tests.append('[b]Hello, [i]World[/b]! [/i]')
1106
tests.append('[b][center]This should be centered![/center][/b]')
1108
tests.append('[list][*]Hello[i][*]World![/i][/list]')
1111
tests.append("""[list=1]
1114
are not the only fruit
1118
tests.append("[b]urls such as http://www.willmcgugan.com are authomaticaly converted to links[/b]")
1123
parser.markup[self.open_pos:self.close_pos]
1128
tests.append("""[list 1]
1135
#tests=["""[b]b[i]i[/b][/i]"""]
1138
print u"<pre>%s</pre>"%str(test.encode("ascii", "xmlcharrefreplace"))
1139
print u"<p>%s</p>"%str(post_markup(test).encode("ascii", "xmlcharrefreplace"))
1143
print repr(post_markup('[url=<script>Attack</script>]Attack[/url]'))
1145
print repr(post_markup('http://www.google.com/search?as_q=bbcode&btnG=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA'))
1147
p = create(use_pygments=False)
1148
print (p('[code]foo\nbar[/code]'))
1150
#print render_bbcode("[b]For the lazy, use the http://www.willmcgugan.com render_bbcode function.[/b]")
1153
def _run_unittests():
1155
# TODO: Expand tests for better coverage!
1159
class TestPostmarkup(unittest.TestCase):
1161
def testsimpletag(self):
1163
postmarkup = create()
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>"),
1171
for test, result in tests:
1172
self.assertEqual(postmarkup(test), result)
1175
def testoverlap(self):
1177
postmarkup = create()
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>')
1183
for test, result in tests:
1184
self.assertEqual(postmarkup(test), result)
1186
def testlinks(self):
1188
postmarkup = create(annotate_links=False)
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>')
1196
for test, result in tests:
1197
self.assertEqual(postmarkup(test), result)
1200
suite = unittest.TestLoader().loadTestsFromTestCase(TestPostmarkup)
1201
unittest.TextTestRunner(verbosity=2).run(suite)
1206
if __name__ == "__main__":