|
|
|
import socket
|
|
|
|
import subprocess
|
|
|
|
import tempfile
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
|
|
|
|
class KatexError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class NPMNotFoundError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class KatexServerError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class KatexClient:
|
|
|
|
_client: socket.socket
|
|
|
|
_server_process: subprocess.Popen[bytes]
|
|
|
|
_socket_file: str
|
|
|
|
_temp_dir: tempfile.TemporaryDirectory[str]
|
|
|
|
_connected: bool
|
|
|
|
|
|
|
|
def __init__(self, socket: str=None, connect: bool=True):
|
|
|
|
if socket is not None:
|
|
|
|
self._socket_file = socket
|
|
|
|
else:
|
|
|
|
self.open_socket()
|
|
|
|
if connect:
|
|
|
|
self.connect()
|
|
|
|
self._client.sendall("init\n".encode("utf-8")) # Reinitialize KaTeX Server in case it was reused.
|
|
|
|
self._connected = True
|
|
|
|
else:
|
|
|
|
self._connected = False
|
|
|
|
|
|
|
|
def open_socket(self):
|
|
|
|
# Create temporary directory for socket
|
|
|
|
self._temp_dir = tempfile.TemporaryDirectory(prefix='formatitko')
|
|
|
|
self._socket_file = self._temp_dir.name + "/katex-socket"
|
|
|
|
|
|
|
|
srcdir = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
|
|
|
|
# Test if `node_modules` directory exists and if not, run `npm install`
|
|
|
|
if not os.path.isdir(srcdir + "/katex-server/node_modules"):
|
|
|
|
print("Installing node dependencies for the first time...")
|
|
|
|
try:
|
|
|
|
subprocess.run(["npm", "install"], cwd=srcdir+"/katex-server", check=True)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
if e.returncode == 127:
|
|
|
|
raise NPMNotFoundError("npm not found. Node.js is required to use KaTeX.")
|
|
|
|
else:
|
|
|
|
raise e
|
|
|
|
|
|
|
|
self._server_process = subprocess.Popen(["node", srcdir + "/katex-server/index.mjs", self._socket_file], stdout=subprocess.PIPE)
|
|
|
|
|
|
|
|
ok = self._server_process.stdout.readline()
|
|
|
|
if ok != b"OK\n":
|
|
|
|
raise KatexServerError("Failed to connect to katex-server")
|
|
|
|
|
|
|
|
def connect(self):
|
|
|
|
self._client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
|
|
self._client.connect(self._socket_file)
|
|
|
|
|
|
|
|
def get_socket(self):
|
|
|
|
return self._socket_file
|
|
|
|
|
|
|
|
def render(self, tex: str, options: dict={}):
|
|
|
|
if not self._connected:
|
|
|
|
raise KatexServerError("KatexClient not connected to Katex server. It should be initialized with connect=True.")
|
|
|
|
# Send formulas to translate
|
|
|
|
self._client.sendall((json.dumps({"formulas":[{"tex":tex}], "options":options})+"\n").encode("utf-8"))
|
|
|
|
|
|
|
|
# Receive response
|
|
|
|
data = self._client.recv(4096)
|
|
|
|
while data[-1] != 0x0a:
|
|
|
|
data += self._client.recv(128)
|
|
|
|
response = json.loads(data)
|
|
|
|
|
|
|
|
if "error" in response:
|
|
|
|
raise KatexServerError(response["error"])
|
|
|
|
if "error" in response["results"][0]:
|
|
|
|
raise KatexError(response["results"][0]["error"] + " in $" + tex + "$")
|
|
|
|
else:
|
|
|
|
return response["results"][0]["html"]
|
|
|
|
|
|
|
|
# Special commands implemented in the JS file for grouping defs together.
|
|
|
|
def begingroup(self):
|
|
|
|
if not self._connected:
|
|
|
|
raise KatexServerError("KatexClient not connected to Katex server. It should be initialized with connect=True.")
|
|
|
|
self._client.sendall("begingroup\n".encode("utf-8"))
|
|
|
|
|
|
|
|
def endgroup(self):
|
|
|
|
if not self._connected:
|
|
|
|
raise KatexServerError("KatexClient not connected to Katex server. It should be initialized with connect=True.")
|
|
|
|
self._client.sendall("endgroup\n".encode("utf-8"))
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, type, value, tb):
|
|
|
|
if hasattr(self, "_server_process") and self._server_process is not None:
|
|
|
|
self._server_process.terminate()
|