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; | |
| } | |
| } |
参与讨论