chiark / gitweb /
Implement caching of rendered math output.
authorVladimír Vondruš <mosra@centrum.cz>
Mon, 11 Jun 2018 13:32:49 +0000 (15:32 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Mon, 11 Jun 2018 13:33:03 +0000 (15:33 +0200)
Stores the output of divsvgm in a dict and (de)serializes that in the
filesystem to preserve it across runs. Rough speedup:

 * For the Magnum website, initial generation went down from 14 seconds
   to 9, with subsequent runs just 2.5 seconds.
 * For the Magnum docs, generation time went down from 1:33 to just 14
   seconds.

17 files changed:
doc/doxygen.rst
doc/plugins/math-and-code.rst
doxygen/dox2html5.py
doxygen/test/contents_custom/Doxyfile
doxygen/test/contents_math/Doxyfile
doxygen/test/contents_math_cached/Doxyfile [new file with mode: 0644]
doxygen/test/contents_math_cached/input.dox [new file with mode: 0644]
doxygen/test/contents_math_cached/math.html [new file with mode: 0644]
doxygen/test/test_contents.py
doxygen/test/test_doxyfile.py
pelican-plugins/latex2svgextra.py
pelican-plugins/m/.gitignore
pelican-plugins/m/math.py
pelican-plugins/m/test/math_cached/page.html [new file with mode: 0644]
pelican-plugins/m/test/math_cached/page.rst [new file with mode: 0644]
pelican-plugins/m/test/math_uncached/page.rst [new file with mode: 0644]
pelican-plugins/m/test/test_math.py

index d2bb35d0c9c830fd40a24bab9c2f6c10fc2ad8c9..f910c5bd5a61836420f8d18952b0ca1aa94c9049 100644 (file)
@@ -317,6 +317,12 @@ Variable                            Description
 :ini:`M_EXPAND_INNER_TYPES`         Whether to expand inner types (e.g. a class
                                     inside a class) in the symbol tree. If not
                                     set, ``NO`` is used.
+:ini:`M_MATH_CACHE_FILE`            File to cache rendered math formulas. If
+                                    not set, ``m.math.cache`` file in the
+                                    output directory is used. Old cached output
+                                    is periodically pruned and new formulas
+                                    added to the file. Set it empty to disable
+                                    caching.
 :ini:`M_SEARCH_DISABLED`            Disable search functionality. If this
                                     option is set, no search data is compiled
                                     and the rendered HTML does not contain any
index 09b4e56846598f5d79e0d1cdebc393466208d547..7828a3d9cc9f7994e662406aea0a86422db34e38 100644 (file)
@@ -63,6 +63,8 @@ assumes presence of `m.htmlsanity <{filename}/plugins/htmlsanity.rst>`_.
 .. code:: python
 
     PLUGINS += ['m.htmlsanity', 'm.math']
+    M_MATH_RENDER_AS_CODE = False
+    M_MATH_CACHE_FILE = 'm.math.cache'
 
 In addition you need some LaTeX distribution installed. Use your distribution
 package manager, for example on Ubuntu:
@@ -146,6 +148,12 @@ want to add additional CSS classes, derive a custom role from it.
     Quaternion-conjugated dual quaternion is :math-info:`\hat q^* = q_0^* + q_\epsilon^*`,
     while dual-conjugation gives :math:`\overline{\hat q} = q_0 - \epsilon q_\epsilon`.
 
+The :py:`M_MATH_CACHE_FILE` setting (defaulting to ``m.math.cache`` in the
+site root directory) describes a file used for caching rendered LaTeX math
+formulas for speeding up subsequent runs. Old cached output is periodically
+pruned and new formulas added to the file. Set it to :py:`None` to disable
+caching.
+
 .. note-info::
 
     LaTeX can be sometimes a real pain to set up. In order to make it possible
index 2f776acbfe0ec4351c7fc3d39f9cc4132ba3ad91..0a758f4630598200cb28df0a21fc2f5c79f183f6 100755 (executable)
@@ -1208,7 +1208,7 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
 
             # Assume that Doxygen wrapped the formula properly to distinguish
             # between inline, block or special environment
-            rendered = latex2svg.latex2svg('{}'.format(i.text), params=latex2svgextra.params)
+            depth, svg = latex2svgextra.fetch_cached_or_render('{}'.format(i.text))
 
             # We should have decided about block/inline above
             assert formula_block is not None
@@ -1216,15 +1216,15 @@ def parse_desc_internal(state: State, element: ET.Element, immediate_parent: ET.
                 has_block_elements = True
                 out.parsed += '<div class="m-math{}">{}</div>'.format(
                     ' ' + add_css_class if add_css_class else '',
-                    latex2svgextra.patch(i.text, rendered, ''))
+                    latex2svgextra.patch(i.text, svg, ''))
             else:
                 # CSS classes and styling for proper vertical alignment. Depth is relative
                 # to font size, describes how below the line the text is. Scaling it back
                 # to 12pt font, scaled by 125% as set above in the config.
                 attribs = ' class="m-math{}" style="vertical-align: -{:.1f}pt;"'.format(
                     ' ' + add_inline_css_class if add_inline_css_class else '',
-                    (rendered['depth'] or 0.0)*12*1.25)
-                out.parsed += latex2svgextra.patch(i.text, rendered, attribs)
+                    (depth or 0.0)*12*1.25)
+                out.parsed += latex2svgextra.patch(i.text, svg, attribs)
 
         # Inline elements
         elif i.tag == 'linebreak':
@@ -2733,6 +2733,7 @@ def parse_doxyfile(state: State, doxyfile, config = None):
         'M_FAVICON': ['favicon-dark.png'],
         'M_LINKS_NAVBAR1': ['pages', 'namespaces'],
         'M_LINKS_NAVBAR2': ['annotated', 'files'],
+        'M_MATH_CACHE_FILE': ['m.math.cache'],
         'M_PAGE_FINE_PRINT': ['[default]'],
         'M_SEARCH_DISABLED': ['NO'],
         'M_SEARCH_DOWNLOAD_BINARY': ['NO'],
@@ -2835,6 +2836,7 @@ list using <span class="m-label m-dim">&darr;</span> and
               'M_PAGE_FINE_PRINT',
               'M_THEME_COLOR',
               'M_FAVICON',
+              'M_MATH_CACHE_FILE',
               'M_SEARCH_HELP',
               'M_SEARCH_EXTERNAL_URL']:
         if i in config: state.doxyfile[i] = ' '.join(config[i])
