diff --git a/remarkbox/__init__.py b/remarkbox/__init__.py index 863097f..a005a2a 100644 --- a/remarkbox/__init__.py +++ b/remarkbox/__init__.py @@ -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. diff --git a/remarkbox/lib/notify.py b/remarkbox/lib/notify.py index 751f91b..bf53d43 100644 --- a/remarkbox/lib/notify.py +++ b/remarkbox/lib/notify.py @@ -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, ) ) diff --git a/remarkbox/lib/render.py b/remarkbox/lib/render.py index 743d64a..2caf5b4 100644 --- a/remarkbox/lib/render.py +++ b/remarkbox/lib/render.py @@ -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: diff --git a/remarkbox/lib/sanitize_html.py b/remarkbox/lib/sanitize_html.py index cc5dea3..56c2657 100644 --- a/remarkbox/lib/sanitize_html.py +++ b/remarkbox/lib/sanitize_html.py @@ -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 = {} diff --git a/remarkbox/models/namespace.py b/remarkbox/models/namespace.py index a19107f..9f654b3 100644 --- a/remarkbox/models/namespace.py +++ b/remarkbox/models/namespace.py @@ -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 diff --git a/remarkbox/scripts/alembic/versions/7ad8508e50de_rename_mathjax_to_katex.py b/remarkbox/scripts/alembic/versions/7ad8508e50de_rename_mathjax_to_katex.py new file mode 100644 index 0000000..0c65c95 --- /dev/null +++ b/remarkbox/scripts/alembic/versions/7ad8508e50de_rename_mathjax_to_katex.py @@ -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") diff --git a/remarkbox/static/js/custom.js b/remarkbox/static/js/custom.js index ab28d8a..51b6e2b 100644 --- a/remarkbox/static/js/custom.js +++ b/remarkbox/static/js/custom.js @@ -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 {%- endif %} -{% if request.namespace and request.namespace.mathjax %} - - + + {%- endif %} {%- include 'google-analytics.j2' %} diff --git a/remarkbox/tests/test_models.py b/remarkbox/tests/test_models.py index b5ffda9..2ee6acd 100644 --- a/remarkbox/tests/test_models.py +++ b/remarkbox/tests/test_models.py @@ -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): diff --git a/remarkbox/tests/test_render.py b/remarkbox/tests/test_render.py index 0091618..a8eaea1 100644 --- a/remarkbox/tests/test_render.py +++ b/remarkbox/tests/test_render.py @@ -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( - '', 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( - '', clean_html - ) + self.assertFalse(cleaner.katex) def test_link_protection_enabled(self): raw_html = markdown_to_raw_html(SAMPLE_MARKDOWN) diff --git a/remarkbox/views/authenticated/authenticated.py b/remarkbox/views/authenticated/authenticated.py index 39067e4..52bf78e 100644 --- a/remarkbox/views/authenticated/authenticated.py +++ b/remarkbox/views/authenticated/authenticated.py @@ -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)