Full Code of lukilabs/beautiful-mermaid for AI

main 65f4e0ab8c28 cached
196 files
1.5 MB
469.3k tokens
555 symbols
1 requests
Download .txt
Showing preview only (1,598K chars total). Download the full file or copy to clipboard to get everything.
Repository: lukilabs/beautiful-mermaid
Branch: main
Commit: 65f4e0ab8c28
Files: 196
Total size: 1.5 MB

Directory structure:
gitextract_riz8wh68/

├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── bench.ts
├── dev.ts
├── index.ts
├── package.json
├── samples-data.ts
├── src/
│   ├── __tests__/
│   │   ├── ascii-edge-styles.test.ts
│   │   ├── ascii-multiline.test.ts
│   │   ├── ascii.test.ts
│   │   ├── class-arrow-directions.test.ts
│   │   ├── class-integration.test.ts
│   │   ├── class-parser.test.ts
│   │   ├── edge-approach-direction.test.ts
│   │   ├── er-integration.test.ts
│   │   ├── er-parser.test.ts
│   │   ├── integration.test.ts
│   │   ├── layout-disconnected.test.ts
│   │   ├── linkstyle.test.ts
│   │   ├── multiline-labels.test.ts
│   │   ├── parser.test.ts
│   │   ├── renderer.test.ts
│   │   ├── sequence-integration.test.ts
│   │   ├── sequence-layout.test.ts
│   │   ├── sequence-parser.test.ts
│   │   ├── styles.test.ts
│   │   ├── testdata/
│   │   │   ├── ascii/
│   │   │   │   ├── ampersand_lhs.txt
│   │   │   │   ├── ampersand_lhs_and_rhs.txt
│   │   │   │   ├── ampersand_rhs.txt
│   │   │   │   ├── ampersand_td_fanin.txt
│   │   │   │   ├── ampersand_td_fanout.txt
│   │   │   │   ├── ampersand_without_edge.txt
│   │   │   │   ├── back_reference_from_child.txt
│   │   │   │   ├── backlink_from_bottom.txt
│   │   │   │   ├── backlink_from_top.txt
│   │   │   │   ├── backlink_with_short_y_padding.txt
│   │   │   │   ├── cls_all_relationships.txt
│   │   │   │   ├── cls_annotation.txt
│   │   │   │   ├── cls_association.txt
│   │   │   │   ├── cls_basic.txt
│   │   │   │   ├── cls_dependency.txt
│   │   │   │   ├── cls_inheritance.txt
│   │   │   │   ├── cls_methods.txt
│   │   │   │   ├── comments.txt
│   │   │   │   ├── custom_padding.txt
│   │   │   │   ├── duplicate_labels.txt
│   │   │   │   ├── er_attributes.txt
│   │   │   │   ├── er_basic.txt
│   │   │   │   ├── er_identifying.txt
│   │   │   │   ├── flowchart_tb_simple.txt
│   │   │   │   ├── graph_bt_direction.txt
│   │   │   │   ├── graph_tb_direction.txt
│   │   │   │   ├── nested_subgraphs_with_labels.txt
│   │   │   │   ├── preserve_order_of_definition.txt
│   │   │   │   ├── self_reference.txt
│   │   │   │   ├── self_reference_with_edge.txt
│   │   │   │   ├── seq_basic.txt
│   │   │   │   ├── seq_multiple_messages.txt
│   │   │   │   ├── seq_self_message.txt
│   │   │   │   ├── single_node.txt
│   │   │   │   ├── single_node_longer_name.txt
│   │   │   │   ├── subgraph_complex_mixed.txt
│   │   │   │   ├── subgraph_complex_nested.txt
│   │   │   │   ├── subgraph_direction_override.txt
│   │   │   │   ├── subgraph_empty.txt
│   │   │   │   ├── subgraph_mixed_nodes.txt
│   │   │   │   ├── subgraph_mixed_nodes_td.txt
│   │   │   │   ├── subgraph_multiple_edges.txt
│   │   │   │   ├── subgraph_multiple_nodes.txt
│   │   │   │   ├── subgraph_nested.txt
│   │   │   │   ├── subgraph_nested_with_external.txt
│   │   │   │   ├── subgraph_node_outside_lr.txt
│   │   │   │   ├── subgraph_single_node.txt
│   │   │   │   ├── subgraph_td_direction.txt
│   │   │   │   ├── subgraph_td_multiple.txt
│   │   │   │   ├── subgraph_td_multiple_paddingy.txt
│   │   │   │   ├── subgraph_three_levels_nested.txt
│   │   │   │   ├── subgraph_three_separate.txt
│   │   │   │   ├── subgraph_two_separate.txt
│   │   │   │   ├── subgraph_with_labels.txt
│   │   │   │   ├── three_nodes.txt
│   │   │   │   ├── three_nodes_single_line.txt
│   │   │   │   ├── two_layer_single_graph.txt
│   │   │   │   ├── two_layer_single_graph_longer_names.txt
│   │   │   │   ├── two_nodes_linked.txt
│   │   │   │   ├── two_nodes_longer_names.txt
│   │   │   │   ├── two_root_nodes.txt
│   │   │   │   ├── two_root_nodes_longer_names.txt
│   │   │   │   └── two_single_root_nodes.txt
│   │   │   └── unicode/
│   │   │       ├── ampersand_lhs.txt
│   │   │       ├── ampersand_lhs_and_rhs.txt
│   │   │       ├── ampersand_rhs.txt
│   │   │       ├── ampersand_without_edge.txt
│   │   │       ├── back_reference_from_child.txt
│   │   │       ├── backlink_from_bottom.txt
│   │   │       ├── backlink_from_top.txt
│   │   │       ├── cls_all_relationships.txt
│   │   │       ├── cls_annotation.txt
│   │   │       ├── cls_association.txt
│   │   │       ├── cls_basic.txt
│   │   │       ├── cls_dependency.txt
│   │   │       ├── cls_inheritance.txt
│   │   │       ├── cls_methods.txt
│   │   │       ├── comments.txt
│   │   │       ├── duplicate_labels.txt
│   │   │       ├── er_attributes.txt
│   │   │       ├── er_basic.txt
│   │   │       ├── er_identifying.txt
│   │   │       ├── graph_bt_direction.txt
│   │   │       ├── preserve_order_of_definition.txt
│   │   │       ├── self_reference.txt
│   │   │       ├── self_reference_with_edge.txt
│   │   │       ├── seq_basic.txt
│   │   │       ├── seq_multiple_messages.txt
│   │   │       ├── seq_self_message.txt
│   │   │       ├── single_node.txt
│   │   │       ├── single_node_longer_name.txt
│   │   │       ├── three_nodes.txt
│   │   │       ├── three_nodes_single_line.txt
│   │   │       ├── two_layer_single_graph.txt
│   │   │       ├── two_layer_single_graph_longer_names.txt
│   │   │       ├── two_nodes_linked.txt
│   │   │       ├── two_nodes_longer_names.txt
│   │   │       ├── two_root_nodes.txt
│   │   │       ├── two_root_nodes_longer_names.txt
│   │   │       └── two_single_root_nodes.txt
│   │   ├── text-metrics.test.ts
│   │   ├── xychart-ascii.test.ts
│   │   └── xychart-integration.test.ts
│   ├── ascii/
│   │   ├── ansi.ts
│   │   ├── canvas.ts
│   │   ├── class-diagram.ts
│   │   ├── converter.ts
│   │   ├── draw.ts
│   │   ├── edge-bundling.ts
│   │   ├── edge-routing.ts
│   │   ├── er-diagram.ts
│   │   ├── grid.ts
│   │   ├── index.ts
│   │   ├── multiline-utils.ts
│   │   ├── pathfinder.ts
│   │   ├── sequence.ts
│   │   ├── shapes/
│   │   │   ├── circle.ts
│   │   │   ├── corners.ts
│   │   │   ├── diamond.ts
│   │   │   ├── hexagon.ts
│   │   │   ├── index.ts
│   │   │   ├── rectangle.ts
│   │   │   ├── rounded.ts
│   │   │   ├── special.ts
│   │   │   ├── stadium.ts
│   │   │   ├── state.ts
│   │   │   └── types.ts
│   │   ├── types.ts
│   │   ├── validate.ts
│   │   └── xychart.ts
│   ├── browser.ts
│   ├── class/
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   ├── renderer.ts
│   │   └── types.ts
│   ├── elk-instance.ts
│   ├── er/
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   ├── renderer.ts
│   │   └── types.ts
│   ├── index.ts
│   ├── layout-engine.ts
│   ├── layout.ts
│   ├── multiline-utils.ts
│   ├── parser.ts
│   ├── renderer.ts
│   ├── sequence/
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   ├── renderer.ts
│   │   └── types.ts
│   ├── shape-clipping.ts
│   ├── styles.ts
│   ├── text-metrics.ts
│   ├── theme.ts
│   ├── types.ts
│   └── xychart/
│       ├── colors.ts
│       ├── layout.ts
│       ├── parser.ts
│       ├── renderer.ts
│       └── types.ts
├── tsconfig.json
├── tsup.config.ts
├── wrangler.toml
├── xychart-design.md
├── xychart-samples-data.ts
├── xychart-test.html
└── xychart-test.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Install dependencies
        run: bun install

      - name: Run tests
        run: bun test

      - name: Type check
        run: bun x tsc --noEmit


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to npm

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Setup Node.js (for npm publish)
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: bun install

      - name: Run tests
        run: bun test

      - name: Publish to npm
        run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}


================================================
FILE: .gitignore
================================================
# Dependencies
node_modules/

# Build output
dist/
site/
*.tsbuildinfo

# Generated files
index.html

# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store

# Logs
*.log
npm-debug.log*

# Environment
.env
.env.local
.env.*.local

# Test coverage
coverage/


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2026 Craft Docs

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<div align="center">

# beautiful-mermaid

**Render Mermaid diagrams as beautiful SVGs or ASCII art**

Ultra-fast, fully themeable, zero DOM dependencies. Built for the AI era.

![beautiful-mermaid sequence diagram example](hero.png)

[![npm version](https://img.shields.io/npm/v/beautiful-mermaid.svg)](https://www.npmjs.com/package/beautiful-mermaid)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

[**Live Demo & Samples**](https://agents.craft.do/mermaid)

**[→ Use it live in Craft Agents](https://agents.craft.do)**

</div>

---

## Why We Built This

Diagrams are essential for AI-assisted programming. When you're working with an AI coding assistant, being able to visualize data flows, state machines, and system architecture—directly in your terminal or chat interface—makes complex concepts instantly graspable.

[Mermaid](https://mermaid.js.org/) is the de facto standard for text-based diagrams. It's brilliant. But the default renderer has problems:

- **Aesthetics** — Might be personal preference, but wished they looked more professional
- **Complex theming** — Customizing colors requires wrestling with CSS classes
- **No terminal output** — Can't render to ASCII for CLI tools
- **Heavy dependencies** — Pulls in a lot of code for simple diagrams

We built `beautiful-mermaid` at [Craft](https://craft.do) to power diagrams in [Craft Agents](https://agents.craft.do). It's fast, beautiful, and works everywhere—from rich UIs to plain terminals.


The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff. We ported it from Go to TypeScript and extended it. Thank you Alexander for the excellent foundation! (And inspiration that this was possible.)

## Features

- **6 diagram types** — Flowcharts, State, Sequence, Class, ER, and XY Charts (bar, line, combined)
- **Dual output** — SVG for rich UIs, ASCII/Unicode for terminals
- **Synchronous rendering** — No async, no flash. Works with React `useMemo()`
- **15 built-in themes** — And dead simple to add your own
- **Full Shiki compatibility** — Use any VS Code theme directly
- **Live theme switching** — CSS custom properties, no re-render needed
- **Mono mode** — Beautiful diagrams from just 2 colors
- **Zero DOM dependencies** — Pure TypeScript, works everywhere
- **Ultra-fast** — Renders 100+ diagrams in under 500ms

## Installation

```bash
npm install beautiful-mermaid
# or
bun add beautiful-mermaid
# or
pnpm add beautiful-mermaid
```

## Quick Start

### SVG Output

```typescript
import { renderMermaidSVG } from 'beautiful-mermaid'

const svg = renderMermaidSVG(`
  graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[Action]
    B -->|No| D[End]
`)
```

Rendering is **fully synchronous** — no `await`, no promises. The ELK.js layout engine runs synchronously via a FakeWorker bypass, so you get your SVG string instantly.

Need async? Use `renderMermaidSVGAsync()` — same output, returns a `Promise<string>`.

### ASCII Output

```typescript
import { renderMermaidASCII } from 'beautiful-mermaid'

const ascii = renderMermaidASCII(`graph LR; A --> B --> C`)
```

```
┌───┐     ┌───┐     ┌───┐
│   │     │   │     │   │
│ A │────►│ B │────►│ C │
│   │     │   │     │   │
└───┘     └───┘     └───┘
```

---

## React Integration

Because rendering is synchronous, you can use `useMemo()` for zero-flash diagram rendering:

```tsx
import { renderMermaidSVG } from 'beautiful-mermaid'

function MermaidDiagram({ code }: { code: string }) {
  const { svg, error } = React.useMemo(() => {
    try {
      return {
        svg: renderMermaidSVG(code, {
          bg: 'var(--background)',
          fg: 'var(--foreground)',
          transparent: true,
        }),
        error: null,
      }
    } catch (err) {
      return { svg: null, error: err instanceof Error ? err : new Error(String(err)) }
    }
  }, [code])

  if (error) return <pre>{error.message}</pre>
  return <div dangerouslySetInnerHTML={{ __html: svg! }} />
}
```

**Why this works well:**
- **No flash** — SVG is computed synchronously during render, not in a useEffect
- **CSS variables** — Pass `var(--background)` etc. instead of hex colors. The SVG inherits from your app's CSS, so theme switches apply instantly without re-rendering
- **Memoized** — Only re-renders when `code` changes

---

## Theming

The theming system is the heart of `beautiful-mermaid`. It's designed to be both powerful and dead simple.

### The Two-Color Foundation

Every diagram needs just two colors: **background** (`bg`) and **foreground** (`fg`). That's it. From these two colors, the entire diagram is derived using `color-mix()`:

```typescript
const svg = renderMermaidSVG(diagram, {
  bg: '#1a1b26',  // Background
  fg: '#a9b1d6',  // Foreground
})
```

This is **Mono Mode**—a coherent, beautiful diagram from just two colors. The system automatically derives:

| Element | Derivation |
|---------|------------|
| Text | `--fg` at 100% |
| Secondary text | `--fg` at 60% into `--bg` |
| Edge labels | `--fg` at 40% into `--bg` |
| Faint text | `--fg` at 25% into `--bg` |
| Connectors | `--fg` at 50% into `--bg` |
| Arrow heads | `--fg` at 85% into `--bg` |
| Node fill | `--fg` at 3% into `--bg` |
| Group header | `--fg` at 5% into `--bg` |
| Inner strokes | `--fg` at 12% into `--bg` |
| Node stroke | `--fg` at 20% into `--bg` |

### Enriched Mode

For richer themes, you can provide optional "enrichment" colors that override specific derivations:

```typescript
const svg = renderMermaidSVG(diagram, {
  bg: '#1a1b26',
  fg: '#a9b1d6',
  // Optional enrichment:
  line: '#3d59a1',    // Edge/connector color
  accent: '#7aa2f7',  // Arrow heads, highlights
  muted: '#565f89',   // Secondary text, labels
  surface: '#292e42', // Node fill tint
  border: '#3d59a1',  // Node stroke
})
```

If an enrichment color isn't provided, it falls back to the `color-mix()` derivation. This means you can provide just the colors you care about.

### CSS Custom Properties = Live Switching

All colors are CSS custom properties on the `<svg>` element. This means you can switch themes instantly without re-rendering:

```javascript
// Switch theme by updating CSS variables
svg.style.setProperty('--bg', '#282a36')
svg.style.setProperty('--fg', '#f8f8f2')
// The entire diagram updates immediately
```

For React apps, pass CSS variable references instead of hex values:

```typescript
const svg = renderMermaidSVG(diagram, {
  bg: 'var(--background)',
  fg: 'var(--foreground)',
  accent: 'var(--accent)',
  transparent: true,
})
// Theme switches apply automatically via CSS cascade — no re-render needed
```

### Built-in Themes

15 carefully curated themes ship out of the box:

| Theme | Type | Background | Accent |
|-------|------|------------|--------|
| `zinc-light` | Light | `#FFFFFF` | Derived |
| `zinc-dark` | Dark | `#18181B` | Derived |
| `tokyo-night` | Dark | `#1a1b26` | `#7aa2f7` |
| `tokyo-night-storm` | Dark | `#24283b` | `#7aa2f7` |
| `tokyo-night-light` | Light | `#d5d6db` | `#34548a` |
| `catppuccin-mocha` | Dark | `#1e1e2e` | `#cba6f7` |
| `catppuccin-latte` | Light | `#eff1f5` | `#8839ef` |
| `nord` | Dark | `#2e3440` | `#88c0d0` |
| `nord-light` | Light | `#eceff4` | `#5e81ac` |
| `dracula` | Dark | `#282a36` | `#bd93f9` |
| `github-light` | Light | `#ffffff` | `#0969da` |
| `github-dark` | Dark | `#0d1117` | `#4493f8` |
| `solarized-light` | Light | `#fdf6e3` | `#268bd2` |
| `solarized-dark` | Dark | `#002b36` | `#268bd2` |
| `one-dark` | Dark | `#282c34` | `#c678dd` |

```typescript
import { renderMermaidSVG, THEMES } from 'beautiful-mermaid'

const svg = renderMermaidSVG(diagram, THEMES['tokyo-night'])
```

### Adding Your Own Theme

Creating a theme is trivial. At minimum, just provide `bg` and `fg`:

```typescript
const myTheme = {
  bg: '#0f0f0f',
  fg: '#e0e0e0',
}

const svg = renderMermaidSVG(diagram, myTheme)
```

Want richer colors? Add any of the optional enrichments:

```typescript
const myRichTheme = {
  bg: '#0f0f0f',
  fg: '#e0e0e0',
  accent: '#ff6b6b',  // Pop of color for arrows
  muted: '#666666',   // Subdued labels
}
```

### Full Shiki Compatibility

Use **any VS Code theme** directly via Shiki integration. This gives you access to hundreds of community themes:

```typescript
import { getSingletonHighlighter } from 'shiki'
import { renderMermaidSVG, fromShikiTheme } from 'beautiful-mermaid'

// Load any theme from Shiki's registry
const highlighter = await getSingletonHighlighter({
  themes: ['vitesse-dark', 'rose-pine', 'material-theme-darker']
})

// Extract diagram colors from the theme
const colors = fromShikiTheme(highlighter.getTheme('vitesse-dark'))

const svg = renderMermaidSVG(diagram, colors)
```

The `fromShikiTheme()` function intelligently maps VS Code editor colors to diagram roles:

| Editor Color | Diagram Role |
|--------------|--------------|
| `editor.background` | `bg` |
| `editor.foreground` | `fg` |
| `editorLineNumber.foreground` | `line` |
| `focusBorder` / keyword token | `accent` |
| comment token | `muted` |
| `editor.selectionBackground` | `surface` |
| `editorWidget.border` | `border` |

---

## Supported Diagrams

### Flowcharts

```
graph TD
  A[Start] --> B{Decision}
  B -->|Yes| C[Process]
  B -->|No| D[End]
  C --> D
```

All directions supported: `TD` (top-down), `LR` (left-right), `BT` (bottom-top), `RL` (right-left).

### State Diagrams

```
stateDiagram-v2
  [*] --> Idle
  Idle --> Processing: start
  Processing --> Complete: done
  Complete --> [*]
```

### Sequence Diagrams

```
sequenceDiagram
  Alice->>Bob: Hello Bob!
  Bob-->>Alice: Hi Alice!
  Alice->>Bob: How are you?
  Bob-->>Alice: Great, thanks!
```

### Class Diagrams

```
classDiagram
  Animal <|-- Duck
  Animal <|-- Fish
  Animal: +int age
  Animal: +String gender
  Animal: +isMammal() bool
  Duck: +String beakColor
  Duck: +swim()
  Duck: +quack()
```

### ER Diagrams

```
erDiagram
  CUSTOMER ||--o{ ORDER : places
  ORDER ||--|{ LINE_ITEM : contains
  PRODUCT ||--o{ LINE_ITEM : "is in"
```

### Inline Edge Styling

Use `linkStyle` to override edge colors and stroke widths — just like [Mermaid's linkStyle](https://mermaid.js.org/syntax/flowchart.html#styling-links):

```
graph TD
  A --> B --> C
  linkStyle 0 stroke:#ff0000,stroke-width:2px
  linkStyle default stroke:#888888
```

|             Syntax              |                 Effect                 |
| ------------------------------- | -------------------------------------- |
| `linkStyle 0 stroke:#f00`       | Style a single edge by index (0-based) |
| `linkStyle 0,2 stroke:#f00`     | Style multiple edges at once           |
| `linkStyle default stroke:#888` | Default style applied to all edges     |

Index-specific styles override the default. Supported properties: `stroke`, `stroke-width`.

Works in both flowcharts and state diagrams.

### XY Charts

Bar charts, line charts, and combinations — using Mermaid's `xychart-beta` syntax.

**Bar chart:**

```
xychart-beta
    title "Monthly Revenue"
    x-axis [Jan, Feb, Mar, Apr, May, Jun]
    y-axis "Revenue ($K)" 0 --> 500
    bar [180, 250, 310, 280, 350, 420]
```

**Line chart:**

```
xychart-beta
    title "User Growth"
    x-axis [Jan, Feb, Mar, Apr, May, Jun]
    line [1200, 1800, 2500, 3100, 3800, 4500]
```

**Combined bar + line:**

```
xychart-beta
    title "Sales with Trend"
    x-axis [Jan, Feb, Mar, Apr, May, Jun]
    bar [300, 380, 280, 450, 350, 520]
    line [300, 330, 320, 353, 352, 395]
```

**Horizontal orientation:**

```
xychart-beta horizontal
    title "Language Popularity"
    x-axis [Python, JavaScript, Java, Go, Rust]
    bar [30, 25, 20, 12, 8]
```

**Axis configuration:**

- Categorical x-axis: `x-axis [A, B, C]`
- Numeric x-axis range: `x-axis 0 --> 100`
- Axis titles: `x-axis "Category" [A, B, C]`
- Y-axis range: `y-axis "Score" 0 --> 100`

**Multi-series:** Add multiple `bar` and/or `line` declarations. Each series gets a distinct color from a monochromatic palette derived from the theme's accent color.

### XY Chart Styling

The chart renderer follows a clean, minimal design philosophy inspired by Apple and Craft:

- **Dot grid** — A subtle dot pattern fills the plot area instead of traditional solid grid lines
- **Rounded bars** — All bar corners are rounded for a modern, polished look
- **Smooth curves** — Line series use natural cubic spline interpolation, producing mathematically smooth curves through all data points (not straight segments or staircase steps)
- **Floating labels** — No visible axis lines or tick marks; labels float freely for a clutter-free aesthetic
- **Drop-shadow lines** — Each line series has a subtle shadow beneath it for depth
- **Monochromatic palette** — Series 0 uses the theme's accent color; additional series get darker/lighter shades of the same hue with subtle hue drift, adapting automatically to light or dark backgrounds
- **Interactive tooltips** — When rendered with `interactive: true`, hovering over bars or data points shows value tooltips. Multi-line tooltips appear when multiple series share an x-position
- **Sparse line dots** — Lines with 12 or fewer data points show data point dots by default for readability
- **Full theme support** — All 15 built-in themes (and custom themes) apply to charts. The accent color drives the entire series color palette
- **Live theme switching** — Chart series colors are CSS custom properties (`--xychart-color-N`), so theme changes apply instantly without re-rendering

---

## ASCII Output

For terminal environments, CLI tools, or anywhere you need plain text, render to ASCII or Unicode box-drawing characters:

```typescript
import { renderMermaidASCII } from 'beautiful-mermaid'

// Unicode mode (default) — prettier box drawing
const unicode = renderMermaidASCII(`graph LR; A --> B`)

// Pure ASCII mode — maximum compatibility
const ascii = renderMermaidASCII(`graph LR; A --> B`, { useAscii: true })
```

**Unicode output:**
```
┌───┐     ┌───┐
│   │     │   │
│ A │────►│ B │
│   │     │   │
└───┘     └───┘
```

**ASCII output:**
```
+---+     +---+
|   |     |   |
| A |---->| B |
|   |     |   |
+---+     +---+
```

### ASCII Options

```typescript
renderMermaidASCII(diagram, {
  useAscii: false,      // true = ASCII, false = Unicode (default)
  paddingX: 5,          // Horizontal spacing between nodes
  paddingY: 5,          // Vertical spacing between nodes
  boxBorderPadding: 1,  // Padding inside node boxes
  colorMode: 'auto',    // 'none' | 'auto' | 'ansi16' | 'ansi256' | 'truecolor' | 'html'
  theme: { ... },       // Partial<AsciiTheme> — override default colors
})
```

### ASCII XY Charts

XY charts render to ASCII with dedicated chart-drawing characters:

- **Bar charts** — `█` blocks (Unicode) or `#` (ASCII mode)
- **Line charts** — Staircase routing with rounded corners: `╭╮╰╯│─` (Unicode) or `+|-` (ASCII)
- **Multi-series** — Each series gets a distinct ANSI color from the theme's accent palette
- **Legends** — Automatically shown when multiple series are present
- **Horizontal charts** — Fully supported with categories on the y-axis

---

## API Reference

### `renderMermaidSVG(text, options?): string`

Render a Mermaid diagram to SVG. Synchronous. Auto-detects diagram type.

**Parameters:**
- `text` — Mermaid source code
- `options` — Optional `RenderOptions` object

**RenderOptions:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `bg` | `string` | `#FFFFFF` | Background color (or CSS variable) |
| `fg` | `string` | `#27272A` | Foreground color (or CSS variable) |
| `line` | `string?` | — | Edge/connector color |
| `accent` | `string?` | — | Arrow heads, highlights |
| `muted` | `string?` | — | Secondary text, labels |
| `surface` | `string?` | — | Node fill tint |
| `border` | `string?` | — | Node stroke color |
| `font` | `string` | `Inter` | Font family |
| `transparent` | `boolean` | `false` | Render with transparent background |
| `padding` | `number` | `40` | Canvas padding in px |
| `nodeSpacing` | `number` | `24` | Horizontal spacing between sibling nodes |
| `layerSpacing` | `number` | `40` | Vertical spacing between layers |
| `componentSpacing` | `number` | `24` | Spacing between disconnected components |
| `thoroughness` | `number` | `3` | Crossing minimization trials (1-7, higher = better but slower) |
| `interactive` | `boolean` | `false` | Enable hover tooltips on XY chart bars and data points |

**XY Charts:** Diagrams starting with `xychart-beta` are auto-detected — no separate function needed. The `accent` color option drives the chart series color palette.

### `renderMermaidSVGAsync(text, options?): Promise<string>`

Async version of `renderMermaidSVG()`. Same output, returns a `Promise<string>`. Useful in async server handlers or data loaders.

### `renderMermaidASCII(text, options?): string`

Render a Mermaid diagram to ASCII/Unicode text. Synchronous.

**AsciiRenderOptions:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `useAscii` | `boolean` | `false` | Use ASCII instead of Unicode |
| `paddingX` | `number` | `5` | Horizontal node spacing |
| `paddingY` | `number` | `5` | Vertical node spacing |
| `boxBorderPadding` | `number` | `1` | Inner box padding |
| `colorMode` | `string` | `'auto'` | `'none'`, `'auto'`, `'ansi16'`, `'ansi256'`, `'truecolor'`, or `'html'` |
| `theme` | `Partial<AsciiTheme>` | — | Override default colors for ASCII output |

### `parseMermaid(text): MermaidGraph`

Parse Mermaid source into a structured graph object (for custom processing).

### `fromShikiTheme(theme): DiagramColors`

Extract diagram colors from a Shiki theme object.

### `THEMES: Record<string, DiagramColors>`

Object containing all 15 built-in themes.

### `DEFAULTS: { bg: string, fg: string }`

Default colors (`#FFFFFF` / `#27272A`).

---

## Attribution

The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff. We ported it from Go to TypeScript and extended it with:

- Sequence diagram support
- Class diagram support
- ER diagram support
- Unicode box-drawing characters
- Configurable spacing and padding

Thank you Alexander for the excellent foundation!

---

## License

MIT — see [LICENSE](LICENSE) for details.

---

<div align="center">

Built with care by the team at [Craft](https://craft.do)

</div>


================================================
FILE: bench.ts
================================================
/**
 * Performance benchmark for beautiful-mermaid.
 *
 * Runs all sample definitions through both renderers (SVG + ASCII) in Bun
 * and prints a table with per-sample timing and aggregate stats.
 *
 * Usage: bun run packages/mermaid/bench.ts
 */

import { samples } from './samples-data.ts'
import { renderMermaid } from './src/index.ts'
import { renderMermaidAscii } from './src/ascii/index.ts'

// ============================================================================
// Types
// ============================================================================

interface Result {
  index: number
  title: string
  category: string
  svgMs: number
  asciiMs: number
  svgError: string | null
  asciiError: string | null
}

// ============================================================================
// Helpers
// ============================================================================

/** Pad/truncate a string to exactly `width` characters, right-aligned if numeric. */
function col(value: string, width: number, align: 'left' | 'right' = 'left'): string {
  const truncated = value.length > width ? value.slice(0, width - 1) + '\u2026' : value
  return align === 'right' ? truncated.padStart(width) : truncated.padEnd(width)
}

function fmtMs(ms: number): string {
  return ms.toFixed(1)
}

// ============================================================================
// Main
// ============================================================================

const results: Result[] = []
const totalStart = performance.now()

console.log(`\nbeautiful-mermaid — Benchmark (${samples.length} samples)`)
console.log('═'.repeat(90))
console.log(
  `${col('#', 4, 'right')}  ${col('Title', 38)}  ${col('Category', 15)}  ${col('SVG (ms)', 10, 'right')}  ${col('ASCII (ms)', 10, 'right')}  ${col('Total', 10, 'right')}`,
)
console.log('─'.repeat(90))

for (let i = 0; i < samples.length; i++) {
  const sample = samples[i]!
  const category = sample.category ?? 'Other'
  let svgMs = 0
  let asciiMs = 0
  let svgError: string | null = null
  let asciiError: string | null = null

  // Render SVG (async — uses dagre layout for flowcharts/state/class/ER)
  try {
    const t0 = performance.now()
    await renderMermaid(sample.source, sample.options)
    svgMs = performance.now() - t0
  } catch (err) {
    svgError = String(err)
    svgMs = -1
  }

  // Render ASCII (sync — custom text layout, no dagre)
  try {
    const t0 = performance.now()
    renderMermaidAscii(sample.source)
    asciiMs = performance.now() - t0
  } catch (err) {
    asciiError = String(err)
    asciiMs = -1
  }

  const totalMs = (svgMs >= 0 ? svgMs : 0) + (asciiMs >= 0 ? asciiMs : 0)

  results.push({ index: i, title: sample.title, category, svgMs, asciiMs, svgError, asciiError })

  // Print row
  const svgStr = svgMs >= 0 ? fmtMs(svgMs) : 'ERR'
  const asciiStr = asciiMs >= 0 ? fmtMs(asciiMs) : 'N/A'
  console.log(
    `${col(String(i + 1), 4, 'right')}  ${col(sample.title, 38)}  ${col(category, 15)}  ${col(svgStr, 10, 'right')}  ${col(asciiStr, 10, 'right')}  ${col(fmtMs(totalMs), 10, 'right')}`,
  )
}

const totalElapsed = performance.now() - totalStart

// ============================================================================
// Aggregates
// ============================================================================

console.log('═'.repeat(90))

const svgTimes = results.filter(r => r.svgMs >= 0).map(r => r.svgMs)
const asciiTimes = results.filter(r => r.asciiMs >= 0).map(r => r.asciiMs)
const svgTotal = svgTimes.reduce((a, b) => a + b, 0)
const asciiTotal = asciiTimes.reduce((a, b) => a + b, 0)

console.log(`Total: ${fmtMs(totalElapsed)}ms (SVG: ${fmtMs(svgTotal)}ms, ASCII: ${fmtMs(asciiTotal)}ms)`)
console.log(`Average: ${fmtMs((svgTotal + asciiTotal) / results.length)}ms per sample`)

// Find slowest SVG and ASCII
if (svgTimes.length > 0) {
  const slowestSvg = results.filter(r => r.svgMs >= 0).sort((a, b) => b.svgMs - a.svgMs)[0]!
  console.log(`Slowest SVG:   #${slowestSvg.index + 1} ${slowestSvg.title} (${fmtMs(slowestSvg.svgMs)}ms)`)
}
if (asciiTimes.length > 0) {
  const slowestAscii = results.filter(r => r.asciiMs >= 0).sort((a, b) => b.asciiMs - a.asciiMs)[0]!
  console.log(`Slowest ASCII: #${slowestAscii.index + 1} ${slowestAscii.title} (${fmtMs(slowestAscii.asciiMs)}ms)`)
}

// Report errors
const svgErrors = results.filter(r => r.svgError)
const asciiErrors = results.filter(r => r.asciiError)
if (svgErrors.length > 0) {
  console.log(`\nSVG errors (${svgErrors.length}):`)
  for (const r of svgErrors) {
    console.log(`  #${r.index + 1} ${r.title}: ${r.svgError}`)
  }
}
if (asciiErrors.length > 0) {
  console.log(`\nASCII unsupported (${asciiErrors.length}):`)
  for (const r of asciiErrors) {
    console.log(`  #${r.index + 1} ${r.title}`)
  }
}

// Category breakdown
console.log('\n── By Category ──')
const catMap = new Map<string, Result[]>()
for (const r of results) {
  if (!catMap.has(r.category)) catMap.set(r.category, [])
  catMap.get(r.category)!.push(r)
}
for (const [cat, catResults] of catMap) {
  const catSvg = catResults.filter(r => r.svgMs >= 0).reduce((a, r) => a + r.svgMs, 0)
  const catAscii = catResults.filter(r => r.asciiMs >= 0).reduce((a, r) => a + r.asciiMs, 0)
  console.log(`  ${col(cat, 16)} ${col(String(catResults.length), 3, 'right')} samples  SVG: ${col(fmtMs(catSvg), 8, 'right')}ms  ASCII: ${col(fmtMs(catAscii), 8, 'right')}ms  Total: ${col(fmtMs(catSvg + catAscii), 8, 'right')}ms`)
}

console.log()


================================================
FILE: dev.ts
================================================
/**
 * Development server with live reload for mermaid samples.
 *
 * Usage: bun run packages/mermaid/dev.ts
 *
 * - Runs `index.ts` to generate index.html on startup
 * - Watches `src/` and `index.ts` for file changes
 * - On change, rebuilds index.html and notifies browsers via SSE
 * - Serves index.html with an injected live-reload script
 *
 * This avoids manually re-running the build and refreshing the browser —
 * just save a file and the page updates automatically.
 */

import { watch } from 'fs'
import { join } from 'path'

const PORT = 3456
const ROOT = import.meta.dir

// ============================================================================
// Build management
// ============================================================================

let building = false
const sseClients = new Set<ReadableStreamDefaultController>()

async function rebuild(): Promise<void> {
  if (building) return
  building = true
  console.log('\x1b[36m[dev]\x1b[0m Rebuilding samples...')
  const t0 = performance.now()

  const proc = Bun.spawn(['bun', 'run', join(ROOT, 'index.ts')], {
    cwd: ROOT,
    stdout: 'inherit',
    stderr: 'inherit',
  })
  await proc.exited

  const ms = (performance.now() - t0).toFixed(0)
  if (proc.exitCode === 0) {
    console.log(`\x1b[32m[dev]\x1b[0m Rebuilt in ${ms}ms`)
    // Notify all connected browsers to reload
    for (const client of sseClients) {
      try {
        client.enqueue('data: reload\n\n')
      } catch {
        sseClients.delete(client)
      }
    }
  } else {
    console.error(`\x1b[31m[dev]\x1b[0m Build failed (exit ${proc.exitCode})`)
  }
  building = false
}

// ============================================================================
// File watching — debounced to coalesce rapid saves
// ============================================================================

let debounce: Timer | null = null
function onFileChange(_event: string, filename: string | null): void {
  // Ignore index.html itself (it's the output, not a source)
  if (filename === 'index.html') return
  if (debounce) clearTimeout(debounce)
  debounce = setTimeout(() => {
    console.log(`\x1b[90m[dev]\x1b[0m Change detected${filename ? `: ${filename}` : ''}`)
    rebuild()
  }, 150)
}

// Watch the entire mermaid package for changes (excludes index.html output)
watch(ROOT, { recursive: true }, onFileChange)

// ============================================================================
// HTTP server
// ============================================================================

// Initial build before starting the server
await rebuild()

console.log(`\x1b[36m[dev]\x1b[0m Server running at \x1b[1mhttp://localhost:${PORT}\x1b[0m`)
console.log(`\x1b[36m[dev]\x1b[0m Watching for changes in src/ and index.ts\n`)

Bun.serve({
  port: PORT,
  async fetch(req) {
    const url = new URL(req.url)

    // SSE endpoint — browsers connect here to receive reload signals
    if (url.pathname === '/__dev_events') {
      let controller!: ReadableStreamDefaultController
      const stream = new ReadableStream({
        start(c) {
          controller = c
          sseClients.add(controller)
        },
        cancel() {
          sseClients.delete(controller)
        },
      })
      return new Response(stream, {
        headers: {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          Connection: 'keep-alive',
        },
      })
    }

    // Serve index.html with injected live-reload script
    const file = Bun.file(join(ROOT, 'index.html'))
    if (!(await file.exists())) {
      return new Response('index.html not found — build may have failed', { status: 404 })
    }

    let html = await file.text()

    // Inject live-reload client before </body>
    html = html.replace(
      '</body>',
      `  <script>
    // Live reload — SSE connection to dev server.
    // When the server signals a rebuild, the page reloads automatically.
    // If the connection drops (server restarting), it reconnects with backoff.
    ;(function() {
      function connect() {
        var es = new EventSource('/__dev_events');
        es.onmessage = function(e) {
          if (e.data === 'reload') location.reload();
        };
        es.onerror = function() {
          es.close();
          setTimeout(connect, 500);
        };
      }
      connect();
    })();
  </script>
</body>`,
    )

    return new Response(html, {
      headers: { 'Content-Type': 'text/html' },
    })
  },
})


================================================
FILE: index.ts
================================================
/**
 * Generates index.html showcasing all beautiful-mermaid rendering capabilities.
 *
 * Usage: bun run index.ts
 *
 * This file doubles as a **visual test suite** — every supported feature,
 * shape, edge type, block construct, and theme variant is exercised by at
 * least one sample. If a rendering change causes regressions, it will be
 * visible in the generated HTML.
 *
 * The generated HTML is **dynamic** — it includes a bundled copy of the
 * mermaid renderer and renders all diagrams client-side in real time,
 * showing progressive loading and per-diagram render timing.
 *
 * Sample definitions live in samples-data.ts (shared with bench.ts).
 */

import { samples } from './samples-data.ts'
import { THEMES } from './src/theme.ts'
import { createHighlighter } from 'shiki'

// ============================================================================
// HTML generation — dynamic version
//
// Instead of pre-rendering SVGs at build time, we:
//   1. Bundle the mermaid renderer for the browser via Bun.build()
//   2. Embed sample definitions as inline JSON
//   3. Emit client-side JS that renders each diagram on page load
// ============================================================================

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
}

/** Convert markdown-style backtick spans to <code> tags in description text. */
function formatDescription(text: string): string {
  return text.replace(/`([^`]+)`/g, '<code>$1</code>')
}

/** Human-readable labels for theme keys */
const THEME_LABELS: Record<string, string> = {
  'zinc-dark': 'Zinc Dark',
  'tokyo-night': 'Tokyo Night',
  'tokyo-night-storm': 'Tokyo Storm',
  'tokyo-night-light': 'Tokyo Light',
  'catppuccin-mocha': 'Catppuccin',
  'catppuccin-latte': 'Latte',
  'nord': 'Nord',
  'nord-light': 'Nord Light',
  'dracula': 'Dracula',
  'github-light': 'GitHub',
  'github-dark': 'GitHub Dark',
  'solarized-light': 'Solarized',
  'solarized-dark': 'Solar Dark',
  'one-dark': 'One Dark',
}

async function generateHtml(): Promise<string> {
  // Step 0: Create Shiki highlighter for mermaid syntax highlighting in source panels.
  // We use 'github-light' as the base theme — its hex colors get overridden by CSS
  // color-mix() rules derived from --t-fg / --t-bg so tokens adapt to any theme.
  const highlighter = await createHighlighter({
    langs: ['mermaid'],
    themes: ['github-light'],
  })

  // Step 1: Bundle the mermaid renderer for the browser
  const buildResult = await Bun.build({
    entrypoints: [new URL('./src/browser.ts', import.meta.url).pathname],
    target: 'browser',
    format: 'esm',
    minify: true,
  })
  if (!buildResult.success) {
    console.error('Bundle build failed:', buildResult.logs)
    process.exit(1)
  }
  const bundleJs = await buildResult.outputs[0]!.text()
  console.log(`Browser bundle: ${(bundleJs.length / 1024).toFixed(1)} KB`)

  // Step 2: Build sample JSON (only serializable fields needed by client)
  const samplesJson = JSON.stringify(samples.map(s => ({
    title: s.title,
    description: s.description,
    source: s.source,
    category: s.category ?? 'Other',
    options: s.options ?? {},
  })))

  // Step 3: Group samples by category for TOC (done at build time since it's static)
  const categories = new Map<string, number[]>()
  samples.forEach((sample, i) => {
    const cat = sample.category ?? 'Other'
    if (!categories.has(cat)) categories.set(cat, [])
    categories.get(cat)!.push(i)
  })

  const categoryBadgeColors: Record<string, string> = {
    Flowchart: '#3b82f6',
    State: '#8b5cf6',
    Sequence: '#10b981',
    Class: '#f59e0b',
    ER: '#ef4444',
    'XY Chart': '#f97316',
    'Theme Showcase': '#06b6d4',
  }

  // Map category names to the title prefixes they use, so we can strip duplicates in the ToC
  const categoryPrefixes: Record<string, string> = {
    'State': 'State: ',
    'Sequence': 'Sequence: ',
    'Class': 'Class: ',
    'ER': 'ER: ',
    'XY Chart': 'XY: ',
    'Theme Showcase': 'Theme: ',
  }

  // Build mapping from original index to display number (excluding Hero samples)
  const heroCount = samples.filter(s => s.category === 'Hero').length
  const displayNum = (i: number) => i + 1 - heroCount

  const tocSections = [...categories.entries()]
    .filter(([cat]) => cat !== 'Hero') // Skip Hero from TOC
    .map(([cat, indices]) => {
    const badgeColor = categoryBadgeColors[cat] ?? '#71717a'
    const prefix = categoryPrefixes[cat]
    const items = indices.map(i => {
      let title = samples[i]!.title
      // Strip the category prefix from the title since it's already under the category heading
      if (prefix && title.startsWith(prefix)) title = title.slice(prefix.length)
      return `<li><a href="#sample-${i}"><span class="toc-num">${displayNum(i)}.</span> ${escapeHtml(title)}</a></li>`
    }).join('\n            ')
    return `
        <div class="toc-category">
          <h3>${escapeHtml(cat)} (${indices.length} samples)</h3>
          <ol start="${displayNum(indices[0]!)}">
            ${items}
          </ol>
        </div>`
  }).join('\n')

  // Step 3b: Build theme selector pills (build-time so we include swatches)
  // Only show Default, Dracula, and Solarized inline; rest go in "More" dropdown
  const VISIBLE_THEMES = new Set(['dracula', 'solarized-light'])

  function buildThemePill(key: string, colors: { bg: string; fg: string }, active = false): string {
    const isDark = parseInt(colors.bg.replace('#', '').slice(0, 2), 16) < 0x80
    const shadow = isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.1)'
    const label = key === '' ? 'Default' : (THEME_LABELS[key] ?? key)
    const activeClass = active ? ' active' : ''
    return `<button class="theme-pill shadow-minimal${activeClass}" data-theme="${key}"><span class="theme-swatch" style="background:${colors.bg};box-shadow:inset 0 0 0 1px ${shadow}"></span>${escapeHtml(label)}</button>`
  }

  const themeEntries = Object.entries(THEMES)
  // Visible inline pills: Default + Dracula + Solarized
  const visiblePills = [
    '<button class="theme-pill shadow-minimal active" data-theme=""><span class="theme-swatch" style="background:#FFFFFF;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.1)"></span>Default</button>',
    ...themeEntries
      .filter(([key]) => VISIBLE_THEMES.has(key))
      .map(([key, colors]) => buildThemePill(key, colors)),
  ]
  // All themes go in the dropdown (including Default, Dracula, Solarized)
  const allDropdownPills = [
    buildThemePill('', { bg: '#FFFFFF', fg: '#27272A' }, true),
    ...themeEntries.map(([key, colors]) => buildThemePill(key, colors)),
  ]
  const totalThemes = allDropdownPills.length

  const themePillsHtml = `
    <div class="theme-pills-inline">
      ${visiblePills.join('\n      ')}
    </div>
    <div class="theme-more-wrapper">
      <button class="theme-pill shadow-minimal" id="theme-more-btn">${totalThemes} Themes</button>
      <div class="theme-more-dropdown shadow-modal-small" id="theme-more-dropdown">
        ${allDropdownPills.join('\n        ')}
      </div>
    </div>`

  // Step 4: Pre-highlight all sample sources with Shiki (build-time only, zero runtime cost).
  // The mermaid TextMate grammar requires a fenced code block prefix to tokenize properly
  // (see https://github.com/shikijs/shiki/issues/973), so we wrap each source with
  // ```mermaid ... ``` and then strip those fence lines from the output HTML.
  // Source panels always use github-dark — Shiki's inline colors are used directly.
  const highlightedSources = samples.map(sample => {
    const fenced = '```mermaid\n' + sample.source.trim() + '\n```'
    const html = highlighter.codeToHtml(fenced, {
      lang: 'mermaid',
      theme: 'github-light',
    })
    // Strip the first line (```mermaid) and last line (```) from the output
    return html.replace(
      /(<code>)<span class="line">.*?<\/span>\n/,  // first line
      '$1'
    ).replace(
      /\n<span class="line">.*?<\/span>(<\/code>)/, // last line
      '$1'
    )
  })

  // Step 5: Build sample card HTML shells (SVG + ASCII are empty, filled client-side)
  // data-sample-bg stores the per-sample background for "Default" mode restoration.
  // Hero samples get special full-width SVG-only treatment and are placed before "Samples" heading.
  const heroCards: string[] = []
  const regularCards: string[] = []

  samples.forEach((sample, i) => {
    const bg = sample.options?.bg ?? ''
    const isHero = sample.category === 'Hero'

    if (isHero) {
      // Hero sample: full-width SVG only, no header/source/ASCII panels
      heroCards.push(`
    <section class="sample sample-hero" id="sample-${i}">
      <div class="hero-diagram-panel" id="svg-panel-${i}" data-sample-bg="${bg}">
        <div class="svg-container" id="svg-${i}">
          <div class="loading-spinner"></div>
        </div>
      </div>
    </section>`)
    } else {
      regularCards.push(`
    <section class="sample" id="sample-${i}">
      <div class="sample-header">
        <h2>${escapeHtml(sample.title)}</h2>
        <p class="description">${formatDescription(sample.description)}</p>
      </div>
      <div class="sample-content">
        <div class="source-panel" id="source-panel-${i}">
          ${highlightedSources[i]}
          ${sample.options ? `<div class="options"><strong>Options:</strong> <code>${escapeHtml(JSON.stringify(sample.options))}</code></div>` : ''}
          <button class="edit-btn" data-sample="${i}">Edit</button>
        </div>
        <div class="svg-panel" id="svg-panel-${i}" data-sample-bg="${bg}">
          <div class="svg-container" id="svg-${i}">
            <div class="loading-spinner"></div>
          </div>
        </div>
        <div class="ascii-panel" id="ascii-panel-${i}">
          <pre class="ascii-output"><code id="ascii-${i}">Rendering\u2026</code></pre>
        </div>
      </div>
    </section>`)
    }
  })

  const heroCardsHtml = heroCards.join('\n')
  const regularCardsHtml = regularCards.join('\n')

  // ============================================================================
  // Step 5: Assemble full HTML
  // ============================================================================

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta name="theme-color" id="theme-color-meta" content="#f9f9fa" />
  <title>Beautiful Mermaid — Mermaid Rendering, Made Beautiful</title>
  <meta name="description" content="Open source diagram rendering library built for the AI era. Ultra-fast, fully themeable, outputs to SVG and ASCII. Supports Flowchart, State, Sequence, Class, and ER diagrams." />
  <link rel="icon" type="image/svg+xml" href="/mermaid/favicon.svg" />
  <link rel="icon" type="image/x-icon" href="/mermaid/favicon.ico" />
  <link rel="apple-touch-icon" href="/mermaid/apple-touch-icon.png" />
  <meta property="og:title" content="Beautiful Mermaid" />
  <meta property="og:description" content="Open source diagram rendering library built for the AI era. Ultra-fast, fully themeable, outputs to SVG and ASCII." />
  <meta property="og:image" content="https://agents.craft.do/mermaid/og-image.png" />
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://agents.craft.do/mermaid" />
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="Beautiful Mermaid" />
  <meta name="twitter:description" content="Mermaid rendering, made beautiful. Ultra-fast, fully themeable, outputs to SVG and ASCII." />
  <meta name="twitter:image" content="https://agents.craft.do/mermaid/og-image.png" />
  <!-- Plausible Analytics -->
  <script defer data-domain="agents.craft.do/mermaid" src="https://plausible.io/js/script.js"></script>
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
  <style>
    /* -- Reset & base -- */
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    /* -----------------------------------------------------------------
     * CSS custom property theming
     *
     * --t-bg and --t-fg drive the entire page color scheme.
     * All other colors are derived via color-mix(). When a theme is
     * selected from the pill bar, JS updates these two variables on
     * <body> — and the whole page adapts instantly.
     * ----------------------------------------------------------------- */
    body {
      --t-bg: #FFFFFF;
      --t-fg: #27272A;
      --t-accent: #3b82f6;
      --foreground-rgb: 39, 39, 42;
      --accent-rgb: 59, 130, 246;
      --shadow-border-opacity: 0.08;
      --shadow-blur-opacity: 0.06;
      --theme-bar-bg: #f9f9fa;  /* Mixed bg for theme bar and top gradient — updated by JS on theme change */

      font-family: 'Geist', system-ui, -apple-system, sans-serif;
      background: color-mix(in srgb, var(--t-fg) 4%, var(--t-bg));
      color: var(--t-fg);
      line-height: 1.6;
      margin: 0;
      transition: background 0.2s, color 0.2s;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    .content-wrapper {
      max-width: 1440px;
      margin: 0 auto;
      padding: 2rem;
      padding-top: 0;
    }
    @media (max-width: 768px) {
      .content-wrapper {
        padding: 1rem;
        padding-top: 0;
      }
    }
    @media (min-width: 1000px) {
      .content-wrapper {
        padding: 3rem;
        padding-top: 0;
      }
    }

    /* -- Scroll fade gradients (GPU accelerated) -- */
    body::before,
    body::after {
      content: '';
      position: fixed;
      left: 0;
      right: 0;
      height: 64px;
      pointer-events: none;
      z-index: 1000;
      will-change: transform;
    }
    body::before {
      top: 0;
      background: linear-gradient(to bottom, var(--theme-bar-bg) 0%, transparent 100%);
    }
    body::after {
      bottom: 0;
      background: linear-gradient(to top, var(--theme-bar-bg) 0%, transparent 100%);
    }

    /* -- Theme selector bar (full-width, sits outside .content-wrapper) -- */
    .theme-bar {
      position: sticky;
      top: 0;
      z-index: 1001;
      background: transparent;
      padding: 0.5rem 2rem;
      display: flex;
      align-items: center;
      gap: 0.75rem;
      overflow: visible;
    }
    @media (max-width: 768px) {
      .theme-bar {
        padding: 0.5rem 1rem;
      }
    }
    .theme-label {
      font-size: 0.7rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
      white-space: nowrap;
    }
    .theme-pills {
      display: flex;
      gap: 0.3rem;
      overflow: visible;
      padding: 4px;
      margin: -4px;
      margin-left: auto;
      position: relative;
      z-index: 2;
    }
    .theme-pills-inline {
      display: flex;
      gap: 0.3rem;
    }
    /* Hide inline theme pills on smaller screens, show only "15 Themes" dropdown */
    @media (max-width: 1024px) {
      .theme-pills-inline {
        display: none;
      }
    }
    .theme-pill {
      display: flex;
      align-items: center;
      height: 30px;
      gap: 8px;
      padding: 0 14px 0 12px;
      border: none;
      border-radius: 8px;
      background: color-mix(in srgb, var(--t-bg) 97%, var(--t-fg));
      color: color-mix(in srgb, var(--t-fg) 80%, var(--t-bg));
      font-size: 12px;
      font-weight: 500;
      font-family: inherit;
      cursor: pointer;
      white-space: nowrap;
      transition: color 0.15s, background 0.15s, box-shadow 0.2s, transform 0.1s;
    }
    .theme-pill:hover {
      color: var(--t-fg);
      background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
    }
    .theme-pill.active {
      color: var(--t-fg);
      background: var(--t-bg);
      font-weight: 600;
    }
    .theme-pill:active {
      transform: translateY(0.5px);
    }
    .theme-swatch {
      display: inline-block;
      width: 14px;
      height: 14px;
      border-radius: 50%;
      flex-shrink: 0;
    }

    /* -- "More" dropdown for overflow themes -- */
    .theme-more-wrapper {
      position: relative;
    }
    .theme-more-dropdown {
      display: none;
      position: absolute;
      top: calc(100% + 6px);
      right: 0;
      background: var(--t-bg);
      border-radius: 12px;
      padding: 6px;
      flex-direction: column;
      gap: 2px;
      min-width: 160px;
      z-index: 1002;
    }
    .theme-more-dropdown.open {
      display: flex;
    }
    .theme-more-dropdown .theme-pill {
      width: 100%;
      justify-content: flex-start;
      background: transparent;
      box-shadow: none;
    }
    .theme-more-dropdown .theme-pill:hover {
      background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
    }
    /* Active pill in dropdown gets bg + shadow-minimal (same as inline pills) */
    .theme-more-dropdown .theme-pill.active,
    .theme-more-dropdown .theme-pill.shadow-tinted {
      background: var(--t-bg);
      box-shadow:
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(var(--foreground-rgb), 0.06) 0px 0px 0px 1px,
        rgba(0, 0, 0, var(--shadow-blur-opacity)) 0px 1px 1px -0.5px,
        rgba(0, 0, 0, var(--shadow-blur-opacity)) 0px 3px 3px -1.5px;
    }

    /* -- Brand badge (left-aligned in theme bar) -- */
    .brand-badge-wrapper {
      position: relative;
    }
    .brand-badge {
      display: flex;
      align-items: center;
      height: 30px;
      gap: 6px;
      padding: 0 12px;
      border: none;
      border-radius: 8px;
      background: color-mix(in srgb, var(--t-bg) 97%, var(--t-fg));
      color: color-mix(in srgb, var(--t-fg) 80%, var(--t-bg));
      font-size: 12px;
      font-weight: 400;
      font-family: inherit;
      white-space: nowrap;
      cursor: pointer;
      transition: color 0.15s, background 0.15s, box-shadow 0.2s, transform 0.1s;
    }
    .brand-badge:hover {
      color: var(--t-fg);
      background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
    }
    .brand-badge.active {
      color: var(--t-fg);
      background: var(--t-bg);
    }
    .brand-badge:active {
      transform: translateY(0.5px);
    }
    .brand-logo {
      width: 14px;
      height: 14px;
      flex-shrink: 0;
    }

    /* -- Brand dropdown -- */
    .brand-dropdown {
      display: none;
      position: absolute;
      top: calc(100% + 6px);
      left: 0;
      background: var(--t-bg);
      border-radius: 12px;
      padding: 6px;
      flex-direction: column;
      gap: 2px;
      width: max-content;
      z-index: 1002;
    }
    .brand-dropdown.open {
      display: flex;
    }
    .brand-dropdown-item {
      display: flex;
      align-items: center;
      height: 34px;
      gap: 8px;
      padding: 0 12px;
      border-radius: 8px;
      background: transparent;
      color: var(--t-fg);
      text-decoration: none;
      font-size: 13px;
      font-weight: 400;
      transition: background 0.15s;
    }
    .brand-dropdown-item:hover {
      background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
    }
    .brand-dropdown-logo {
      flex-shrink: 0;
    }
    .brand-dropdown-item .tagline {
      color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
      font-weight: 300;
      margin-left: 0.25rem;
    }
    .brand-dropdown-item .tagline::before {
      content: '·';
      margin-right: 0.25rem;
    }

    /* -- Contents button (screen-centered via absolute positioning) -- */
    .contents-btn {
      position: absolute;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      align-items: center;
      height: 30px;
      gap: 6px;
      padding: 0 12px;
      border: none;
      border-radius: 8px;
      background: color-mix(in srgb, var(--t-bg) 97%, var(--t-fg));
      color: color-mix(in srgb, var(--t-fg) 80%, var(--t-bg));
      font-size: 12px;
      font-weight: 500;
      font-family: inherit;
      cursor: pointer;
      white-space: nowrap;
      transition: color 0.15s, background 0.15s, box-shadow 0.2s, transform 0.1s;
    }
    .contents-btn:hover {
      color: var(--t-fg);
      background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
    }
    .contents-btn.active {
      color: var(--t-fg);
      background: var(--t-bg);
    }
    .contents-btn:active {
      transform: translateX(-50%) translateY(0.5px);
    }
    .contents-btn svg {
      width: 14px;
      height: 14px;
      flex-shrink: 0;
    }
    /* Hide contents button on smaller screens */
    @media (max-width: 1024px) {
      .contents-btn,
      .mega-menu {
        display: none !important;
      }
    }
    /* -- Craft shadow + radius utilities -- */
    .rounded-6px { border-radius: 6px; }
    .shadow-minimal {
      box-shadow:
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(var(--foreground-rgb), 0.06) 0px 0px 0px 1px,
        rgba(0, 0, 0, var(--shadow-blur-opacity)) 0px 1px 1px -0.5px,
        rgba(0, 0, 0, var(--shadow-blur-opacity)) 0px 3px 3px -1.5px;
    }
    .shadow-modal-small {
      box-shadow:
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(var(--foreground-rgb), 0.06) 0px 0px 0px 1px,
        rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.67)) 0px 1px 1px -0.5px,
        rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.67)) 0px 3px 3px 0px,
        rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.33)) 0px 6px 6px 0px,
        rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.33)) 0px 12px 12px 0px,
        rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.33)) 0px 24px 24px 0px;
    }
    .shadow-tinted {
      --shadow-color: 0, 0, 0;
      box-shadow:
        rgba(var(--shadow-color), 0) 0px 0px 0px 0px,
        rgba(var(--shadow-color), 0) 0px 0px 0px 0px,
        rgba(var(--shadow-color), calc(var(--shadow-border-opacity) * 1.5)) 0px 0px 0px 1px,
        rgba(var(--shadow-color), var(--shadow-border-opacity)) 0px 1px 1px -0.5px,
        rgba(var(--shadow-color), var(--shadow-blur-opacity)) 0px 3px 3px -1.5px,
        rgba(var(--shadow-color), calc(var(--shadow-blur-opacity) * 0.67)) 0px 6px 6px -3px;
    }

    /* -- Mega menu dropdown (x-centered with Contents button) -- */
    .mega-menu {
      display: none;
      position: absolute;
      top: calc(100% + 6px);
      left: 50%;
      transform: translateX(-50%);
      max-width: min(1180px, calc(100vw - 2rem));
      width: max-content;
      background: var(--t-bg);
      border-radius: 12px;
      padding: 1.5rem 2rem;
      max-height: 70vh;
      overflow-y: auto;
      overflow-x: hidden;
      z-index: 998;
    }
    .mega-menu.open {
      display: block;
    }
    .toc-grid {
      columns: 4;
      column-gap: 2rem;
    }
    @media (max-width: 1200px) {
      .toc-grid {
        columns: 3;
      }
    }
    .toc-category {
      display: inline-block;
      width: 100%;
      margin: 0;
      padding-bottom: 1rem;
    }
    .toc-category h3 {
      font-size: 0.85rem;
      font-weight: 600;
      margin: 0 0 0.5rem 0;
      color: var(--t-fg);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .toc-category ol {
      padding: 0;
      margin: 0;
      list-style: none;
      font-size: 0.8rem;
    }
    .toc-category li {
      margin-bottom: 0.15rem;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .toc-category a { color: var(--t-fg); text-decoration: none; }
    .toc-category a:hover { text-decoration: underline; }
    .toc-num { color: color-mix(in srgb, var(--t-fg) 30%, var(--t-bg)); }

    /* -- Sample card -- */
    .sample {
      background: var(--t-bg);
      margin-bottom: 2rem;
      overflow: hidden;
    }

    /* -- Hero sample (full-width SVG showcase, above Samples heading) -- */
    .sample-hero {
      margin-bottom: 0;
      background: transparent;
    }
    .hero-diagram-panel {
      padding: 1rem 0;
      display: flex;
      justify-content: center;
      align-items: center;
      background: transparent;
    }
    .hero-diagram-panel .svg-container {
      width: 100%;
      max-width: 100%;
    }
    .hero-diagram-panel .svg-container svg {
      width: 100%;
      height: auto;
    }

    .sample-header {
      padding: 1.25rem 1.5rem;
      max-width: 48rem;
      border-bottom: 1px solid color-mix(in srgb, var(--t-fg) 5%, var(--t-bg));
    }
    .sample-header h2 {
      font-size: 1.5rem;
      font-weight: 500;
      color: var(--t-fg);
    }
    .description {
      color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
      font-size: 1rem;
      font-weight: 400;
      margin-top: 0.1rem;
    }
    .description code {
      font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
      font-size: 0.875em;
      color: color-mix(in srgb, var(--t-fg) 85%, var(--t-bg));
      background: color-mix(in srgb, var(--t-fg) 6%, var(--t-bg));
      padding: 0.15rem 0.4rem;
      border-radius: 3px;
    }

    .sample-content {
      display: grid;
      grid-template-columns:
        minmax(200px, 1fr)
        minmax(250px, 2fr)
        minmax(250px, 2fr);
      min-height: 420px;
    }
    @media (max-width: 900px) {
      .sample-content { grid-template-columns: 1fr; }
      .ascii-panel { border-left: none !important; border-top: 1px solid color-mix(in srgb, var(--t-fg) 5%, var(--t-bg)) !important; }
    }

    /* -- Source panel -- */
    .source-panel {
      position: relative;
      padding: 0.75rem 1.5rem;
      border-right: 1px solid color-mix(in srgb, var(--t-fg) 5%, var(--t-bg));
      min-width: 0;      /* grid child: allow shrinking below content width */
      overflow-y: auto;
    }
    .source-panel h3 {
      font-size: 0.8rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
      margin-bottom: 0.75rem;
    }
    .source-panel pre {
      padding: 1rem;
      font-size: 0.8rem;
      line-height: 1.5;
      overflow-x: auto;
      white-space: pre-wrap;
      word-break: break-word;
    }
    .source-panel code {
      font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
    }

    /* -- Shiki syntax highlighting overrides --
     * Shiki outputs inline style="color:#hex" per token. We override these with
     * color-mix() rules derived from --t-fg / --t-bg so tokens adapt to any theme.
     * The hex values below are from the github-light Shiki theme used at build time. */
    .source-panel {
      background: color-mix(in srgb, var(--t-fg) 1.5%, var(--t-bg));
    }
    .source-panel .shiki {
      background: transparent !important;
      padding: 0.5rem 0;
      font-size: 0.8rem;
      line-height: 1.5;
      overflow-x: auto;
      white-space: pre-wrap;
      word-break: break-word;
      margin: 0;
    }
    .source-panel .shiki code {
      background: transparent;
      font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
    }
    /* Default text */
    .source-panel .shiki,
    .source-panel .shiki span[style*="#24292e"],
    .source-panel .shiki span[style*="#24292E"] {
      color: color-mix(in srgb, var(--t-fg) 70%, var(--t-bg)) !important;
    }
    /* Keywords: graph, subgraph, end, participant, -->, classDef, brackets */
    .source-panel .shiki span[style*="#D73A49"],
    .source-panel .shiki span[style*="#d73a49"] {
      color: color-mix(in srgb, var(--t-fg) 90%, var(--t-bg)) !important;
      font-weight: 500;
    }
    /* Direction labels, subgraph names */
    .source-panel .shiki span[style*="#6F42C1"],
    .source-panel .shiki span[style*="#6f42c1"] {
      color: color-mix(in srgb, var(--t-fg) 65%, var(--t-bg)) !important;
    }
    /* Node IDs */
    .source-panel .shiki span[style*="#E36209"],
    .source-panel .shiki span[style*="#e36209"] {
      color: color-mix(in srgb, var(--t-fg) 75%, var(--t-bg)) !important;
    }
    /* Strings, labels, message text */
    .source-panel .shiki span[style*="#032F62"],
    .source-panel .shiki span[style*="#032f62"] {
      color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg)) !important;
    }
    .options {
      margin-top: 0.75rem;
      font-size: 0.8rem;
      color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
    }
    .options code {
      background: color-mix(in srgb, var(--t-fg) 6%, var(--t-bg));
      padding: 0.15rem 0.4rem;
      border-radius: 3px;
      font-size: 0.75rem;
    }

    /* -- Edit button (subtle text link, bottom-left of source panel) -- */
    .edit-btn {
      position: absolute;
      bottom: 0.75rem;
      left: 1.5rem;
      background: none;
      border: none;
      padding: 0;
      font-size: 0.75rem;
      font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
      color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
      cursor: pointer;
      text-decoration: none;
      transition: color 0.15s;
    }
    .edit-btn:hover {
      color: var(--t-fg);
      text-decoration: underline;
    }

    /* -- Edit dialog overlay -- */
    .edit-overlay {
      display: none;
      position: fixed;
      inset: 0;
      z-index: 2000;
      background: rgba(0, 0, 0, 0.4);
      backdrop-filter: blur(4px);
      align-items: center;
      justify-content: center;
    }
    .edit-overlay.open { display: flex; }
    .edit-dialog {
      background: var(--t-bg);
      border-radius: 16px;
      width: min(680px, calc(100vw - 3rem));
      max-height: calc(100vh - 4rem);
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
    .edit-dialog-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 1rem 1.25rem;
      border-bottom: 1px solid color-mix(in srgb, var(--t-fg) 8%, var(--t-bg));
    }
    .edit-dialog-title {
      font-size: 0.95rem;
      font-weight: 600;
      color: var(--t-fg);
    }
    .edit-dialog-close {
      background: none;
      border: none;
      font-size: 1.25rem;
      color: color-mix(in srgb, var(--t-fg) 40%, var(--t-bg));
      cursor: pointer;
      padding: 0 0.25rem;
      line-height: 1;
    }
    .edit-dialog-close:hover { color: var(--t-fg); }
    .edit-dialog-textarea {
      flex: 1;
      min-height: 300px;
      max-height: 60vh;
      margin: 0;
      padding: 1rem 1.25rem;
      border: none;
      outline: none;
      resize: none;
      background: color-mix(in srgb, var(--t-fg) 2%, var(--t-bg));
      color: var(--t-fg);
      font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
      font-size: 0.8rem;
      line-height: 1.5;
      white-space: pre;
      tab-size: 2;
    }
    .edit-dialog-footer {
      display: flex;
      justify-content: flex-end;
      gap: 0.5rem;
      padding: 0.75rem 1.25rem;
      border-top: 1px solid color-mix(in srgb, var(--t-fg) 8%, var(--t-bg));
    }
    .edit-dialog-btn {
      padding: 0.5rem 1rem;
      border-radius: 8px;
      border: none;
      font-size: 0.8rem;
      font-weight: 500;
      font-family: inherit;
      cursor: pointer;
      transition: opacity 0.15s;
    }
    .edit-dialog-btn:hover { opacity: 0.85; }
    .edit-dialog-cancel {
      background: color-mix(in srgb, var(--t-fg) 8%, var(--t-bg));
      color: var(--t-fg);
    }
    .edit-dialog-save {
      background: var(--t-fg);
      color: var(--t-bg);
    }

    /* -- SVG panel -- */
    .svg-panel {
      padding: 1.25rem 1.5rem;
      display: flex;
      flex-direction: column;
      min-width: 0;      /* grid child: allow shrinking below content width */
      /* Background set dynamically: matches the SVG --bg in default mode,
         or the global theme bg when a theme is active. */
    }
    .svg-panel h3 {
      font-size: 0.8rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
      margin-bottom: 0.75rem;
    }
    .svg-container {
      flex: 1;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 0;     /* flex child: allow shrinking to fit */
    }
    .svg-container svg {
      max-width: 100%;
      max-height: 100%;  /* scale down to fit both axes */
      height: auto;
    }

    /* -- ASCII panel -- */
    .ascii-panel {
      padding: 1.25rem 1.5rem;
      border-left: 1px solid color-mix(in srgb, var(--t-fg) 5%, var(--t-bg));
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-width: 0;      /* grid child: allow shrinking below content width */
    }
    .ascii-panel h3 {
      font-size: 0.8rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
      margin-bottom: 0.75rem;
    }
    .ascii-output {
      padding: 1rem;
      font-size: 0.7rem;
      line-height: 1.3;
      overflow-x: auto;   /* horizontal scroll only */
      overflow-y: hidden;  /* scale to height, no vertical scroll */
      white-space: pre;
      flex: 1;
      max-width: 100%;
      font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
    }

    /* -- Loading spinner -- */
    .loading-spinner {
      width: 24px;
      height: 24px;
      border: 2px solid color-mix(in srgb, var(--t-fg) 12%, var(--t-bg));
      border-top-color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
    }
    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    /* -- Timing badge -- */
    .timing {
      font-size: 0.7rem;
      font-weight: 400;
      color: color-mix(in srgb, var(--t-fg) 30%, var(--t-bg));
      margin-left: 0.5rem;
      text-transform: none;
      letter-spacing: normal;
    }

    /* -- Error state -- */
    .render-error {
      color: #ef4444;
      font-size: 0.85rem;
      font-family: 'JetBrains Mono', monospace;
      white-space: pre-wrap;
      word-break: break-word;
    }

    /* -- Hero header section -- */
    .hero-header {
      max-width: 1440px;
      margin: 0 auto;
      padding: 6rem 2rem 2rem;
      text-align: left;
    }
    @media (min-width: 1000px) {
      .hero-header {
        padding: 6rem 3rem 2rem;
      }
    }
    .hero-title {
      font-size: 2.25rem;
      font-weight: 800;
      line-height: 1.2;
      margin: 0 0 0.25rem;
      color: var(--t-fg);
    }
    .hero-tagline {
      font-size: 1rem;
      font-weight: 500;
      color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
      margin: 0 0 1rem;
    }
    .hero-description {
      font-size: 0.95rem;
      line-height: 1.6;
      color: color-mix(in srgb, var(--t-fg) 70%, var(--t-bg));
      margin: 0 0 1.5rem;
      max-width: 680px;
    }
    .hero-description a {
      color: var(--t-fg);
      text-decoration: underline;
      text-underline-offset: 2px;
    }
    .hero-description a:hover {
      color: var(--t-accent);
    }
    .hero-buttons {
      display: flex;
      flex-wrap: wrap;
      align-items: flex-start;
      gap: 0.5rem;
    }
    @media (max-width: 768px) {
      .hero-buttons {
        flex-direction: column;
      }
    }
    .hero-btn {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.75rem 1.25rem;
      font-size: 0.875rem;
      font-weight: 500;
      border-radius: 12px;
      text-decoration: none;
      transition: opacity 0.15s, transform 0.1s;
      cursor: pointer;
      border: none;
      font-family: inherit;
    }
    .hero-btn:hover {
      opacity: 0.9;
    }
    .hero-btn:active {
      transform: translateY(0.5px);
    }
    .hero-btn-primary {
      background: var(--t-fg);
      color: var(--t-bg);
      box-shadow:
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
        rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
    }
    .hero-btn-secondary {
      background: var(--t-bg);
      color: var(--t-fg);
      box-shadow:
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(0, 0, 0, 0) 0px 0px 0px 0px,
        rgba(var(--foreground-rgb), 0.06) 0px 0px 0px 1px,
        rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
        rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
    }
    .hero-btn svg {
      width: 16px;
      height: 16px;
    }
    .hero-description code {
      font-family: 'JetBrains Mono', 'Fira Code', monospace;
      font-size: 0.85em;
      background: color-mix(in srgb, var(--t-fg) 8%, var(--t-bg));
      padding: 0.15em 0.4em;
      border-radius: 4px;
    }

    /* -- Hero meta (below buttons) -- */
    .hero-meta {
      margin-top: 1.25rem;
    }
    .hero-meta .meta {
      font-size: 0.85rem;
      color: color-mix(in srgb, var(--t-fg) 40%, var(--t-bg));
      margin: 0.15rem 0;
    }

    .hero-meta .meta a {
      color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
      text-decoration: underline;
      text-underline-offset: 2px;
    }
    .hero-meta .meta a:hover {
      color: var(--t-fg);
    }

    /* -- Section title -- */
    .section-title {
      font-size: 1.875rem;
      font-weight: 800;
      line-height: 1.2;
      margin: 0;
      padding: 2.5rem 0 1.5rem;
      color: var(--t-fg);
    }

    /* -- Footer -- */
    .site-footer {
      position: relative;
      z-index: 10;
      padding: 1.5rem 2rem 2rem;
      display: flex;
      align-items: center;
      justify-content: space-between;
      max-width: 1440px;
      width: 100%;
      margin: 0 auto;
      font-size: 12px;
      color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
    }
    @media (min-width: 1000px) {
      .site-footer {
        padding: 1.5rem 3rem 2rem;
      }
    }
    .footer-links {
      display: flex;
      align-items: center;
      gap: 1rem;
    }
    .footer-links a {
      color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
      text-decoration: none;
      transition: color 0.15s;
    }
    .footer-links a:hover {
      color: var(--t-fg);
    }
    .footer-links svg {
      width: 1.25rem;
      height: 1.25rem;
      display: block;
    }
  </style>
</head>
<body>
  <!-- Safari 26+ reads title bar color from the topmost fixed element's background.
       This invisible 1px div provides a real DOM element for Safari to detect. -->
  <div id="safari-theme-color" style="position:fixed;top:0;left:0;right:0;height:1px;background:var(--theme-bar-bg);z-index:9999;pointer-events:none;"></div>

  <!-- Navigation + theme bar -->
  <div class="theme-bar" id="theme-bar">
    <div class="brand-badge-wrapper">
      <button class="brand-badge shadow-minimal" id="brand-badge-btn"><svg class="brand-logo" viewBox="0 0 299 300" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M137.879,300.001 L137.875,300.001 C62.3239,300.001 0.966154,239.232 0.0117188,163.908 L2.56478e-10,162.126 L137.879,162.126 L137.879,300.001 Z" fill="#06367A"/><path d="M137.879,0 L137.875,0 C61.729,0 0,61.729 0,137.875 L0,137.878 L137.879,137.878 L137.879,0 Z" fill="#FF51FF"/><path d="M160.558,137.883 L160.561,137.883 C236.707,137.883 298.436,76.1537 298.436,0.00758561 L298.436,0.00562043 L160.558,0.00562043 L160.558,137.883 Z" fill="#007CFF"/><path d="M160.558,162.123 L160.561,162.123 C236.112,162.123 297.471,222.891 298.426,298.216 L298.436,299.998 L160.558,299.998 L160.558,162.123 Z" fill="#0A377B"/></svg><span><strong>Beautiful Mermaid</strong> by Craft</span></button>
      <div class="brand-dropdown shadow-modal-small" id="brand-dropdown">
        <a href="https://agents.craft.do" class="brand-dropdown-item" target="_blank" rel="noopener">
          <svg width="18" height="18" class="brand-dropdown-logo" style="margin-left: -4px;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g transform="translate(3.4502, 3)" fill="#9570BE"><path d="M3.17890888,3.6 L3.17890888,0 L16,0 L16,3.6 L3.17890888,3.6 Z M9.642,7.2 L9.64218223,10.8 L0,10.8 L0,3.6 L16,3.6 L16,7.2 L9.642,7.2 Z M3.17890888,18 L3.178,14.4 L0,14.4 L0,10.8 L16,10.8 L16,18 L3.17890888,18 Z" fill-rule="nonzero"></path></g></svg>
          <span style="margin-left: -2px;">Craft Agents<span class="tagline">Simply mind-blowing</span></span>
        </a>
        <a href="https://craft.do" class="brand-dropdown-item" target="_blank" rel="noopener">
          <svg width="12" height="12" class="brand-dropdown-logo" viewBox="0 0 299 300" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M137.879 300L137.875 300.001C62.3239 300.001 0.966154 239.232 0.0117188 163.908L2.56478e-10 162.126H137.879V300Z" fill="currentColor"/><path d="M137.879 0.000976562L137.875 0C61.729 6.6569e-06 0.000194275 61.729 0 137.875L2.56478e-10 137.878L137.879 137.878L137.879 0.000976562Z" fill="currentColor"/><path d="M160.558 137.882L160.561 137.883C236.707 137.882 298.436 76.1537 298.436 0.00758561V0.00563248L160.558 0.00562043L160.558 137.882Z" fill="currentColor"/><path d="M160.558 162.124L160.561 162.123C236.112 162.123 297.471 222.891 298.426 298.216L298.436 299.998H160.558V162.124Z" fill="currentColor"/></svg>
          <span>Craft Docs<span class="tagline">Amazing Notes &amp; Docs</span></span>
        </a>
      </div>
    </div>
    <button class="contents-btn shadow-minimal" id="contents-btn"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="4" x2="13" y2="4"/><line x1="3" y1="8" x2="13" y2="8"/><line x1="3" y1="12" x2="10" y2="12"/></svg>Contents</button>
    <div class="theme-pills" id="theme-pills">
      ${themePillsHtml}
    </div>
    <div class="mega-menu shadow-modal-small" id="mega-menu">
      <div class="toc-grid">
        ${tocSections}
      </div>
    </div>
  </div>

  <!-- Hero header section -->
  <header class="hero-header">
    <h1 class="hero-title">Beautiful Mermaid</h1>
    <p class="hero-tagline">Mermaid Rendering, made beautiful.</p>
    <p class="hero-description">
      An open source library for rendering diagrams, designed for the age of AI: <a href="https://www.npmjs.com/package/beautiful-mermaid" target="_blank" rel="noopener"><code>beautiful-mermaid</code></a>.
      Ultra-fast, fully themeable, and outputs to both SVG and ASCII.<br>
      Built by the team at <a href="https://craft.do" target="_blank" rel="noopener">Craft</a> — because diagrams deserve great design too.
    </p>
    <div class="hero-buttons">
      <a href="https://agents.craft.do" target="_blank" rel="noopener" class="hero-btn hero-btn-primary">
        <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g transform="translate(3.4502, 3)" fill="currentColor"><path d="M3.17890888,3.6 L3.17890888,0 L16,0 L16,3.6 L3.17890888,3.6 Z M9.642,7.2 L9.64218223,10.8 L0,10.8 L0,3.6 L16,3.6 L16,7.2 L9.642,7.2 Z M3.17890888,18 L3.178,14.4 L0,14.4 L0,10.8 L16,10.8 L16,18 L3.17890888,18 Z" fill-rule="nonzero"></path></g></svg>
        Use in Craft Agents
      </a>
      <a href="https://github.com/lukilabs/beautiful-mermaid" target="_blank" rel="noopener" class="hero-btn hero-btn-secondary">
        <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
        GitHub
      </a>
      <button type="button" class="hero-btn hero-btn-secondary" id="random-theme-btn">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
        Random Theme
      </button>
    </div>
    <div class="hero-meta">
      <p class="meta" id="total-timing">Rendering ${samples.length * 2} samples\u2026</p>
      <div class="meta">ASCII rendering based on <a href="https://github.com/AlexanderGrooff/mermaid-ascii" target="_blank" rel="noopener">Mermaid-ASCII</a></div>
      <div class="meta">Early preview — actively evolving</div>
    </div>
  </header>

  <div class="content-wrapper">

${heroCardsHtml}

  <h2 class="section-title">Samples</h2>

${regularCardsHtml}

  <!-- Bundled mermaid renderer — exposes window.__mermaid -->
  <script type="module">
${bundleJs}

  // ============================================================================
  // Client-side rendering + theme switching
  // ============================================================================

  var samples = ${samplesJson};
  var THEMES = window.__mermaid.THEMES;
  var renderMermaid = window.__mermaid.renderMermaidSVGAsync;
  var renderMermaidAscii = window.__mermaid.renderMermaidASCII;
  var diagramColorsToAsciiTheme = window.__mermaid.diagramColorsToAsciiTheme;
  var getSeriesColor = window.__mermaid.getSeriesColor;
  var CHART_ACCENT_FALLBACK = window.__mermaid.CHART_ACCENT_FALLBACK;

  var totalTimingEl = document.getElementById('total-timing');

  // -- Theme state --
  // Stores each SVG element's original inline style attribute (from initial render)
  // so we can restore per-sample colors when switching back to "Default".
  var originalSvgStyles = [];

  function hexToRgb(hex) {
    if (!hex || typeof hex !== 'string') return null;
    var value = hex.trim();
    if (value[0] === '#') value = value.slice(1);
    if (value.length === 3) {
      value = value[0] + value[0] + value[1] + value[1] + value[2] + value[2];
    }
    if (value.length !== 6) return null;
    var intValue = parseInt(value, 16);
    if (Number.isNaN(intValue)) return null;
    return {
      r: (intValue >> 16) & 255,
      g: (intValue >> 8) & 255,
      b: intValue & 255,
    };
  }

  function setShadowVars(theme) {
    var body = document.body;
    var fg = theme ? theme.fg : '#27272A';
    var bg = theme ? theme.bg : '#FFFFFF';
    var accent = theme ? (theme.accent || '#3b82f6') : '#3b82f6';
    var fgRgb = hexToRgb(fg) || { r: 39, g: 39, b: 42 };
    var bgRgb = hexToRgb(bg) || { r: 255, g: 255, b: 255 };
    var accentRgb = hexToRgb(accent) || { r: 59, g: 130, b: 246 };
    var brightness = (bgRgb.r * 299 + bgRgb.g * 587 + bgRgb.b * 114) / 1000;
    var darkMode = brightness < 140;

    body.style.setProperty('--foreground-rgb', fgRgb.r + ', ' + fgRgb.g + ', ' + fgRgb.b);
    body.style.setProperty('--accent-rgb', accentRgb.r + ', ' + accentRgb.g + ', ' + accentRgb.b);
    body.style.setProperty('--shadow-border-opacity', darkMode ? '0.15' : '0.08');
    body.style.setProperty('--shadow-blur-opacity', darkMode ? '0.12' : '0.06');
  }

  // Update <meta name="theme-color"> so Safari 26+ title bar matches the page.
  // Computes color-mix(in srgb, fg 4%, bg) in JS since browsers may not
  // reliably re-evaluate CSS color-mix() for the meta tag.
  function updateThemeColor(fg, bg) {
    var fgRgb = hexToRgb(fg) || { r: 39, g: 39, b: 42 };
    var bgRgb = hexToRgb(bg) || { r: 255, g: 255, b: 255 };
    // Mix: 4% foreground, 96% background (matches body CSS)
    var r = Math.round(bgRgb.r * 0.96 + fgRgb.r * 0.04);
    var g = Math.round(bgRgb.g * 0.96 + fgRgb.g * 0.04);
    var b = Math.round(bgRgb.b * 0.96 + fgRgb.b * 0.04);
    var hex = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
    document.getElementById('theme-color-meta').setAttribute('content', hex);
    // Update --theme-bar-bg on body so gradients update instantly
    document.body.style.setProperty('--theme-bar-bg', hex);
    // Force Safari 26+ to re-read title bar color by updating the invisible fixed div
    // and triggering a reflow (display toggle + offsetHeight read)
    var safariDiv = document.getElementById('safari-theme-color');
    safariDiv.style.background = hex;
    safariDiv.style.display = 'none';
    void safariDiv.offsetHeight;
    safariDiv.style.display = '';
  }

  // ----------------------------------------------------------------
  // Apply a named theme (or '' for Default) to the entire page.
  //
  // This is instant — no re-rendering needed. SVGs use CSS custom
  // properties internally, so updating --bg/--fg on the <svg> tag
  // re-paints all nodes, edges, text, and backgrounds via color-mix().
  // ----------------------------------------------------------------
  function applyTheme(themeKey) {
    var theme = themeKey ? THEMES[themeKey] : null;
    var body = document.body;

    // 1. Update body CSS variables — the entire page derives from these
    if (theme) {
      body.style.setProperty('--t-bg', theme.bg);
      body.style.setProperty('--t-fg', theme.fg);
      body.style.setProperty('--t-accent', theme.accent || '#3b82f6');
    } else {
      body.style.setProperty('--t-bg', '#FFFFFF');
      body.style.setProperty('--t-fg', '#27272A');
      body.style.setProperty('--t-accent', '#3b82f6');
    }
    setShadowVars(theme);
    updateThemeColor(theme ? theme.fg : '#27272A', theme ? theme.bg : '#FFFFFF');

    // 2. Update all rendered SVG elements' CSS variables
    var svgs = document.querySelectorAll('.svg-container svg');
    for (var j = 0; j < svgs.length; j++) {
      var svgEl = svgs[j];
      if (theme) {
        // Override with the global theme colors
        svgEl.style.setProperty('--bg', theme.bg);
        svgEl.style.setProperty('--fg', theme.fg);
        // Set enrichment variables if provided, else remove so SVG
        // internal color-mix() fallbacks activate
        var enrichment = ['line', 'accent', 'muted', 'surface', 'border'];
        for (var k = 0; k < enrichment.length; k++) {
          var prop = enrichment[k];
          if (theme[prop]) svgEl.style.setProperty('--' + prop, theme[prop]);
          else svgEl.style.removeProperty('--' + prop);
        }
        // Recompute xychart series color vars from the new accent
        var maxColor = parseInt(svgEl.getAttribute('data-xychart-colors') || '-1', 10);
        if (maxColor >= 0) {
          var accent = theme.accent || CHART_ACCENT_FALLBACK;
          svgEl.style.setProperty('--xychart-color-0', accent);
          for (var ci = 1; ci <= maxColor; ci++) {
            svgEl.style.setProperty('--xychart-color-' + ci, getSeriesColor(ci, accent, theme.bg));
          }
        }
      } else {
        // Restore original inline style from initial render
        if (originalSvgStyles[j] !== undefined) {
          svgEl.setAttribute('style', originalSvgStyles[j]);
        }
      }
    }

    // 3. Update SVG panel backgrounds to match (skip hero panels - keep transparent)
    for (var j = 0; j < samples.length; j++) {
      var panel = document.getElementById('svg-panel-' + j);
      if (!panel) continue;
      // Skip hero panels - they stay transparent
      if (panel.classList.contains('hero-diagram-panel')) continue;
      if (theme) {
        panel.style.background = theme.bg;
      } else {
        // Default mode: use the per-sample bg (or clear for page default)
        var sampleBg = panel.getAttribute('data-sample-bg');
        panel.style.background = sampleBg || '';
      }
    }

    // 4. Re-render ASCII panels with new theme colors
    var asciiTheme = theme ? diagramColorsToAsciiTheme(theme) : null;
    for (var j = 0; j < samples.length; j++) {
      var asciiEl = document.getElementById('ascii-' + j);
      if (!asciiEl) continue;
      try {
        asciiEl.innerHTML = renderMermaidAscii(
          samples[j].source,
          asciiTheme ? { theme: asciiTheme } : {}
        );
      } catch (e) { /* keep existing content */ }
    }

    // 5. Update active pill
    var pills = document.querySelectorAll('.theme-pill');
    for (var j = 0; j < pills.length; j++) {
      var isActive = pills[j].getAttribute('data-theme') === themeKey;
      pills[j].classList.toggle('active', isActive);
      pills[j].classList.toggle('shadow-tinted', isActive);
    }

    // 6. Persist selection
    if (themeKey) {
      localStorage.setItem('mermaid-theme', themeKey);
    } else {
      localStorage.removeItem('mermaid-theme');
    }
  }

  // -- Set up theme pill click handlers --
  document.getElementById('theme-pills').addEventListener('click', function(e) {
    var pill = e.target.closest('.theme-pill');
    if (!pill || pill.id === 'theme-more-btn') return;
    applyTheme(pill.getAttribute('data-theme') || '');
    // Close "More" dropdown if a theme was picked from it
    var dd = document.getElementById('theme-more-dropdown');
    if (dd && dd.classList.contains('open')) dd.classList.remove('open');
  });

  // -- "More" themes dropdown (direct listener, same pattern as Contents) --
  var moreBtn = document.getElementById('theme-more-btn');
  var moreDropdown = document.getElementById('theme-more-dropdown');

  if (moreBtn && moreDropdown) {
    moreBtn.addEventListener('click', function(e) {
      e.stopPropagation();
      moreDropdown.classList.toggle('open');
    });

    // Close on outside click
    document.addEventListener('click', function(e) {
      if (!moreDropdown.classList.contains('open')) return;
      if (!e.target.closest('.theme-more-wrapper')) {
        moreDropdown.classList.remove('open');
      }
    });

    // Close on Escape
    document.addEventListener('keydown', function(e) {
      if (e.key === 'Escape' && moreDropdown.classList.contains('open')) {
        moreDropdown.classList.remove('open');
      }
    });
  }

  // -- Random theme button --
  var randomThemeBtn = document.getElementById('random-theme-btn');
  var themeKeys = Object.keys(THEMES);
  var currentThemeKey = localStorage.getItem('mermaid-theme') || '';

  if (randomThemeBtn) {
    randomThemeBtn.addEventListener('click', function() {
      // Filter out the current theme so we never pick the same one
      var availableKeys = themeKeys.filter(function(k) { return k !== currentThemeKey; });
      // Also include default ('') if not currently selected
      if (currentThemeKey !== '') availableKeys.push('');
      // Pick a random theme
      var randomIndex = Math.floor(Math.random() * availableKeys.length);
      var newThemeKey = availableKeys[randomIndex];
      currentThemeKey = newThemeKey;
      applyTheme(newThemeKey);
    });
  }

  // -- Brand dropdown --
  var brandBtn = document.getElementById('brand-badge-btn');
  var brandDropdown = document.getElementById('brand-dropdown');

  if (brandBtn && brandDropdown) {
    brandBtn.addEventListener('click', function(e) {
      e.stopPropagation();
      var isOpen = brandDropdown.classList.toggle('open');
      brandBtn.classList.toggle('active', isOpen);
      brandBtn.classList.toggle('shadow-tinted', isOpen);
    });

    // Close on outside click
    document.addEventListener('click', function(e) {
      if (!brandDropdown.classList.contains('open')) return;
      if (!e.target.closest('.brand-badge-wrapper')) {
        brandDropdown.classList.remove('open');
        brandBtn.classList.remove('active');
        brandBtn.classList.remove('shadow-tinted');
      }
    });

    // Close on Escape
    document.addEventListener('keydown', function(e) {
      if (e.key === 'Escape' && brandDropdown.classList.contains('open')) {
        brandDropdown.classList.remove('open');
        brandBtn.classList.remove('active');
        brandBtn.classList.remove('shadow-tinted');
      }
    });
  }

  // -- Mega menu (Contents dropdown) --
  var contentsBtn = document.getElementById('contents-btn');
  var megaMenu = document.getElementById('mega-menu');

  contentsBtn.addEventListener('click', function(e) {
    e.stopPropagation();
    var isOpen = megaMenu.classList.toggle('open');
    contentsBtn.classList.toggle('active', isOpen);
    contentsBtn.classList.toggle('shadow-tinted', isOpen);
  });

  // Close on clicking a ToC link (smooth scroll to target)
  megaMenu.addEventListener('click', function(e) {
    var link = e.target.closest('a');
    if (!link) return;
    e.preventDefault();
    megaMenu.classList.remove('open');
    contentsBtn.classList.remove('active');
    contentsBtn.classList.remove('shadow-tinted');
    var target = document.querySelector(link.getAttribute('href'));
    if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
  });

  // Close on outside click
  document.addEventListener('click', function(e) {
    if (!megaMenu.classList.contains('open')) return;
    if (!e.target.closest('.mega-menu') && !e.target.closest('.contents-btn')) {
      megaMenu.classList.remove('open');
      contentsBtn.classList.remove('active');
      contentsBtn.classList.remove('shadow-tinted');
    }
  });

  // Close on Escape
  document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape' && megaMenu.classList.contains('open')) {
      megaMenu.classList.remove('open');
      contentsBtn.classList.remove('active');
      contentsBtn.classList.remove('shadow-tinted');
    }
  });

  // -- Restore saved theme immediately (before rendering begins) --
  var savedTheme = localStorage.getItem('mermaid-theme');
  if (savedTheme && THEMES[savedTheme]) {
    // Apply page-level CSS variables right away to avoid flash
    document.body.style.setProperty('--t-bg', THEMES[savedTheme].bg);
    document.body.style.setProperty('--t-fg', THEMES[savedTheme].fg);
    document.body.style.setProperty('--t-accent', THEMES[savedTheme].accent || '#3b82f6');
    setShadowVars(THEMES[savedTheme]);
    updateThemeColor(THEMES[savedTheme].fg, THEMES[savedTheme].bg);
    // Mark the correct pill as active
    var pills = document.querySelectorAll('.theme-pill');
    for (var j = 0; j < pills.length; j++) {
      var isActive = pills[j].getAttribute('data-theme') === savedTheme;
      pills[j].classList.toggle('active', isActive);
      pills[j].classList.toggle('shadow-tinted', isActive);
    }
  } else {
    setShadowVars(null);
  }

  // ============================================================================
  // Progressive rendering — render each diagram sequentially
  // ============================================================================

  var totalStart = performance.now();

  for (var i = 0; i < samples.length; i++) {
    var sample = samples[i];
    var svgContainer = document.getElementById('svg-' + i);
    var asciiContainer = document.getElementById('ascii-' + i);
    var svgPanel = document.getElementById('svg-panel-' + i);

    // Render SVG — wrapped in a timeout guard so a stalled layout
    // doesn't block all remaining diagrams from rendering.
    try {
      var svg = await renderMermaid(sample.source, sample.options);
      svgContainer.innerHTML = svg;

      // Store the SVG's original inline style for Default mode restoration
      var svgEl = svgContainer.querySelector('svg');
      if (svgEl) {
        originalSvgStyles.push(svgEl.getAttribute('style') || '');

        // If a global theme is active, immediately override the SVG's variables
        if (savedTheme && THEMES[savedTheme]) {
          var th = THEMES[savedTheme];
          svgEl.style.setProperty('--bg', th.bg);
          svgEl.style.setProperty('--fg', th.fg);
          var enrichment = ['line', 'accent', 'muted', 'surface', 'border'];
          for (var k = 0; k < enrichment.length; k++) {
            if (th[enrichment[k]]) svgEl.style.setProperty('--' + enrichment[k], th[enrichment[k]]);
            else svgEl.style.removeProperty('--' + enrichment[k]);
          }
          // Recompute xychart series color vars from the saved theme's accent
          var maxColor = parseInt(svgEl.getAttribute('data-xychart-colors') || '-1', 10);
          if (maxColor >= 0) {
            var accent = th.accent || CHART_ACCENT_FALLBACK;
            svgEl.style.setProperty('--xychart-color-0', accent);
            for (var ci = 1; ci <= maxColor; ci++) {
              svgEl.style.setProperty('--xychart-color-' + ci, getSeriesColor(ci, accent, th.bg));
            }
          }
        }
      } else {
        originalSvgStyles.push('');
      }

      // Set panel background to match the SVG (skip for hero panels - keep transparent)
      var isHeroPanel = svgPanel.classList.contains('hero-diagram-panel');
      if (!isHeroPanel) {
        if (savedTheme && THEMES[savedTheme]) {
          svgPanel.style.background = THEMES[savedTheme].bg;
        } else {
          var sampleBg = svgPanel.getAttribute('data-sample-bg');
          if (sampleBg) svgPanel.style.background = sampleBg;
        }
      }
    } catch (err) {
      svgContainer.innerHTML = '<div class="render-error">SVG Error: ' + escapeHtml(String(err)) + '</div>';
      originalSvgStyles.push('');
    }

    // Hero samples don't have ASCII panels
    if (asciiContainer) {
      try {
        var asciiOpts = savedTheme && THEMES[savedTheme]
          ? { theme: diagramColorsToAsciiTheme(THEMES[savedTheme]) }
          : {};
        asciiContainer.innerHTML = renderMermaidAscii(sample.source, asciiOpts);
      } catch (e) {
        asciiContainer.textContent = '(ASCII not supported for this diagram type)';
      }
    }

  }

  // Done — show total time
  var totalMs = (performance.now() - totalStart).toFixed(0);
  totalTimingEl.textContent = (samples.length * 2) + ' samples (SVG+ASCII) rendered in ' + totalMs + ' ms';

  function escapeHtml(text) {
    return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }

  // ============================================================================
  // Edit dialog — open, close, save & re-render
  // ============================================================================

  var editOverlay = document.getElementById('edit-overlay');
  var editTextarea = document.getElementById('edit-dialog-textarea');
  var editSaveBtn = document.getElementById('edit-dialog-save');
  var editCancelBtn = document.getElementById('edit-dialog-cancel');
  var editCloseBtn = document.getElementById('edit-dialog-close');
  var editingSampleIndex = -1;

  function openEditDialog(index) {
    editingSampleIndex = index;
    editTextarea.value = samples[index].source;
    editOverlay.classList.add('open');
    editTextarea.focus();
  }

  function closeEditDialog() {
    editOverlay.classList.remove('open');
    editingSampleIndex = -1;
  }

  async function saveAndRender() {
    var index = editingSampleIndex;
    if (index < 0) return;
    var source = editTextarea.value;
    samples[index].source = source;

    // Close dialog immediately so user sees results rendering
    closeEditDialog();

    // Update source panel with plain text (Shiki not available at runtime)
    var sourcePanel = document.getElementById('source-panel-' + index);
    if (sourcePanel) {
      var shikiEl = sourcePanel.querySelector('.shiki');
      if (shikiEl) {
        shikiEl.innerHTML = '<code>' + escapeHtml(source) + '</code>';
      }
    }

    // Re-render SVG (async — renderMermaid returns a Promise)
    var svgContainer = document.getElementById('svg-' + index);
    try {
      var svg = await renderMermaid(source, samples[index].options);
      svgContainer.innerHTML = svg;
      var svgEl = svgContainer.querySelector('svg');
      if (svgEl) {
        originalSvgStyles[index] = svgEl.getAttribute('style') || '';
        var activeTheme = localStorage.getItem('mermaid-theme');
        if (activeTheme && THEMES[activeTheme]) {
          var th = THEMES[activeTheme];
          svgEl.style.setProperty('--bg', th.bg);
          svgEl.style.setProperty('--fg', th.fg);
          var enrichment = ['line', 'accent', 'muted', 'surface', 'border'];
          for (var k = 0; k < enrichment.length; k++) {
            if (th[enrichment[k]]) svgEl.style.setProperty('--' + enrichment[k], th[enrichment[k]]);
            else svgEl.style.removeProperty('--' + enrichment[k]);
          }
          // Recompute xychart series color vars
          var maxColor = parseInt(svgEl.getAttribute('data-xychart-colors') || '-1', 10);
          if (maxColor >= 0) {
            var accent = th.accent || CHART_ACCENT_FALLBACK;
            svgEl.style.setProperty('--xychart-color-0', accent);
            for (var ci = 1; ci <= maxColor; ci++) {
              svgEl.style.setProperty('--xychart-color-' + ci, getSeriesColor(ci, accent, th.bg));
            }
          }
        }
      }
    } catch (err) {
      svgContainer.innerHTML = '<div class="render-error">' + escapeHtml(String(err)) + '</div>';
    }

    // Re-render ASCII
    var asciiContainer = document.getElementById('ascii-' + index);
    if (asciiContainer) {
      try {
        var activeThemeKey = localStorage.getItem('mermaid-theme');
        var editAsciiOpts = activeThemeKey && THEMES[activeThemeKey]
          ? { theme: diagramColorsToAsciiTheme(THEMES[activeThemeKey]) }
          : {};
        asciiContainer.innerHTML = renderMermaidAscii(source, editAsciiOpts);
      } catch (e) {
        asciiContainer.textContent = '(ASCII error: ' + e.message + ')';
      }
    }
  }

  // Event listeners
  document.addEventListener('click', function(e) {
    var btn = e.target.closest('.edit-btn');
    if (btn) openEditDialog(parseInt(btn.dataset.sample, 10));
  });
  editSaveBtn.addEventListener('click', saveAndRender);
  editCancelBtn.addEventListener('click', closeEditDialog);
  editCloseBtn.addEventListener('click', closeEditDialog);
  editOverlay.addEventListener('click', function(e) {
    if (e.target === editOverlay) closeEditDialog();
  });
  document.addEventListener('keydown', function(e) {
    if (!editOverlay.classList.contains('open')) return;
    if (e.key === 'Escape') closeEditDialog();
    if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') saveAndRender();
  });

  </script>

  <!-- Edit dialog (shared single instance) -->
  <div class="edit-overlay" id="edit-overlay">
    <div class="edit-dialog shadow-modal-small">
      <div class="edit-dialog-header">
        <span class="edit-dialog-title">Edit Diagram</span>
        <button class="edit-dialog-close" id="edit-dialog-close">&times;</button>
      </div>
      <textarea class="edit-dialog-textarea" id="edit-dialog-textarea"
        spellcheck="false" autocomplete="off" autocorrect="off"></textarea>
      <div class="edit-dialog-footer">
        <button class="edit-dialog-btn edit-dialog-cancel" id="edit-dialog-cancel">Cancel</button>
        <button class="edit-dialog-btn edit-dialog-save" id="edit-dialog-save">Save &amp; Render</button>
      </div>
    </div>
  </div>

  </div><!-- .content-wrapper -->

  <footer class="site-footer">
    <span>&copy; 2026 Craft Docs Limited, Inc. All rights reserved.</span>
    <div class="footer-links">
      <a href="mailto:agents@craft.do">Contact</a>
      <a href="https://github.com/lukilabs/beautiful-mermaid" target="_blank" rel="noopener noreferrer">
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
        </svg>
      </a>
      <a href="https://x.com/craftdocs" target="_blank" rel="noopener noreferrer">
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
        </svg>
      </a>
    </div>
  </footer>
</body>
</html>`
}

// ============================================================================
// Main
// ============================================================================

const html = await generateHtml()
const outPath = new URL('./index.html', import.meta.url).pathname
await Bun.write(outPath, html)
console.log(`Written to ${outPath} (${(html.length / 1024).toFixed(1)} KB)`)


================================================
FILE: package.json
================================================
{
  "name": "beautiful-mermaid",
  "version": "1.1.3",
  "license": "MIT",
  "description": "Render Mermaid diagrams as beautiful SVGs or ASCII art. Ultra-fast, fully themeable, zero DOM dependencies.",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "bun": "./src/index.ts",
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/lukilabs/beautiful-mermaid"
  },
  "homepage": "https://github.com/lukilabs/beautiful-mermaid",
  "bugs": {
    "url": "https://github.com/lukilabs/beautiful-mermaid/issues"
  },
  "keywords": [
    "mermaid",
    "diagram",
    "svg",
    "ascii",
    "flowchart",
    "sequence-diagram",
    "class-diagram",
    "er-diagram",
    "xychart",
    "state-diagram",
    "visualization",
    "theming"
  ],
  "author": "Craft Docs",
  "files": ["src/", "dist/", "LICENSE", "README.md"],
  "scripts": {
    "test": "bun test src/__tests__/",
    "samples": "bun run index.ts",
    "dev": "bun run dev.ts",
    "bench": "bun run bench.ts",
    "build": "tsup",
    "build:samples": "bun run index.ts && mkdir -p site && mv index.html site/ && cp -r public/* site/",
    "deploy": "bun run build:samples && wrangler pages deploy site --project-name craft-agents-mermaid",
    "xychart-test": "bun run xychart-test.ts",
    "prepublishOnly": "npm run build"
  },
  "dependencies": {
    "elkjs": "^0.11.0",
    "entities": "^7.0.1"
  },
  "devDependencies": {
    "@types/bun": "^1.3.9",
    "shiki": "^3.19.0",
    "tsup": "^8.5.1",
    "typescript": "^5.9.3"
  }
}


================================================
FILE: samples-data.ts
================================================
/**
 * Sample definitions for the beautiful-mermaid visual test suite.
 *
 * Shared by:
 *   - index.ts     — generates the HTML visual test page
 *   - bench.ts     — runs performance benchmarks in Bun (no browser)
 *   - dev.ts       — dev server with live reload
 *
 * Every supported feature, shape, edge type, block construct, and theme
 * variant is exercised by at least one sample.
 */

export interface Sample {
  title: string
  description: string
  source: string
  /** Optional category tag for grouping in the Table of Contents */
  category?: string
  options?: { bg?: string; fg?: string; line?: string; accent?: string; muted?: string; surface?: string; border?: string; font?: string; padding?: number; transparent?: boolean; interactive?: boolean }
}

export const samples: Sample[] = [

  // ══════════════════════════════════════════════════════════════════════════
  //  HERO — Showcase diagram
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Beautiful Mermaid',
    category: 'Hero',
    description: 'Mermaid rendering, made beautiful.',
    source: `stateDiagram-v2
    direction LR
    [*] --> Input
    Input --> Parse: DSL
    Parse --> Layout: AST
    Layout --> SVG: Vector
    Layout --> ASCII: Text
    SVG --> Theme
    ASCII --> Theme
    Theme --> Output
    Output --> [*]`,
    options: { transparent: true },
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  FLOWCHART — Shapes
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Simple Flow',
    category: 'Flowchart',
    description: 'Basic linear flow with three nodes connected by solid arrows.',
    source: `graph TD
  A[Start] --> B[Process] --> C[End]`,
  },
  {
    title: 'Original Node Shapes',
    category: 'Flowchart',
    description: 'Rectangle, rounded, diamond, stadium, and circle.',
    source: `graph LR
  A[Rectangle] --> B(Rounded)
  B --> C{Diamond}
  C --> D([Stadium])
  D --> E((Circle))`,
  },
  {
    title: 'Batch 1 Shapes',
    category: 'Flowchart',
    description: 'Subroutine `[[text]]`, double circle `(((text)))`, and hexagon `{{text}}`.',
    source: `graph LR
  A[[Subroutine]] --> B(((Double Circle)))
  B --> C{{Hexagon}}`,
  },
  {
    title: 'Batch 2 Shapes',
    category: 'Flowchart',
    description: 'Cylinder `[(text)]`, asymmetric `>text]`, trapezoid `[/text\\]`, and inverse trapezoid `[\\text/]`.',
    source: `graph LR
  A[(Database)] --> B>Flag Shape]
  B --> C[/Wider Bottom\\]
  C --> D[\\Wider Top/]`,
  },
  {
    title: 'All 12 Flowchart Shapes',
    category: 'Flowchart',
    description: 'Every supported flowchart shape in a single diagram.',
    source: `graph LR
  A[Rectangle] --> B(Rounded)
  B --> C{Diamond}
  C --> D([Stadium])
  D --> E((Circle))
  E --> F[[Subroutine]]
  F --> G(((Double Circle)))
  G --> H{{Hexagon}}
  H --> I[(Database)]
  I --> J>Flag]
  J --> K[/Trapezoid\\]
  K --> L[\\Inverse Trap/]`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  FLOWCHART — Edges
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'All Edge Styles',
    category: 'Flowchart',
    description: 'Solid, dotted, and thick arrows with labels.',
    source: `graph TD
  A[Source] -->|solid| B[Target 1]
  A -.->|dotted| C[Target 2]
  A ==>|thick| D[Target 3]`,
  },
  {
    title: 'No-Arrow Edges',
    category: 'Flowchart',
    description: 'Lines without arrowheads: solid `---`, dotted `-.-`, thick `===`.',
    source: `graph TD
  A[Node 1] ---|related| B[Node 2]
  B -.- C[Node 3]
  C === D[Node 4]`,
  },
  {
    title: 'Text-Embedded Labels',
    category: 'Flowchart',
    description: 'Using `-- label -->` syntax instead of `-->|label|` for edge labels.',
    source: `flowchart TD
  A(Start) --> B{Is it sunny?}
  B -- Yes --> C[Go to the park]
  B -- No --> D[Stay indoors]
  C --> E[Finish]
  D --> E`,
  },
  {
    title: 'Bidirectional Arrows',
    category: 'Flowchart',
    description: 'Arrows in both directions: `<-->`, `<-.->`, `<==>`.',
    source: `graph LR
  A[Client] <-->|sync| B[Server]
  B <-.->|heartbeat| C[Monitor]
  C <==>|data| D[Storage]`,
  },
  {
    title: 'Parallel Links (&)',
    category: 'Flowchart',
    description: 'Using `&` to create multiple edges from/to groups of nodes.',
    source: `graph TD
  A[Input] & B[Config] --> C[Processor]
  C --> D[Output] & E[Log]`,
  },
  {
    title: 'Chained Edges',
    category: 'Flowchart',
    description: 'A long chain of nodes demonstrating edge chaining syntax.',
    source: `graph LR
  A[Step 1] --> B[Step 2] --> C[Step 3] --> D[Step 4] --> E[Step 5]`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  FLOWCHART — Edge Styling (linkStyle)
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'linkStyle: Color-Coded Edges',
    category: 'Flowchart',
    description: 'Using `linkStyle` to color specific edges by index (0-based).',
    source: `graph TD
  A[Start] --> B{Decision}
  B -->|Yes| C[Accept]
  B -->|No| D[Reject]
  C --> E[Done]
  D --> E
  linkStyle 0 stroke:#7aa2f7,stroke-width:3px
  linkStyle 1 stroke:#9ece6a,stroke-width:2px
  linkStyle 2 stroke:#f7768e,stroke-width:2px
  linkStyle default stroke:#565f89`,
  },
  {
    title: 'linkStyle: Default + Override',
    category: 'Flowchart',
    description: 'Default edge style with index-specific overrides for critical paths.',
    source: `graph LR
  A[Request] --> B[Auth]
  B --> C[Process]
  C --> D[Response]
  B --> E[Reject]
  linkStyle default stroke:#6b7280,stroke-width:1px
  linkStyle 0,1,2 stroke:#22c55e,stroke-width:2px
  linkStyle 3 stroke:#ef4444,stroke-width:3px`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  FLOWCHART — Directions
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Direction: Left-Right (LR)',
    category: 'Flowchart',
    description: 'Horizontal layout flowing left to right.',
    source: `graph LR
  A[Input] --> B[Transform] --> C[Output]`,
  },
  {
    title: 'Direction: Bottom-Top (BT)',
    category: 'Flowchart',
    description: 'Vertical layout flowing from bottom to top.',
    source: `graph BT
  A[Foundation] --> B[Layer 2] --> C[Top]`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  FLOWCHART — Subgraphs
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Subgraphs',
    category: 'Flowchart',
    description: 'Grouped nodes inside labeled subgraph containers.',
    source: `graph TD
  subgraph Frontend
    A[React App] --> B[State Manager]
  end
  subgraph Backend
    C[API Server] --> D[Database]
  end
  B --> C`,
  },
  {
    title: 'Nested Subgraphs',
    category: 'Flowchart',
    description: 'Subgraphs inside subgraphs for hierarchical grouping.',
    source: `graph TD
  subgraph Cloud
    subgraph us-east [US East Region]
      A[Web Server] --> B[App Server]
    end
    subgraph us-west [US West Region]
      C[Web Server] --> D[App Server]
    end
  end
  E[Load Balancer] --> A
  E --> C`,
  },
  {
    title: 'Subgraph Direction Override',
    category: 'Flowchart',
    description: 'Using `direction LR` inside a subgraph while the outer graph flows TD.',
    source: `graph TD
  subgraph pipeline [Processing Pipeline]
    direction LR
    A[Input] --> B[Parse] --> C[Transform] --> D[Output]
  end
  E[Source] --> A
  D --> F[Sink]`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  FLOWCHART — Styling
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: '::: Class Shorthand',
    category: 'Flowchart',
    description: 'Assigning classes with `:::` syntax directly on node definitions.',
    source: `graph TD
  A[Normal]:::default --> B[Highlighted]:::highlight --> C[Error]:::error
  classDef default fill:#f4f4f5,stroke:#a1a1aa
  classDef highlight fill:#fbbf24,stroke:#d97706
  classDef error fill:#ef4444,stroke:#dc2626`,
  },
  {
    title: 'Inline Style Overrides',
    category: 'Flowchart',
    description: 'Using `style` statements to override node fill and stroke colors.',
    source: `graph TD
  A[Default] --> B[Custom Colors] --> C[Another Custom]
  style B fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
  style C fill:#10b981,stroke:#059669`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  FLOWCHART — Real-World Diagrams
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'CI/CD Pipeline',
    category: 'Flowchart',
    description: 'A realistic CI/CD pipeline with decision points, feedback loops, and deployment stages.',
    source: `graph TD
  subgraph ci [CI Pipeline]
    A[Push Code] --> B{Tests Pass?}
    B -->|Yes| C[Build Image]
    B -->|No| D[Fix & Retry]
    D -.-> A
  end
  C --> E([Deploy Staging])
  E --> F{QA Approved?}
  F -->|Yes| G((Production))
  F -->|No| D`,
  },
  {
    title: 'System Architecture',
    category: 'Flowchart',
    description: 'A microservices architecture with multiple services and data stores.',
    source: `graph LR
  subgraph clients [Client Layer]
    A([Web App]) --> B[API Gateway]
    C([Mobile App]) --> B
  end
  subgraph services [Service Layer]
    B --> D[Auth Service]
    B --> E[User Service]
    B --> F[Order Service]
  end
  subgraph data [Data Layer]
    D --> G[(Auth DB)]
    E --> H[(User DB)]
    F --> I[(Order DB)]
    F --> J([Message Queue])
  end`,
  },
  {
    title: 'Decision Tree',
    category: 'Flowchart',
    description: 'A branching decision flowchart with multiple outcomes.',
    source: `graph TD
  A{Is it raining?} -->|Yes| B{Have umbrella?}
  A -->|No| C([Go outside])
  B -->|Yes| D([Go with umbrella])
  B -->|No| E{Is it heavy?}
  E -->|Yes| F([Stay inside])
  E -->|No| G([Run for it])`,
  },
  {
    title: 'Git Branching Workflow',
    category: 'Flowchart',
    description: 'A git flow showing feature branches, PRs, and release cycle.',
    source: `graph LR
  A[main] --> B[develop]
  B --> C[feature/auth]
  B --> D[feature/ui]
  C --> E{PR Review}
  D --> E
  E -->|approved| B
  B --> F[release/1.0]
  F --> G{Tests?}
  G -->|pass| A
  G -->|fail| F`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  STATE DIAGRAMS
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Basic State Diagram',
    category: 'State',
    description: 'A simple `stateDiagram-v2` with start/end pseudostates and transitions.',
    source: `stateDiagram-v2
  [*] --> Idle
  Idle --> Active : start
  Active --> Idle : cancel
  Active --> Done : complete
  Done --> [*]`,
  },
  {
    title: 'State: Composite States',
    category: 'State',
    description: 'Nested composite states with inner transitions.',
    source: `stateDiagram-v2
  [*] --> Idle
  Idle --> Processing : submit
  state Processing {
    parse --> validate
    validate --> execute
  }
  Processing --> Complete : done
  Processing --> Error : fail
  Error --> Idle : retry
  Complete --> [*]`,
  },
  {
    title: 'State: Connection Lifecycle',
    category: 'State',
    description: 'TCP-like connection state machine with multiple states.',
    source: `stateDiagram-v2
  [*] --> Closed
  Closed --> Connecting : connect
  Connecting --> Connected : success
  Connecting --> Closed : timeout
  Connected --> Disconnecting : close
  Connected --> Reconnecting : error
  Reconnecting --> Connected : success
  Reconnecting --> Closed : max_retries
  Disconnecting --> Closed : done
  Closed --> [*]`,
  },

  {
    title: 'State: CJK State Names',
    category: 'State',
    description: 'State diagram using Chinese characters for state names.',
    source: `stateDiagram-v2
  [*] --> 空闲
  空闲 --> 处理中 : 提交
  处理中 --> 完成 : 成功
  处理中 --> 错误 : 失败
  错误 --> 空闲 : 重试
  完成 --> [*]`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  SEQUENCE DIAGRAMS — Core Features
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Sequence: Basic Messages',
    category: 'Sequence',
    description: 'Simple request/response between two participants.',
    source: `sequenceDiagram
  Alice->>Bob: Hello Bob!
  Bob-->>Alice: Hi Alice!`,
  },
  {
    title: 'Sequence: Participant Aliases',
    category: 'Sequence',
    description: 'Using `participant ... as ...` for compact diagram IDs with readable labels.',
    source: `sequenceDiagram
  participant A as Alice
  participant B as Bob
  participant C as Charlie
  A->>B: Hello
  B->>C: Forward
  C-->>A: Reply`,
  },
  {
    title: 'Sequence: Actor Stick Figures',
    category: 'Sequence',
    description: 'Using `actor` instead of `participant` renders stick figures instead of boxes.',
    source: `sequenceDiagram
  actor U as User
  participant S as System
  participant DB as Database
  U->>S: Click button
  S->>DB: Query
  DB-->>S: Results
  S-->>U: Display`,
  },
  {
    title: 'Sequence: Arrow Types',
    category: 'Sequence',
    description: 'All arrow types: solid `->>` and dashed `-->>` with filled arrowheads, open arrows `-)` .',
    source: `sequenceDiagram
  A->>B: Solid arrow (sync)
  B-->>A: Dashed arrow (return)
  A-)B: Open arrow (async)
  B--)A: Open dashed arrow`,
  },
  {
    title: 'Sequence: Activation Boxes',
    category: 'Sequence',
    description: 'Using `+` and `-` to show when participants are active.',
    source: `sequenceDiagram
  participant C as Client
  participant S as Server
  C->>+S: Request
  S->>+S: Process
  S->>-S: Done
  S-->>-C: Response`,
  },
  {
    title: 'Sequence: Self-Messages',
    category: 'Sequence',
    description: 'A participant sending a message to itself (displayed as a loop arrow).',
    source: `sequenceDiagram
  participant S as Server
  S->>S: Internal process
  S->>S: Validate
  S-->>S: Log`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  SEQUENCE DIAGRAMS — Blocks
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Sequence: Loop Block',
    category: 'Sequence',
    description: 'A `loop` construct wrapping repeated message exchanges.',
    source: `sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: Connect
  loop Every 30s
    C->>S: Heartbeat
    S-->>C: Ack
  end
  C->>S: Disconnect`,
  },
  {
    title: 'Sequence: Alt/Else Block',
    category: 'Sequence',
    description: 'Conditional branching with `alt` (if) and `else` blocks.',
    source: `sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: Login
  alt Valid credentials
    S-->>C: 200 OK
  else Invalid
    S-->>C: 401 Unauthorized
  else Account locked
    S-->>C: 403 Forbidden
  end`,
  },
  {
    title: 'Sequence: Opt Block',
    category: 'Sequence',
    description: 'Optional block — executes only if condition is met.',
    source: `sequenceDiagram
  participant A as App
  participant C as Cache
  participant DB as Database
  A->>C: Get data
  C-->>A: Cache miss
  opt Cache miss
    A->>DB: Query
    DB-->>A: Results
    A->>C: Store in cache
  end`,
  },
  {
    title: 'Sequence: Par Block',
    category: 'Sequence',
    description: 'Parallel execution with `par`/`and` constructs.',
    source: `sequenceDiagram
  participant C as Client
  participant A as AuthService
  participant U as UserService
  participant O as OrderService
  C->>A: Authenticate
  par Fetch user data
    A->>U: Get profile
  and Fetch orders
    A->>O: Get orders
  end
  A-->>C: Combined response`,
  },
  {
    title: 'Sequence: Critical Block',
    category: 'Sequence',
    description: 'Critical section that must complete atomically.',
    source: `sequenceDiagram
  participant A as App
  participant DB as Database
  A->>DB: BEGIN
  critical Transaction
    A->>DB: UPDATE accounts
    A->>DB: INSERT log
  end
  A->>DB: COMMIT`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  SEQUENCE DIAGRAMS — Notes
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Sequence: Notes (Right/Left/Over)',
    category: 'Sequence',
    description: 'Notes positioned to the right, left, or over participants.',
    source: `sequenceDiagram
  participant A as Alice
  participant B as Bob
  Note left of A: Alice prepares
  A->>B: Hello
  Note right of B: Bob thinks
  B-->>A: Reply
  Note over A,B: Conversation complete`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  SEQUENCE DIAGRAMS — Complex / Real-World
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Sequence: OAuth 2.0 Flow',
    category: 'Sequence',
    description: 'Full OAuth 2.0 authorization code flow with token exchange.',
    source: `sequenceDiagram
  actor U as User
  participant App as Client App
  participant Auth as Auth Server
  participant API as Resource API
  U->>App: Click Login
  App->>Auth: Authorization request
  Auth->>U: Login page
  U->>Auth: Credentials
  Auth-->>App: Authorization code
  App->>Auth: Exchange code for token
  Auth-->>App: Access token
  App->>API: Request + token
  API-->>App: Protected resource
  App-->>U: Display data`,
  },
  {
    title: 'Sequence: Database Transaction',
    category: 'Sequence',
    description: 'Multi-step database transaction with rollback handling.',
    source: `sequenceDiagram
  participant C as Client
  participant S as Server
  participant DB as Database
  C->>S: POST /transfer
  S->>DB: BEGIN
  S->>DB: Debit account A
  alt Success
    S->>DB: Credit account B
    S->>DB: INSERT audit_log
    S->>DB: COMMIT
    S-->>C: 200 OK
  else Insufficient funds
    S->>DB: ROLLBACK
    S-->>C: 400 Bad Request
  end`,
  },
  {
    title: 'Sequence: Microservice Orchestration',
    category: 'Sequence',
    description: 'Complex multi-service flow with parallel calls and error handling.',
    source: `sequenceDiagram
  participant G as Gateway
  participant A as Auth
  participant U as Users
  participant O as Orders
  participant N as Notify
  G->>A: Validate token
  A-->>G: Valid
  par Fetch data
    G->>U: Get user
    U-->>G: User data
  and
    G->>O: Get orders
    O-->>G: Order list
  end
  G->>N: Send notification
  N-->>G: Queued
  Note over G: Aggregate response`,
  },
  {
    title: 'Sequence: Self-Messages with Notes',
    category: 'Sequence',
    description: 'Self-referencing messages inside alt blocks with notes — tests that notes clear self-message loops and stack without overlapping.',
    source: `sequenceDiagram
  participant User
  participant Main as Main Process
  participant Renderer
  participant Timer as 3s Fallback Timer
  User->>Main: CMD+W
  Main->>Main: event.preventDefault()
  Main->>Renderer: WINDOW_CLOSE_REQUESTED
  Main->>Timer: Start 3s timer
  alt Multiple panels
    Renderer->>Renderer: closePanel(focusedId)
    Note over Renderer: Panel removed
    Note over Renderer: No confirmCloseWindow!
    Timer-->>Main: 3s elapsed → window.destroy()
  else Single panel
    Renderer->>Renderer: closePanel(lastId)
    Note over Renderer: Stack becomes []
    Renderer->>Renderer: Auto-select fires → new panel created!
    Note over Renderer: Panel reopens
    Timer-->>Main: 3s elapsed → window.destroy()
  end`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  CLASS DIAGRAMS — Core Features
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Class: Basic Class',
    category: 'Class',
    description: 'A single class with attributes and methods, rendered as a 3-compartment box.',
    source: `classDiagram
  class Animal {
    +String name
    +int age
    +eat() void
    +sleep() void
  }`,
  },
  {
    title: 'Class: Visibility Markers',
    category: 'Class',
    description: 'All four visibility levels: `+` (public), `-` (private), `#` (protected), `~` (package).',
    source: `classDiagram
  class User {
    +String name
    -String password
    #int internalId
    ~String packageField
    +login() bool
    -hashPassword() String
    #validate() void
    ~notify() void
  }`,
  },
  {
    title: 'Class: Interface Annotation',
    category: 'Class',
    description: 'Using `<<interface>>` annotation above the class name.',
    source: `classDiagram
  class Serializable {
    <<interface>>
    +serialize() String
    +deserialize(data) void
  }`,
  },
  {
    title: 'Class: Abstract Annotation',
    category: 'Class',
    description: 'Using `<<abstract>>` annotation for abstract classes.',
    source: `classDiagram
  class Shape {
    <<abstract>>
    +String color
    +area() double
    +draw() void
  }`,
  },
  {
    title: 'Class: Enum Annotation',
    category: 'Class',
    description: 'Using `<<enumeration>>` annotation for enum types.',
    source: `classDiagram
  class Status {
    <<enumeration>>
    ACTIVE
    INACTIVE
    PENDING
    DELETED
  }`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  CLASS DIAGRAMS — Relationships
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Class: Inheritance (<|--)',
    category: 'Class',
    description: 'Inheritance relationship rendered with a hollow triangle marker.',
    source: `classDiagram
  class Animal {
    +String name
    +eat() void
  }
  class Dog {
    +String breed
    +bark() void
  }
  class Cat {
    +bool isIndoor
    +meow() void
  }
  Animal <|-- Dog
  Animal <|-- Cat`,
  },
  {
    title: 'Class: Composition (*--)',
    category: 'Class',
    description: 'Composition — "owns" relationship with filled diamond marker.',
    source: `classDiagram
  class Car {
    +String model
    +start() void
  }
  class Engine {
    +int horsepower
    +rev() void
  }
  Car *-- Engine`,
  },
  {
    title: 'Class: Aggregation (o--)',
    category: 'Class',
    description: 'Aggregation — "has" relationship with hollow diamond marker.',
    source: `classDiagram
  class University {
    +String name
  }
  class Department {
    +String faculty
  }
  University o-- Department`,
  },
  {
    title: 'Class: Association (-->)',
    category: 'Class',
    description: 'Basic association — simple directed arrow.',
    source: `classDiagram
  class Customer {
    +String name
  }
  class Order {
    +int orderId
  }
  Customer --> Order`,
  },
  {
    title: 'Class: Dependency (..>)',
    category: 'Class',
    description: 'Dependency — dashed line with open arrow.',
    source: `classDiagram
  class Service {
    +process() void
  }
  class Repository {
    +find() Object
  }
  Service ..> Repository`,
  },
  {
    title: 'Class: Realization (..|>)',
    category: 'Class',
    description: 'Realization — dashed line with hollow triangle (implements interface).',
    source: `classDiagram
  class Flyable {
    <<interface>>
    +fly() void
  }
  class Bird {
    +fly() void
    +sing() void
  }
  Bird ..|> Flyable`,
  },
  {
    title: 'Class: All 6 Relationship Types',
    category: 'Class',
    description: 'Every relationship type in a single diagram for comparison.',
    source: `classDiagram
  A <|-- B : inheritance
  C *-- D : composition
  E o-- F : aggregation
  G --> H : association
  I ..> J : dependency
  K ..|> L : realization`,
  },
  {
    title: 'Class: Relationship Labels',
    category: 'Class',
    description: 'Labeled relationships between classes with descriptive text.',
    source: `classDiagram
  class Teacher {
    +String name
  }
  class Student {
    +String name
  }
  class Course {
    +String title
  }
  Teacher --> Course : teaches
  Student --> Course : enrolled in`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  CLASS DIAGRAMS — Complex / Real-World
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'Class: Design Pattern — Observer',
    category: 'Class',
    description: 'The Observer (publish-subscribe) design pattern with interface + concrete implementations.',
    source: `classDiagram
  class Subject {
    <<interface>>
    +attach(Observer) void
    +detach(Observer) void
    +notify() void
  }
  class Observer {
    <<interface>>
    +update() void
  }
  class EventEmitter {
    -List~Observer~ observers
    +attach(Observer) void
    +detach(Observer) void
    +notify() void
  }
  class Logger {
    +update() void
  }
  class Alerter {
    +update() void
  }
  Subject <|.. EventEmitter
  Observer <|.. Logger
  Observer <|.. Alerter
  EventEmitter --> Observer`,
  },
  {
    title: 'Class: MVC Architecture',
    category: 'Class',
    description: 'Model-View-Controller pattern showing relationships between layers.',
    source: `classDiagram
  class Model {
    -data Map
    +getData() Map
    +setData(key, val) void
    +notify() void
  }
  class View {
    -model Model
    +render() void
    +update() void
  }
  class Controller {
    -model Model
    -view View
    +handleInput(event) void
    +updateModel(data) void
  }
  Controller --> Model : updates
  Controller --> View : refreshes
  View --> Model : reads
  Model ..> View : notifies`,
  },
  {
    title: 'Class: Full Hierarchy',
    category: 'Class',
    description: 'A complete class hierarchy with abstract base, interfaces, and concrete classes.',
    source: `classDiagram
  class Animal {
    <<abstract>>
    +String name
    +int age
    +eat() void
    +sleep() void
  }
  class Mammal {
    +bool warmBlooded
    +nurse() void
  }
  class Bird {
    +bool canFly
    +layEggs() void
  }
  class Dog {
    +String breed
    +bark() void
  }
  class Cat {
    +bool isIndoor
    +purr() void
  }
  class Parrot {
    +String vocabulary
    +speak() void
  }
  Animal <|-- Mammal
  Animal <|-- Bird
  Mammal <|-- Dog
  Mammal <|-- Cat
  Bird <|-- Parrot`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  ER DIAGRAMS — Core Features
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'ER: Basic Relationship',
    category: 'ER',
    description: 'A simple one-to-many relationship between two entities.',
    source: `erDiagram
  CUSTOMER ||--o{ ORDER : places`,
  },
  {
    title: 'ER: Entity with Attributes',
    category: 'ER',
    description: 'An entity with typed attributes and `PK`/`FK`/`UK` key badges.',
    source: `erDiagram
  CUSTOMER {
    int id PK
    string name
    string email UK
    date created_at
  }`,
  },
  {
    title: 'ER: Attribute Keys (PK, FK, UK)',
    category: 'ER',
    description: 'All three key constraint types rendered as badges.',
    source: `erDiagram
  ORDER {
    int id PK
    int customer_id FK
    string invoice_number UK
    decimal total
    date order_date
    string status
  }`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  ER DIAGRAMS — Cardinality Types
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'ER: Exactly One to Exactly One (||--||)',
    category: 'ER',
    description: 'One-to-one mandatory relationship.',
    source: `erDiagram
  PERSON ||--|| PASSPORT : has`,
  },
  {
    title: 'ER: Exactly One to Zero-or-Many (||--o{)',
    category: 'ER',
    description: 'Classic one-to-many optional relationship (crow\'s foot).',
    source: `erDiagram
  CUSTOMER ||--o{ ORDER : places`,
  },
  {
    title: 'ER: Zero-or-One to One-or-Many (|o--|{)',
    category: 'ER',
    description: 'Optional on one side, at-least-one on the other.',
    source: `erDiagram
  SUPERVISOR |o--|{ EMPLOYEE : manages`,
  },
  {
    title: 'ER: One-or-More to Zero-or-Many (}|--o{)',
    category: 'ER',
    description: 'At-least-one to zero-or-many relationship.',
    source: `erDiagram
  TEACHER }|--o{ COURSE : teaches`,
  },
  {
    title: 'ER: All Cardinality Types',
    category: 'ER',
    description: 'Every cardinality combination in one diagram.',
    source: `erDiagram
  A ||--|| B : one-to-one
  C ||--o{ D : one-to-many
  E |o--|{ F : opt-to-many
  G }|--o{ H : many-to-many`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  ER DIAGRAMS — Line Styles
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'ER: Identifying (Solid) Relationship',
    category: 'ER',
    description: 'Solid line indicating an identifying relationship (child depends on parent for identity).',
    source: `erDiagram
  ORDER ||--|{ LINE_ITEM : contains`,
  },
  {
    title: 'ER: Non-Identifying (Dashed) Relationship',
    category: 'ER',
    description: 'Dashed line indicating a non-identifying relationship.',
    source: `erDiagram
  USER ||..o{ LOG_ENTRY : generates
  USER ||..o{ SESSION : opens`,
  },
  {
    title: 'ER: Mixed Identifying & Non-Identifying',
    category: 'ER',
    description: 'Both solid and dashed lines in the same diagram.',
    source: `erDiagram
  ORDER ||--|{ LINE_ITEM : contains
  ORDER ||..o{ SHIPMENT : ships-via
  PRODUCT ||--o{ LINE_ITEM : includes
  PRODUCT ||..o{ REVIEW : receives`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  ER DIAGRAMS — Complex / Real-World
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'ER: E-Commerce Schema',
    category: 'ER',
    description: 'Full e-commerce database schema with customers, orders, products, and line items.',
    source: `erDiagram
  CUSTOMER {
    int id PK
    string name
    string email UK
  }
  ORDER {
    int id PK
    date created
    int customer_id FK
  }
  PRODUCT {
    int id PK
    string name
    float price
  }
  LINE_ITEM {
    int id PK
    int order_id FK
    int product_id FK
    int quantity
  }
  CUSTOMER ||--o{ ORDER : places
  ORDER ||--|{ LINE_ITEM : contains
  PRODUCT ||--o{ LINE_ITEM : includes`,
  },
  {
    title: 'ER: Blog Platform Schema',
    category: 'ER',
    description: 'Blog system with users, posts, comments, and tags.',
    source: `erDiagram
  USER {
    int id PK
    string username UK
    string email UK
    date joined
  }
  POST {
    int id PK
    string title
    text content
    int author_id FK
    date published
  }
  COMMENT {
    int id PK
    text body
    int post_id FK
    int user_id FK
    date created
  }
  TAG {
    int id PK
    string name UK
  }
  USER ||--o{ POST : writes
  USER ||--o{ COMMENT : authors
  POST ||--o{ COMMENT : has
  POST }|--o{ TAG : tagged-with`,
  },
  {
    title: 'ER: School Management Schema',
    category: 'ER',
    description: 'School system with students, teachers, courses, and enrollments.',
    source: `erDiagram
  STUDENT {
    int id PK
    string name
    date dob
    string grade
  }
  TEACHER {
    int id PK
    string name
    string department
  }
  COURSE {
    int id PK
    string title
    int teacher_id FK
    int credits
  }
  ENROLLMENT {
    int id PK
    int student_id FK
    int course_id FK
    string semester
    float grade
  }
  TEACHER ||--o{ COURSE : teaches
  STUDENT ||--o{ ENROLLMENT : enrolled
  COURSE ||--o{ ENROLLMENT : has`,
  },

  // ══════════════════════════════════════════════════════════════════════════
  //  XY CHARTS (xychart-beta)
  // ══════════════════════════════════════════════════════════════════════════

  {
    title: 'XY: Simple Bar Chart',
    category: 'XY Chart',
    description: 'Basic bar chart with categorical x-axis.',
    source: `xychart-beta
    title "Product Sales"
    x-axis [Widgets, Gadgets, Gizmos, Doodads, Thingamajigs]
    bar [150, 230, 180, 95, 310]`,
    options: { interactive: true },
  },
  {
    title: 'XY: Line Chart',
    category: 'XY Chart',
    description: 'Line chart showing revenue growth over years.',
    source: `xychart-beta
    title "Revenue Growth"
    x-axis [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
    line [320, 420, 540, 680, 820, 950, 1080, 1200]`,
    options: { interactive: true },
  },
  {
    title: 'XY: Bar and Line Overlay',
    category: 'XY Chart',
    description: 'Bars with a line overlay and both axis titles.',
    source: `xychart-beta
    title "Monthly Revenue"
    x-axis "Month" [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
    y-axis "Revenue (USD)" 0 --> 10000
    bar [4200, 5000, 5800, 6200, 5500, 7000, 7800, 7200, 8400, 8100, 9000, 9200]
    line [4200, 5000, 5800, 6200, 5500, 7000, 7800, 7200, 8400, 8100, 9000, 9200]`,
    options: { interactive: true },
  },
  {
    title: 'XY: Horizontal Bars',
    category: 'XY Chart',
    description: 'Horizontal bar chart showing language popularity.',
    source: `xychart-beta horizontal
    title "Language Popularity"
    x-axis [Python, JavaScript, Java, Go, Rust]
    bar [30, 25, 20, 12, 8]`,
    options: { interactive: true },
  },
  {
    title: 'XY: Multiple Bar Series',
    category: 'XY Chart',
    description: 'Two bar series comparing years side by side.',
    source: `xychart-beta
    title "2023 vs 2024 Sales"
    x-axis [Q1, Q2, Q3, Q4]
    bar [200, 250, 300, 280]
    bar [230, 280, 320, 350]`,
    options: { interactive: true },
  },
  {
    title: 'XY: Dual Lines',
    category: 'XY Chart',
    description: 'Two lines comparing planned vs actual values.',
    source: `xychart-beta
    title "Planned vs Actual"
    x-axis [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug]
    line [100, 145, 190, 240, 280, 320, 360, 400]
    line [90, 130, 185, 235, 275, 340, 380, 420]`,
    options: { interactive: true },
  },
  {
    title: 'XY: Numeric X-Axis',
    category: 'XY Chart',
    description: 'Line chart using a numeric x-axis range.',
    source: `xychart-beta
    title "Distribution Curve"
    x-axis 0 --> 100
    line [4, 7, 13, 21, 31, 43, 58, 71, 84, 91, 95, 91, 84, 71, 58, 43, 31, 21, 13, 7, 4]`,
    options: { interactive: true },
  },
  {
    title: 'XY: 12-Month Dataset',
    category: 'XY Chart',
    description: 'Full year monthly data with bar and trend line.',
    source: `xychart-beta
    title "Monthly Active Users (2024)"
    x-axis [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
    y-axis "Users" 0 --> 30000
    bar [12000, 13500, 15200, 16800, 18500, 20100, 19800, 21500, 23000, 24200, 25800, 28000]
    line [12000, 13500, 15200, 16800, 18500, 20100, 19800, 21500, 23000, 24200, 25800, 28000]`,
    options: { interactive: true },
  },
  {
    title: 'XY: Horizontal Combined',
    category: 'XY Chart',
    description: 'Horizontal chart with both bars and a trend line.',
    source: `xychart-beta horizontal
    title "Budget vs Actual"
    x-axis [Eng, Sales, Marketing, Product, Ops, HR, Finance, Legal]
    bar [500, 350, 250, 200, 150, 120, 100, 80]
    line [480, 380, 230, 180, 160, 110, 95, 75]`,
    options: { interactive: true },
  },
  {
    title: 'XY: Sprint Burndown',
    category: 'XY Chart',
    description: 'Sprint burndown chart with actual and ideal lines.',
    source: `xychart-beta
    title "Sprint Burndown"
    x-axis [D1, D2, D3, D4, D5, D6, D7, D8, D9, D10]
    y-axis "Story Points" 0 --> 80
    line [72, 65, 58, 50, 45, 38, 30, 22, 12, 0]
    line [72, 65, 58, 50, 43, 36, 29, 22, 14, 0]`,
    options: { interactive: true },
  },
]


================================================
FILE: src/__tests__/ascii-edge-styles.test.ts
================================================
// ============================================================================
// ASCII edge style tests — dotted and thick line rendering
// ============================================================================

import { describe, it, expect } from 'bun:test'
import { renderMermaidAscii } from '../ascii/index.ts'

describe('ASCII edge styles', () => {
  describe('solid edges (default)', () => {
    it('renders solid edges with ─ in unicode mode', () => {
      const result = renderMermaidAscii(`
        graph LR
          A --> B
      `)
      expect(result).toContain('─')
      expect(result).not.toContain('┄')
      expect(result).not.toContain('━')
    })

    it('renders solid edges with - in ascii mode', () => {
      const result = renderMermaidAscii(`
        graph LR
          A --> B
      `, { useAscii: true })
      expect(result).toContain('-')
    })
  })

  describe('dotted edges (-.->)', () => {
    it('renders dotted edges with ┄ in unicode mode', () => {
      const result = renderMermaidAscii(`
        graph LR
          A -.-> B
      `)
      // Should contain dotted horizontal line character
      expect(result).toContain('┄')
    })

    it('renders dotted edges with . in ascii mode', () => {
      const result = renderMermaidAscii(`
        graph LR
          A -.-> B
      `, { useAscii: true })
      // Should contain dots for dotted lines
      expect(result).toContain('.')
    })

    it('renders dotted vertical edges with ┆ in unicode mode', () => {
      const result = renderMermaidAscii(`
        graph TD
          A -.-> B
      `)
      // Should contain dotted vertical line character
      expect(result).toContain('┆')
    })

    it('renders dotted vertical edges with : in ascii mode', () => {
      const result = renderMermaidAscii(`
        graph TD
          A -.-> B
      `, { useAscii: true })
      // Should contain colons for dotted vertical lines
      expect(result).toContain(':')
    })

    it('renders dotted edges with labels', () => {
      const result = renderMermaidAscii(`
        graph LR
          A -.->|optional| B
      `)
      expect(result).toContain('┄')
      expect(result).toContain('optional')
    })
  })

  describe('thick edges (==>)', () => {
    it('renders thick edges with ━ in unicode mode', () => {
      const result = renderMermaidAscii(`
        graph LR
          A ==> B
      `)
      // Should contain thick horizontal line character
      expect(result).toContain('━')
    })

    it('renders thick edges with = in ascii mode', () => {
      const result = renderMermaidAscii(`
        graph LR
          A ==> B
      `, { useAscii: true })
      // Should contain equals for thick lines
      expect(result).toContain('=')
    })

    it('renders thick vertical edges with ┃ in unicode mode', () => {
      const result = renderMermaidAscii(`
        graph TD
          A ==> B
      `)
      // Should contain thick vertical line character
      expect(result).toContain('┃')
    })
  })

  describe('mixed edge styles', () => {
    it('renders different styles in the same diagram', () => {
      const result = renderMermaidAscii(`
        graph LR
          A --> B
          B -.-> C
          C ==> D
      `)
      // Should have all three line types
      expect(result).toContain('─')  // solid
      expect(result).toContain('┄')  // dotted
      expect(result).toContain('━')  // thick
    })

    it('renders mixed styles in ascii mode', () => {
      const result = renderMermaidAscii(`
        graph LR
          A --> B
          B -.-> C
          C ==> D
      `, { useAscii: true })
      // Note: ASCII mode uses - for solid, . for dotted, = for thick
      // We just check that the diagram renders without error
      expect(result).toContain('A')
      expect(result).toContain('B')
      expect(result).toContain('C')
      expect(result).toContain('D')
    })
  })
})


================================================
FILE: src/__tests__/ascii-multiline.test.ts
================================================
import { describe, it, expect } from 'bun:test'
import { renderMermaidAscii } from '../ascii/index.ts'

describe('ASCII multi-line labels', () => {
  describe('flowchart nodes', () => {
    it('renders multi-line node labels', () => {
      const ascii = renderMermaidAscii('graph TD\n  A[Line1<br>Line2]', { useAscii: false })
      expect(ascii).toContain('Line1')
      expect(ascii).toContain('Line2')
      // Lines should be on different rows
      const lines = ascii.split('\n')
      const line1Row = lines.findIndex(l => l.includes('Line1'))
      const line2Row = lines.findIndex(l => l.includes('Line2'))
      expect(line2Row).toBeGreaterThan(line1Row)
    })

    it('handles 3+ line labels', () => {
      const ascii = renderMermaidAscii('graph TD\n  A[A<br>B<br>C]', { useAscii: false })
      expect(ascii).toContain('A')
      expect(ascii).toContain('B')
      expect(ascii).toContain('C')
      // Verify vertical ordering
      const lines = ascii.split('\n')
      const aRow = lines.findIndex(l => l.includes('A') && !l.includes('─') && !l.includes('-'))
      const bRow = lines.findIndex(l => l.includes('B'))
      const cRow = lines.findIndex(l => l.includes('C'))
      expect(bRow).toBeGreaterThan(aRow)
      expect(cRow).toBeGreaterThan(bRow)
    })

    it('renders in ASCII mode (not Unicode)', () => {
      const ascii = renderMermaidAscii('graph TD\n  A[Line1<br>Line2]', { useAscii: true })
      expect(ascii).toContain('Line1')
      expect(ascii).toContain('Line2')
      // Should use ASCII box characters
      expect(ascii).toContain('+')
      expect(ascii).toContain('-')
    })
  })

  describe('flowchart edge labels', () => {
    it('renders multi-line edge labels', () => {
      const ascii = renderMermaidAscii('graph TD\n  A --> B\n  A -->|Line1<br>Line2| C', { useAscii: false })
      expect(ascii).toContain('Line1')
      expect(ascii).toContain('Line2')
    })
  })

  describe('flowchart subgraph labels', () => {
    it('renders multi-line subgraph labels', () => {
      const ascii = renderMermaidAscii(`graph TD
        subgraph sg [Group<br>Header]
          A[Node]
        end
      `, { useAscii: false })
      expect(ascii).toContain('Group')
      expect(ascii).toContain('Header')
    })
  })

  describe('sequence diagram', () => {
    it('renders multi-line actor labels', () => {
      const ascii = renderMermaidAscii(`sequenceDiagram
        participant A as Actor<br>One
        A->>A: msg
      `, { useAscii: false })
      expect(ascii).toContain('Actor')
      expect(ascii).toContain('One')
    })

    it('renders multi-line message labels', () => {
      const ascii = renderMermaidAscii(`sequenceDiagram
        participant A
        participant B
        A->>B: Line1<br>Line2
      `, { useAscii: false })
      expect(ascii).toContain('Line1')
      expect(ascii).toContain('Line2')
    })

    it('preserves existing note multi-line support', () => {
      const ascii = renderMermaidAscii(`sequenceDiagram
        participant A
        A->>A: self
        Note over A: Note line 1<br>Note line 2
      `, { useAscii: false })
      expect(ascii).toContain('Note line 1')
      expect(ascii).toContain('Note line 2')
    })
  })

  describe('class diagram', () => {
    it('renders multi-line class names', () => {
      const ascii = renderMermaidAscii(`classDiagram
        class MyClass["Long<br>Name"]
      `, { useAscii: false })
      expect(ascii).toContain('Long')
      expect(ascii).toContain('Name')
    })

    it('renders multi-line relationship labels', () => {
      const ascii = renderMermaidAscii(`classDiagram
        A --> B : uses<br>implements
      `, { useAscii: false })
      expect(ascii).toContain('uses')
      expect(ascii).toContain('implements')
    })
  })

  describe('ER diagram', () => {
    it('renders multi-line entity names', () => {
      const ascii = renderMermaidAscii(`erDiagram
        "Entity<br>Name" {
          string id
        }
      `, { useAscii: false })
      expect(ascii).toContain('Entity')
      expect(ascii).toContain('Name')
    })

    it('renders multi-line relationship labels', () => {
      const ascii = renderMermaidAscii(`erDiagram
        A ||--o{ B : "has<br>many"
      `, { useAscii: false })
      expect(ascii).toContain('has')
      expect(ascii).toContain('many')
    })
  })

  describe('edge cases', () => {
    it('handles empty lines from consecutive <br>', () => {
      const ascii = renderMermaidAscii('graph TD\n  A[Line1<br><br>Line3]', { useAscii: false })
      expect(ascii).toContain('Line1')
      expect(ascii).toContain('Line3')
    })

    it('handles single-line labels (no <br>)', () => {
      const ascii = renderMermaidAscii('graph TD\n  A[SingleLine]', { useAscii: false })
      expect(ascii).toContain('SingleLine')
    })

    it('handles very long lines', () => {
      const long = 'A'.repeat(30)
      const ascii = renderMermaidAscii(`graph TD\n  A[${long}<br>Short]`, { useAscii: false })
      expect(ascii).toContain(long)
      expect(ascii).toContain('Short')
    })

    it('handles mixed short and long lines', () => {
      const ascii = renderMermaidAscii('graph TD\n  A[Short<br>VeryLongSecondLine<br>Med]', { useAscii: false })
      expect(ascii).toContain('Short')
      expect(ascii).toContain('VeryLongSecondLine')
      expect(ascii).toContain('Med')
    })
  })

  describe('multiline-utils functions', () => {
    it('splitLines splits on newlines', () => {
      // Test through the rendering pipeline
      const ascii = renderMermaidAscii('graph TD\n  A[One<br>Two<br>Three]', { useAscii: false })
      const lines = ascii.split('\n')
      // All three words should appear on separate lines
      expect(lines.some(l => l.includes('One'))).toBe(true)
      expect(lines.some(l => l.includes('Two'))).toBe(true)
      expect(lines.some(l => l.includes('Three'))).toBe(true)
    })

    it('maxLineWidth uses longest line for box sizing', () => {
      // Box should be wide enough for the longest line
      const ascii = renderMermaidAscii('graph TD\n  A[X<br>LongLine<br>Y]', { useAscii: false })
      // The box should contain LongLine without truncation
      expect(ascii).toContain('LongLine')
    })
  })
})


================================================
FILE: src/__tests__/ascii.test.ts
================================================
/**
 * Golden-file tests for the ASCII/Unicode renderer.
 *
 * Ported from AlexanderGrooff/mermaid-ascii cmd/graph_test.go.
 * Each .txt file contains mermaid input above a `---` separator
 * and the expected ASCII/Unicode output below it.
 *
 * Test data: 44 ASCII files + 22 Unicode files = 66 total.
 */
import { describe, it, expect } from 'bun:test'
import { renderMermaidAscii } from '../ascii/index.ts'
import { hasDiagonalLines, DIAGONAL_CHARS } from '../ascii/validate.ts'
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'

// ============================================================================
// Test case parser — matches Go's testutil.ReadTestCase format
// ============================================================================

interface TestCase {
  mermaid: string
  expected: string
  paddingX: number
  paddingY: number
}

/**
 * Parse a golden test file into its components.
 * Format:
 *   [paddingX=N]     (optional)
 *   [paddingY=N]     (optional)
 *   <mermaid code>
 *   ---
 *   <expected output>
 */
function parseTestCase(content: string): TestCase {
  const tc: TestCase = { mermaid: '', expected: '', paddingX: 5, paddingY: 5 }
  const lines = content.split('\n')
  const paddingRegex = /^(?:padding([xy]))\s*=\s*(\d+)\s*$/i

  let inMermaid = true
  let mermaidStarted = false
  const mermaidLines: string[] = []
  const expectedLines: string[] = []

  for (const line of lines) {
    if (line === '---') {
      inMermaid = false
      continue
    }

    if (inMermaid) {
      const trimmed = line.trim()

      // Before mermaid code starts, parse padding directives and skip blanks
      if (!mermaidStarted) {
        if (trimmed === '') continue
        const match = trimmed.match(paddingRegex)
        if (match) {
          const value = parseInt(match[2]!, 10)
          if (match[1]!.toLowerCase() === 'x') {
            tc.paddingX = value
          } else {
            tc.paddingY = value
          }
          continue
        }
      }

      mermaidStarted = true
      mermaidLines.push(line)
    } else {
      expectedLines.push(line)
    }
  }

  tc.mermaid = mermaidLines.join('\n') + '\n'

  // Strip final trailing newline (matches Go's strings.TrimSuffix(expected, "\n"))
  let expected = expectedLines.join('\n')
  if (expected.endsWith('\n')) {
    expected = expected.slice(0, -1)
  }
  tc.expected = expected

  return tc
}

// ============================================================================
// Whitespace normalization — matches Go's testutil.NormalizeWhitespace
// ============================================================================

/**
 * Normalize whitespace for comparison:
 * - Trim trailing spaces from each line
 * - Remove leading/trailing blank lines
 */
function normalizeWhitespace(s: string): string {
  const lines = s.split('\n')
  let normalized = lines.map(l => l.trimEnd())

  // Remove leading blank lines
  while (normalized.length > 0 && normalized[0] === '') {
    normalized.shift()
  }
  // Remove trailing blank lines
  while (normalized.length > 0 && normalized[normalized.length - 1] === '') {
    normalized.pop()
  }

  return normalized.join('\n')
}

/** Replace spaces with middle dots for clearer diff output. */
function visualizeWhitespace(s: string): string {
  return s.replaceAll(' ', '·')
}

// ============================================================================
// Test runner — dynamically loads all golden files from testdata directories
// ============================================================================

function runGoldenTests(dir: string, useAscii: boolean): void {
  const files = readdirSync(dir).filter(f => f.endsWith('.txt')).sort()

  for (const file of files) {
    const testName = file.replace('.txt', '')

    it(testName, () => {
      const content = readFileSync(join(dir, file), 'utf-8')
      const tc = parseTestCase(content)

      const actual = renderMermaidAscii(tc.mermaid, {
        useAscii,
        paddingX: tc.paddingX,
        paddingY: tc.paddingY,
      })

      const normalizedExpected = normalizeWhitespace(tc.expected)
      const normalizedActual = normalizeWhitespace(actual)

      if (normalizedExpected !== normalizedActual) {
        const expectedVis = visualizeWhitespace(normalizedExpected)
        const actualVis = visualizeWhitespace(normalizedActual)
        expect(actualVis).toBe(expectedVis)
      }
    })
  }
}

// ============================================================================
// Test suites
// ============================================================================

const testdataDir = join(import.meta.dir, 'testdata')

describe('ASCII rendering', () => {
  runGoldenTests(join(testdataDir, 'ascii'), true)
})

describe('Unicode rendering', () => {
  runGoldenTests(join(testdataDir, 'unicode'), false)
})

// ============================================================================
// Config behavior tests — ported from Go's TestGraphUseAsciiConfig
// ============================================================================

describe('Config behavior', () => {
  const mermaidInput = 'graph LR\nA --> B'

  it('ASCII and Unicode outputs should differ', () => {
    const asciiOutput = renderMermaidAscii(mermaidInput, { useAscii: true })
    const unicodeOutput = renderMermaidAscii(mermaidInput, { useAscii: false })
    expect(asciiOutput).not.toBe(unicodeOutput)
  })

  it('ASCII output should not contain Unicode box-drawing characters', () => {
    const output = renderMermaidAscii(mermaidInput, { useAscii: true })
    expect(output).not.toContain('┌')
    expect(output).not.toContain('─')
    expect(output).not.toContain('│')
  })

  it('Unicode output should contain Unicode box-drawing characters', () => {
    const output = renderMermaidAscii(mermaidInput, { useAscii: false })
    const hasUnicode = output.includes('┌') || output.includes('─') || output.includes('│')
    expect(hasUnicode).toBe(true)
  })
})

// ============================================================================
// Diagonal validation — ensures all edges use orthogonal Manhattan routing
// ============================================================================

describe('Diagonal validation', () => {
  const asciiDir = join(testdataDir, 'ascii')
  const unicodeDir = join(testdataDir, 'unicode')

  it('ASCII output should never contain diagonal characters', () => {
    // Test all ASCII golden files
    const files = readdirSync(asciiDir).filter((f) => f.endsWith('.txt'))
    for (const file of files) {
      const content = readFileSync(join(asciiDir, file), 'utf-8')
      const { mermaid, paddingX, paddingY } = parseTestCase(content)
      const output = renderMermaidAscii(mermaid, {
        useAscii: true,
        boxBorderPadding: paddingX,
        paddingY: paddingY,
      })

      // Check for diagonal characters
      for (const char of DIAGONAL_CHARS.ascii) {
        expect(output).not.toContain(char)
      }
    }
  })

  it('Unicode output should never contain diagonal characters', () => {
    // Test all Unicode golden files
    const files = readdirSync(unicodeDir).filter((f) => f.endsWith('.txt'))
    for (const file of files) {
      const content = readFileSync(join(unicodeDir, file), 'utf-8')
      const { mermaid, paddingX, paddingY } = parseTestCase(content)
      const output = renderMermaidAscii(mermaid, {
        useAscii: false,
        boxBorderPadding: paddingX,
        paddingY: paddingY,
      })

      // Check for diagonal characters
      for (const char of DIAGONAL_CHARS.unicode) {
        expect(output).not.toContain(char)
      }
    }
  })

  it('hasDiagonalLines utility correctly detects diagonal characters', () => {
    // Should detect ASCII diagonals
    expect(hasDiagonalLines('A / B')).toBe(true)
    expect(hasDiagonalLines('A \\ B')).toBe(true)

    // Should detect Unicode diagonals
    expect(hasDiagonalLines('A ╱ B')).toBe(true)
    expect(hasDiagonalLines('A ╲ B')).toBe(true)

    // Should not flag clean output
    expect(hasDiagonalLines('┌───┐\n│ A │\n└───┘')).toBe(false)
    expect(hasDiagonalLines('+---+\n| A |\n+---+')).toBe(false)
  })
})


================================================
FILE: src/__tests__/class-arrow-directions.test.ts
================================================
/**
 * Comprehensive tests for class diagram arrow directions.
 *
 * Ensures all relationship types have correctly oriented arrows:
 * - Inheritance/Realization: hollow triangles point toward parent/interface
 * - Association/Dependency: filled arrows point from source to target
 * - Composition/Aggregation: diamonds are omnidirectional
 */

import { describe, test, expect } from 'bun:test'
import { renderMermaidAscii } from '../ascii/index.ts'

describe('Class Diagram Arrow Directions', () => {

  // ============================================================================
  // INHERITANCE (<|--)
  // ============================================================================

  describe('Inheritance (<|--)', () => {
    test('parent above child - triangle points UP toward parent', () => {
      const diagram = `classDiagram
        Animal <|-- Dog`
      const result = renderMermaidAscii(diagram)

      // Should contain upward triangle
      expect(result).toContain('△')
      expect(result).not.toContain('▽')

      // Parent should be above child
      const lines = result.split('\n')
      const animalLine = lines.findIndex(l => l.includes('Animal'))
      const dogLine = lines.findIndex(l => l.includes('Dog'))
      expect(animalLine).toBeLessThan(dogLine)
    })

    test('multiple inheritance creates separate arrows', () => {
      const diagram = `classDiagram
        Animal <|-- Dog
        Animal <|-- Cat
        Dog <|-- Puppy`
      const result = renderMermaidAscii(diagram)

      // Animal should be at top, then Dog/Cat, then Puppy
      const lines = result.split('\n')
      const animalLine = lines.findIndex(l => l.includes('Animal'))
      const dogLine = lines.findIndex(l => l.includes('Dog'))
      const catLine = lines.findIndex(l => l.includes('Cat'))
      const puppyLine = lines.findIndex(l => l.includes('Puppy'))

      expect(animalLine).toBeLessThan(dogLine)
      expect(animalLine).toBeLessThan(catLine)
      expect(dogLine).toBeLessThan(puppyLine)
    })

    test('multi-level inheritance - all triangles point UP', () => {
      const diagram = `classDiagram
        Animal <|-- Mammal
        Mammal <|-- Dog`
      const result = renderMermaidAscii(diagram)

      // Verify ordering: Animal > Mammal > Dog (top to bottom)
      const lines = result.split('\n')
      const animalLine = lines.findIndex(l => l.includes('Animal'))
      const mammalLine = lines.findIndex(l => l.includes('Mammal'))
      const dogLine = lines.findIndex(l => l.includes('Dog'))

      expect(animalLine).toBeLessThan(mammalLine)
      expect(mammalLine).toBeLessThan(dogLine)

      // All triangles should point up
      expect(result.match(/△/g)?.length).toBe(2)
    })

    test('multiple inheritance from same parent', () => {
      const diagram = `classDiagram
        Animal <|-- Dog
        Animal <|-- Cat`
      const result = renderMermaidAscii(diagram)

      // Animal should be above both children
      const lines = result.split('\n')
      const animalLine = lines.findIndex(l => l.includes('Animal'))
      const dogLine = lines.findIndex(l => l.includes('Dog'))
      const catLine = lines.findIndex(l => l.includes('Cat'))

      expect(animalLine).toBeLessThan(dogLine)
      expect(animalLine).toBeLessThan(catLine)

      // Should have at least one triangle pointing up (may merge visually)
      expect(result).toContain('△')
    })

    test('ASCII mode uses ^ for upward triangle', () => {
      const diagram = `classDiagram
        Animal <|-- Dog`
      const result = renderMermaidAscii(diagram, { useAscii: true })

      expect(result).toContain('^')
      expect(result).not.toContain('v')
    })
  })

  // ============================================================================
  // ASSOCIATION (-->)
  // ============================================================================

  describe('Association (-->)', () => {
    test('source above target - arrow points DOWN', () => {
      const diagram = `classDiagram
        Person --> Address`
      const result = renderMermaidAscii(diagram)

      // Should contain downward arrow
      expect(result).toContain('▼')
      expect(result).not.toContain('▲')

      // Person should be above Address
      const lines = result.split('\n')
      const personLine = lines.findIndex(l => l.includes('Person'))
      const addressLine = lines.findIndex(l => l.includes('Address'))
      expect(personLine).toBeLessThan(addressLine)
    })

    test('multiple associations from same source', () => {
      const diagram = `classDiagram
        Person --> Address
        Person --> Phone`
      const result = renderMermaidAscii(diagram)

      // Person should be above both targets
      const lines = result.split('\n')
      const personLine = lines.findIndex(l => l.includes('Person'))
      const addressLine = lines.findIndex(l => l.includes('Address'))
      const phoneLine = lines.findIndex(l => l.includes('Phone'))

      expect(personLine).toBeLessThan(addressLine)
      expect(personLine).toBeLessThan(phoneLine)
    })

    test('chain of associations', () => {
      const diagram = `classDiagram
        A --> B
        B --> C`
      const result = renderMermaidAscii(diagram)

      // A > B > C ordering
      const lines = result.split('\n')
      const aLine = lines.findIndex(l => l.includes('│ A │'))
      const bLine = lines.findIndex(l => l.includes('│ B │'))
      const cLine = lines.findIndex(l => l.includes('│ C │'))

      expect(aLine).toBeLessThan(bLine)
      expect(bLine).toBeLessThan(cLine)

      // Both arrows point down
      expect(result.match(/▼/g)?.length).toBe(2)
    })

    test('ASCII mode uses v for downward arrow', () => {
      const diagram = `classDiagram
        Person --> Address`
      const result = renderMermaidAscii(diagram, { useAscii: true })

      expect(result).toContain('v')
      expect(result).not.toContain('^')
    })
  })

  // ============================================================================
  // DEPENDENCY (..>)
  // ============================================================================

  describe('Dependency (..>)', () => {
    test('source above target - arrow points DOWN', () => {
      const diagram = `classDiagram
        Client ..> Server`
      const result = renderMermaidAscii(diagram)

      expect(result).toContain('▼')
      expect(result).not.toContain('▲')

      const lines = result.split('\n')
      const clientLine = lines.findIndex(l => l.includes('Client'))
      const serverLine = lines.findIndex(l => l.includes('Server'))
      expect(clientLine).toBeLessThan(serverLine)
    })

    test('multiple dependencies', () => {
      const diagram = `classDiagram
        Client ..> Server
        Client ..> Database`
      const result = renderMermaidAscii(diagram)

      const lines = result.split('\n')
      const clientLine = lines.findIndex(l => l.includes('Client'))
      const serverLine = lines.findIndex(l => l.includes('Server'))
      const dbLine = lines.findIndex(l => l.includes('Database'))

      expect(clientLine).toBeLessThan(serverLine)
      expect(clientLine).toBeLessThan(dbLine)
    })

    test('ASCII mode uses v for downward arrow', () => {
      const diagram = `classDiagram
        Client ..> Server`
      const result = renderMermaidAscii(diagram, { useAscii: true })

      expect(result).toContain('v')
    })
  })

  // ============================================================================
  // REALIZATION (..|>)
  // ============================================================================

  describe('Realization (..|>)', () => {
    test('interface above implementation - triangle points UP', () => {
      // Circle ..|> Shape means "Circle implements Shape"
      // Shape (interface/parent) should be placed ABOVE Circle (implementation/child)
      const diagram = `classDiagram
        Circle ..|> Shape`
      const result = renderMermaidAscii(diagram)

      // Shape (interface) should be above Circle (implementation)
      const lines = result.split('\n')
      const shapeLine = lines.findIndex(l => l.includes('Shape'))
      const circleLine = lines.findIndex(l => l.includes('Circle'))
      expect(shapeLine).toBeLessThan(circleLine)
      expect(result).toContain('△')
    })

    test('realization with <|.. syntax (marker at from end)', () => {
      // Shape <|.. Circle means "Circle implements Shape" (same as Circle ..|> Shape)
      const diagram = `classDiagram
        Shape <|.. Circle`
      const result = renderMermaidAscii(diagram)

      // Shape (interface) should be above Circle (implementation)
      const lines = result.split('\n')
      const shapeLine = lines.findIndex(l => l.includes('Shape'))
      const circleLine = lines.findIndex(l => l.includes('Circle'))
      expect(shapeLine).toBeLessThan(circleLine)
      expect(result).toContain('△')
    })

    test('multiple implementations', () => {
      // Circle and Square both implement Shape
      const diagram = `classDiagram
        Circle ..|> Shape
        Square ..|> Shape`
      const result = renderMermaidAscii(diagram)

      // Shape (interface) above both implementations
      const lines = result.split('\n')
      const shapeLine = lines.findIndex(l => l.includes('Shape'))
      const circleLine = lines.findIndex(l => l.includes('Circle'))
      const squareLine = lines.findIndex(l => l.includes('Square'))

      expect(shapeLine).toBeLessThan(circleLine)
      expect(shapeLine).toBeLessThan(squareLine)
      // At least one triangle (may merge visually if same connection point)
      expect(result).toContain('△')
    })
  })

  // ============================================================================
  // COMPOSITION & AGGREGATION (omnidirectional diamonds)
  // ============================================================================

  describe('Composition (*--) and Aggregation (o--)', () => {
    test('composition - diamond is omnidirectional', () => {
      const diagram = `classDiagram
        Car *-- Engine`
      const result = renderMermaidAscii(diagram)

      // Should contain filled diamond
      expect(result).toContain('◆')
    })

    test('aggregation - hollow diamond is omnidirectional', () => {
      const diagram = `classDiagram
        Team o-- Player`
      const result = renderMermaidAscii(diagram)

      // Should contain hollow diamond
      expect(result).toContain('◇')
    })
  })

  // ============================================================================
  // MIXED SCENARIOS
  // ============================================================================

  describe('Mixed Relationship Scenarios', () => {
    test('all 6 relationship types together', () => {
      const diagram = `classDiagram
        A <|-- B : inheritance
        C *-- D : composition
        E o-- F : aggregation
        G --> H : association
        I ..> J : dependency
        K ..|> L : realization`
      const result = renderMermaidAscii(diagram)

      // Upward triangles for inheritance and realization
      expect(result.match(/△/g)?.length).toBe(2)

      // Downward arrows for association and dependency
      expect(result.match(/▼/g)?.length).toBe(2)

      // Diamonds for composition and aggregation
      expect(result).toContain('◆')
      expect(result).toContain('◇')
    })

    test('inheritance with association - different arrow directions', () => {
      const diagram = `classDiagram
        Animal <|-- Dog
        Dog --> Food`
      const result = renderMermaidAscii(diagram)

      // Should have both up triangle (inheritance) and down arrow (association)
      expect(result).toContain('△')
      expect(result).toContain('▼')
    })

    test('circular reference creates valid layout', () => {
      const diagram = `classDiagram
        A --> B
        B --> C
        C ..> A`
      const result = renderMermaidAscii(diagram)

      // Cycles may create mixed arrow directions (up and down) to avoid overlaps
      // Just verify arrows are present and classes are rendered
      const hasUpArrow = result.includes('▲')
      const hasDownArrow = result.includes('▼')
      expect(hasUpArrow || hasDownArrow).toBe(true)
      expect(result).toContain('│ A │')
      expect(result).toContain('│ B │')
      expect(result).toContain('│ C │')
    })
  })

  // ============================================================================
  // ASCII vs UNICODE CONSISTENCY
  // ============================================================================

  describe('ASCII and Unicode Mode Consistency', () => {
    test('same diagram produces consistent layouts in both modes', () => {
      const diagram = `classDiagram
        Animal <|-- Dog
        Person --> Address`

      const unicode = renderMermaidAscii(diagram)
      const ascii = renderMermaidAscii(diagram, { useAscii: true })

      // Both should have same node ordering
      const unicodeLines = unicode.split('\n')
      const asciiLines = ascii.split('\n')

      const uAnimal = unicodeLines.findIndex(l => l.includes('Animal'))
      const uDog = unicodeLines.findIndex(l => l.includes('Dog'))
      const aPerson = asciiLines.findIndex(l => l.includes('Person'))
      const aAddress = asciiLines.findIndex(l => l.includes('Address'))

      expect(uAnimal).toBeLessThan(uDog)
      expect(aPerson).toBeLessThan(aAddress)

      // Unicode has △ and ▼, ASCII has ^ and v
      expect(unicode).toContain('△')
      expect(unicode).toContain('▼')
      expect(ascii).toContain('^')
      expect(ascii).toContain('v')
    })
  })

  // ============================================================================
  // EDGE CASES
  // ============================================================================

  describe('Edge Cases', () => {
    test('single inheritance relationship', () => {
      const diagram = `classDiagram
        A <|-- B`
      const result = renderMermaidAscii(diagram)

      expect(result).toContain('△')
      const lines = result.split('\n')
      const aLine = lines.findIndex(l => l.includes('│ A │'))
      const bLine = lines.findIndex(l => l.includes('│ B │'))
      expect(aLine).toBeLessThan(bLine)
    })

    test('classes with members maintain arrow directions', () => {
      const diagram = `classDiagram
        class Animal {
          +String name
          +eat() void
        }
        class Dog {
          +bark() void
        }
        Animal <|-- Dog`
      const result = renderMermaidAscii(diagram)

      expect(result).toContain('△')
      const lines = result.split('\n')
      const animalLine = lines.findIndex(l => l.includes('Animal'))
      const dogLine = lines.findIndex(l => l.includes('Dog'))
      expect(animalLine).toBeLessThan(dogLine)
    })
  })
})


================================================
FILE: src/__tests__/class-integration.test.ts
================================================
/**
 * Integration tests for class diagrams — end-to-end parse → layout → render.
 */
import { describe, it, expect } from 'bun:test'
import { renderMermaidSVG } from '../index.ts'

describe('renderMermaidSVG – class diagrams', () => {
  it('renders a basic class diagram to valid SVG', () => {
    const svg = renderMermaidSVG(`classDiagram
      class Animal {
        +String name
        +eat() void
      }`)
    expect(svg).toContain('<svg')
    expect(svg).toContain('</svg>')
    expect(svg).toContain('Animal')
    expect(svg).toContain('name')
    expect(svg).toContain('eat')
  })

  it('renders class with annotation', () => {
    const svg = renderMermaidSVG(`classDiagram
      class Flyable {
        <<interface>>
        +fly() void
      }`)
    expect(svg).toContain('interface')
    expect(svg).toContain('Flyable')
    expect(svg).toContain('fly')
  })

  it('renders inheritance relationship with triangle marker', () => {
    const svg = renderMermaidSVG(`classDiagram
      Animal <|-- Dog`)
    expect(svg).toContain('Animal')
    expect(svg).toContain('Dog')
    // Inheritance uses a hollow triangle marker
    expect(svg).toContain('cls-inherit')
  })

  it('renders composition with filled diamond', () => {
    const svg = renderMermaidSVG(`classDiagram
      Car *-- Engine`)
    expect(svg).toContain('cls-composition')
  })

  it('renders aggregation with hollow diamond', () => {
    const svg = renderMermaidSVG(`classDiagram
      University o-- Department`)
    expect(svg).toContain('cls-aggregation')
  })

  it('renders dependency with dashed line', () => {
    const svg = renderMermaidSVG(`classDiagram
      Service ..> Repository`)
    expect(svg).toContain('stroke-dasharray')
    expect(svg).toContain('cls-arrow')
  })

  it('renders realization with dashed line and triangle', () => {
    const svg = renderMermaidSVG(`classDiagram
      Bird ..|> Flyable`)
    expect(svg).toContain('stroke-dasharray')
    expect(svg).toContain('cls-inherit')
  })

  it('renders relationship labels', () => {
    const svg = renderMermaidSVG(`classDiagram
      Customer --> Order : places`)
    expect(svg).toContain('places')
  })

  it('renders class compartments with divider lines', () => {
    const svg = renderMermaidSVG(`classDiagram
      class Animal {
        +String name
        +eat() void
      }`)
    // Should have horizontal divider lines between compartments
    const lines = svg.match(/<line /g) ?? []
    // At least 2 dividers (header-attrs, attrs-methods)
    expect(lines.length).toBeGreaterThanOrEqual(2)
  })

  it('renders with dark colors', () => {
    const svg = renderMermaidSVG(`classDiagram
      class A {
        +x int
      }`, { bg: '#18181B', fg: '#FAFAFA' })
    expect(svg).toContain('--bg:#18181B')
  })

  it('renders a complete class hierarchy', () => {
    const svg = renderMermaidSVG(`classDiagram
      class Animal {
        <<abstract>>
        +String name
        +eat() void
      }
      class Dog {
        +String breed
        +bark() void
      }
      class Cat {
        +bool isIndoor
        +meow() void
      }
      Animal <|-- Dog
      Animal <|-- Cat`)
    expect(svg).toContain('Animal')
    expect(svg).toContain('Dog')
    expect(svg).toContain('Cat')
    expect(svg).toContain('abstract')
  })
})


================================================
FILE: src/__tests__/class-parser.test.ts
================================================
/**
 * Tests for the class diagram parser.
 *
 * Covers: class blocks, attributes, methods, visibility, annotations,
 * relationships (all 6 types), cardinality, labels, inline attributes.
 */
import { describe, it, expect } from 'bun:test'
import { parseClassDiagram } from '../class/parser.ts'

/** Helper to parse — preprocesses text the same way index.ts does */
function parse(text: string) {
  const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith('%%'))
  return parseClassDiagram(lines)
}

// ============================================================================
// Class definitions
// ============================================================================

describe('parseClassDiagram – class definitions', () => {
  it('parses a class block with attributes and methods', () => {
    const d = parse(`classDiagram
      class Animal {
        +String name
        +int age
        +eat() void
        +sleep()
      }`)
    expect(d.classes).toHaveLength(1)
    expect(d.classes[0]!.id).toBe('Animal')
    expect(d.classes[0]!.attributes).toHaveLength(2)
    expect(d.classes[0]!.methods).toHaveLength(2)
  })

  it('parses attribute visibility (+ - # ~)', () => {
    const d = parse(`classDiagram
      class MyClass {
        +String publicField
        -int privateField
        #double protectedField
        ~bool packageField
      }`)
    expect(d.classes[0]!.attributes[0]!.visibility).toBe('+')
    expect(d.classes[0]!.attributes[1]!.visibility).toBe('-')
    expect(d.classes[0]!.attributes[2]!.visibility).toBe('#')
    expect(d.classes[0]!.attributes[3]!.visibility).toBe('~')
  })

  it('parses method with return type', () => {
    const d = parse(`classDiagram
      class Calc {
        +add(a, b) int
      }`)
    expect(d.classes[0]!.methods[0]!.name).toBe('add')
    expect(d.classes[0]!.methods[0]!.type).toBe('int')
  })

  it('parses annotation <<interface>>', () => {
    const d = parse(`classDiagram
      class Flyable {
        <<interface>>
        +fly() void
      }`)
    expect(d.classes[0]!.annotation).toBe('interface')
    expect(d.classes[0]!.methods).toHaveLength(1)
  })

  it('parses inline annotation syntax', () => {
    const d = parse(`classDiagram
      class Shape { <<abstract>> }`)
    expect(d.classes[0]!.annotation).toBe('abstract')
  })

  it('parses standalone class declaration', () => {
    const d = parse(`classDiagram
      class EmptyClass`)
    expect(d.classes).toHaveLength(1)
    expect(d.classes[0]!.id).toBe('EmptyClass')
  })

  it('auto-creates classes from relationships', () => {
    const d = parse(`classDiagram
      Animal <|-- Dog`)
    expect(d.classes).toHaveLength(2)
    expect(d.classes.find(c => c.id === 'Animal')).toBeDefined()
    expect(d.classes.find(c => c.id === 'Dog')).toBeDefined()
  })
})

// ============================================================================
// Inline attributes
// ============================================================================

describe('parseClassDiagram – inline attributes', () => {
  it('parses inline attribute: ClassName : +Type name', () => {
    const d = parse(`classDiagram
      class Animal
      Animal : +String name
      Animal : +int age`)
    const cls = d.classes.find(c => c.id === 'Animal')!
    expect(cls.attributes).toHaveLength(2)
    expect(cls.attributes[0]!.name).toBe('name')
  })
})

// ============================================================================
// Relationships
// ============================================================================

describe('parseClassDiagram – relationships', () => {
  it('parses inheritance: <|-- (marker at from)', () => {
    const d = parse(`classDiagram
      Animal <|-- Dog`)
    expect(d.relationships).toHaveLength(1)
    expect(d.relationships[0]!.type).toBe('inheritance')
    expect(d.relationships[0]!.from).toBe('Animal')
    expect(d.relationships[0]!.to).toBe('Dog')
    expect(d.relationships[0]!.markerAt).toBe('from')
  })

  it('parses composition: *-- (marker at from)', () => {
    const d = parse(`classDiagram
      Car *-- Engine`)
    expect(d.relationships[0]!.type).toBe('composition')
    expect(d.relationships[0]!.markerAt).toBe('from')
  })

  it('parses aggregation: o-- (marker at from)', () => {
    const d = parse(`classDiagram
      University o-- Department`)
    expect(d.relationships[0]!.type).toBe('aggregation')
    expect(d.relationships[0]!.markerAt).toBe('from')
  })

  it('parses association: --> (marker at to)', () => {
    const d = parse(`classDiagram
      Customer --> Order`)
    expect(d.relationships[0]!.type).toBe('association')
    expect(d.relationships[0]!.markerAt).toBe('to')
  })

  it('parses dependency: ..> (marker at to)', () => {
    const d = parse(`classDiagram
      Service ..> Repository`)
    expect(d.relationships[0]!.type).toBe('dependency')
    expect(d.relationships[0]!.markerAt).toBe('to')
  })

  it('parses realization: ..|> (marker at to)', () => {
    const d = parse(`classDiagram
      Bird ..|> Flyable`)
    expect(d.relationships[0]!.type).toBe('realization')
    expect(d.relationships[0]!.markerAt).toBe('to')
  })

  // --- Reversed arrow variants ---

  it('parses reversed realization: <|.. (marker at from)', () => {
    const d = parse(`classDiagram
      Flyable <|.. Bird`)
    expect(d.relationships[0]!.type).toBe('realization')
    expect(d.relationships[0]!.from).toBe('Flyable')
    expect(d.relationships[0]!.to).toBe('Bird')
    expect(d.relationships[0]!.markerAt).toBe('from')
  })

  it('parses reversed composition: --* (marker at to)', () => {
    const d = parse(`classDiagram
      Engine --* Car`)
    expect(d.relationships[0]!.type).toBe('composition')
    expect(d.relationships[0]!.from).toBe('Engine')
    expect(d.relationships[0]!.to).toBe('Car')
    expect(d.relationships[0]!.markerAt).toBe('to')
  })

  it('parses reversed aggregation: --o (marker at to)', () => {
    const d = parse(`classDiagram
      Department --o University`)
    expect(d.relationships[0]!.type).toBe('aggregation')
    expect(d.relationships[0]!.from).toBe('Department')
    expect(d.relationships[0]!.to).toBe('University')
    expect(d.relationships[0]!.markerAt).toBe('to')
  })

  it('parses relationship with label', () => {
    const d = parse(`classDiagram
      Customer --> Order : places`)
    expect(d.relationships[0]!.label).toBe('places')
  })

  it('parses relationship with cardinality', () => {
    const d = parse(`classDiagram
      Customer "1" --> "*" Order : places`)
    expect(d.relationships[0]!.fromCardinality).toBe('1')
    expect(d.relationships[0]!.toCardinality).toBe('*')
  })

  it('handles multiple relationships', () => {
    const d = parse(`classDiagram
      Animal <|-- Dog
      Animal <|-- Cat
      Dog *-- Leg`)
    expect(d.relationships).toHaveLength(3)
  })
})

// ============================================================================
// Full diagram
// ============================================================================

describe('parseClassDiagram – full diagram', () => {
  it('parses a complete class hierarchy', () => {
    const d = parse(`classDiagram
      class Animal {
        <<abstract>>
        +String name
        +eat() void
        +sleep() void
      }
      class Dog {
        +String breed
        +bark() void
      }
      class Cat {
        +bool isIndoor
        +meow() void
      }
      Animal <|-- Dog
      Animal <|-- Cat`)

    expect(d.classes).toHaveLength(3)
    expect(d.relationships).toHaveLength(2)
    const animal = d.classes.find(c => c.id === 'Animal')!
    expect(animal.annotation).toBe('abstract')
    expect(animal.attributes).toHaveLength(1)
    expect(animal.methods).toHaveLength(2)
  })
})


================================================
FILE: src/__tests__/edge-approach-direction.test.ts
================================================
/**
 * Edge Approach Direction Tests
 *
 * Verifies that edges approach target nodes from the correct direction:
 * - Edges entering from TOP should have a vertical final segment (coming from above)
 * - Edges entering from BOTTOM should have a vertical final segment (coming from below)
 * - Edges entering from LEFT should have a horizontal final segment (coming from left)
 * - Edges entering from RIGHT should have a horizontal final segment (coming from right)
 *
 * This prevents visual artifacts where an arrow points to the top of a node
 * but approaches horizontally, creating an awkward bend at the arrowhead.
 */

import { describe, it, expect } from 'bun:test'
import { parseMermaid } from '../parser.ts'
import { layoutGraphSync } from '../layout.ts'

interface Point {
  x: number
  y: number
}

/**
 * Determine if a segment is primarily vertical (dy > dx).
 */
function isVerticalSegment(p1: Point, p2: Point, tolerance = 1): boolean {
  const dx = Math.abs(p2.x - p1.x)
  const dy = Math.abs(p2.y - p1.y)
  return dy > dx || dx < tolerance
}

/**
 * Determine if a segment is primarily horizontal (dx > dy).
 */
function isHorizontalSegment(p1: Point, p2: Point, tolerance = 1): boolean {
  const dx = Math.abs(p2.x - p1.x)
  const dy = Math.abs(p2.y - p1.y)
  return dx > dy || dy < tolerance
}

/**
 * Get the final segment of an edge (last two points).
 */
function getFinalSegment(points: Point[]): { p1: Point; p2: Point } | null {
  if (points.length < 2) return null
  return {
    p1: points[points.length - 2]!,
    p2: points[points.length - 1]!,
  }
}

/**
 * Get the first segment of an edge (first two points).
 */
function getFirstSegment(points: Point[]): { p1: Point; p2: Point } | null {
  if (points.length < 2) return null
  return {
    p1: points[0]!,
    p2: points[1]!,
  }
}

/**
 * Determine which side of a node a point is on.
 */
function getApproachSide(
  point: Point,
  nodeX: number,
  nodeY: number,
  nodeWidth: number,
  nodeHeight: number
): 'top' | 'bottom' | 'left' | 'right' {
  const cx = nodeX + nodeWidth / 2
  const cy = nodeY + nodeHeight / 2
  const dx = point.x - cx
  const dy = point.y - cy

  // Check which edge the point is closest to
  const distTop = Math.abs(point.y - nodeY)
  const distBottom = Math.abs(point.y - (nodeY + nodeHeight))
  const distLeft = Math.abs(point.x - nodeX)
  const distRight = Math.abs(point.x - (nodeX + nodeWidth))

  const minDist = Math.min(distTop, distBottom, distLeft, distRight)

  if (minDist === distTop) return 'top'
  if (minDist === distBottom) return 'bottom'
  if (minDist === distLeft) return 'left'
  return 'right'
}

// ============================================================================
// Test Cases
// ============================================================================

describe('Edge Approach Direction', () => {
  describe('TD layout - edges should approach targets vertically from top', () => {
    it('simple two-node vertical: final segment should be vertical', () => {
      const parsed = parseMermaid(`graph TD
        A --> B`)
      const positioned = layoutGraphSync(parsed, {})

      const edge = positioned.edges.find(
        (e) => e.source === 'A' && e.target === 'B'
      )
      expect(edge).toBeDefined()

      const finalSeg = getFinalSegment(edge!.points)
      expect(finalSeg).not.toBeNull()

      // Edge enters B from top, so final segment should be vertical
      expect(isVerticalSegment(finalSeg!.p1, finalSeg!.p2)).toBe(true)
    })

    it('fan-out pattern: multiple edges to one target should all be vertical', () => {
      // This is the exact pattern from the screenshot
      const parsed = parseMermaid(`graph TD
        Input --> Processor
        Config --> Processor`)
      const positioned = layoutGraphSync(parsed, {})

      const processorNode = positioned.nodes.find((n) => n.id === 'Processor')
      expect(processorNode).toBeDefined()

      // Both edges should approach Processor with vertical final segments
      for (const edge of positioned.edges) {
        if (edge.target === 'Processor') {
          const finalSeg = getFinalSegment(edge.points)
          expect(finalSeg).not.toBeNull()

          // The final segment should be vertical (approaching top edge)
          const isVertical = isVerticalSegment(finalSeg!.p1, finalSeg!.p2)
          expect(isVertical).toBe(true)
        }
      }
    })

    it('fan-in and fan-out pattern: all vertical approaches', () => {
      // Full pattern from the screenshot
      const parsed = parseMermaid(`graph TD
        Input --> Processor
        Config --> Processor
        Processor --> Output
        Processor --> Log`)
      const positioned = layoutGraphSync(parsed, {})

      // Check all edges
      for (const edge of positioned.edges) {
        const targetNode = positioned.nodes.find((n) => n.id === edge.target)
        expect(targetNode).toBeDefined()

        const finalSeg = getFinalSegment(edge.points)
        expect(finalSeg).not.toBeNull()

        // Determine which side the edge approaches
        const approachSide = getApproachSide(
          finalSeg!.p2,
          targetNode!.x,
          targetNode!.y,
          targetNode!.width,
          targetNode!.height
        )

        // For top/bottom approach, final segment must be vertical
        // For left/right approach, final segment must be horizontal
        if (approachSide === 'top' || approachSide === 'bottom') {
          expect(isVerticalSegment(finalSeg!.p1, finalSeg!.p2)).toBe(true)
        } else {
          expect(isHorizontalSegment(finalSeg!.p1, finalSeg!.p2)).toBe(true)
        }
      }
    })
  })

  describe('LR layout - edges should approach targets horizontally', () => {
    it('simple two-node horizontal: final segment should be horizontal', () => {
      const parsed = parseMermaid(`graph LR
        A --> B`)
      const positioned = layoutGraphSync(parsed, {})

      const edge = positioned.edges.find(
        (e) => e.source === 'A' && e.target === 'B'
      )
      expect(edge).toBeDefined()

      const finalSeg = getFinalSegment(edge!.points)
      expect(finalSeg).not.toBeNull()

      // Edge enters B from left, so final segment should be horizontal
      expect(isHorizontalSegment(finalSeg!.p1, finalSeg!.p2)).toBe(true)
    })

    it('fan-out pattern in LR: final segments should be horizontal', () => {
      const parsed = parseMermaid(`graph LR
        Input --> Processor
        Config --> Processor`)
      const positioned = layoutGraphSync(parsed, {})

      for (const edge of positioned.edges) {
        if (edge.target === 'Processor') {
          const finalSeg = getFinalSegment(edge.points)
          expect(finalSeg).not.toBeNull()
          expect(isHorizontalSegment(finalSeg!.p1, finalSeg!.p2)).toBe(true)
        }
      }
    })
  })

  describe('Source exit direction matches side', () => {
    it('TD layout: edges should exit source vertically from bottom', () => {
      const parsed = parseMermaid(`graph TD
        A --> B
        A --> C`)
      const positioned = layoutGraphSync(parsed, {})

      for (const edge of positioned.edges) {
        if (edge.source === 'A') {
          const firstSeg = getFirstSegment(edge.points)
          expect(firstSeg).not.toBeNull()

          // Edge exits A from bottom, so first segment should be vertical
          expect(isVerticalSegment(firstSeg!.p1, firstSeg!.p2)).toBe(true)
        }
      }
    })

    it('LR layout: edges should exit source horizontally from right', () => {
      const parsed = parseMermaid(`graph LR
        A --> B
        A --> C`)
      const positioned = layoutGraphSync(parsed, {})

      for (const edge of positioned.edges) {
        if (edge.source === 'A') {
          const firstSeg = getFirstSegment(edge.points)
          expect(firstSeg).not.toBeNull()

          // Edge exits A from right, so first segment should be horizontal
          expect(isHorizontalSegment(firstSeg!.p1, firstSeg!.p2)).toBe(true)
        }
      }
    })
  })

  describe('Diamond shapes - approach direction preserved', () => {
    it('edges to diamond should approach with correct direction', () => {
      const parsed = parseMermaid(`graph TD
        A --> B{Decision}
        B -->|Yes| C
        B -->|No| D`)
      const positioned = layoutGraphSync(parsed, {})

      // Edge A → B should approach B's top vertically
      const edgeAB = positioned.edges.find(
        (e) => e.source === 'A' && e.target === 'B'
      )
      expect(edgeAB).toBeDefined()

      const finalSegAB = getFinalSegment(edgeAB!.points)
      expect(finalSegAB).not.toBeNull()
      expect(isVerticalSegment(finalSegAB!.p1, finalSegAB!.p2)).toBe(true)
    })

    it('edges should terminate at diamond vertices, not bounding box', () => {
      // In TD layout, edges approaching from above should hit the top vertex,
      // and edges leaving downward should start from the bottom vertex.
      const parsed = parseMermaid(`graph TD
        A[Start] --> B{Decision}
        B --> C[End]`)
      const positioned = layoutGraphSync(parsed, {})

      const diamond = positioned.nodes.find(n => n.id === 'B')
      expect(diamond).toBeDefined()
      expect(diamond!.shape).toBe('diamond')

      // Calculate diamond vertices
      const cx = diamond!.x + diamond!.width / 2
      const topY = diamond!.y
      const bottomY = diamond!.y + diamond!.height

      // Edge A → B should end at the diamond's top vertex
      const edgeAB = positioned.edges.find(e => e.source === 'A' && e.target === 'B')
      expect(edgeAB).toBeDefined()
      const endPointAB = edgeAB!.points[edgeAB!.points.length - 1]!
      expect(endPointAB.x).toBeCloseTo(cx, 0)
      expect(endPointAB.y).toBeCloseTo(topY, 0)

      // Edge B → C should start from the diamond's bottom vertex
      const edgeBC = positioned.edges.find(e => e.source === 'B' && e.target === 'C')
      expect(edgeBC).toBeDefined()
      const startPointBC = edgeBC!.points[0]!
      expect(startPointBC.x).toBeCloseTo(cx, 0)
      expect(startPointBC.y).toBeCloseTo(bottomY, 0)
    })
  })
})


================================================
FILE: src/__tests__/er-integration.test.ts
================================================
/**
 * Integration tests for ER diagrams — end-to-end parse → layout → render.
 */
import { describe, it, expect } from 'bun:test'
import { renderMermaidSVG } from '../index.ts'

describe('renderMermaidSVG – ER diagrams', () => {
  it('renders a basic ER diagram to valid SVG', () => {
    const svg = renderMermaidSVG(`erDiagram
      CUSTOMER ||--o{ ORDER : places`)
    expect(svg).toContain('<svg')
    expect(svg).toContain('</svg>')
    expect(svg).toContain('CUSTOMER')
    expect(svg).toContain('ORDER')
    expect(svg).toContain('places')
  })

  it('renders entity with attributes', () => {
    const svg = renderMermaidSVG(`erDiagram
      CUSTOMER {
        int id PK
        string name
        string email UK
      }`)
    expect(svg).toContain('CUSTOMER')
    expect(svg).toContain('id')
    expect(svg).toContain('name')
    expect(svg).toContain('email')
    // PK/UK key badges
    expect(svg).toContain('PK')
    expect(svg).toContain('UK')
  })

  it('renders relationship lines between entities', () => {
    const svg = renderMermaidSVG(`erDiagram
      A ||--o{ B : has`)
    // Should have polyline for the relationship
    expect(svg).toContain('<polyline')
  })

  it('renders crow\'s foot cardinality markers', () => {
    const svg = renderMermaidSVG(`erDiagram
      CUSTOMER ||--o{ ORDER : places`)
    // Crow's foot markers are rendered as lines
    const lineCount = (svg.match(/<line /g) ?? []).length
    // Entity divider lines + cardinality markers
    expect(lineCount).toBeGreaterThan(2)
  })

  it('renders non-identifying (dashed) relationships', () => {
    const svg = renderMermaidSVG(`erDiagram
      USER ||..o{ LOG : generates`)
    expect(svg).toContain('stroke-dasharray')
  })

  it('renders relationship labels with background pills', () => {
    const svg = renderMermaidSVG(`erDiagram
      A ||--o{ B : places`)
    expect(svg).toContain('places')
    // Background pill behind label
    expect(svg).toContain('rx="2"')
  })

  it('renders with dark colors', () => {
    const svg = renderMermaidSVG(`erDiagram
      A ||--|| B : links`, { bg: '#18181B', fg: '#FAFAFA' })
    expect(svg).toContain('--bg:#18181B')
  })

  it('renders entity boxes with header and attribute rows', () => {
    const svg = renderMermaidSVG(`erDiagram
      USER {
        int id PK
        string name
        string email
      }`)
    // Should have rectangles for entity box and header
    const rectCount = (svg.match(/<rect /g) ?? []).length
    expect(rectCount).toBeGreaterThanOrEqual(2) // outer box + header
  })

  it('renders a complete e-commerce schema', () => {
    const svg = renderMermaidSVG(`erDiagram
      CUSTOMER {
        int id PK
        string name
        string email UK
      }
      ORDER {
        int id PK
        date created
        int customer_id FK
      }
      PRODUCT {
        int id PK
        string name
        float price
      }
      CUSTOMER ||--o{ ORDER : places
      ORDER ||--|{ LINE_ITEM : contains
      PRODUCT ||--o{ LINE_ITEM : includes`)
    expect(svg).toContain('CUSTOMER')
    expect(svg).toContain('ORDER')
    expect(svg).toContain('PRODUCT')
    expect(svg).toContain('LINE_ITEM')
    expect(svg).toContain('places')
    expect(svg).toContain('contains')
    expect(svg).toContain('includes')
  })
})

// ============================================================================
// Label positioning tests — verify that relationship labels sit ON the
// polyline path, not floating in space. The renderer's midpoint() computes
// the arc-length midpoint of the relationship polyline. These tests parse
// the SVG output to verify positioning for both straight and multi-segment
// (orthogonal, bent) paths.
// ============================================================================

/** Extract entity box rects from SVG: returns Map<label, {x, y, width, height, rightEdge}> */
function extractEntityBoxes(svg: string): Map<string, { x: number; y: number; width: number; height: number; rightEdge: number }> {
  const boxes = new Map<string, { x: number; y: number; width: number; height: number; rightEdge: number }>()

  // Entity header text: <text x="..." y="..." ... font-weight="700" ...>LABEL</text>
  const headerPattern = /<text x="([\d.]+)" y="([\d.]+)"[^>]*font-weight="700"[^>]*>([^<]+)<\/text>/g
  let match
  while ((match = headerPattern.exec(svg)) !== null) {
    const centerX = parseFloat(match[1]!)
    const label = match[3]!

    // Find the corresponding outer rect that contains this text.
    const rectPattern = /<rect x="([\d.]+)" y="([\d.]+)" width="([\d.]+)" height="([\d.]+)" rx="0" ry="0"/g
    let rectMatch
    while ((rectMatch = rectPattern.exec(svg)) !== null) {
      const rx = parseFloat(rectMatch[1]!)
      const ry = parseFloat(rectMatch[2]!)
      const rw = parseFloat(rectMatch[3]!)
      const rh = parseFloat(rectMatch[4]!)
      if (centerX >= rx && centerX <= rx + rw) {
        boxes.set(label, { x: rx, y: ry, width: rw, height: rh, rightEdge: rx + rw })
        break
      }
    }
  }

  return boxes
}

/** Extract relationship label positions from SVG: returns Map<label, {x, y}> */
function extractLabelPositions(svg: string): Map<string, { x: number; y: number }> {
  const labels = new Map<string, { x: number; y: number }>()
  // Relationship labels use font-size="11" font-weight="400" — match flexibly
  // regardless of attribute order
  const labelPattern = /<text x="([\d.]+)" y="([\d.]+)"[^>]*font-size="11"[^>]*font-weight="400"[^>]*>([^<]+)<\/text>/g
  let match
  while ((match = labelPattern.exec(svg)) !== null) {
    labels.set(match[3]!, { x: parseFloat(match[1]!), y: parseFloat(match[2]!) })
  }
  return labels
}

/** Extract polyline paths from SVG: returns array of point arrays */
function extractPolylines(svg: stri
Download .txt
gitextract_riz8wh68/

├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── bench.ts
├── dev.ts
├── index.ts
├── package.json
├── samples-data.ts
├── src/
│   ├── __tests__/
│   │   ├── ascii-edge-styles.test.ts
│   │   ├── ascii-multiline.test.ts
│   │   ├── ascii.test.ts
│   │   ├── class-arrow-directions.test.ts
│   │   ├── class-integration.test.ts
│   │   ├── class-parser.test.ts
│   │   ├── edge-approach-direction.test.ts
│   │   ├── er-integration.test.ts
│   │   ├── er-parser.test.ts
│   │   ├── integration.test.ts
│   │   ├── layout-disconnected.test.ts
│   │   ├── linkstyle.test.ts
│   │   ├── multiline-labels.test.ts
│   │   ├── parser.test.ts
│   │   ├── renderer.test.ts
│   │   ├── sequence-integration.test.ts
│   │   ├── sequence-layout.test.ts
│   │   ├── sequence-parser.test.ts
│   │   ├── styles.test.ts
│   │   ├── testdata/
│   │   │   ├── ascii/
│   │   │   │   ├── ampersand_lhs.txt
│   │   │   │   ├── ampersand_lhs_and_rhs.txt
│   │   │   │   ├── ampersand_rhs.txt
│   │   │   │   ├── ampersand_td_fanin.txt
│   │   │   │   ├── ampersand_td_fanout.txt
│   │   │   │   ├── ampersand_without_edge.txt
│   │   │   │   ├── back_reference_from_child.txt
│   │   │   │   ├── backlink_from_bottom.txt
│   │   │   │   ├── backlink_from_top.txt
│   │   │   │   ├── backlink_with_short_y_padding.txt
│   │   │   │   ├── cls_all_relationships.txt
│   │   │   │   ├── cls_annotation.txt
│   │   │   │   ├── cls_association.txt
│   │   │   │   ├── cls_basic.txt
│   │   │   │   ├── cls_dependency.txt
│   │   │   │   ├── cls_inheritance.txt
│   │   │   │   ├── cls_methods.txt
│   │   │   │   ├── comments.txt
│   │   │   │   ├── custom_padding.txt
│   │   │   │   ├── duplicate_labels.txt
│   │   │   │   ├── er_attributes.txt
│   │   │   │   ├── er_basic.txt
│   │   │   │   ├── er_identifying.txt
│   │   │   │   ├── flowchart_tb_simple.txt
│   │   │   │   ├── graph_bt_direction.txt
│   │   │   │   ├── graph_tb_direction.txt
│   │   │   │   ├── nested_subgraphs_with_labels.txt
│   │   │   │   ├── preserve_order_of_definition.txt
│   │   │   │   ├── self_reference.txt
│   │   │   │   ├── self_reference_with_edge.txt
│   │   │   │   ├── seq_basic.txt
│   │   │   │   ├── seq_multiple_messages.txt
│   │   │   │   ├── seq_self_message.txt
│   │   │   │   ├── single_node.txt
│   │   │   │   ├── single_node_longer_name.txt
│   │   │   │   ├── subgraph_complex_mixed.txt
│   │   │   │   ├── subgraph_complex_nested.txt
│   │   │   │   ├── subgraph_direction_override.txt
│   │   │   │   ├── subgraph_empty.txt
│   │   │   │   ├── subgraph_mixed_nodes.txt
│   │   │   │   ├── subgraph_mixed_nodes_td.txt
│   │   │   │   ├── subgraph_multiple_edges.txt
│   │   │   │   ├── subgraph_multiple_nodes.txt
│   │   │   │   ├── subgraph_nested.txt
│   │   │   │   ├── subgraph_nested_with_external.txt
│   │   │   │   ├── subgraph_node_outside_lr.txt
│   │   │   │   ├── subgraph_single_node.txt
│   │   │   │   ├── subgraph_td_direction.txt
│   │   │   │   ├── subgraph_td_multiple.txt
│   │   │   │   ├── subgraph_td_multiple_paddingy.txt
│   │   │   │   ├── subgraph_three_levels_nested.txt
│   │   │   │   ├── subgraph_three_separate.txt
│   │   │   │   ├── subgraph_two_separate.txt
│   │   │   │   ├── subgraph_with_labels.txt
│   │   │   │   ├── three_nodes.txt
│   │   │   │   ├── three_nodes_single_line.txt
│   │   │   │   ├── two_layer_single_graph.txt
│   │   │   │   ├── two_layer_single_graph_longer_names.txt
│   │   │   │   ├── two_nodes_linked.txt
│   │   │   │   ├── two_nodes_longer_names.txt
│   │   │   │   ├── two_root_nodes.txt
│   │   │   │   ├── two_root_nodes_longer_names.txt
│   │   │   │   └── two_single_root_nodes.txt
│   │   │   └── unicode/
│   │   │       ├── ampersand_lhs.txt
│   │   │       ├── ampersand_lhs_and_rhs.txt
│   │   │       ├── ampersand_rhs.txt
│   │   │       ├── ampersand_without_edge.txt
│   │   │       ├── back_reference_from_child.txt
│   │   │       ├── backlink_from_bottom.txt
│   │   │       ├── backlink_from_top.txt
│   │   │       ├── cls_all_relationships.txt
│   │   │       ├── cls_annotation.txt
│   │   │       ├── cls_association.txt
│   │   │       ├── cls_basic.txt
│   │   │       ├── cls_dependency.txt
│   │   │       ├── cls_inheritance.txt
│   │   │       ├── cls_methods.txt
│   │   │       ├── comments.txt
│   │   │       ├── duplicate_labels.txt
│   │   │       ├── er_attributes.txt
│   │   │       ├── er_basic.txt
│   │   │       ├── er_identifying.txt
│   │   │       ├── graph_bt_direction.txt
│   │   │       ├── preserve_order_of_definition.txt
│   │   │       ├── self_reference.txt
│   │   │       ├── self_reference_with_edge.txt
│   │   │       ├── seq_basic.txt
│   │   │       ├── seq_multiple_messages.txt
│   │   │       ├── seq_self_message.txt
│   │   │       ├── single_node.txt
│   │   │       ├── single_node_longer_name.txt
│   │   │       ├── three_nodes.txt
│   │   │       ├── three_nodes_single_line.txt
│   │   │       ├── two_layer_single_graph.txt
│   │   │       ├── two_layer_single_graph_longer_names.txt
│   │   │       ├── two_nodes_linked.txt
│   │   │       ├── two_nodes_longer_names.txt
│   │   │       ├── two_root_nodes.txt
│   │   │       ├── two_root_nodes_longer_names.txt
│   │   │       └── two_single_root_nodes.txt
│   │   ├── text-metrics.test.ts
│   │   ├── xychart-ascii.test.ts
│   │   └── xychart-integration.test.ts
│   ├── ascii/
│   │   ├── ansi.ts
│   │   ├── canvas.ts
│   │   ├── class-diagram.ts
│   │   ├── converter.ts
│   │   ├── draw.ts
│   │   ├── edge-bundling.ts
│   │   ├── edge-routing.ts
│   │   ├── er-diagram.ts
│   │   ├── grid.ts
│   │   ├── index.ts
│   │   ├── multiline-utils.ts
│   │   ├── pathfinder.ts
│   │   ├── sequence.ts
│   │   ├── shapes/
│   │   │   ├── circle.ts
│   │   │   ├── corners.ts
│   │   │   ├── diamond.ts
│   │   │   ├── hexagon.ts
│   │   │   ├── index.ts
│   │   │   ├── rectangle.ts
│   │   │   ├── rounded.ts
│   │   │   ├── special.ts
│   │   │   ├── stadium.ts
│   │   │   ├── state.ts
│   │   │   └── types.ts
│   │   ├── types.ts
│   │   ├── validate.ts
│   │   └── xychart.ts
│   ├── browser.ts
│   ├── class/
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   ├── renderer.ts
│   │   └── types.ts
│   ├── elk-instance.ts
│   ├── er/
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   ├── renderer.ts
│   │   └── types.ts
│   ├── index.ts
│   ├── layout-engine.ts
│   ├── layout.ts
│   ├── multiline-utils.ts
│   ├── parser.ts
│   ├── renderer.ts
│   ├── sequence/
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   ├── renderer.ts
│   │   └── types.ts
│   ├── shape-clipping.ts
│   ├── styles.ts
│   ├── text-metrics.ts
│   ├── theme.ts
│   ├── types.ts
│   └── xychart/
│       ├── colors.ts
│       ├── layout.ts
│       ├── parser.ts
│       ├── renderer.ts
│       └── types.ts
├── tsconfig.json
├── tsup.config.ts
├── wrangler.toml
├── xychart-design.md
├── xychart-samples-data.ts
├── xychart-test.html
└── xychart-test.ts
Download .txt
SYMBOL INDEX (555 symbols across 73 files)

FILE: bench.ts
  type Result (line 18) | interface Result {
  function col (line 33) | function col(value: string, width: number, align: 'left' | 'right' = 'le...
  function fmtMs (line 38) | function fmtMs(ms: number): string {

FILE: dev.ts
  constant PORT (line 18) | const PORT = 3456
  constant ROOT (line 19) | const ROOT = import.meta.dir
  function rebuild (line 28) | async function rebuild(): Promise<void> {
  function onFileChange (line 63) | function onFileChange(_event: string, filename: string | null): void {
  method fetch (line 88) | async fetch(req) {

FILE: index.ts
  function escapeHtml (line 31) | function escapeHtml(text: string): string {
  function formatDescription (line 40) | function formatDescription(text: string): string {
  constant THEME_LABELS (line 45) | const THEME_LABELS: Record<string, string> = {
  function generateHtml (line 62) | async function generateHtml(): Promise<string> {

FILE: samples-data.ts
  type Sample (line 13) | interface Sample {

FILE: src/__tests__/ascii.test.ts
  type TestCase (line 20) | interface TestCase {
  function parseTestCase (line 36) | function parseTestCase(content: string): TestCase {
  function normalizeWhitespace (line 98) | function normalizeWhitespace(s: string): string {
  function visualizeWhitespace (line 115) | function visualizeWhitespace(s: string): string {
  function runGoldenTests (line 123) | function runGoldenTests(dir: string, useAscii: boolean): void {

FILE: src/__tests__/class-parser.test.ts
  function parse (line 11) | function parse(text: string) {

FILE: src/__tests__/edge-approach-direction.test.ts
  type Point (line 18) | interface Point {
  function isVerticalSegment (line 26) | function isVerticalSegment(p1: Point, p2: Point, tolerance = 1): boolean {
  function isHorizontalSegment (line 35) | function isHorizontalSegment(p1: Point, p2: Point, tolerance = 1): boole...
  function getFinalSegment (line 44) | function getFinalSegment(points: Point[]): { p1: Point; p2: Point } | nu...
  function getFirstSegment (line 55) | function getFirstSegment(points: Point[]): { p1: Point; p2: Point } | nu...
  function getApproachSide (line 66) | function getApproachSide(

FILE: src/__tests__/er-integration.test.ts
  function extractEntityBoxes (line 121) | function extractEntityBoxes(svg: string): Map<string, { x: number; y: nu...
  function extractLabelPositions (line 150) | function extractLabelPositions(svg: string): Map<string, { x: number; y:...
  function extractPolylines (line 163) | function extractPolylines(svg: string): Array<Array<{ x: number; y: numb...
  function distanceToPolyline (line 183) | function distanceToPolyline(point: { x: number; y: number }, polyline: A...
  function pointToSegmentDist (line 195) | function pointToSegmentDist(p: { x: number; y: number }, a: { x: number;...
  function closestPolylineDistance (line 211) | function closestPolylineDistance(label: { x: number; y: number }, polyli...

FILE: src/__tests__/er-parser.test.ts
  function parse (line 11) | function parse(text: string) {

FILE: src/__tests__/layout-disconnected.test.ts
  function rectanglesOverlap (line 19) | function rectanglesOverlap(
  function getBoundingBox (line 32) | function getBoundingBox(items: Array<{ x: number; y: number; width: numb...

FILE: src/__tests__/multiline-labels.test.ts
  function extractFirstRectHeight (line 837) | function extractFirstRectHeight(svg: string): number {
  function extractFirstRectWidth (line 842) | function extractFirstRectWidth(svg: string): number {

FILE: src/__tests__/renderer.test.ts
  function makeGraph (line 13) | function makeGraph(overrides: Partial<PositionedGraph> = {}): Positioned...
  function makeNode (line 25) | function makeNode(overrides: Partial<PositionedNode> = {}): PositionedNo...
  function makeEdge (line 39) | function makeEdge(overrides: Partial<PositionedEdge> = {}): PositionedEd...

FILE: src/__tests__/sequence-layout.test.ts
  function layout (line 13) | function layout(source: string) {

FILE: src/__tests__/sequence-parser.test.ts
  function parse (line 11) | function parse(text: string) {

FILE: src/__tests__/xychart-ascii.test.ts
  function render (line 14) | function render(text: string, useAscii = false): string {

FILE: src/__tests__/xychart-integration.test.ts
  constant BAR_CHART (line 10) | const BAR_CHART = `xychart-beta
  constant LINE_CHART (line 15) | const LINE_CHART = `xychart-beta
  constant MIXED_CHART (line 20) | const MIXED_CHART = `xychart-beta

FILE: src/ascii/ansi.ts
  constant DEFAULT_ASCII_THEME (line 23) | const DEFAULT_ASCII_THEME: AsciiTheme = {
  function mixColors (line 41) | function mixColors(fg: string, bg: string, pct: number): string {
  function diagramColorsToAsciiTheme (line 53) | function diagramColorsToAsciiTheme(colors: DiagramColors): AsciiTheme {
  function detectColorMode (line 83) | function detectColorMode(): ColorMode {
  function parseHex (line 131) | function parseHex(hex: string): { r: number; g: number; b: number } {
  constant ESC (line 152) | const ESC = '\x1b['
  constant RESET (line 154) | const RESET = `${ESC}0m`
  function truecolorFg (line 160) | function truecolorFg(hex: string): string {
  function rgbTo256 (line 172) | function rgbTo256(r: number, g: number, b: number): number {
  function ansi256Fg (line 203) | function ansi256Fg(hex: string): string {
  function ansi16Fg (line 217) | function ansi16Fg(hex: string): string {
  function escapeHtml (line 244) | function escapeHtml(text: string): string {
  function htmlSpan (line 249) | function htmlSpan(hex: string, text: string): string {
  function getRoleColor (line 260) | function getRoleColor(role: CharRole, theme: AsciiTheme): string {
  function getAnsiColor (line 275) | function getAnsiColor(role: CharRole, theme: AsciiTheme, mode: ColorMode...
  function getAnsiReset (line 291) | function getAnsiReset(mode: ColorMode): string {
  function colorizeChar (line 298) | function colorizeChar(
  function colorizeLine (line 316) | function colorizeLine(
  function colorizeLineHtml (line 387) | function colorizeLineHtml(
  function colorizeText (line 436) | function colorizeText(text: string, hex: string, mode: ColorMode): string {

FILE: src/ascii/canvas.ts
  function mkCanvas (line 16) | function mkCanvas(x: number, y: number): Canvas {
  function copyCanvas (line 29) | function copyCanvas(source: Canvas): Canvas {
  function mkRoleCanvas (line 42) | function mkRoleCanvas(x: number, y: number): RoleCanvas {
  function copyRoleCanvas (line 55) | function copyRoleCanvas(source: RoleCanvas): RoleCanvas {
  function increaseRoleCanvasSize (line 65) | function increaseRoleCanvasSize(roleCanvas: RoleCanvas, newX: number, ne...
  function setRole (line 87) | function setRole(roleCanvas: RoleCanvas, x: number, y: number, role: Cha...
  function mergeRoleCanvases (line 98) | function mergeRoleCanvases(
  function getCanvasSize (line 142) | function getCanvasSize(canvas: Canvas): [number, number] {
  function increaseSize (line 150) | function increaseSize(canvas: Canvas, newX: number, newY: number): Canvas {
  constant JUNCTION_CHARS (line 173) | const JUNCTION_CHARS = new Set([
  function isJunctionChar (line 177) | function isJunctionChar(c: string): boolean {
  function isAlphanumeric (line 182) | function isAlphanumeric(c: string): boolean {
  constant JUNCTION_MAP (line 191) | const JUNCTION_MAP: Record<string, Record<string, string>> = {
  function mergeJunctions (line 204) | function mergeJunctions(c1: string, c2: string): string {
  function mergeCanvases (line 217) | function mergeCanvases(
  type CanvasToStringOptions (line 271) | interface CanvasToStringOptions {
  function canvasToString (line 284) | function canvasToString(canvas: Canvas, options?: CanvasToStringOptions)...
  constant VERTICAL_FLIP_MAP (line 327) | const VERTICAL_FLIP_MAP: Record<string, string> = {
  function flipCanvasVertically (line 351) | function flipCanvasVertically(canvas: Canvas): Canvas {
  function flipRoleCanvasVertically (line 372) | function flipRoleCanvasVertically(roleCanvas: RoleCanvas): RoleCanvas {
  function drawText (line 384) | function drawText(
  function setCanvasSizeToGrid (line 405) | function setCanvasSizeToGrid(
  function setRoleCanvasSizeToGrid (line 421) | function setRoleCanvasSizeToGrid(

FILE: src/ascii/class-diagram.ts
  function classifyBoxChar (line 21) | function classifyBoxChar(ch: string): CharRole {
  function formatMember (line 31) | function formatMember(m: ClassMember): string {
  function buildClassSections (line 38) | function buildClassSections(cls: ClassNode): string[][] {
  type RelMarker (line 64) | interface RelMarker {
  function getRelMarker (line 77) | function getRelMarker(type: RelationshipType, markerAt: 'from' | 'to'): ...
  function getMarkerShape (line 87) | function getMarkerShape(
  type PlacedClass (line 137) | interface PlacedClass {
  function renderClassAscii (line 151) | function renderClassAscii(text: string, config: AsciiConfig, colorMode?:...

FILE: src/ascii/converter.ts
  function convertToAsciiGraph (line 25) | function convertToAsciiGraph(parsed: MermaidGraph, config: AsciiConfig):...
  function convertSubgraph (line 118) | function convertSubgraph(
  function deduplicateSubgraphNodes (line 177) | function deduplicateSubgraphNodes(
  function isAncestorOrSelf (line 241) | function isAncestorOrSelf(candidate: AsciiSubgraph, target: AsciiSubgrap...
  function buildSgMap (line 251) | function buildSgMap(

FILE: src/ascii/draw.ts
  function drawNode (line 39) | function drawNode(node: AsciiNode, graph: AsciiGraph): Canvas {
  function drawBoxWithGridDimensions (line 56) | function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): ...
  function drawBox (line 123) | function drawBox(node: AsciiNode, graph: AsciiGraph): Canvas {
  function drawMultiBox (line 141) | function drawMultiBox(
  constant LINE_CHARS (line 236) | const LINE_CHARS = {
  function drawLine (line 260) | function drawLine(
  function drawArrow (line 380) | function drawArrow(
  function reverseDirection (line 436) | function reverseDirection(dir: Direction): Direction {
  function drawPath (line 452) | function drawPath(
  function drawBoxStart (line 488) | function drawBoxStart(
  function drawArrowHead (line 517) | function drawArrowHead(
  function drawCorners (line 575) | function drawCorners(graph: AsciiGraph, path: GridCoord[]): Canvas {
  function drawArrowLabel (line 612) | function drawArrowLabel(graph: AsciiGraph, edge: AsciiEdge): Canvas {
  function drawTextOnLine (line 647) | function drawTextOnLine(canvas: Canvas, line: DrawingCoord[], label: str...
  function getNodeAttachmentPoint (line 691) | function getNodeAttachmentPoint(
  function drawBundledEdgeSegment (line 731) | function drawBundledEdgeSegment(
  function drawBundleSharedPath (line 832) | function drawBundleSharedPath(graph: AsciiGraph, bundle: EdgeBundle): [C...
  function drawBundleArrowhead (line 908) | function drawBundleArrowhead(graph: AsciiGraph, bundle: EdgeBundle): Can...
  function drawBundledEdgeArrowhead (line 952) | function drawBundledEdgeArrowhead(graph: AsciiGraph, edge: AsciiEdge): C...
  function drawJunctionCharacter (line 1001) | function drawJunctionCharacter(graph: AsciiGraph, bundle: EdgeBundle): C...
  function drawSubgraphBox (line 1098) | function drawSubgraphBox(sg: AsciiSubgraph, graph: AsciiGraph): Canvas {
  function drawSubgraphLabel (line 1131) | function drawSubgraphLabel(sg: AsciiSubgraph, graph: AsciiGraph): [Canva...
  function sortSubgraphsByDepth (line 1163) | function sortSubgraphsByDepth(subgraphs: AsciiSubgraph[]): AsciiSubgraph...
  function fillRolesFromCanvas (line 1180) | function fillRolesFromCanvas(
  function fillRolesFromCanvases (line 1204) | function fillRolesFromCanvases(
  function fillRolesForNodeBox (line 1219) | function fillRolesForNodeBox(
  function drawGraph (line 1255) | function drawGraph(graph: AsciiGraph): Canvas {

FILE: src/ascii/edge-bundling.ts
  function analyzeEdgeBundles (line 41) | function analyzeEdgeBundles(graph: AsciiGraph): EdgeBundle[] {
  function canBundle (line 133) | function canBundle(edges: AsciiEdge[], graph: AsciiGraph): boolean {
  function calculateJunctionPoint (line 178) | function calculateJunctionPoint(
  function routeBundledEdges (line 248) | function routeBundledEdges(graph: AsciiGraph, bundle: EdgeBundle): void {
  function processBundles (line 324) | function processBundles(graph: AsciiGraph): void {

FILE: src/ascii/edge-routing.ts
  function getOpposite (line 21) | function getOpposite(d: Direction): Direction {
  function dirEquals (line 34) | function dirEquals(a: Direction, b: Direction): boolean {
  function determineDirection (line 42) | function determineDirection(from: { x: number; y: number }, to: { x: num...
  function selfReferenceDirection (line 59) | function selfReferenceDirection(graphDirection: string): [Direction, Dir...
  function determineStartAndEndDir (line 71) | function determineStartAndEndDir(
  function determinePath (line 155) | function determinePath(graph: AsciiGraph, edge: AsciiEdge): void {
  function determineLabelLine (line 227) | function determineLabelLine(graph: AsciiGraph, edge: AsciiEdge): void {
  function calculateLineWidth (line 288) | function calculateLineWidth(graph: AsciiGraph, line: [GridCoord, GridCoo...

FILE: src/ascii/er-diagram.ts
  function classifyBoxChar (line 20) | function classifyBoxChar(ch: string): CharRole {
  function formatAttribute (line 30) | function formatAttribute(attr: ErAttribute): string {
  function buildEntitySections (line 36) | function buildEntitySections(entity: ErEntity): string[][] {
  function getCrowsFootChars (line 62) | function getCrowsFootChars(card: Cardinality, useAscii: boolean, isRight...
  type PlacedEntity (line 85) | interface PlacedEntity {
  function findConnectedComponents (line 104) | function findConnectedComponents(diagram: ErDiagram): Set<string>[] {
  function renderErAscii (line 159) | function renderErAscii(text: string, config: AsciiConfig, colorMode?: Co...

FILE: src/ascii/grid.ts
  function gridToDrawingCoord (line 30) | function gridToDrawingCoord(
  function lineToDrawing (line 58) | function lineToDrawing(graph: AsciiGraph, line: GridCoord[]): DrawingCoo...
  function reserveSpotInGrid (line 75) | function reserveSpotInGrid(
  function setColumnWidth (line 114) | function setColumnWidth(graph: AsciiGraph, node: AsciiNode): void {
  function increaseGridSizeForPath (line 159) | function increaseGridSizeForPath(graph: AsciiGraph, path: GridCoord[]): ...
  function isNodeInAnySubgraph (line 174) | function isNodeInAnySubgraph(graph: AsciiGraph, node: AsciiNode): boolean {
  function getNodeSubgraph (line 182) | function getNodeSubgraph(graph: AsciiGraph, node: AsciiNode): AsciiSubgr...
  function isAncestorOrSelf (line 197) | function isAncestorOrSelf(candidate: AsciiSubgraph, target: AsciiSubgrap...
  function getEffectiveDirection (line 211) | function getEffectiveDirection(graph: AsciiGraph, node: AsciiNode): 'LR'...
  function hasIncomingEdgeFromOutsideSubgraph (line 224) | function hasIncomingEdgeFromOutsideSubgraph(graph: AsciiGraph, node: Asc...
  function calculateSubgraphBoundingBox (line 266) | function calculateSubgraphBoundingBox(graph: AsciiGraph, sg: AsciiSubgra...
  function ensureSubgraphSpacing (line 307) | function ensureSubgraphSpacing(graph: AsciiGraph): void {
  function calculateSubgraphBoundingBoxes (line 336) | function calculateSubgraphBoundingBoxes(graph: AsciiGraph): void {
  function offsetDrawingForSubgraphs (line 347) | function offsetDrawingForSubgraphs(graph: AsciiGraph): void {
  function createMapping (line 394) | function createMapping(graph: AsciiGraph): void {
  function getEdgesFromNode (line 571) | function getEdgesFromNode(graph: AsciiGraph, node: AsciiNode): AsciiGrap...
  function getChildren (line 576) | function getChildren(graph: AsciiGraph, node: AsciiNode): AsciiNode[] {

FILE: src/ascii/index.ts
  type AsciiRenderOptions (line 35) | interface AsciiRenderOptions {
  function detectDiagramType (line 63) | function detectDiagramType(text: string): 'flowchart' | 'sequence' | 'cl...
  function renderMermaidASCII (line 101) | function renderMermaidASCII(

FILE: src/ascii/multiline-utils.ts
  function splitLines (line 16) | function splitLines(label: string): string[] {
  function maxLineWidth (line 24) | function maxLineWidth(label: string): number {
  function lineCount (line 33) | function lineCount(label: string): number {
  function drawMultilineTextCentered (line 42) | function drawMultilineTextCentered(
  function drawMultilineTextLeft (line 66) | function drawMultilineTextLeft(

FILE: src/ascii/pathfinder.ts
  type PQItem (line 16) | interface PQItem {
  class MinHeap (line 25) | class MinHeap {
    method length (line 28) | get length(): number {
    method push (line 32) | push(item: PQItem): void {
    method pop (line 37) | pop(): PQItem | undefined {
    method bubbleUp (line 48) | private bubbleUp(i: number): void {
    method sinkDown (line 60) | private sinkDown(i: number): void {
  function heuristic (line 90) | function heuristic(a: GridCoord, b: GridCoord): number {
  constant MOVE_DIRS (line 104) | const MOVE_DIRS: GridCoord[] = [
  function isFreeInGrid (line 112) | function isFreeInGrid(grid: Map<string, AsciiNode>, c: GridCoord): boole...
  function getPath (line 121) | function getPath(
  function mergePath (line 180) | function mergePath(path: GridCoord[]): GridCoord[] {

FILE: src/ascii/sequence.ts
  function classifyBoxChar (line 19) | function classifyBoxChar(ch: string): CharRole {
  function renderSequenceAscii (line 29) | function renderSequenceAscii(text: string, config: AsciiConfig, colorMod...

FILE: src/ascii/shapes/circle.ts
  method render (line 21) | render(label, dimensions, options) {

FILE: src/ascii/shapes/corners.ts
  type CornerChars (line 14) | interface CornerChars {
  type ShapeCorners (line 28) | interface ShapeCorners {
  constant SHAPE_CORNERS (line 41) | const SHAPE_CORNERS: Record<AsciiNodeShape, ShapeCorners> = {
  function getCorners (line 124) | function getCorners(shape: AsciiNodeShape, useAscii: boolean): CornerCha...

FILE: src/ascii/shapes/diamond.ts
  method render (line 21) | render(label, dimensions, options) {

FILE: src/ascii/shapes/hexagon.ts
  method render (line 21) | render(label, dimensions, options) {

FILE: src/ascii/shapes/index.ts
  function getShapeRenderer (line 59) | function getShapeRenderer(shape: AsciiNodeShape): ShapeRenderer {
  function renderShape (line 67) | function renderShape(
  function getShapeDimensions (line 81) | function getShapeDimensions(
  function getShapeAttachmentPoint (line 93) | function getShapeAttachmentPoint(

FILE: src/ascii/shapes/rectangle.ts
  function getBoxDimensions (line 25) | function getBoxDimensions(label: string, options: ShapeRenderOptions): S...
  function renderBox (line 68) | function renderBox(
  function getBoxAttachmentPoint (line 132) | function getBoxAttachmentPoint(
  method render (line 167) | render(label: string, dimensions: ShapeDimensions, options: ShapeRenderO...

FILE: src/ascii/shapes/rounded.ts
  method render (line 21) | render(label, dimensions, options) {

FILE: src/ascii/shapes/special.ts
  method getDimensions (line 29) | getDimensions(label: string, options: ShapeRenderOptions): ShapeDimensio...
  method render (line 53) | render(label: string, dimensions: ShapeDimensions, options: ShapeRenderO...
  method render (line 121) | render(label, dimensions, options) {
  method getDimensions (line 143) | getDimensions(label: string, options: ShapeRenderOptions): ShapeDimensio...
  method render (line 167) | render(label: string, dimensions: ShapeDimensions, options: ShapeRenderO...
  method render (line 239) | render(label, dimensions, options) {
  method render (line 263) | render(label, dimensions, options) {
  method render (line 287) | render(label, dimensions, options) {

FILE: src/ascii/shapes/stadium.ts
  method getDimensions (line 31) | getDimensions(label: string, options: ShapeRenderOptions): ShapeDimensio...
  method render (line 55) | render(label: string, dimensions: ShapeDimensions, options: ShapeRenderO...

FILE: src/ascii/shapes/state.ts
  method getDimensions (line 25) | getDimensions(_label: string, _options: ShapeRenderOptions): ShapeDimens...
  method render (line 39) | render(_label: string, dimensions: ShapeDimensions, options: ShapeRender...
  method getAttachmentPoint (line 84) | getAttachmentPoint(
  method getDimensions (line 117) | getDimensions(_label: string, _options: ShapeRenderOptions): ShapeDimens...
  method render (line 131) | render(_label: string, dimensions: ShapeDimensions, options: ShapeRender...
  method getAttachmentPoint (line 176) | getAttachmentPoint(

FILE: src/ascii/shapes/types.ts
  type ShapeDimensions (line 10) | interface ShapeDimensions {
  type ShapeRenderOptions (line 31) | interface ShapeRenderOptions {
  type ShapeRenderer (line 42) | interface ShapeRenderer {
  type ShapeRegistry (line 73) | type ShapeRegistry = Map<AsciiNodeShape, ShapeRenderer>

FILE: src/ascii/types.ts
  type AsciiNodeShape (line 18) | type AsciiNodeShape = NodeShape
  type GridCoord (line 21) | interface GridCoord {
  type DrawingCoord (line 27) | interface DrawingCoord {
  type Direction (line 41) | interface Direction {
  constant ALL_DIRECTIONS (line 57) | const ALL_DIRECTIONS: readonly Direction[] = [
  type Canvas (line 65) | type Canvas = string[][]
  type AsciiNode (line 68) | interface AsciiNode {
  type AsciiStyleClass (line 85) | interface AsciiStyleClass {
  type AsciiEdgeStyle (line 91) | type AsciiEdgeStyle = 'solid' | 'dotted' | 'thick'
  type AsciiEdge (line 94) | interface AsciiEdge {
  type AsciiSubgraph (line 119) | interface AsciiSubgraph {
  type AsciiConfig (line 133) | interface AsciiConfig {
  type AsciiGraph (line 147) | interface AsciiGraph {
  function gridCoordEquals (line 170) | function gridCoordEquals(a: GridCoord, b: GridCoord): boolean {
  function drawingCoordEquals (line 174) | function drawingCoordEquals(a: DrawingCoord, b: DrawingCoord): boolean {
  function gridCoordDirection (line 179) | function gridCoordDirection(c: GridCoord, dir: Direction): GridCoord {
  function gridKey (line 184) | function gridKey(c: GridCoord): string {
  constant EMPTY_STYLE (line 189) | const EMPTY_STYLE: AsciiStyleClass = { name: '', styles: {} }
  type CharRole (line 199) | type CharRole =
  type RoleCanvas (line 212) | type RoleCanvas = (CharRole | null)[][]
  type AsciiTheme (line 218) | interface AsciiTheme {
  type ColorMode (line 238) | type ColorMode =
  type EdgeBundle (line 256) | interface EdgeBundle {

FILE: src/ascii/validate.ts
  constant DIAGONAL_CHARS (line 12) | const DIAGONAL_CHARS = {
  type DiagonalPosition (line 21) | interface DiagonalPosition {
  function hasDiagonalLines (line 34) | function hasDiagonalLines(asciiOutput: string): boolean {
  function findDiagonalLines (line 48) | function findDiagonalLines(asciiOutput: string): DiagonalPosition[] {
  function assertNoDiagonals (line 104) | function assertNoDiagonals(asciiOutput: string, context?: string): void {

FILE: src/ascii/xychart.ts
  constant PLOT_WIDTH (line 24) | const PLOT_WIDTH = 60
  constant PLOT_HEIGHT (line 25) | const PLOT_HEIGHT = 20
  constant UNI (line 28) | const UNI = {
  constant ASC (line 43) | const ASC = {
  type HexCanvas (line 62) | type HexCanvas = (string | null)[][]
  function getSeriesColors (line 65) | function getSeriesColors(total: number, theme: AsciiTheme): string[] {
  function roleToHex (line 72) | function roleToHex(role: CharRole, theme: AsciiTheme): string {
  function renderXYChartAscii (line 88) | function renderXYChartAscii(
  function renderVertical (line 108) | function renderVertical(
  function renderHorizontal (line 270) | function renderHorizontal(
  function drawStaircaseLine (line 420) | function drawStaircaseLine(
  function drawHorizontalStaircaseLine (line 532) | function drawHorizontalStaircaseLine(
  function drawLegend (line 620) | function drawLegend(
  function createCanvas (line 673) | function createCanvas(width: number, height: number): Canvas {
  function createRoleCanvas (line 677) | function createRoleCanvas(width: number, height: number): RoleCanvas {
  function createHexCanvas (line 681) | function createHexCanvas(width: number, height: number): HexCanvas {
  function set (line 685) | function set(
  function get (line 697) | function get(canvas: Canvas, row: number, col: number): string {
  function writeText (line 704) | function writeText(canvas: Canvas, roles: RoleCanvas, row: number, start...
  function canvasToString (line 714) | function canvasToString(
  function colorizeRow (line 764) | function colorizeRow(
  function getDataCount (line 820) | function getDataCount(chart: XYChart): number {
  function getCategoryLabels (line 828) | function getCategoryLabels(chart: XYChart, count: number): string[] {
  function niceTickValues (line 839) | function niceTickValues(min: number, max: number): number[] {
  function formatTickValue (line 860) | function formatTickValue(v: number): string {

FILE: src/class/layout.ts
  constant CLS (line 18) | const CLS = {
  type ClassSizeMap (line 33) | type ClassSizeMap = Map<string, { width: number; height: number; headerH...
  function buildClassElkGraph (line 36) | function buildClassElkGraph(
  function extractClassLayout (line 98) | function extractClassLayout(
  function layoutClassDiagramSync (line 180) | function layoutClassDiagramSync(
  function maxMemberWidth (line 194) | function maxMemberWidth(members: ClassMember[]): number {
  function memberToString (line 206) | function memberToString(m: ClassMember): string {

FILE: src/class/parser.ts
  function parseClassDiagram (line 27) | function parseClassDiagram(lines: string[]): ClassDiagram {
  function ensureClass (line 166) | function ensureClass(classMap: Map<string, ClassNode>, id: string): Clas...
  function parseMember (line 176) | function parseMember(line: string): { member: ClassMember; isMethod: boo...
  function parseRelationship (line 242) | function parseRelationship(line: string): ClassRelationship | null {
  function parseArrow (line 271) | function parseArrow(arrow: string): { type: RelationshipType; markerAt: ...

FILE: src/class/renderer.ts
  constant CLS_FONT (line 22) | const CLS_FONT = {
  function renderClassSvg (line 35) | function renderClassSvg(
  function relationshipMarkerDefs (line 85) | function relationshipMarkerDefs(): string {
  function renderClassBox (line 114) | function renderClassBox(cls: PositionedClassNode): string {
  function renderMember (line 204) | function renderMember(member: ClassMember, x: number, y: number): string {
  function renderRelationship (line 241) | function renderRelationship(rel: PositionedClassRelationship): string {
  function getRelationshipMarkers (line 287) | function getRelationshipMarkers(type: RelationshipType, markerAt: 'from'...
  function getMarkerDefId (line 299) | function getMarkerDefId(type: RelationshipType): string | null {
  function renderRelationshipLabels (line 317) | function renderRelationshipLabels(rel: PositionedClassRelationship): str...
  function midpoint (line 358) | function midpoint(points: Array<{ x: number; y: number }>): { x: number;...
  function cardinalityOffset (line 365) | function cardinalityOffset(
  function escapeAttr (line 391) | function escapeAttr(value: string): string {

FILE: src/class/types.ts
  type ClassDiagram (line 9) | interface ClassDiagram {
  type ClassNode (line 18) | interface ClassNode {
  type ClassMember (line 29) | interface ClassMember {
  type RelationshipType (line 47) | type RelationshipType =
  type ClassRelationship (line 55) | interface ClassRelationship {
  type ClassNamespace (line 74) | interface ClassNamespace {
  type PositionedClassDiagram (line 83) | interface PositionedClassDiagram {
  type PositionedClassNode (line 90) | interface PositionedClassNode {
  type PositionedClassRelationship (line 108) | interface PositionedClassRelationship {

FILE: src/elk-instance.ts
  type RawFakeWorker (line 20) | interface RawFakeWorker {
  function ensureElk (line 39) | function ensureElk(): void {
  function elkLayoutSync (line 85) | function elkLayoutSync(graph: ElkNode): ElkNode {

FILE: src/er/layout.ts
  type EntitySizeMap (line 29) | type EntitySizeMap = Map<string, { width: number; height: number }>
  function buildErElkGraph (line 32) | function buildErElkGraph(
  function extractErLayout (line 85) | function extractErLayout(
  function layoutErDiagramSync (line 150) | function layoutErDiagramSync(

FILE: src/er/parser.ts
  function parseErDiagram (line 32) | function parseErDiagram(lines: string[]): ErDiagram {
  function ensureEntity (line 86) | function ensureEntity(entityMap: Map<string, ErEntity>, id: string): ErE...
  function parseAttribute (line 96) | function parseAttribute(line: string): ErAttribute | null {
  function parseRelationshipLine (line 137) | function parseRelationshipLine(line: string): ErRelationship | null {
  function parseCardinality (line 167) | function parseCardinality(str: string): Cardinality | null {

FILE: src/er/renderer.ts
  constant ER_FONT (line 22) | const ER_FONT = {
  function renderErSvg (line 35) | function renderErSvg(
  function renderEntityBox (line 81) | function renderEntityBox(entity: PositionedErEntity): string {
  function renderAttribute (line 147) | function renderAttribute(attr: ErAttribute, boxX: number, y: number, box...
  function renderRelationshipLine (line 204) | function renderRelationshipLine(rel: PositionedErRelationship): string {
  function renderRelationshipLabel (line 228) | function renderRelationshipLabel(rel: PositionedErRelationship): string {
  function renderCardinality (line 255) | function renderCardinality(rel: PositionedErRelationship): string {
  function renderCrowsFoot (line 276) | function renderCrowsFoot(
  function midpoint (line 370) | function midpoint(points: Array<{ x: number; y: number }>): { x: number;...
  function escapeAttr (line 414) | function escapeAttr(value: string): string {

FILE: src/er/types.ts
  type ErDiagram (line 9) | interface ErDiagram {
  type ErEntity (line 16) | interface ErEntity {
  type ErAttribute (line 24) | interface ErAttribute {
  type Cardinality (line 42) | type Cardinality = 'one' | 'zero-one' | 'many' | 'zero-many'
  type ErRelationship (line 44) | interface ErRelationship {
  type PositionedErDiagram (line 61) | interface PositionedErDiagram {
  type PositionedErEntity (line 68) | interface PositionedErEntity {
  type PositionedErRelationship (line 82) | interface PositionedErRelationship {

FILE: src/index.ts
  function detectDiagramType (line 54) | function detectDiagramType(text: string): 'flowchart' | 'sequence' | 'cl...
  function buildColors (line 71) | function buildColors(options: RenderOptions): DiagramColors {
  function renderMermaidSVG (line 111) | function renderMermaidSVG(
  function renderMermaidSVGAsync (line 162) | async function renderMermaidSVGAsync(

FILE: src/layout-engine.ts
  constant DEFAULTS (line 42) | const DEFAULTS = {
  function directionToElk (line 52) | function directionToElk(dir: MermaidGraph['direction']): string {
  function estimateNodeSize (line 67) | function estimateNodeSize(id: string, label: string, shape: string): { w...
  type ElkGraphNode (line 115) | interface ElkGraphNode extends ElkNode {
  type HierarchicalEdgeInfo (line 124) | interface HierarchicalEdgeInfo {
  function mermaidToElk (line 138) | function mermaidToElk(
  function subgraphToElk (line 335) | function subgraphToElk(
  function collectSubgraphNodeIds (line 439) | function collectSubgraphNodeIds(sg: MermaidSubgraph, nodeIds: Set<string...
  function buildNodeToSubgraphMap (line 454) | function buildNodeToSubgraphMap(subgraphs: MermaidSubgraph[]): Map<strin...
  type MarginInfo (line 483) | interface MarginInfo {
  function flattenGroupBounds (line 489) | function flattenGroupBounds(groups: PositionedGroup[]): Array<{ x: numbe...
  function elkToPositioned (line 498) | function elkToPositioned(
  function extractNodesAndGroups (line 588) | function extractNodesAndGroups(
  type EdgeSegment (line 653) | interface EdgeSegment {
  function calculatePathMidpoint (line 664) | function calculatePathMidpoint(points: Point[]): Point {
  function extractEdgesRecursively (line 703) | function extractEdgesRecursively(
  function orthogonalizeEdgePoints (line 808) | function orthogonalizeEdgePoints(
  function collectEdgeSegments (line 862) | function collectEdgeSegments(
  function findSubgraph (line 942) | function findSubgraph(subgraphs: MermaidSubgraph[], id: string): Mermaid...
  function collectAllSubgraphIds (line 952) | function collectAllSubgraphIds(sg: MermaidSubgraph, out: Set<string>): v...
  function resolveNodeStyle (line 963) | function resolveNodeStyle(
  function resolveEdgeStyle (line 991) | function resolveEdgeStyle(
  function alignLayerNodes (line 1031) | function alignLayerNodes(
  function findGroupsContainingPoint (line 1165) | function findGroupsContainingPoint(
  function adjustJunctionForGroups (line 1183) | function adjustJunctionForGroups(
  function bundleEdgePaths (line 1228) | function bundleEdgePaths(
  function layoutGraphSync (line 1402) | function layoutGraphSync(
  function convertToElkFormat (line 1415) | function convertToElkFormat(

FILE: src/multiline-utils.ts
  function normalizeBrTags (line 16) | function normalizeBrTags(label: string): string {
  function stripFormattingTags (line 33) | function stripFormattingTags(text: string): string {
  function escapeXml (line 40) | function escapeXml(text: string): string {
  type StyledSegment (line 53) | interface StyledSegment {
  constant FORMAT_TAG_REGEX (line 62) | const FORMAT_TAG_REGEX = /<(\/)?(?:(b|strong)|(i|em)|(u)|(s|del))\s*>/gi
  function parseInlineFormatting (line 68) | function parseInlineFormatting(line: string): StyledSegment[] {
  constant HAS_FORMAT_TAGS (line 101) | const HAS_FORMAT_TAGS = /<\/?(?:b|strong|i|em|u|s|del)\s*>/i
  function renderLineContent (line 107) | function renderLineContent(line: string): string {
  function renderMultilineText (line 154) | function renderMultilineText(
  function renderMultilineTextWithBackground (line 199) | function renderMultilineTextWithBackground(

FILE: src/parser.ts
  function parseMermaid (line 20) | function parseMermaid(text: string): MermaidGraph {
  function parseFlowchart (line 43) | function parseFlowchart(lines: string[]): MermaidGraph {
  function parseStateDiagram (line 182) | function parseStateDiagram(lines: string[]): MermaidGraph {
  function registerStateNode (line 323) | function registerStateNode(
  function ensureStateNode (line 341) | function ensureStateNode(
  function parseStyleProps (line 364) | function parseStyleProps(propsStr: string): Record<string, string> {
  constant ARROW_REGEX (line 399) | const ARROW_REGEX = /^(<)?(-->|-.->|==>|---|-\.-|===)(?:\|([^|]*)\|)?/
  constant TEXT_ARROW_REGEX (line 407) | const TEXT_ARROW_REGEX = /^(<)?(--|-\.|==)\s+(.+?)\s+(-->|---|\.\->|-\.\...
  constant NODE_PATTERNS (line 413) | const NODE_PATTERNS: Array<{ regex: RegExp; shape: NodeShape }> = [
  constant BARE_NODE_REGEX (line 440) | const BARE_NODE_REGEX = /^([\w-]+)/
  constant CLASS_SHORTHAND_REGEX (line 443) | const CLASS_SHORTHAND_REGEX = /^:::([\w][\w-]*)/
  function parseEdgeLine (line 450) | function parseEdgeLine(
  type ConsumedNodeGroup (line 518) | interface ConsumedNodeGroup {
  function consumeNodeGroup (line 527) | function consumeNodeGroup(
  type ConsumedNode (line 550) | interface ConsumedNode {
  function consumeNode (line 561) | function consumeNode(
  function registerNode (line 608) | function registerNode(
  function trackInSubgraph (line 621) | function trackInSubgraph(subgraphStack: MermaidSubgraph[], nodeId: strin...
  function arrowStyleFromOp (line 631) | function arrowStyleFromOp(op: string): EdgeStyle {
  function textArrowStyleFromOps (line 641) | function textArrowStyleFromOps(openOp: string, closeOp: string): EdgeSty...

FILE: src/renderer.ts
  function renderSvg (line 35) | function renderSvg(
  function arrowMarkerDefs (line 98) | function arrowMarkerDefs(): string {
  function arrowMarkerDefsForColor (line 121) | function arrowMarkerDefsForColor(color: string): string {
  function markerSuffix (line 141) | function markerSuffix(color: string): string {
  function renderGroup (line 149) | function renderGroup(group: PositionedGroup, font: string): string {
  function renderEdge (line 197) | function renderEdge(edge: PositionedEdge): string {
  function pointsToPolylinePath (line 238) | function pointsToPolylinePath(points: Point[]): string {
  function renderEdgeLabel (line 242) | function renderEdgeLabel(edge: PositionedEdge, font: string): string {
  function edgeMidpoint (line 276) | function edgeMidpoint(points: Point[]): Point {
  function dist (line 303) | function dist(a: Point, b: Point): number {
  function renderNode (line 319) | function renderNode(node: PositionedNode, font: string): string {
  function renderNodeShape (line 338) | function renderNodeShape(node: PositionedNode): string {
  function renderRect (line 383) | function renderRect(x: number, y: number, w: number, h: number, fill: st...
  function renderRoundedRect (line 390) | function renderRoundedRect(x: number, y: number, w: number, h: number, f...
  function renderStadium (line 397) | function renderStadium(x: number, y: number, w: number, h: number, fill:...
  function renderCircle (line 405) | function renderCircle(x: number, y: number, w: number, h: number, fill: ...
  function renderDiamond (line 415) | function renderDiamond(x: number, y: number, w: number, h: number, fill:...
  function renderSubroutine (line 435) | function renderSubroutine(x: number, y: number, w: number, h: number, fi...
  function renderDoubleCircle (line 448) | function renderDoubleCircle(x: number, y: number, w: number, h: number, ...
  function renderHexagon (line 462) | function renderHexagon(x: number, y: number, w: number, h: number, fill:...
  function renderCylinder (line 479) | function renderCylinder(x: number, y: number, w: number, h: number, fill...
  function renderAsymmetric (line 502) | function renderAsymmetric(x: number, y: number, w: number, h: number, fi...
  function renderTrapezoid (line 516) | function renderTrapezoid(x: number, y: number, w: number, h: number, fil...
  function renderTrapezoidAlt (line 529) | function renderTrapezoidAlt(x: number, y: number, w: number, h: number, ...
  function renderStateStart (line 544) | function renderStateStart(x: number, y: number, w: number, h: number): s...
  function renderStateEnd (line 552) | function renderStateEnd(x: number, y: number, w: number, h: number): str...
  function renderNodeLabel (line 568) | function renderNodeLabel(node: PositionedNode, font: string): string {
  function escapeAttr (line 597) | function escapeAttr(value: string): string {

FILE: src/sequence/layout.ts
  constant SEQ (line 19) | const SEQ = {
  function layoutSequenceDiagram (line 55) | function layoutSequenceDiagram(

FILE: src/sequence/parser.ts
  function parseSequenceDiagram (line 31) | function parseSequenceDiagram(lines: string[]): SequenceDiagram {
  function ensureActor (line 202) | function ensureActor(diagram: SequenceDiagram, actorIds: Set<string>, id...

FILE: src/sequence/renderer.ts
  function renderSequenceSvg (line 28) | function renderSequenceSvg(
  function arrowMarkerDefs (line 83) | function arrowMarkerDefs(): string {
  function renderActor (line 105) | function renderActor(actor: PositionedActor): string {
  function renderLifeline (line 161) | function renderLifeline(lifeline: Lifeline): string {
  function renderActivation (line 173) | function renderActivation(activation: Activation): string {
  function renderMessage (line 185) | function renderMessage(msg: PositionedMessage): string {
  function renderBlock (line 234) | function renderBlock(block: PositionedBlock): string {
  function renderNote (line 294) | function renderNote(note: PositionedNote): string {
  function escapeAttr (line 340) | function escapeAttr(value: string): string {

FILE: src/sequence/types.ts
  type SequenceDiagram (line 9) | interface SequenceDiagram {
  type Actor (line 20) | interface Actor {
  type Message (line 27) | interface Message {
  type Block (line 41) | interface Block {
  type Note (line 54) | interface Note {
  type PositionedSequenceDiagram (line 69) | interface PositionedSequenceDiagram {
  type PositionedActor (line 80) | interface PositionedActor {
  type Lifeline (line 93) | interface Lifeline {
  type PositionedMessage (line 100) | interface PositionedMessage {
  type Activation (line 117) | interface Activation {
  type PositionedBlock (line 125) | interface PositionedBlock {
  type PositionedNote (line 136) | interface PositionedNote {

FILE: src/shape-clipping.ts
  function clipEdgeToShape (line 22) | function clipEdgeToShape(
  function clipToDiamond (line 67) | function clipToDiamond(endpoint: Point, adjacent: Point, node: Positione...
  function intersectHorizontalRayWithEdge (line 143) | function intersectHorizontalRayWithEdge(rayY: number, p1: Point, p2: Poi...
  function intersectVerticalRayWithEdge (line 164) | function intersectVerticalRayWithEdge(rayX: number, p1: Point, p2: Point...

FILE: src/styles.ts
  function estimateTextWidth (line 13) | function estimateTextWidth(text: string, fontSize: number, fontWeight: n...
  function estimateMonoTextWidth (line 20) | function estimateMonoTextWidth(text: string, fontSize: number): number {
  constant MONO_FONT (line 29) | const MONO_FONT = "'JetBrains Mono'" as const
  constant MONO_FONT_STACK (line 32) | const MONO_FONT_STACK = `${MONO_FONT}, 'SF Mono', 'Fira Code', ui-monosp...
  constant FONT_SIZES (line 35) | const FONT_SIZES = {
  constant FONT_WEIGHTS (line 45) | const FONT_WEIGHTS = {
  constant GROUP_HEADER_CONTENT_PAD (line 58) | const GROUP_HEADER_CONTENT_PAD = 12
  constant NODE_PADDING (line 61) | const NODE_PADDING = {
  constant STROKE_WIDTHS (line 71) | const STROKE_WIDTHS = {
  constant TEXT_BASELINE_SHIFT (line 89) | const TEXT_BASELINE_SHIFT = '0.35em' as const
  constant ARROW_HEAD (line 92) | const ARROW_HEAD = {

FILE: src/text-metrics.ts
  constant NARROW_CHARS (line 17) | const NARROW_CHARS = new Set(['i', 'l', 't', 'f', 'j', 'I', '1', '!', '|...
  constant WIDE_CHARS (line 22) | const WIDE_CHARS = new Set(['W', 'M', 'w', 'm', '@', '%'])
  constant VERY_WIDE_CHARS (line 27) | const VERY_WIDE_CHARS = new Set(['W', 'M'])
  constant SEMI_NARROW_PUNCT (line 33) | const SEMI_NARROW_PUNCT = new Set(['(', ')', '[', ']', '{', '}', '/', '\...
  function isCombiningMark (line 38) | function isCombiningMark(code: number): boolean {
  function isFullwidth (line 57) | function isFullwidth(code: number): boolean {
  constant EMOJI_REGEX (line 104) | const EMOJI_REGEX = /\p{Emoji_Presentation}|\p{Extended_Pictographic}/u
  function isEmoji (line 109) | function isEmoji(char: string): boolean {
  function getCharWidth (line 126) | function getCharWidth(char: string): number {
  function measureTextWidth (line 175) | function measureTextWidth(text: string, fontSize: number, fontWeight: nu...
  constant LINE_HEIGHT_RATIO (line 199) | const LINE_HEIGHT_RATIO = 1.3
  type MultilineMetrics (line 202) | interface MultilineMetrics {
  function measureMultilineText (line 224) | function measureMultilineText(

FILE: src/theme.ts
  type DiagramColors (line 26) | interface DiagramColors {
  constant DEFAULTS (line 51) | const DEFAULTS: Readonly<{ bg: string; fg: string }> = {
  constant MIX (line 64) | const MIX = {
  constant THEMES (line 96) | const THEMES: Record<string, DiagramColors> = {
  type ThemeName (line 157) | type ThemeName = keyof typeof THEMES
  type ShikiThemeLike (line 170) | interface ShikiThemeLike {
  function fromShikiTheme (line 202) | function fromShikiTheme(theme: ShikiThemeLike): DiagramColors {
  function buildStyleBlock (line 238) | function buildStyleBlock(font: string, hasMonoFont: boolean): string {
  function svgOpenTag (line 281) | function svgOpenTag(

FILE: src/types.ts
  type MermaidGraph (line 5) | interface MermaidGraph {
  type Direction (line 19) | type Direction = 'TD' | 'TB' | 'LR' | 'BT' | 'RL'
  type MermaidNode (line 21) | interface MermaidNode {
  type NodeShape (line 27) | type NodeShape =
  type MermaidEdge (line 46) | interface MermaidEdge {
  type EdgeStyle (line 57) | type EdgeStyle = 'solid' | 'dotted' | 'thick'
  type MermaidSubgraph (line 59) | interface MermaidSubgraph {
  type PositionedGraph (line 72) | interface PositionedGraph {
  type PositionedNode (line 80) | interface PositionedNode {
  type PositionedEdge (line 92) | interface PositionedEdge {
  type Point (line 107) | interface Point {
  type PositionedGroup (line 112) | interface PositionedGroup {
  type RenderOptions (line 131) | interface RenderOptions {

FILE: src/xychart/colors.ts
  constant CHART_ACCENT_FALLBACK (line 13) | const CHART_ACCENT_FALLBACK = '#3b82f6' // blue-500
  function hexToHsl (line 19) | function hexToHsl(hex: string): [number, number, number] {
  function hslToHex (line 42) | function hslToHex(h: number, s: number, l: number): string {
  function hexToRgb (line 66) | function hexToRgb(hex: string): [number, number, number] {
  function rgbToHex (line 75) | function rgbToHex(r: number, g: number, b: number): string {
  function isValidHex (line 85) | function isValidHex(color: string): boolean {
  function isDarkBackground (line 92) | function isDarkBackground(bgHex: string): boolean {
  function mixHexColors (line 101) | function mixHexColors(bgHex: string, fgHex: string, ratio: number): stri...
  function getSeriesColor (line 118) | function getSeriesColor(index: number, accentColor: string, bgColor?: st...

FILE: src/xychart/layout.ts
  function layoutXYChart (line 47) | function layoutXYChart(
  function layoutVertical (line 59) | function layoutVertical(chart: XYChart): PositionedXYChart {
  function layoutHorizontal (line 154) | function layoutHorizontal(chart: XYChart): PositionedXYChart {
  function getDataCount (line 297) | function getDataCount(chart: XYChart): number {
  function getCategoryLabels (line 306) | function getCategoryLabels(chart: XYChart, count: number): string[] {
  function buildXTicks (line 316) | function buildXTicks(chart: XYChart, xScale: (i: number) => number, axis...
  function layoutBars (line 328) | function layoutBars(
  function layoutLines (line 371) | function layoutLines(chart: XYChart, xScale: (i: number) => number, ySca...
  function niceTickValues (line 386) | function niceTickValues(min: number, max: number): number[] {
  function formatTickValue (line 408) | function formatTickValue(v: number): string {
  function buildLegendItems (line 415) | function buildLegendItems(chart: XYChart, centerX: number, y: number, co...

FILE: src/xychart/parser.ts
  function parseXYChart (line 24) | function parseXYChart(lines: string[]): XYChart {
  function parseNumericArray (line 113) | function parseNumericArray(str: string): number[] {

FILE: src/xychart/renderer.ts
  constant CHART_FONT (line 30) | const CHART_FONT = {
  constant TIP (line 44) | const TIP = {
  function renderXYChartSvg (line 58) | function renderXYChartSvg(
  function chartStyles (line 289) | function chartStyles(chart: PositionedXYChart, interactive: boolean, spa...
  function roundedTopBarPath (line 349) | function roundedTopBarPath(x: number, y: number, w: number, h: number, r...
  function roundedRightBarPath (line 371) | function roundedRightBarPath(x: number, y: number, w: number, h: number,...
  function smoothCurvePath (line 401) | function smoothCurvePath(points: Array<{ x: number; y: number }>): string {
  function multiTooltipAbove (line 471) | function multiTooltipAbove(cx: number, topY: number, label: string, entr...
  function tooltipAbove (line 511) | function tooltipAbove(cx: number, topY: number, text: string): string {
  function formatTipValue (line 532) | function formatTipValue(v: number): string {
  function r (line 537) | function r(n: number): string {
  function escapeXml (line 541) | function escapeXml(text: string): string {

FILE: src/xychart/types.ts
  type XYChart (line 10) | interface XYChart {
  type XYAxis (line 24) | interface XYAxis {
  type XYChartSeries (line 34) | interface XYChartSeries {
  type PositionedXYChart (line 45) | interface PositionedXYChart {
  type LegendItem (line 68) | interface LegendItem {
  type PositionedTitle (line 82) | interface PositionedTitle {
  type PositionedAxis (line 88) | interface PositionedAxis {
  type AxisTick (line 97) | interface AxisTick {
  type PlotArea (line 113) | interface PlotArea {
  type PositionedBar (line 120) | interface PositionedBar {
  type PositionedLine (line 136) | interface PositionedLine {
  type GridLine (line 145) | interface GridLine {

FILE: xychart-samples-data.ts
  type Sample (line 9) | interface Sample {

FILE: xychart-test.ts
  function escapeHtml (line 16) | function escapeHtml(text: string): string {
  function formatDescription (line 24) | function formatDescription(text: string): string {
  constant THEME_LABELS (line 28) | const THEME_LABELS: Record<string, string> = {
  function generateHtml (line 45) | async function generateHtml(): Promise<string> {
Condensed preview — 196 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,636K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 438,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 678,
    "preview": "name: Publish to npm\n\non:\n  release:\n    types: [published]\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n\n    steps:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 246,
    "preview": "# Dependencies\nnode_modules/\n\n# Build output\ndist/\nsite/\n*.tsbuildinfo\n\n# Generated files\nindex.html\n\n# IDE\n.idea/\n.vsco"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2026 Craft Docs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 18405,
    "preview": "<div align=\"center\">\n\n# beautiful-mermaid\n\n**Render Mermaid diagrams as beautiful SVGs or ASCII art**\n\nUltra-fast, fully"
  },
  {
    "path": "bench.ts",
    "chars": 5499,
    "preview": "/**\n * Performance benchmark for beautiful-mermaid.\n *\n * Runs all sample definitions through both renderers (SVG + ASCI"
  },
  {
    "path": "dev.ts",
    "chars": 4499,
    "preview": "/**\n * Development server with live reload for mermaid samples.\n *\n * Usage: bun run packages/mermaid/dev.ts\n *\n * - Run"
  },
  {
    "path": "index.ts",
    "chars": 69359,
    "preview": "/**\n * Generates index.html showcasing all beautiful-mermaid rendering capabilities.\n *\n * Usage: bun run index.ts\n *\n *"
  },
  {
    "path": "package.json",
    "chars": 1647,
    "preview": "{\n  \"name\": \"beautiful-mermaid\",\n  \"version\": \"1.1.3\",\n  \"license\": \"MIT\",\n  \"description\": \"Render Mermaid diagrams as "
  },
  {
    "path": "samples-data.ts",
    "chars": 35619,
    "preview": "/**\n * Sample definitions for the beautiful-mermaid visual test suite.\n *\n * Shared by:\n *   - index.ts     — generates "
  },
  {
    "path": "src/__tests__/ascii-edge-styles.test.ts",
    "chars": 3919,
    "preview": "// ============================================================================\n// ASCII edge style tests — dotted and t"
  },
  {
    "path": "src/__tests__/ascii-multiline.test.ts",
    "chars": 6238,
    "preview": "import { describe, it, expect } from 'bun:test'\nimport { renderMermaidAscii } from '../ascii/index.ts'\n\ndescribe('ASCII "
  },
  {
    "path": "src/__tests__/ascii.test.ts",
    "chars": 8219,
    "preview": "/**\n * Golden-file tests for the ASCII/Unicode renderer.\n *\n * Ported from AlexanderGrooff/mermaid-ascii cmd/graph_test."
  },
  {
    "path": "src/__tests__/class-arrow-directions.test.ts",
    "chars": 14813,
    "preview": "/**\n * Comprehensive tests for class diagram arrow directions.\n *\n * Ensures all relationship types have correctly orien"
  },
  {
    "path": "src/__tests__/class-integration.test.ts",
    "chars": 3306,
    "preview": "/**\n * Integration tests for class diagrams — end-to-end parse → layout → render.\n */\nimport { describe, it, expect } fr"
  },
  {
    "path": "src/__tests__/class-parser.test.ts",
    "chars": 7786,
    "preview": "/**\n * Tests for the class diagram parser.\n *\n * Covers: class blocks, attributes, methods, visibility, annotations,\n * "
  },
  {
    "path": "src/__tests__/edge-approach-direction.test.ts",
    "chars": 10106,
    "preview": "/**\n * Edge Approach Direction Tests\n *\n * Verifies that edges approach target nodes from the correct direction:\n * - Ed"
  },
  {
    "path": "src/__tests__/er-integration.test.ts",
    "chars": 15367,
    "preview": "/**\n * Integration tests for ER diagrams — end-to-end parse → layout → render.\n */\nimport { describe, it, expect } from "
  },
  {
    "path": "src/__tests__/er-parser.test.ts",
    "chars": 5819,
    "preview": "/**\n * Tests for the ER diagram parser.\n *\n * Covers: entity definitions, attribute parsing (types, names, keys, comment"
  },
  {
    "path": "src/__tests__/integration.test.ts",
    "chars": 17290,
    "preview": "/**\n * Integration tests for the full renderMermaidSVG pipeline.\n *\n * These tests exercise parse → layout → render end-"
  },
  {
    "path": "src/__tests__/layout-disconnected.test.ts",
    "chars": 12273,
    "preview": "/**\n * Integration tests for disconnected component layout.\n *\n * These tests verify that the full layout pipeline corre"
  },
  {
    "path": "src/__tests__/linkstyle.test.ts",
    "chars": 4333,
    "preview": "import { describe, it, expect } from 'bun:test'\nimport { parseMermaid } from '../parser.ts'\nimport { renderMermaidSVG } "
  },
  {
    "path": "src/__tests__/multiline-labels.test.ts",
    "chars": 31634,
    "preview": "/**\n * Tests for multi-line label support via <br> tags.\n *\n * Covers:\n * - Parser: normalization of <br>, <br/>, <br />"
  },
  {
    "path": "src/__tests__/parser.test.ts",
    "chars": 30295,
    "preview": "/**\n * Tests for the Mermaid parser.\n *\n * Covers:\n * - Flowcharts: graph headers, node shapes (all 13), edge styles, ch"
  },
  {
    "path": "src/__tests__/renderer.test.ts",
    "chars": 20932,
    "preview": "/**\n * Tests for the SVG renderer.\n *\n * Uses hand-crafted PositionedGraph data to test SVG output without\n * depending "
  },
  {
    "path": "src/__tests__/sequence-integration.test.ts",
    "chars": 3546,
    "preview": "/**\n * Integration tests for sequence diagrams — end-to-end parse → layout → render.\n */\nimport { describe, it, expect }"
  },
  {
    "path": "src/__tests__/sequence-layout.test.ts",
    "chars": 24652,
    "preview": "/**\n * Layout tests for sequence diagrams — verify that block headers and dividers\n * get extra vertical space so they d"
  },
  {
    "path": "src/__tests__/sequence-parser.test.ts",
    "chars": 7330,
    "preview": "/**\n * Tests for the sequence diagram parser.\n *\n * Covers: participants, actors, messages (solid/dashed, filled/open ar"
  },
  {
    "path": "src/__tests__/styles.test.ts",
    "chars": 6167,
    "preview": "/**\n * Tests for styles module — text measurement and constants.\n * Theme resolution tests are in theme.test.ts (CSS cus"
  },
  {
    "path": "src/__tests__/testdata/ascii/ampersand_lhs.txt",
    "chars": 265,
    "preview": "graph LR\nA --> B & C\n---\n+---+     +---+\n|   |     |   |\n| A |---->| B |\n|   |     |   |\n+---+     +---+\n  |            "
  },
  {
    "path": "src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt",
    "chars": 269,
    "preview": "graph LR\nA & B --> C & D\n---\n+---+     +---+\n|   |     |   |\n| A |--+->| C |\n|   |  |  |   |\n+---+  |  +---+\n  |    |   "
  },
  {
    "path": "src/__tests__/testdata/ascii/ampersand_rhs.txt",
    "chars": 265,
    "preview": "graph LR\nA & B --> C\n---\n+---+     +---+\n|   |     |   |\n| A |---->| C |\n|   |     |   |\n+---+     +---+\n            ^  "
  },
  {
    "path": "src/__tests__/testdata/ascii/ampersand_td_fanin.txt",
    "chars": 185,
    "preview": "graph TD\nA & B --> C\n---\n+---+     +---+\n|   |     |   |\n| A |     | B |\n|   |     |   |\n+---+     +---+\n  |         |\n "
  },
  {
    "path": "src/__tests__/testdata/ascii/ampersand_td_fanout.txt",
    "chars": 185,
    "preview": "graph TD\nA --> B & C\n---\n+---+\n|   |\n| A |\n|   |\n+---+\n  |\n  |\n  +---------+\n  |         |\n  v         v\n+---+     +---+"
  },
  {
    "path": "src/__tests__/testdata/ascii/ampersand_without_edge.txt",
    "chars": 109,
    "preview": "graph LR\nA & B\n---\n+---+\n|   |\n| A |\n|   |\n+---+\n     \n     \n     \n     \n     \n+---+\n|   |\n| B |\n|   |\n+---+\n"
  },
  {
    "path": "src/__tests__/testdata/ascii/back_reference_from_child.txt",
    "chars": 215,
    "preview": "graph LR\nA --> B --> C --> A\n---\n+---+     +---+     +---+\n|   |     |   |     |   |\n| A |---->| B |---->| C |\n|   |    "
  },
  {
    "path": "src/__tests__/testdata/ascii/backlink_from_bottom.txt",
    "chars": 443,
    "preview": "graph LR\nA --> B\nB --> C\nA --> C\nB --> D\nC --> D\n---\n+---+     +---+     +---+\n|   |     |   |     |   |\n| A |---->| B |"
  },
  {
    "path": "src/__tests__/testdata/ascii/backlink_from_top.txt",
    "chars": 443,
    "preview": "graph LR\nA --> B\nB --> C\nA --> C\nB --> D\nD --> C\n---\n+---+     +---+     +---+\n|   |     |   |     |   |\n| A |---->| B |"
  },
  {
    "path": "src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt",
    "chars": 405,
    "preview": "paddingX=5\npaddingY=3\ngraph LR\nA --> B & C\nB --> C & D\nD --> C\n---\n+---+     +---+     +---+\n|   |     |   |     |   |\n|"
  },
  {
    "path": "src/__tests__/testdata/ascii/cls_all_relationships.txt",
    "chars": 590,
    "preview": "classDiagram\n  A <|-- B : inherits\n  C *-- D : owns\n  E o-- F : has\n  G --> H : uses\n  I ..> J : depends\n  K ..|> L : im"
  },
  {
    "path": "src/__tests__/testdata/ascii/cls_annotation.txt",
    "chars": 565,
    "preview": "classDiagram\n  class Shape {\n    <<abstract>>\n    +draw() void\n  }\n  class Circle {\n    +radius int\n    +draw() void\n  }"
  },
  {
    "path": "src/__tests__/testdata/ascii/cls_association.txt",
    "chars": 224,
    "preview": "classDiagram\n  Student --> Course : enrolls\n---\n+---------+    \n| Student |    \n+---------+    \n     |         \n  enroll"
  },
  {
    "path": "src/__tests__/testdata/ascii/cls_basic.txt",
    "chars": 269,
    "preview": "classDiagram\n  class Animal {\n    +String name\n    +eat() void\n  }\n---\n+---------------+    \n| Animal        |    \n+----"
  },
  {
    "path": "src/__tests__/testdata/ascii/cls_dependency.txt",
    "chars": 209,
    "preview": "classDiagram\n  Client ..> Server : uses\n---\n+--------+    \n| Client |    \n+--------+    \n     :        \n   uses       \n "
  },
  {
    "path": "src/__tests__/testdata/ascii/cls_inheritance.txt",
    "chars": 454,
    "preview": "classDiagram\n  Animal <|-- Dog\n  Animal : +String name\n  Dog : +bark() void\n---\n+---------------+    \n| Animal        | "
  },
  {
    "path": "src/__tests__/testdata/ascii/cls_methods.txt",
    "chars": 448,
    "preview": "classDiagram\n  class BankAccount {\n    +String owner\n    -int balance\n    +deposit(int amount) void\n    +withdraw(int am"
  },
  {
    "path": "src/__tests__/testdata/ascii/comments.txt",
    "chars": 334,
    "preview": "graph LR\n%% This is a comment\nA --> B\n%% Another comment\nB --> C\nA --> C\n%% Final comment\n---\n+---+     +---+\n|   |     "
  },
  {
    "path": "src/__tests__/testdata/ascii/custom_padding.txt",
    "chars": 108,
    "preview": "paddingX=2\npaddingY=1\ngraph LR\nA --> B\n---\n+---+  +---+\n|   |  |   |\n| A |->| B |\n|   |  |   |\n+---+  +---+\n"
  },
  {
    "path": "src/__tests__/testdata/ascii/duplicate_labels.txt",
    "chars": 451,
    "preview": "graph TD\nA[Server] --> B[Client]\nC[Server] --> D[Client]\n---\n+--------+     +--------+\n|        |     |        |\n| Serve"
  },
  {
    "path": "src/__tests__/testdata/ascii/er_attributes.txt",
    "chars": 457,
    "preview": "erDiagram\n  CUSTOMER {\n    string name PK\n    string email UK\n    int age\n  }\n  ORDER {\n    int id PK\n    string status\n"
  },
  {
    "path": "src/__tests__/testdata/ascii/er_basic.txt",
    "chars": 133,
    "preview": "erDiagram\n  CUSTOMER ||--o{ ORDER : places\n---\n+----------+      +-------+\n| CUSTOMER ||---o<| ORDER |\n+----------+place"
  },
  {
    "path": "src/__tests__/testdata/ascii/er_identifying.txt",
    "chars": 403,
    "preview": "erDiagram\n  PERSON ||--o{ ADDRESS : lives_at\n  PERSON {\n    string name PK\n  }\n  ADDRESS {\n    string street\n    string "
  },
  {
    "path": "src/__tests__/testdata/ascii/flowchart_tb_simple.txt",
    "chars": 191,
    "preview": "flowchart TB\n    A --> B\n    B --> C\n---\n+---+\n|   |\n| A |\n|   |\n+---+\n  |  \n  |  \n  |  \n  |  \n  v  \n+---+\n|   |\n| B |\n|"
  },
  {
    "path": "src/__tests__/testdata/ascii/graph_bt_direction.txt",
    "chars": 179,
    "preview": "graph BT\n  A --> B --> C\n---\n+---+\n|   |\n| C |\n|   |\n+---+\n  ^  \n  |  \n  |  \n  |  \n  |  \n+---+\n|   |\n| B |\n|   |\n+---+\n "
  },
  {
    "path": "src/__tests__/testdata/ascii/graph_tb_direction.txt",
    "chars": 252,
    "preview": "graph TB\nsubgraph one\n    A --> B\nend\n---\n+-------+\n|  one  |\n|       |\n|       |\n| +---+ |\n| |   | |\n| | A | |\n| |   | "
  },
  {
    "path": "src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt",
    "chars": 515,
    "preview": "graph TD\nA[Web Server] --> B[API Gateway]\nB --> C[Web Server]\nsubgraph Frontend\nA\nend\nsubgraph Backend\nB\nC\nend\n---\n+----"
  },
  {
    "path": "src/__tests__/testdata/ascii/preserve_order_of_definition.txt",
    "chars": 289,
    "preview": "graph LR\nA\nB\nB --> A\nA --> A\nB --> C\nC --> A\n---\n+---+     +---+\n|   |     |   |\n| A |<-+--| C |\n|   |  |  |   |\n+---+  "
  },
  {
    "path": "src/__tests__/testdata/ascii/self_reference.txt",
    "chars": 77,
    "preview": "graph LR\nA --> A\n---\n+---+  \n|   |  \n| A |-+\n|   | |\n+---+ |\n  ^   |\n  +---+\n"
  },
  {
    "path": "src/__tests__/testdata/ascii/self_reference_with_edge.txt",
    "chars": 137,
    "preview": "graph LR\nA --> A & B\n---\n+---+     +---+\n|   |     |   |\n| A |--+->| B |\n|   |  |  |   |\n+---+  |  +---+\n  ^    |       "
  },
  {
    "path": "src/__tests__/testdata/ascii/seq_basic.txt",
    "chars": 434,
    "preview": "sequenceDiagram\n  Alice->>Bob: Hello Bob\n  Bob-->>Alice: Hi Alice\n---\n +-------+       +-----+   \n | Alice |       | Bob"
  },
  {
    "path": "src/__tests__/testdata/ascii/seq_multiple_messages.txt",
    "chars": 852,
    "preview": "sequenceDiagram\n  Alice->>Bob: Hello\n  Bob->>Charlie: Forward\n  Charlie-->>Bob: Reply\n  Bob-->>Alice: Done\n---\n +-------"
  },
  {
    "path": "src/__tests__/testdata/ascii/seq_self_message.txt",
    "chars": 415,
    "preview": "sequenceDiagram\n  Alice->>Alice: Think\n  Alice->>Bob: Result\n---\n +-------+    +-----+   \n | Alice |    | Bob |   \n +---"
  },
  {
    "path": "src/__tests__/testdata/ascii/single_node.txt",
    "chars": 45,
    "preview": "graph LR\nA\n---\n+---+\n|   |\n| A |\n|   |\n+---+\n"
  },
  {
    "path": "src/__tests__/testdata/ascii/single_node_longer_name.txt",
    "chars": 99,
    "preview": "graph LR\nLongerName\n---\n+------------+\n|            |\n| LongerName |\n|            |\n+------------+\n"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_complex_mixed.txt",
    "chars": 1454,
    "preview": "graph LR\nStart\nsubgraph Processing\n    A --> B\n    B --> C\nend\nsubgraph Storage\n    D\n    E\nend\nStart --> A\nC --> D\nC --"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_complex_nested.txt",
    "chars": 629,
    "preview": "graph LR\nsubgraph outer\n    A\n    subgraph inner\n        B\n    end\n    C\nend\nD\n---\n+-----------+\n|   outer   |\n|        "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_direction_override.txt",
    "chars": 488,
    "preview": "graph TD\nsubgraph one [LR Group]\n    direction LR\n    A --> B\nend\nX --> A\nB --> Y\n---\n  +---+\n  |   |\n  | X |\n  |   |\n  "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_empty.txt",
    "chars": 118,
    "preview": "graph LR\nsubgraph one\nend\nA --> B\n---\n+---+     +---+\n|   |     |   |\n| A |---->| B |\n|   |     |   |\n+---+     +---+\n"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt",
    "chars": 458,
    "preview": "graph LR\nX\nsubgraph one\n    A --> B\nend\nY\nX --> A\nB --> Y\n---\n        +-----------------+        \n        |       one   "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt",
    "chars": 452,
    "preview": "graph TD\nX\nsubgraph one\n    A --> B\nend\nY\nX --> A\nB --> Y\n---\n  +---+  \n  |   |  \n  | X |  \n  |   |  \n  +---+  \n    |   "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_multiple_edges.txt",
    "chars": 729,
    "preview": "graph LR\nsubgraph one\n    A --> B\n    A --> C\nend\nsubgraph two\n    D --> E\nend\nB --> D\nC --> E\n---\n+-----------------+ +"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt",
    "chars": 262,
    "preview": "graph LR\nsubgraph one\n    A --> B\nend\n---\n+-----------------+\n|       one       |\n|                 |\n|                 "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_nested.txt",
    "chars": 307,
    "preview": "graph LR\nsubgraph outer\n    subgraph inner\n        A\n    end\nend\n---\n+-----------+\n|   outer   |\n|           |\n|        "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_nested_with_external.txt",
    "chars": 895,
    "preview": "graph LR\nX\nsubgraph outer\n    A\n    subgraph inner\n        B --> C\n    end\n    A --> B\nend\nX --> A\nC --> Y\nY\n---\n       "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt",
    "chars": 352,
    "preview": "graph LR\nX\nsubgraph one\n    A --> B\nend\n---\n        +-----------------+\n        |       one       |\n        |           "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_single_node.txt",
    "chars": 146,
    "preview": "graph LR\nsubgraph one\n    A\nend\n---\n+-------+\n|  one  |\n|       |\n|       |\n| +---+ |\n| |   | |\n| | A | |\n| |   | |\n| +-"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_td_direction.txt",
    "chars": 252,
    "preview": "graph TD\nsubgraph one\n    A --> B\nend\n---\n+-------+\n|  one  |\n|       |\n|       |\n| +---+ |\n| |   | |\n| | A | |\n| |   | "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_td_multiple.txt",
    "chars": 1195,
    "preview": "graph TD\nsubgraph Frontend\n    UI --> API\nend\nsubgraph Backend\n    API --> Database\n    API --> Cache\nend\n---\n+---------"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt",
    "chars": 1031,
    "preview": "paddingX=3\npaddingY=3\ngraph TD\nsubgraph Frontend\n    UI --> API\nend\nsubgraph Backend\n    API --> Database\n    API --> Ca"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt",
    "chars": 525,
    "preview": "graph LR\nsubgraph level1\n    subgraph level2\n        subgraph level3\n            A\n        end\n    end\nend\n---\n+--------"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_three_separate.txt",
    "chars": 496,
    "preview": "graph LR\nsubgraph Frontend\n    UI\nend\nsubgraph Backend\n    API\nend\nsubgraph Database\n    DB\nend\nUI --> API\nAPI --> DB\n--"
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_two_separate.txt",
    "chars": 287,
    "preview": "graph LR\nsubgraph one\n    A\nend\nsubgraph two\n    B\nend\nA --> B\n---\n+-------+ +-------+\n|  one  | |  two  |\n|       | |  "
  },
  {
    "path": "src/__tests__/testdata/ascii/subgraph_with_labels.txt",
    "chars": 630,
    "preview": "graph LR\nsubgraph one\n    A -->|sends| B\nend\nsubgraph two\n    C -->|receives| D\nend\nB -->|data| C\n---\n+-----------------"
  },
  {
    "path": "src/__tests__/testdata/ascii/three_nodes.txt",
    "chars": 159,
    "preview": "graph LR\nA --> B\nB --> C\n---\n+---+     +---+     +---+\n|   |     |   |     |   |\n| A |---->| B |---->| C |\n|   |     |  "
  },
  {
    "path": "src/__tests__/testdata/ascii/three_nodes_single_line.txt",
    "chars": 157,
    "preview": "graph LR\nA --> B --> C\n---\n+---+     +---+     +---+\n|   |     |   |     |   |\n| A |---->| B |---->| C |\n|   |     |   |"
  },
  {
    "path": "src/__tests__/testdata/ascii/two_layer_single_graph.txt",
    "chars": 269,
    "preview": "graph LR\nA --> B\nA --> C\n---\n+---+     +---+\n|   |     |   |\n| A |---->| B |\n|   |     |   |\n+---+     +---+\n  |        "
  },
  {
    "path": "src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt",
    "chars": 404,
    "preview": "graph LR\nABC --> BCDEFG\nABC --> CDEFGHI\n---\n+-----+     +---------+\n|     |     |         |\n| ABC |---->|  BCDEFG |\n|   "
  },
  {
    "path": "src/__tests__/testdata/ascii/two_nodes_linked.txt",
    "chars": 101,
    "preview": "graph LR\nA --> B\n---\n+---+     +---+\n|   |     |   |\n| A |---->| B |\n|   |     |   |\n+---+     +---+\n"
  },
  {
    "path": "src/__tests__/testdata/ascii/two_nodes_longer_names.txt",
    "chars": 221,
    "preview": "graph LR\nLongerName1 --> LongerName2\n---\n+-------------+     +-------------+\n|             |     |             |\n| Longe"
  },
  {
    "path": "src/__tests__/testdata/ascii/two_root_nodes.txt",
    "chars": 269,
    "preview": "graph LR\nA --> B\nC --> D\n---\n+---+     +---+\n|   |     |   |\n| A |---->| B |\n|   |     |   |\n+---+     +---+\n           "
  },
  {
    "path": "src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt",
    "chars": 433,
    "preview": "graph LR\nABC --> BCDEFG\nCDEFGH --> DEF\n---\n+--------+     +--------+\n|        |     |        |\n|  ABC   |---->| BCDEFG |"
  },
  {
    "path": "src/__tests__/testdata/ascii/two_single_root_nodes.txt",
    "chars": 107,
    "preview": "graph LR\nA\nB\n---\n+---+\n|   |\n| A |\n|   |\n+---+\n     \n     \n     \n     \n     \n+---+\n|   |\n| B |\n|   |\n+---+\n"
  },
  {
    "path": "src/__tests__/testdata/unicode/ampersand_lhs.txt",
    "chars": 265,
    "preview": "graph LR\nA --> B & C\n---\n┌───┐     ┌───┐\n│   │     │   │\n│ A ├────►│ B │\n│   │     │   │\n└─┬─┘     └───┘\n  │            "
  },
  {
    "path": "src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt",
    "chars": 269,
    "preview": "graph LR\nA & B --> C & D\n---\n┌───┐     ┌───┐\n│   │     │   │\n│ A ├──┬─►│ C │\n│   │  │  │   │\n└─┬─┘  │  └───┘\n  │    │   "
  },
  {
    "path": "src/__tests__/testdata/unicode/ampersand_rhs.txt",
    "chars": 265,
    "preview": "graph LR\nA & B --> C\n---\n┌───┐     ┌───┐\n│   │     │   │\n│ A ├────►│ C │\n│   │     │   │\n└───┘     └───┘\n            ▲  "
  },
  {
    "path": "src/__tests__/testdata/unicode/ampersand_without_edge.txt",
    "chars": 109,
    "preview": "graph LR\nA & B\n---\n┌───┐\n│   │\n│ A │\n│   │\n└───┘\n     \n     \n     \n     \n     \n┌───┐\n│   │\n│ B │\n│   │\n└───┘\n"
  },
  {
    "path": "src/__tests__/testdata/unicode/back_reference_from_child.txt",
    "chars": 215,
    "preview": "graph LR\nA --> B --> C --> A\n---\n┌───┐     ┌───┐     ┌───┐\n│   │     │   │     │   │\n│ A ├────►│ B ├────►│ C │\n│   │    "
  },
  {
    "path": "src/__tests__/testdata/unicode/backlink_from_bottom.txt",
    "chars": 443,
    "preview": "graph LR\nA --> B\nB --> C\nA --> C\nB --> D\nC --> D\n---\n┌───┐     ┌───┐     ┌───┐\n│   │     │   │     │   │\n│ A ├────►│ B ├"
  },
  {
    "path": "src/__tests__/testdata/unicode/backlink_from_top.txt",
    "chars": 443,
    "preview": "graph LR\nA --> B\nB --> C\nA --> C\nB --> D\nD --> C\n---\n┌───┐     ┌───┐     ┌───┐\n│   │     │   │     │   │\n│ A ├────►│ B ├"
  },
  {
    "path": "src/__tests__/testdata/unicode/cls_all_relationships.txt",
    "chars": 590,
    "preview": "classDiagram\n  A <|-- B : inherits\n  C *-- D : owns\n  E o-- F : has\n  G --> H : uses\n  I ..> J : depends\n  K ..|> L : im"
  },
  {
    "path": "src/__tests__/testdata/unicode/cls_annotation.txt",
    "chars": 565,
    "preview": "classDiagram\n  class Shape {\n    <<abstract>>\n    +draw() void\n  }\n  class Circle {\n    +radius int\n    +draw() void\n  }"
  },
  {
    "path": "src/__tests__/testdata/unicode/cls_association.txt",
    "chars": 224,
    "preview": "classDiagram\n  Student --> Course : enrolls\n---\n┌─────────┐    \n│ Student │    \n└─────────┘    \n     │         \n  enroll"
  },
  {
    "path": "src/__tests__/testdata/unicode/cls_basic.txt",
    "chars": 269,
    "preview": "classDiagram\n  class Animal {\n    +String name\n    +eat() void\n  }\n---\n┌───────────────┐    \n│ Animal        │    \n├────"
  },
  {
    "path": "src/__tests__/testdata/unicode/cls_dependency.txt",
    "chars": 209,
    "preview": "classDiagram\n  Client ..> Server : uses\n---\n┌────────┐    \n│ Client │    \n└────────┘    \n     ┊        \n   uses       \n "
  },
  {
    "path": "src/__tests__/testdata/unicode/cls_inheritance.txt",
    "chars": 454,
    "preview": "classDiagram\n  Animal <|-- Dog\n  Animal : +String name\n  Dog : +bark() void\n---\n┌───────────────┐    \n│ Animal        │ "
  },
  {
    "path": "src/__tests__/testdata/unicode/cls_methods.txt",
    "chars": 448,
    "preview": "classDiagram\n  class BankAccount {\n    +String owner\n    -int balance\n    +deposit(int amount) void\n    +withdraw(int am"
  },
  {
    "path": "src/__tests__/testdata/unicode/comments.txt",
    "chars": 334,
    "preview": "graph LR\n%% This is a comment\nA --> B\n%% Another comment\nB --> C\nA --> C\n%% Final comment\n---\n┌───┐     ┌───┐\n│   │     "
  },
  {
    "path": "src/__tests__/testdata/unicode/duplicate_labels.txt",
    "chars": 451,
    "preview": "graph TD\nA[Server] --> B[Client]\nC[Server] --> D[Client]\n---\n┌────────┐     ┌────────┐\n│        │     │        │\n│ Serve"
  },
  {
    "path": "src/__tests__/testdata/unicode/er_attributes.txt",
    "chars": 457,
    "preview": "erDiagram\n  CUSTOMER {\n    string name PK\n    string email UK\n    int age\n  }\n  ORDER {\n    int id PK\n    string status\n"
  },
  {
    "path": "src/__tests__/testdata/unicode/er_basic.txt",
    "chars": 133,
    "preview": "erDiagram\n  CUSTOMER ||--o{ ORDER : places\n---\n┌──────────┐      ┌───────┐\n│ CUSTOMER ││───○╟│ ORDER │\n└──────────┘place"
  },
  {
    "path": "src/__tests__/testdata/unicode/er_identifying.txt",
    "chars": 403,
    "preview": "erDiagram\n  PERSON ||--o{ ADDRESS : lives_at\n  PERSON {\n    string name PK\n  }\n  ADDRESS {\n    string street\n    string "
  },
  {
    "path": "src/__tests__/testdata/unicode/graph_bt_direction.txt",
    "chars": 179,
    "preview": "graph BT\n  A --> B --> C\n---\n┌───┐\n│   │\n│ C │\n│   │\n└───┘\n  ▲  \n  │  \n  │  \n  │  \n  │  \n┌─┴─┐\n│   │\n│ B │\n│   │\n└───┘\n "
  },
  {
    "path": "src/__tests__/testdata/unicode/preserve_order_of_definition.txt",
    "chars": 289,
    "preview": "graph LR\nA\nB\nB --> A\nA --> A\nB --> C\nC --> A\n---\n┌───┐     ┌───┐\n│   │     │   │\n│ A ├◄─┬──┤ C │\n│   │  │  │   │\n└───┘  "
  },
  {
    "path": "src/__tests__/testdata/unicode/self_reference.txt",
    "chars": 77,
    "preview": "graph LR\nA --> A\n---\n┌───┐  \n│   │  \n│ A ├─┐\n│   │ │\n└───┘ │\n  ▲   │\n  └───┘\n"
  },
  {
    "path": "src/__tests__/testdata/unicode/self_reference_with_edge.txt",
    "chars": 137,
    "preview": "graph LR\nA --> A & B\n---\n┌───┐     ┌───┐\n│   │     │   │\n│ A ├──┬─►│ B │\n│   │  │  │   │\n└───┘  │  └───┘\n  ▲    │       "
  },
  {
    "path": "src/__tests__/testdata/unicode/seq_basic.txt",
    "chars": 434,
    "preview": "sequenceDiagram\n  Alice->>Bob: Hello Bob\n  Bob-->>Alice: Hi Alice\n---\n ┌───────┐       ┌─────┐   \n │ Alice │       │ Bob"
  },
  {
    "path": "src/__tests__/testdata/unicode/seq_multiple_messages.txt",
    "chars": 852,
    "preview": "sequenceDiagram\n  Alice->>Bob: Hello\n  Bob->>Charlie: Forward\n  Charlie-->>Bob: Reply\n  Bob-->>Alice: Done\n---\n ┌───────"
  },
  {
    "path": "src/__tests__/testdata/unicode/seq_self_message.txt",
    "chars": 415,
    "preview": "sequenceDiagram\n  Alice->>Alice: Think\n  Alice->>Bob: Result\n---\n ┌───────┐    ┌─────┐   \n │ Alice │    │ Bob │   \n └───"
  },
  {
    "path": "src/__tests__/testdata/unicode/single_node.txt",
    "chars": 45,
    "preview": "graph LR\nA\n---\n┌───┐\n│   │\n│ A │\n│   │\n└───┘\n"
  },
  {
    "path": "src/__tests__/testdata/unicode/single_node_longer_name.txt",
    "chars": 99,
    "preview": "graph LR\nLongerName\n---\n┌────────────┐\n│            │\n│ LongerName │\n│            │\n└────────────┘\n"
  },
  {
    "path": "src/__tests__/testdata/unicode/three_nodes.txt",
    "chars": 159,
    "preview": "graph LR\nA --> B\nB --> C\n---\n┌───┐     ┌───┐     ┌───┐\n│   │     │   │     │   │\n│ A ├────►│ B ├────►│ C │\n│   │     │  "
  },
  {
    "path": "src/__tests__/testdata/unicode/three_nodes_single_line.txt",
    "chars": 157,
    "preview": "graph LR\nA --> B --> C\n---\n┌───┐     ┌───┐     ┌───┐\n│   │     │   │     │   │\n│ A ├────►│ B ├────►│ C │\n│   │     │   │"
  },
  {
    "path": "src/__tests__/testdata/unicode/two_layer_single_graph.txt",
    "chars": 269,
    "preview": "graph LR\nA --> B\nA --> C\n---\n┌───┐     ┌───┐\n│   │     │   │\n│ A ├────►│ B │\n│   │     │   │\n└─┬─┘     └───┘\n  │        "
  },
  {
    "path": "src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt",
    "chars": 404,
    "preview": "graph LR\nABC --> BCDEFG\nABC --> CDEFGHI\n---\n┌─────┐     ┌─────────┐\n│     │     │         │\n│ ABC ├────►│  BCDEFG │\n│   "
  },
  {
    "path": "src/__tests__/testdata/unicode/two_nodes_linked.txt",
    "chars": 101,
    "preview": "graph LR\nA --> B\n---\n┌───┐     ┌───┐\n│   │     │   │\n│ A ├────►│ B │\n│   │     │   │\n└───┘     └───┘\n"
  },
  {
    "path": "src/__tests__/testdata/unicode/two_nodes_longer_names.txt",
    "chars": 221,
    "preview": "graph LR\nLongerName1 --> LongerName2\n---\n┌─────────────┐     ┌─────────────┐\n│             │     │             │\n│ Longe"
  },
  {
    "path": "src/__tests__/testdata/unicode/two_root_nodes.txt",
    "chars": 269,
    "preview": "graph LR\nA --> B\nC --> D\n---\n┌───┐     ┌───┐\n│   │     │   │\n│ A ├────►│ B │\n│   │     │   │\n└───┘     └───┘\n           "
  },
  {
    "path": "src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt",
    "chars": 433,
    "preview": "graph LR\nABC --> BCDEFG\nCDEFGH --> DEF\n---\n┌────────┐     ┌────────┐\n│        │     │        │\n│  ABC   ├────►│ BCDEFG │"
  },
  {
    "path": "src/__tests__/testdata/unicode/two_single_root_nodes.txt",
    "chars": 107,
    "preview": "graph LR\nA\nB\n---\n┌───┐\n│   │\n│ A │\n│   │\n└───┘\n     \n     \n     \n     \n     \n┌───┐\n│   │\n│ B │\n│   │\n└───┘\n"
  },
  {
    "path": "src/__tests__/text-metrics.test.ts",
    "chars": 9348,
    "preview": "/**\n * Tests for text-metrics module — variable-width character measurement.\n */\nimport { describe, it, expect } from 'b"
  },
  {
    "path": "src/__tests__/xychart-ascii.test.ts",
    "chars": 9473,
    "preview": "/**\n * Tests for xychart-beta ASCII rendering.\n *\n * Tests bar charts, line charts, mixed charts, horizontal orientation"
  },
  {
    "path": "src/__tests__/xychart-integration.test.ts",
    "chars": 5219,
    "preview": "/**\n * Integration tests for xychart-beta rendering.\n *\n * Tests data-* attributes (always emitted) and interactive tool"
  },
  {
    "path": "src/ascii/ansi.ts",
    "chars": 13393,
    "preview": "// ============================================================================\n// ASCII renderer — color utilities\n//\n/"
  },
  {
    "path": "src/ascii/canvas.ts",
    "chars": 14241,
    "preview": "// ============================================================================\n// ASCII renderer — 2D text canvas\n//\n//"
  },
  {
    "path": "src/ascii/class-diagram.ts",
    "chars": 25814,
    "preview": "// ============================================================================\n// ASCII renderer — class diagrams\n//\n//"
  },
  {
    "path": "src/ascii/converter.ts",
    "chars": 9285,
    "preview": "// ============================================================================\n// ASCII renderer — MermaidGraph → Ascii"
  },
  {
    "path": "src/ascii/draw.ts",
    "chars": 50402,
    "preview": "// ============================================================================\n// ASCII renderer — drawing operations\n/"
  },
  {
    "path": "src/ascii/edge-bundling.ts",
    "chars": 11974,
    "preview": "// ============================================================================\n// ASCII renderer — edge bundling for pa"
  },
  {
    "path": "src/ascii/edge-routing.ts",
    "chars": 11649,
    "preview": "// ============================================================================\n// ASCII renderer — direction system and"
  },
  {
    "path": "src/ascii/er-diagram.ts",
    "chars": 15592,
    "preview": "// ============================================================================\n// ASCII renderer — ER diagrams\n//\n// Re"
  },
  {
    "path": "src/ascii/grid.ts",
    "chars": 20383,
    "preview": "// ============================================================================\n// ASCII renderer — grid-based layout\n//"
  },
  {
    "path": "src/ascii/index.ts",
    "chars": 6086,
    "preview": "// ============================================================================\n// beautiful-mermaid — ASCII renderer pu"
  },
  {
    "path": "src/ascii/multiline-utils.ts",
    "chars": 2315,
    "preview": "// ============================================================================\n// ASCII renderer — multi-line text util"
  },
  {
    "path": "src/ascii/pathfinder.ts",
    "chars": 6866,
    "preview": "// ============================================================================\n// ASCII renderer — A* pathfinding for e"
  },
  {
    "path": "src/ascii/sequence.ts",
    "chars": 16517,
    "preview": "// ============================================================================\n// ASCII renderer — sequence diagrams\n//"
  },
  {
    "path": "src/ascii/shapes/circle.ts",
    "chars": 884,
    "preview": "// ============================================================================\n// Circle shape renderer — uses corner d"
  },
  {
    "path": "src/ascii/shapes/corners.ts",
    "chars": 3729,
    "preview": "// ============================================================================\n// Corner character lookup table for sha"
  },
  {
    "path": "src/ascii/shapes/diamond.ts",
    "chars": 891,
    "preview": "// ============================================================================\n// Diamond shape renderer — uses corner "
  },
  {
    "path": "src/ascii/shapes/hexagon.ts",
    "chars": 890,
    "preview": "// ============================================================================\n// Hexagon shape renderer — uses corner "
  },
  {
    "path": "src/ascii/shapes/index.ts",
    "chars": 3079,
    "preview": "// ============================================================================\n// Shape registry — pluggable ASCII shap"
  },
  {
    "path": "src/ascii/shapes/rectangle.ts",
    "chars": 6186,
    "preview": "// ============================================================================\n// Rectangle shape renderer — standard b"
  },
  {
    "path": "src/ascii/shapes/rounded.ts",
    "chars": 884,
    "preview": "// ============================================================================\n// Rounded rectangle shape renderer — us"
  },
  {
    "path": "src/ascii/shapes/special.ts",
    "chars": 9399,
    "preview": "// ============================================================================\n// Special shape renderers — subroutine,"
  },
  {
    "path": "src/ascii/shapes/stadium.ts",
    "chars": 3501,
    "preview": "// ============================================================================\n// Stadium (pill) shape renderer — speci"
  },
  {
    "path": "src/ascii/shapes/state.ts",
    "chars": 5482,
    "preview": "// ============================================================================\n// State pseudo-state renderers — UML st"
  },
  {
    "path": "src/ascii/shapes/types.ts",
    "chars": 2068,
    "preview": "// ============================================================================\n// Shape renderer types — interface for "
  },
  {
    "path": "src/ascii/types.ts",
    "chars": 9388,
    "preview": "// ============================================================================\n// ASCII renderer — type definitions\n//\n"
  },
  {
    "path": "src/ascii/validate.ts",
    "chars": 3898,
    "preview": "/**\n * ASCII Rendering Validation Utilities\n *\n * Provides validation functions for ASCII diagram output,\n * including d"
  },
  {
    "path": "src/ascii/xychart.ts",
    "chars": 28625,
    "preview": "// ============================================================================\n// ASCII renderer — XY Chart\n//\n// Rende"
  },
  {
    "path": "src/browser.ts",
    "chars": 869,
    "preview": "// ============================================================================\n// Browser entry point for beautiful-mer"
  },
  {
    "path": "src/class/layout.ts",
    "chars": 6915,
    "preview": "/**\n * Class diagram layout engine (ELK.js).\n *\n * Each class box has 3 compartments:\n *   1. Header (class name + optio"
  },
  {
    "path": "src/class/parser.ts",
    "chars": 9613,
    "preview": "import type { ClassDiagram, ClassNode, ClassRelationship, ClassMember, RelationshipType, ClassNamespace } from './types."
  },
  {
    "path": "src/class/renderer.ts",
    "chars": 14804,
    "preview": "import type { PositionedClassDiagram, PositionedClassNode, PositionedClassRelationship, ClassMember, RelationshipType } "
  },
  {
    "path": "src/class/types.ts",
    "chars": 3999,
    "preview": "// ============================================================================\n// Class diagram types\n//\n// Models the "
  },
  {
    "path": "src/elk-instance.ts",
    "chars": 4004,
    "preview": "/**\n * Shared ELK instance singleton.\n *\n * Uses elk.bundled.js (pure synchronous JS, ~1.6 MB) for all environments.\n * "
  },
  {
    "path": "src/er/layout.ts",
    "chars": 5294,
    "preview": "/**\n * ER diagram layout engine (ELK.js).\n *\n * Each entity box has:\n *   1. Header (entity name)\n *   2. Attribute rows"
  },
  {
    "path": "src/er/parser.ts",
    "chars": 5578,
    "preview": "import type { ErDiagram, ErEntity, ErAttribute, ErRelationship, Cardinality } from './types.ts'\nimport { normalizeBrTags"
  },
  {
    "path": "src/er/renderer.ts",
    "chars": 14693,
    "preview": "import type { PositionedErDiagram, PositionedErEntity, PositionedErRelationship, ErAttribute, Cardinality } from './type"
  },
  {
    "path": "src/er/types.ts",
    "chars": 2527,
    "preview": "// ============================================================================\n// ER diagram types\n//\n// Models the par"
  },
  {
    "path": "src/index.ts",
    "chars": 6512,
    "preview": "// ============================================================================\n// beautiful-mermaid — public API\n//\n// "
  },
  {
    "path": "src/layout-engine.ts",
    "chars": 48325,
    "preview": "/**\n * Layout engine for beautiful-mermaid (ELK.js based).\n *\n * Converts MermaidGraph to ELK's JSON format, runs layout"
  },
  {
    "path": "src/layout.ts",
    "chars": 241,
    "preview": "/**\n * Layout module for flowchart and state diagrams.\n *\n * Uses ELK.js for graph layout — battle-tested, full subgraph"
  },
  {
    "path": "src/multiline-utils.ts",
    "chars": 7917,
    "preview": "// ============================================================================\n// Multi-line Text Rendering Utilities\n/"
  },
  {
    "path": "src/parser.ts",
    "chars": 21927,
    "preview": "import type { MermaidGraph, MermaidNode, MermaidEdge, MermaidSubgraph, Direction, NodeShape, EdgeStyle } from './types.t"
  },
  {
    "path": "src/renderer.ts",
    "chars": 24071,
    "preview": "import type { PositionedGraph, PositionedNode, PositionedEdge, PositionedGroup, Point } from './types.ts'\nimport type { "
  },
  {
    "path": "src/sequence/layout.ts",
    "chars": 15998,
    "preview": "import type { SequenceDiagram, PositionedSequenceDiagram, PositionedActor, Lifeline, PositionedMessage, Activation, Posi"
  },
  {
    "path": "src/sequence/parser.ts",
    "chars": 6917,
    "preview": "import type { SequenceDiagram, Actor, Message, Block, Note } from './types.ts'\nimport { normalizeBrTags } from '../multi"
  },
  {
    "path": "src/sequence/renderer.ts",
    "chars": 13902,
    "preview": "import type { PositionedSequenceDiagram, PositionedActor, Lifeline, PositionedMessage, Activation, PositionedBlock, Posi"
  },
  {
    "path": "src/sequence/types.ts",
    "chars": 3940,
    "preview": "// ============================================================================\n// Sequence diagram types\n//\n// Models t"
  },
  {
    "path": "src/shape-clipping.ts",
    "chars": 6098,
    "preview": "/**\n * Shape-aware edge clipping utilities.\n *\n * ELK.js treats all nodes as rectangles for edge routing. For non-rectan"
  },
  {
    "path": "src/styles.ts",
    "chars": 3927,
    "preview": "// ============================================================================\n// Font metrics — character width estima"
  },
  {
    "path": "src/text-metrics.ts",
    "chars": 8506,
    "preview": "// ============================================================================\n// Text Metrics — Variable-width charact"
  },
  {
    "path": "src/theme.ts",
    "chars": 11517,
    "preview": "// ============================================================================\n// Theme system — CSS custom property-ba"
  },
  {
    "path": "src/types.ts",
    "chars": 5295,
    "preview": "// ============================================================================\n// Parsed graph — logical structure extr"
  },
  {
    "path": "src/xychart/colors.ts",
    "chars": 5145,
    "preview": "// ============================================================================\n// XY Chart — shared color palette\n//\n//"
  },
  {
    "path": "src/xychart/layout.ts",
    "chars": 15743,
    "preview": "import type {\n  XYChart, PositionedXYChart, PositionedAxis, AxisTick,\n  PositionedBar, PositionedLine, GridLine, PlotAre"
  },
  {
    "path": "src/xychart/parser.ts",
    "chars": 3757,
    "preview": "import type { XYChart, XYAxis, XYChartSeries } from './types.ts'\n\n// ==================================================="
  },
  {
    "path": "src/xychart/renderer.ts",
    "chars": 23792,
    "preview": "import type { PositionedXYChart } from './types.ts'\nimport type { DiagramColors } from '../theme.ts'\nimport { svgOpenTag"
  },
  {
    "path": "src/xychart/types.ts",
    "chars": 4263,
    "preview": "// ============================================================================\n// XY Chart types\n//\n// Models the parse"
  },
  {
    "path": "tsconfig.json",
    "chars": 900,
    "preview": "{\n  \"compilerOptions\": {\n    \"types\": [\"@types/bun\"],\n    \"lib\": [\"ESNext\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNe"
  },
  {
    "path": "tsup.config.ts",
    "chars": 256,
    "preview": "import { defineConfig } from 'tsup'\n\nexport default defineConfig({\n  entry: ['src/index.ts'],\n  format: ['esm'],\n  dts: "
  },
  {
    "path": "wrangler.toml",
    "chars": 96,
    "preview": "name = \"craft-agents-mermaid\"\ncompatibility_date = \"2024-01-01\"\npages_build_output_dir = \"dist\"\n"
  },
  {
    "path": "xychart-design.md",
    "chars": 16815,
    "preview": "# XY Chart (xychart-beta) — Phase 2 Design Document\n\n## Overview\n\nThis document specifies the architecture for adding `x"
  },
  {
    "path": "xychart-samples-data.ts",
    "chars": 34976,
    "preview": "/**\n * XY Chart sample definitions for the beautiful-mermaid visual test suite.\n *\n * Contains ~100 xychart-beta example"
  },
  {
    "path": "xychart-test.html",
    "chars": 450847,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, "
  },
  {
    "path": "xychart-test.ts",
    "chars": 38964,
    "preview": "/**\n * Generates xychart-test.html showcasing xychart-beta Mermaid examples.\n *\n * Usage: bun run xychart-test.ts\n *\n * "
  }
]

About this extraction

This page contains the full source code of the lukilabs/beautiful-mermaid GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 196 files (1.5 MB), approximately 469.3k tokens, and a symbol index with 555 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!