# ariel-rs Refactor Plan
A structured cleanup plan to complete before the first push to GitHub.
Items are grouped by category and ordered by dependency (foundational first).
---
## 1. Code Organisation
### 1.1 Constants files
**What:** Move every magic number and string literal out of renderer files into a sibling `constants.rs` per diagram.
**Pattern:**
```
src/diagrams/flowchart/
constants.rs ← FONT_SIZE, PADDING, NODE_H, ARROW_SIZE, …
parser.rs
renderer.rs ← imports from constants.rs
templates.rs
```
**Scope:** ~23 diagram modules.
---
### 1.2 SVG template files
**What:** Move all inline SVG `format!()` strings out of renderer logic into a sibling `templates.rs` per diagram. Templates are functions with typed parameters, not runtime files.
**Pattern:**
```rust
// templates.rs
pub fn node_rect(x: f64, y: f64, w: f64, h: f64, fill: &str, stroke: &str) -> String {
format!(r#"<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="{fill}" stroke="{stroke}"/>"#)
}
```
Renderer becomes pure logic — no raw SVG strings inline.
**Scope:** ~23 diagram modules.
---
### 1.3 Shared SVG primitives
**What:** Enrich `src/svg.rs` with typed builder functions covering the SVG elements used across multiple diagrams — `rect`, `circle`, `path`, `text`, `line`, `g`, `marker`, `defs`. Eliminate copy-paste across renderers.
**Pattern:**
```rust
// svg.rs
pub fn rect(x: f64, y: f64, w: f64, h: f64) -> SvgRect { … }
pub fn text(x: f64, y: f64, content: &str) -> SvgText { … }
```
---
### 1.4 Shared style/colour module
**What:** Create `src/style.rs` with typed colour and stroke structs. Replace per-renderer HSL strings, hex colours, and stroke-width literals.
**Pattern:**
```rust
pub struct Color(pub &'static str);
pub const ENTITY_FILL: Color = Color("#ECECFF");
pub struct Stroke { pub color: Color, pub width: f64 }
```
---
### 1.5 Strict parser/renderer separation
**What:** Any parsing logic inside renderer functions moves to the parser. Renderer becomes a pure `fn render(diagram: &Diagram, theme: Theme) -> String`.
**Check:** Each `renderer.rs` should contain zero token parsing, no `split()`, no `starts_with()` on raw input.
---
## 2. Correctness
### 2.1 Structured error types
**What:** Replace the current `render() -> String` (which silently returns an error SVG) with a proper `Result`:
```rust
#[derive(Debug)]
pub struct RenderError {
pub diagram_type: String,
pub message: String,
}
pub fn render(input: &str, theme: Theme) -> Result<String, RenderError>
pub fn render_or_error_svg(input: &str, theme: Theme) -> String // ← current behaviour, kept for compat
```
---
### 2.2 Structured parse errors
**What:** Parsers currently silently skip unknown tokens. Add a `ParseError` type with line/column/message. Each parser returns `Result<Diagram, Vec<ParseError>>` so callers can surface problems.
```rust
pub struct ParseError {
pub line: usize,
pub column: usize,
pub message: String,
}
```
---
### 2.3 Input validation
**What:** After parsing, validate the diagram model before rendering — e.g. check that edge endpoints reference existing nodes, date ranges are valid, required fields are present. Return structured errors rather than rendering broken output.
---
## 3. Performance
### 3.1 Lazy font loading (`OnceLock`)
**What:** `ab_glyph` font data is currently embedded and parsed on every `measure()` call. Use `std::sync::OnceLock` (stable since Rust 1.70) to load and parse the font once at first use.
```rust
static FONT: OnceLock<FontRef<'static>> = OnceLock::new();
fn font() -> &'static FontRef<'static> {
FONT.get_or_init(|| FontRef::try_from_slice(FONT_BYTES).unwrap())
}
```
---
### 3.2 String builder / `SvgWriter`
**What:** Replace `String::push_str()` chains in renderers with a lightweight `SvgWriter` that implements `Write`. Reduces intermediate allocations and makes SVG construction composable.
```rust
pub struct SvgWriter(String);
impl SvgWriter {
pub fn elem(&mut self, tag: &str, attrs: &[(&str, &str)], children: impl FnOnce(&mut Self)) { … }
pub fn finish(self) -> String { self.0 }
}
```
---
## 4. Testing
### 4.1 Unit tests per renderer
**What:** Every renderer module gets a `#[cfg(test)]` block with fast unit tests covering:
- Key SVG properties (viewBox, correct element counts)
- Theme application (correct colours)
- Edge cases (empty diagram, single node, max nodes)
Currently only `timeline` and `journey` have renderer tests. The other 21 rely entirely on visual regression.
---
### 4.2 Snapshot testing with `insta`
**What:** Replace pixel-based visual regression PNGs with SVG snapshot tests using the [`insta`](https://crates.io/crates/insta) crate.
**Benefits:**
- No resvg / PNG conversion step
- Diffs are readable text
- Run as normal `cargo test`
- CI doesn't need image comparison scripts
**Migration:** Keep the existing visual regression suite as a secondary check; add `insta` snapshots as the primary fast feedback loop.
---
## 5. Public API
### 5.1 `DiagramType` enum
**What:** Expose the detected diagram type so callers can inspect it without re-parsing.
```rust
#[derive(Debug, Clone, PartialEq)]
pub enum DiagramType {
Flowchart, Sequence, Class, State, Er, Gantt, Git, Pie,
Mindmap, Timeline, Quadrant, XyChart, C4, Block, Packet,
Journey, Requirement, Kanban, Sankey, Treemap, Radar,
Venn, Architecture, Unknown,
}
pub fn detect(input: &str) -> DiagramType { … }
```
---
### 5.2 `RenderOptions`
**What:** Extend the render API beyond `Theme` to support per-call configuration.
```rust
pub struct RenderOptions {
pub theme: Theme,
pub font_family: Option<String>,
pub font_size: Option<f64>,
pub max_width: Option<f64>,
pub background: Option<String>,
}
pub fn render_with_options(input: &str, options: RenderOptions) -> Result<String, RenderError>
```
---
## Execution order
Start with the items you specifically requested, then build outward:
`
PHASE 1 — what you asked for first
1.1 Constants files (per diagram)
1.2 SVG template files (per diagram, depends on 1.1)
PHASE 2 — shared foundations
1.3 Shared SVG primitives (src/svg.rs enrichment)
1.4 Shared style module (src/style.rs, new)
3.1 Font OnceLock (src/text.rs, quick win)
PHASE 3 — architecture
1.5 Parser/renderer split (per diagram)
3.2 SvgWriter (depends on 1.2, 1.3)
PHASE 4 — correctness
2.1 Structured error types (lib.rs public API)
2.2 Structured parse errors (per parser)
2.3 Input validation (depends on 2.2)
PHASE 5 — API surface
5.1 DiagramType enum (lib.rs)
5.2 RenderOptions (lib.rs, depends on 2.1, 5.1)
PHASE 6 — testing
4.1 Unit tests per renderer
4.2 Snapshot testing (insta)
PHASE 7 — WASM / npm drop-in replacement
7.1 ariel-rs-wasm crate (new repo, depends on ariel-rs via crates.io)
7.2 JS/TS API wrapper (implements Mermaid JS surface)
7.3 npm package (@rinfimate/ariel or ariel-js)
7.4 TypeScript declarations (.d.ts matching mermaid types)
7.5 DOM integration (mermaid.run() / .mermaid element scanning)
`
---
---
## 6. WASM / npm Drop-in Replacement
### 7.1 `ariel-rs-wasm` crate (new repo)
**What:** A thin Rust crate that wraps `ariel-rs` and exposes it via `wasm-bindgen`.
```
ariel-rs-wasm/
Cargo.toml ← [dependencies] ariel-rs = "x.y", wasm-bindgen, web-sys
src/lib.rs ← #[wasm_bindgen] pub fn render(...) -> String
js/
index.js ← Mermaid API surface: initialize, render, parse, run
index.d.ts ← TypeScript declarations matching mermaid types
package.json ← name: "@rinfimate/ariel", main: index.js
```
### 7.2 Mermaid JS API surface to implement
```typescript
// User changes ONE line:
import mermaid from '@rinfimate/ariel'; // was: from 'mermaid'
// These all work identically:
mermaid.initialize({ theme: 'dark', securityLevel: 'strict' });
const { svg } = await mermaid.render('diagram-id', 'flowchart TD\nA-->B');
const { diagramType } = await mermaid.parse('flowchart TD\nA-->B');
mermaid.run({ nodes: document.querySelectorAll('.mermaid') });
```
### 7.3 API mapping
| `initialize(config)` | Store config; map `theme` → `Theme` enum; fetch font if `fontFamily` set |
| `render(id, text)` | Call WASM `render()`, return `{svg, bindFunctions: ()=>{}}` |
| `parse(text)` | Call WASM `detect()`, return `{diagramType}` |
| `run(options)` | Query DOM, call `render()` per element, inject SVG |
| `contentLoaded()` | Legacy alias for `run()` |
### 7.4 Font fidelity — `fontFamily` config
`mermaid.initialize({ fontFamily: 'Inter' })` works identically in Mermaid JS and ariel-rs-wasm. The JS wrapper handles font fetching transparently:
```javascript
// js/index.js
async function initialize(config = {}) {
await wasmInit(); // load .wasm
if (config.fontFamily) {
const bytes = await fetchFontBytes(config.fontFamily);
wasmSetFont(bytes); // passes Uint8Array into WASM for text measurement
}
storedConfig = config;
}
async function fetchFontBytes(family) {
// Ask Google Fonts for the CSS, extract the actual font file URL, fetch bytes.
const css = await fetch(
`https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}`
).then(r => r.text());
const url = css.match(/url\(([^)]+)\)/)[1];
return new Uint8Array(await fetch(url).then(r => r.arrayBuffer()));
}
```
On the Rust/WASM side:
```rust
#[wasm_bindgen]
pub fn set_font(bytes: Vec<u8>) {
// Overrides the bundled Liberation Sans for text measurement.
CUSTOM_FONT.set(bytes).ok();
}
```
- **Default (no fontFamily set):** Liberation Sans bundled in the `.wasm` — zero network calls.
- **Custom font:** one Google Fonts fetch during `initialize()`, then all renders use it.
- **ariel-react-native web:** same path — `initialize()` is async and already handles WASM init, font fetch is one more `await` in the same chain.
### 7.5 Risks and mitigations
| WASM load is async (network fetch) | `initialize()` returns a Promise; document that it must be awaited |
| `bindFunctions` expected by some users | Return no-op — most users don't use it |
| DOM scanning (`mermaid.run()`) | Implement in JS wrapper, not WASM |
| 2 unsupported diagram types | Return error SVG (same as ariel-rs today) |
| Bundle size (~1-2 MB) | Similar to Mermaid JS; acceptable |
| Google Fonts fetch blocked (CSP / offline) | Caller can pass `fontBytes: Uint8Array` directly in config as escape hatch |
---
## 8. `ariel-rs-cli` — Mermaid CLI Drop-in Replacement
**What:** A standalone binary crate (`ariel-rs-cli`, new repo) with full command-line fidelity to [`@mermaid-js/mermaid-cli`](https://github.com/mermaid-js/mermaid-cli) (`mmdc`). Users swap `mmdc` for `ariel` and all scripts/CI pipelines continue to work.
### 8.1 Mermaid CLI flags to match
```
mmdc [options]
-i, --input <file> Input .mmd file (or stdin with -)
-o, --output <file> Output file (.svg, .png, .pdf)
-w, --width <px> Output width in pixels (for PNG)
-H, --height <px> Output height in pixels (for PNG)
-s, --scale <factor> PNG scale factor (default: 1)
-f, --configFile <file> JSON config file
-c, --cssFile <file> Custom CSS file to inject
-e, --puppeteerConfigFile ← IGNORE (no puppeteer needed)
-p, --puppeteerConfig ← IGNORE
--quiet Suppress output
--version Print version and exit
```
### 8.2 Repo structure
```
ariel-rs-cli/
Cargo.toml ← [[bin]] name = "ariel", depends on ariel-rs
src/
main.rs ← CLI entry point (clap)
args.rs ← Arg definitions matching mmdc flags
output.rs ← SVG/PNG/PDF writer
config.rs ← JSON config file parsing
README.md
CHANGELOG.md
LICENSE
```
### 8.3 Output formats
| `.svg` | Direct from ariel-rs `render()` |
| `.png` | SVG → PNG via `resvg` (already used in visual-regression) |
| `.pdf` | SVG → PDF via `svg2pdf` crate |
### 8.4 Stdin/stdout support
```sh
# All of these should work:
ariel -i diagram.mmd -o output.svg
ariel -i diagram.mmd -o output.png -w 800
```
### 8.5 Install experience (goal)
```sh
# Rust users:
cargo install ariel-rs-cli
# npm users (via ariel-rs-wasm npm package):
npx @rinfimate/ariel -i diagram.mmd -o output.svg
# Eventually:
brew install ariel # Homebrew formula
```
### 8.6 CI workflow
Same pattern as dagre-dgl-rs and ariel-rs: test → lint → fmt → publish on release. Add cross-compilation targets for pre-built binaries (Linux x86_64, macOS arm64, Windows x86_64) attached to GitHub Releases.
---
## 7. Benchmarks
### 7.1 Criterion benchmarks (ariel-rs)
**What:** Add `benches/` to ariel-rs using the `criterion` crate. Measure render time per diagram type.
```toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "render"
harness = false
```
```rust
// benches/render.rs
fn bench_flowchart(c: &mut Criterion) {
c.bench_function("flowchart_christmas", |b| {
b.iter(|| render(FLOWCHART_CHRISTMAS, Theme::Default))
});
}
```
Run: `cargo bench` → generates HTML report in `target/criterion/`
### 7.2 Comparison vs Mermaid JS
**What:** After WASM is built, add a `benchmarks/` folder to `ariel-rs-wasm` with a Node.js script that times both `mermaid` and `@rinfimate/ariel` on the same 71 corpus diagrams.
```
benchmarks/
run.mjs ← times mermaid-js vs ariel-rs-wasm, outputs table
results.md ← checked-in results for the README badge
```
Expected result: **100–1400x faster** (based on prior benchmarks in the Rust/WASM ecosystem for similar diagram renderers).
---
---
## 9. Mermaid Live Editor Fork — Ultimate Integration Test
**What:** Fork [mermaid-js/mermaid-live-editor](https://github.com/mermaid-js/mermaid-live-editor) and replace the single mermaid import with riel-rs-wasm. If the editor works with zero other changes, API fidelity is proven.
### 9.1 Why this is the gold standard
The Live Editor uses the full Mermaid API surface:
- mermaid.initialize() with config
- mermaid.render() for live preview
- mermaid.parse() for syntax validation
- Error rendering for bad syntax
- Theme switching in the UI
- All 23+ diagram types exercised interactively
If it works, ariel-rs-wasm is a true drop-in. If it breaks, the diff reveals exactly what API gaps remain.
### 9.2 Approach
`sh
# 1. Fork mermaid-live-editor to rinfimate/ariel-live-editor
# 2. One-line change in package.json:
- "mermaid": "^11.x"
+ "@rinfimate/ariel-rs-wasm": "^0.1.0"
# 3. One-line change in the source:
- import mermaid from 'mermaid'
+ import mermaid from '@rinfimate/ariel-rs-wasm'
# 4. npm install && npm run dev → verify all diagrams render
`
### 9.3 Expected gaps to fix (before this works)
| mermaid.initialize() config keys | ariel-rs-wasm JS wrapper |
| mermaid.render() returning {svg, bindFunctions} | ariel-rs-wasm JS wrapper |
| mermaid.parse() error format | ariel-rs-wasm JS wrapper |
| Theme names as strings ('dark') | ariel-rs-wasm config mapping |
| securityLevel config | stub in JS wrapper |
| Real-time re-render performance | should be faster than mermaid-js |
### 9.4 Deployment
Once working, deploy riel-live-editor to a public URL (Vercel/Netlify) as a live demo. This becomes the headline showcase for ariel-rs-wasm.
---
## 10. Flutter / Dart Package — `ariel_flutter`
**What:** A Flutter plugin that calls ariel-rs natively via [`flutter_rust_bridge`](https://pub.dev/packages/flutter_rust_bridge). Renders diagrams on mobile and desktop with no JS engine, no WebView, no network call.
### 10.1 Repo structure
```
ariel-flutter/
ariel_rs/ ← git submodule or path dep pointing at ariel-rs
rust/
Cargo.toml ← [lib] crate-type = ["cdylib", "staticlib"]
src/lib.rs ← #[flutter_rust_bridge::frb] pub fn render(...)
lib/
ariel_flutter.dart ← generated + hand-written Dart API
src/
bridge_generated.dart ← flutter_rust_bridge output (do not edit)
android/ ← Android JNI glue (auto-generated)
ios/ ← iOS xcframework (auto-generated)
macos/ ← macOS dylib (auto-generated)
windows/ ← Windows DLL (auto-generated)
linux/ ← Linux .so (auto-generated)
example/
lib/main.dart ← demo app rendering all 23 diagram types
pubspec.yaml ← name: ariel_flutter
CHANGELOG.md
README.md
```
### 10.2 Rust API surface exposed to Dart
```rust
// rust/src/lib.rs
use flutter_rust_bridge::frb;
#[frb(sync)] // synchronous — no await in Dart
pub fn render(input: String, theme: String) -> String {
ariel_rs::render(&input, theme.parse().unwrap_or_default())
}
#[frb(sync)]
pub fn try_render(input: String, theme: String) -> Result<String, String> {
ariel_rs::try_render(&input, theme.parse().unwrap_or_default())
.map_err(|e| e.to_string())
}
#[frb(sync)]
pub fn detect(input: String) -> String {
format!("{:?}", ariel_rs::detect(&input))
}
```
### 10.3 Dart API (hand-written wrapper over generated bridge)
```dart
// lib/ariel_flutter.dart
import 'src/bridge_generated.dart';
class Ariel {
/// Renders [input] to an SVG string. Returns an error SVG on failure.
static String render(String input, {String theme = 'default'}) =>
ArielFlutterImpl.render(input: input, theme: theme);
/// Returns null on success, error message on failure.
static String? tryRender(String input, {String theme = 'default'}) {
try {
return ArielFlutterImpl.tryRender(input: input, theme: theme);
} catch (e) {
return null;
}
}
}
```
### 10.4 Flutter widget (bonus)
```dart
// lib/src/mermaid_view.dart
class MermaidView extends StatelessWidget {
final String source;
final String theme;
const MermaidView({required this.source, this.theme = 'default'});
@override
Widget build(BuildContext context) {
final svg = Ariel.render(source, theme: theme);
return SvgPicture.string(svg); // flutter_svg package
}
}
```
Usage in any Flutter app:
```dart
MermaidView(source: 'flowchart TD\n A --> B --> C')
```
### 10.5 Build & publish
```sh
# Generate bridge (run once after changing Rust API)
flutter_rust_bridge_codegen generate
# Build for all platforms
flutter build apk
flutter build ios
flutter build macos
# Publish to pub.dev
flutter pub publish
```
### 10.6 Dependencies
| `flutter_rust_bridge` | Rust ↔ Dart FFI codegen |
| `flutter_svg` | Render SVG string as Flutter widget |
| `ariel-rs` | Core renderer (path dep or crates.io) |
| `cargo-ndk` | Build Android `.so` targets |
| `xcode-select` | Build iOS/macOS `.xcframework` |
### 10.7 Platform targets
| Android | arm64-v8a, armeabi-v7a, x86_64 | `.so` via cargo-ndk |
| iOS | arm64 + sim | `.xcframework` via cargo-lipo |
| macOS | arm64 + x86_64 | `.dylib` universal |
| Windows | x86_64 | `.dll` |
| Linux | x86_64 | `.so` |
| Web | wasm32 | via ariel-rs-wasm (separate) |
### 10.8 CI
Same GitHub Actions pattern as dagre-dgl-rs and ariel-rs. Add matrix for Android NDK and iOS cross-compilation. Publish to pub.dev on release tag.
---
---
## 11. React Native Package — `ariel-react-native`
**What:** A React Native library with platform-split implementations:
- **Android / iOS**: Rust compiled to native binary, called via JSI. No WebView, no WASM — Hermes does not support WASM.
- **Web (react-native-web)**: WASM via `@rinfimate/ariel-rs-wasm` — the browser webpack bundle supports WASM natively.
Metro resolves `.native.ts` on Android/iOS; webpack resolves `.web.ts` on web. One package, two implementations, identical public API.
### 11.1 Architecture
```
React Native (Android/iOS) react-native-web (browser)
───────────────────────── ──────────────────────────
index.native.ts index.web.ts
│ JSI (synchronous) │ WASM (async, pre-initialized)
▼ ▼
C++ JSI HostObject @rinfimate/ariel-rs-wasm
│ C FFI (ariel-rs-wasm npm package)
▼
ariel_rn.so / .a (Rust)
```
### 11.2 Repo structure
```
ariel-react-native/
rust/
Cargo.toml ← crate-type = ["cdylib", "staticlib"]
src/
lib.rs ← #[no_mangle] extern "C" fn ariel_render(...)
ffi.rs ← C-safe types, string handling
cpp/
ArielRn.h ← C++ JSI HostObject declaration
ArielRn.cpp ← JSI HostObject implementation
android/
src/main/jni/
CMakeLists.txt ← links Rust .so
ArielRnJni.cpp ← JNI entry point
build.gradle
ios/
ArielRn.h
ArielRn.mm ← Objective-C++ JSI install
ArielRn.xcodeproj
src/
index.ts ← shared types + re-export
index.native.ts ← native impl (JSI → Rust)
index.web.ts ← web impl (WASM via ariel-rs-wasm)
NativeAriel.ts ← TurboModule spec (native only)
MermaidView.tsx ← cross-platform React component
package.json ← name: ariel-react-native
ariel-react-native.podspec ← iOS CocoaPods spec
```
### 11.3 Rust C FFI layer (native only)
```rust
// rust/src/lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn ariel_render(input: *const c_char, theme: *const c_char) -> *mut c_char {
let input = unsafe { CStr::from_ptr(input) }.to_str().unwrap_or("");
let theme = unsafe { CStr::from_ptr(theme) }.to_str().unwrap_or("default");
let svg = ariel_rs::render(input, theme.parse().unwrap_or_default());
CString::new(svg).unwrap().into_raw()
}
#[no_mangle]
pub extern "C" fn ariel_free_string(ptr: *mut c_char) {
if !ptr.is_null() { unsafe { drop(CString::from_raw(ptr)) }; }
}
#[no_mangle]
pub extern "C" fn ariel_detect(input: *const c_char) -> *mut c_char {
let input = unsafe { CStr::from_ptr(input) }.to_str().unwrap_or("");
CString::new(format!("{:?}", ariel_rs::detect(input))).unwrap().into_raw()
}
```
### 11.4 Platform-split TypeScript API
```typescript
// src/index.ts — shared types
export interface ArielApi {
render(input: string, theme?: Theme): Promise<string>;
detect(input: string): Promise<string>;
}
```
```typescript
// src/index.native.ts — JSI path (Android/iOS)
import { NativeModules } from 'react-native';
const { ArielRn } = NativeModules;
export type { Theme } from './index';
export function render(input: string, theme: Theme = 'default'): Promise<string> {
return Promise.resolve(ArielRn.render(input, theme));
}
export function detect(input: string): Promise<string> {
return Promise.resolve(ArielRn.detect(input));
}
```
```typescript
// src/index.web.ts — WASM path (react-native-web / browser)
import init, {
render as wasmRender,
detect as wasmDetect,
} from '@rinfimate/ariel-rs-wasm';
export type { Theme } from './index';
// Initialize once; subsequent calls are instant.
const ready = init();
export async function render(input: string, theme: Theme = 'default'): Promise<string> {
await ready;
return wasmRender(input, theme);
}
export async function detect(input: string): Promise<string> {
await ready;
return wasmDetect(input);
}
```
### 11.5 Cross-platform MermaidView component
```tsx
// src/MermaidView.tsx — works on Android, iOS, and web
import React, { useEffect, useState } from 'react';
import { ActivityIndicator } from 'react-native';
import { SvgXml } from 'react-native-svg';
import { render } from './index'; // resolved to .native or .web by bundler
interface Props {
source: string;
theme?: Theme;
width?: number;
height?: number;
}
export function MermaidView({ source, theme = 'default', width, height }: Props) {
useEffect(() => {
let cancelled = false;
render(source, theme).then(s => { if (!cancelled) setSvg(s); });
return () => { cancelled = true; };
}, [source, theme]);
if (!svg) return <ActivityIndicator />;
return <SvgXml xml={svg} width={width} height={height} />;
}
```
### 11.6 Platform targets
| Android | arm64-v8a, armeabi-v7a, x86_64 | `.so` | Rust via cargo-ndk |
| iOS | arm64 + sim | `.xcframework` | Rust via cargo-lipo |
| Web (react-native-web) | wasm32 | `.wasm` bundle | ariel-rs-wasm via npm |
### 11.7 Dependencies
| `cargo-ndk` | Build Android `.so` targets |
| `cbindgen` | Generate C header from Rust FFI |
| `react-native-svg` | Render SVG string as RN + web component |
| `@rinfimate/ariel-rs-wasm` | Web implementation (peer dependency) |
| `react-native-nitro-modules` | Optional: JSI HostObject for zero-overhead native calls |
### 11.8 peer dependencies in package.json
```json
{
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-svg": ">=13.0.0",
"@rinfimate/ariel-rs-wasm": ">=0.1.0"
},
"peerDependenciesMeta": {
"@rinfimate/ariel-rs-wasm": { "optional": true }
}
}
```
`ariel-rs-wasm` is optional — native-only apps don't need it. react-native-web users add it explicitly.
### 11.9 CI
Matrix build: Android NDK on ubuntu-latest, iOS on macos-latest, web bundle check on ubuntu-latest. Publish to npm on release tag.
---
## Known remaining items (core, post-push)
### font_family wiring
`RenderOptions.font_family` and a new `font_data: Option<Vec<u8>>` field need to be threaded through to `src/text.rs` so callers can override the bundled Liberation Sans at render time. On the WASM build the JS wrapper fetches the font bytes from Google Fonts and calls `wasmSetFont(bytes)` — the Rust side needs to accept those bytes via a `OnceLock` or thread-local override.
### Validation stubs (2.3 follow-up)
23 of 29 `validate()` functions return `vec![]`. Fill in real checks for:
pie, git, mindmap, timeline, quadrant, xychart, c4, block, packet, journey, requirement, kanban, sankey, treemap, radar, venn, architecture, eventmodeling, cynefin, ishikawa, wardley, zenuml, railroad.
### Non-deterministic snapshot tests
`state` and `mindmap` snapshot tests are `#[ignore]` due to HashMap ordering non-determinism in dagre-dgl-rs node IDs. Fix by sorting node IDs before comparing in the snapshot, or by seeding the ID counter to a fixed value in tests.
### pie_equal visual regression WARN
`pie_equal` sits at MAD 1.1% / PDIFF 13% — below the FAIL threshold (15%) but above PASS (5%). Pre-existing before the Liberation Sans swap. Investigate and fix as part of the diagram polish pass.
### Diagram visual polish (2–3 diagrams)
A small number of diagrams are not yet pixel-perfect against the Mermaid JS reference — identified during earlier work sessions. Fix in core ariel-rs; all downstream packages (WASM, CLI, Flutter, React Native) pick up the fix automatically.
---
## Definition of done
- [x] cargo clippy — 0 warnings
- [x] cargo test — all tests pass
- [x] Visual regression — PASS 70, WARN 1, FAIL 0
- [x] cargo publish --dry-run — clean package
- [x] No #[allow(...)] suppressions except where documented