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: "&sbquo;"\n'
306				        '      right-single-quote: "&lsquo;"\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": "&sbquo;",
317			                               "right-single-quote": "&lsquo;"},
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>&sbquo;foo&lsquo; -- 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