Upgrade MathJax to version 3

version 3 is more performant, smaller and simpler to integrate, without
monkeypatching. On the down side, it does not do line breaking, as yet.

Note that only the viewer is currently ported
This commit is contained in:
Kovid Goyal 2020-06-19 18:43:23 +05:30
parent acba68ead4
commit e2243bf7a9
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 43 additions and 209 deletions

View File

@ -8,7 +8,7 @@
<!-- This script tag is needed to make calibre's ebook-viewer recpgnize that this file needs math typesetting -->
<script type="text/x-mathjax-config">
// This line adds numbers to all equations automatically, unless explicitly suppressed.
MathJax.Hub.Config({ TeX: { equationNumbers: {autoNumber: "all"} } });
MathJax.tex = {tags: 'all'};
</script>
<style>

Binary file not shown.

View File

@ -7,7 +7,6 @@ __copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, json
from io import BytesIO
from hashlib import sha1
@ -19,9 +18,8 @@ class MathJax(ReVendor):
description = 'Create the MathJax bundle'
NAME = 'mathjax'
TAR_NAME = 'MathJax'
VERSION = '2.7.6'
VERSION = '3.0.5'
DOWNLOAD_URL = 'https://github.com/mathjax/MathJax/archive/%s.tar.gz' % VERSION
FONT_FAMILY = 'TeX'
def add_file_pre(self, name, raw):
self.h.update(raw)
@ -46,14 +44,10 @@ class MathJax(ReVendor):
with self.temp_dir(suffix='-calibre-mathjax-build') as tdir:
src = opts.path_to_mathjax or self.download_vendor_release(tdir, opts.mathjax_url)
self.info('Adding MathJax...')
unpacked = 'unpacked' if self.e(self.j(src, 'unpacked')) else ''
self.add_file(self.j(src, unpacked, 'MathJax.js'), 'MathJax.js')
for x in 'core loader startup input/tex-full input/asciimath input/mml input/mml/entities output/chtml'.split():
self.add_file(self.j(src, 'es5', x + '.js'), x + '.js')
self.add_tree(
self.j(src, 'fonts', 'HTML-CSS', self.FONT_FAMILY, 'woff'),
'fonts/HTML-CSS/%s/woff' % self.FONT_FAMILY,
lambda x: not x.endswith('.woff'))
for d in 'extensions jax/element jax/input jax/output/CommonHTML'.split():
self.add_tree(self.j(src, unpacked, *d.split('/')), d)
self.j(src, 'es5', 'output', 'chtml'), 'output/chtml')
etag = self.h.hexdigest()
with open(self.j(self.RESOURCES, 'mathjax', 'manifest.json'), 'wb') as f:
f.write(json.dumps({'etag': etag, 'files': self.mathjax_files, 'version': self.VERSION}, indent=2).encode('utf-8'))

View File

@ -1,72 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals
PATCHED_MATHJAX = '''
function postprocess_mathjax(link_uid) {
Array.prototype.forEach.call(document.getElementsByTagName('a'), function(a) {
var href = a.getAttribute('href');
if (href && href.startsWith('#')) {
a.setAttribute('href', 'javascript: void(0)')
a.setAttribute('data-' + link_uid, JSON.stringify({'frag':href.slice(1)}))
}
});
document.documentElement.dispatchEvent(new CustomEvent("calibre-mathjax-init-done"));
}
function monkeypatch(mathjax_files) {
var orig = window.MathJax.Ajax.fileURL.bind(window.MathJax.Ajax);
var StyleString = window.MathJax.Ajax.StyleString.bind(window.MathJax.Ajax);
window.MathJax.Ajax.StyleString = function(styles) {
return StyleString(styles).replace(/url\\('?(.*?)'?\\)/g, function(match, url) {
if (!url.endsWith('.woff')) return match;
url = mathjax_files[url];
if (!url) return match;
if (typeof url != "string") {
url = window.URL.createObjectURL(url);
mathjax_files[name] = url;
}
return "url('" + url + "')";
});
}
window.MathJax.Ajax.fileURL = function(mathjax_name) {
var ans = orig(mathjax_name);
var name = ans.replace(/^\\//g, '');
if (name.startsWith('../fonts')) name = name.slice(3);
ans = mathjax_files[name];
if (!ans) ans = name;
if (typeof ans !== 'string') {
mathjax_files[name] = window.URL.createObjectURL(ans);
ans = mathjax_files[name];
}
if (ans === name && !name.startsWith('blob:') && !name.endsWith('/eot') && !name.endsWith('/woff') && !name.endsWith('/otf')) {
if (ans.endsWith('.eot') || ans.endsWith('.otf')) ans = '';
else console.log('WARNING: Failed to resolve MathJax file: ' + mathjax_name);
}
return ans;
}
window.MathJax.Ajax.fileRev = function(mathjax_name) { return ''; }
}
function init_mathjax(link_uid, mathjax_files) {
monkeypatch(mathjax_files);
window.MathJax.Hub.Register.StartupHook("End", postprocess_mathjax.bind(this, link_uid))
}
document.documentElement.addEventListener("calibre-mathjax-init", function(ev) {
window.MathJax = ev.detail.MathJax;
window.MathJax.AuthorInit = init_mathjax.bind(this, ev.detail.link_uid, ev.detail.mathjax_files);
__SRC__
});
'''
def monkeypatch_mathjax(src):
return PATCHED_MATHJAX.replace('__SRC__', src, 1)

