1"""
2Python Markdown
3
4A Python implementation of John Gruber's Markdown.
5
6Documentation: https://python-markdown.github.io/
7GitHub: https://github.com/Python-Markdown/markdown/
8PyPI: https://pypi.org/project/Markdown/
9
10Started by Manfred Stienstra (http://www.dwerg.net/).
11Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
12Currently maintained by Waylan Limberg (https://github.com/waylan),
13Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
14
15Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later)
16Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
17Copyright 2004 Manfred Stienstra (the original version)
18
19License: BSD (see LICENSE.md for details).
20
21Python-Markdown Regression Tests
22================================
23
24Tests of the various APIs with the python markdown lib.
25"""
26
27import unittest
28import sys
29import os
30import markdown
31import warnings
32from markdown.__main__ import parse_options
33from logging import DEBUG, WARNING, CRITICAL
34import yaml
35import tempfile
36from io import BytesIO
37import xml.etree.ElementTree as etree
38from xml.etree.ElementTree import ProcessingInstruction
39
40
41class TestMarkdownBasics(unittest.TestCase):
42    """ Tests basics of the Markdown class. """
43
44    def setUp(self):
45        """ Create instance of Markdown. """
46        self.md = markdown.Markdown()
47
48    def testBlankInput(self):
49        """ Test blank input. """
50        self.assertEqual(self.md.convert(''), '')
51
52    def testWhitespaceOnly(self):
53        """ Test input of only whitespace. """
54        self.assertEqual(self.md.convert(' '), '')
55
56    def testSimpleInput(self):
57        """ Test simple input. """
58        self.assertEqual(self.md.convert('foo'), '<p>foo</p>')
59
60    def testInstanceExtension(self):
61        """ Test Extension loading with a class instance. """
62        from markdown.extensions.footnotes import FootnoteExtension
63        markdown.Markdown(extensions=[FootnoteExtension()])
64
65    def testEntryPointExtension(self):
66        """ Test Extension loading with an entry point. """
67        markdown.Markdown(extensions=['footnotes'])
68
69    def testDotNotationExtension(self):
70        """ Test Extension loading with Name (`path.to.module`). """
71        markdown.Markdown(extensions=['markdown.extensions.footnotes'])
72
73    def testDotNotationExtensionWithClass(self):
74        """ Test Extension loading with class name (`path.to.module:Class`). """
75        markdown.Markdown(extensions=['markdown.extensions.footnotes:FootnoteExtension'])
76
77
78class TestConvertFile(unittest.TestCase):
79    """ Tests of ConvertFile. """
80
81    def setUp(self):
82        self.saved = sys.stdin, sys.stdout
83        sys.stdin = BytesIO(bytes('foo', encoding='utf-8'))
84        sys.stdout = BytesIO()
85
86    def tearDown(self):
87        sys.stdin, sys.stdout = self.saved
88
89    def getTempFiles(self, src):
90        """ Return the file names for two temp files. """
91        infd, infile = tempfile.mkstemp(suffix='.txt')
92        with os.fdopen(infd, 'w') as fp:
93            fp.write(src)
94        outfd, outfile = tempfile.mkstemp(suffix='.html')
95        return infile, outfile, outfd
96
97    def testFileNames(self):
98        infile, outfile, outfd = self.getTempFiles('foo')
99        markdown.markdownFromFile(input=infile, output=outfile)
100        with os.fdopen(outfd, 'r') as fp:
101            output = fp.read()
102        self.assertEqual(output, '<p>foo</p>')
103
104    def testFileObjects(self):
105        infile = BytesIO(bytes('foo', encoding='utf-8'))
106        outfile = BytesIO()
107        markdown.markdownFromFile(input=infile, output=outfile)
108        outfile.seek(0)
109        self.assertEqual(outfile.read().decode('utf-8'), '<p>foo</p>')
110
111    def testStdinStdout(self):
112        markdown.markdownFromFile()
113        sys.stdout.seek(0)
114        self.assertEqual(sys.stdout.read().decode('utf-8'), '<p>foo</p>')
115
116
117class TestBlockParser(unittest.TestCase):
118    """ Tests of the BlockParser class. """
119
120    def setUp(self):
121        """ Create instance of BlockParser. """
122        self.parser = markdown.Markdown().parser
123
124    def testParseChunk(self):
125        """ Test BlockParser.parseChunk. """
126        root = etree.Element("div")
127        text = 'foo'
128        self.parser.parseChunk(root, text)
129        self.assertEqual(
130            markdown.serializers.to_xhtml_string(root),
131            "<div><p>foo</p></div>"
132        )
133
134    def testParseDocument(self):
135        """ Test BlockParser.parseDocument. """
136        lines = ['#foo', '', 'bar', '', '    baz']
137        tree = self.parser.parseDocument(lines)
138        self.assertIsInstance(tree, etree.ElementTree)
139        self.assertIs(etree.iselement(tree.getroot()), True)
140        self.assertEqual(
141            markdown.serializers.to_xhtml_string(tree.getroot()),
142            "<div><h1>foo</h1><p>bar</p><pre><code>baz\n</code></pre></div>"
143        )
144
145
146class TestBlockParserState(unittest.TestCase):
147    """ Tests of the State class for BlockParser. """
148
149    def setUp(self):
150        self.state = markdown.blockparser.State()
151
152    def testBlankState(self):
153        """ Test State when empty. """
154        self.assertEqual(self.state, [])
155
156    def testSetSate(self):
157        """ Test State.set(). """
158        self.state.set('a_state')
159        self.assertEqual(self.state, ['a_state'])
160        self.state.set('state2')
161        self.assertEqual(self.state, ['a_state', 'state2'])
162
163    def testIsSate(self):
164        """ Test State.isstate(). """
165        self.assertEqual(self.state.isstate('anything'), False)
166        self.state.set('a_state')
167        self.assertEqual(self.state.isstate('a_state'), True)
168        self.state.set('state2')
169        self.assertEqual(self.state.isstate('state2'), True)
170        self.assertEqual(self.state.isstate('a_state'), False)
171        self.assertEqual(self.state.isstate('missing'), False)
172
173    def testReset(self):
174        """ Test State.reset(). """
175        self.state.set('a_state')
176        self.state.reset()
177        self.assertEqual(self.state, [])
178        self.state.set('state1')
179        self.state.set('state2')
180        self.state.reset()
181        self.assertEqual(self.state, ['state1'])
182
183
184class TestHtmlStash(unittest.TestCase):
185    """ Test Markdown's HtmlStash. """
186
187    def setUp(self):
188        self.stash = markdown.util.HtmlStash()
189        self.placeholder = self.stash.store('foo')
190
191    def testSimpleStore(self):
192        """ Test HtmlStash.store. """
193        self.assertEqual(self.placeholder, self.stash.get_placeholder(0))
194        self.assertEqual(self.stash.html_counter, 1)
195        self.assertEqual(self.stash.rawHtmlBlocks, ['foo'])
196
197    def testStoreMore(self):
198        """ Test HtmlStash.store with additional blocks. """
199        placeholder = self.stash.store('bar')
200        self.assertEqual(placeholder, self.stash.get_placeholder(1))
201        self.assertEqual(self.stash.html_counter, 2)
202        self.assertEqual(
203            self.stash.rawHtmlBlocks,
204            ['foo', 'bar']
205        )
206
207    def testReset(self):
208        """ Test HtmlStash.reset. """
209        self.stash.reset()
210        self.assertEqual(self.stash.html_counter, 0)
211        self.assertEqual(self.stash.rawHtmlBlocks, [])
212
213
214class Item:
215    """ A dummy Registry item object for testing. """
216    def __init__(self, data):
217        self.data = data
218
219    def __repr__(self):
220        return repr(self.data)
221
222    def __eq__(self, other):
223        return self.data == other
224
225
226class RegistryTests(unittest.TestCase):
227    """ Test the processor registry. """
228
229    def testCreateRegistry(self):
230        r = markdown.util.Registry()
231        r.register(Item('a'), 'a', 20)
232        self.assertEqual(len(r), 1)
233        self.assertIsInstance(r, markdown.util.Registry)
234
235    def testRegisterWithoutPriority(self):
236        r = markdown.util.Registry()
237        with self.assertRaises(TypeError):
238            r.register(Item('a'))
239
240    def testSortRegistry(self):
241        r = markdown.util.Registry()
242        r.register(Item('a'), 'a', 20)
243        r.register(Item('b'), 'b', 21)
244        r.register(Item('c'), 'c', 20.5)
245        self.assertEqual(len(r), 3)
246        self.assertEqual(list(r), ['b', 'c', 'a'])
247
248    def testIsSorted(self):
249        r = markdown.util.Registry()
250        self.assertIs(r._is_sorted, False)
251        r.register(Item('a'), 'a', 20)
252        list(r)
253        self.assertIs(r._is_sorted, True)
254        r.register(Item('b'), 'b', 21)
255        self.assertIs(r._is_sorted, False)
256        r['a']
257        self.assertIs(r._is_sorted, True)
258        r._is_sorted = False
259        r.get_index_for_name('a')
260        self.assertIs(r._is_sorted, True)
261        r._is_sorted = False
262        repr(r)
263        self.assertIs(r._is_sorted, True)
264
265    def testDeregister(self):
266        r = markdown.util.Registry()
267        r.register(Item('a'), 'a',  20)
268        r.register(Item('b'), 'b', 30)
269        r.register(Item('c'), 'c', 40)
270        self.assertEqual(len(r), 3)
271        r.deregister('b')
272        self.assertEqual(len(r), 2)
273        r.deregister('c', strict=False)
274        self.assertEqual(len(r), 1)
275        # deregister non-existant item with strict=False
276        r.deregister('d', strict=False)
277        self.assertEqual(len(r), 1)
278        with self.assertRaises(ValueError):
279            # deregister non-existant item with strict=True
280            r.deregister('e')
281        self.assertEqual(list(r), ['a'])
282
283    def testRegistryContains(self):
284        r = markdown.util.Registry()
285        item = Item('a')
286        r.register(item, 'a', 20)
287        self.assertIs('a' in r, True)
288        self.assertIn(item, r)
289        self.assertNotIn('b', r)
290
291    def testRegistryIter(self):
292        r = markdown.util.Registry()
293        r.register(Item('a'), 'a', 20)
294        r.register(Item('b'), 'b', 30)
295        self.assertEqual(list(r), ['b', 'a'])
296
297    def testRegistryGetItemByIndex(self):
298        r = markdown.util.Registry()
299        r.register(Item('a'), 'a', 20)
300        r.register(Item('b'), 'b', 30)
301        self.assertEqual(r[0], 'b')
302        self.assertEqual(r[1], 'a')
303        with self.assertRaises(IndexError):
304            r[3]
305
306    def testRegistryGetItemByItem(self):
307        r = markdown.util.Registry()
308        r.register(Item('a'), 'a', 20)
309        r.register(Item('b'), 'b', 30)
310        self.assertEqual(r['a'], 'a')
311        self.assertEqual(r['b'], 'b')
312        with self.assertRaises(KeyError):
313            r['c']
314
315    def testRegistrySetItem(self):
316        r = markdown.util.Registry()
317        with self.assertRaises(TypeError):
318            r[0] = 'a'
319        # TODO: restore this when deprecated __setitem__ is removed.
320        # with self.assertRaises(TypeError):
321        #     r['a'] = 'a'
322        # TODO: remove this when deprecated __setitem__ is removed.
323        with warnings.catch_warnings(record=True) as w:
324            warnings.simplefilter("always")
325
326            r['a'] = Item('a')
327            self.assertEqual(list(r), ['a'])
328            r['b'] = Item('b')
329            self.assertEqual(list(r), ['a', 'b'])
330            r['a'] = Item('a1')
331            self.assertEqual(list(r), ['a1', 'b'])
332
333            # Check the warnings
334            self.assertEqual(len(w), 3)
335            self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w))
336
337    def testRegistryDelItem(self):
338        r = markdown.util.Registry()
339        r.register(Item('a'), 'a', 20)
340        with self.assertRaises(KeyError):
341            del r[0]
342        # TODO: restore this when deprecated __del__ is removed.
343        # with self.assertRaises(TypeError):
344        #     del r['a']
345        # TODO: remove this when deprecated __del__ is removed.
346        with warnings.catch_warnings(record=True) as w:
347            warnings.simplefilter("always")
348
349            r.register(Item('b'), 'b', 15)
350            r.register(Item('c'), 'c', 10)
351            del r['b']
352            self.assertEqual(list(r), ['a', 'c'])
353            del r['a']
354            self.assertEqual(list(r), ['c'])
355            with self.assertRaises(KeyError):
356                del r['badname']
357            del r['c']
358            self.assertEqual(list(r), [])
359
360            # Check the warnings
361            self.assertEqual(len(w), 3)
362            self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w))
363
364    def testRegistrySlice(self):
365        r = markdown.util.Registry()
366        r.register(Item('a'), 'a', 20)
367        r.register(Item('b'), 'b', 30)
368        r.register(Item('c'), 'c', 40)
369        slc = r[1:]
370        self.assertEqual(len(slc), 2)
371        self.assertIsInstance(slc, markdown.util.Registry)
372        self.assertEqual(list(slc), ['b', 'a'])
373
374    def testGetIndexForName(self):
375        r = markdown.util.Registry()
376        r.register(Item('a'), 'a', 20)
377        r.register(Item('b'), 'b', 30)
378        self.assertEqual(r.get_index_for_name('a'), 1)
379        self.assertEqual(r.get_index_for_name('b'), 0)
380        with self.assertRaises(ValueError):
381            r.get_index_for_name('c')
382
383    def testRegisterDupplicate(self):
384        r = markdown.util.Registry()
385        r.register(Item('a'), 'a', 20)
386        r.register(Item('b1'), 'b', 10)
387        self.assertEqual(list(r), ['a', 'b1'])
388        self.assertEqual(len(r), 2)
389        r.register(Item('b2'), 'b', 30)
390        self.assertEqual(len(r), 2)
391        self.assertEqual(list(r), ['b2', 'a'])
392
393    def testRegistryDeprecatedAdd(self):
394        with warnings.catch_warnings(record=True) as w:
395            warnings.simplefilter("always")
396
397            r = markdown.util.Registry()
398            # Add first item
399            r.add('c', Item('c'), '_begin')
400            self.assertEqual(list(r), ['c'])
401            # Added to beginning
402            r.add('b', Item('b'), '_begin')
403            self.assertEqual(list(r), ['b', 'c'])
404            # Add before first item
405            r.add('a', Item('a'), '<b')
406            self.assertEqual(list(r), ['a', 'b', 'c'])
407            # Add before non-first item
408            r.add('a1', Item('a1'), '<b')
409            self.assertEqual(list(r), ['a', 'a1', 'b', 'c'])
410            # Add after non-last item
411            r.add('b1', Item('b1'), '>b')
412            self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c'])
413            # Add after last item
414            r.add('d', Item('d'), '>c')
415            self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c', 'd'])
416            # Add to end
417            r.add('e', Item('e'), '_end')
418            self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c', 'd', 'e'])
419            with self.assertRaises(ValueError):
420                r.add('f', Item('f'), 'badlocation')
421
422            # Check the warnings
423            self.assertEqual(len(w), 7)
424            self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w))
425
426
427class TestErrors(unittest.TestCase):
428    """ Test Error Reporting. """
429
430    def setUp(self):
431        # Set warnings to be raised as errors
432        warnings.simplefilter('error')
433
434    def tearDown(self):
435        # Reset warning behavior back to default
436        warnings.simplefilter('default')
437
438    def testBadOutputFormat(self):
439        """ Test failure on bad output_format. """
440        self.assertRaises(KeyError, markdown.Markdown, output_format='invalid')
441
442    def testLoadExtensionFailure(self):
443        """ Test failure of an extension to load. """
444        self.assertRaises(
445            ImportError,
446            markdown.Markdown, extensions=['non_existant_ext']
447        )
448
449    def testLoadBadExtension(self):
450        """ Test loading of an Extension with no makeExtension function. """
451        self.assertRaises(AttributeError, markdown.Markdown, extensions=['markdown.util'])
452
453    def testNonExtension(self):
454        """ Test loading a non Extension object as an extension. """
455        self.assertRaises(TypeError, markdown.Markdown, extensions=[object])
456
457    def testDotNotationExtensionWithBadClass(self):
458        """ Test Extension loading with non-existant class name (`path.to.module:Class`). """
459        self.assertRaises(
460            AttributeError,
461            markdown.Markdown,
462            extensions=['markdown.extensions.footnotes:MissingExtension']
463        )
464
465    def testBaseExtention(self):
466        """ Test that the base Extension class will raise NotImplemented. """
467        self.assertRaises(
468            NotImplementedError,
469            markdown.Markdown, extensions=[markdown.extensions.Extension()]
470        )
471
472
473class testETreeComments(unittest.TestCase):
474    """
475    Test that ElementTree Comments work.
476
477    These tests should only be a concern when using cElementTree with third
478    party serializers (including markdown's (x)html serializer). While markdown
479    doesn't use ElementTree.Comment itself, we should certainly support any
480    third party extensions which may. Therefore, these tests are included to
481    ensure such support is maintained.
482    """
483
484    def setUp(self):
485        # Create comment node
486        self.comment = etree.Comment('foo')
487
488    def testCommentIsComment(self):
489        """ Test that an ElementTree Comment passes the `is Comment` test. """
490        self.assertIs(self.comment.tag, etree.Comment)
491
492    def testCommentIsBlockLevel(self):
493        """ Test that an ElementTree Comment is recognized as BlockLevel. """
494        md = markdown.Markdown()
495        self.assertIs(md.is_block_level(self.comment.tag), False)
496
497    def testCommentSerialization(self):
498        """ Test that an ElementTree Comment serializes properly. """
499        self.assertEqual(
500            markdown.serializers.to_html_string(self.comment),
501            '<!--foo-->'
502        )
503
504    def testCommentPrettify(self):
505        """ Test that an ElementTree Comment is prettified properly. """
506        pretty = markdown.treeprocessors.PrettifyTreeprocessor(markdown.Markdown())
507        pretty.run(self.comment)
508        self.assertEqual(
509            markdown.serializers.to_html_string(self.comment),
510            '<!--foo-->\n'
511        )
512
513
514class testElementTailTests(unittest.TestCase):
515    """ Element Tail Tests """
516    def setUp(self):
517        self.pretty = markdown.treeprocessors.PrettifyTreeprocessor(markdown.Markdown())
518
519    def testBrTailNoNewline(self):
520        """ Test that last <br> in tree has a new line tail """
521        root = etree.Element('root')
522        br = etree.SubElement(root, 'br')
523        self.assertEqual(br.tail, None)
524        self.pretty.run(root)
525        self.assertEqual(br.tail, "\n")
526
527
528class testSerializers(unittest.TestCase):
529    """ Test the html and xhtml serializers. """
530
531    def testHtml(self):
532        """ Test HTML serialization. """
533        el = etree.Element('div')
534        el.set('id', 'foo<&">')
535        p = etree.SubElement(el, 'p')
536        p.text = 'foo <&escaped>'
537        p.set('hidden', 'hidden')
538        etree.SubElement(el, 'hr')
539        non_element = etree.SubElement(el, None)
540        non_element.text = 'non-element text'
541        script = etree.SubElement(non_element, 'script')
542        script.text = '<&"test\nescaping">'
543        el.tail = "tail text"
544        self.assertEqual(
545            markdown.serializers.to_html_string(el),
546            '<div id="foo&lt;&amp;&quot;&gt;">'
547            '<p hidden>foo &lt;&amp;escaped&gt;</p>'
548            '<hr>'
549            'non-element text'
550            '<script><&"test\nescaping"></script>'
551            '</div>tail text'
552        )
553
554    def testXhtml(self):
555        """" Test XHTML serialization. """
556        el = etree.Element('div')
557        el.set('id', 'foo<&">')
558        p = etree.SubElement(el, 'p')
559        p.text = 'foo<&escaped>'
560        p.set('hidden', 'hidden')
561        etree.SubElement(el, 'hr')
562        non_element = etree.SubElement(el, None)
563        non_element.text = 'non-element text'
564        script = etree.SubElement(non_element, 'script')
565        script.text = '<&"test\nescaping">'
566        el.tail = "tail text"
567        self.assertEqual(
568            markdown.serializers.to_xhtml_string(el),
569            '<div id="foo&lt;&amp;&quot;&gt;">'
570            '<p hidden="hidden">foo&lt;&amp;escaped&gt;</p>'
571            '<hr />'
572            'non-element text'
573            '<script><&"test\nescaping"></script>'
574            '</div>tail text'
575        )
576
577    def testMixedCaseTags(self):
578        """" Test preservation of tag case. """
579        el = etree.Element('MixedCase')
580        el.text = 'not valid '
581        em = etree.SubElement(el, 'EMPHASIS')
582        em.text = 'html'
583        etree.SubElement(el, 'HR')
584        self.assertEqual(
585            markdown.serializers.to_xhtml_string(el),
586            '<MixedCase>not valid <EMPHASIS>html</EMPHASIS><HR /></MixedCase>'
587        )
588
589    def testProsessingInstruction(self):
590        """ Test serialization of ProcessignInstruction. """
591        pi = ProcessingInstruction('foo', text='<&"test\nescaping">')
592        self.assertIs(pi.tag, ProcessingInstruction)
593        self.assertEqual(
594            markdown.serializers.to_xhtml_string(pi),
595            '<?foo &lt;&amp;"test\nescaping"&gt;?>'
596        )
597
598    def testQNameTag(self):
599        """ Test serialization of QName tag. """
600        div = etree.Element('div')
601        qname = etree.QName('http://www.w3.org/1998/Math/MathML', 'math')
602        math = etree.SubElement(div, qname)
603        math.set('display', 'block')
604        sem = etree.SubElement(math, 'semantics')
605        msup = etree.SubElement(sem, 'msup')
606        mi = etree.SubElement(msup, 'mi')
607        mi.text = 'x'
608        mn = etree.SubElement(msup, 'mn')
609        mn.text = '2'
610        ann = etree.SubElement(sem, 'annotations')
611        ann.text = 'x^2'
612        self.assertEqual(
613            markdown.serializers.to_xhtml_string(div),
614            '<div>'
615            '<math display="block" xmlns="http://www.w3.org/1998/Math/MathML">'
616            '<semantics>'
617            '<msup>'
618            '<mi>x</mi>'
619            '<mn>2</mn>'
620            '</msup>'
621            '<annotations>x^2</annotations>'
622            '</semantics>'
623            '</math>'
624            '</div>'
625        )
626
627    def testQNameAttribute(self):
628        """ Test serialization of QName attribute. """
629        div = etree.Element('div')
630        div.set(etree.QName('foo'), etree.QName('bar'))
631        self.assertEqual(
632            markdown.serializers.to_xhtml_string(div),
633            '<div foo="bar"></div>'
634        )
635
636    def testBadQNameTag(self):
637        """ Test serialization of QName with no tag. """
638        qname = etree.QName('http://www.w3.org/1998/Math/MathML')
639        el = etree.Element(qname)
640        self.assertRaises(ValueError, markdown.serializers.to_xhtml_string, el)
641
642    def testQNameEscaping(self):
643        """ Test QName escaping. """
644        qname = etree.QName('<&"test\nescaping">', 'div')
645        el = etree.Element(qname)
646        self.assertEqual(
647            markdown.serializers.to_xhtml_string(el),
648            '<div xmlns="&lt;&amp;&quot;test&#10;escaping&quot;&gt;"></div>'
649        )
650
651    def testQNamePreEscaping(self):
652        """ Test QName that is already partially escaped. """
653        qname = etree.QName('&lt;&amp;"test&#10;escaping"&gt;', 'div')
654        el = etree.Element(qname)
655        self.assertEqual(
656            markdown.serializers.to_xhtml_string(el),
657            '<div xmlns="&lt;&amp;&quot;test&#10;escaping&quot;&gt;"></div>'
658        )
659
660    def buildExtension(self):
661        """ Build an extension which registers fakeSerializer. """
662        def fakeSerializer(elem):
663            # Ignore input and return hardcoded output
664            return '<div><p>foo</p></div>'
665
666        class registerFakeSerializer(markdown.extensions.Extension):
667            def extendMarkdown(self, md):
668                md.output_formats['fake'] = fakeSerializer
669
670        return registerFakeSerializer()
671
672    def testRegisterSerializer(self):
673        self.assertEqual(
674            markdown.markdown(
675                'baz', extensions=[self.buildExtension()], output_format='fake'
676            ),
677            '<p>foo</p>'
678        )
679
680    def testXHTMLOutput(self):
681        self.assertEqual(
682            markdown.markdown('foo  \nbar', output_format='xhtml'),
683            '<p>foo<br />\nbar</p>'
684        )
685
686    def testHTMLOutput(self):
687        self.assertEqual(
688            markdown.markdown('foo  \nbar', output_format='html'),
689            '<p>foo<br>\nbar</p>'
690        )
691
692
693class testAtomicString(unittest.TestCase):
694    """ Test that AtomicStrings are honored (not parsed). """
695
696    def setUp(self):
697        md = markdown.Markdown()
698        self.inlineprocessor = md.treeprocessors['inline']
699
700    def testString(self):
701        """ Test that a regular string is parsed. """
702        tree = etree.Element('div')
703        p = etree.SubElement(tree, 'p')
704        p.text = 'some *text*'
705        new = self.inlineprocessor.run(tree)
706        self.assertEqual(
707            markdown.serializers.to_html_string(new),
708            '<div><p>some <em>text</em></p></div>'
709        )
710
711    def testSimpleAtomicString(self):
712        """ Test that a simple AtomicString is not parsed. """
713        tree = etree.Element('div')
714        p = etree.SubElement(tree, 'p')
715        p.text = markdown.util.AtomicString('some *text*')
716        new = self.inlineprocessor.run(tree)
717        self.assertEqual(
718            markdown.serializers.to_html_string(new),
719            '<div><p>some *text*</p></div>'
720        )
721
722    def testNestedAtomicString(self):
723        """ Test that a nested AtomicString is not parsed. """
724        tree = etree.Element('div')
725        p = etree.SubElement(tree, 'p')
726        p.text = markdown.util.AtomicString('*some* ')
727        span1 = etree.SubElement(p, 'span')
728        span1.text = markdown.util.AtomicString('*more* ')
729        span2 = etree.SubElement(span1, 'span')
730        span2.text = markdown.util.AtomicString('*text* ')
731        span3 = etree.SubElement(span2, 'span')
732        span3.text = markdown.util.AtomicString('*here*')
733        span3.tail = markdown.util.AtomicString(' *to*')
734        span2.tail = markdown.util.AtomicString(' *test*')
735        span1.tail = markdown.util.AtomicString(' *with*')
736        new = self.inlineprocessor.run(tree)
737        self.assertEqual(
738            markdown.serializers.to_html_string(new),
739            '<div><p>*some* <span>*more* <span>*text* <span>*here*</span> '
740            '*to*</span> *test*</span> *with*</p></div>'
741        )
742
743
744class TestConfigParsing(unittest.TestCase):
745    def assertParses(self, value, result):
746        self.assertIs(markdown.util.parseBoolValue(value, False), result)
747
748    def testBooleansParsing(self):
749        self.assertParses(True, True)
750        self.assertParses('novalue', None)
751        self.assertParses('yES', True)
752        self.assertParses('FALSE', False)
753        self.assertParses(0., False)
754        self.assertParses('none', False)
755
756    def testPreserveNone(self):
757        self.assertIsNone(markdown.util.parseBoolValue('None', preserve_none=True))
758        self.assertIsNone(markdown.util.parseBoolValue(None, preserve_none=True))
759
760    def testInvalidBooleansParsing(self):
761        self.assertRaises(ValueError, markdown.util.parseBoolValue, 'novalue')
762
763
764class TestCliOptionParsing(unittest.TestCase):
765    """ Test parsing of Command Line Interface Options. """
766
767    def setUp(self):
768        self.default_options = {
769            'input': None,
770            'output': None,
771            'encoding': None,
772            'output_format': 'xhtml',
773            'lazy_ol': True,
774            'extensions': [],
775            'extension_configs': {},
776        }
777        self.tempfile = ''
778
779    def tearDown(self):
780        if os.path.isfile(self.tempfile):
781            os.remove(self.tempfile)
782
783    def testNoOptions(self):
784        options, logging_level = parse_options([])
785        self.assertEqual(options, self.default_options)
786        self.assertEqual(logging_level, CRITICAL)
787
788    def testQuietOption(self):
789        options, logging_level = parse_options(['-q'])
790        self.assertGreater(logging_level, CRITICAL)
791
792    def testVerboseOption(self):
793        options, logging_level = parse_options(['-v'])
794        self.assertEqual(logging_level, WARNING)
795
796    def testNoisyOption(self):
797        options, logging_level = parse_options(['--noisy'])
798        self.assertEqual(logging_level, DEBUG)
799
800    def testInputFileOption(self):
801        options, logging_level = parse_options(['foo.txt'])
802        self.default_options['input'] = 'foo.txt'
803        self.assertEqual(options, self.default_options)
804
805    def testOutputFileOption(self):
806        options, logging_level = parse_options(['-f', 'foo.html'])
807        self.default_options['output'] = 'foo.html'
808        self.assertEqual(options, self.default_options)
809
810    def testInputAndOutputFileOptions(self):
811        options, logging_level = parse_options(['-f', 'foo.html', 'foo.txt'])
812        self.default_options['output'] = 'foo.html'
813        self.default_options['input'] = 'foo.txt'
814        self.assertEqual(options, self.default_options)
815
816    def testEncodingOption(self):
817        options, logging_level = parse_options(['-e', 'utf-8'])
818        self.default_options['encoding'] = 'utf-8'
819        self.assertEqual(options, self.default_options)
820
821    def testOutputFormatOption(self):
822        options, logging_level = parse_options(['-o', 'html'])
823        self.default_options['output_format'] = 'html'
824        self.assertEqual(options, self.default_options)
825
826    def testNoLazyOlOption(self):
827        options, logging_level = parse_options(['-n'])
828        self.default_options['lazy_ol'] = False
829        self.assertEqual(options, self.default_options)
830
831    def testExtensionOption(self):
832        options, logging_level = parse_options(['-x', 'markdown.extensions.footnotes'])
833        self.default_options['extensions'] = ['markdown.extensions.footnotes']
834        self.assertEqual(options, self.default_options)
835
836    def testMultipleExtensionOptions(self):
837        options, logging_level = parse_options([
838            '-x', 'markdown.extensions.footnotes',
839            '-x', 'markdown.extensions.smarty'
840        ])
841        self.default_options['extensions'] = [
842            'markdown.extensions.footnotes',
843            'markdown.extensions.smarty'
844        ]
845        self.assertEqual(options, self.default_options)
846
847    def create_config_file(self, config):
848        """ Helper to create temp config files. """
849        if not isinstance(config, str):
850            # convert to string
851            config = yaml.dump(config)
852        fd, self.tempfile = tempfile.mkstemp('.yml')
853        with os.fdopen(fd, 'w') as fp:
854            fp.write(config)
855
856    def testExtensionConfigOption(self):
857        config = {
858            'markdown.extensions.wikilinks': {
859                'base_url': 'http://example.com/',
860                'end_url': '.html',
861                'html_class': 'test',
862            },
863            'markdown.extensions.footnotes:FootnotesExtension': {
864                'PLACE_MARKER': '~~~footnotes~~~'
865            }
866        }
867        self.create_config_file(config)
868        options, logging_level = parse_options(['-c', self.tempfile])
869        self.default_options['extension_configs'] = config
870        self.assertEqual(options, self.default_options)
871
872    def textBoolExtensionConfigOption(self):
873        config = {
874            'markdown.extensions.toc': {
875                'title': 'Some Title',
876                'anchorlink': True,
877                'permalink': True
878            }
879        }
880        self.create_config_file(config)
881        options, logging_level = parse_options(['-c', self.tempfile])
882        self.default_options['extension_configs'] = config
883        self.assertEqual(options, self.default_options)
884
885    def testExtensionConfigOptionAsJSON(self):
886        config = {
887            'markdown.extensions.wikilinks': {
888                'base_url': 'http://example.com/',
889                'end_url': '.html',
890                'html_class': 'test',
891            },
892            'markdown.extensions.footnotes:FootnotesExtension': {
893                'PLACE_MARKER': '~~~footnotes~~~'
894            }
895        }
896        import json
897        self.create_config_file(json.dumps(config))
898        options, logging_level = parse_options(['-c', self.tempfile])
899        self.default_options['extension_configs'] = config
900        self.assertEqual(options, self.default_options)
901
902    def testExtensionConfigOptionMissingFile(self):
903        self.assertRaises(IOError, parse_options, ['-c', 'missing_file.yaml'])
904
905    def testExtensionConfigOptionBadFormat(self):
906        config = """
907[footnotes]
908PLACE_MARKER= ~~~footnotes~~~
909"""
910        self.create_config_file(config)
911        self.assertRaises(yaml.YAMLError, parse_options, ['-c', self.tempfile])
912
913
914class TestEscapeAppend(unittest.TestCase):
915    """ Tests escape character append. """
916
917    def testAppend(self):
918        """ Test that appended escapes are only in the current instance. """
919        md = markdown.Markdown()
920        md.ESCAPED_CHARS.append('|')
921        self.assertEqual('|' in md.ESCAPED_CHARS, True)
922        md2 = markdown.Markdown()
923        self.assertEqual('|' not in md2.ESCAPED_CHARS, True)
924
925
926class TestBlockAppend(unittest.TestCase):
927    """ Tests block kHTML append. """
928
929    def testBlockAppend(self):
930        """ Test that appended escapes are only in the current instance. """
931        md = markdown.Markdown()
932        md.block_level_elements.append('test')
933        self.assertEqual('test' in md.block_level_elements, True)
934        md2 = markdown.Markdown()
935        self.assertEqual('test' not in md2.block_level_elements, True)
936
937
938class TestAncestorExclusion(unittest.TestCase):
939    """ Tests exclusion of tags in ancestor list. """
940
941    class AncestorExample(markdown.inlinepatterns.SimpleTagInlineProcessor):
942        """ Ancestor Test. """
943
944        ANCESTOR_EXCLUDES = ('a',)
945
946        def handleMatch(self, m, data):
947            """ Handle match. """
948            el = etree.Element(self.tag)
949            el.text = m.group(2)
950            return el, m.start(0), m.end(0)
951
952    class AncestorExtension(markdown.Extension):
953
954        def __init__(self, *args, **kwargs):
955            """Initialize."""
956
957            self.config = {}
958
959        def extendMarkdown(self, md):
960            """Modify inline patterns."""
961
962            pattern = r'(\+)([^\+]+)\1'
963            md.inlinePatterns.register(TestAncestorExclusion.AncestorExample(pattern, 'strong'), 'ancestor-test', 0)
964
965    def setUp(self):
966        """Setup markdown object."""
967        self.md = markdown.Markdown(extensions=[TestAncestorExclusion.AncestorExtension()])
968
969    def test_ancestors(self):
970        """ Test that an extension can exclude parent tags. """
971        test = """
972Some +test+ and a [+link+](http://test.com)
973"""
974        result = """<p>Some <strong>test</strong> and a <a href="http://test.com">+link+</a></p>"""
975
976        self.md.reset()
977        self.assertEqual(self.md.convert(test), result)
978
979    def test_ancestors_tail(self):
980        """ Test that an extension can exclude parent tags when dealing with a tail. """
981        test = """
982[***+em+*+strong+**](http://test.com)
983"""
984        result = """<p><a href="http://test.com"><strong><em>+em+</em>+strong+</strong></a></p>"""
985
986        self.md.reset()
987        self.assertEqual(self.md.convert(test), result)
988
989
990class TestGeneralDeprecations(unittest.TestCase):
991    """Test general deprecations."""
992
993    def test_version_deprecation(self):
994        """Test that version is deprecated."""
995
996        with warnings.catch_warnings(record=True) as w:
997            # Cause all warnings to always be triggered.
998            warnings.simplefilter("always")
999            # Trigger a warning.
1000            version = markdown.version
1001            # Verify some things
1002            self.assertEqual(len(w), 1)
1003            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
1004            self.assertEqual(version, markdown.__version__)
1005
1006    def test_version_info_deprecation(self):
1007        """Test that version info is deprecated."""
1008
1009        with warnings.catch_warnings(record=True) as w:
1010            # Cause all warnings to always be triggered.
1011            warnings.simplefilter("always")
1012            # Trigger a warning.
1013            version_info = markdown.version_info
1014            # Verify some things
1015            self.assertEqual(len(w), 1)
1016            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
1017            self.assertEqual(version_info, markdown.__version_info__)
1018
1019    def test_deprecation_wrapper_dir(self):
1020        """Tests the `__dir__` attribute of the class as it replaces the module's."""
1021
1022        dir_attr = dir(markdown)
1023        self.assertNotIn('version', dir_attr)
1024        self.assertIn('__version__', dir_attr)
1025        self.assertNotIn('version_info', dir_attr)
1026        self.assertIn('__version_info__', dir_attr)
1027