snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
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
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
This commit is contained in:
99
zerver/lib/tex.py
Normal file
99
zerver/lib/tex.py
Normal file
@@ -0,0 +1,99 @@
|
||||
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
|
||||
Reference in New Issue
Block a user