ariel-rs 0.1.0

A faithful Rust port of Mermaid JS — headless SVG diagram rendering without a browser
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
# 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

| Mermaid method | ariel-rs-wasm implementation |
|----------------|--------------------------|
| `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

| Risk | Mitigation |
|------|-----------|
| 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)
  -t, --theme <name>        Theme: default | dark | forest | neutral
  -b, --backgroundColor     Background colour (e.g. transparent, #fff)
  -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

| Format | Implementation |
|--------|---------------|
| `.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
cat diagram.mmd | ariel -i - -o output.svg
ariel -i diagram.mmd -o -          # stdout
```

### 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)

| Gap | Fix required in |
|-----|----------------|
| 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

| Tool / Crate | Purpose |
|---|---|
| `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

| Platform | Arch | Output |
|---|---|---|
| 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 type Theme = 'default' | 'dark' | 'forest' | 'neutral';

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) {
  const [svg, setSvg] = useState<string | null>(null);

  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

| Platform | Arch | Output | Implementation |
|---|---|---|---|
| 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

| Tool / Package | Purpose |
|---|---|
| `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