Introduction
Phabricator is a great tool for team cooperation and project management, especially in software development. However, by default Phabricator does not support LaTeX formula in its documents. To make it more viable to be used in scientific projects and lab cooperation, I wrote a plugin for it to make it able to render LaTeX using KaTeX library. This plugin is now deployed in production on our lab's instance.
Implementation
The plugin is comprised of two parts: render2katex
and phabritex
. The first is a simple node.js program which calls the KaTeX library to parse LaTeX input. The second one is actually embedded into the Phabricator installation as an extension.
Render2KaTeX
The render2katex
repo is here. It only has a few lines of code:
katex = require('katex');
process.stdin.resume();
process.stdin.setEncoding('utf8');
var dataset = "";
process.stdin.on('data', function(chunk) {
dataset = dataset + chunk;
});
process.stdin.on('end', function() {
display = true;
if(process.argv[2]=="i") display = false;
process.stdout.write(katex.renderToString(dataset,{
displayMode: display,
throwOnError: false
}));
process.exit(0);
});
It basically reads LaTeX input from stdin
and write the results to stdout
.
PhabriTeX
PhabriTeX registers itself as an extension at runtime to hook rendering functions.
Basics of the Phabricator Remarkup Parser
The Phabricator Remarkup parser is modular. This makes writing a custom parser easy. Let's first look at the code structure for inline processing:
<?php
final class PhabricatorInlineKaTeXRemarkupRule
extends PhabricatorRemarkupCustomInlineRule {
public function getPriority() {
return 300.0; // The priority of the rule
}
public function apply($text) {
return preg_replace_callback(
'@{tex\b([^{}]*({[^{}]*?([^{}]|(?R))*[^{}]*?}[^{}]*)*[^{}]*)}@m',
array($this, 'markupNavigation'),
$text); // Matches {tex *} tags using PCRE
} // (?R) is regex recursion (match brackets)
public function markupNavigation(array $matches) {
// Set up a future for execute a shell command
$future =
id(new ExecFuture('node /var/www/render2katex/index.js i'))
->setTimeout(15)
->write(trim($matches[1]));
// $matches[1] is the body (* in {tex *})
// Resolves the future (execution of render2katex)
list($err, $stdout, $stderr) = $future->resolve();
// Error Handling
if ($err) {
return
pht(
'Execution of `%s` failed (#%d), check your syntax: %s',
'render2katex',
$err,
$stderr);
}
$result = $stdout;
$engine = $this->getEngine(); // Get render engine
if ($engine->isTextMode()) {
return $matches[1]; // Just plain text
}
if ($engine->isHTMLMailMode()) {
return phutil_safe_html($result);
}
// Normal mode, return parsed text
return $this->getEngine()->storeText(phutil_safe_html($result));
}
}
Non-inline Parser
For non-inline processor, it is even easier as we don't need to handle the recursion for matching brackets.
<?php
final class PhabricatorRemarkupKaTeXBlockInterpreter
extends PhutilRemarkupBlockInterpreter {
public function getInterpreterName() {
return 'katex';
}
public function markupContent($content, array $argv) {
$future = id(new ExecFuture('node /var/www/render2katex/index.js d'))
->setTimeout(15)
->write(trim($content));
list($err, $stdout, $stderr) = $future->resolve();
if ($err) {
return $this->markupError(
pht(
'Execution of `%s` failed (#%d), check your syntax: %s',
'render2katex',
$err,
$stderr));
}
$result = $stdout;
$engine = $this->getEngine();
if ($engine->isTextMode()) {
return $result;
}
if ($engine->isHTMLMailMode()) {
return $result;
}
return phutil_safe_html($result);
}
}
The same pattern can be used to provide custom rendering for many other types of tags. Feel free to use this as a boilerplate for your own work :)