@@ -2879,6 +2881,15 @@ def run(doxyfile, templates=default_templates, wildcard=default_wildcard, index_
     xml_files = [os.path.join(xml_input, f) for f in glob.glob(os.path.join(xml_input, wildcard))]
     html_output = os.path.join(state.basedir, state.doxyfile['OUTPUT_DIRECTORY'], state.doxyfile['HTML_OUTPUT'])
 
+    # If math rendering cache is not disabled, load the previous version. If
+    # there is no cache, reset the cache to an empty state to avoid
+    # order-dependent issues when testing
+    math_cache_file = os.path.join(state.basedir, state.doxyfile['OUTPUT_DIRECTORY'], state.doxyfile['M_MATH_CACHE_FILE'])
+    if state.doxyfile['M_MATH_CACHE_FILE'] and os.path.exists(math_cache_file):
+        latex2svgextra.unpickle_cache(math_cache_file)
+    else:
+        latex2svgextra.unpickle_cache(None)
+
     if sort_globbed_files:
         xml_files_metadata.sort()
         xml_files.sort()
@@ -2982,6 +2993,10 @@ def run(doxyfile, templates=default_templates, wildcard=default_wildcard, index_
         logging.debug("copying {} to output".format(i))
         shutil.copy(i, os.path.join(html_output, os.path.basename(i)))
 
+    # Save updated math cache file
+    if state.doxyfile['M_MATH_CACHE_FILE']:
+        latex2svgextra.pickle_cache(math_cache_file)
+
 if __name__ == '__main__': # pragma: no cover
     parser = argparse.ArgumentParser()
     parser.add_argument('doxyfile', help="where the Doxyfile is")
index a0a7a22b561bc560a04f09d488812ef915cf252b..ecc3d464871fe6e35f26fde7f79cda8cc7f68336 100644 (file)
@@ -11,6 +11,7 @@ M_THEME_COLOR           =
 M_FAVICON               =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
+M_MATH_CACHE_FILE       =
 M_SEARCH_DISABLED       = YES
 
 ALIASES = \
index d1b0fce7bc98e5c47122d84ef122c6d216958ee6..59c69bc403351959f240b516f1594353d6df345e 100644 (file)
@@ -11,3 +11,4 @@ M_FAVICON               =
 M_LINKS_NAVBAR1         =
 M_LINKS_NAVBAR2         =
 M_SEARCH_DISABLED       = YES
+M_MATH_CACHE_FILE       =
diff --git a/doxygen/test/contents_math_cached/Doxyfile b/doxygen/test/contents_math_cached/Doxyfile
new file mode 100644 (file)
index 0000000..933dfe7
--- /dev/null
@@ -0,0 +1,14 @@
+INPUT                   = input.dox
+QUIET                   = YES
+GENERATE_HTML           = NO
+GENERATE_LATEX          = NO
+GENERATE_XML            = YES
+XML_PROGRAMLISTING      = NO
+
+M_PAGE_FINE_PRINT       =
+M_THEME_COLOR           =
+M_FAVICON               =
+M_LINKS_NAVBAR1         =
+M_LINKS_NAVBAR2         =
+M_SEARCH_DISABLED       = YES
+M_MATH_CACHE_FILE       = xml/math.cache
diff --git a/doxygen/test/contents_math_cached/input.dox b/doxygen/test/contents_math_cached/input.dox
new file mode 100644 (file)
index 0000000..4a1943e
--- /dev/null
@@ -0,0 +1,26 @@
+/** @page math Math
+
+In order to actually test use of the cache, I need to cheat a bit. Inline
+formula which is pi in the source but @f$ \pi @f$ here. Then a block
+formula which is a Pythagorean theorem in source but not in the output:
+
+@f[
+    a^2 + b^2 = c^2
+@f]
+
+*/
+
+/* The (uncached) output of below should be the same as the cached output of
+   above. Be sure to preserve the line breaks as well. */
+
+/** @page math-uncached Math
+
+In order to actually test use of the cache, I need to cheat a bit. Inline
+formula which is pi in the source but @f$ \frac{\tau}{2} @f$ here. Then a block
+formula which is a Pythagorean theorem in source but not in the output:
+
+@f[
+    a^3 + b^3 \neq c^3
+@f]
+
+*/
diff --git a/doxygen/test/contents_math_cached/math.html b/doxygen/test/contents_math_cached/math.html
new file mode 100644 (file)
index 0000000..533ac54
--- /dev/null
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>Math | My Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+doxygen.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          Math
+        </h1>
+<p>In order to actually test use of the cache, I need to cheat a bit. Inline formula which is pi in the source but <svg class="m-math" style="vertical-align: -5.2pt;" height='15.3267pt' version='1.1' viewBox='1.19551 -8.1387 4.67835 12.2613' width='5.84794pt'>
+<title>LaTeX Math</title>
+<desc>
+$ \pi $
+</desc>
+<defs>
+<path d='M2.50262 -2.90909H3.92927C4.05679 -2.90909 4.14446 -2.90909 4.22416 -2.97285C4.3198 -3.06052 4.34371 -3.16413 4.34371 -3.21196C4.34371 -3.43512 4.14446 -3.43512 4.00897 -3.43512H1.60199C1.43462 -3.43512 1.13176 -3.43512 0.74122 -3.05255C0.454296 -2.76563 0.231133 -2.399 0.231133 -2.34321C0.231133 -2.27148 0.286924 -2.24757 0.350685 -2.24757C0.430386 -2.24757 0.446326 -2.27148 0.494147 -2.33524C0.884682 -2.90909 1.35492 -2.90909 1.53823 -2.90909H2.22366L1.53823 -0.70137C1.48244 -0.518057 1.37883 -0.191283 1.37883 -0.151432C1.37883 0.0318804 1.5462 0.0956413 1.64184 0.0956413C1.93674 0.0956413 1.98456 -0.183313 2.00847 -0.302864L2.50262 -2.90909Z' id='eq1-g0-28'/>
+<path d='M2.24757 -1.6259C2.37509 -1.74545 2.70984 -2.00847 2.83736 -2.12005C3.33151 -2.57435 3.80174 -3.0127 3.80174 -3.73798C3.80174 -4.68643 3.00473 -5.30012 2.00847 -5.30012C1.05205 -5.30012 0.422416 -4.57484 0.422416 -3.8655C0.422416 -3.47497 0.73325 -3.41918 0.844832 -3.41918C1.0122 -3.41918 1.25928 -3.53873 1.25928 -3.84159C1.25928 -4.25604 0.860772 -4.25604 0.765131 -4.25604C0.996264 -4.83786 1.53026 -5.03711 1.9208 -5.03711C2.66202 -5.03711 3.04458 -4.40747 3.04458 -3.73798C3.04458 -2.90909 2.46276 -2.30336 1.52229 -1.33898L0.518057 -0.302864C0.422416 -0.215193 0.422416 -0.199253 0.422416 0H3.57061L3.80174 -1.42665H3.55467C3.53076 -1.26725 3.467 -0.868742 3.37136 -0.71731C3.32354 -0.653549 2.71781 -0.653549 2.59029 -0.653549H1.17161L2.24757 -1.6259Z' id='eq1-g1-50'/>
+</defs>
+<g id='eq1-page1'>
+<use x='1.19551' xlink:href='#eq1-g0-28' y='-4.70713'/>
+<rect height='0.478187' width='4.67835' x='1.19551' y='-3.22789'/>
+<use x='1.4176' xlink:href='#eq1-g1-50' y='4.12263'/>
+</g>
+</svg> here. Then a block formula which is a Pythagorean theorem in source but not in the output:</p><div class="m-math"><svg height='15.4964pt' version='1.1' viewBox='164.011 -12.3971 60.0231 12.3971' width='75.0289pt'>
+<title>LaTeX Math</title>
+<desc>
+\[ a^2 + b^2 = c^2 \]
+</desc>
+<defs>
+<path d='M7.53176 -8.09365C7.6274 -8.26102 7.6274 -8.28493 7.6274 -8.3208C7.6274 -8.40448 7.55567 -8.5599 7.38829 -8.5599C7.24483 -8.5599 7.20897 -8.48817 7.12528 -8.3208L1.75741 2.11606C1.66177 2.28344 1.66177 2.30735 1.66177 2.34321C1.66177 2.43885 1.74545 2.58232 1.90087 2.58232C2.04433 2.58232 2.0802 2.51059 2.16389 2.34321L7.53176 -8.09365Z' id='eq2-g0-54'/>
+<path d='M3.59851 -1.42267C3.53873 -1.21943 3.53873 -1.19552 3.37136 -0.968369C3.10834 -0.633624 2.58232 -0.119552 2.02042 -0.119552C1.53026 -0.119552 1.25529 -0.561893 1.25529 -1.26725C1.25529 -1.92478 1.6259 -3.26376 1.85305 -3.76588C2.25953 -4.60274 2.82142 -5.03313 3.28767 -5.03313C4.07671 -5.03313 4.23213 -4.0528 4.23213 -3.95716C4.23213 -3.94521 4.19626 -3.78979 4.18431 -3.76588L3.59851 -1.42267ZM4.36364 -4.48319C4.23213 -4.79402 3.90934 -5.27223 3.28767 -5.27223C1.93674 -5.27223 0.478207 -3.52677 0.478207 -1.75741C0.478207 -0.573848 1.17161 0.119552 1.98456 0.119552C2.64209 0.119552 3.20399 -0.394521 3.53873 -0.789041C3.65828 -0.0836862 4.22017 0.119552 4.57883 0.119552S5.22441 -0.0956413 5.4396 -0.526027C5.63088 -0.932503 5.79826 -1.66177 5.79826 -1.70959C5.79826 -1.76936 5.75044 -1.81719 5.6787 -1.81719C5.57111 -1.81719 5.55915 -1.75741 5.51133 -1.57808C5.332 -0.872727 5.10486 -0.119552 4.61469 -0.119552C4.268 -0.119552 4.24408 -0.430386 4.24408 -0.669489C4.24408 -0.944458 4.27995 -1.07597 4.38755 -1.54222C4.47123 -1.8411 4.53101 -2.10411 4.62665 -2.45081C5.06899 -4.24408 5.17659 -4.67447 5.17659 -4.7462C5.17659 -4.91357 5.04508 -5.04508 4.86575 -5.04508C4.48319 -5.04508 4.38755 -4.62665 4.36364 -4.48319Z' id='eq2-g1-97'/>
+<path d='M2.76164 -7.99801C2.7736 -8.04583 2.79751 -8.11756 2.79751 -8.17733C2.79751 -8.29689 2.67796 -8.29689 2.65405 -8.29689C2.64209 -8.29689 2.21171 -8.26102 1.99651 -8.23711C1.79328 -8.22516 1.61395 -8.20125 1.39875 -8.18929C1.11183 -8.16538 1.02814 -8.15342 1.02814 -7.93823C1.02814 -7.81868 1.1477 -7.81868 1.26725 -7.81868C1.87696 -7.81868 1.87696 -7.71108 1.87696 -7.59153C1.87696 -7.50785 1.78132 -7.16115 1.7335 -6.94595L1.44658 -5.79826C1.32702 -5.32005 0.645579 -2.60623 0.597758 -2.39103C0.537983 -2.09215 0.537983 -1.88892 0.537983 -1.7335C0.537983 -0.514072 1.21943 0.119552 1.99651 0.119552C3.38331 0.119552 4.81793 -1.66177 4.81793 -3.39527C4.81793 -4.49514 4.19626 -5.27223 3.29963 -5.27223C2.67796 -5.27223 2.11606 -4.75816 1.88892 -4.51905L2.76164 -7.99801ZM2.00847 -0.119552C1.6259 -0.119552 1.20747 -0.406476 1.20747 -1.33898C1.20747 -1.7335 1.24334 -1.96065 1.45853 -2.79751C1.4944 -2.95293 1.68568 -3.71806 1.7335 -3.87347C1.75741 -3.96912 2.46276 -5.03313 3.27572 -5.03313C3.80174 -5.03313 4.04085 -4.5071 4.04085 -3.88543C4.04085 -3.31158 3.7061 -1.96065 3.40722 -1.33898C3.10834 -0.6934 2.55841 -0.119552 2.00847 -0.119552Z' id='eq2-g1-98'/>
+<path d='M4.67447 -4.49514C4.44732 -4.49514 4.33973 -4.49514 4.17235 -4.35168C4.10062 -4.29191 3.96912 -4.11258 3.96912 -3.9213C3.96912 -3.68219 4.14844 -3.53873 4.37559 -3.53873C4.66252 -3.53873 4.98531 -3.77783 4.98531 -4.25604C4.98531 -4.82989 4.43537 -5.27223 3.61046 -5.27223C2.04433 -5.27223 0.478207 -3.56264 0.478207 -1.86501C0.478207 -0.824907 1.12379 0.119552 2.34321 0.119552C3.96912 0.119552 4.99726 -1.1477 4.99726 -1.30311C4.99726 -1.37484 4.92553 -1.43462 4.87771 -1.43462C4.84184 -1.43462 4.82989 -1.42267 4.72229 -1.31507C3.95716 -0.298879 2.82142 -0.119552 2.36712 -0.119552C1.54222 -0.119552 1.2792 -0.836862 1.2792 -1.43462C1.2792 -1.85305 1.48244 -3.0127 1.91283 -3.82565C2.22366 -4.38755 2.86924 -5.03313 3.62242 -5.03313C3.77783 -5.03313 4.43537 -5.00922 4.67447 -4.49514Z' id='eq2-g1-99'/>
+<path d='M2.01644 -2.66202C2.64608 -2.66202 3.04458 -2.19975 3.04458 -1.36289C3.04458 -0.366625 2.4787 -0.071731 2.05629 -0.071731C1.61793 -0.071731 1.02017 -0.231133 0.74122 -0.653549C1.02814 -0.653549 1.2274 -0.836862 1.2274 -1.09988C1.2274 -1.35492 1.04408 -1.53823 0.789041 -1.53823C0.573848 -1.53823 0.350685 -1.40274 0.350685 -1.08394C0.350685 -0.326775 1.16364 0.167372 2.07223 0.167372C3.13225 0.167372 3.87347 -0.565878 3.87347 -1.36289C3.87347 -2.02441 3.34745 -2.63014 2.5345 -2.80548C3.16413 -3.02864 3.63437 -3.57061 3.63437 -4.20822S2.91706 -5.30012 2.08817 -5.30012C1.23537 -5.30012 0.589788 -4.83786 0.589788 -4.23213C0.589788 -3.93724 0.789041 -3.80971 0.996264 -3.80971C1.24334 -3.80971 1.40274 -3.98506 1.40274 -4.21619C1.40274 -4.51108 1.1477 -4.62267 0.972354 -4.63064C1.3071 -5.06899 1.9208 -5.0929 2.06426 -5.0929C2.27148 -5.0929 2.87721 -5.02914 2.87721 -4.20822C2.87721 -3.65031 2.64608 -3.31557 2.5345 -3.18804C2.29539 -2.94097 2.11208 -2.92503 1.6259 -2.89315C1.47447 -2.88518 1.41071 -2.87721 1.41071 -2.7736C1.41071 -2.66202 1.48244 -2.66202 1.61793 -2.66202H2.01644Z' id='eq2-g2-51'/>
+<path d='M4.77011 -2.76164H8.06974C8.23711 -2.76164 8.4523 -2.76164 8.4523 -2.97684C8.4523 -3.20399 8.24907 -3.20399 8.06974 -3.20399H4.77011V-6.50361C4.77011 -6.67098 4.77011 -6.88618 4.55492 -6.88618C4.32777 -6.88618 4.32777 -6.68294 4.32777 -6.50361V-3.20399H1.02814C0.860772 -3.20399 0.645579 -3.20399 0.645579 -2.98879C0.645579 -2.76164 0.848817 -2.76164 1.02814 -2.76164H4.32777V0.537983C4.32777 0.705355 4.32777 0.920548 4.54296 0.920548C4.77011 0.920548 4.77011 0.71731 4.77011 0.537983V-2.76164Z' id='eq2-g3-43'/>
+<path d='M8.06974 -3.87347C8.23711 -3.87347 8.4523 -3.87347 8.4523 -4.08867C8.4523 -4.31582 8.24907 -4.31582 8.06974 -4.31582H1.02814C0.860772 -4.31582 0.645579 -4.31582 0.645579 -4.10062C0.645579 -3.87347 0.848817 -3.87347 1.02814 -3.87347H8.06974ZM8.06974 -1.64981C8.23711 -1.64981 8.4523 -1.64981 8.4523 -1.86501C8.4523 -2.09215 8.24907 -2.09215 8.06974 -2.09215H1.02814C0.860772 -2.09215 0.645579 -2.09215 0.645579 -1.87696C0.645579 -1.64981 0.848817 -1.64981 1.02814 -1.64981H8.06974Z' id='eq2-g3-61'/>
+</defs>
+<g id='eq2-page1'>
+<use x='164.011' xlink:href='#eq2-g1-97' y='-2.3246'/>
+<use x='170.156' xlink:href='#eq2-g2-51' y='-7.26078'/>
+<use x='177.545' xlink:href='#eq2-g3-43' y='-2.3246'/>
+<use x='189.306' xlink:href='#eq2-g1-98' y='-2.3246'/>
+<use x='194.283' xlink:href='#eq2-g2-51' y='-7.26078'/>
+<use x='202.336' xlink:href='#eq2-g0-54' y='-2.3246'/>
+<use x='202.336' xlink:href='#eq2-g3-61' y='-2.3246'/>
+<use x='214.762' xlink:href='#eq2-g1-99' y='-2.3246'/>
+<use x='219.8' xlink:href='#eq2-g2-51' y='-7.26078'/>
+</g>
+</svg></div>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
index be1090e95ea4211f8f2cf4ed662f4718c697d933..c916e8d1772decde8496ad2728816400f4c52c35 100644 (file)
 #
 
 import os
+import pickle
 import shutil
 import subprocess
 import unittest
 
+from hashlib import sha1
+
 from distutils.version import LooseVersion
 
 from . import BaseTestCase, IntegrationTestCase, doxygen_version
@@ -118,6 +121,111 @@ class Math(IntegrationTestCase):
         with self.assertRaises(subprocess.CalledProcessError) as context:
             self.run_dox2html5(wildcard='error.xml')
 
+class MathCached(IntegrationTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, 'math_cached', *args, **kwargs)
+
+       # Actually generated from $ \frac{\tau}{2} $ tho
+        self.tau_half_hash = sha1("""$ \pi $""".encode('utf-8')).digest()
+        self.tau_half = """<?xml version='1.0' encoding='UTF-8'?>
+<!-- This file was generated by dvisvgm 2.1.3 -->
+<svg height='15.3267pt' version='1.1' viewBox='1.19551 -8.1387 4.67835 12.2613' width='5.84794pt' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
+<defs>
+<path d='M2.50262 -2.90909H3.92927C4.05679 -2.90909 4.14446 -2.90909 4.22416 -2.97285C4.3198 -3.06052 4.34371 -3.16413 4.34371 -3.21196C4.34371 -3.43512 4.14446 -3.43512 4.00897 -3.43512H1.60199C1.43462 -3.43512 1.13176 -3.43512 0.74122 -3.05255C0.454296 -2.76563 0.231133 -2.399 0.231133 -2.34321C0.231133 -2.27148 0.286924 -2.24757 0.350685 -2.24757C0.430386 -2.24757 0.446326 -2.27148 0.494147 -2.33524C0.884682 -2.90909 1.35492 -2.90909 1.53823 -2.90909H2.22366L1.53823 -0.70137C1.48244 -0.518057 1.37883 -0.191283 1.37883 -0.151432C1.37883 0.0318804 1.5462 0.0956413 1.64184 0.0956413C1.93674 0.0956413 1.98456 -0.183313 2.00847 -0.302864L2.50262 -2.90909Z' id='g0-28'/>
+<path d='M2.24757 -1.6259C2.37509 -1.74545 2.70984 -2.00847 2.83736 -2.12005C3.33151 -2.57435 3.80174 -3.0127 3.80174 -3.73798C3.80174 -4.68643 3.00473 -5.30012 2.00847 -5.30012C1.05205 -5.30012 0.422416 -4.57484 0.422416 -3.8655C0.422416 -3.47497 0.73325 -3.41918 0.844832 -3.41918C1.0122 -3.41918 1.25928 -3.53873 1.25928 -3.84159C1.25928 -4.25604 0.860772 -4.25604 0.765131 -4.25604C0.996264 -4.83786 1.53026 -5.03711 1.9208 -5.03711C2.66202 -5.03711 3.04458 -4.40747 3.04458 -3.73798C3.04458 -2.90909 2.46276 -2.30336 1.52229 -1.33898L0.518057 -0.302864C0.422416 -0.215193 0.422416 -0.199253 0.422416 0H3.57061L3.80174 -1.42665H3.55467C3.53076 -1.26725 3.467 -0.868742 3.37136 -0.71731C3.32354 -0.653549 2.71781 -0.653549 2.59029 -0.653549H1.17161L2.24757 -1.6259Z' id='g1-50'/>
+</defs>
+<g id='page1'>
+<use x='1.19551' xlink:href='#g0-28' y='-4.70713'/>
+<rect height='0.478187' width='4.67835' x='1.19551' y='-3.22789'/>
+<use x='1.4176' xlink:href='#g1-50' y='4.12263'/>
+</g>
+</svg>"""
+        # Actually generated from \[ a^3 + b^3 \neq c^3 \] tho
+        self.fermat_hash = sha1("""\[ a^2 + b^2 = c^2 \]""".encode('utf-8')).digest()
+        self.fermat = """<?xml version='1.0' encoding='UTF-8'?>
+<!-- This file was generated by dvisvgm 2.1.3 -->
+<svg height='15.4964pt' version='1.1' viewBox='164.011 -12.3971 60.0231 12.3971' width='75.0289pt' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
+<defs>
+<path d='M7.53176 -8.09365C7.6274 -8.26102 7.6274 -8.28493 7.6274 -8.3208C7.6274 -8.40448 7.55567 -8.5599 7.38829 -8.5599C7.24483 -8.5599 7.20897 -8.48817 7.12528 -8.3208L1.75741 2.11606C1.66177 2.28344 1.66177 2.30735 1.66177 2.34321C1.66177 2.43885 1.74545 2.58232 1.90087 2.58232C2.04433 2.58232 2.0802 2.51059 2.16389 2.34321L7.53176 -8.09365Z' id='g0-54'/>
+<path d='M3.59851 -1.42267C3.53873 -1.21943 3.53873 -1.19552 3.37136 -0.968369C3.10834 -0.633624 2.58232 -0.119552 2.02042 -0.119552C1.53026 -0.119552 1.25529 -0.561893 1.25529 -1.26725C1.25529 -1.92478 1.6259 -3.26376 1.85305 -3.76588C2.25953 -4.60274 2.82142 -5.03313 3.28767 -5.03313C4.07671 -5.03313 4.23213 -4.0528 4.23213 -3.95716C4.23213 -3.94521 4.19626 -3.78979 4.18431 -3.76588L3.59851 -1.42267ZM4.36364 -4.48319C4.23213 -4.79402 3.90934 -5.27223 3.28767 -5.27223C1.93674 -5.27223 0.478207 -3.52677 0.478207 -1.75741C0.478207 -0.573848 1.17161 0.119552 1.98456 0.119552C2.64209 0.119552 3.20399 -0.394521 3.53873 -0.789041C3.65828 -0.0836862 4.22017 0.119552 4.57883 0.119552S5.22441 -0.0956413 5.4396 -0.526027C5.63088 -0.932503 5.79826 -1.66177 5.79826 -1.70959C5.79826 -1.76936 5.75044 -1.81719 5.6787 -1.81719C5.57111 -1.81719 5.55915 -1.75741 5.51133 -1.57808C5.332 -0.872727 5.10486 -0.119552 4.61469 -0.119552C4.268 -0.119552 4.24408 -0.430386 4.24408 -0.669489C4.24408 -0.944458 4.27995 -1.07597 4.38755 -1.54222C4.47123 -1.8411 4.53101 -2.10411 4.62665 -2.45081C5.06899 -4.24408 5.17659 -4.67447 5.17659 -4.7462C5.17659 -4.91357 5.04508 -5.04508 4.86575 -5.04508C4.48319 -5.04508 4.38755 -4.62665 4.36364 -4.48319Z' id='g1-97'/>
+<path d='M2.76164 -7.99801C2.7736 -8.04583 2.79751 -8.11756 2.79751 -8.17733C2.79751 -8.29689 2.67796 -8.29689 2.65405 -8.29689C2.64209 -8.29689 2.21171 -8.26102 1.99651 -8.23711C1.79328 -8.22516 1.61395 -8.20125 1.39875 -8.18929C1.11183 -8.16538 1.02814 -8.15342 1.02814 -7.93823C1.02814 -7.81868 1.1477 -7.81868 1.26725 -7.81868C1.87696 -7.81868 1.87696 -7.71108 1.87696 -7.59153C1.87696 -7.50785 1.78132 -7.16115 1.7335 -6.94595L1.44658 -5.79826C1.32702 -5.32005 0.645579 -2.60623 0.597758 -2.39103C0.537983 -2.09215 0.537983 -1.88892 0.537983 -1.7335C0.537983 -0.514072 1.21943 0.119552 1.99651 0.119552C3.38331 0.119552 4.81793 -1.66177 4.81793 -3.39527C4.81793 -4.49514 4.19626 -5.27223 3.29963 -5.27223C2.67796 -5.27223 2.11606 -4.75816 1.88892 -4.51905L2.76164 -7.99801ZM2.00847 -0.119552C1.6259 -0.119552 1.20747 -0.406476 1.20747 -1.33898C1.20747 -1.7335 1.24334 -1.96065 1.45853 -2.79751C1.4944 -2.95293 1.68568 -3.71806 1.7335 -3.87347C1.75741 -3.96912 2.46276 -5.03313 3.27572 -5.03313C3.80174 -5.03313 4.04085 -4.5071 4.04085 -3.88543C4.04085 -3.31158 3.7061 -1.96065 3.40722 -1.33898C3.10834 -0.6934 2.55841 -0.119552 2.00847 -0.119552Z' id='g1-98'/>
+<path d='M4.67447 -4.49514C4.44732 -4.49514 4.33973 -4.49514 4.17235 -4.35168C4.10062 -4.29191 3.96912 -4.11258 3.96912 -3.9213C3.96912 -3.68219 4.14844 -3.53873 4.37559 -3.53873C4.66252 -3.53873 4.98531 -3.77783 4.98531 -4.25604C4.98531 -4.82989 4.43537 -5.27223 3.61046 -5.27223C2.04433 -5.27223 0.478207 -3.56264 0.478207 -1.86501C0.478207 -0.824907 1.12379 0.119552 2.34321 0.119552C3.96912 0.119552 4.99726 -1.1477 4.99726 -1.30311C4.99726 -1.37484 4.92553 -1.43462 4.87771 -1.43462C4.84184 -1.43462 4.82989 -1.42267 4.72229 -1.31507C3.95716 -0.298879 2.82142 -0.119552 2.36712 -0.119552C1.54222 -0.119552 1.2792 -0.836862 1.2792 -1.43462C1.2792 -1.85305 1.48244 -3.0127 1.91283 -3.82565C2.22366 -4.38755 2.86924 -5.03313 3.62242 -5.03313C3.77783 -5.03313 4.43537 -5.00922 4.67447 -4.49514Z' id='g1-99'/>
+<path d='M2.01644 -2.66202C2.64608 -2.66202 3.04458 -2.19975 3.04458 -1.36289C3.04458 -0.366625 2.4787 -0.071731 2.05629 -0.071731C1.61793 -0.071731 1.02017 -0.231133 0.74122 -0.653549C1.02814 -0.653549 1.2274 -0.836862 1.2274 -1.09988C1.2274 -1.35492 1.04408 -1.53823 0.789041 -1.53823C0.573848 -1.53823 0.350685 -1.40274 0.350685 -1.08394C0.350685 -0.326775 1.16364 0.167372 2.07223 0.167372C3.13225 0.167372 3.87347 -0.565878 3.87347 -1.36289C3.87347 -2.02441 3.34745 -2.63014 2.5345 -2.80548C3.16413 -3.02864 3.63437 -3.57061 3.63437 -4.20822S2.91706 -5.30012 2.08817 -5.30012C1.23537 -5.30012 0.589788 -4.83786 0.589788 -4.23213C0.589788 -3.93724 0.789041 -3.80971 0.996264 -3.80971C1.24334 -3.80971 1.40274 -3.98506 1.40274 -4.21619C1.40274 -4.51108 1.1477 -4.62267 0.972354 -4.63064C1.3071 -5.06899 1.9208 -5.0929 2.06426 -5.0929C2.27148 -5.0929 2.87721 -5.02914 2.87721 -4.20822C2.87721 -3.65031 2.64608 -3.31557 2.5345 -3.18804C2.29539 -2.94097 2.11208 -2.92503 1.6259 -2.89315C1.47447 -2.88518 1.41071 -2.87721 1.41071 -2.7736C1.41071 -2.66202 1.48244 -2.66202 1.61793 -2.66202H2.01644Z' id='g2-51'/>
+<path d='M4.77011 -2.76164H8.06974C8.23711 -2.76164 8.4523 -2.76164 8.4523 -2.97684C8.4523 -3.20399 8.24907 -3.20399 8.06974 -3.20399H4.77011V-6.50361C4.77011 -6.67098 4.77011 -6.88618 4.55492 -6.88618C4.32777 -6.88618 4.32777 -6.68294 4.32777 -6.50361V-3.20399H1.02814C0.860772 -3.20399 0.645579 -3.20399 0.645579 -2.98879C0.645579 -2.76164 0.848817 -2.76164 1.02814 -2.76164H4.32777V0.537983C4.32777 0.705355 4.32777 0.920548 4.54296 0.920548C4.77011 0.920548 4.77011 0.71731 4.77011 0.537983V-2.76164Z' id='g3-43'/>
+<path d='M8.06974 -3.87347C8.23711 -3.87347 8.4523 -3.87347 8.4523 -4.08867C8.4523 -4.31582 8.24907 -4.31582 8.06974 -4.31582H1.02814C0.860772 -4.31582 0.645579 -4.31582 0.645579 -4.10062C0.645579 -3.87347 0.848817 -3.87347 1.02814 -3.87347H8.06974ZM8.06974 -1.64981C8.23711 -1.64981 8.4523 -1.64981 8.4523 -1.86501C8.4523 -2.09215 8.24907 -2.09215 8.06974 -2.09215H1.02814C0.860772 -2.09215 0.645579 -2.09215 0.645579 -1.87696C0.645579 -1.64981 0.848817 -1.64981 1.02814 -1.64981H8.06974Z' id='g3-61'/>
+</defs>
+<g id='page1'>
+<use x='164.011' xlink:href='#g1-97' y='-2.3246'/>
+<use x='170.156' xlink:href='#g2-51' y='-7.26078'/>
+<use x='177.545' xlink:href='#g3-43' y='-2.3246'/>
+<use x='189.306' xlink:href='#g1-98' y='-2.3246'/>
+<use x='194.283' xlink:href='#g2-51' y='-7.26078'/>
+<use x='202.336' xlink:href='#g0-54' y='-2.3246'/>
+<use x='202.336' xlink:href='#g3-61' y='-2.3246'/>
+<use x='214.762' xlink:href='#g1-99' y='-2.3246'/>
+<use x='219.8' xlink:href='#g2-51' y='-7.26078'/>
+</g>
+</svg>"""
+
+    # This is using the cache, so doesn't matter if LaTeX is found or not
+    def test(self):
+        math_cache = (0, 5, {
+            self.tau_half_hash: (5, 0.3448408333333333, self.tau_half),
+            self.fermat_hash: (5, 0.0, self.fermat),
+            b'does not exist': (5, 0.0, 'something')})
+        with open(os.path.join(self.path, 'xml/math.cache'), 'wb') as f:
+            pickle.dump(math_cache, f)
+
+        self.run_dox2html5(wildcard='math.xml')
+        self.assertEqual(*self.actual_expected_contents('math.html'))
+
+        # Expect that after the operation the global cache age is bumped,
+        # unused entries removed and used entries age bumped as well
+        with open(os.path.join(self.path, 'xml/math.cache'), 'rb') as f:
+            math_cache_actual = pickle.load(f)
+        math_cache_expected = (0, 6, {
+            self.tau_half_hash: (6, 0.3448408333333333, self.tau_half),
+            self.fermat_hash: (6, 0.0, self.fermat)})
+        self.assertEqual(math_cache_actual, math_cache_expected)
+
+    @unittest.skipUnless(shutil.which('latex'),
+                         "Math rendering requires LaTeX installed")
+    def test_uncached(self):
+        # Write some bullshit there, which gets immediately reset
+        with open(os.path.join(self.path, 'xml/math.cache'), 'wb') as f:
+            pickle.dump((1337, 0, {"something different"}), f)
+
+        self.run_dox2html5(wildcard='math-uncached.xml')
+
+        with open(os.path.join(self.path, 'math.html')) as f:
+            expected_contents = f.read().strip()
+        # The file is the same expect for titles of the formulas. Replace them
+        # and then compare.
+        with open(os.path.join(self.path, 'html', 'math-uncached.html')) as f:
+            actual_contents = f.read().strip().replace('a^3 + b^3 \\neq c^3', 'a^2 + b^2 = c^2').replace('\\frac{\\tau}{2}', '\pi')
+
+        self.assertEqual(actual_contents, expected_contents)
+
+        # Expect that after the operation the global cache is filled
+        with open(os.path.join(self.path, 'xml/math.cache'), 'rb') as f:
+            math_cache_actual = pickle.load(f)
+        math_cache_expected = (0, 0, {
+            sha1("$ \\frac{\\tau}{2} $".encode('utf-8')).digest():
+                (0, 0.3448408333333333, self.tau_half),
+            sha1("\\[ a^3 + b^3 \\neq c^3 \\]".encode('utf-8')).digest():
+                (0, 0.0, self.fermat)})
+        self.assertEqual(math_cache_actual, math_cache_expected)
+
+    def test_noop(self):
+        if os.path.exists(os.path.join(self.path, 'xml/math.cache')):
+            shutil.rmtree(os.path.join(self.path, 'xml/math.cache'))
+
+        # Processing without any math
+        self.run_dox2html5(wildcard='indexpage.xml')
+
+        # There should be no file generated
+        self.assertFalse(os.path.exists(os.path.join(self.path, 'xml/math.cache')))
+
 class Tagfile(IntegrationTestCase):
     def __init__(self, *args, **kwargs):
         super().__init__(__file__, 'tagfile', *args, **kwargs)
index 3a7b4621d7783f5dccafc66038483868e70eb38b..e058b0835c7cc616e901671c835df2b97fd55ed4 100644 (file)
@@ -46,6 +46,7 @@ class Doxyfile(unittest.TestCase):
             'M_FILE_TREE_EXPAND_LEVELS': 1,
             'M_LINKS_NAVBAR1': ['pages', 'namespaces'],
             'M_LINKS_NAVBAR2': ['annotated', 'files'],
+            'M_MATH_CACHE_FILE': 'm.math.cache',
             'M_PAGE_FINE_PRINT': 'this is "quotes"',
             'M_PAGE_HEADER': 'this is "quotes" \'apostrophes\'',
             'M_SEARCH_DISABLED': False,
index 0386d5fe7ab34012cfcb7ece7f1349e53db948e2..f28b5c0b9b19f581887daa923ce1ef779ca5cdbf 100644 (file)
@@ -22,7 +22,9 @@
 #   DEALINGS IN THE SOFTWARE.
 #
 
+import pickle
 import re
+from hashlib import sha1
 
 import latex2svg
 
@@ -63,11 +65,63 @@ _unique_dst = r"""\g<name>='\g<ref>eq{counter}-\g<id>'"""
 # Reset back to zero on start of a new page for reproducible behavior.
 counter = 0
 
+# Cache for rendered formulas (source formula sha1 -> (depth, svg data)). The
+# counter is not included
+_cache_version = 0
+_cache = None
+
+# Fetch cached formula or render it and add to the cache. The formula has to
+# be already wrapped in $, $$ etc. environment.
+def fetch_cached_or_render(formula):
+    global _cache
+
+    # unpickle_cache() should be called first
+    assert _cache
+
+    hash = sha1(formula.encode('utf-8')).digest()
+    if not _cache or not hash in _cache[2]:
+        out = latex2svg.latex2svg(formula, params=params)
+        _cache[2][hash] = (_cache[1], out['depth'], out['svg'])
+    else:
+        _cache[2][hash] = (_cache[1], _cache[2][hash][1], _cache[2][hash][2])
+    return (_cache[2][hash][1], _cache[2][hash][2])
+
+def unpickle_cache(file):
+    global _cache
+
+    if file:
+        with open(file, 'rb') as f:
+            _cache = pickle.load(f)
+    else:
+        _cache = None
+
+    # Reset the cache if not valid or not expected version
+    if not _cache or _cache[0] != _cache_version:
+        _cache = (_cache_version, 0, {})
+
+    # Otherwise bump cache age
+    else: _cache = (_cache[0], _cache[1] + 1, _cache[2])
+
+def pickle_cache(file):
+    global _cache
+
+    # Don't save any file if there is nothing
+    if not _cache or not _cache[2]: return
+
+    # Prune entries that were not used
+    cache_to_save = (_cache_version, _cache[1], {})
+    for hash, entry in _cache[2].items():
+        if entry[0] != _cache[1]: continue
+        cache_to_save[2][hash] = entry
+
+    with open(file, 'wb') as f:
+        pickle.dump(cache_to_save, f)
+
 # Patches the output from dvisvgm
 # - w/o the XML preamble and needless xmlns attributes
 # - unique element IDs (see `counter`)
 # - adds additional `attribs` to the <svg> element
-def patch(formula, out, attribs):
+def patch(formula, svg, attribs):
     global counter
     counter += 1
-    return _unique_src.sub(_unique_dst.format(counter=counter), _patch_src.sub(_patch_dst.format(attribs=attribs, formula=formula.replace('\\', '\\\\')), out['svg']))
+    return _unique_src.sub(_unique_dst.format(counter=counter), _patch_src.sub(_patch_dst.format(attribs=attribs, formula=formula.replace('\\', '\\\\')), svg))
index bee8a64b79a99590d5303307144172cfe824fbf7..7aa69f6c01d8fce5abbe49d8082061f5bd928709 100644 (file)
@@ -1 +1,2 @@
 __pycache__
+test/*/math.cache
index 067b57d8536920f3369f2833f1c17189930548a3..a0108c4dff9e88dd97b10f902b185663f13b2be6 100644 (file)
@@ -22,6 +22,7 @@
 #   DEALINGS IN THE SOFTWARE.
 #
 
+import os
 import re
 
 from docutils import nodes, utils
@@ -59,11 +60,11 @@ class Math(rst.Directive):
             if not block:
                 continue
 
-            out = latex2svg.latex2svg("$$" + block + "$$", params=latex2svgextra.params)
+            _, svg = latex2svgextra.fetch_cached_or_render("$$" + block + "$$")
 
             container = nodes.container(**self.options)
             container['classes'] += ['m-math']
-            node = nodes.raw(self.block_text, latex2svgextra.patch(block, out, ''), format='html')
+            node = nodes.raw(self.block_text, latex2svgextra.patch(block, svg, ''), format='html')
             node.line = self.content_offset + 1
             self.add_name(node)
             container.append(node)
@@ -98,22 +99,32 @@ def math(role, rawtext, text, lineno, inliner, options={}, content=[]):
         classes += ' ' + ' '.join(options['classes'])
         del options['classes']
 
-    out = latex2svg.latex2svg("$" + text + "$", params=latex2svgextra.params)
+    depth, svg = latex2svgextra.fetch_cached_or_render("$" + text + "$")
 
     # CSS classes and styling for proper vertical alignment. Depth is relative
     # to font size, describes how below the line the text is. Scaling it back
     # to 12pt font, scaled by 125% as set above in the config.
-    attribs = ' class="{}" style="vertical-align: -{:.1f}pt;"'.format(classes, out['depth']*12*1.25)
+    attribs = ' class="{}" style="vertical-align: -{:.1f}pt;"'.format(classes, depth*12*1.25)
 
-    node = nodes.raw(rawtext, latex2svgextra.patch(text, out, attribs), format='html', **options)
+    node = nodes.raw(rawtext, latex2svgextra.patch(text, svg, attribs), format='html', **options)
     return [node], []
 
 def configure_pelican(pelicanobj):
     global render_as_code
     render_as_code = pelicanobj.settings.get('M_MATH_RENDER_AS_CODE', False)
+    cache_file = pelicanobj.settings.get('M_MATH_CACHE_FILE', 'm.math.cache')
+    if cache_file and os.path.exists(cache_file):
+        latex2svgextra.unpickle_cache(cache_file)
+    else:
+        latex2svgextra.unpickle_cache(None)
+
+def save_cache(pelicanobj):
+    cache_file = pelicanobj.settings.get('M_MATH_CACHE_FILE', 'm.math.cache')
+    if cache_file: latex2svgextra.pickle_cache(cache_file)
 
 def register():
     pelican.signals.initialized.connect(configure_pelican)
+    pelican.signals.finalized.connect(save_cache)
     pelican.signals.content_object_init.connect(new_page)
     rst.directives.register_directive('math', Math)
     rst.roles.register_canonical_role('math', math)
diff --git a/pelican-plugins/m/test/math_cached/page.html b/pelican-plugins/m/test/math_cached/page.html
new file mode 100644 (file)
index 0000000..542d38b
--- /dev/null
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>Math | A Pelican Blog</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i" />
+  <link rel="stylesheet" href="static/m-dark.css" />
+  <link rel="canonical" href="page.html" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="./" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">A Pelican Blog</a>
+    </div>
+  </div>
+</nav></header>
+<main>
+<article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>Math</h1>
+<!-- content -->
+<p>In order to actually test use of the cache, I need to cheat a bit. Inline
+formula which is pi in the source but <svg class="m-math" style="vertical-align: -5.2pt;" height='15.3267pt' version='1.1' viewBox='1.19551 -8.1387 4.67835 12.2613' width='5.84794pt'>
+<title>LaTeX Math</title>
+<desc>
+\pi
+</desc>
+<defs>
+<path d='M2.50262 -2.90909H3.92927C4.05679 -2.90909 4.14446 -2.90909 4.22416 -2.97285C4.3198 -3.06052 4.34371 -3.16413 4.34371 -3.21196C4.34371 -3.43512 4.14446 -3.43512 4.00897 -3.43512H1.60199C1.43462 -3.43512 1.13176 -3.43512 0.74122 -3.05255C0.454296 -2.76563 0.231133 -2.399 0.231133 -2.34321C0.231133 -2.27148 0.286924 -2.24757 0.350685 -2.24757C0.430386 -2.24757 0.446326 -2.27148 0.494147 -2.33524C0.884682 -2.90909 1.35492 -2.90909 1.53823 -2.90909H2.22366L1.53823 -0.70137C1.48244 -0.518057 1.37883 -0.191283 1.37883 -0.151432C1.37883 0.0318804 1.5462 0.0956413 1.64184 0.0956413C1.93674 0.0956413 1.98456 -0.183313 2.00847 -0.302864L2.50262 -2.90909Z' id='eq1-g0-28'/>
+<path d='M2.24757 -1.6259C2.37509 -1.74545 2.70984 -2.00847 2.83736 -2.12005C3.33151 -2.57435 3.80174 -3.0127 3.80174 -3.73798C3.80174 -4.68643 3.00473 -5.30012 2.00847 -5.30012C1.05205 -5.30012 0.422416 -4.57484 0.422416 -3.8655C0.422416 -3.47497 0.73325 -3.41918 0.844832 -3.41918C1.0122 -3.41918 1.25928 -3.53873 1.25928 -3.84159C1.25928 -4.25604 0.860772 -4.25604 0.765131 -4.25604C0.996264 -4.83786 1.53026 -5.03711 1.9208 -5.03711C2.66202 -5.03711 3.04458 -4.40747 3.04458 -3.73798C3.04458 -2.90909 2.46276 -2.30336 1.52229 -1.33898L0.518057 -0.302864C0.422416 -0.215193 0.422416 -0.199253 0.422416 0H3.57061L3.80174 -1.42665H3.55467C3.53076 -1.26725 3.467 -0.868742 3.37136 -0.71731C3.32354 -0.653549 2.71781 -0.653549 2.59029 -0.653549H1.17161L2.24757 -1.6259Z' id='eq1-g1-50'/>
+</defs>
+<g id='eq1-page1'>
+<use x='1.19551' xlink:href='#eq1-g0-28' y='-4.70713'/>
+<rect height='0.478187' width='4.67835' x='1.19551' y='-3.22789'/>
+<use x='1.4176' xlink:href='#eq1-g1-50' y='4.12263'/>
+</g>
+</svg> here. Then a block
+formula which is a Pythagorean theorem in source but not in the output:</p>
+<div class="m-math">
+<svg height='15.4964pt' version='1.1' viewBox='164.011 -12.3971 60.0231 12.3971' width='75.0289pt'>
+<title>LaTeX Math</title>
+<desc>
+a^2 + b^2 = c^2
+</desc>
+<defs>
+<path d='M7.53176 -8.09365C7.6274 -8.26102 7.6274 -8.28493 7.6274 -8.3208C7.6274 -8.40448 7.55567 -8.5599 7.38829 -8.5599C7.24483 -8.5599 7.20897 -8.48817 7.12528 -8.3208L1.75741 2.11606C1.66177 2.28344 1.66177 2.30735 1.66177 2.34321C1.66177 2.43885 1.74545 2.58232 1.90087 2.58232C2.04433 2.58232 2.0802 2.51059 2.16389 2.34321L7.53176 -8.09365Z' id='eq2-g0-54'/>
+<path d='M3.59851 -1.42267C3.53873 -1.21943 3.53873 -1.19552 3.37136 -0.968369C3.10834 -0.633624 2.58232 -0.119552 2.02042 -0.119552C1.53026 -0.119552 1.25529 -0.561893 1.25529 -1.26725C1.25529 -1.92478 1.6259 -3.26376 1.85305 -3.76588C2.25953 -4.60274 2.82142 -5.03313 3.28767 -5.03313C4.07671 -5.03313 4.23213 -4.0528 4.23213 -3.95716C4.23213 -3.94521 4.19626 -3.78979 4.18431 -3.76588L3.59851 -1.42267ZM4.36364 -4.48319C4.23213 -4.79402 3.90934 -5.27223 3.28767 -5.27223C1.93674 -5.27223 0.478207 -3.52677 0.478207 -1.75741C0.478207 -0.573848 1.17161 0.119552 1.98456 0.119552C2.64209 0.119552 3.20399 -0.394521 3.53873 -0.789041C3.65828 -0.0836862 4.22017 0.119552 4.57883 0.119552S5.22441 -0.0956413 5.4396 -0.526027C5.63088 -0.932503 5.79826 -1.66177 5.79826 -1.70959C5.79826 -1.76936 5.75044 -1.81719 5.6787 -1.81719C5.57111 -1.81719 5.55915 -1.75741 5.51133 -1.57808C5.332 -0.872727 5.10486 -0.119552 4.61469 -0.119552C4.268 -0.119552 4.24408 -0.430386 4.24408 -0.669489C4.24408 -0.944458 4.27995 -1.07597 4.38755 -1.54222C4.47123 -1.8411 4.53101 -2.10411 4.62665 -2.45081C5.06899 -4.24408 5.17659 -4.67447 5.17659 -4.7462C5.17659 -4.91357 5.04508 -5.04508 4.86575 -5.04508C4.48319 -5.04508 4.38755 -4.62665 4.36364 -4.48319Z' id='eq2-g1-97'/>
+<path d='M2.76164 -7.99801C2.7736 -8.04583 2.79751 -8.11756 2.79751 -8.17733C2.79751 -8.29689 2.67796 -8.29689 2.65405 -8.29689C2.64209 -8.29689 2.21171 -8.26102 1.99651 -8.23711C1.79328 -8.22516 1.61395 -8.20125 1.39875 -8.18929C1.11183 -8.16538 1.02814 -8.15342 1.02814 -7.93823C1.02814 -7.81868 1.1477 -7.81868 1.26725 -7.81868C1.87696 -7.81868 1.87696 -7.71108 1.87696 -7.59153C1.87696 -7.50785 1.78132 -7.16115 1.7335 -6.94595L1.44658 -5.79826C1.32702 -5.32005 0.645579 -2.60623 0.597758 -2.39103C0.537983 -2.09215 0.537983 -1.88892 0.537983 -1.7335C0.537983 -0.514072 1.21943 0.119552 1.99651 0.119552C3.38331 0.119552 4.81793 -1.66177 4.81793 -3.39527C4.81793 -4.49514 4.19626 -5.27223 3.29963 -5.27223C2.67796 -5.27223 2.11606 -4.75816 1.88892 -4.51905L2.76164 -7.99801ZM2.00847 -0.119552C1.6259 -0.119552 1.20747 -0.406476 1.20747 -1.33898C1.20747 -1.7335 1.24334 -1.96065 1.45853 -2.79751C1.4944 -2.95293 1.68568 -3.71806 1.7335 -3.87347C1.75741 -3.96912 2.46276 -5.03313 3.27572 -5.03313C3.80174 -5.03313 4.04085 -4.5071 4.04085 -3.88543C4.04085 -3.31158 3.7061 -1.96065 3.40722 -1.33898C3.10834 -0.6934 2.55841 -0.119552 2.00847 -0.119552Z' id='eq2-g1-98'/>
+<path d='M4.67447 -4.49514C4.44732 -4.49514 4.33973 -4.49514 4.17235 -4.35168C4.10062 -4.29191 3.96912 -4.11258 3.96912 -3.9213C3.96912 -3.68219 4.14844 -3.53873 4.37559 -3.53873C4.66252 -3.53873 4.98531 -3.77783 4.98531 -4.25604C4.98531 -4.82989 4.43537 -5.27223 3.61046 -5.27223C2.04433 -5.27223 0.478207 -3.56264 0.478207 -1.86501C0.478207 -0.824907 1.12379 0.119552 2.34321 0.119552C3.96912 0.119552 4.99726 -1.1477 4.99726 -1.30311C4.99726 -1.37484 4.92553 -1.43462 4.87771 -1.43462C4.84184 -1.43462 4.82989 -1.42267 4.72229 -1.31507C3.95716 -0.298879 2.82142 -0.119552 2.36712 -0.119552C1.54222 -0.119552 1.2792 -0.836862 1.2792 -1.43462C1.2792 -1.85305 1.48244 -3.0127 1.91283 -3.82565C2.22366 -4.38755 2.86924 -5.03313 3.62242 -5.03313C3.77783 -5.03313 4.43537 -5.00922 4.67447 -4.49514Z' id='eq2-g1-99'/>
+<path d='M2.01644 -2.66202C2.64608 -2.66202 3.04458 -2.19975 3.04458 -1.36289C3.04458 -0.366625 2.4787 -0.071731 2.05629 -0.071731C1.61793 -0.071731 1.02017 -0.231133 0.74122 -0.653549C1.02814 -0.653549 1.2274 -0.836862 1.2274 -1.09988C1.2274 -1.35492 1.04408 -1.53823 0.789041 -1.53823C0.573848 -1.53823 0.350685 -1.40274 0.350685 -1.08394C0.350685 -0.326775 1.16364 0.167372 2.07223 0.167372C3.13225 0.167372 3.87347 -0.565878 3.87347 -1.36289C3.87347 -2.02441 3.34745 -2.63014 2.5345 -2.80548C3.16413 -3.02864 3.63437 -3.57061 3.63437 -4.20822S2.91706 -5.30012 2.08817 -5.30012C1.23537 -5.30012 0.589788 -4.83786 0.589788 -4.23213C0.589788 -3.93724 0.789041 -3.80971 0.996264 -3.80971C1.24334 -3.80971 1.40274 -3.98506 1.40274 -4.21619C1.40274 -4.51108 1.1477 -4.62267 0.972354 -4.63064C1.3071 -5.06899 1.9208 -5.0929 2.06426 -5.0929C2.27148 -5.0929 2.87721 -5.02914 2.87721 -4.20822C2.87721 -3.65031 2.64608 -3.31557 2.5345 -3.18804C2.29539 -2.94097 2.11208 -2.92503 1.6259 -2.89315C1.47447 -2.88518 1.41071 -2.87721 1.41071 -2.7736C1.41071 -2.66202 1.48244 -2.66202 1.61793 -2.66202H2.01644Z' id='eq2-g2-51'/>
+<path d='M4.77011 -2.76164H8.06974C8.23711 -2.76164 8.4523 -2.76164 8.4523 -2.97684C8.4523 -3.20399 8.24907 -3.20399 8.06974 -3.20399H4.77011V-6.50361C4.77011 -6.67098 4.77011 -6.88618 4.55492 -6.88618C4.32777 -6.88618 4.32777 -6.68294 4.32777 -6.50361V-3.20399H1.02814C0.860772 -3.20399 0.645579 -3.20399 0.645579 -2.98879C0.645579 -2.76164 0.848817 -2.76164 1.02814 -2.76164H4.32777V0.537983C4.32777 0.705355 4.32777 0.920548 4.54296 0.920548C4.77011 0.920548 4.77011 0.71731 4.77011 0.537983V-2.76164Z' id='eq2-g3-43'/>
+<path d='M8.06974 -3.87347C8.23711 -3.87347 8.4523 -3.87347 8.4523 -4.08867C8.4523 -4.31582 8.24907 -4.31582 8.06974 -4.31582H1.02814C0.860772 -4.31582 0.645579 -4.31582 0.645579 -4.10062C0.645579 -3.87347 0.848817 -3.87347 1.02814 -3.87347H8.06974ZM8.06974 -1.64981C8.23711 -1.64981 8.4523 -1.64981 8.4523 -1.86501C8.4523 -2.09215 8.24907 -2.09215 8.06974 -2.09215H1.02814C0.860772 -2.09215 0.645579 -2.09215 0.645579 -1.87696C0.645579 -1.64981 0.848817 -1.64981 1.02814 -1.64981H8.06974Z' id='eq2-g3-61'/>
+</defs>
+<g id='eq2-page1'>
+<use x='164.011' xlink:href='#eq2-g1-97' y='-2.3246'/>
+<use x='170.156' xlink:href='#eq2-g2-51' y='-7.26078'/>
+<use x='177.545' xlink:href='#eq2-g3-43' y='-2.3246'/>
+<use x='189.306' xlink:href='#eq2-g1-98' y='-2.3246'/>
+<use x='194.283' xlink:href='#eq2-g2-51' y='-7.26078'/>
+<use x='202.336' xlink:href='#eq2-g0-54' y='-2.3246'/>
+<use x='202.336' xlink:href='#eq2-g3-61' y='-2.3246'/>
+<use x='214.762' xlink:href='#eq2-g1-99' y='-2.3246'/>
+<use x='219.8' xlink:href='#eq2-g2-51' y='-7.26078'/>
+</g>
+</svg></div>
+<!-- /content -->
+      </div>
+    </div>
+  </div>
+</article>
+</main>
+</body>
+</html>
diff --git a/pelican-plugins/m/test/math_cached/page.rst b/pelican-plugins/m/test/math_cached/page.rst
new file mode 100644 (file)
index 0000000..adeda7c
--- /dev/null
@@ -0,0 +1,10 @@
+Math
+####
+
+In order to actually test use of the cache, I need to cheat a bit. Inline
+formula which is pi in the source but :math:`\pi` here. Then a block
+formula which is a Pythagorean theorem in source but not in the output:
+
+.. math::
+
+    a^2 + b^2 = c^2
diff --git a/pelican-plugins/m/test/math_uncached/page.rst b/pelican-plugins/m/test/math_uncached/page.rst
new file mode 100644 (file)
index 0000000..6a7f42a
--- /dev/null
@@ -0,0 +1,10 @@
+Math
+####
+
+In order to actually test use of the cache, I need to cheat a bit. Inline
+formula which is pi in the source but :math:`\frac{\tau}{2}` here. Then a block
+formula which is a Pythagorean theorem in source but not in the output:
+
+.. math::
+
+    a^3 + b^3 \neq c^3
index 8a9336856694ab1010cf3f1fd8de207eb138972e..ba4130a9e894a25a5fd8a351647326a8fe4084fb 100644 (file)
 #   DEALINGS IN THE SOFTWARE.
 #
 
+import os
+import pickle
 import sys
 import shutil
 import unittest
 
+from hashlib import sha1
+
 from distutils.version import LooseVersion
 
-from m.test import PluginTestCase
+from . import PluginTestCase
 
 class Math(PluginTestCase):
     def __init__(self, *args, **kwargs):
@@ -38,11 +42,15 @@ class Math(PluginTestCase):
                          "The math plugin requires at least Python 3.5 and LaTeX installed")
     def test(self):
         self.run_pelican({
-            'PLUGINS': ['m.htmlsanity', 'm.math']
+            'PLUGINS': ['m.htmlsanity', 'm.math'],
+            'M_MATH_CACHE_FILE': None
         })
 
         self.assertEqual(*self.actual_expected_contents('page.html'))
 
+        # No file should be generated when math caching is disabled
+        self.assertFalse(os.path.exists(os.path.join(self.path, 'm.math.cache')))
+
     def test_code_fallback(self):
         self.run_pelican({
             'PLUGINS': ['m.htmlsanity', 'm.math'],
@@ -50,3 +58,117 @@ class Math(PluginTestCase):
         })
 
         self.assertEqual(*self.actual_expected_contents('page.html', 'page-code-fallback.html'))
+
+        # No file should be generated when there was nothing to cache
+        self.assertFalse(os.path.exists(os.path.join(self.path, 'm.math.cache')))
+
+# Actually generated from $\frac{\tau}{2}$ tho
+tau_half_hash = sha1("""$\pi$""".encode('utf-8')).digest()
+tau_half = """<?xml version='1.0' encoding='UTF-8'?>
+<!-- This file was generated by dvisvgm 2.1.3 -->
+<svg height='15.3267pt' version='1.1' viewBox='1.19551 -8.1387 4.67835 12.2613' width='5.84794pt' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
+<defs>
+<path d='M2.50262 -2.90909H3.92927C4.05679 -2.90909 4.14446 -2.90909 4.22416 -2.97285C4.3198 -3.06052 4.34371 -3.16413 4.34371 -3.21196C4.34371 -3.43512 4.14446 -3.43512 4.00897 -3.43512H1.60199C1.43462 -3.43512 1.13176 -3.43512 0.74122 -3.05255C0.454296 -2.76563 0.231133 -2.399 0.231133 -2.34321C0.231133 -2.27148 0.286924 -2.24757 0.350685 -2.24757C0.430386 -2.24757 0.446326 -2.27148 0.494147 -2.33524C0.884682 -2.90909 1.35492 -2.90909 1.53823 -2.90909H2.22366L1.53823 -0.70137C1.48244 -0.518057 1.37883 -0.191283 1.37883 -0.151432C1.37883 0.0318804 1.5462 0.0956413 1.64184 0.0956413C1.93674 0.0956413 1.98456 -0.183313 2.00847 -0.302864L2.50262 -2.90909Z' id='g0-28'/>
+<path d='M2.24757 -1.6259C2.37509 -1.74545 2.70984 -2.00847 2.83736 -2.12005C3.33151 -2.57435 3.80174 -3.0127 3.80174 -3.73798C3.80174 -4.68643 3.00473 -5.30012 2.00847 -5.30012C1.05205 -5.30012 0.422416 -4.57484 0.422416 -3.8655C0.422416 -3.47497 0.73325 -3.41918 0.844832 -3.41918C1.0122 -3.41918 1.25928 -3.53873 1.25928 -3.84159C1.25928 -4.25604 0.860772 -4.25604 0.765131 -4.25604C0.996264 -4.83786 1.53026 -5.03711 1.9208 -5.03711C2.66202 -5.03711 3.04458 -4.40747 3.04458 -3.73798C3.04458 -2.90909 2.46276 -2.30336 1.52229 -1.33898L0.518057 -0.302864C0.422416 -0.215193 0.422416 -0.199253 0.422416 0H3.57061L3.80174 -1.42665H3.55467C3.53076 -1.26725 3.467 -0.868742 3.37136 -0.71731C3.32354 -0.653549 2.71781 -0.653549 2.59029 -0.653549H1.17161L2.24757 -1.6259Z' id='g1-50'/>
+</defs>
+<g id='page1'>
+<use x='1.19551' xlink:href='#g0-28' y='-4.70713'/>
+<rect height='0.478187' width='4.67835' x='1.19551' y='-3.22789'/>
+<use x='1.4176' xlink:href='#g1-50' y='4.12263'/>
+</g>
+</svg>"""
+
+# Actually generated from $$a^3 + b^3 \neq c^3$$ tho
+fermat_hash = sha1("""$$a^2 + b^2 = c^2$$""".encode('utf-8')).digest()
+fermat = """<?xml version='1.0' encoding='UTF-8'?>
+<!-- This file was generated by dvisvgm 2.1.3 -->
+<svg height='15.4964pt' version='1.1' viewBox='164.011 -12.3971 60.0231 12.3971' width='75.0289pt' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
+<defs>
+<path d='M7.53176 -8.09365C7.6274 -8.26102 7.6274 -8.28493 7.6274 -8.3208C7.6274 -8.40448 7.55567 -8.5599 7.38829 -8.5599C7.24483 -8.5599 7.20897 -8.48817 7.12528 -8.3208L1.75741 2.11606C1.66177 2.28344 1.66177 2.30735 1.66177 2.34321C1.66177 2.43885 1.74545 2.58232 1.90087 2.58232C2.04433 2.58232 2.0802 2.51059 2.16389 2.34321L7.53176 -8.09365Z' id='g0-54'/>
+<path d='M3.59851 -1.42267C3.53873 -1.21943 3.53873 -1.19552 3.37136 -0.968369C3.10834 -0.633624 2.58232 -0.119552 2.02042 -0.119552C1.53026 -0.119552 1.25529 -0.561893 1.25529 -1.26725C1.25529 -1.92478 1.6259 -3.26376 1.85305 -3.76588C2.25953 -4.60274 2.82142 -5.03313 3.28767 -5.03313C4.07671 -5.03313 4.23213 -4.0528 4.23213 -3.95716C4.23213 -3.94521 4.19626 -3.78979 4.18431 -3.76588L3.59851 -1.42267ZM4.36364 -4.48319C4.23213 -4.79402 3.90934 -5.27223 3.28767 -5.27223C1.93674 -5.27223 0.478207 -3.52677 0.478207 -1.75741C0.478207 -0.573848 1.17161 0.119552 1.98456 0.119552C2.64209 0.119552 3.20399 -0.394521 3.53873 -0.789041C3.65828 -0.0836862 4.22017 0.119552 4.57883 0.119552S5.22441 -0.0956413 5.4396 -0.526027C5.63088 -0.932503 5.79826 -1.66177 5.79826 -1.70959C5.79826 -1.76936 5.75044 -1.81719 5.6787 -1.81719C5.57111 -1.81719 5.55915 -1.75741 5.51133 -1.57808C5.332 -0.872727 5.10486 -0.119552 4.61469 -0.119552C4.268 -0.119552 4.24408 -0.430386 4.24408 -0.669489C4.24408 -0.944458 4.27995 -1.07597 4.38755 -1.54222C4.47123 -1.8411 4.53101 -2.10411 4.62665 -2.45081C5.06899 -4.24408 5.17659 -4.67447 5.17659 -4.7462C5.17659 -4.91357 5.04508 -5.04508 4.86575 -5.04508C4.48319 -5.04508 4.38755 -4.62665 4.36364 -4.48319Z' id='g1-97'/>
+<path d='M2.76164 -7.99801C2.7736 -8.04583 2.79751 -8.11756 2.79751 -8.17733C2.79751 -8.29689 2.67796 -8.29689 2.65405 -8.29689C2.64209 -8.29689 2.21171 -8.26102 1.99651 -8.23711C1.79328 -8.22516 1.61395 -8.20125 1.39875 -8.18929C1.11183 -8.16538 1.02814 -8.15342 1.02814 -7.93823C1.02814 -7.81868 1.1477 -7.81868 1.26725 -7.81868C1.87696 -7.81868 1.87696 -7.71108 1.87696 -7.59153C1.87696 -7.50785 1.78132 -7.16115 1.7335 -6.94595L1.44658 -5.79826C1.32702 -5.32005 0.645579 -2.60623 0.597758 -2.39103C0.537983 -2.09215 0.537983 -1.88892 0.537983 -1.7335C0.537983 -0.514072 1.21943 0.119552 1.99651 0.119552C3.38331 0.119552 4.81793 -1.66177 4.81793 -3.39527C4.81793 -4.49514 4.19626 -5.27223 3.29963 -5.27223C2.67796 -5.27223 2.11606 -4.75816 1.88892 -4.51905L2.76164 -7.99801ZM2.00847 -0.119552C1.6259 -0.119552 1.20747 -0.406476 1.20747 -1.33898C1.20747 -1.7335 1.24334 -1.96065 1.45853 -2.79751C1.4944 -2.95293 1.68568 -3.71806 1.7335 -3.87347C1.75741 -3.96912 2.46276 -5.03313 3.27572 -5.03313C3.80174 -5.03313 4.04085 -4.5071 4.04085 -3.88543C4.04085 -3.31158 3.7061 -1.96065 3.40722 -1.33898C3.10834 -0.6934 2.55841 -0.119552 2.00847 -0.119552Z' id='g1-98'/>
+<path d='M4.67447 -4.49514C4.44732 -4.49514 4.33973 -4.49514 4.17235 -4.35168C4.10062 -4.29191 3.96912 -4.11258 3.96912 -3.9213C3.96912 -3.68219 4.14844 -3.53873 4.37559 -3.53873C4.66252 -3.53873 4.98531 -3.77783 4.98531 -4.25604C4.98531 -4.82989 4.43537 -5.27223 3.61046 -5.27223C2.04433 -5.27223 0.478207 -3.56264 0.478207 -1.86501C0.478207 -0.824907 1.12379 0.119552 2.34321 0.119552C3.96912 0.119552 4.99726 -1.1477 4.99726 -1.30311C4.99726 -1.37484 4.92553 -1.43462 4.87771 -1.43462C4.84184 -1.43462 4.82989 -1.42267 4.72229 -1.31507C3.95716 -0.298879 2.82142 -0.119552 2.36712 -0.119552C1.54222 -0.119552 1.2792 -0.836862 1.2792 -1.43462C1.2792 -1.85305 1.48244 -3.0127 1.91283 -3.82565C2.22366 -4.38755 2.86924 -5.03313 3.62242 -5.03313C3.77783 -5.03313 4.43537 -5.00922 4.67447 -4.49514Z' id='g1-99'/>
+<path d='M2.01644 -2.66202C2.64608 -2.66202 3.04458 -2.19975 3.04458 -1.36289C3.04458 -0.366625 2.4787 -0.071731 2.05629 -0.071731C1.61793 -0.071731 1.02017 -0.231133 0.74122 -0.653549C1.02814 -0.653549 1.2274 -0.836862 1.2274 -1.09988C1.2274 -1.35492 1.04408 -1.53823 0.789041 -1.53823C0.573848 -1.53823 0.350685 -1.40274 0.350685 -1.08394C0.350685 -0.326775 1.16364 0.167372 2.07223 0.167372C3.13225 0.167372 3.87347 -0.565878 3.87347 -1.36289C3.87347 -2.02441 3.34745 -2.63014 2.5345 -2.80548C3.16413 -3.02864 3.63437 -3.57061 3.63437 -4.20822S2.91706 -5.30012 2.08817 -5.30012C1.23537 -5.30012 0.589788 -4.83786 0.589788 -4.23213C0.589788 -3.93724 0.789041 -3.80971 0.996264 -3.80971C1.24334 -3.80971 1.40274 -3.98506 1.40274 -4.21619C1.40274 -4.51108 1.1477 -4.62267 0.972354 -4.63064C1.3071 -5.06899 1.9208 -5.0929 2.06426 -5.0929C2.27148 -5.0929 2.87721 -5.02914 2.87721 -4.20822C2.87721 -3.65031 2.64608 -3.31557 2.5345 -3.18804C2.29539 -2.94097 2.11208 -2.92503 1.6259 -2.89315C1.47447 -2.88518 1.41071 -2.87721 1.41071 -2.7736C1.41071 -2.66202 1.48244 -2.66202 1.61793 -2.66202H2.01644Z' id='g2-51'/>
+<path d='M4.77011 -2.76164H8.06974C8.23711 -2.76164 8.4523 -2.76164 8.4523 -2.97684C8.4523 -3.20399 8.24907 -3.20399 8.06974 -3.20399H4.77011V-6.50361C4.77011 -6.67098 4.77011 -6.88618 4.55492 -6.88618C4.32777 -6.88618 4.32777 -6.68294 4.32777 -6.50361V-3.20399H1.02814C0.860772 -3.20399 0.645579 -3.20399 0.645579 -2.98879C0.645579 -2.76164 0.848817 -2.76164 1.02814 -2.76164H4.32777V0.537983C4.32777 0.705355 4.32777 0.920548 4.54296 0.920548C4.77011 0.920548 4.77011 0.71731 4.77011 0.537983V-2.76164Z' id='g3-43'/>
+<path d='M8.06974 -3.87347C8.23711 -3.87347 8.4523 -3.87347 8.4523 -4.08867C8.4523 -4.31582 8.24907 -4.31582 8.06974 -4.31582H1.02814C0.860772 -4.31582 0.645579 -4.31582 0.645579 -4.10062C0.645579 -3.87347 0.848817 -3.87347 1.02814 -3.87347H8.06974ZM8.06974 -1.64981C8.23711 -1.64981 8.4523 -1.64981 8.4523 -1.86501C8.4523 -2.09215 8.24907 -2.09215 8.06974 -2.09215H1.02814C0.860772 -2.09215 0.645579 -2.09215 0.645579 -1.87696C0.645579 -1.64981 0.848817 -1.64981 1.02814 -1.64981H8.06974Z' id='g3-61'/>
+</defs>
+<g id='page1'>
+<use x='164.011' xlink:href='#g1-97' y='-2.3246'/>
+<use x='170.156' xlink:href='#g2-51' y='-7.26078'/>
+<use x='177.545' xlink:href='#g3-43' y='-2.3246'/>
+<use x='189.306' xlink:href='#g1-98' y='-2.3246'/>
+<use x='194.283' xlink:href='#g2-51' y='-7.26078'/>
+<use x='202.336' xlink:href='#g0-54' y='-2.3246'/>
+<use x='202.336' xlink:href='#g3-61' y='-2.3246'/>
+<use x='214.762' xlink:href='#g1-99' y='-2.3246'/>
+<use x='219.8' xlink:href='#g2-51' y='-7.26078'/>
+</g>
+</svg>"""
+
+class MathCached(PluginTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, 'cached', *args, **kwargs)
+
+    # This is using the cache, so doesn't matter if LaTeX is found or not
+    def test(self):
+        cache_file = os.path.join(self.path, 'math.cache')
+
+        math_cache = (0, 5, {
+            tau_half_hash: (5, 0.3448408333333333, tau_half),
+            fermat_hash: (5, 0.0, fermat),
+            b'does not exist': (5, 0.0, 'something')})
+        with open(cache_file, 'wb') as f:
+            pickle.dump(math_cache, f)
+
+        self.run_pelican({
+            'PLUGINS': ['m.htmlsanity', 'm.math'],
+            'M_MATH_CACHE_FILE': cache_file
+        })
+
+        self.assertEqual(*self.actual_expected_contents('page.html'))
+
+        # Expect that after the operation the global cache age is bumped,
+        # unused entries removed and used entries age bumped as well
+        with open(cache_file, 'rb') as f:
+            math_cache_actual = pickle.load(f)
+        math_cache_expected = (0, 6, {
+            tau_half_hash: (6, 0.3448408333333333, tau_half),
+            fermat_hash: (6, 0.0, fermat)})
+        self.assertEqual(math_cache_actual, math_cache_expected)
+
+class MathUncached(PluginTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, 'uncached', *args, **kwargs)
+
+    @unittest.skipUnless(shutil.which('latex'),
+                         "Math rendering requires LaTeX installed")
+    def test(self):
+        cache_file = os.path.join(self.path, 'math.cache')
+
+        # Write some bullshit there, which gets immediately reset
+        with open(cache_file, 'wb') as f:
+            pickle.dump((1337, 0, {"something different"}), f)
+
+        self.run_pelican({
+            'PLUGINS': ['m.htmlsanity', 'm.math'],
+            'M_MATH_CACHE_FILE': cache_file
+        })
+
+        with open(os.path.join(self.path, '../math_cached/page.html')) as f:
+            expected_contents = f.read().strip()
+        # The file is the same expect for titles of the formulas. Replace them
+        # and then compare.
+        with open(os.path.join(self.path, 'output', 'page.html')) as f:
+            actual_contents = f.read().strip().replace('a^3 + b^3 \\neq c^3', 'a^2 + b^2 = c^2').replace('\\frac{\\tau}{2}', '\pi')
+
+        self.assertEqual(actual_contents, expected_contents)
+
+        # Expect that after the operation the global cache is filled
+        with open(cache_file, 'rb') as f:
+            math_cache_actual = pickle.load(f)
+        math_cache_expected = (0, 0, {
+            sha1("$\\frac{\\tau}{2}$".encode('utf-8')).digest():
+                (0, 0.3448408333333333, tau_half),
+            sha1("$$a^3 + b^3 \\neq c^3$$".encode('utf-8')).digest():
+                (0, 0.0, fermat)})
+        self.assertEqual(math_cache_actual, math_cache_expected)