Files
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

100 lines
3.8 KiB
Python

import logging
import os
import subprocess
from typing import Any
import lxml.html
import requests
from django.conf import settings
from zerver.lib.outgoing_http import OutgoingSession
from zerver.lib.storage import static_path
class KatexSession(OutgoingSession):
def __init__(self, **kwargs: Any) -> None:
# We set a very short timeout because these requests are
# expected to be quite fast (milliseconds) and blocking on
# this affects message rendering performance.
super().__init__(role="katex", timeout=0.5, **kwargs)
def render_tex(tex: str, is_inline: bool = True) -> str | None:
r"""Render a TeX string into HTML using KaTeX
Returns the HTML string, or None if there was some error in the TeX syntax
Keyword arguments:
tex -- Text string with the TeX to render
Don't include delimiters ('$$', '\[ \]', etc.)
is_inline -- Boolean setting that indicates whether the render should be
inline (i.e. for embedding it in text) or not. The latter
will show the content centered, and in the "expanded" form
(default True)
"""
if settings.KATEX_SERVER:
try:
resp = KatexSession().post(
# We explicitly disable the Smokescreen proxy for this
# call, since it intentionally connects to localhost.
# This is safe because the host is explicitly fixed, and
# the port is pulled from our own configuration.
f"http://localhost:{settings.KATEX_SERVER_PORT}/",
data={
"content": tex,
"is_display": "false" if is_inline else "true",
"shared_secret": settings.SHARED_SECRET,
},
proxies={"http": ""},
)
except requests.exceptions.Timeout:
logging.warning("KaTeX rendering service timed out with %d byte long input", len(tex))
return None
except requests.exceptions.RequestException as e:
logging.warning("KaTeX rendering service failed: %s", type(e).__name__)
return None
if resp.status_code == 200:
return resp.content.decode().strip()
elif resp.status_code == 400:
return None
else:
logging.warning(
"KaTeX rendering service failed: (%s) %s", resp.status_code, resp.content.decode()
)
return None
katex_path = (
static_path("webpack-bundles/katex-cli.js")
if settings.PRODUCTION
else os.path.join(settings.DEPLOY_ROOT, "node_modules/katex/cli.js")
)
if not os.path.isfile(katex_path):
logging.error("Cannot find KaTeX for latex rendering!")
return None
command = ["node", katex_path]
if not is_inline:
command.extend(["--display-mode"])
try:
stdout = subprocess.check_output(command, input=tex, stderr=subprocess.DEVNULL, text=True)
# stdout contains a newline at the end
return stdout.strip()
except subprocess.CalledProcessError:
return None
def change_katex_to_raw_latex(fragment: lxml.html.HtmlElement) -> None:
# Selecting the <span> elements with class 'katex'
katex_spans = fragment.xpath("//span[@class='katex']")
# Iterate through 'katex_spans' and replace with a new <span> having LaTeX text.
for katex_span in katex_spans:
latex_text = katex_span.xpath(".//annotation[@encoding='application/x-tex']")[0].text
# We store 'tail' to insert them back as the replace operation removes it.
tail = katex_span.tail
latex_span = lxml.html.Element("span")
latex_span.text = f"$${latex_text}$$"
katex_span.getparent().replace(katex_span, latex_span)
latex_span.tail = tail