Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/scripts/MERMAID.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Mermaid Diagram Rendering

Client-side rendering of Mermaid diagrams with Cloudflare branding (orange `#f6821f`), light/dark theme support, and annotation footers.

## Usage

### Basic Diagram

````markdown
```mermaid
flowchart LR
A[Client] --> B[Server]
B --> C[Database]
```
````

### With Title and Accessibility

````markdown
```mermaid
flowchart LR
accTitle: Workers for Platforms - Main Flow
accDescr: Shows how requests are routed through the platform

A[Browser Request] --> B[Router Worker]
B -->|Service Binding| C[User Worker A]
B -->|Service Binding| D[User Worker B]
```
````

The `accTitle` appears in the annotation footer. Always include `accDescr` for screen readers.

## How It Works

1. Rehype plugin (`src/plugins/rehype/mermaid.ts`) transforms markdown code blocks to `<pre class="mermaid">`
2. Client script (`src/scripts/mermaid.ts`) renders diagrams as SVG with custom theme variables
3. Theme changes trigger automatic re-rendering via `MutationObserver`

## Customization

- **Theme variables**: Edit `mermaid.ts`
- **Container styles**: Edit `src/styles/mermaid.css`
- See [Mermaid theming docs](https://mermaid.js.org/config/theming.html)

## Troubleshooting

- **Not rendering**: Check browser console, validate syntax at [mermaid.live](https://mermaid.live/)
- **No annotation**: Ensure `accTitle` is included in diagram definition

## Related Files

- `src/scripts/mermaid.ts` - Rendering script
- `src/styles/mermaid.css` - Styles
- `src/plugins/rehype/mermaid.ts` - Markdown transformer
119 changes: 113 additions & 6 deletions src/scripts/mermaid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,96 @@ const diagrams = document.querySelectorAll<HTMLPreElement>("pre.mermaid");

let init = false;

// Get computed font family from CSS variable
function getFontFamily(): string {
const computedStyle = getComputedStyle(document.documentElement);
const slFont = computedStyle.getPropertyValue("--__sl-font").trim();
return slFont || "system-ui, -apple-system, sans-serif";
}

// Create wrapper container with annotation
function wrapDiagram(diagram: HTMLPreElement, title: string | null) {
// Skip if already wrapped
if (diagram.parentElement?.classList.contains("mermaid-container")) {
return;
}

// Create container
const container = document.createElement("div");
container.className = "mermaid-container";

// Wrap the diagram
diagram.parentNode?.insertBefore(container, diagram);
container.appendChild(diagram);

// Add annotation footer if title exists
if (title) {
const footer = document.createElement("div");
footer.className = "mermaid-annotation";

const titleSpan = document.createElement("span");
titleSpan.className = "mermaid-annotation-title";
titleSpan.textContent = title;

const logo = document.createElement("img");
logo.src = "/logo.svg";
logo.alt = "Cloudflare";
logo.className = "mermaid-annotation-logo";

footer.appendChild(titleSpan);
footer.appendChild(logo);
container.appendChild(footer);
}
}

async function render() {
const theme =
document.documentElement.getAttribute("data-theme") === "light"
? "neutral"
: "dark";
const isLight =
document.documentElement.getAttribute("data-theme") === "light";
const fontFamily = getFontFamily();

// Custom theme variables for Cloudflare branding
const lightThemeVars = {
fontFamily,
primaryColor: "#fef1e6", // cl1-orange-9 (very light orange for node backgrounds)
primaryBorderColor: "#f6821f", // cl1-brand-orange
primaryTextColor: "#1d1d1d", // cl1-gray-0
secondaryColor: "#f2f2f2", // cl1-gray-9
secondaryBorderColor: "#999999", // cl1-gray-6
secondaryTextColor: "#1d1d1d", // cl1-gray-0
tertiaryColor: "#f2f2f2", // cl1-gray-9
tertiaryBorderColor: "#999999", // cl1-gray-6
tertiaryTextColor: "#1d1d1d", // cl1-gray-0
lineColor: "#f6821f", // cl1-brand-orange for arrows
textColor: "#1d1d1d", // cl1-gray-0
mainBkg: "#fef1e6", // cl1-orange-9
errorBkgColor: "#ffefee", // cl1-red-9
errorTextColor: "#3c0501", // cl1-red-0
edgeLabelBackground: "#ffffff", // white background for edge labels in light mode
labelBackground: "#ffffff", // white background for labels in light mode
};

const darkThemeVars = {
fontFamily,
primaryColor: "#482303", // cl1-orange-1 (dark orange for node backgrounds)
primaryBorderColor: "#f6821f", // cl1-brand-orange
primaryTextColor: "#f2f2f2", // cl1-gray-9
secondaryColor: "#313131", // cl1-gray-1
secondaryBorderColor: "#797979", // cl1-gray-5
secondaryTextColor: "#f2f2f2", // cl1-gray-9
tertiaryColor: "#313131", // cl1-gray-1
tertiaryBorderColor: "#797979", // cl1-gray-5
tertiaryTextColor: "#f2f2f2", // cl1-gray-9
lineColor: "#f6821f", // cl1-brand-orange for arrows
textColor: "#f2f2f2", // cl1-gray-9
mainBkg: "#482303", // cl1-orange-1
background: "#1d1d1d", // cl1-gray-0
errorBkgColor: "#3c0501", // cl1-red-0
errorTextColor: "#ffefee", // cl1-red-9
edgeLabelBackground: "#1d1d1d", // dark background for edge labels
labelBackground: "#1d1d1d", // dark background for labels
};

const themeVariables = isLight ? lightThemeVars : darkThemeVars;

for (const diagram of diagrams) {
if (!init) {
Expand All @@ -17,10 +102,30 @@ async function render() {

const def = diagram.getAttribute("data-diagram") as string;

mermaid.initialize({ startOnLoad: false, theme });
// Initialize with base theme and custom variables
mermaid.initialize({
startOnLoad: false,
theme: "base",
themeVariables,
flowchart: {
htmlLabels: true,
useMaxWidth: true,
},
});

await mermaid
.render(`mermaid-${crypto.randomUUID()}`, def)
.then(({ svg }) => (diagram.innerHTML = svg));
.then(({ svg }) => {
diagram.innerHTML = svg;

// Extract title from SVG for annotation
const svgElement = diagram.querySelector("svg");
const titleElement = svgElement?.querySelector("title");
const title = titleElement?.textContent?.trim() || null;

// Wrap diagram with container and annotation
wrapDiagram(diagram, title);
});

diagram.setAttribute("data-processed", "true");
}
Expand All @@ -34,3 +139,5 @@ obs.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});

render();
72 changes: 72 additions & 0 deletions src/styles/mermaid.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,75 @@
/* Hide unprocessed diagrams to prevent flash of unstyled content */
pre.mermaid:not([data-processed]) {
visibility: hidden;
}

/* Container wrapper for diagram + annotation */
.mermaid-container {
background: transparent;
overflow: hidden;
margin: 1rem 0;
border-radius: 0;
box-shadow: none;
}

/* The diagram itself */
pre.mermaid[data-processed] {
padding: 1.5rem;
margin: 0;
background: transparent;
border-radius: 0;
box-shadow: none;
display: block;
line-height: 0;
border-color: var(--sl-color-hairline) !important;
}

/* Ensure SVG fills the width nicely */
pre.mermaid[data-processed] svg {
max-width: 100%;
height: auto;
display: block;
margin: 0;
padding: 0;
vertical-align: top;
}

/* Remove any box shadow from SVG elements */
pre.mermaid[data-processed] svg * {
box-shadow: none !important;
}

/* Annotation footer */
.mermaid-annotation {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--sl-color-gray-3);
background: rgba(0, 0, 0, 0.02);
margin: 0;
}

.mermaid-annotation-title {
flex: 1;
}

.mermaid-annotation-logo {
height: 16px;
width: auto;
opacity: 0.7;
}

/* Dark mode adjustments */
:root[data-theme="dark"] .mermaid-annotation {
background: rgba(255, 255, 255, 0.03);
color: var(--sl-color-gray-2);
}

:root[data-theme="dark"] .mermaid-annotation-logo {
opacity: 0.8;
}
Loading