Adding Custom Tag Support to Phabricator

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 :)