Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
- Dropped support for Django < 5.2
- Upgraded to MathJax 4.1.2, using [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity)
for the CDN script. The template tag has also changed to `mathjax_script`. See upgrade considerations
- Added a `WAGTAIL_POLYMATH` settings dict, with `mathjax_url` and `mathjax_sri` keys, to allow
loading MathJax from a different CDN, or self-hosted, instead of the pinned jsdelivr default.
See [Configuration](README.md#configuration)

### Upgrade considerations

Expand Down Expand Up @@ -38,6 +41,12 @@ The `mathjax` template tag has changed to `mathjax_script` and should no longer
+ {% mathjax_script %}
```

#### `MATHJAX_VERSION`/`MATHJAX_SRI` moved out of `widgets.py`
These were never documented as public API, but if you imported them
directly, they now live in `wagtail_polymath.settings`, which also exposes
a `wagtail_polymath_settings` object (`.mathjax_url`/`.mathjax_sri`) for
reading the effective, resolved settings.

## 2.0.0.dev1 (2026-06-18)

- The project namespace has changed from wagtailmath to wagtail_polymath.
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,64 @@ MathJax library:
{% mathjax_script %}
```

## Configuration

All `wagtail-polymath` settings are defined in a single `WAGTAIL_POLYMATH`
dictionary in your settings file.

By default, wagtail-polymath loads MathJax from jsdelivr, pinned to a specific
version with a matching [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity)
Comment thread
zerolab marked this conversation as resolved.
(SRI) hash, so the browser can verify the script hasn't been tampered with.

If you'd rather load MathJax from a different CDN, your own static files, or
a different version, set `mathjax_url` to the full script URL:

```python
# settings.py
WAGTAIL_POLYMATH = {
"mathjax_url": "https://example.com/path/to/tex-mml-chtml.js",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my last item for pause is whether we use the full URL to the library, or the URL base (i.e. https://example.com/path/to/), thus hard-coding tex-mml-chtml.js as that is what we account for in the initialisation script 🤔

On one hand the full library URL gives extra flexibility, on the other we don't have much control.

I say let's go with the full URL for now, but leave the option open to change things if we want to

}
```

Since we can't know the SRI hash for a script we don't control, setting a
Comment thread
zerolab marked this conversation as resolved.
custom URL on its own disables integrity checking for that script (no
`integrity`/`crossorigin` attributes are rendered). If you want that
protection back, also set `mathjax_sri` to the hash for your chosen file:

```python
# settings.py
WAGTAIL_POLYMATH = {
"mathjax_url": "https://example.com/path/to/tex-mml-chtml.js",
"mathjax_sri": "sha256-...",
}
```

`mathjax_sri` has no effect unless `mathjax_url` is also set — the built-in
default URL always uses its own pinned hash.

To generate the hash for your chosen file, download it and use `openssl`.
Note that the `integrity` attribute requires a **base64**-encoded digest —
`sha256sum`/`shasum` produce a hex digest instead, which will not work:

```sh
openssl dgst -sha256 -binary tex-mml-chtml.js | openssl base64 -A
```

Prefix the output with `sha256-` to get the full `mathjax_sri` value:

```python
WAGTAIL_POLYMATH = {
"mathjax_url": "https://example.com/path/to/tex-mml-chtml.js",
"mathjax_sri": "sha256-dPV35kaoLq1rg+JbYf8p1kTrZamwMY+XIwaWUPwqtpU=",
}
```

Both settings apply to the MathJax script loaded in the Wagtail admin (for
the `MathBlock` live preview) and the one loaded by the `mathjax_script`
template tag. Note that the bundled preview JS assumes MathJax's combined
`tex-mml-chtml` component and its `input/asciimath` loader — if you switch to
a different version or build of MathJax, you're responsible for keeping it
compatible with that configuration.

## Contributing

Expand Down
37 changes: 37 additions & 0 deletions src/wagtail_polymath/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.conf import settings


MATHJAX_VERSION = "4.1.2"
MATHJAX_DEFAULT_URL = (
f"https://cdn.jsdelivr.net/npm/mathjax@{MATHJAX_VERSION}/tex-mml-chtml.js"
)
MATHJAX_DEFAULT_SRI = "sha256-dPV35kaoLq1rg+JbYf8p1kTrZamwMY+XIwaWUPwqtpU="


class WagtailPolymathSettings:
"""
Shadows Django's settings, exposing the WAGTAIL_POLYMATH dict as attributes.
For example:
from wagtail_polymath.settings import wagtail_polymath_settings
print(wagtail_polymath_settings.mathjax_url)
"""

@property
def _user_settings(self):
user_settings = getattr(settings, "WAGTAIL_POLYMATH", None)
return user_settings if isinstance(user_settings, dict) else {}
@property
def mathjax_url(self):
return self._user_settings.get("mathjax_url") or MATHJAX_DEFAULT_URL

@property
def mathjax_sri(self):
if self._user_settings.get("mathjax_url"):
# We can't know the hash for a script we don't control, so a
# custom URL without a matching mathjax_sri setting intentionally
# omits integrity checking rather than erroring.
return self._user_settings.get("mathjax_sri")
return MATHJAX_DEFAULT_SRI


wagtail_polymath_settings = WagtailPolymathSettings()
11 changes: 8 additions & 3 deletions src/wagtail_polymath/templatetags/wagtail_polymath.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
from django.utils.html import format_html
from wagtail.admin.staticfiles import versioned_static

from wagtail_polymath.widgets import MATHJAX_SRI, MATHJAX_VERSION
from wagtail_polymath.settings import wagtail_polymath_settings


register = template.Library()


@register.simple_tag
def mathjax_script():
attributes = {"crossorigin": "anonymous", "integrity": MATHJAX_SRI, "defer": True}
attributes = {"defer": True}
integrity = wagtail_polymath_settings.mathjax_sri
if integrity:
attributes["crossorigin"] = "anonymous"
attributes["integrity"] = integrity

return format_html(
'<script src="{init_path}"></script><script src="{path}"{attributes}></script>',
init_path=versioned_static("wagtail_polymath/js/mathjax_init.js"),
path=f"https://cdn.jsdelivr.net/npm/mathjax@{MATHJAX_VERSION}/tex-mml-chtml.js",
path=wagtail_polymath_settings.mathjax_url,
attributes=flatatt(attributes),
)
19 changes: 8 additions & 11 deletions src/wagtail_polymath/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
from django.forms import Script
from wagtail.admin.staticfiles import versioned_static


MATHJAX_VERSION = "4.1.2"
MATHJAX_SRI = "sha256-dPV35kaoLq1rg+JbYf8p1kTrZamwMY+XIwaWUPwqtpU="
from .settings import wagtail_polymath_settings


class MathJaxWidget(forms.Textarea):
Expand All @@ -18,16 +16,15 @@ def build_attrs(self, *args, **kwargs):

@property
def media(self):
attrs = {"defer": True}
integrity = wagtail_polymath_settings.mathjax_sri
if integrity:
attrs["crossorigin"] = "anonymous"
attrs["integrity"] = integrity

return forms.Media(
js=(
Script(
f"https://cdn.jsdelivr.net/npm/mathjax@{MATHJAX_VERSION}/tex-mml-chtml.js",
**{
"crossorigin": "anonymous",
"integrity": MATHJAX_SRI,
"defer": True,
},
),
Script(wagtail_polymath_settings.mathjax_url, **attrs),
versioned_static("wagtail_polymath/js/wagtail_polymath.js"),
versioned_static(
"wagtail_polymath/js/wagtail_polymath-mathjax-controller.js"
Expand Down
101 changes: 101 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from wagtail_polymath.settings import (
MATHJAX_DEFAULT_SRI,
MATHJAX_DEFAULT_URL,
wagtail_polymath_settings,
)
from wagtail_polymath.templatetags.wagtail_polymath import mathjax_script
from wagtail_polymath.widgets import MathJaxWidget


CUSTOM_URL = "https://example.com/mathjax/tex-mml-chtml.js"
CUSTOM_SRI = "sha256-Ynv3Q3nAtRTr6UDX+X6vbn9d1t8ZO5oV2Y4gvL9y0ck="


def widget_media_html():
return str(MathJaxWidget().media)


class TestDefaultMathJaxSettings:
"""No WAGTAIL_POLYMATH setting configured."""

def test_mathjax_url_returns_default(self):
assert wagtail_polymath_settings.mathjax_url == MATHJAX_DEFAULT_URL

def test_mathjax_sri_returns_default(self):
assert wagtail_polymath_settings.mathjax_sri == MATHJAX_DEFAULT_SRI

def test_widget_media_uses_default_url_and_integrity(self):
html = widget_media_html()
assert MATHJAX_DEFAULT_URL in html
assert f'integrity="{MATHJAX_DEFAULT_SRI}"' in html
assert 'crossorigin="anonymous"' in html

def test_template_tag_uses_default_url_and_integrity(self):
html = mathjax_script()
assert MATHJAX_DEFAULT_URL in html
assert f'integrity="{MATHJAX_DEFAULT_SRI}"' in html
assert 'crossorigin="anonymous"' in html


class TestCustomUrlOnly:
"""WAGTAIL_POLYMATH["mathjax_url"] set, no matching SRI hash supplied."""

def test_mathjax_url_returns_custom_url(self, settings):
settings.WAGTAIL_POLYMATH = {"mathjax_url": CUSTOM_URL}
assert wagtail_polymath_settings.mathjax_url == CUSTOM_URL

def test_mathjax_sri_is_none(self, settings):
settings.WAGTAIL_POLYMATH = {"mathjax_url": CUSTOM_URL}
assert wagtail_polymath_settings.mathjax_sri is None

def test_widget_media_omits_integrity(self, settings):
settings.WAGTAIL_POLYMATH = {"mathjax_url": CUSTOM_URL}
html = widget_media_html()
assert CUSTOM_URL in html
assert MATHJAX_DEFAULT_URL not in html
assert "integrity" not in html
assert "crossorigin" not in html

def test_template_tag_omits_integrity(self, settings):
settings.WAGTAIL_POLYMATH = {"mathjax_url": CUSTOM_URL}
html = mathjax_script()
assert CUSTOM_URL in html
assert "integrity" not in html
assert "crossorigin" not in html


class TestCustomUrlAndSri:
"""Both mathjax_url and mathjax_sri set in WAGTAIL_POLYMATH."""

def test_mathjax_sri_returns_custom_sri(self, settings):
settings.WAGTAIL_POLYMATH = {
"mathjax_url": CUSTOM_URL,
"mathjax_sri": CUSTOM_SRI,
}
assert wagtail_polymath_settings.mathjax_sri == CUSTOM_SRI

def test_widget_media_uses_custom_url_and_integrity(self, settings):
settings.WAGTAIL_POLYMATH = {
"mathjax_url": CUSTOM_URL,
"mathjax_sri": CUSTOM_SRI,
}
html = widget_media_html()
assert CUSTOM_URL in html
assert f'integrity="{CUSTOM_SRI}"' in html
assert 'crossorigin="anonymous"' in html

def test_template_tag_uses_custom_url_and_integrity(self, settings):
settings.WAGTAIL_POLYMATH = {
"mathjax_url": CUSTOM_URL,
"mathjax_sri": CUSTOM_SRI,
}
html = mathjax_script()
assert CUSTOM_URL in html
assert f'integrity="{CUSTOM_SRI}"' in html
assert 'crossorigin="anonymous"' in html

def test_sri_ignored_without_matching_url(self, settings):
"""The mathjax_sri key alone (no mathjax_url) must not affect the default."""
settings.WAGTAIL_POLYMATH = {"mathjax_sri": CUSTOM_SRI}
assert wagtail_polymath_settings.mathjax_url == MATHJAX_DEFAULT_URL
assert wagtail_polymath_settings.mathjax_sri == MATHJAX_DEFAULT_SRI