{$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 = '';
+
+ 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('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()