#!/usr/bin/env node
// 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 < void > }
* * /
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 } )
/ * A d d e d b y G S : A s t a c k o f k a t e x ' s ` m a c r o s ` o b j e c t s , e a c h g r o u p i n h e r i t s
* 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 // Because I can't be arsed to update all nodejs installations in existence just for this single line
// 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' )
}
}
}