Migrate from MathJax to KaTeX #31
14 changed files with 99 additions and 95 deletions
|
|
@ -524,8 +524,8 @@ def main(global_config, **settings):
|
|||
def add_node_order(request):
|
||||
return request.params.get("order", request.namespace.node_order)
|
||||
|
||||
def add_mathjax(request):
|
||||
return "true" if request.namespace.mathjax else "false"
|
||||
def add_katex(request):
|
||||
return "true" if request.namespace.katex else "false"
|
||||
|
||||
def add_theme_mode(request):
|
||||
"""
|
||||
|
|
@ -604,7 +604,7 @@ def main(global_config, **settings):
|
|||
config.add_request_method(add_page_size, "page_size", reify=True)
|
||||
config.add_request_method(add_page_offset, "page_offset", reify=True)
|
||||
config.add_request_method(add_node_order, "node_order", reify=True)
|
||||
config.add_request_method(add_mathjax, "mathjax", reify=True)
|
||||
config.add_request_method(add_katex, "katex", reify=True)
|
||||
config.add_request_method(add_theme_mode, "theme_mode", reify=True)
|
||||
|
||||
# all of the web application routes.
|
||||
|
|
|
|||
|
|
@ -229,8 +229,8 @@ def send_immediate_notifications(request, notifications):
|
|||
},
|
||||
)
|
||||
log.info(
|
||||
"notification frequency=immediately email={}, count={}".format(
|
||||
notification.user.email,
|
||||
"notification frequency=immediately username={}, count={}".format(
|
||||
notification.user.name,
|
||||
notification.id,
|
||||
)
|
||||
)
|
||||
|
|
@ -285,9 +285,9 @@ def send_digest_notifications(request, notification_dict, frequency="daily"):
|
|||
},
|
||||
)
|
||||
log.info(
|
||||
"notification frequency={} email={}, count={}".format(
|
||||
"notification frequency={} username={}, count={}".format(
|
||||
frequency,
|
||||
recipient_email,
|
||||
user.name,
|
||||
notifications_count,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,28 +12,14 @@ log = logging.getLogger(__name__)
|
|||
|
||||
def remarkbox_tag_acl(namespace):
|
||||
"""
|
||||
Returns a tag_acl document, in this form:
|
||||
Returns a tag_acl document for controlling allowed HTML tags.
|
||||
|
||||
tag_acl = {
|
||||
"script": [
|
||||
("type", "math/tex; mode=display", "allow"),
|
||||
],
|
||||
}
|
||||
With KaTeX, math is rendered client-side from text delimiters,
|
||||
so we don't need special script tag handling like we did with MathJax.
|
||||
"""
|
||||
# defaultdict which assumes new keys are lists.
|
||||
tag_acl = default_tag_acl()
|
||||
|
||||
allow_mathjax_acl = ("type", "math/tex; mode=display", "allow")
|
||||
deny_mathjax_acl = ("type", "math/tex; mode=display", "deny")
|
||||
|
||||
log.info("Building remarkbox_tag_acl.")
|
||||
mathjax_acl = deny_mathjax_acl
|
||||
if namespace.mathjax:
|
||||
log.info("This Namespace has Mathjax enabled.")
|
||||
mathjax_acl = allow_mathjax_acl
|
||||
|
||||
tag_acl["script"].append(mathjax_acl)
|
||||
|
||||
return tag_acl
|
||||
|
||||
|
||||
|
|
@ -45,14 +31,15 @@ def make_cleaner_from_namespace(namespace):
|
|||
)
|
||||
# add some properties onto the cleaner object.
|
||||
cleaner.link_protection = namespace.link_protection
|
||||
cleaner.mathjax = namespace.mathjax
|
||||
cleaner.katex = namespace.katex
|
||||
cleaner.whitelist_domains.append(namespace.name)
|
||||
cleaner.absolute_domain = namespace.name
|
||||
return cleaner
|
||||
|
||||
|
||||
def markdown_to_html(data, namespace=None):
|
||||
raw_html = markdown_to_raw_html(data, extra_extensions=["mdx_math"])
|
||||
# No special math extension needed - KaTeX auto-render handles $ delimiters client-side
|
||||
raw_html = markdown_to_raw_html(data)
|
||||
if namespace:
|
||||
cleaner = make_cleaner_from_namespace(namespace)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -30,22 +30,7 @@ def default_cleaner(tag_acl=None):
|
|||
We use BeautifulSoup to conditionally whitelist or blacklist
|
||||
tags based on tag attr_name and attr_value pairs.
|
||||
|
||||
For example, this will whitelist Mathjax script tags:
|
||||
|
||||
tag_acl = {
|
||||
"script": [
|
||||
("type", "math/tex; mode=display", "allow"),
|
||||
],
|
||||
}
|
||||
|
||||
While this example will blacklist Mathjax script tags:
|
||||
|
||||
tag_acl = {
|
||||
"script": [
|
||||
("type", "math/tex; mode=display", "deny"),
|
||||
],
|
||||
}
|
||||
|
||||
tag_acl allows controlling which tags are allowed based on attributes.
|
||||
"""
|
||||
if tag_acl is None:
|
||||
tag_acl = {}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ PROTECTED_ATTRIBUTES = {
|
|||
"hide_unverified": False,
|
||||
"hide_unless_approved": False,
|
||||
"hide_powered_by": False,
|
||||
"mathjax": False,
|
||||
"katex": False,
|
||||
"link_protection": False,
|
||||
"ignore_query_string": False,
|
||||
"reverse_order": False,
|
||||
|
|
@ -96,8 +96,8 @@ class Namespace(RBase, Base):
|
|||
hide_powered_by = Column(Boolean, default=False)
|
||||
# should the list of root nodes in this namespace be public or hidden?
|
||||
public = Column(Boolean, default=False)
|
||||
# should we enable MathJax?
|
||||
mathjax = Column(Boolean, default=False)
|
||||
# should we enable KaTeX for math rendering?
|
||||
katex = Column(Boolean, default=False)
|
||||
# should we enable Link Protection to prevent comments from having links?
|
||||
link_protection = Column(Boolean, default=False)
|
||||
# check this box if the external site does _not_ use
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
"""Add katex column in rb_namespace
|
||||
|
||||
Revision ID: 7ad8508e50de
|
||||
Revises: b8f3c9d4e5a1
|
||||
Create Date: 2025-01-10 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "7ad8508e50de"
|
||||
down_revision = "b8f3c9d4e5a1"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Add new katex column with default OFF
|
||||
op.add_column("rb_namespace", sa.Column("katex", sa.Boolean(), server_default=sa.false()))
|
||||
|
||||
# Turn katex ON for any namespace that had mathjax ON
|
||||
op.execute("UPDATE rb_namespace SET katex = 1 WHERE mathjax = 1")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Copy katex values back to mathjax
|
||||
# Note: SQLite doesn't support DROP COLUMN, so we just sync the data back
|
||||
op.execute("UPDATE rb_namespace SET mathjax = katex")
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
// previewTimer must live outside the functions.
|
||||
var previewTimer = null;
|
||||
|
||||
function previewAjax(textarea, div, show_raw = false, mathjax = false){
|
||||
function previewAjax(textarea, div, show_raw = false, katex = false){
|
||||
// set div to raw textarea while waiting for remote Markdown rendering.
|
||||
if (show_raw) {
|
||||
// bust HTML tags like <script> to prevent running evil code.
|
||||
|
|
@ -14,12 +14,12 @@ function previewAjax(textarea, div, show_raw = false, mathjax = false){
|
|||
clearTimeout(previewTimer);
|
||||
}
|
||||
previewTimer = setTimeout(
|
||||
function() { sendPreview( textarea, div, mathjax ); },
|
||||
function() { sendPreview( textarea, div, katex ); },
|
||||
800
|
||||
);
|
||||
}
|
||||
|
||||
function sendPreview(textarea, div, mathjax=false){
|
||||
function sendPreview(textarea, div, katex=false){
|
||||
$.ajaxSetup ({
|
||||
cache: false
|
||||
});
|
||||
|
|
@ -29,10 +29,17 @@ function sendPreview(textarea, div, mathjax=false){
|
|||
|
||||
$( '#' + div ).load( url, params );
|
||||
|
||||
if (mathjax) {
|
||||
if (katex) {
|
||||
setTimeout(function(){
|
||||
// hack: block MathJax rerender for 50ms, give div chance to load.
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub,div]);
|
||||
// Give div chance to load before rendering math
|
||||
if (typeof renderMathInElement !== 'undefined') {
|
||||
renderMathInElement(document.getElementById(div), {
|
||||
delimiters: [
|
||||
{left: '$$', right: '$$', display: true},
|
||||
{left: '$', right: '$', display: false}
|
||||
]
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ If checked, disable infinite nesting and limit to only a depth of 2.
|
|||
If checked, both <code>example.com/1</code> and <code>example.com/1?a=2</code> will load the same thread because <code>a=2</code> is ignored.
|
||||
<br>
|
||||
<br>
|
||||
<label>Enable MathJax:</label>
|
||||
<input type="checkbox" name="mathjax-checkbox" id="mathjax-checkbox" {% if request.namespace.mathjax %}checked{% endif %}></input>
|
||||
If checked, allow beautiful math equations with <a href="https://www.mathjax.org/" target="_blank" rel="nofollow">MathJax</a> <span class="status">(could introduce slowness)</span>
|
||||
<label>Enable KaTeX:</label>
|
||||
<input type="checkbox" name="katex-checkbox" id="katex-checkbox" {% if request.namespace.katex %}checked{% endif %}></input>
|
||||
If checked, allow beautiful math equations with <a href="https://katex.org/" target="_blank" rel="nofollow">KaTeX</a> <span class="status">(fast math rendering)</span>
|
||||
<br>
|
||||
<code>$$E = mc^2$$</code>
|
||||
<br>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
name = "thread_data"
|
||||
id = "thread_data_textarea"
|
||||
class = "common-textarea"
|
||||
onkeyup = "previewAjax( 'thread_data_textarea', 'preview', show_raw=true, mathjax={{ request.mathjax }} )"
|
||||
onkeyup = "previewAjax( 'thread_data_textarea', 'preview', show_raw=true, katex={{ request.katex }} )"
|
||||
placeholder = "What do you want to say? markdown"
|
||||
required></textarea>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
name = "thread_data"
|
||||
id = "textarea-{{ node.id }}"
|
||||
class = "common-textarea"
|
||||
onkeyup = "previewAjax( 'textarea-{{ node.id }}', 'preview-{{ node.id }}', show_raw=true, mathjax={{ request.mathjax }} )"
|
||||
onkeyup = "previewAjax( 'textarea-{{ node.id }}', 'preview-{{ node.id }}', show_raw=true, katex={{ request.katex }} )"
|
||||
placeholder = "{% if request.namespace.placeholder_text %}{{ request.namespace.placeholder_text }}{% else %}What do you want to say? markdown{% endif %}"
|
||||
required></textarea>
|
||||
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
name = "thread_data"
|
||||
id = "edit-textarea-{{ node.id }}"
|
||||
class = "common-textarea textarea_edit"
|
||||
onkeyup = "previewAjax( 'edit-textarea-{{ node.id }}', 'node-data-{{ node.id }}', show_raw=false, mathjax={{ request.mathjax }} )"
|
||||
onkeyup = "previewAjax( 'edit-textarea-{{ node.id }}', 'node-data-{{ node.id }}', show_raw=false, katex={{ request.katex }} )"
|
||||
placeholder = "{% if request.namespace.placeholder_text %}{{ request.namespace.placeholder_text }}{% else %}What do you want to say? markdown{% endif %}"
|
||||
required>{{ node.data }}</textarea>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,13 +17,18 @@
|
|||
gtag('config', '{{ request.namespace.google_analytics_id }}');
|
||||
</script>
|
||||
{%- endif %}
|
||||
{% if request.namespace and request.namespace.mathjax %}
|
||||
<!-- references:
|
||||
* https://docs.mathjax.org/en/v2.7-latest/safe-mode.html
|
||||
* reference https://docs.mathjax.org/en/v2.7-latest/start.html#using-a-content-delivery-network-cdn
|
||||
-->
|
||||
<script type="text/javascript" async
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML,Safe">
|
||||
{% if request.namespace and request.namespace.katex %}
|
||||
<!-- KaTeX CSS and JS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV" crossorigin="anonymous">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js" integrity="sha384-XjKyOOlGwcjNTAIQHIpgOno0Hl1YQqzUOEleOLALmuqehneUG+vnGctmUb0ZY0l8" crossorigin="anonymous"></script>
|
||||
<!-- KaTeX auto-render extension -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js" integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05" crossorigin="anonymous"
|
||||
onload="renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{left: '$$', right: '$$', display: true},
|
||||
{left: '$', right: '$', display: false}
|
||||
]
|
||||
});">
|
||||
</script>
|
||||
{%- endif %}
|
||||
{%- include 'google-analytics.j2' %}
|
||||
|
|
|
|||
|
|
@ -138,20 +138,20 @@ class TestNamespace(unittest.TestCase):
|
|||
self.namespace.subscription_type = "production"
|
||||
self.namespace.link_protection = True
|
||||
self.namespace.hide_powered_by = True
|
||||
self.namespace.mathjax = True
|
||||
self.namespace.katex = True
|
||||
self.assertTrue(self.namespace.link_protection)
|
||||
self.assertTrue(self.namespace.hide_powered_by)
|
||||
self.assertTrue(self.namespace.mathjax)
|
||||
self.assertTrue(self.namespace.katex)
|
||||
self.assertFalse(self.namespace.memoized_attr_protection)
|
||||
|
||||
def test_namespace_development_custom_settings(self):
|
||||
self.namespace.subscription_type = "development"
|
||||
self.namespace.link_protection = True
|
||||
self.namespace.hide_powered_by = True
|
||||
self.namespace.mathjax = True
|
||||
self.namespace.katex = True
|
||||
self.assertFalse(self.namespace.link_protection)
|
||||
self.assertFalse(self.namespace.hide_powered_by)
|
||||
self.assertFalse(self.namespace.mathjax)
|
||||
self.assertFalse(self.namespace.katex)
|
||||
self.assertTrue(self.namespace.memoized_attr_protection)
|
||||
|
||||
def test_namespace_simulate_expired_subscription(self):
|
||||
|
|
@ -159,10 +159,10 @@ class TestNamespace(unittest.TestCase):
|
|||
self.namespace.subscription_type = "production"
|
||||
self.namespace.link_protection = True
|
||||
self.namespace.hide_powered_by = True
|
||||
self.namespace.mathjax = True
|
||||
self.namespace.katex = True
|
||||
self.assertTrue(self.namespace.link_protection)
|
||||
self.assertTrue(self.namespace.hide_powered_by)
|
||||
self.assertTrue(self.namespace.mathjax)
|
||||
self.assertTrue(self.namespace.katex)
|
||||
|
||||
# next the namespace subscription expires with custom settings.
|
||||
self.namespace.subscription_type = "development"
|
||||
|
|
@ -170,7 +170,7 @@ class TestNamespace(unittest.TestCase):
|
|||
self.assertTrue(self.namespace.memoized_attr_protection)
|
||||
self.assertFalse(self.namespace.link_protection)
|
||||
self.assertFalse(self.namespace.hide_powered_by)
|
||||
self.assertFalse(self.namespace.mathjax)
|
||||
self.assertFalse(self.namespace.katex)
|
||||
|
||||
# finally the namespace subscription is renewed with custom settings.
|
||||
self.namespace.subscription_type = "production"
|
||||
|
|
@ -178,7 +178,7 @@ class TestNamespace(unittest.TestCase):
|
|||
self.assertFalse(self.namespace.memoized_attr_protection)
|
||||
self.assertTrue(self.namespace.link_protection)
|
||||
self.assertTrue(self.namespace.hide_powered_by)
|
||||
self.assertTrue(self.namespace.mathjax)
|
||||
self.assertTrue(self.namespace.katex)
|
||||
|
||||
class TestNamespaceRequest(unittest.TestCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -49,36 +49,26 @@ class TestRenderMarkdown(unittest.TestCase):
|
|||
def test_make_cleaner_from_custom_namespace(self):
|
||||
namespace = Namespace("russell.ballestrini.net")
|
||||
namespace.subscription_type = "production"
|
||||
namespace.mathjax = True
|
||||
namespace.katex = True
|
||||
namespace.link_protection = True
|
||||
cleaner = make_cleaner_from_namespace(namespace)
|
||||
self.assertTrue(cleaner.link_protection)
|
||||
self.assertIn("script", cleaner.tags)
|
||||
self.assertEqual("russell.ballestrini.net", cleaner.absolute_domain)
|
||||
self.assertIn("russell.ballestrini.net", cleaner.whitelist_domains)
|
||||
|
||||
def test_mathjax_enabled_and_disabled(self):
|
||||
raw_html = markdown_to_raw_html(SAMPLE_MARKDOWN)
|
||||
def test_katex_enabled_and_disabled(self):
|
||||
namespace = Namespace("russell.ballestrini.net")
|
||||
namespace.subscription_type = "production"
|
||||
|
||||
# test mathjax enabled.
|
||||
namespace.mathjax = True
|
||||
# test katex enabled.
|
||||
namespace.katex = True
|
||||
cleaner = make_cleaner_from_namespace(namespace)
|
||||
self.assertTrue(cleaner.mathjax)
|
||||
clean_html = clean_raw_html(raw_html, cleaner)
|
||||
self.assertIn(
|
||||
'<script type="math/tex; mode=display">y=x^2</script>', clean_html
|
||||
)
|
||||
self.assertTrue(cleaner.katex)
|
||||
|
||||
# test mathjax disabled.
|
||||
namespace.mathjax = False
|
||||
# test katex disabled.
|
||||
namespace.katex = False
|
||||
cleaner = make_cleaner_from_namespace(namespace)
|
||||
self.assertFalse(cleaner.mathjax)
|
||||
clean_html = clean_raw_html(raw_html, cleaner)
|
||||
self.assertNotIn(
|
||||
'<script type="math/tex; mode=display">y=x^2</script>', clean_html
|
||||
)
|
||||
self.assertFalse(cleaner.katex)
|
||||
|
||||
def test_link_protection_enabled(self):
|
||||
raw_html = markdown_to_raw_html(SAMPLE_MARKDOWN)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ def namespace_settings(request):
|
|||
link_protection_checkbox = p.get("link-protection-checkbox", "off")
|
||||
reverse_order_checkbox = p.get("reverse-order-checkbox", "off")
|
||||
group_conversations_checkbox = p.get("group-conversations-checkbox", "off")
|
||||
mathjax_checkbox = p.get("mathjax-checkbox", "off")
|
||||
katex_checkbox = p.get("katex-checkbox", "off")
|
||||
ignore_query_string_checkbox = p.get("ignore-query-string-checkbox", "off")
|
||||
hide_powered_by_checkbox = p.get("hide-powered-by-checkbox", "off")
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ def namespace_settings(request):
|
|||
link_protection = checkbox_to_bool(link_protection_checkbox)
|
||||
reverse_order = checkbox_to_bool(reverse_order_checkbox)
|
||||
group_conversations = checkbox_to_bool(group_conversations_checkbox)
|
||||
mathjax = checkbox_to_bool(mathjax_checkbox)
|
||||
katex = checkbox_to_bool(katex_checkbox)
|
||||
ignore_query_string = checkbox_to_bool(ignore_query_string_checkbox)
|
||||
hide_powered_by = checkbox_to_bool(hide_powered_by_checkbox)
|
||||
|
||||
|
|
@ -195,10 +195,10 @@ def namespace_settings(request):
|
|||
)
|
||||
)
|
||||
|
||||
if mathjax != request.namespace.mathjax:
|
||||
request.namespace.mathjax = mathjax
|
||||
if katex != request.namespace.katex:
|
||||
request.namespace.katex = katex
|
||||
request.session.flash(
|
||||
("You turned {} MathJax".format(mathjax_checkbox), "success")
|
||||
("You turned {} KaTeX".format(katex_checkbox), "success")
|
||||
)
|
||||
|
||||
request.dbsession.add(request.namespace)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue