diff options
Diffstat (limited to 'plugins/render_math')
-rwxr-xr-x | plugins/render_math/Readme.md | 167 | ||||
-rwxr-xr-x | plugins/render_math/__init__.py | 1 | ||||
-rwxr-xr-x | plugins/render_math/math.py | 367 | ||||
-rwxr-xr-x | plugins/render_math/mathjax_script_template | 61 | ||||
-rwxr-xr-x | plugins/render_math/pelican_mathjax_markdown_extension.py | 158 | ||||
-rwxr-xr-x | plugins/render_math/requirements.txt | 1 | ||||
-rwxr-xr-x | plugins/render_math/test_data/article.ipynb | 42 | ||||
-rwxr-xr-x | plugins/render_math/test_data/article.nbdata | 5 | ||||
-rwxr-xr-x | plugins/render_math/test_data/article_with_math_formulas.rst | 20 | ||||
-rwxr-xr-x | plugins/render_math/test_render_math.py | 56 |
10 files changed, 878 insertions, 0 deletions
diff --git a/plugins/render_math/Readme.md b/plugins/render_math/Readme.md new file mode 100755 index 0000000..7d541aa --- /dev/null +++ b/plugins/render_math/Readme.md @@ -0,0 +1,167 @@ +Math Render Plugin For Pelican +============================== + +**NOTE: [This plugin has been moved to its own repository](https://github.com/pelican-plugins/render-math). Please file any issues/PRs there. Once all plugins have been migrated to the [new Pelican Plugins organization](https://github.com/pelican-plugins), this monolithic repository will be archived.** + +This plugin gives pelican the ability to render mathematics. It accomplishes +this by using the [MathJax](http://www.mathjax.org/) javascript engine. + +The plugin also ensures that Typogrify and recognized math "play" nicely together, by +ensuring [Typogrify](https://github.com/mintchaos/typogrify) does not alter math content. + +Both Markdown and reStructuredText is supported. + +Requirements +------------ + + * Pelican version *3.6* or above is required. + * Typogrify version *2.0.7* or higher is needed for Typogrify to play + "nicely" with this plugin. If this version is not available, Typogrify + will be disabled for the entire site. + * BeautifulSoup4 is required to correct summaries. If BeautifulSoup4 is + not installed, summary processing will be ignored, even if specified + in user settings. + +Installation +------------ +To enable, ensure that `render_math` plugin is accessible. +Then add the following to settings.py: + + PLUGINS = ["render_math"] + +Your site is now capable of rendering math math using the mathjax JavaScript +engine. No alterations to the template is needed, just use and enjoy! + +However, if you wish, you can set the `auto_insert` setting to `False` which +will disable the mathjax script from being automatically inserted into the +content. You would only want to do this if you had control over the template +and wanted to insert the script manually. + +### Typogrify +In the past, using [Typgogrify](https://github.com/mintchaos/typogrify) would +alter the math contents resulting in math that could not be rendered by MathJax. +The only option was to ensure that Typogrify was disabled in the settings. + +The problem has been rectified in this plugin, but it requires at a minimum +[Typogrify version 2.0.7](https://pypi.python.org/pypi/typogrify) (or higher). +If this version is not present, the plugin will disable Typogrify for the entire +site. + +### BeautifulSoup4 +Pelican creates summaries by truncating the contents to a specified user length. +The truncation process is oblivious to any math and can therefore destroy +the math output in the summary. + +To restore math, [BeautifulSoup4](https://pypi.python.org/pypi/beautifulsoup4/4.4.0) +is used. If it is not installed, no summary processing will happen. + +Usage +----- +### Templates +No alteration is needed to a template for this plugin to work. Just install +the plugin and start writing your Math. + +### Settings +Certain MathJax rendering options can be set. These options +are in a dictionary variable called `MATH_JAX` in the pelican +settings file. + +The dictionary can be set with the following keys: + + * `align`: [string] controls how displayed math will be aligned. Can be set to either +`'left'`, `'right'` or `'center'`. **Default Value**: `'center'`. + * `auto_insert`: [boolean] will insert the mathjax script into content that it is +detected to have math in it. Setting it to false is not recommended. +**Default Value**: `True` + * `indent`: [string] if `align` not set to `'center'`, then this controls the indent +level. **Default Value**: `'0em'`. + * `show_menu`: [boolean] controls whether the mathjax contextual menu is shown. +**Default Value**: `True` + * `process_escapes`: [boolean] controls whether mathjax processes escape sequences. +**Default Value**: `True` + * `mathjax_font`: [string] will force mathjax to use the chosen font. Current choices +for the font is `sanserif`, `typewriter` or `fraktur`. If this is not set, it will +use the default font settings. **Default Value**: `default` + * `latex_preview`: [string] controls the preview message users are shown while mathjax is +rendering LaTex. If set to `'Tex'`, then the TeX code is used as the preview +(which will be visible until it is processed by MathJax). **Default Value**: `'Tex'` + * `color`: [string] controls the color of the mathjax rendered font. **Default Value**: `'inherit'` + * `linebreak_automatic`: [boolean] If set, Mathjax will try to *intelligently* break up displayed math +(Note: It will not work for inline math). This is very useful for a responsive site. It +is turned off by default due to it potentially being CPU expensive. **Default Value**: `False` + * `tex_extensions`: [list] a list of [latex extensions](http://docs.mathjax.org/en/latest/tex.html#tex-and-latex-extensions) +accepted by mathjax. **Default Value**: `[]` (empty list) + * `responsive`: [boolean] tries to make displayed math render responsively. It does by determining if the width +is less than `responsive_break` (see below) and if so, sets `align` to `left`, `indent` to `0em` and `linebreak_automatic` to `True`. +**Default Value**: `False` (defaults to `False` for backward compatibility) + * `responsive_break`: [integer] a number (in pixels) representing the width breakpoint that is used +when setting `responsive_align` to `True`. **Default Value**: 768 + * `process_summary`: [boolean] ensures math will render in summaries and fixes math in that were cut off. +Requires [BeautifulSoup4](http://www.crummy.com/software/BeautifulSoup/bs4/doc/) be installed. **Default Value**: `True` + * `message_style`: [string] This value controls the verbosity of the messages in the lower left-hand corner. Set it to `None` to eliminate all messages. +**Default Value**: normal + +#### Settings Examples +Make math render in blue and displaymath align to the left: + + MATH_JAX = {'color':'blue','align':left} + +Use the [color](http://docs.mathjax.org/en/latest/tex.html#color) and +[mhchem](http://docs.mathjax.org/en/latest/tex.html#mhchem) extensions: + + MATH_JAX = {'tex_extensions': ['color.js','mhchem.js']} + +#### Resulting HTML +Inlined math is wrapped in `span` tags, while displayed math is wrapped in `div` tags. +These tags will have a class attribute that is set to `math` which +can be used by template designers to alter the display of the math. + +Markdown +-------- +This plugin implements a custom extension for markdown resulting in math +being a "first class citizen" for Pelican. + +### Inlined Math +Math between `$`..`$`, for example, `$`x^2`$`, will be rendered inline +with respect to the current html block. Note: To use inline math, there +must *not* be any whitespace before the ending `$`. So for example: + + * **Relevant inline math**: `$e=mc^2$` + * **Will not render as inline math**: `$40 vs $50` + +### Displayed Math +Math between `$$`..`$$` will be rendered "block style", for example, `$$`x^2`$$`, will be rendered centered in a +new paragraph. + +#### Other Latex Display Math commands +The other LaTeX commands which usually invoke display math mode from text mode +are supported, +and are automatically treated like `$$`-style displayed math +in that they are rendered "block" style on their own lines. +For example, `\begin{equation}` x^2 `\end{equation}`, +will be rendered in its own block with a right justified equation number +at the top of the block. This equation number can be referenced in the document. +To do this, use a `label` inside of the equation format and then refer to that label +using `ref`. For example: `\begin{equation}` `\label{eq}` X^2 `\end{equation}`. +Now refer to that equation number by `$`\ref{eq}`$`. + +reStructuredText +---------------- +If there is math detected in reStructuredText document, the plugin will automatically +set the [math_output](http://docutils.sourceforge.net/docs/user/config.html#math-output) configuration setting to `MathJax`. + +### Inlined Math +Inlined math needs to use the [math role](http://docutils.sourceforge.net/docs/ref/rst/roles.html#math): + +``` +The area of a circle is :math:`A_\text{c} = (\pi/4) d^2`. +``` + +### Displayed Math +Displayed math uses the [math block](http://docutils.sourceforge.net/docs/ref/rst/directives.html#math): + +``` +.. math:: + + α_t(i) = P(O_1, O_2, … O_t, q_t = S_i λ) +``` diff --git a/plugins/render_math/__init__.py b/plugins/render_math/__init__.py new file mode 100755 index 0000000..2ac15dd --- /dev/null +++ b/plugins/render_math/__init__.py @@ -0,0 +1 @@ +from .math import * diff --git a/plugins/render_math/math.py b/plugins/render_math/math.py new file mode 100755 index 0000000..165d59e --- /dev/null +++ b/plugins/render_math/math.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- +""" +Math Render Plugin for Pelican +============================== +This plugin allows your site to render Math. It uses +the MathJax JavaScript engine. + +For markdown, the plugin works by creating a Markdown +extension which is used during the markdown compilation +stage. Math therefore gets treated like a "first class +citizen" in Pelican + +For reStructuredText, the plugin instructs the rst engine +to output Mathjax for all math. + +The mathjax script is by default automatically inserted +into the HTML. + +Typogrify Compatibility +----------------------- +This plugin now plays nicely with Typogrify, but it +requires Typogrify version 2.07 or above. + +User Settings +------------- +Users are also able to pass a dictionary of settings +in the settings file which will control how the MathJax +library renders things. This could be very useful for +template builders that want to adjust the look and feel of +the math. See README for more details. +""" + +import os +import sys + +from pelican import signals, generators + +try: + from bs4 import BeautifulSoup +except ImportError as e: + BeautifulSoup = None + +try: + from . pelican_mathjax_markdown_extension import PelicanMathJaxExtension +except ImportError as e: + PelicanMathJaxExtension = None + +try: + string_type = basestring +except NameError: + string_type = str + + +def process_settings(pelicanobj): + """Sets user specified MathJax settings (see README for more details)""" + + mathjax_settings = {} + + # NOTE TO FUTURE DEVELOPERS: Look at the README and what is happening in + # this function if any additional changes to the mathjax settings need to + # be incorporated. Also, please inline comment what the variables + # will be used for + + # Default settings + mathjax_settings['auto_insert'] = True # if set to true, it will insert mathjax script automatically into content without needing to alter the template. + mathjax_settings['align'] = 'center' # controls alignment of of displayed equations (values can be: left, right, center) + mathjax_settings['indent'] = '0em' # if above is not set to 'center', then this setting acts as an indent + mathjax_settings['show_menu'] = 'true' # controls whether to attach mathjax contextual menu + mathjax_settings['process_escapes'] = 'true' # controls whether escapes are processed + mathjax_settings['latex_preview'] = 'TeX' # controls what user sees while waiting for LaTex to render + mathjax_settings['color'] = 'inherit' # controls color math is rendered in + 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) + 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) + mathjax_settings['responsive'] = 'false' # Tries to make displayed math responsive + mathjax_settings['responsive_break'] = '768' # The break point at which it math is responsively aligned (in pixels) + mathjax_settings['mathjax_font'] = 'default' # forces mathjax to use the specified font. + mathjax_settings['process_summary'] = BeautifulSoup is not None # will fix up summaries if math is cut off. Requires beautiful soup + 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 + mathjax_settings['font_list'] = ['STIX', 'TeX'] # Include in order of preference among TeX, STIX-Web, Asana-Math, Neo-Euler, Gyre-Pagella, Gyre-Termes and Latin-Modern + mathjax_settings['equation_numbering'] = 'none' # AMS, auto, none + + # Source for MathJax + mathjax_settings['source'] = "'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML'" + + # Get the user specified settings + try: + settings = pelicanobj.settings['MATH_JAX'] + except: + settings = None + + # If no settings have been specified, then return the defaults + if not isinstance(settings, dict): + return mathjax_settings + + # The following mathjax settings can be set via the settings dictionary + for key, value in ((key, settings[key]) for key in settings): + # Iterate over dictionary in a way that is compatible with both version 2 + # and 3 of python + + if key == 'align': + typeVal = isinstance(value, string_type) + + if not typeVal: + continue + + if value == 'left' or value == 'right' or value == 'center': + mathjax_settings[key] = value + else: + mathjax_settings[key] = 'center' + + if key == 'indent': + mathjax_settings[key] = value + + if key == 'source': + mathjax_settings[key] = value + + if key == 'show_menu' and isinstance(value, bool): + mathjax_settings[key] = 'true' if value else 'false' + + if key == 'message_style': + mathjax_settings[key] = value if value is not None else 'none' + + if key == 'auto_insert' and isinstance(value, bool): + mathjax_settings[key] = value + + if key == 'process_escapes' and isinstance(value, bool): + mathjax_settings[key] = 'true' if value else 'false' + + if key == 'latex_preview': + typeVal = isinstance(value, string_type) + + if not typeVal: + continue + + mathjax_settings[key] = value + + if key == 'color': + typeVal = isinstance(value, string_type) + + if not typeVal: + continue + + mathjax_settings[key] = value + + if key == 'linebreak_automatic' and isinstance(value, bool): + mathjax_settings[key] = 'true' if value else 'false' + + if key == 'process_summary' and isinstance(value, bool): + if value and BeautifulSoup is None: + print("BeautifulSoup4 is needed for summaries to be processed by render_math\nPlease install it") + value = False + + mathjax_settings[key] = value + + if key == 'responsive' and isinstance(value, bool): + mathjax_settings[key] = 'true' if value else 'false' + + if key == 'responsive_break' and isinstance(value, int): + mathjax_settings[key] = str(value) + + if key == 'tex_extensions' and isinstance(value, list): + # filter string values, then add '' to them + value = filter(lambda string: isinstance(string, string_type), value) + value = map(lambda string: "'%s'" % string, value) + mathjax_settings[key] = ',' + ','.join(value) + + if key == 'mathjax_font': + typeVal = isinstance(value, string_type) + + if not typeVal: + continue + + value = value.lower() + + if value == 'sanserif': + value = 'SansSerif' + elif value == 'fraktur': + value = 'Fraktur' + elif value == 'typewriter': + value = 'Typewriter' + else: + value = 'default' + + mathjax_settings[key] = value + + if key == 'font_list' and isinstance(value, list): + # make an array string from the list + value = filter(lambda string: isinstance(string, string_type), value) + value = map(lambda string: ",'%s'" % string, value) + mathjax_settings[key] = ''.join(value)[1:] + + if key == 'equation_numbering': + mathjax_settings[key] = value if value is not None else 'none' + + return mathjax_settings + +def process_summary(article): + """Ensures summaries are not cut off. Also inserts + mathjax script so that math will be rendered""" + + summary = article.summary + summary_parsed = BeautifulSoup(summary, 'html.parser') + math = summary_parsed.find_all(class_='math') + + if len(math) > 0: + last_math_text = math[-1].get_text() + if len(last_math_text) > 3 and last_math_text[-3:] == '...': + content_parsed = BeautifulSoup(article._content, 'html.parser') + full_text = content_parsed.find_all(class_='math')[len(math)-1].get_text() + math[-1].string = "%s ..." % full_text + summary = summary_parsed.decode() + + # clear memoization cache + import functools + if isinstance(article.get_summary, functools.partial): + memoize_instance = article.get_summary.func.__self__ + memoize_instance.cache.clear() + + article._summary = "%s<script type='text/javascript'>%s</script>" % (summary, process_summary.mathjax_script) + +def configure_typogrify(pelicanobj, mathjax_settings): + """Instructs Typogrify to ignore math tags - which allows Typogrify + to play nicely with math related content""" + + # If Typogrify is not being used, then just exit + if not pelicanobj.settings.get('TYPOGRIFY', False): + return + + try: + import typogrify + from distutils.version import LooseVersion + + if LooseVersion(typogrify.__version__) < LooseVersion('2.0.7'): + raise TypeError('Incorrect version of Typogrify') + + from typogrify.filters import typogrify + + # At this point, we are happy to use Typogrify, meaning + # it is installed and it is a recent enough version + # that can be used to ignore all math + # Instantiate markdown extension and append it to the current extensions + pelicanobj.settings['TYPOGRIFY_IGNORE_TAGS'].extend(['.math', 'script']) # ignore math class and script + + except (ImportError, TypeError) as e: + pelicanobj.settings['TYPOGRIFY'] = False # disable Typogrify + + if isinstance(e, ImportError): + print("\nTypogrify is not installed, so it is being ignored.\nIf you want to use it, please install via: pip install typogrify\n") + + if isinstance(e, TypeError): + 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") + +def process_mathjax_script(mathjax_settings): + """Load the mathjax script template from file, and render with the settings""" + + # Read the mathjax javascript template from file + with open (os.path.dirname(os.path.realpath(__file__)) + + '/mathjax_script_template', 'r') as mathjax_script_template: + mathjax_template = mathjax_script_template.read() + + return mathjax_template.format(**mathjax_settings) + +def mathjax_for_markdown(pelicanobj, mathjax_script, mathjax_settings): + """Instantiates a customized markdown extension for handling mathjax + related content""" + + # Create the configuration for the markdown template + config = {} + config['mathjax_script'] = mathjax_script + config['math_tag_class'] = 'math' + config['auto_insert'] = mathjax_settings['auto_insert'] + + # Instantiate markdown extension and append it to the current extensions + try: + if isinstance(pelicanobj.settings.get('MD_EXTENSIONS'), list): # pelican 3.6.3 and earlier + pelicanobj.settings['MD_EXTENSIONS'].append(PelicanMathJaxExtension(config)) + else: + pelicanobj.settings['MARKDOWN'].setdefault('extensions', []).append(PelicanMathJaxExtension(config)) + except: + sys.excepthook(*sys.exc_info()) + sys.stderr.write("\nError - the pelican mathjax markdown extension failed to configure. MathJax is non-functional.\n") + sys.stderr.flush() + +def mathjax_for_rst(pelicanobj, mathjax_script, mathjax_settings): + """Setup math for RST""" + docutils_settings = pelicanobj.settings.get('DOCUTILS_SETTINGS', {}) + docutils_settings.setdefault('math_output', 'MathJax %s' % mathjax_settings['source']) + pelicanobj.settings['DOCUTILS_SETTINGS'] = docutils_settings + rst_add_mathjax.mathjax_script = mathjax_script + +def pelican_init(pelicanobj): + """ + Loads the mathjax script according to the settings. + Instantiate the Python markdown extension, passing in the mathjax + script as config parameter. + """ + + # Process settings, and set global var + mathjax_settings = process_settings(pelicanobj) + + # Generate mathjax script + mathjax_script = process_mathjax_script(mathjax_settings) + + # Configure Typogrify + configure_typogrify(pelicanobj, mathjax_settings) + + # Configure Mathjax For Markdown + if PelicanMathJaxExtension: + mathjax_for_markdown(pelicanobj, mathjax_script, mathjax_settings) + + # Configure Mathjax For RST + mathjax_for_rst(pelicanobj, mathjax_script, mathjax_settings) + + # Set process_summary's mathjax_script variable + process_summary.mathjax_script = None + if mathjax_settings['process_summary']: + process_summary.mathjax_script = mathjax_script + +def rst_add_mathjax(content): + """Adds mathjax script for reStructuredText""" + + # .rst is the only valid extension for reStructuredText files + _, ext = os.path.splitext(os.path.basename(content.source_path)) + if ext != '.rst': + return + + # If math class is present in text, add the javascript + # note that RST hardwires mathjax to be class "math" + if 'class="math"' in content._content: + content._content += "<script type='text/javascript'>%s</script>" % rst_add_mathjax.mathjax_script + +def process_rst_and_summaries(content_generators): + """ + Ensure mathjax script is applied to RST and summaries are + corrected if specified in user settings. + + Handles content attached to ArticleGenerator and PageGenerator objects, + since the plugin doesn't know how to handle other Generator types. + + For reStructuredText content, examine both articles and pages. + If article or page is reStructuredText and there is math present, + append the mathjax script. + + Also process summaries if present (only applies to articles) + and user wants summaries processed (via user settings) + """ + + for generator in content_generators: + if isinstance(generator, generators.ArticlesGenerator): + for article in ( + generator.articles + + generator.translations + + generator.drafts): + rst_add_mathjax(article) + #optionally fix truncated formulae in summaries. + if process_summary.mathjax_script is not None: + process_summary(article) + elif isinstance(generator, generators.PagesGenerator): + for page in generator.pages: + rst_add_mathjax(page) + for page in generator.hidden_pages: + rst_add_mathjax(page) + +def register(): + """Plugin registration""" + signals.initialized.connect(pelican_init) + signals.all_generators_finalized.connect(process_rst_and_summaries) diff --git a/plugins/render_math/mathjax_script_template b/plugins/render_math/mathjax_script_template new file mode 100755 index 0000000..db8aeba --- /dev/null +++ b/plugins/render_math/mathjax_script_template @@ -0,0 +1,61 @@ +if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {{ + var align = "{align}", + indent = "{indent}", + linebreak = "{linebreak_automatic}"; + + if ({responsive}) {{ + align = (screen.width < {responsive_break}) ? "left" : align; + indent = (screen.width < {responsive_break}) ? "0em" : indent; + linebreak = (screen.width < {responsive_break}) ? 'true' : linebreak; + }} + + var mathjaxscript = document.createElement('script'); + mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#'; + mathjaxscript.type = 'text/javascript'; + mathjaxscript.src = {source}; + + var configscript = document.createElement('script'); + configscript.type = 'text/x-mathjax-config'; + configscript[(window.opera ? "innerHTML" : "text")] = + "MathJax.Hub.Config({{" + + " config: ['MMLorHTML.js']," + + " TeX: {{ extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'{tex_extensions}], equationNumbers: {{ autoNumber: '{equation_numbering}' }} }}," + + " jax: ['input/TeX','input/MathML','output/HTML-CSS']," + + " extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," + + " displayAlign: '"+ align +"'," + + " displayIndent: '"+ indent +"'," + + " showMathMenu: {show_menu}," + + " messageStyle: '{message_style}'," + + " tex2jax: {{ " + + " inlineMath: [ ['\\\\(','\\\\)'] ], " + + " displayMath: [ ['$$','$$'] ]," + + " processEscapes: {process_escapes}," + + " preview: '{latex_preview}'," + + " }}, " + + " 'HTML-CSS': {{ " + + " availableFonts: {font_list}," + + " preferredFont: 'STIX'," + + " styles: {{ '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {{color: '{color} ! important'}} }}," + + " linebreaks: {{ automatic: "+ linebreak +", width: '90% container' }}," + + " }}, " + + "}}); " + + "if ('{mathjax_font}' !== 'default') {{" + + "MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {{" + + "var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" + + "VARIANT['normal'].fonts.unshift('MathJax_{mathjax_font}');" + + "VARIANT['bold'].fonts.unshift('MathJax_{mathjax_font}-bold');" + + "VARIANT['italic'].fonts.unshift('MathJax_{mathjax_font}-italic');" + + "VARIANT['-tex-mathit'].fonts.unshift('MathJax_{mathjax_font}-italic');" + + "}});" + + "MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {{" + + "var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" + + "VARIANT['normal'].fonts.unshift('MathJax_{mathjax_font}');" + + "VARIANT['bold'].fonts.unshift('MathJax_{mathjax_font}-bold');" + + "VARIANT['italic'].fonts.unshift('MathJax_{mathjax_font}-italic');" + + "VARIANT['-tex-mathit'].fonts.unshift('MathJax_{mathjax_font}-italic');" + + "}});" + + "}}"; + + (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript); + (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript); +}} diff --git a/plugins/render_math/pelican_mathjax_markdown_extension.py b/plugins/render_math/pelican_mathjax_markdown_extension.py new file mode 100755 index 0000000..e739363 --- /dev/null +++ b/plugins/render_math/pelican_mathjax_markdown_extension.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +""" +Pelican Mathjax Markdown Extension +================================== +An extension for the Python Markdown module that enables +the Pelican python blog to process mathjax. This extension +gives Pelican the ability to use Mathjax as a "first class +citizen" of the blog +""" + +import markdown + +from markdown.util import etree +from markdown.util import AtomicString + +class PelicanMathJaxPattern(markdown.inlinepatterns.Pattern): + """Inline markdown processing that matches mathjax""" + + def __init__(self, pelican_mathjax_extension, tag, pattern): + super(PelicanMathJaxPattern,self).__init__(pattern) + self.math_tag_class = pelican_mathjax_extension.getConfig('math_tag_class') + self.pelican_mathjax_extension = pelican_mathjax_extension + self.tag = tag + + def handleMatch(self, m): + node = markdown.util.etree.Element(self.tag) + node.set('class', self.math_tag_class) + + prefix = '\\(' if m.group('prefix') == '$' else m.group('prefix') + suffix = '\\)' if m.group('suffix') == '$' else m.group('suffix') + node.text = markdown.util.AtomicString(prefix + m.group('math') + suffix) + + # If mathjax was successfully matched, then JavaScript needs to be added + # for rendering. The boolean below indicates this + self.pelican_mathjax_extension.mathjax_needed = True + return node + +class PelicanMathJaxCorrectDisplayMath(markdown.treeprocessors.Treeprocessor): + """Corrects invalid html that results from a <div> being put inside + a <p> for displayed math""" + + def __init__(self, pelican_mathjax_extension): + self.pelican_mathjax_extension = pelican_mathjax_extension + + def correct_html(self, root, children, div_math, insert_idx, text): + """Separates out <div class="math"> from the parent tag <p>. Anything + in between is put into its own parent tag of <p>""" + + current_idx = 0 + + for idx in div_math: + el = markdown.util.etree.Element('p') + el.text = text + el.extend(children[current_idx:idx]) + + # Test to ensure that empty <p> is not inserted + if len(el) != 0 or (el.text and not el.text.isspace()): + root.insert(insert_idx, el) + insert_idx += 1 + + text = children[idx].tail + children[idx].tail = None + root.insert(insert_idx, children[idx]) + insert_idx += 1 + current_idx = idx+1 + + el = markdown.util.etree.Element('p') + el.text = text + el.extend(children[current_idx:]) + + if len(el) != 0 or (el.text and not el.text.isspace()): + root.insert(insert_idx, el) + + def run(self, root): + """Searches for <div class="math"> that are children in <p> tags and corrects + the invalid HTML that results""" + + math_tag_class = self.pelican_mathjax_extension.getConfig('math_tag_class') + + for parent in root: + div_math = [] + children = list(parent) + + for div in parent.findall('div'): + if div.get('class') == math_tag_class: + div_math.append(children.index(div)) + + # Do not process further if no displayed math has been found + if not div_math: + continue + + insert_idx = list(root).index(parent) + self.correct_html(root, children, div_math, insert_idx, parent.text) + root.remove(parent) # Parent must be removed last for correct insertion index + + return root + +class PelicanMathJaxAddJavaScript(markdown.treeprocessors.Treeprocessor): + """Tree Processor for adding Mathjax JavaScript to the blog""" + + def __init__(self, pelican_mathjax_extension): + self.pelican_mathjax_extension = pelican_mathjax_extension + + def run(self, root): + # If no mathjax was present, then exit + if (not self.pelican_mathjax_extension.mathjax_needed): + return root + + # Add the mathjax script to the html document + mathjax_script = etree.Element('script') + mathjax_script.set('type','text/javascript') + mathjax_script.text = AtomicString(self.pelican_mathjax_extension.getConfig('mathjax_script')) + root.append(mathjax_script) + + # Reset the boolean switch to false so that script is only added + # to other pages if needed + self.pelican_mathjax_extension.mathjax_needed = False + return root + +class PelicanMathJaxExtension(markdown.Extension): + """A markdown extension enabling mathjax processing in Markdown for Pelican""" + def __init__(self, config): + + try: + # Needed for markdown versions >= 2.5 + self.config['mathjax_script'] = ['', 'Mathjax JavaScript script'] + self.config['math_tag_class'] = ['math', 'The class of the tag in which mathematics is wrapped'] + self.config['auto_insert'] = [True, 'Determines if mathjax script is automatically inserted into content'] + super(PelicanMathJaxExtension,self).__init__(**config) + except AttributeError: + # Markdown versions < 2.5 + config['mathjax_script'] = [config['mathjax_script'], 'Mathjax JavaScript script'] + config['math_tag_class'] = [config['math_tag_class'], 'The class of the tag in which mathematic is wrapped'] + config['auto_insert'] = [config['auto_insert'], 'Determines if mathjax script is automatically inserted into content'] + super(PelicanMathJaxExtension,self).__init__(config) + + # Used as a flag to determine if javascript + # needs to be injected into a document + self.mathjax_needed = False + + def extendMarkdown(self, md): + # Regex to detect mathjax + mathjax_inline_regex = r'(?P<prefix>\$)(?P<math>.+?)(?P<suffix>(?<!\s)\2)' + mathjax_display_regex = r'(?P<prefix>\$\$|\\begin\{(.+?)\})(?P<math>.+?)(?P<suffix>\2|\\end\{\3\})' + + # Process mathjax before escapes are processed since escape processing will + # intefer with mathjax. The order in which the displayed and inlined math + # is registered below matters: we should have higher priority than 'escape' which has 180 + md.inlinePatterns.register(PelicanMathJaxPattern(self, 'div', mathjax_display_regex), 'mathjax_displayed', 186) + md.inlinePatterns.register(PelicanMathJaxPattern(self, 'span', mathjax_inline_regex), 'mathjax_inlined', 185) + + # Correct the invalid HTML that results from teh displayed math (<div> tag within a <p> tag) + md.treeprocessors.register(PelicanMathJaxCorrectDisplayMath(self), 'mathjax_correctdisplayedmath', 15) + + # If necessary, add the JavaScript Mathjax library to the document. This must + # be last in the ordered dict (hence it is given the position '_end') + if self.getConfig('auto_insert'): + md.treeprocessors.register(PelicanMathJaxAddJavaScript(self), 'mathjax_addjavascript', 0) diff --git a/plugins/render_math/requirements.txt b/plugins/render_math/requirements.txt new file mode 100755 index 0000000..be64ec9 --- /dev/null +++ b/plugins/render_math/requirements.txt @@ -0,0 +1 @@ +typogrify diff --git a/plugins/render_math/test_data/article.ipynb b/plugins/render_math/test_data/article.ipynb new file mode 100755 index 0000000..890f8be --- /dev/null +++ b/plugins/render_math/test_data/article.ipynb @@ -0,0 +1,42 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "The formula is:\n", + "\n", + "\\begin{align*}A =\n", + "LL^{T}\n", + "\\end{align*}\n" + ], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3" + }, + "language_info": { + "name": "python", + "version": "3.7.3", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + }, + "kernel_info": { + "name": "python3" + }, + "nteract": { + "version": "0.14.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}
\ No newline at end of file diff --git a/plugins/render_math/test_data/article.nbdata b/plugins/render_math/test_data/article.nbdata new file mode 100755 index 0000000..bf13538 --- /dev/null +++ b/plugins/render_math/test_data/article.nbdata @@ -0,0 +1,5 @@ +Title: An article from a Jupyter notebook +Date: 2019-03-05 12:14 +Category: Mathematics +Tags: Linear Algebra, Python, Numpy, Scipy +Summary: This is a advance part of Linear Algebra with Python
\ No newline at end of file diff --git a/plugins/render_math/test_data/article_with_math_formulas.rst b/plugins/render_math/test_data/article_with_math_formulas.rst new file mode 100755 index 0000000..87dcc45 --- /dev/null +++ b/plugins/render_math/test_data/article_with_math_formulas.rst @@ -0,0 +1,20 @@ +Math formulas +############# + +:date: 2019-09-10 +:yeah: oh yeah ! +:summary: :math:`A_\text{c} = (\pi/4) d^2` + +The area of a circle is :math:`A_\text{c} = (\pi/4) d^2`. + +.. math:: + + α_t(i) = P(O_1, O_2, … O_t, q_t = S_i λ) + + A = + \begin{bmatrix} + a_{11} & a_{12} & a_{13} \ + a_{21} & a_{22} & a_{23} \ + a_{31} & a_{32} & a_{33} + \end{bmatrix} +
\ No newline at end of file diff --git a/plugins/render_math/test_render_math.py b/plugins/render_math/test_render_math.py new file mode 100755 index 0000000..b71f4e7 --- /dev/null +++ b/plugins/render_math/test_render_math.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from os.path import dirname, join +from tempfile import TemporaryDirectory + +from pelican import Pelican +from pelican.generators import ArticlesGenerator +from pelican.settings import configure_settings +from pelican.tests.support import get_settings, unittest +from pelican.writers import Writer + +from .math import pelican_init, process_rst_and_summaries + + +CUR_DIR = dirname(__file__) + + +class RenderMathTest(unittest.TestCase): + def test_ok_on_shared_test_data(self): + settings = get_settings(filenames={}) + settings['PATH'] = join(CUR_DIR, '..', 'test_data') + pelican_init(PelicanMock(settings)) + with TemporaryDirectory() as tmpdirname: + generator = _build_article_generator(settings, tmpdirname) + process_rst_and_summaries([generator]) + def test_ok_on_custom_data(self): + settings = get_settings(filenames={}) + settings['PATH'] = join(CUR_DIR, 'test_data') + settings['PLUGINS'] = ['pelican-ipynb.markup'] # to also parse .ipynb files + configure_settings(settings) + pelican_mock = PelicanMock(settings) + pelican_init(pelican_mock) + Pelican.init_plugins(pelican_mock) + with TemporaryDirectory() as tmpdirname: + generator = _build_article_generator(settings, tmpdirname) + process_rst_and_summaries([generator]) + for article in generator.articles: + if article.source_path.endswith('.rst'): + self.assertIn('mathjaxscript_pelican', article.content) + generator.generate_output(Writer(tmpdirname, settings=settings)) + + +def _build_article_generator(settings, output_path): + context = settings.copy() + context['generated_content'] = dict() + context['static_links'] = set() + article_generator = ArticlesGenerator( + context=context, settings=settings, + path=settings['PATH'], theme=settings['THEME'], output_path=output_path) + article_generator.generate_context() + return article_generator + +class PelicanMock: + 'A dummy class exposing the only attributes needed' + def __init__(self, settings): + self.plugins = [] + self.settings = settings |