math.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. # -*- coding: utf-8 -*-
  2. """
  3. Math Render Plugin for Pelican
  4. ==============================
  5. This plugin allows your site to render Math. It uses
  6. the MathJax JavaScript engine.
  7. For markdown, the plugin works by creating a Markdown
  8. extension which is used during the markdown compilation
  9. stage. Math therefore gets treated like a "first class
  10. citizen" in Pelican
  11. For reStructuredText, the plugin instructs the rst engine
  12. to output Mathjax for all math.
  13. The mathjax script is by default automatically inserted
  14. into the HTML.
  15. Typogrify Compatibility
  16. -----------------------
  17. This plugin now plays nicely with Typogrify, but it
  18. requires Typogrify version 2.07 or above.
  19. User Settings
  20. -------------
  21. Users are also able to pass a dictionary of settings
  22. in the settings file which will control how the MathJax
  23. library renders things. This could be very useful for
  24. template builders that want to adjust the look and feel of
  25. the math. See README for more details.
  26. """
  27. import os
  28. import sys
  29. from pelican import signals, generators
  30. try:
  31. from bs4 import BeautifulSoup
  32. except ImportError as e:
  33. BeautifulSoup = None
  34. try:
  35. from . pelican_mathjax_markdown_extension import PelicanMathJaxExtension
  36. except ImportError as e:
  37. PelicanMathJaxExtension = None
  38. try:
  39. string_type = basestring
  40. except NameError:
  41. string_type = str
  42. def process_settings(pelicanobj):
  43. """Sets user specified MathJax settings (see README for more details)"""
  44. mathjax_settings = {}
  45. # NOTE TO FUTURE DEVELOPERS: Look at the README and what is happening in
  46. # this function if any additional changes to the mathjax settings need to
  47. # be incorporated. Also, please inline comment what the variables
  48. # will be used for
  49. # Default settings
  50. mathjax_settings['auto_insert'] = True # if set to true, it will insert mathjax script automatically into content without needing to alter the template.
  51. mathjax_settings['align'] = 'center' # controls alignment of of displayed equations (values can be: left, right, center)
  52. mathjax_settings['indent'] = '0em' # if above is not set to 'center', then this setting acts as an indent
  53. mathjax_settings['show_menu'] = 'true' # controls whether to attach mathjax contextual menu
  54. mathjax_settings['process_escapes'] = 'true' # controls whether escapes are processed
  55. mathjax_settings['latex_preview'] = 'TeX' # controls what user sees while waiting for LaTex to render
  56. mathjax_settings['color'] = 'inherit' # controls color math is rendered in
  57. mathjax_settings['linebreak_automatic'] = 'false' # Set to false by default for performance reasons (see http://docs.mathjax.org/en/latest/output.html#automatic-line-breaking)
  58. mathjax_settings['tex_extensions'] = '' # latex extensions that can be embedded inside mathjax (see http://docs.mathjax.org/en/latest/tex.html#tex-and-latex-extensions)
  59. mathjax_settings['responsive'] = 'false' # Tries to make displayed math responsive
  60. mathjax_settings['responsive_break'] = '768' # The break point at which it math is responsively aligned (in pixels)
  61. mathjax_settings['mathjax_font'] = 'default' # forces mathjax to use the specified font.
  62. mathjax_settings['process_summary'] = BeautifulSoup is not None # will fix up summaries if math is cut off. Requires beautiful soup
  63. mathjax_settings['message_style'] = 'normal' # This value controls the verbosity of the messages in the lower left-hand corner. Set it to "none" to eliminate all messages
  64. # Source for MathJax
  65. mathjax_settings['source'] = "'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML'"
  66. # Get the user specified settings
  67. try:
  68. settings = pelicanobj.settings['MATH_JAX']
  69. except:
  70. settings = None
  71. # If no settings have been specified, then return the defaults
  72. if not isinstance(settings, dict):
  73. return mathjax_settings
  74. # The following mathjax settings can be set via the settings dictionary
  75. for key, value in ((key, settings[key]) for key in settings):
  76. # Iterate over dictionary in a way that is compatible with both version 2
  77. # and 3 of python
  78. if key == 'align':
  79. typeVal = isinstance(value, string_type)
  80. if not typeVal:
  81. continue
  82. if value == 'left' or value == 'right' or value == 'center':
  83. mathjax_settings[key] = value
  84. else:
  85. mathjax_settings[key] = 'center'
  86. if key == 'indent':
  87. mathjax_settings[key] = value
  88. if key == 'source':
  89. mathjax_settings[key] = value
  90. if key == 'show_menu' and isinstance(value, bool):
  91. mathjax_settings[key] = 'true' if value else 'false'
  92. if key == 'message_style':
  93. mathjax_settings[key] = value if value is not None else 'none'
  94. if key == 'auto_insert' and isinstance(value, bool):
  95. mathjax_settings[key] = value
  96. if key == 'process_escapes' and isinstance(value, bool):
  97. mathjax_settings[key] = 'true' if value else 'false'
  98. if key == 'latex_preview':
  99. typeVal = isinstance(value, string_type)
  100. if not typeVal:
  101. continue
  102. mathjax_settings[key] = value
  103. if key == 'color':
  104. typeVal = isinstance(value, string_type)
  105. if not typeVal:
  106. continue
  107. mathjax_settings[key] = value
  108. if key == 'linebreak_automatic' and isinstance(value, bool):
  109. mathjax_settings[key] = 'true' if value else 'false'
  110. if key == 'process_summary' and isinstance(value, bool):
  111. if value and BeautifulSoup is None:
  112. print("BeautifulSoup4 is needed for summaries to be processed by render_math\nPlease install it")
  113. value = False
  114. mathjax_settings[key] = value
  115. if key == 'responsive' and isinstance(value, bool):
  116. mathjax_settings[key] = 'true' if value else 'false'
  117. if key == 'responsive_break' and isinstance(value, int):
  118. mathjax_settings[key] = str(value)
  119. if key == 'tex_extensions' and isinstance(value, list):
  120. # filter string values, then add '' to them
  121. value = filter(lambda string: isinstance(string, string_type), value)
  122. value = map(lambda string: "'%s'" % string, value)
  123. mathjax_settings[key] = ',' + ','.join(value)
  124. if key == 'mathjax_font':
  125. typeVal = isinstance(value, string_type)
  126. if not typeVal:
  127. continue
  128. value = value.lower()
  129. if value == 'sanserif':
  130. value = 'SansSerif'
  131. elif value == 'fraktur':
  132. value = 'Fraktur'
  133. elif value == 'typewriter':
  134. value = 'Typewriter'
  135. else:
  136. value = 'default'
  137. mathjax_settings[key] = value
  138. return mathjax_settings
  139. def process_summary(article):
  140. """Ensures summaries are not cut off. Also inserts
  141. mathjax script so that math will be rendered"""
  142. summary = article.summary
  143. summary_parsed = BeautifulSoup(summary, 'html.parser')
  144. math = summary_parsed.find_all(class_='math')
  145. if len(math) > 0:
  146. last_math_text = math[-1].get_text()
  147. if len(last_math_text) > 3 and last_math_text[-3:] == '...':
  148. content_parsed = BeautifulSoup(article._content, 'html.parser')
  149. full_text = content_parsed.find_all(class_='math')[len(math)-1].get_text()
  150. math[-1].string = "%s ..." % full_text
  151. summary = summary_parsed.decode()
  152. # clear memoization cache
  153. import functools
  154. if isinstance(article.get_summary, functools.partial):
  155. memoize_instance = article.get_summary.func.__self__
  156. memoize_instance.cache.clear()
  157. article._summary = "%s<script type='text/javascript'>%s</script>" % (summary, process_summary.mathjax_script)
  158. def configure_typogrify(pelicanobj, mathjax_settings):
  159. """Instructs Typogrify to ignore math tags - which allows Typogrify
  160. to play nicely with math related content"""
  161. # If Typogrify is not being used, then just exit
  162. if not pelicanobj.settings.get('TYPOGRIFY', False):
  163. return
  164. try:
  165. import typogrify
  166. from distutils.version import LooseVersion
  167. if LooseVersion(typogrify.__version__) < LooseVersion('2.0.7'):
  168. raise TypeError('Incorrect version of Typogrify')
  169. from typogrify.filters import typogrify
  170. # At this point, we are happy to use Typogrify, meaning
  171. # it is installed and it is a recent enough version
  172. # that can be used to ignore all math
  173. # Instantiate markdown extension and append it to the current extensions
  174. pelicanobj.settings['TYPOGRIFY_IGNORE_TAGS'].extend(['.math', 'script']) # ignore math class and script
  175. except (ImportError, TypeError) as e:
  176. pelicanobj.settings['TYPOGRIFY'] = False # disable Typogrify
  177. if isinstance(e, ImportError):
  178. print("\nTypogrify is not installed, so it is being ignored.\nIf you want to use it, please install via: pip install typogrify\n")
  179. if isinstance(e, TypeError):
  180. print("\nA more recent version of Typogrify is needed for the render_math module.\nPlease upgrade Typogrify to the latest version (anything equal or above version 2.0.7 is okay).\nTypogrify will be turned off due to this reason.\n")
  181. def process_mathjax_script(mathjax_settings):
  182. """Load the mathjax script template from file, and render with the settings"""
  183. # Read the mathjax javascript template from file
  184. with open (os.path.dirname(os.path.realpath(__file__))
  185. + '/mathjax_script_template', 'r') as mathjax_script_template:
  186. mathjax_template = mathjax_script_template.read()
  187. return mathjax_template.format(**mathjax_settings)
  188. def mathjax_for_markdown(pelicanobj, mathjax_script, mathjax_settings):
  189. """Instantiates a customized markdown extension for handling mathjax
  190. related content"""
  191. # Create the configuration for the markdown template
  192. config = {}
  193. config['mathjax_script'] = mathjax_script
  194. config['math_tag_class'] = 'math'
  195. config['auto_insert'] = mathjax_settings['auto_insert']
  196. # Instantiate markdown extension and append it to the current extensions
  197. try:
  198. if isinstance(pelicanobj.settings.get('MD_EXTENSIONS'), list): # pelican 3.6.3 and earlier
  199. pelicanobj.settings['MD_EXTENSIONS'].append(PelicanMathJaxExtension(config))
  200. else:
  201. pelicanobj.settings['MARKDOWN'].setdefault('extensions', []).append(PelicanMathJaxExtension(config))
  202. except:
  203. sys.excepthook(*sys.exc_info())
  204. sys.stderr.write("\nError - the pelican mathjax markdown extension failed to configure. MathJax is non-functional.\n")
  205. sys.stderr.flush()
  206. def mathjax_for_rst(pelicanobj, mathjax_script, mathjax_settings):
  207. """Setup math for RST"""
  208. docutils_settings = pelicanobj.settings.get('DOCUTILS_SETTINGS', {})
  209. docutils_settings.setdefault('math_output', 'MathJax %s' % mathjax_settings['source'])
  210. pelicanobj.settings['DOCUTILS_SETTINGS'] = docutils_settings
  211. rst_add_mathjax.mathjax_script = mathjax_script
  212. def pelican_init(pelicanobj):
  213. """
  214. Loads the mathjax script according to the settings.
  215. Instantiate the Python markdown extension, passing in the mathjax
  216. script as config parameter.
  217. """
  218. # Process settings, and set global var
  219. mathjax_settings = process_settings(pelicanobj)
  220. # Generate mathjax script
  221. mathjax_script = process_mathjax_script(mathjax_settings)
  222. # Configure Typogrify
  223. configure_typogrify(pelicanobj, mathjax_settings)
  224. # Configure Mathjax For Markdown
  225. if PelicanMathJaxExtension:
  226. mathjax_for_markdown(pelicanobj, mathjax_script, mathjax_settings)
  227. # Configure Mathjax For RST
  228. mathjax_for_rst(pelicanobj, mathjax_script, mathjax_settings)
  229. # Set process_summary's mathjax_script variable
  230. process_summary.mathjax_script = None
  231. if mathjax_settings['process_summary']:
  232. process_summary.mathjax_script = mathjax_script
  233. def rst_add_mathjax(content):
  234. """Adds mathjax script for reStructuredText"""
  235. # .rst is the only valid extension for reStructuredText files
  236. _, ext = os.path.splitext(os.path.basename(content.source_path))
  237. if ext != '.rst':
  238. return
  239. # If math class is present in text, add the javascript
  240. # note that RST hardwires mathjax to be class "math"
  241. if 'class="math"' in content._content:
  242. content._content += "<script type='text/javascript'>%s</script>" % rst_add_mathjax.mathjax_script
  243. def process_rst_and_summaries(content_generators):
  244. """
  245. Ensure mathjax script is applied to RST and summaries are
  246. corrected if specified in user settings.
  247. Handles content attached to ArticleGenerator and PageGenerator objects,
  248. since the plugin doesn't know how to handle other Generator types.
  249. For reStructuredText content, examine both articles and pages.
  250. If article or page is reStructuredText and there is math present,
  251. append the mathjax script.
  252. Also process summaries if present (only applies to articles)
  253. and user wants summaries processed (via user settings)
  254. """
  255. for generator in content_generators:
  256. if isinstance(generator, generators.ArticlesGenerator):
  257. for article in (
  258. generator.articles +
  259. generator.translations +
  260. generator.drafts):
  261. rst_add_mathjax(article)
  262. #optionally fix truncated formulae in summaries.
  263. if process_summary.mathjax_script is not None:
  264. process_summary(article)
  265. elif isinstance(generator, generators.PagesGenerator):
  266. for page in generator.pages:
  267. rst_add_mathjax(page)
  268. def register():
  269. """Plugin registration"""
  270. signals.initialized.connect(pelican_init)
  271. signals.all_generators_finalized.connect(process_rst_and_summaries)