21
21
pygments_available = False
25
24
def annotate_link(domain):
26
"""This function is called by the url tag. Override to disable or change behaviour.
25
"""This function is called by the url tag. Override to disable or change
28
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('<.*?>|\&.*?\;')
31
return u" [%s]" % _escape(domain)
35
r"((https?):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)", re.MULTILINE | re.UNICODE)
38
re_html = re.compile('<.*?>|\&.*?\;')
39
"""Remove markup from html"""
40
return re_html.sub("", s)
42
"""Remove markup from html."""
43
return re_html.sub('', s)
42
45
re_excerpt = re.compile(r'\[".*?\]+?.*?\[/".*?\]+?', re.DOTALL)
43
46
re_remove_markup = re.compile(r'\[.*?\]', re.DOTALL)
45
49
def remove_markup(post):
46
50
"""Removes html tags from a string."""
47
return re_remove_markup.sub("", post)
51
return re_remove_markup.sub('', post)
49
54
def get_excerpt(post):
50
55
"""Returns an excerpt between ["] and [/"]
52
post -- BBCode string"""
54
61
match = re_excerpt.search(post)
57
64
excerpt = match.group(0)
58
65
excerpt = excerpt.replace(u'\n', u"<br/>")
59
66
return remove_markup(excerpt)
61
69
def strip_bbcode(bbcode):
63
""" Strips bbcode tags from a string.
70
"""Strips bbcode tags from a string.
65
72
bbcode -- A string to remove tags from
72
79
def create(include=None, exclude=None, use_pygments=True, **kwargs):
74
80
"""Create a postmarkup object that converts bbcode to XML snippets.
76
82
include -- List or similar iterable containing the names of the tags to use
79
85
If omitted, no tags will be excluded
80
86
use_pygments -- If True, Pygments (http://pygments.org/) will be used for the code tag,
81
87
otherwise it will use <pre>code</pre>
84
91
postmarkup = PostMarkup()
91
98
postmarkup_add_tag(tag_class, name, *args, **kwargs)
95
100
add_tag(SimpleTag, 'b', 'strong')
96
101
add_tag(SimpleTag, 'i', 'em')
97
102
add_tag(SimpleTag, 'u', 'u')
120
125
add_tag(CenterTag, u"center")
123
assert pygments_available, "Install Pygments (http://pygments.org/) or call create with use_pygments=False"
128
assert pygments_available, 'Install Pygments (http://pygments.org/) or call create with use_pygments=False'
124
129
add_tag(PygmentsCodeTag, u'code', **kwargs)
126
131
add_tag(CodeTag, u'code', **kwargs)
128
133
return postmarkup
132
136
_postmarkup = None
133
def render_bbcode(bbcode, encoding="ascii", exclude_tags=None, auto_urls=True):
139
def render_bbcode(bbcode, encoding='ascii', exclude_tags=None, auto_urls=True):
135
140
"""Renders a bbcode string in to XHTML. This is a shortcut if you don't
136
141
need to customize any tags.
173
178
self.close_node_index = None
175
180
def open(self, parser, params, open_pos, node_index):
176
""" Called when the open tag is initially encountered. """
181
"""Called when the open tag is initially encountered."""
177
182
self.params = params
178
183
self.open_pos = open_pos
179
184
self.open_node_index = node_index
181
186
def close(self, parser, close_pos, node_index):
182
""" Called when the close tag is initially encountered. """
187
"""Called when the close tag is initially encountered."""
183
188
self.close_pos = close_pos
184
189
self.close_node_index = node_index
186
191
def render_open(self, parser, node_index):
187
""" Called to render the open tag. """
192
"""Called to render the open tag."""
190
195
def render_close(self, parser, node_index):
191
""" Called to render the close tag. """
196
"""Called to render the close tag."""
194
199
def get_contents(self, parser):
196
201
return parser.markup[self.open_pos:self.close_pos]
198
203
def get_contents_text(self, parser):
199
"""Returns the string between the the open and close tag, minus bbcode tags."""
200
return u"".join( parser.get_text_nodes(self.open_node_index, self.close_node_index) )
204
"""Returns the string between the the open and close tag, minus bbcode
206
return u"".join(parser.get_text_nodes(self.open_node_index, self.close_node_index))
202
208
def skip_contents(self, parser):
203
209
"""Skips the contents of a tag while rendering."""
204
210
parser.skip_to_node(self.close_node_index)
206
212
def __str__(self):
207
return '[%s]'%self.name
213
return '[%s]' % self.name
210
216
class SimpleTag(TagBase):
212
"""A tag that can be rendered with a simple substitution. """
218
"""A tag that can be rendered with a simple substitution."""
214
220
def __init__(self, name, html_name, **kwargs):
215
221
""" html_name -- the html tag to substitute."""
217
223
self.html_name = html_name
219
225
def render_open(self, parser, node_index):
220
return u"<%s>"%self.html_name
226
return u"<%s>" % self.html_name
222
228
def render_close(self, parser, node_index):
223
return u"</%s>"%self.html_name
229
return u"</%s>" % self.html_name
226
232
class DivStyleTag(TagBase):
247
253
self.annotate_links = annotate_links
250
255
def render_open(self, parser, node_index):
252
257
self.domain = u''
253
258
tag_data = parser.tag_data
254
nest_level = tag_data['link_nest_level'] = tag_data.setdefault('link_nest_level', 0) + 1
259
nest_level = tag_data['link_nest_level'] = tag_data.setdefault(
260
'link_nest_level', 0) + 1
256
262
if nest_level > 1:
262
268
url = self.get_contents_text(parser).strip()
266
272
self.url = unquote(url)
268
#Disallow javascript links
274
# Disallow javascript links
269
275
if u"javascript:" in self.url.lower():
272
#Disallow non http: links
278
# Disallow non http: links
273
279
url_parsed = urlparse(self.url)
274
280
if url_parsed[0] and not url_parsed[0].lower().startswith(u'http'):
277
#Prepend http: if it is not present
283
# Prepend http: if it is not present
278
284
if not url_parsed[0]:
279
self.url="http://"+self.url
285
self.url = 'http://' + self.url
280
286
url_parsed = urlparse(self.url)
283
289
self.domain = url_parsed[1].lower()
285
#Remove www for brevity
291
# Remove www for brevity
286
292
if self.domain.startswith(u'www.'):
287
293
self.domain = self.domain[4:]
290
296
#self.url="http:"+urlunparse( map(quote, (u"",)+url_parsed[1:]) )
291
self.url= unicode( urlunparse([quote(component.encode("utf-8"), safe='/=&?:+') for component in url_parsed]) )
297
self.url = unicode(urlunparse(
298
[quote(component.encode('utf-8'), safe='/=&?:+') for component in url_parsed]))
297
return u'<a href="%s">'%self.url
304
return u'<a href="%s">' % self.url
333
340
def render_open(self, parser, node_index):
335
return u'<blockquote><em>%s</em><br/>'%(PostMarkup.standard_replace(self.params))
342
return u'<blockquote><em>%s</em><br/>' % (PostMarkup.standard_replace(self.params))
337
344
return u'<blockquote>'
340
346
def render_close(self, parser, node_index):
341
347
return u"</blockquote>"
344
350
class SearchTag(TagBase):
346
def __init__(self, name, url, label="", annotate_links=True, **kwargs):
352
def __init__(self, name, url, label='', annotate_links=True, **kwargs):
347
353
TagBase.__init__(self, name, inline=True)
349
355
self.label = label
352
358
def render_open(self, parser, node_idex):
357
search=self.get_contents(parser)
363
search = self.get_contents(parser)
358
364
link = u'<a href="%s">' % self.url
360
return link%quote_plus(search.encode("UTF-8"))
366
return link % quote_plus(search.encode('UTF-8'))
389
395
contents = _escape(contents)
390
396
return '<div class="code"><pre>%s</pre></div>' % contents
392
formatter = HtmlFormatter(linenos=self.line_numbers, cssclass="code")
398
formatter = HtmlFormatter(linenos=self.line_numbers, cssclass='code')
393
399
return highlight(contents, lexer, formatter)
397
402
class CodeTag(TagBase):
399
404
def __init__(self, name, **kwargs):
416
421
contents = self.get_contents(parser)
417
422
self.skip_contents(parser)
419
contents = strip_bbcode(contents).replace(u'"', "%22")
424
contents = strip_bbcode(contents).replace(u'"', '%22')
421
426
return u'<img src="%s"></img>' % contents
432
437
def close(self, parser, close_pos, node_index):
433
438
TagBase.close(self, parser, close_pos, node_index)
436
440
def render_open(self, parser, node_index):
438
442
self.close_tag = u""
440
444
tag_data = parser.tag_data
441
tag_data.setdefault("ListTag.count", 0)
445
tag_data.setdefault('ListTag.count', 0)
443
if tag_data["ListTag.count"]:
447
if tag_data['ListTag.count']:
446
tag_data["ListTag.count"] += 1
448
tag_data["ListItemTag.initial_item"]=True
450
if self.params == "1":
450
tag_data['ListTag.count'] += 1
452
tag_data['ListItemTag.initial_item'] = True
454
if self.params == '1':
451
455
self.close_tag = u"</li></ol>"
452
456
return u"<ol><li>"
453
elif self.params == "a":
457
elif self.params == 'a':
454
458
self.close_tag = u"</li></ol>"
455
459
return u'<ol style="list-style-type: lower-alpha;"><li>'
456
elif self.params == "A":
460
elif self.params == 'A':
457
461
self.close_tag = u"</li></ol>"
458
462
return u'<ol style="list-style-type: upper-alpha;"><li>'
463
467
def render_close(self, parser, node_index):
465
469
tag_data = parser.tag_data
466
tag_data["ListTag.count"] -= 1
470
tag_data['ListTag.count'] -= 1
468
472
return self.close_tag
477
481
def render_open(self, parser, node_index):
479
483
tag_data = parser.tag_data
480
if not tag_data.setdefault("ListTag.count", 0):
484
if not tag_data.setdefault('ListTag.count', 0):
483
if tag_data["ListItemTag.initial_item"]:
484
tag_data["ListItemTag.initial_item"] = False
487
if tag_data['ListItemTag.initial_item']:
488
tag_data['ListItemTag.initial_item'] = False
487
491
return u"</li><li>"
490
494
class SizeTag(TagBase):
492
valid_chars = frozenset("0123456789")
496
valid_chars = frozenset('0123456789')
494
498
def __init__(self, name, **kwargs):
495
499
TagBase.__init__(self, name, inline=True)
525
530
class ColorTag(TagBase):
527
valid_chars = frozenset("#0123456789abcdefghijklmnopqrstuvwxyz")
532
valid_chars = frozenset('#0123456789abcdefghijklmnopqrstuvwxyz')
529
534
def __init__(self, name, **kwargs):
530
535
TagBase.__init__(self, name, inline=True)
554
559
return u'<div style="text-align:center">'
557
561
def render_close(self, parser, node_index):
561
565
# http://effbot.org/zone/python-replace.htm
562
568
class MultiReplace:
564
570
def __init__(self, repl_dict):
566
572
# string to string mapping; use a regular expression
567
573
keys = repl_dict.keys()
568
keys.sort() # lexical order
569
keys.reverse() # use longest match first
574
keys.sort() # lexical order
575
keys.reverse() # use longest match first
570
576
pattern = u"|".join([re.escape(key) for key in keys])
571
577
self.pattern = re.compile(pattern)
572
578
self.dict = repl_dict
598
606
def tag_factory_callable(cls, tag_class, name, *args, **kwargs):
600
Returns a callable that returns a new tag instance.
607
"""Returns a callable that returns a new tag instance."""
603
609
return tag_class(name, *args, **kwargs)
608
613
def add_tag(self, cls, name, *args, **kwargs):
610
615
self.tags[name] = self.tag_factory_callable(cls, name, *args, **kwargs)
628
633
class _Parser(object):
630
""" This is an interface to the parser, used by Tag classes. """
635
"""This is an interface to the parser, used by Tag classes."""
632
637
def __init__(self, post_markup):
636
641
self.render_node_index = 0
638
643
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"
644
"""Skips to a node, ignoring intermediate nodes."""
645
assert node_index is not None, 'Node index must be non-None'
642
646
self.render_node_index = node_index
644
648
def get_text_nodes(self, node1, node2):
646
""" Retrieves the text nodes between two node indices. """
649
"""Retrieves the text nodes between two node indices."""
648
651
if node2 is None:
651
654
return [node for node in self.nodes[node1:node2] if not callable(node)]
653
656
def begin_no_breaks(self):
657
"""Disables replacing of newlines with break tags at the start and end
655
"""Disables replacing of newlines with break tags at the start and end of text nodes.
656
660
Can only be called from a tags 'open' method.
659
assert self.phase==1, "Can not be called from render_open or render_close"
663
assert self.phase == 1, 'Can not be called from render_open or render_close'
660
664
self.no_breaks_count += 1
662
666
def end_no_breaks(self):
664
667
"""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"
669
assert self.phase == 1, 'Can not be called from render_open or render_close'
667
670
if self.no_breaks_count:
668
671
self.no_breaks_count -= 1
671
674
class PostMarkup(object):
673
standard_replace = MultiReplace({ u'<':u'<',
676
standard_replace = MultiReplace({u'<': u'<',
678
standard_replace_no_break = MultiReplace({ u'<':u'<',
681
standard_replace_no_break = MultiReplace({u'<': u'<',
682
685
TOKEN_TAG, TOKEN_PTAG, TOKEN_TEXT = range(3)
685
687
# I tried to use RE's. Really I did.
687
689
def tokenize(cls, post):
703
705
brace_pos = post.find(u'[', pos)
704
706
if brace_pos == -1:
706
708
yield PostMarkup.TOKEN_TEXT, post[pos:], pos, len(post)
708
710
if brace_pos - pos > 0:
709
711
yield PostMarkup.TOKEN_TEXT, post[pos:brace_pos], pos, brace_pos
714
716
open_tag_pos = post.find(u'[', end_pos)
715
717
end_pos = find_first(post, end_pos, u']=')
726
728
if post[end_pos] == ']':
727
yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
729
yield PostMarkup.TOKEN_TAG, post[pos:end_pos + 1], pos, end_pos + 1
731
733
if post[end_pos] == '=':
734
736
while post[end_pos] == ' ':
736
738
if post[end_pos] != '"':
737
end_pos = post.find(u']', end_pos+1)
739
end_pos = post.find(u']', end_pos + 1)
738
740
if end_pos == -1:
740
yield PostMarkup.TOKEN_TAG, post[pos:end_pos+1], pos, end_pos+1
742
yield PostMarkup.TOKEN_TAG, post[pos:end_pos + 1], pos, end_pos + 1
742
744
end_pos = find_first(post, end_pos, u'"]')
746
748
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
749
end_pos = post.find(u'"', end_pos + 1)
752
end_pos = post.find(u']', end_pos + 1)
755
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
757
yield PostMarkup.TOKEN_TAG, post[pos:end_pos + 1], pos, end_pos
757
759
except IndexError:
760
def tagify_urls(self, postmarkup ):
762
""" Surrounds urls with url bbcode tags. """
762
def tagify_urls(self, postmarkup):
763
"""Surrounds urls with url bbcode tags."""
765
766
return u'[url]%s[/url]' % match.group(0)
775
776
return u"".join(text_tokens)
778
778
def __init__(self, tag_factory=None):
780
780
self.tag_factory = tag_factory or TagFactory()
783
782
def default_tags(self):
785
""" Add some basic tags. """
783
"""Add some basic tags."""
787
785
add_tag = self.tag_factory.add_tag
791
789
add_tag(SimpleTag, u'u', u'u')
792
790
add_tag(SimpleTag, u's', u's')
795
792
def get_supported_tags(self):
797
""" Returns a list of the supported tags. """
793
"""Returns a list of the supported tags."""
799
795
return sorted(self.tag_factory.tags.keys())
803
797
def render_to_html(self,
806
800
exclude_tags=None,
809
802
"""Converts Post Markup to XHTML.
811
804
post_markup -- String containing bbcode.
1027
1015
tests.append('[')
1028
1016
tests.append(':-[ Hello, [b]World[/b]')
1030
tests.append("[link=http://www.willmcgugan.com]My homepage[/link]")
1018
tests.append('[link=http://www.willmcgugan.com]My homepage[/link]')
1031
1019
tests.append('[link="http://www.willmcgugan.com"]My homepage[/link]')
1032
tests.append("[link http://www.willmcgugan.com]My homepage[/link]")
1033
tests.append("[link]http://www.willmcgugan.com[/link]")
1020
tests.append('[link http://www.willmcgugan.com]My homepage[/link]')
1021
tests.append('[link]http://www.willmcgugan.com[/link]')
1035
1023
tests.append(u"[b]Hello AndrУЉ[/b]")
1036
1024
tests.append(u"[google]AndrУЉ[/google]")
1037
tests.append("[s]Strike through[/s]")
1038
tests.append("[b]bold [i]bold and italic[/b] italic[/i]")
1039
tests.append("[google]Will McGugan[/google]")
1040
tests.append("[wiki Will McGugan]Look up my name in Wikipedia[/wiki]")
1025
tests.append('[s]Strike through[/s]')
1026
tests.append('[b]bold [i]bold and italic[/b] italic[/i]')
1027
tests.append('[google]Will McGugan[/google]')
1028
tests.append('[wiki Will McGugan]Look up my name in Wikipedia[/wiki]')
1042
tests.append("[quote Will said...]BBCode is very cool[/quote]")
1030
tests.append('[quote Will said...]BBCode is very cool[/quote]')
1044
1032
tests.append("""[code python]
1045
1033
# A proxy object that calls a callback when converted to a string
1054
1042
return self.__str__()
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]")
1045
tests.append(u"[img]http://upload.wikimedia.org/wikipedia/commons"
1046
'/6/61/Triops_longicaudatus.jpg[/img]')
1048
tests.append('[list][*]Apples[*]Oranges[*]Pears[/list]')
1062
1049
tests.append("""[list=1]
1065
1052
are not the only fruit
1068
tests.append("[list=a][*]Apples[*]Oranges[*]Pears[/list]")
1069
tests.append("[list=A][*]Apples[*]Oranges[*]Pears[/list]")
1055
tests.append('[list=a][*]Apples[*]Oranges[*]Pears[/list]')
1056
tests.append('[list=A][*]Apples[*]Oranges[*]Pears[/list]')
1071
long_test="""[b]Long test[/b]
1058
long_test = """[b]Long test[/b]
1073
1060
New lines characters are converted to breaks."""\
1074
1061
"""Tags my be [b]ove[i]rl[/b]apped[/i].
1079
1066
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]")
1068
tests.append('[dict]Will[/dict]')
1071
"[code unknownlanguage]10 print 'In yr code'; 20 goto 10[/code]")
1074
'[url=http://www.google.com/coop/cse?cx=006850030468302103399%3Amqxv78bdfdo]CakePHP Google Groups[/url]')
1075
tests.append('[url=http://www.google.com/search?hl=en&safe=off&client=opera&rls=en&hs=pO1&q=python+bbcode&btnG=Search]Search for Python BBCode[/url]')
1088
1077
# Attempt to inject html in to unicode
1089
tests.append("[url=http://www.test.com/sfsdfsdf/ter?t=\"></a><h1>HACK</h1><a>\"]Test Hack[/url]")
1079
"[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.')
1082
'Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1093
1084
tests.append(u'[google]ЩИЮВfvЮИУАsz[/google]')
1098
1089
tests.append(u'[color #0f0]This should be green[/color]')
1099
1090
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.')
1093
'Nested urls, i.e. [url][url]www.becontrary.com[/url][/url], are condensed in to a single tag.')
1104
1096
tests.append('[b]Hello, [i]World[/b]! [/i]')
1135
#tests=["""[b]b[i]i[/b][/i]"""]
1125
# tests=["""[b]b[i]i[/b][/i]"""]
1137
1127
for test in tests:
1138
print u"<pre>%s</pre>"%str(test.encode("ascii", "xmlcharrefreplace"))
1139
print u"<p>%s</p>"%str(post_markup(test).encode("ascii", "xmlcharrefreplace"))
1128
print u"<pre>%s</pre>" % str(test.encode('ascii', 'xmlcharrefreplace'))
1129
print u"<p>%s</p>" % str(post_markup(test).encode('ascii', 'xmlcharrefreplace'))
1145
1135
print repr(post_markup('http://www.google.com/search?as_q=bbcode&btnG=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA'))
1147
1137
p = create(use_pygments=False)
1148
print (p('[code]foo\nbar[/code]'))
1138
print(p('[code]foo\nbar[/code]'))
1150
#print render_bbcode("[b]For the lazy, use the http://www.willmcgugan.com render_bbcode function.[/b]")
1140
# print render_bbcode("[b]For the lazy, use the http://www.willmcgugan.com
1141
# render_bbcode function.[/b]")
1153
1144
def _run_unittests():
1163
1154
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>"),
1156
tests = [('[b]Hello[/b]', '<strong>Hello</strong>'),
1157
('[i]Italic[/i]', '<em>Italic</em>'),
1158
('[s]Strike[/s]', '<strike>Strike</strike>'),
1159
('[u]underlined[/u]', '<u>underlined</u>'),
1171
1162
for test, result in tests:
1172
1163
self.assertEqual(postmarkup(test), result)
1175
1165
def testoverlap(self):
1177
1167
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>')
1169
tests = [('[i][b]Hello[/i][/b]', '<em><strong>Hello</strong></em>'),
1170
('[b]bold [u]both[/b] underline[/u]',
1171
'<strong>bold <u>both</u></strong><u> underline</u>')
1183
1174
for test, result in tests:
1188
1179
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>')
1181
tests = [('[link=http://www.willmcgugan.com]blog1[/link]', '<a href="http://www.willmcgugan.com">blog1</a>'),
1182
('[link="http://www.willmcgugan.com"]blog2[/link]',
1183
'<a href="http://www.willmcgugan.com">blog2</a>'),
1184
('[link http://www.willmcgugan.com]blog3[/link]',
1185
'<a href="http://www.willmcgugan.com">blog3</a>'),
1186
('[link]http://www.willmcgugan.com[/link]',
1187
'<a href="http://www.willmcgugan.com">http://www.willmcgugan.com</a>')
1196
1190
for test, result in tests:
1197
1191
self.assertEqual(postmarkup(test), result)
1200
1193
suite = unittest.TestLoader().loadTestsFromTestCase(TestPostmarkup)
1201
1194
unittest.TextTestRunner(verbosity=2).run(suite)
1206
if __name__ == "__main__":
1197
if __name__ == '__main__':
1209
1200
_run_unittests()