From 9c852f7be8c082faca61768e8cb492561274f4d3 Mon Sep 17 00:00:00 2001 From: Greenscreener Date: Tue, 20 Feb 2024 18:23:11 +0100 Subject: [PATCH] KaTeX server is now in separate repo. --- .gitmodules | 4 +- katex-server | 1 - src/formatitko/katex-server/.gitignore | 1 + src/formatitko/katex-server/README.md | 1 + src/formatitko/katex-server/index.js | 1 + src/formatitko/katex-server/index.mjs | 131 ++++++++++++++++++ src/formatitko/katex-server/package-lock.json | 42 ++++++ src/formatitko/katex-server/package.json | 17 +++ src/formatitko/katex-server/yarn.lock | 15 ++ src/formatitko/katex.py | 28 ++-- 10 files changed, 226 insertions(+), 15 deletions(-) delete mode 160000 katex-server create mode 100644 src/formatitko/katex-server/.gitignore create mode 100644 src/formatitko/katex-server/README.md create mode 100644 src/formatitko/katex-server/index.js create mode 100755 src/formatitko/katex-server/index.mjs create mode 100644 src/formatitko/katex-server/package-lock.json create mode 100644 src/formatitko/katex-server/package.json create mode 100644 src/formatitko/katex-server/yarn.lock diff --git a/.gitmodules b/.gitmodules index c82be69..4f484b8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "ucwmac"] path = ucwmac url = git://git.ucw.cz/ucwmac.git -[submodule "katex-server"] - path = katex-server +[submodule "src/formatitko/katex-server"] + path = src/formatitko/katex-server url = https://gitea.ks.matfyz.cz/KSP/formatitko-katex-server.git diff --git a/katex-server b/katex-server deleted file mode 160000 index 9a122d9..0000000 --- a/katex-server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9a122d9ee999a4e14f3543fd9f01d38de40a7706 diff --git a/src/formatitko/katex-server/.gitignore b/src/formatitko/katex-server/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/src/formatitko/katex-server/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/src/formatitko/katex-server/README.md b/src/formatitko/katex-server/README.md new file mode 100644 index 0000000..082cd55 --- /dev/null +++ b/src/formatitko/katex-server/README.md @@ -0,0 +1 @@ +This was made by Standa Lukeš @exyi diff --git a/src/formatitko/katex-server/index.js b/src/formatitko/katex-server/index.js new file mode 100644 index 0000000..d6754c8 --- /dev/null +++ b/src/formatitko/katex-server/index.js @@ -0,0 +1 @@ +console.log(require('katex').renderToString('\\frac{2a}{b}')) diff --git a/src/formatitko/katex-server/index.mjs b/src/formatitko/katex-server/index.mjs new file mode 100755 index 0000000..2c1c05c --- /dev/null +++ b/src/formatitko/katex-server/index.mjs @@ -0,0 +1,131 @@ +// KaTeX rendering server +// Listens on unix socket, path is provided as first argument +// Expects JSON lines, each line is a query with the following schema: +// { +// formulas: [ +// { +// tex: string, +// options?: object +// } +// ], +// options?: object +// } + +// see https://katex.org/docs/options.html for list of available options +// If options formulas[].options field is used, the global options field is ignored. + +// For each line, returns one JSON line with the following schema: +// { +// results: [ +// { html?: string } | { error?: string } +// ] +// } | { error?: string } + + +// If one formula is invalid, the error in results is used +// If the entire query is invalid (couldn't parse JSON, for example), the outer error field is used + + +import katex from 'katex' +import net from 'net' +import * as readline from 'readline' + +const myArgs = process.argv.slice(2) + +const unixSocketPath = myArgs[0] +if (!unixSocketPath) { + console.error('you must specify socket path') + process.exit(1) +} + +// This server listens on a Unix socket at /var/run/mysocket +var unixServer = net.createServer(handleClient); +unixServer.listen(unixSocketPath); +console.log("OK") + +function handleExit(signal) { + // unixServer.emit('close') + unixServer.close(function () { + + }); + process.exit(0); // put this into the callback to avoid closing open connections +} +process.on('SIGINT', handleExit); +process.on('SIGQUIT', handleExit); +process.on('SIGTERM', handleExit); +process.on('exit', handleExit); + +const defaultOptions = {} + +/** + * @param {net.Socket} socket + * @returns {Promise} + * */ +function socketWrite(socket, data) { + return new Promise((resolve, reject) => { + socket.write(data, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +/** + * @param {net.Socket} client + * */ +async function handleClient(client) { + const rl = readline.createInterface({ input: client }) + + /* Added by GS: A stack of katex's `macros` objects, each group inherits + * the one from the parent group and can add its own stuff without + * affecting the parent. + */ + let macroStack = [{}] + for await (const line of rl) { + try { + // The custom commands for pushing and popping the macro stack. + if (line === "begingroup") { + // Copy the current state of macros and push it onto the stack. + macroStack.push({...macroStack.slice(-1)[0]}) + continue + } else if (line === "endgroup") { + macroStack.pop() + continue + } else if (line === "init") { + macroStack = [{}] + continue + } + const query = JSON.parse(line) + const results = [] + for (const input of query.formulas) { + const options = input.options ?? query.options ?? defaultOptions + // Add macros from the macros option + if (options.macros) { + for (const macro of Object.keys(options.macros)) { + macroStack.slice(-1)[macro] = options.macros[macro] + } + } + options.macros = macroStack.slice(-1)[0] + // Enforce globalGroup option, katex then saves created macros + // into the options.macros object. + options.globalGroup = true + try { + const html = katex.renderToString(input.tex, options) + results.push({ html }) + } catch (e) { + results.push({ error: String(e) }) + } + } + await socketWrite(client, JSON.stringify({ results }, null, query.debug ? ' ' : undefined)) + await socketWrite(client, '\n') + } catch (e) { + console.error(e) + await socketWrite(client, JSON.stringify({ error: String(e) })) + await socketWrite(client, '\n') + } + } +} + diff --git a/src/formatitko/katex-server/package-lock.json b/src/formatitko/katex-server/package-lock.json new file mode 100644 index 0000000..05246dc --- /dev/null +++ b/src/formatitko/katex-server/package-lock.json @@ -0,0 +1,42 @@ +{ + "name": "formatitko-katex-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "formatitko-katex-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "katex": "^0.16.3" + }, + "bin": { + "formatitko-katex-server": "index.mjs" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/katex": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.3.tgz", + "integrity": "sha512-3EykQddareoRmbtNiNEDgl3IGjryyrp2eg/25fHDEnlHymIDi33bptkMv6K4EOC2LZCybLW/ZkEo6Le+EM9pmA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + } + } +} diff --git a/src/formatitko/katex-server/package.json b/src/formatitko/katex-server/package.json new file mode 100644 index 0000000..adbb8ed --- /dev/null +++ b/src/formatitko/katex-server/package.json @@ -0,0 +1,17 @@ +{ + "name": "formatitko-katex-server", + "version": "1.0.0", + "description": "", + "main": "index.mjs", + "bin": { + "formatitko-katex-server":"index.mjs" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "katex": "^0.16.3" + } +} diff --git a/src/formatitko/katex-server/yarn.lock b/src/formatitko/katex-server/yarn.lock new file mode 100644 index 0000000..2760613 --- /dev/null +++ b/src/formatitko/katex-server/yarn.lock @@ -0,0 +1,15 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +katex@^0.16.3: + version "0.16.9" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.9.tgz#bc62d8f7abfea6e181250f85a56e4ef292dcb1fa" + integrity sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ== + dependencies: + commander "^8.3.0" diff --git a/src/formatitko/katex.py b/src/formatitko/katex.py index 39e521f..f81db24 100644 --- a/src/formatitko/katex.py +++ b/src/formatitko/katex.py @@ -3,6 +3,7 @@ import subprocess import tempfile import json import os +import shutil class KatexError(Exception): pass @@ -20,8 +21,10 @@ class KatexClient: _socket_file: str _temp_dir: tempfile.TemporaryDirectory[str] _connected: bool + _katex_server_path: str - def __init__(self, socket: str=None, connect: bool=True): + def __init__(self, socket: str=None, connect: bool=True, katex_server_path: str=None): + self._katex_server_path = katex_server_path if socket is not None: self._socket_file = socket else: @@ -38,20 +41,21 @@ class KatexClient: self._temp_dir = tempfile.TemporaryDirectory(prefix='formatitko') self._socket_file = self._temp_dir.name + "/katex-socket" - srcdir = os.path.dirname(os.path.realpath(__file__)) + if self._katex_server_path is None: + + 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: + # 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...") + npm = shutil.which("npm") or shutil.which("yarnpkg") + if npm is None: raise NPMNotFoundError("npm not found. Node.js is required to use KaTeX.") - else: - raise e + subprocess.run([npm, "install"], cwd=srcdir+"/katex-server", check=True) + + self._katex_server_path = srcdir + "/katex-server/index.mjs" - self._server_process = subprocess.Popen(["node", srcdir + "/katex-server/index.mjs", self._socket_file], stdout=subprocess.PIPE) + self._server_process = subprocess.Popen(["node", self._katex_server_path, self._socket_file], stdout=subprocess.PIPE) ok = self._server_process.stdout.readline() if ok != b"OK\n":