# MarkdownMessage

Chat-tuned, read-only markdown renderer. Drop-in for any chat bubble — assistant or user.

```tsx
import { MarkdownMessage } from '@djangocfg/ui-tools';

<MarkdownMessage content={text} isUser={false} />
```

## What it gives you

| Feature | Source |
|---|---|
| GitHub Flavored Markdown — tables, strikethrough, autolinks, task lists | `remark-gfm` |
| Single `\n` becomes a hard line break (chat convention) | `remark-breaks` |
| Smart typography — `"…"` → `"…"`, `--` → `—`, `...` → `…` | `remark-smartypants` |
| Emoji shortcodes — `:rocket:` → 🚀 | `remark-emoji` |
| External links auto-tagged with `target="_blank" rel="noopener noreferrer"` | `rehype-external-links` |
| Inline HTML, parsed and sanitized (XSS-safe) | `rehype-raw` + `rehype-sanitize` |
| Syntax-highlighted code fences with copy button | `<PrettyCode>` |
| Mermaid diagrams (` ```mermaid ` fence) with click-to-fullscreen | `<Mermaid>` |
| Custom URL schemes via declarative `linkRules` | `./linkRules.ts` |
| Plain-text fast path — short messages skip the markdown pipeline | `./plainText.ts` |
| Optional collapsible "Read more" for long replies | `useCollapsibleContent` |
| User vs assistant palette via `isUser` flag | semantic theme tokens |

## Why these plugins

Chat bubbles aren't documents. We picked plugins that fix the gap between CommonMark
and how people actually type in chats:

- **`remark-breaks`** — CommonMark collapses single newlines into spaces. LLMs and
  users routinely write joke punchlines, poems, and dialogue separated by single
  newlines. Without this, "— Доктор, я общаюсь только через мессенджер.\n— Что жена
  говорит?" renders as one run-on line. ChatGPT, Slack, Discord, Linear all do this.
- **`remark-smartypants`** — Cheap "humanized" polish: ASCII quotes become typographic
  quotes, double-dash becomes em-dash, three dots become an ellipsis. Same rule as
  Medium / Substack. Applied before emoji so quotes around shortcodes still curl.
- **`remark-emoji`** — Shortcode → Unicode (`:tada:` → 🎉). Already-Unicode emoji
  pass through untouched.
- **`rehype-external-links`** — One pass at the end tags every `<a>` that survived
  sanitize. Replaces ad-hoc `target=_blank` checks in the `a` renderer; works for
  custom `linkRules`-supplied anchors too.

Plugin order in `MarkdownMessage.tsx` is load-bearing — see the inline comment.

## Plain-text fast path

`MarkdownMessage` looks at the content and decides:

- **Short, single-paragraph, no markdown markers** → render as flat
  `<div whitespace-pre-wrap>`. Cheaper, preserves newlines verbatim, doesn't
  parse `*stars*` or `#hash` that the user happened to type.
- **Anything else** → full ReactMarkdown pipeline.

Override with `plainText={true | false}` if you know better than the heuristic
(common case: user-typed bubbles always pass `plainText` so their `*` stays as `*`).

## Custom URL schemes (`linkRules`)

`linkRules` is the supported way to plug in app-specific URL handlers (`cmdop://`,
`obsidian://`, custom file links, mention chips). Each rule:

- declares its `protocols` (whitelisted into the sanitizer);
- optionally `preprocess`es the source string;
- `match`es an href and `render`s a custom node.

```tsx
const rules: LinkRule[] = [
  {
    name: 'cmdop-machine-mention',
    protocols: ['cmdop'],
    match: (href) => href.startsWith('cmdop://machine/'),
    render: ({ href, children }) => <MentionChip id={href} label={children} />,
  },
];

<MarkdownMessage content="Talk to [Server-A](cmdop://machine/abc-123)" linkRules={rules} />
```

The plain `a` renderer keeps its palette and typography; `linkRules` only fires
when both the `protocols` and the `match` predicate accept the href. See
`linkRules.ts` for the sanitize-extension logic.

## Files

| File | Purpose |
|---|---|
| `MarkdownMessage.tsx` | Main component — composes plugins, runs heuristic, renders |
| `components.tsx` | The `Components` map — typography, lists, table, code, blockquote, hr |
| `CodeBlock.tsx` | Code-fence renderer (`PrettyCode`) + plain `<pre>` fallback |
| `linkRules.ts` | Declarative custom-URL primitive — schema & helpers |
| `plainText.ts` | `looksLikePlainProse` heuristic + `extractTextFromChildren` utility |
| `sanitize.ts` | Extended rehype-sanitize schema + `urlTransform` builder |
| `CollapseToggle.tsx` | "Read more / Show less" affordance for long replies |
| `MarkdownMessage.story.tsx` | Storybook — kitchen sink, link rules, mermaid, soft breaks, plugin sampler |

## Stories

Open the storybook and look at:

- `Markdown Message / Link Rules` — declarative `linkRules` API in three modes.
- `Markdown Message / Chat Bubbles` — plain-text fast path vs heuristic vs long reply.
- `Markdown Message / Soft Line Breaks` — the `remark-breaks` regression case (joke,
  poem, dialogue).
- `Markdown Message / Chat Plugins` — emoji shortcodes, smart typography, external
  link tagging, in one bubble.
- `Markdown Message / Kitchen Sink` — every block element together, used to spot
  spacing regressions at a glance.
- `Markdown Message / Mermaid Diagrams` — flowchart + sequence diagram in a bubble.