View File

@ -121,7 +121,6 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler):
def __init__(self, parent=None):
QWebEngineUrlSchemeHandler.__init__(self, parent)
self.mathjax_dir = P('mathjax', allow_user_override=False)
self.mathjax_manifest = None
self.allowed_hosts = (FAKE_HOST, SANDBOX_HOST)
def requestStarted(self, rq):
@ -132,7 +131,7 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler):
if host not in self.allowed_hosts or url.scheme() != FAKE_PROTOCOL:
return self.fail_request(rq)
name = url.path()[1:]
if host == SANDBOX_HOST and not name.startswith('book/'):
if host == SANDBOX_HOST and name.partition('/')[0] not in ('book', 'mathjax'):
return self.fail_request(rq)
if name.startswith('book/'):
name = name.partition('/')[2]
@ -169,14 +168,6 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler):
else:
rq.fail(rq.UrlNotFound)
elif name.startswith('mathjax/'):
from calibre.gui2.viewer.mathjax import monkeypatch_mathjax
if name == 'mathjax/manifest.json':
if self.mathjax_manifest is None:
import json
from calibre.srv.books import get_mathjax_manifest
self.mathjax_manifest = as_bytes(json.dumps(get_mathjax_manifest()['files']))
send_reply(rq, 'application/json', self.mathjax_manifest)
return
path = os.path.abspath(os.path.join(self.mathjax_dir, '..', name))
if path.startswith(self.mathjax_dir):
mt = guess_type(name)
@ -186,10 +177,35 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler):
except EnvironmentError as err:
prints("Failed to get mathjax file: {} with error: {}".format(name, err))
return self.fail_request(rq, rq.RequestFailed)
if 'MathJax.js' in name:
# raw = open(os.path.expanduser('~/work/mathjax/unpacked/MathJax.js')).read()
raw = monkeypatch_mathjax(raw.decode('utf-8')).encode('utf-8')
if name.endswith('/startup.js'):
raw = b'''
window.MathJax = {};
window.MathJax.options = {
renderActions: {
// disable the mathjax context menu
addMenu: [0, '', ''],
},
};
window.MathJax.loader = {
load: ['input/tex-full', 'input/asciimath', 'input/mml', 'output/chtml'],
};
window.MathJax.startup = {
ready: () => {
MathJax.startup.defaultReady();
MathJax.startup.promise.then(() => {
document.documentElement.dispatchEvent(new CustomEvent("calibre-mathjax-typeset-done"));
});
},
};
for (const s of document.scripts) {
if (s.type === "text/x-mathjax-config") {
es = document.createElement('script');
es.text = s.text;
document.head.appendChild(es);
document.head.removeChild(es);
}
}
''' + raw
send_reply(rq, mt, raw)
elif not name:
send_reply(rq, 'text/html', viewer_html())

View File

