diff --git a/README.md b/README.md index 328fb05..8945865 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ For platforms without native table support (Telegram, WhatsApp, Slack, Instagram We support the most common Markdown features used in AI responses: headings, code blocks, lists, links, images, blockquotes, horizontal rules, and text formatting. See the Supported Features section for details. ### What about message length limits? -The `TextChunker` intelligently splits long messages at safe breakpoints (after sentences, paragraphs, or list items) while preserving formatting and avoiding broken markup. +The `TextChunker` intelligently splits long messages at safe breakpoints (after sentences, paragraphs, or list items) while preserving formatting and avoiding broken markup. To land on a clean breakpoint, a chunk may exceed the requested `maxLength` by up to 50 characters; budget for that overshoot when choosing your limit (e.g. pass `maxLength: 4046` if you absolutely must stay under Telegram's 4096-char hard cap). ### Can I use this with any PHP version? This library requires PHP 8.3 or higher, taking advantage of modern PHP features like match expressions and readonly properties. diff --git a/src/Parser.php b/src/Parser.php index 9eb45eb..636db72 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -116,7 +116,7 @@ public function parse(string $markdown): IntermediateRepresentation $blockquoteResult = $this->blockquoteParser->parseLine($line); if ($blockquoteResult !== null) { - $ir = $ir->addBlock('blockquote', $blockquoteResult); + $ir = $ir->addBlock('blockquote', $this->processInlineForBlockquote($blockquoteResult)); } continue; @@ -125,7 +125,7 @@ public function parse(string $markdown): IntermediateRepresentation if ($this->blockquoteParser->isInBlockquote()) { $blockquoteResult = $this->blockquoteParser->parseLine($line); if ($blockquoteResult !== null) { - $ir = $ir->addBlock('blockquote', $blockquoteResult); + $ir = $ir->addBlock('blockquote', $this->processInlineForBlockquote($blockquoteResult)); } continue; @@ -174,7 +174,7 @@ public function parse(string $markdown): IntermediateRepresentation if ($this->blockquoteParser->isInBlockquote()) { $blockquoteResult = $this->blockquoteParser->finishBlockquote(); if ($blockquoteResult !== null) { - $ir = $ir->addBlock('blockquote', $blockquoteResult); + $ir = $ir->addBlock('blockquote', $this->processInlineForBlockquote($blockquoteResult)); } $this->blockquoteParser->reset(); } @@ -182,6 +182,23 @@ public function parse(string $markdown): IntermediateRepresentation return $ir; } + private function processInlineForBlockquote(array $blockquoteResult): array + { + $content = $blockquoteResult['content'] ?? ''; + + if ($this->options['parse_links']) { + $content = $this->linkParser->parse($content); + } + + if ($this->options['parse_styles']) { + $content = $this->styleParser->parse($content); + } + + $blockquoteResult['content'] = $content; + + return $blockquoteResult; + } + public function withOptions(array $options): self { $this->options = array_merge($this->options, $options); diff --git a/src/Parsers/LinkParser.php b/src/Parsers/LinkParser.php index 67b296a..daf1980 100644 --- a/src/Parsers/LinkParser.php +++ b/src/Parsers/LinkParser.php @@ -24,6 +24,6 @@ private function formatLink(string $text, string $url): string private function formatImage(string $alt, string $url): string { - return "!{$alt} ({$url})"; + return "{$alt} ({$url})"; } } diff --git a/src/Renderers/AbstractRenderer.php b/src/Renderers/AbstractRenderer.php index 809c26e..5bb08cf 100644 --- a/src/Renderers/AbstractRenderer.php +++ b/src/Renderers/AbstractRenderer.php @@ -25,7 +25,48 @@ public function render(IntermediateRepresentation $ir, ?int $maxLength = null): return $output; } - abstract protected function renderBlock(array $block): string; + protected function renderBlock(array $block): string + { + return match ($block['type']) { + 'paragraph' => $this->renderParagraph($block['content']), + 'header' => $this->renderHeader($block['content'], $block['level'] ?? 1), + 'code' => $this->renderCodeBlock($block['content'], $block['lang'] ?? null), + 'table' => $this->renderTable($block), + 'blockquote' => $this->renderBlockquote($block['content']), + 'horizontal_rule' => $this->renderHorizontalRule(), + default => '', + }; + } + + protected function renderParagraph(string $content): string + { + return $content; + } + + protected function renderHeader(string $content, int $level): string + { + return $content; + } + + protected function renderCodeBlock(string $content, ?string $lang = null): string + { + return $content; + } + + protected function renderTable(array $data): string + { + return ''; + } + + protected function renderBlockquote(string $content): string + { + return "> {$content}"; + } + + protected function renderHorizontalRule(): string + { + return '---'; + } protected function escapeText(string $text): string { diff --git a/src/Renderers/DiscordRenderer.php b/src/Renderers/DiscordRenderer.php index a96b354..9bda759 100644 --- a/src/Renderers/DiscordRenderer.php +++ b/src/Renderers/DiscordRenderer.php @@ -4,19 +4,6 @@ class DiscordRenderer extends AbstractRenderer { - protected function renderBlock(array $block): string - { - return match ($block['type']) { - 'paragraph' => $this->renderParagraph($block['content']), - 'header' => $this->renderHeader($block['content'], $block['level'] ?? 1), - 'code' => $this->renderCodeBlock($block['content'], $block['lang'] ?? null), - 'table' => $this->renderTable($block), - 'blockquote' => $this->renderBlockquote($block['content']), - 'horizontal_rule' => $this->renderHorizontalRule(), - default => '', - }; - } - protected function escapeText(string $text): string { $text = str_replace('\\', '\\\\', $text); @@ -40,8 +27,6 @@ protected function renderParagraph(string $content): string $content = preg_replace_callback('/__HIGHLIGHT__(.+?)__HIGHLIGHT__/', fn ($m) => "**{$m[1]}**", $content); - $content = preg_replace('/!/', '', $content); - $content = $this->convertLinks($content); return $content; @@ -81,7 +66,7 @@ protected function renderTable(array $data): string protected function renderBlockquote(string $content): string { - return "> {$content}"; + return '> '.$this->renderParagraph($content); } protected function renderHorizontalRule(): string diff --git a/src/Renderers/InstagramRenderer.php b/src/Renderers/InstagramRenderer.php index 179153e..7716c75 100644 --- a/src/Renderers/InstagramRenderer.php +++ b/src/Renderers/InstagramRenderer.php @@ -8,19 +8,6 @@ class InstagramRenderer extends AbstractRenderer { private const HORIZONTAL_RULE = '━━━━━━━━━━━━━━━━'; - protected function renderBlock(array $block): string - { - return match ($block['type']) { - 'paragraph' => $this->renderParagraph($block['content']), - 'header' => $this->renderHeader($block['content'], $block['level'] ?? 1), - 'code' => $this->renderCodeBlock($block['content'], $block['lang'] ?? null), - 'table' => $this->renderTable($block), - 'blockquote' => $this->renderBlockquote($block['content']), - 'horizontal_rule' => $this->renderHorizontalRule(), - default => '', - }; - } - protected function renderHeader(string $content, int $level): string { return UnicodeStyler::bold($content); @@ -70,8 +57,6 @@ protected function renderParagraph(string $content): string $content ); - $content = preg_replace('/!/', '', $content); - $content = preg_replace('/-\s+\[x\]\s*(.*)/', '✅ $1', $content); $content = preg_replace('/-\s+\[\s\]\s*(.*)/', '⬜ $1', $content); @@ -109,7 +94,7 @@ protected function renderTable(array $data): string protected function renderBlockquote(string $content): string { - return "❝ {$content} ❞"; + return '❝ '.$this->renderParagraph($content).' ❞'; } protected function renderHorizontalRule(): string diff --git a/src/Renderers/SlackRenderer.php b/src/Renderers/SlackRenderer.php index 156b8ef..4092bcf 100644 --- a/src/Renderers/SlackRenderer.php +++ b/src/Renderers/SlackRenderer.php @@ -4,19 +4,6 @@ class SlackRenderer extends AbstractRenderer { - protected function renderBlock(array $block): string - { - return match ($block['type']) { - 'paragraph' => $this->renderParagraph($block['content']), - 'header' => $this->renderHeader($block['content'], $block['level'] ?? 1), - 'code' => $this->renderCodeBlock($block['content'], $block['lang'] ?? null), - 'table' => $this->renderTable($block), - 'blockquote' => $this->renderBlockquote($block['content']), - 'horizontal_rule' => $this->renderHorizontalRule(), - default => '', - }; - } - protected function escapeText(string $text): string { $text = str_replace('&', '&', $text); @@ -37,8 +24,6 @@ protected function renderParagraph(string $content): string $content = preg_replace_callback('/__HIGHLIGHT__(.+?)__HIGHLIGHT__/', fn ($m) => "*{$m[1]}*", $content); - $content = preg_replace('/!/', '', $content); - $content = $this->convertLinks($content); $content = preg_replace_callback('/~~(.+?)~~/', fn ($m) => "~{$m[1]}~", $content); $content = preg_replace_callback('/\*\*(.+?)\*\*/', fn ($m) => "*{$m[1]}*", $content); @@ -81,7 +66,7 @@ protected function renderTable(array $data): string protected function renderBlockquote(string $content): string { - return "> {$content}"; + return '> '.$this->renderParagraph($content); } protected function renderHorizontalRule(): string diff --git a/src/Renderers/TelegramRenderer.php b/src/Renderers/TelegramRenderer.php index fee78b7..6faac74 100644 --- a/src/Renderers/TelegramRenderer.php +++ b/src/Renderers/TelegramRenderer.php @@ -4,19 +4,6 @@ class TelegramRenderer extends AbstractRenderer { - protected function renderBlock(array $block): string - { - return match ($block['type']) { - 'paragraph' => $this->renderParagraph($block['content']), - 'header' => $this->renderHeader($block['content'], $block['level'] ?? 1), - 'code' => $this->renderCodeBlock($block['content'], $block['lang'] ?? null), - 'table' => $this->renderTable($block), - 'blockquote' => $this->renderBlockquote($block['content']), - 'horizontal_rule' => $this->renderHorizontalRule(), - default => '', - }; - } - protected function escapeText(string $text): string { return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); @@ -42,8 +29,6 @@ protected function renderParagraph(string $content): string $content = preg_replace_callback('/_(.+?)_/', fn ($m) => "{$m[1]}", $content); $content = preg_replace_callback('/`(.+?)`/', fn ($m) => "{$m[1]}", $content); - $content = preg_replace('/!/', '', $content); - return $content; } @@ -81,9 +66,7 @@ protected function renderTable(array $data): string protected function renderBlockquote(string $content): string { - $content = $this->escapeText($content); - - return "💬 {$content}"; + return '💬 '.$this->renderParagraph($content); } protected function renderHorizontalRule(): string diff --git a/src/Renderers/WhatsAppRenderer.php b/src/Renderers/WhatsAppRenderer.php index 2eb3a65..2e235ad 100644 --- a/src/Renderers/WhatsAppRenderer.php +++ b/src/Renderers/WhatsAppRenderer.php @@ -4,19 +4,6 @@ class WhatsAppRenderer extends AbstractRenderer { - protected function renderBlock(array $block): string - { - return match ($block['type']) { - 'paragraph' => $this->renderParagraph($block['content']), - 'header' => $this->renderHeader($block['content'], $block['level'] ?? 1), - 'code' => $this->renderCodeBlock($block['content'], $block['lang'] ?? null), - 'table' => $this->renderTable($block), - 'blockquote' => $this->renderBlockquote($block['content']), - 'horizontal_rule' => $this->renderHorizontalRule(), - default => '', - }; - } - protected function renderHeader(string $content, int $level): string { return "*{$content}*"; @@ -39,8 +26,6 @@ protected function renderParagraph(string $content): string $content = preg_replace_callback('/(.+?) \((https?:\/\/[^\)]+)\)/', fn ($matches) => "{$matches[1]}: {$matches[2]}", $content); - $content = preg_replace('/!/', '', $content); - $content = preg_replace('/-\s+\[x\]\s*(.*)/', '✅ $1', $content); $content = preg_replace('/-\s+\[\s\]\s*(.*)/', '⬜ $1', $content); @@ -72,7 +57,7 @@ protected function renderTable(array $data): string protected function renderBlockquote(string $content): string { - return "💬 {$content}"; + return '💬 '.$this->renderParagraph($content); } protected function renderHorizontalRule(): string diff --git a/src/Support/TextChunker.php b/src/Support/TextChunker.php index cb8b403..15c4baa 100644 --- a/src/Support/TextChunker.php +++ b/src/Support/TextChunker.php @@ -6,6 +6,11 @@ final class TextChunker { private const SAFE_BREAKPOINTS = [' ', "\n", "\t", '.', ',', ';', ':', '-', '!', '?', ')', ']', '}', '"', "'"]; + /** + * Maximum number of characters a chunk is allowed to exceed `maxLength` by + * in order to land on a safe breakpoint instead of mid-word. Callers that + * must respect a hard cap should pass `maxLength = hardCap - 50`. + */ private const MAX_CHUNK_OVERSHOOT = 50; public static function chunk(string $text, int $maxLength, string $encoding = 'UTF-8'): array diff --git a/tests/Feature/MarkdownConverterTest.php b/tests/Feature/MarkdownConverterTest.php index e23f971..0dead00 100644 --- a/tests/Feature/MarkdownConverterTest.php +++ b/tests/Feature/MarkdownConverterTest.php @@ -3,6 +3,7 @@ use Blockshift\ChatMarkdown\MarkdownConverter; use Blockshift\ChatMarkdown\Renderers\TelegramRenderer; use Blockshift\ChatMarkdown\Renderers\WhatsAppRenderer; +use Blockshift\ChatMarkdown\Support\UnicodeStyler; it('can convert markdown to telegram', function () { $markdown = '**Hello** world'; @@ -131,6 +132,70 @@ expect($result)->toContain('https://example.com/image.png'); }); +it('preserves exclamation marks in plain prose for every platform', function () { + $markdown = 'Hello world! This is great!'; + + expect(MarkdownConverter::toTelegram($markdown))->toContain('Hello world!'); + expect(MarkdownConverter::toWhatsApp($markdown))->toContain('Hello world!'); + expect(MarkdownConverter::toDiscord($markdown))->toContain('Hello world!'); + expect(MarkdownConverter::toSlack($markdown))->toContain('Hello world!'); + expect(MarkdownConverter::toInstagram($markdown))->toContain('Hello world!'); +}); + +it('strips exclamation mark from image markdown across platforms', function () { + $markdown = '![Alt](https://example.com/i.png)'; + + expect(MarkdownConverter::toTelegram($markdown))->not->toContain('!'); + expect(MarkdownConverter::toWhatsApp($markdown))->not->toContain('!'); + expect(MarkdownConverter::toDiscord($markdown))->not->toContain('!'); + expect(MarkdownConverter::toSlack($markdown))->not->toContain('!'); + expect(MarkdownConverter::toInstagram($markdown))->not->toContain('!'); +}); + +it('processes inline formatting inside blockquotes for Telegram (HTML)', function () { + $result = MarkdownConverter::toTelegram('> Use **bold** and *italic* inside quotes'); + + expect($result)->toContain('bold'); + expect($result)->toContain('italic'); +}); + +it('processes inline formatting inside blockquotes for WhatsApp', function () { + $result = MarkdownConverter::toWhatsApp('> Use **bold** and *italic* inside quotes'); + + expect($result)->toContain('*bold*'); + expect($result)->toContain('_italic_'); +}); + +it('processes inline formatting inside blockquotes for Discord', function () { + $result = MarkdownConverter::toDiscord('> Use **bold** and `code` inside quotes'); + + expect($result)->toContain('**bold**'); + expect($result)->toContain('`code`'); +}); + +it('processes inline formatting inside blockquotes for Slack', function () { + $result = MarkdownConverter::toSlack('> Use **bold** and ~~strike~~ inside quotes'); + + expect($result)->toContain('*bold*'); + expect($result)->toContain('~strike~'); +}); + +it('converts links inside blockquotes for every platform', function () { + $markdown = '> [link](https://example.com)'; + + expect(MarkdownConverter::toTelegram($markdown))->toContain('link'); + expect(MarkdownConverter::toWhatsApp($markdown))->toContain('link: https://example.com'); + expect(MarkdownConverter::toDiscord($markdown))->toContain('[link](https://example.com)'); + expect(MarkdownConverter::toSlack($markdown))->toContain(''); + expect(MarkdownConverter::toInstagram($markdown))->toContain('link: https://example.com'); +}); + +it('processes inline formatting inside blockquotes for Instagram', function () { + $result = MarkdownConverter::toInstagram('> Quoted **wow**'); + + expect($result)->toContain(UnicodeStyler::bold('wow')); +}); + it('handles escaped characters', function () { $markdown = '\\*not bold\\* and \\_not italic\\_'; $result = MarkdownConverter::toTelegram($markdown); diff --git a/tests/Feature/SnapshotTest.php b/tests/Feature/SnapshotTest.php new file mode 100644 index 0000000..cb54a73 --- /dev/null +++ b/tests/Feature/SnapshotTest.php @@ -0,0 +1,46 @@ +.txt", \ + * MarkdownConverter::to($md));' + */ +function fixture(string $name): string +{ + return file_get_contents(__DIR__.'/../Fixtures/'.$name); +} + +it('renders the LLM document snapshot for Telegram', function () { + expect(MarkdownConverter::toTelegram(fixture('llm-document.md'))) + ->toBe(fixture('llm-document.telegram.txt')); +}); + +it('renders the LLM document snapshot for WhatsApp', function () { + expect(MarkdownConverter::toWhatsApp(fixture('llm-document.md'))) + ->toBe(fixture('llm-document.whatsapp.txt')); +}); + +it('renders the LLM document snapshot for Discord', function () { + expect(MarkdownConverter::toDiscord(fixture('llm-document.md'))) + ->toBe(fixture('llm-document.discord.txt')); +}); + +it('renders the LLM document snapshot for Slack', function () { + expect(MarkdownConverter::toSlack(fixture('llm-document.md'))) + ->toBe(fixture('llm-document.slack.txt')); +}); + +it('renders the LLM document snapshot for Instagram', function () { + expect(MarkdownConverter::toInstagram(fixture('llm-document.md'))) + ->toBe(fixture('llm-document.instagram.txt')); +}); diff --git a/tests/Fixtures/llm-document.discord.txt b/tests/Fixtures/llm-document.discord.txt new file mode 100644 index 0000000..26b6393 --- /dev/null +++ b/tests/Fixtures/llm-document.discord.txt @@ -0,0 +1,43 @@ +**Project Status: Q2 2026** + +**Overview** + +We just shipped the **payments rewrite** and *cut p99 latency* by **58%**. ***Huge win!*** Backend codebase down ~~12,500~~ to 9,800 LOC. + +**Highlights** + +- Migrated to event-sourced ledger +- Replaced `legacy/billing.php` with stateless service +- 99.97% uptime over the rolling 30 days + +**Tasks for next sprint** + +- [x] Land observability dashboards +- [x] Cut over EU region +- [ ] Backfill Q1 reconciliations +- [ ] Author postmortem for the March incident + +**Performance** + +• **Metric**: p50 +• **Before**: 180ms +• **After**: 42ms +• **Metric**: p99 +• **Before**: 2400ms +• **After**: 1010ms + +**Code sample** + +```php +function settle(Invoice $invoice): Receipt +{ + $ledger->record($invoice); + return Receipt::for($invoice); +} +``` + +> Reliability is not a feature — it's a *prerequisite*. + +--- + +[Read the full report at the wiki](https://wiki.example.com/q2-2026)[ and see the dashboard dashboard](https://img.example.com/dash.png). \ No newline at end of file diff --git a/tests/Fixtures/llm-document.instagram.txt b/tests/Fixtures/llm-document.instagram.txt new file mode 100644 index 0000000..d7c6ab5 --- /dev/null +++ b/tests/Fixtures/llm-document.instagram.txt @@ -0,0 +1,43 @@ +𝗣𝗿𝗼𝗷𝗲𝗰𝘁 𝗦𝘁𝗮𝘁𝘂𝘀: 𝗤𝟮 𝟮𝟬𝟮𝟲 + +𝗢𝘃𝗲𝗿𝘃𝗶𝗲𝘄 + +We just shipped the 𝗽𝗮𝘆𝗺𝗲𝗻𝘁𝘀 𝗿𝗲𝘄𝗿𝗶𝘁𝗲 and 𝘤𝘶𝘵 𝘱99 𝘭𝘢𝘵𝘦𝘯𝘤𝘺 by 𝟱𝟴%. 𝙃𝙪𝙜𝙚 𝙬𝙞𝙣! Backend codebase down 1̶2̶,̶5̶0̶0̶ to 9,800 LOC. + +𝗛𝗶𝗴𝗵𝗹𝗶𝗴𝗵𝘁𝘀 + +• Migrated to event-sourced ledger +• Replaced 𝚕𝚎𝚐𝚊𝚌𝚢/𝚋𝚒𝚕𝚕𝚒𝚗𝚐.𝚙𝚑𝚙 with stateless service +• 99.97% uptime over the rolling 30 days + +𝗧𝗮𝘀𝗸𝘀 𝗳𝗼𝗿 𝗻𝗲𝘅𝘁 𝘀𝗽𝗿𝗶𝗻𝘁 + +✅ Land observability dashboards +✅ Cut over EU region +⬜ Backfill Q1 reconciliations +⬜ Author postmortem for the March incident + +𝗣𝗲𝗿𝗳𝗼𝗿𝗺𝗮𝗻𝗰𝗲 + +• 𝗠𝗲𝘁𝗿𝗶𝗰: p50 +• 𝗕𝗲𝗳𝗼𝗿𝗲: 180ms +• 𝗔𝗳𝘁𝗲𝗿: 42ms +• 𝗠𝗲𝘁𝗿𝗶𝗰: p99 +• 𝗕𝗲𝗳𝗼𝗿𝗲: 2400ms +• 𝗔𝗳𝘁𝗲𝗿: 1010ms + +𝗖𝗼𝗱𝗲 𝘀𝗮𝗺𝗽𝗹𝗲 + +━━━━━━━━━━━━━━━━ +𝚏𝚞𝚗𝚌𝚝𝚒𝚘𝚗 𝚜𝚎𝚝𝚝𝚕𝚎(𝙸𝚗𝚟𝚘𝚒𝚌𝚎 $𝚒𝚗𝚟𝚘𝚒𝚌𝚎): 𝚁𝚎𝚌𝚎𝚒𝚙𝚝 +{ + $𝚕𝚎𝚍𝚐𝚎𝚛->𝚛𝚎𝚌𝚘𝚛𝚍($𝚒𝚗𝚟𝚘𝚒𝚌𝚎); + 𝚛𝚎𝚝𝚞𝚛𝚗 𝚁𝚎𝚌𝚎𝚒𝚙𝚝::𝚏𝚘𝚛($𝚒𝚗𝚟𝚘𝚒𝚌𝚎); +} +━━━━━━━━━━━━━━━━ + +❝ Reliability is not a feature — it's a 𝘱𝘳𝘦𝘳𝘦𝘲𝘶𝘪𝘴𝘪𝘵𝘦. ❞ + +━━━━━━━━━━━━━━━━ + +Read the full report at the wiki: https://wiki.example.com/q2-2026 and see the dashboard dashboard: https://img.example.com/dash.png. \ No newline at end of file diff --git a/tests/Fixtures/llm-document.md b/tests/Fixtures/llm-document.md new file mode 100644 index 0000000..063a111 --- /dev/null +++ b/tests/Fixtures/llm-document.md @@ -0,0 +1,41 @@ +# Project Status: Q2 2026 + +## Overview + +We just shipped the **payments rewrite** and *cut p99 latency* by ==58%==. ***Huge win!*** Backend codebase down ~~12,500~~ to 9,800 LOC. + +## Highlights + +- Migrated to event-sourced ledger +- Replaced `legacy/billing.php` with stateless service +- 99.97% uptime over the rolling 30 days + +## Tasks for next sprint + +- [x] Land observability dashboards +- [x] Cut over EU region +- [ ] Backfill Q1 reconciliations +- [ ] Author postmortem for the March incident + +## Performance + +| Metric | Before | After | +| --- | --- | --- | +| p50 | 180ms | 42ms | +| p99 | 2400ms | 1010ms | + +## Code sample + +```php +function settle(Invoice $invoice): Receipt +{ + $ledger->record($invoice); + return Receipt::for($invoice); +} +``` + +> Reliability is not a feature — it's a *prerequisite*. + +--- + +Read the full report at [the wiki](https://wiki.example.com/q2-2026) and see the dashboard ![dashboard](https://img.example.com/dash.png). diff --git a/tests/Fixtures/llm-document.slack.txt b/tests/Fixtures/llm-document.slack.txt new file mode 100644 index 0000000..c93738a --- /dev/null +++ b/tests/Fixtures/llm-document.slack.txt @@ -0,0 +1,43 @@ +*Project Status: Q2 2026* + +*Overview* + +We just shipped the *payments rewrite* and *cut p99 latency* by *58%*. *_Huge win!_* Backend codebase down ~12,500~ to 9,800 LOC. + +*Highlights* + +- Migrated to event-sourced ledger +- Replaced `legacy/billing.php` with stateless service +- 99.97% uptime over the rolling 30 days + +*Tasks for next sprint* + +- [x] Land observability dashboards +- [x] Cut over EU region +- [ ] Backfill Q1 reconciliations +- [ ] Author postmortem for the March incident + +*Performance* + +• *Metric*: p50 +• *Before*: 180ms +• *After*: 42ms +• *Metric*: p99 +• *Before*: 2400ms +• *After*: 1010ms + +*Code sample* + +```php +function settle(Invoice $invoice): Receipt +{ + $ledger->record($invoice); + return Receipt::for($invoice); +} +``` + +> Reliability is not a feature — it's a *prerequisite*. + +--- + +. \ No newline at end of file diff --git a/tests/Fixtures/llm-document.telegram.txt b/tests/Fixtures/llm-document.telegram.txt new file mode 100644 index 0000000..ef46d88 --- /dev/null +++ b/tests/Fixtures/llm-document.telegram.txt @@ -0,0 +1,41 @@ +Project Status: Q2 2026 + +Overview + +We just shipped the payments rewrite and cut p99 latency by 58%. Huge win! Backend codebase down 12,500 to 9,800 LOC. + +Highlights + +- Migrated to event-sourced ledger +- Replaced legacy/billing.php with stateless service +- 99.97% uptime over the rolling 30 days + +Tasks for next sprint + +- [x] Land observability dashboards +- [x] Cut over EU region +- [ ] Backfill Q1 reconciliations +- [ ] Author postmortem for the March incident + +Performance + +• Metric: p50 +• Before: 180ms +• After: 42ms +• Metric: p99 +• Before: 2400ms +• After: 1010ms + +Code sample + +
function settle(Invoice $invoice): Receipt
+{
+    $ledger->record($invoice);
+    return Receipt::for($invoice);
+}
+ +💬 Reliability is not a feature — it's a prerequisite. + +--- + +Read the full report at the wiki and see the dashboard dashboard. \ No newline at end of file diff --git a/tests/Fixtures/llm-document.whatsapp.txt b/tests/Fixtures/llm-document.whatsapp.txt new file mode 100644 index 0000000..3a9f05e --- /dev/null +++ b/tests/Fixtures/llm-document.whatsapp.txt @@ -0,0 +1,43 @@ +*Project Status: Q2 2026* + +*Overview* + +We just shipped the *payments rewrite* and _cut p99 latency_ by *58%*. _*Huge win!*_ Backend codebase down ~12,500~ to 9,800 LOC. + +*Highlights* + +- Migrated to event-sourced ledger +- Replaced `legacy/billing.php` with stateless service +- 99.97% uptime over the rolling 30 days + +*Tasks for next sprint* + +✅ Land observability dashboards +✅ Cut over EU region +⬜ Backfill Q1 reconciliations +⬜ Author postmortem for the March incident + +*Performance* + +• Metric: p50 +• Before: 180ms +• After: 42ms +• Metric: p99 +• Before: 2400ms +• After: 1010ms + +*Code sample* + +``` +function settle(Invoice $invoice): Receipt +{ + $ledger->record($invoice); + return Receipt::for($invoice); +} +``` + +💬 Reliability is not a feature — it's a _prerequisite_. + +--- + +Read the full report at the wiki: https://wiki.example.com/q2-2026 and see the dashboard dashboard: https://img.example.com/dash.png. \ No newline at end of file diff --git a/tests/Unit/Renderers/InstagramRendererTest.php b/tests/Unit/Renderers/InstagramRendererTest.php index 1f4bb9b..98cfbaa 100644 --- a/tests/Unit/Renderers/InstagramRendererTest.php +++ b/tests/Unit/Renderers/InstagramRendererTest.php @@ -82,9 +82,9 @@ expect($result)->toContain('Site: https://example.com'); }); -it('strips exclamation mark from image alt text', function () { +it('renders parser-emitted image as plain link without exclamation mark', function () { $renderer = new InstagramRenderer; - $ir = IntermediateRepresentation::empty()->addBlock('paragraph', ['content' => '!Alt (https://example.com/img.png)']); + $ir = IntermediateRepresentation::empty()->addBlock('paragraph', ['content' => 'Alt (https://example.com/img.png)']); $result = $renderer->render($ir); @@ -92,6 +92,12 @@ expect($result)->not->toContain('!'); }); +it('preserves natural exclamation marks in prose', function () { + $result = MarkdownConverter::toInstagram('Hello world!'); + + expect($result)->toContain('Hello world!'); +}); + it('renders headers as sans-serif bold regardless of level', function () { $renderer = new InstagramRenderer; $ir = IntermediateRepresentation::empty()