diff --git a/app/Filament/Resources/SupportTicketResource.php b/app/Filament/Resources/SupportTicketResource.php
index 6c67f703..66aec521 100644
--- a/app/Filament/Resources/SupportTicketResource.php
+++ b/app/Filament/Resources/SupportTicketResource.php
@@ -13,6 +13,7 @@
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;
+use Illuminate\Support\Str;
class SupportTicketResource extends Resource
{
@@ -77,7 +78,10 @@ public static function infolist(Schema $schema): Schema
->label('Subject'),
Infolists\Components\TextEntry::make('message')
->label('Message')
- ->markdown(),
+ ->formatStateUsing(fn (?string $state): ?HtmlString => $state === null
+ ? null
+ : new HtmlString(self::renderTicketMessage($state)))
+ ->html(),
Infolists\Components\TextEntry::make('attachments')
->label('Attachments')
->formatStateUsing(function (SupportTicket $record): HtmlString {
@@ -168,6 +172,127 @@ public static function getRelations(): array
return [];
}
+ public static function renderTicketMessage(string $message): string
+ {
+ $html = Str::markdown(self::convertAsciiTablesToHtml($message), [
+ 'renderer' => [
+ 'soft_break' => "
\n",
+ ],
+ ]);
+
+ return str_replace('
', '
', $html); + } + + protected static function convertAsciiTablesToHtml(string $message): string + { + $lines = preg_split('/\R/', $message) ?: []; + $result = []; + $buffer = []; + + $flush = function () use (&$result, &$buffer): void { + if ($buffer === []) { + return; + } + + $rendered = self::renderAsciiTable($buffer); + + if ($rendered === null) { + foreach ($buffer as $bufferedLine) { + $result[] = $bufferedLine; + } + } else { + $result[] = ''; + $result[] = $rendered; + $result[] = ''; + } + + $buffer = []; + }; + + foreach ($lines as $line) { + if (preg_match('/^\s*[+|]/', $line)) { + $buffer[] = $line; + + continue; + } + + $flush(); + $result[] = $line; + } + + $flush(); + + return implode("\n", $result); + } + + protected static function renderAsciiTable(array $lines): ?string + { + $rows = []; + $separatorAfterRow = []; + + foreach ($lines as $line) { + $trimmed = ltrim($line); + + if (str_starts_with($trimmed, '+')) { + $separatorAfterRow[count($rows)] = true; + + continue; + } + + if (str_starts_with($trimmed, '|')) { + $rows[] = self::splitAsciiTableRow($trimmed); + } + } + + if ($rows === []) { + return null; + } + + $hasHeader = count($rows) > 1 && isset($separatorAfterRow[1]); + + $tableStyle = 'border-collapse: collapse; width: auto; margin: 0 0 1rem 0; border: 1px solid rgba(127, 127, 127, 0.25);'; + $cellStyle = 'padding: 0.25rem 0.75rem; border: 1px solid rgba(127, 127, 127, 0.2); text-align: left; vertical-align: top;'; + $headerCellStyle = $cellStyle.' font-weight: 600; background: rgba(127, 127, 127, 0.12);'; + $stripeStyle = 'background: rgba(127, 127, 127, 0.06);'; + + $html = '
| '.e($cell).' | '; + } + $html .= '
|---|
| '.e($cell).' | '; + } + $html .= '
First paragraph
', $html); + $this->assertStringContainsString('Second paragraph
', $html); + } + + #[Test] + public function render_ticket_message_converts_single_newlines_to_line_breaks(): void + { + $message = "Line one\nLine two"; + + $html = SupportTicketResource::renderTicketMessage($message); + + $this->assertStringContainsString("Line one