Convert Vanilla Forums Rich2 format to HTML

<?php
// For converting Vanilla Forums Rich2 formatted posts to HTML
// Intended for use when migrating away from Vanilla to something like Flarum
// Doesn't capture everything. Isn't particularly elegant.
// It evolved from something else, and I have made minimal effort to clean it up.
// Comes with no express or implied warranty of any kind.
// Always make backups of your data and verify this has done what you expect.
// This is my own work to reverse engineer Rich2
// into somewhat acceptable HTML while migrating an
// online community off their software.
// Sample usage:
/*
require __DIR__ . '/Rich2Formatter.php';
$samplePost = file_get_contents(__DIR__ . '/sample_post.txt');
$formatter = new Rich2Formatter();
$output = $formatter->toHtml($samplePost);
file_put_contents(__DIR__ . '/output.html', $output);
*/
class Rich2Formatter
{
private readonly array $supportedTypes;
private readonly array $supportedEmbedDataSourceTypes;
public function __construct()
{
$this->supportedTypes = [
'@',
'a',
'code_block',
'h2',
'h3',
'h4',
'p',
'rich_embed_card',
'table',
'tbody',
'thead',
'td',
'th',
'tr',
'ul',
'ol',
];
$this->supportedEmbedDataSourceTypes = [
'image',
'url',
];
}
public function toHtml(string $content): string
{
$output = '';
$parts = json_decode($content, associative: true);
foreach ($parts ?? [] as $part)
{
if (! in_array($part['type'], $this->supportedTypes))
{
throw new \Exception("Unable to render section for " . $part['type']);
}
$output .= $this->handlePart($part);
}
return $output;
}
private function handlePart(array $part): string
{
$output = '';
if (isset($part['text']))
{
$output .= $this->handleTextNode($part);
}
elseif (isset($part['type']))
{
if ($part['type'] === 'p')
{
$output .= $this->handleParagraph($part);
}
elseif ($part['type'] === 'rich_embed_card')
{
$output .= $this->handleRichEmbed($part, inline: false);
}
elseif ($part['type'] === 'rich_embed_inline')
{
$output .= $this->handleRichEmbed($part, inline: true);
}
elseif ($part['type'] === 'h2')
{
$output .= $this->handleHeading($part, 2);
}
elseif ($part['type'] === 'h3')
{
$output .= $this->handleHeading($part, 3);
}
elseif ($part['type'] === 'h4')
{
$output .= $this->handleHeading($part, 4);
}
elseif ($part['type'] === 'ol')
{
$output .= $this->handleOrderedList($part);
}
elseif ($part['type'] === 'ul')
{
$output .= $this->handleUnorderedList($part);
}
elseif ($part['type'] === 'a')
{
$output .= $this->handleLink($part);
}
elseif ($part['type'] === 'code_block')
{
$output .= $this->handleCodeBlock($part);
}
elseif ($part['type'] === 'code_line')
{
$output .= $this->handleCodeLine($part);
}
elseif ($part['type'] === '@')
{
$output .= $this->handleUserTag($part);
}
elseif ($part['type'] === 'table')
{
$output .= $this->handleTable($part);
}
elseif ($part['type'] === 'thead')
{
$output .= $this->handleThead($part);
}
elseif ($part['type'] === 'tbody')
{
$output .= $this->handleTbody($part);
}
elseif ($part['type'] === 'td')
{
$output .= $this->handleTd($part);
}
elseif ($part['type'] === 'th')
{
$output .= $this->handleTh($part);
}
elseif ($part['type'] === 'tr')
{
$output .= $this->handleTr($part);
}
else
{
throw new \Exception("Unsupported part " . $part['type']);
}
}
return $output;
}
private function handleTable(array $part): string
{
$output = '';
foreach($part['children'] as $child) {
$output .= $this->handlePart($child);
}
$output .= '</table>';
return $output;
}
private function handleTbody(array $part): string
{
$output = '';
foreach($part['children'] as $child) {
$output .= $this->handlePart($child);
}
$output .= '</tbody>';
return $output;
}
private function handleThead(array $part): string
{
$output = '';
foreach($part['children'] as $child) {
$output .= $this->handlePart($child);
}
$output .= '</thead>';
return $output;
}
private function handleTd(array $part): string
{
$output = '';
foreach($part['children'] as $child) {
$output .= $this->handlePart($child);
}
$output .= '</td>';
return $output;
}
private function handleTr(array $part): string
{
$output = '';
foreach($part['children'] as $child) {
$output .= $this->handlePart($child);
}
$output .= '</tr>';
return $output;
}
private function handleTh(array $part): string
{
$output = '';
foreach($part['children'] as $child) {
$output .= $this->handlePart($child);
}
$output .= '</th>';
return $output;
}
private function handleUserTag(array $part): string
{
return $part['name'];
}
private function handleCodeLine(array $part): string
{
$output = '';
foreach ($part['children'] as $child) {
$output .= $this->handlePart($child) . PHP_EOL;
}
return $output;
}
private function handleCodeBlock(array $part): string
{
$output = '
';
foreach ($part['children'] as $child) {
$output .= $this->handlePart($child) . PHP_EOL;
}
$output .= '</pre>';
return $output;
}
private function handleLink(array $part): string
{
$output = '<a ';
if (isset($part['url']))
{
$output .= 'href="' . $part['url'] . '" ';
}
if (isset($part['target']))
{
$output .= 'target="' . $part['target'] . '" ';
}
$output = trim($output);
$output .= '>';
$output .= $this->handleTextNode($part['children'][0]);
$output .= '</a>';
return $output;
}
private function handleUnorderedList(array $part): string
{
$output = '
foreach ($part['children'] as $listItem)
{
$output .= '
  • ';
  • foreach ($listItem['children'][0]['children'] as $textNode)
    {
    $output .= $this->handleTextNode($textNode);
    }
    $output .= '</li>';
    }
    $output .= '</ul>';
    return $output;
    }
    private function handleOrderedList(array $part): string
    {
    $output = '
    foreach ($part['children'] as $listItem)
    {
    $output .= '
  • ';
  • foreach ($listItem['children'][0]['children'] as $textNode)
    {
    $output .= $this->handleTextNode($textNode);
    }
    $output .= '</li>';
    }
    $output .= '</ol>';
    return $output;
    }
    private function handleTextNode(array $textNode): string
    {
    if (! isset($textNode['text']))
    {
    return $this->handlePart($textNode);
    }
    $text = '';
    if (isset($textNode['bold']) && $textNode['bold'] === true)
    {
    $text .= '';
    }
    if (isset($textNode['italic']) && $textNode['italic'] === true)
    {
    $text .= '';
    }
    $text .= $textNode['text'];
    if (isset($textNode['italic']) && $textNode['italic'] === true)
    {
    $text .= '</em>';
    }
    if (isset($textNode['bold']) && $textNode['bold'] === true)
    {
    $text .= '</strong>';
    }
    return $text;
    }
    private function handleHeading(array $part, int $level): string
    {
    $text = '';
    foreach ($part['children'] as $child)
    {
    $text .= $this->handleTextNode($child);
    }
    return "{$text}";
    }
    private function handleParagraph(array $part): string
    {
    $output = '

    ';

    foreach ($part['children'] as $child)
    {
    $output .= $this->handlePart($child);
    }
    $output .= '</p>';
    return $output;
    }
    private function handleRichEmbed(array $part, bool $inline = false): string
    {
    $output = '';
    if (array_key_exists('error', $part))
    {
    return $output;
    }
    if (! $inline)
    {
    $output .= '';
    }
    if (in_array($part['dataSourceType'], $this->supportedEmbedDataSourceTypes))
    {
    if ($part['dataSourceType'] === 'image')
    {
    $output .= <<<HTML
    <img
    src="{$part['embedData']['url']}"
    data-src="{$part['embedData']['url']}"
    alt="{$part['children'][0]['text']}"
    width="{$part['embedData']['width']}"
    height="{$part['embedData']['height']}"
    />
    HTML;
    }
    elseif ($part['dataSourceType'] === 'url')
    {
    $output .= "";
    foreach ($part['children'] as $child) {
    $output .= $this->handlePart($child);
    }
    $output .= "</a>";
    }
    }
    else
    {
    throw new Exception('Unsupported embed type ' . $part['dataSourceType']);
    }
    if (! $inline)
    {
    $output .= '</div>';
    }
    return $output;
    }
    }
    参与讨论