1# vim: ts=8:sts=8:sw=8:noexpandtab 2 3# This file is part of python-markups module 4# License: 3-clause BSD, see LICENSE file 5# Copyright: (C) Dmitry Shachnev, 2012-2021 6 7import importlib 8import os 9import re 10import warnings 11import markups.common as common 12from markups.abstract import AbstractMarkup, ConvertedMarkup 13 14try: 15 import yaml 16except ImportError: 17 yaml = None 18 19MATHJAX2_CONFIG = \ 20'''<script type="text/x-mathjax-config"> 21MathJax.Hub.Config({ 22 config: ["MMLorHTML.js"], 23 jax: ["input/TeX", "input/AsciiMath", "output/HTML-CSS", "output/NativeMML"], 24 extensions: ["MathMenu.js", "MathZoom.js"], 25 TeX: { 26 extensions: ["AMSmath.js", "AMSsymbols.js"], 27 equationNumbers: {autoNumber: "AMS"} 28 } 29}); 30</script> 31''' 32 33# Taken from: 34# https://docs.mathjax.org/en/latest/upgrading/v2.html?highlight=upgrading#changes-in-the-mathjax-api 35MATHJAX3_CONFIG = \ 36''' 37<script> 38MathJax = { 39 options: { 40 renderActions: { 41 find: [10, function (doc) { 42 for (const node of document.querySelectorAll('script[type^="math/tex"]')) { 43 const display = !!node.type.match(/; *mode=display/); 44 const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display); 45 const text = document.createTextNode(''); 46 node.parentNode.replaceChild(text, node); 47 math.start = {node: text, delim: '', n: 0}; 48 math.end = {node: text, delim: '', n: 0}; 49 doc.math.push(math); 50 } 51 }, ''] 52 } 53 } 54}; 55</script> 56''' 57 58extensions_re = re.compile(r'required.extensions: (.+)', flags=re.IGNORECASE) 59extension_name_re = re.compile(r'[a-z0-9_.]+(?:\([^)]+\))?', flags=re.IGNORECASE) 60 61_canonicalized_ext_names = {} 62 63class MarkdownMarkup(AbstractMarkup): 64 """Markup class for Markdown language. 65 Inherits :class:`~markups.abstract.AbstractMarkup`. 66 67 :param extensions: list of extension names 68 :type extensions: list 69 """ 70 name = 'Markdown' 71 attributes = { 72 common.LANGUAGE_HOME_PAGE: 'https://daringfireball.net/projects/markdown/', 73 common.MODULE_HOME_PAGE: 'https://github.com/Python-Markdown/markdown', 74 common.SYNTAX_DOCUMENTATION: 'https://daringfireball.net/projects/markdown/syntax' 75 } 76 77 file_extensions = ('.md', '.mkd', '.mkdn', '.mdwn', '.mdown', '.markdown') 78 default_extension = '.mkd' 79 80 @staticmethod 81 def available(): 82 try: 83 import markdown 84 except ImportError: 85 return False 86 return (hasattr(markdown, '__version_info__') or # underscored attribute means 3.x 87 hasattr(markdown, 'version_info') and markdown.version_info >= (2, 6)) 88 89 def _load_extensions_list_from_txt_file(self, filename): 90 with open(filename) as extensions_file: 91 for line in extensions_file: 92 if not line.startswith('#'): 93 yield self._split_extension_config(line.rstrip()) 94 95 def _load_extensions_list_from_yaml_file(self, filename): 96 with open(filename) as extensions_file: 97 try: 98 data = yaml.safe_load(extensions_file) 99 except yaml.YAMLError as ex: 100 warnings.warn(f'Failed parsing {filename}: {ex}', SyntaxWarning) 101 raise IOError from ex 102 if isinstance(data, list): 103 for item in data: 104 if isinstance(item, dict): 105 yield from item.items() 106 elif isinstance(item, str): 107 yield item, {} 108 109 def _get_global_extensions(self, filename): 110 local_directory = os.path.dirname(filename) if filename else '' 111 choices = [ 112 os.path.join(local_directory, 'markdown-extensions.yaml'), 113 os.path.join(local_directory, 'markdown-extensions.txt'), 114 os.path.join(common.CONFIGURATION_DIR, 'markdown-extensions.yaml'), 115 os.path.join(common.CONFIGURATION_DIR, 'markdown-extensions.txt'), 116 ] 117 for choice in choices: 118 if choice.endswith('.yaml') and yaml is None: 119 continue 120 try: 121 if choice.endswith('.txt'): 122 yield from self._load_extensions_list_from_txt_file(choice) 123 else: 124 yield from self._load_extensions_list_from_yaml_file(choice) 125 except IOError: 126 continue # Cannot open file, move to the next choice 127 else: 128 break # File loaded successfully, skip the remaining choices 129 130 def _get_document_extensions(self, text): 131 lines = text.splitlines() 132 match = extensions_re.search(lines[0]) if lines else None 133 if match: 134 extensions = extension_name_re.findall(match.group(1)) 135 yield from self._split_extensions_configs(extensions) 136 137 def _canonicalize_extension_name(self, extension_name): 138 prefixes = ('markdown.extensions.', '', 'mdx_') 139 for prefix in prefixes: 140 try: 141 module = importlib.import_module(prefix + extension_name) 142 if not hasattr(module, 'makeExtension'): 143 continue 144 except (ImportError, ValueError, TypeError): 145 pass 146 else: 147 return prefix + extension_name 148 149 def _split_extension_config(self, extension_name): 150 """Splits the configuration options from the extension name.""" 151 lb = extension_name.find('(') 152 if lb == -1: 153 return extension_name, {} 154 extension_name, parameters = extension_name[:lb], extension_name[lb + 1:-1] 155 pairs = [x.split("=") for x in parameters.split(",")] 156 return extension_name, {x.strip(): y.strip() for (x, y) in pairs} 157 158 def _split_extensions_configs(self, extensions): 159 """Splits the configuration options from a list of strings. 160 161 :returns: a generator of (name, config) tuples 162 """ 163 for extension in extensions: 164 yield self._split_extension_config(extension) 165 166 def _apply_extensions(self, document_extensions=None): 167 extensions = self.global_extensions.copy() 168 extensions.extend( 169 self._split_extensions_configs(self.requested_extensions)) 170 if document_extensions is not None: 171 extensions.extend(document_extensions) 172 173 extension_names = {"markdown.extensions.extra", "mdx_math"} 174 extension_configs = {} 175 176 for name, config in extensions: 177 if name == 'mathjax': 178 mathjax_config = {"enable_dollar_delimiter": True} 179 extension_configs["mdx_math"] = mathjax_config 180 elif name == 'remove_extra': 181 if "markdown.extensions.extra" in extension_names: 182 extension_names.remove("markdown.extensions.extra") 183 if "mdx_math" in extension_names: 184 extension_names.remove("mdx_math") 185 else: 186 if name in _canonicalized_ext_names: 187 canonical_name = _canonicalized_ext_names[name] 188 else: 189 canonical_name = self._canonicalize_extension_name(name) 190 if canonical_name is None: 191 warnings.warn('Extension "%s" does not exist.' % 192 name, ImportWarning) 193 continue 194 _canonicalized_ext_names[name] = canonical_name 195 extension_names.add(canonical_name) 196 extension_configs[canonical_name] = config 197 self.md = self.markdown.Markdown(extensions=list(extension_names), 198 extension_configs=extension_configs, 199 output_format='html5') 200 self.extensions = extension_names 201 self.extension_configs = extension_configs 202 203 def __init__(self, filename=None, extensions=None): 204 AbstractMarkup.__init__(self, filename) 205 import markdown 206 self.markdown = markdown 207 self.requested_extensions = extensions or [] 208 self.global_extensions = [] 209 if extensions is None: 210 self.global_extensions.extend(self._get_global_extensions(filename)) 211 self._apply_extensions() 212 213 def convert(self, text): 214 215 # Determine body 216 self.md.reset() 217 self._apply_extensions(self._get_document_extensions(text)) 218 body = self.md.convert(text) + '\n' 219 220 # Determine title 221 if hasattr(self.md, 'Meta') and 'title' in self.md.Meta: 222 title = str.join(' ', self.md.Meta['title']) 223 else: 224 title = '' 225 226 # Determine stylesheet 227 css_class = None 228 229 if 'markdown.extensions.codehilite' in self.extensions: 230 config = self.extension_configs.get('markdown.extensions.codehilite', {}) 231 css_class = config.get('css_class', 'codehilite') 232 stylesheet = common.get_pygments_stylesheet('.%s' % css_class) 233 elif 'pymdownx.highlight' in self.extensions: 234 config = self.extension_configs.get('pymdownx.highlight', {}) 235 css_class = config.get('css_class', 'highlight') 236 stylesheet = common.get_pygments_stylesheet('.%s' % css_class) 237 else: 238 stylesheet = '' 239 240 return ConvertedMarkdown(body, title, stylesheet) 241 242class ConvertedMarkdown(ConvertedMarkup): 243 244 def get_javascript(self, webenv=False): 245 if '<script type="math/' not in self.body: 246 return '' 247 mathjax_url, mathjax_version = common.get_mathjax_url_and_version(webenv) 248 config = MATHJAX3_CONFIG if mathjax_version == 3 else MATHJAX2_CONFIG 249 async_attr = ' async' if mathjax_version == 3 else '' 250 script_tag = '<script type="text/javascript" src="%s"%s></script>' 251 return config + script_tag % (mathjax_url, async_attr) 252