Skip to content

Instantly share code, notes, and snippets.

@dpvc
Last active June 30, 2022 10:59
Show Gist options
  • Save dpvc/e34f45cee6fb85dc40ea5a38ecba2754 to your computer and use it in GitHub Desktop.
Save dpvc/e34f45cee6fb85dc40ea5a38ecba2754 to your computer and use it in GitHub Desktop.
Perl Pipe to MathJax

Using Pipes to Run MathJax from Perl

These files illustrate how to open a process running MathJax from perl, using pipes to send TeX commands to be typeset and receveing SVG output (or error messages) back.

  • main.pl is the main file. It calls MathJax::Typeset() to typeset some examples and output the resulting SVG.
  • MathJax.pl implements the MathJax::Typeset() and MathJax::ResetTeX() commands by opening a node.js process that runs MathJax behind the scenes. Perl communicates with this process using pipes: commands are written on one pipe and the results from MathJax are recevied on the other.
  • pipe-handler.js implements a simple handle for the commands that are piped to it from perl. Each line on the pipe should start with a command (the current ones are LINE, TYPESET, RESET and END). The LINE command is used to collect lines of TeX code to be typeset. The TYPESET command causes the previous lines to be typeset and the SVG output returned. The RESET command reset's TeX's label and equation numbering (if automatic numbering is used). The END command ends the MathJax process.
  • mathjax.js configures MathJax and sets up the commands used by the pipe-handler. I kept these separate to isolate the MathJax portion from the pipe handling portion, to make it easier to understand (I hope).
  • package.json is the file used by npm to know what node modules you need.

To install, copy these files to a directory, CD to the directory and use the comand

npm install

to set everything up. Then

perl main.pl

should get you two SVG images from MathJax.

require "./MathJax.pl";
#
# Test that the command works to produce several results
#
print STDOUT MathJax::Typeset("\\frac{x^2}{2}", display => 1), "\n";
print STDOUT "\n";
print STDOUT MathJax::Typeset("E=mc^2\\tag{1}"), "\n";
//
// Minimal CSS needed for stand-alone image
//
const CSS = [
'svg a{fill:blue;stroke:blue}',
'[data-mml-node="merror"]>g{fill:red;stroke:red}',
'[data-mml-node="merror"]>rect[data-background]{fill:yellow;stroke:none}',
'[data-frame],[data-line]{stroke-width:70px;fill:none}',
'.mjx-dashed{stroke-dasharray:140}',
'.mjx-dotted{stroke-linecap:round;stroke-dasharray:0,140}',
'use[data-c]{stroke-width:3px}'
].join('');
//
// Configure MathJax
//
MathJax = {
options: {
enableAssistiveMml: false
},
loader: {
paths: {mathjax: 'mathjax/es5'},
require: require,
load: ['adaptors/liteDOM']
},
svg: {
fontCache: 'local'
},
startup: {
typeset: false
}
}
//
// Load the MathJax startup module
//
require('mathjax/es5/tex-svg.js');
//
// Export a function that returns a promise that resolves when MathJax is ready
//
exports.start = async function() {
return MathJax.startup.promise;
}
//
// Typeset a TeX expression
//
exports.typeset = async function (tex, options) {
return MathJax.tex2svgPromise(tex, options).then(node => {
const svg = MathJax.startup.adaptor.innerHTML(node);
return svg.replace(/<defs>/, `<defs><style>${CSS}</style>`);
});
};
//
// Reset TeX's counters
//
exports.reset = function () {MathJax.texReset()};
use IPC::Open2;
use JSON;
#
# Package provides
#
# my $svg = MathJax->Typeset(tex, options);
# MathJax->ResetTeX;
#
package MathJax;
#
# Open the pipe-handler connected by pipes
#
my $pid = IPC::Open2::open2(\*OPIPE, \*IPIPE, 'node pipe-handler') || die "Can't run pipe-handler: $@";
#
# Wait for MathJax to start
#
my $line = <OPIPE>;
die "Pipe-handler didn't send START command ($line)" unless $line eq "START\n";
#
# Typeset a TeX expression.
#
# Send the TeX to the handler
# Issue the typeset command
# Read the result
# Return the result if it is the SVG output
# Issue a MathJax error if there was one
# Otherwise report that we don't understand the result
#
sub Typeset {
my $tex = shift;
print IPIPE map {"LINE $_\n"} split(/[\n\r]+/g, $tex);
print IPIPE "TYPESET " . JSON::to_json({@_}) . "\n";
my $line = <OPIPE>; chomp($line);
return substr($line, 4) if $line =~ m/^SVG ?/;
die 'MathJax Error: ' . substr($line, 6) if $line =~ m/^ERROR ?/;
die "Unknown command " . (split(/ /, $line))[0];
}
#
# Reset TeX's labels and equation numbers
#
sub ResetTeX {
print IPIPE "RESET\n";
}
{
"dependencies": {
"commander": "^9.3.0",
"mathjax": "^3.2.2",
"wicked-good-xpath": "^1.3.0",
"xmldom-sre": "^0.1.31"
}
}
/*
* Simple shell command to process MathJax HTML snippets
*
* This file first prints 'Start' when it is ready to go,
* then reads from stdin a command: LINE, TYPESET, or END.
* For Typeset, it reads lines until it finds the end marker
* and typesets those lines as SVG. It prints to
* stdout the resulting SVG on lines starting prefixed by
* 'SVG ' followed by an EOF line
* For End, it exits.
*/
const fs = require('fs');
const readline = require('readline');
/*
* The two custom functions from Mathjax
*/
const {start, typeset, reset} = require('./mathjax.js');
/*
* Get a line-by-line interface to stdin and stdout.
*/
const buffer = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
/*
* The lines read for typesetting
*/
let lines = [];
/*
* Read the input a line at a time:
* If it starts with LINE, save the line
* Otherwise if it is TYPESET, typeset the lines
* Otherwise if it is RESET, reset the MathJax labels and line numbers
* Otherwise if it is END, then end the process
* Otherwise produce an error
*/
buffer.on('line', async (line) => {
try {
if (line.substr(0, 5).trim() === 'LINE') lines.push(line.slice(5));
else if (line.substr(0, 8).trim() === 'TYPESET') await Typeset(JSON.parse(line.substr(8)));
else if (line === 'RESET') Reset();
else if (line === 'END') process.exit();
else Error('Unknown command ' + line.split(' ', 1));
} catch (err) {
Error(err.message.replace(/\n/g, '\\n'));
}
});
/*
* To typeset an HTML snippet:
* Call MathJax to do the typesetting and print the result
* Print the end marker
* Clear the line buffer
*/
async function Typeset(options) {
svg = await typeset(lines.join('\n'), options);
console.log('SVG ' + svg);
lines = [];
}
/*
* Reset TeX's labels and equation numbers
*/
function Reset() {
reset();
}
/**
* Produce an error message
*/
function Error(message) {
console.log('ERROR ' + message);
}
/*
* Print the start message when MathJax is ready
*/
start().then(() => console.log('START'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment