// 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); 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. */ const 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 } 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') } } }