@ -16,75 +16,20 @@ def get_url(mathjax_files, name):
return ans
def monkeypatch(mathjax_files):
StyleString = window.MathJax.Ajax.StyleString.bind(window.MathJax.Ajax)
def style_string(styles):
# replace the URLs in the generated stylesheet
return StyleString(styles).replace(/url\('?(.*?)'?\)/g, def (match, url):
if not url.endsWith('.woff'):
return match
url = get_url(mathjax_files, url)
ans = f"url('{url}')"
return ans
)
orig = window.MathJax.Ajax.fileURL.bind(window.MathJax.Ajax)
def file_url(file):
ans = orig(file)
name = ans.lstrip('/')
if name.startswith('../fonts'):
name = name[3:]
if name.rpartition('/')[-1] in 'otf eot woff':
return name
ans = get_url(mathjax_files, name)
if ans is name and not name.startswith('blob:'):
if ans.endswith('.eot') or ans.endswith('.otf'):
return ''
print('WARNING: Failed to resolve MathJax file:', name)
return ans
window.MathJax.Ajax.fileURL = file_url
window.MathJax.Ajax.StyleString = style_string
window.MathJax.Ajax.fileRev = def(file):
return ''
def postprocess(link_uid, proceed):
def postprocess(link_uid):
for a in document.getElementsByTagName('a'):
href = a.getAttribute('href')
if href.startswith('#'):
a.setAttribute('href', 'javascript: void(0)')
a.setAttribute('data-' + link_uid, JSON.stringify({'frag':href[1:]}))
proceed()
def init_mathjax(mathjax_files, link_uid, proceed):
monkeypatch(mathjax_files)
window.MathJax.Hub.Register.StartupHook("End", postprocess.bind(this, link_uid, proceed))
def apply_mathjax(mathjax_files, link_uid, proceed):
window.MathJax = m = v'{}'
m.positionToHash = False
m.showMathMenu = False
m.showMathMenuMSIE = False
m.extensions = "tex2jax.js asciimath2jax.js mml2jax.js".split(' ')
m.jax = "input/TeX input/MathML input/AsciiMath output/CommonHTML".split(' ')
m.TeX = v'{}'
m.TeX.extensions = "AMSmath.js AMSsymbols.js noErrors.js noUndefined.js".split(' ')
m.CommonHTML = v'{ linebreaks: { automatic: true} }'
document.documentElement.addEventListener("calibre-mathjax-typeset-done", def(ev):
postprocess(link_uid)
proceed()
)
script = E.script(type='text/javascript')
script.async = True
script.src = f'{runtime.FAKE_PROTOCOL}://{runtime.SANDBOX_HOST}/mathjax/startup.js'
document.head.appendChild(script)
if runtime.is_standalone_viewer:
script.onload = def():
ev = new CustomEvent("calibre-mathjax-init", {
'detail': {
'MathJax': m,
'link_uid': link_uid,
'mathjax_files': mathjax_files,
}
})
document.documentElement.dispatchEvent(ev)
document.documentElement.addEventListener("calibre-mathjax-init-done", def(ev):
proceed()
)
else:
m.AuthorInit = init_mathjax.bind(this, mathjax_files, link_uid, proceed)
script.src = get_url(mathjax_files, 'MathJax.js')

View File

@ -63,58 +63,9 @@ def get_file(book, name, proceed):
xhr.responseType = 'blob'
xhr.send()
def mathjax_file_received(name, proceed, end_type, xhr, ev):
end_type = workaround_qt_bug(xhr, end_type)
if end_type is 'abort':
return
if end_type is not 'load':
show_error(_('Failed to load MathJax file'), _(
'Could not load the file: {} with error: {}').format(name, xhr.error_html))
return
if not xhr.responseType or xhr.responseType is 'text':
result = xhr.responseText
else if xhr.responseType is 'blob' or xhr.responseType is 'json':
result = xhr.response
else:
show_error(_('Failed to load MathJax file'), _(
'Could not load the file: {} unknown response type: {}')
.format(name, xhr.responseType))
return
if name is 'manifest.json':
get_mathjax_files.manifest = result
get_mathjax_files_stage2.files_to_get = list(Object.keys(result))
get_mathjax_files_stage2.file_data = {}
get_mathjax_files_stage2(proceed)
return
get_mathjax_files_stage2.file_data[name] = result
get_mathjax_files_stage2.files_to_get.remove(name)
if not get_mathjax_files_stage2.files_to_get.length:
proceed(get_mathjax_files_stage2.file_data)
def get_mathjax_manifest(proceed):
xhr = ajax('mathjax/manifest.json', mathjax_file_received.bind(None, 'manifest.json', proceed), ok_code=0)
xhr.responseType = 'json'
xhr.send()
def get_mathjax_files_stage2(proceed):
if not get_mathjax_files_stage2.files_to_get.length:
proceed(get_mathjax_files_stage2.file_data)
return
for filename in get_mathjax_files_stage2.files_to_get:
xhr = ajax(f'mathjax/{filename}', mathjax_file_received.bind(None, filename, proceed), ok_code=0)
xhr.responseType = 'blob'
xhr.send()
def get_mathjax_files(proceed):
if not get_mathjax_files.manifest:
get_mathjax_manifest(proceed)
else:
get_mathjax_files_stage2(proceed)
proceed({})
def update_url_state(replace):