1# vim: ts=8:sts=8:sw=8:noexpandtab 2 3# This file is part of python-markups test suite 4# License: 3-clause BSD, see LICENSE file 5# Copyright: (C) Dmitry Shachnev, 2012-2021 6 7from markups.markdown import MarkdownMarkup, _canonicalized_ext_names 8from os.path import join 9from tempfile import TemporaryDirectory 10import unittest 11import warnings 12 13try: 14 import pymdownx 15except ImportError: 16 pymdownx = None 17 18tables_source = \ 19'''th1 | th2 20--- | --- 21t11 | t21 22t12 | t22''' 23 24tables_output = \ 25'''<table> 26<thead> 27<tr> 28<th>th1</th> 29<th>th2</th> 30</tr> 31</thead> 32<tbody> 33<tr> 34<td>t11</td> 35<td>t21</td> 36</tr> 37<tr> 38<td>t12</td> 39<td>t22</td> 40</tr> 41</tbody> 42</table> 43''' 44 45deflists_source = \ 46'''Apple 47: Pomaceous fruit of plants of the genus Malus in 48 the family Rosaceae. 49 50Orange 51: The fruit of an evergreen tree of the genus Citrus.''' 52 53deflists_output = \ 54'''<dl> 55<dt>Apple</dt> 56<dd>Pomaceous fruit of plants of the genus Malus in 57the family Rosaceae.</dd> 58<dt>Orange</dt> 59<dd>The fruit of an evergreen tree of the genus Citrus.</dd> 60</dl> 61''' 62 63mathjax_header = \ 64'<!--- Type: markdown; Required extensions: mathjax --->\n\n' 65 66mathjax_source = \ 67r'''$i_1$ some text \$escaped\$ $i_2$ 68 69\(\LaTeX\) \\(escaped\) 70 71$$m_1$$ text $$m_2$$ 72 73\[m_3\] text \[m_4\] 74 75\( \sin \alpha \) text \( \sin \beta \) 76 77\[ \alpha \] text \[ \beta \] 78 79\$$escaped\$$ \\[escaped\] 80''' 81 82mathjax_output = \ 83r'''<p> 84<script type="math/tex">i_1</script> some text $escaped$ <script type="math/tex">i_2</script> 85</p> 86<p> 87<script type="math/tex">\LaTeX</script> \(escaped)</p> 88<p> 89<script type="math/tex; mode=display">m_1</script> text <script type="math/tex; mode=display">m_2</script> 90</p> 91<p> 92<script type="math/tex; mode=display">m_3</script> text <script type="math/tex; mode=display">m_4</script> 93</p> 94<p> 95<script type="math/tex"> \sin \alpha </script> text <script type="math/tex"> \sin \beta </script> 96</p> 97<p> 98<script type="math/tex; mode=display"> \alpha </script> text <script type="math/tex; mode=display"> \beta </script> 99</p> 100<p>$$escaped$$ \[escaped]</p> 101''' 102 103mathjax_multiline_source = \ 104r''' 105$$ 106\TeX 107\LaTeX 108$$ 109''' 110 111mathjax_multiline_output = \ 112r'''<p> 113<script type="math/tex; mode=display"> 114\TeX 115\LaTeX 116</script> 117</p> 118''' 119 120mathjax_multilevel_source = \ 121r''' 122\begin{equation*} 123 \begin{pmatrix} 124 1 & 0\\ 125 0 & 1 126 \end{pmatrix} 127\end{equation*} 128''' 129 130mathjax_multilevel_output = \ 131r'''<p> 132<script type="math/tex; mode=display">\begin{equation*} 133 \begin{pmatrix} 134 1 & 0\\ 135 0 & 1 136 \end{pmatrix} 137\end{equation*}</script> 138</p> 139''' 140 141@unittest.skipUnless(MarkdownMarkup.available(), 'Markdown not available') 142class MarkdownTest(unittest.TestCase): 143 maxDiff = None 144 145 def setUp(self): 146 warnings.simplefilter("ignore", Warning) 147 148 def test_empty_file(self): 149 markup = MarkdownMarkup() 150 self.assertEqual(markup.convert('').get_document_body(), '\n') 151 152 def test_extensions_loading(self): 153 markup = MarkdownMarkup() 154 self.assertIsNone(markup._canonicalize_extension_name('nonexistent')) 155 self.assertIsNone(markup._canonicalize_extension_name('nonexistent(someoption)')) 156 self.assertIsNone(markup._canonicalize_extension_name('.foobar')) 157 self.assertEqual(markup._canonicalize_extension_name('meta'), 'markdown.extensions.meta') 158 name, parameters = markup._split_extension_config('toc(anchorlink=1, foo=bar)') 159 self.assertEqual(name, 'toc') 160 self.assertEqual(parameters, {'anchorlink': '1', 'foo': 'bar'}) 161 162 def test_loading_extensions_by_module_name(self): 163 markup = MarkdownMarkup(extensions=['markdown.extensions.footnotes']) 164 source = ('Footnotes[^1] have a label and the content.\n\n' 165 '[^1]: This is a footnote content.') 166 html = markup.convert(source).get_document_body() 167 self.assertIn('<sup', html) 168 self.assertIn('footnote-backref', html) 169 170 def test_removing_duplicate_extensions(self): 171 markup = MarkdownMarkup(extensions=['remove_extra', 'toc', 'markdown.extensions.toc']) 172 self.assertEqual(len(markup.extensions), 1) 173 self.assertIn('markdown.extensions.toc', markup.extensions) 174 175 def test_extensions_parameters(self): 176 markup = MarkdownMarkup(extensions=['toc(anchorlink=1)']) 177 html = markup.convert('## Header').get_document_body() 178 self.assertEqual(html, 179 '<h2 id="header"><a class="toclink" href="#header">Header</a></h2>\n') 180 self.assertEqual(_canonicalized_ext_names['toc'], 'markdown.extensions.toc') 181 182 def test_document_extensions_parameters(self): 183 markup = MarkdownMarkup(extensions=[]) 184 toc_header = '<!--- Required extensions: toc(anchorlink=1) --->\n\n' 185 html = markup.convert(toc_header + '## Header').get_document_body() 186 self.assertEqual(html, toc_header + 187 '<h2 id="header"><a class="toclink" href="#header">Header</a></h2>\n') 188 toc_header = '<!--- Required extensions: toc(title=Table of contents, baselevel=3) wikilinks --->\n\n' 189 html = markup.convert(toc_header + '[TOC]\n\n# Header\n[[Link]]').get_document_body() 190 self.assertEqual(html, toc_header + 191 '<div class="toc"><span class="toctitle">Table of contents</span><ul>\n' 192 '<li><a href="#header">Header</a></li>\n' 193 '</ul>\n</div>\n' 194 '<h3 id="header">Header</h3>\n' 195 '<p><a class="wikilink" href="/Link/">Link</a></p>\n') 196 197 def test_document_extensions_change(self): 198 """Extensions from document should be replaced on each run, not added.""" 199 markup = MarkdownMarkup(extensions=[]) 200 toc_header = '<!-- Required extensions: toc -->\n\n' 201 content = '[TOC]\n\n# Header' 202 html = markup.convert(toc_header + content).get_document_body() 203 self.assertNotIn('<p>[TOC]</p>', html) 204 html = markup.convert(content).get_document_body() 205 self.assertIn('<p>[TOC]</p>', html) 206 html = markup.convert(toc_header + content).get_document_body() 207 self.assertNotIn('<p>[TOC]</p>', html) 208 209 def test_extra(self): 210 markup = MarkdownMarkup() 211 html = markup.convert(tables_source).get_document_body() 212 self.assertEqual(tables_output, html) 213 html = markup.convert(deflists_source).get_document_body() 214 self.assertEqual(deflists_output, html) 215 216 def test_remove_extra(self): 217 markup = MarkdownMarkup(extensions=['remove_extra']) 218 html = markup.convert(tables_source).get_document_body() 219 self.assertNotIn('<table>', html) 220 221 def test_remove_extra_document_extension(self): 222 markup = MarkdownMarkup(extensions=[]) 223 html = markup.convert( 224 'Required-Extensions: remove_extra\n\n' + 225 tables_source).get_document_body() 226 self.assertNotIn('<table>', html) 227 228 def test_remove_extra_double(self): 229 """Removing extra twice should not cause a crash.""" 230 markup = MarkdownMarkup(extensions=['remove_extra']) 231 markup.convert('Required-Extensions: remove_extra\n') 232 233 def test_remove_extra_removes_mathjax(self): 234 markup = MarkdownMarkup(extensions=['remove_extra']) 235 html = markup.convert('$$1$$').get_document_body() 236 self.assertNotIn('math/tex', html) 237 238 def test_meta(self): 239 markup = MarkdownMarkup() 240 text = ('Required-Extensions: meta\n' 241 'Title: Hello, world!\n\n' 242 'Some text here.') 243 title = markup.convert(text).get_document_title() 244 self.assertEqual('Hello, world!', title) 245 246 def test_default_math(self): 247 # by default $...$ delimeter should be disabled 248 markup = MarkdownMarkup(extensions=[]) 249 self.assertEqual('<p>$1$</p>\n', markup.convert('$1$').get_document_body()) 250 self.assertEqual('<p>\n<script type="math/tex; mode=display">1</script>\n</p>\n', 251 markup.convert('$$1$$').get_document_body()) 252 253 def test_mathjax(self): 254 markup = MarkdownMarkup(extensions=['mathjax']) 255 # Escaping should work 256 self.assertEqual('', markup.convert('Hello, \\$2+2$!').get_javascript()) 257 js = markup.convert(mathjax_source).get_javascript() 258 self.assertIn('<script', js) 259 body = markup.convert(mathjax_source).get_document_body() 260 self.assertEqual(mathjax_output, body) 261 262 def test_mathjax_document_extension(self): 263 markup = MarkdownMarkup() 264 text = mathjax_header + mathjax_source 265 body = markup.convert(text).get_document_body() 266 self.assertEqual(mathjax_header + mathjax_output, body) 267 268 def test_mathjax_multiline(self): 269 markup = MarkdownMarkup(extensions=['mathjax']) 270 body = markup.convert(mathjax_multiline_source).get_document_body() 271 self.assertEqual(mathjax_multiline_output, body) 272 273 def test_mathjax_multilevel(self): 274 markup = MarkdownMarkup() 275 body = markup.convert(mathjax_multilevel_source).get_document_body() 276 self.assertEqual(mathjax_multilevel_output, body) 277 278 def test_mathjax_asciimath(self): 279 markup = MarkdownMarkup(extensions=['mdx_math(use_asciimath=1)']) 280 converted = markup.convert(r'\( [[a,b],[c,d]] \)') 281 body = converted.get_document_body() 282 self.assertIn('<script type="math/asciimath">', body) 283 self.assertIn('<script type="text/javascript"', converted.get_javascript()) 284 285 def test_not_loading_sys(self): 286 with self.assertWarnsRegex(ImportWarning, 'Extension "sys" does not exist.'): 287 markup = MarkdownMarkup(extensions=['sys']) 288 self.assertNotIn('sys', markup.extensions) 289 290 def test_extensions_txt_file(self): 291 with TemporaryDirectory() as tmpdirname: 292 txtfilename = join(tmpdirname, "markdown-extensions.txt") 293 with open(txtfilename, "w") as f: 294 f.write("foo\n# bar\nbaz(arg=value)\n") 295 markup = MarkdownMarkup(filename=join(tmpdirname, "foo.md")) 296 self.assertEqual(markup.global_extensions, 297 [("foo", {}), ("baz", {"arg": "value"})]) 298 299 def test_extensions_yaml_file(self): 300 with TemporaryDirectory() as tmpdirname: 301 yamlfilename = join(tmpdirname, "markdown-extensions.yaml") 302 with open(yamlfilename, "w") as f: 303 f.write('- smarty:\n' 304 ' substitutions:\n' 305 ' left-single-quote: "‚"\n' 306 ' right-single-quote: "‘"\n' 307 ' smart_dashes: False\n' 308 '- toc:\n' 309 ' permalink: True\n' 310 ' separator: "_"\n' 311 ' toc_depth: 3\n' 312 '- sane_lists\n') 313 markup = MarkdownMarkup(filename=join(tmpdirname, "foo.md")) 314 self.assertEqual( 315 markup.global_extensions, 316 [("smarty", {"substitutions": {"left-single-quote": "‚", 317 "right-single-quote": "‘"}, 318 "smart_dashes": False}), 319 ("toc", {"permalink": True, "separator": "_", "toc_depth": 3}), 320 ("sane_lists", {}), 321 ]) 322 converted = markup.convert("'foo' -- bar") 323 body = converted.get_document_body() 324 self.assertEqual(body, '<p>‚foo‘ -- bar</p>\n') 325 326 def test_extensions_yaml_file_invalid(self): 327 with TemporaryDirectory() as tmpdirname: 328 yamlfilename = join(tmpdirname, "markdown-extensions.yaml") 329 with open(yamlfilename, "w") as f: 330 f.write('[this is an invalid YAML file') 331 with self.assertWarns(SyntaxWarning) as cm: 332 MarkdownMarkup(filename=join(tmpdirname, "foo.md")) 333 self.assertIn("Failed parsing", str(cm.warning)) 334 self.assertIn("expected ',' or ']'", str(cm.warning)) 335 336 def test_codehilite(self): 337 markup = MarkdownMarkup(extensions=["codehilite"]) 338 converted = markup.convert(' :::python\n import foo') 339 stylesheet = converted.get_stylesheet() 340 self.assertIn(".codehilite .k {", stylesheet) 341 body = converted.get_document_body() 342 self.assertIn('<div class="codehilite">', body) 343 344 def test_codehilite_custom_class(self): 345 markup = MarkdownMarkup(extensions=["codehilite(css_class=myclass)"]) 346 converted = markup.convert(' :::python\n import foo') 347 stylesheet = converted.get_stylesheet() 348 self.assertIn(".myclass .k {", stylesheet) 349 body = converted.get_document_body() 350 self.assertIn('<div class="myclass">', body) 351 352 @unittest.skipIf(pymdownx is None, "pymdownx module is not available") 353 def test_pymdownx_highlight(self): 354 markup = MarkdownMarkup(extensions=["pymdownx.highlight"]) 355 converted = markup.convert(' import foo') 356 stylesheet = converted.get_stylesheet() 357 self.assertIn(".highlight .k {", stylesheet) 358 body = converted.get_document_body() 359 self.assertIn('<div class="highlight">', body) 360 361 @unittest.skipIf(pymdownx is None, "pymdownx module is not available") 362 def test_pymdownx_highlight_custom_class(self): 363 markup = MarkdownMarkup(extensions=["pymdownx.highlight(css_class=myclass)"]) 364 converted = markup.convert(' import foo') 365 stylesheet = converted.get_stylesheet() 366 self.assertIn(".myclass .k {", stylesheet) 367 body = converted.get_document_body() 368 self.assertIn('<div class="myclass">', body) 369