Skip to content
Merged
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ Each hands its parsed diagram straight to `MermaidRenderer.pngData(diagram:)` /
take `(source, format)` and dispatch to the right front-end, so a DOT or Dippin
block gets the same image, sized/themed text attachment, and accessibility
narration that Mermaid does — `MermaidRenderer.attachmentString(source:format:theme:)`,
`altText(source:format:)`, `image(source:format:theme:)`, with
`format: .mermaid | .dot | .dippin | .sqlDDL | .gitLog`.
`altText(source:format:)`, `image(source:format:theme:)`, `pngData(source:format:theme:)`,
with `format: .mermaid | .dot | .dippin | .sqlDDL | .gitLog`.

## Supported diagram types — honestly

Expand Down Expand Up @@ -419,7 +419,8 @@ that same scene now drives the shipped native **Android** (Kotlin `Canvas`) and
SQL DDL, and `git log` output → gitgraph) into the same IR;
`DOTExporter.export(_:)` is the inverse (Flowchart → DOT).
- `MermaidRenderer.attachmentString(source:format:theme:)` /
`altText(source:format:)` / `image(source:format:theme:)` — **format-aware**
`altText(source:format:)` / `image(source:format:theme:)` /
`pngData(source:format:theme:)` — **format-aware**
entry points that dispatch on `DiagramSourceFormat` (`.mermaid`/`.dot`/`.dippin`/
`.sqlDDL`/`.gitLog`), so a non-Mermaid block gets the same attachment, narration,
and image without the consumer touching a parser (`.mermaid` = the source-only
Expand Down
6 changes: 5 additions & 1 deletion Sources/MermaidRender/DiagramRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,11 @@ enum DiagramRenderer {
static func attachmentString(diagram: MermaidDiagram, title: String?, theme: DiagramTheme,
spacing: DiagramSpacing = .regular) -> NSAttributedString? {
guard let image = image(for: diagram, title: title, theme: theme, spacing: spacing) else { return nil }
return attributedString(for: Entry(image: image, altText: MermaidAltText.describe(diagram)))
// Fold the visible caption into the narration so assistive-tech users get
// the same context sighted users read above the diagram.
let narration = MermaidAltText.describe(diagram)
let alt = (title?.isEmpty == false) ? "\(title!). \(narration)" : narration
return attributedString(for: Entry(image: image, altText: alt))
}

/// Cached format-aware attachment. Keyed on the source TAGGED by format (so a
Expand Down
18 changes: 18 additions & 0 deletions Tests/MermaidRenderTests/FormatAwareEntryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ final class FormatAwareEntryTests: XCTestCase {
}

#if canImport(AppKit)
/// A caption passed to the diagram-based attachment is rendered visually AND
/// folded into the narration, so assistive-tech users get the same context.
func testCaptionIsNarrated() throws {
let diagram = try XCTUnwrap(MermaidParser.parse("flowchart TD\n A[Start] --> B[Done]"))
let base = MermaidRenderer.altText(diagram: diagram)

let titled = try XCTUnwrap(MermaidRenderer.attachmentString(diagram: diagram, title: "Q3 pipeline", theme: theme))
let tImage = try XCTUnwrap((titled.attribute(.attachment, at: 0, effectiveRange: nil) as? NSTextAttachment)?.image)
let desc = try XCTUnwrap(tImage.accessibilityDescription)
XCTAssertTrue(desc.hasPrefix("Q3 pipeline. "), "caption must lead the narration: \(desc)")
XCTAssertTrue(desc.contains(base), "diagram narration must remain after the caption")

// No caption → narration is exactly the diagram description (no stray prefix).
let plain = try XCTUnwrap(MermaidRenderer.attachmentString(diagram: diagram, title: nil, theme: theme))
let pImage = try XCTUnwrap((plain.attribute(.attachment, at: 0, effectiveRange: nil) as? NSTextAttachment)?.image)
XCTAssertEqual(pImage.accessibilityDescription, base)
}

/// The core of #46: the attachment's image carries the SAME narration as
/// `altText(source:format:)` — accessibility parity with Mermaid, for every
/// front-end. (AppKit sets `accessibilityDescription` off the main thread;
Expand Down
14 changes: 8 additions & 6 deletions docs/website/BRIEF.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ feels thin, use fewer claims — the diagrams do the impressing.

## What MermaidKit is

MermaidKit renders [Mermaid](https://mermaid.js.org) diagrams natively on
Apple platforms — pure Swift and CoreGraphics. No JavaScript, no WebView,
zero dependencies. All **30 mermaid diagram types**, a one-line SwiftUI
view, image / attributed-string / vector-PDF output, VoiceOver
descriptions for every diagram, themed with a single value. MIT license.
MermaidKit renders [Mermaid](https://mermaid.js.org) diagrams natively —
pure Swift and CoreGraphics on Apple, with Android, Windows/.NET,
WebAssembly, and Flutter renderers over the same platform-free layout core.
No JavaScript, no WebView, zero dependencies. All **30 mermaid diagram
types**, a one-line SwiftUI view, image / attributed-string / vector-PDF /
SVG output, VoiceOver descriptions for every diagram, themed with a single
value. MIT license.

- Repo: https://github.com/2389-research/MermaidKit
- Latest release: v2.2.0
Expand Down Expand Up @@ -112,7 +114,7 @@ Install:
that plainness — it's part of the identity.
- Platforms: renders natively on Apple (macOS 14+, iOS 17+, visionOS 1+),
Linux, Android, Windows/.NET, WebAssembly, and Flutter, from one Swift
layout core (Swift 6 language mode, Xcode 16+ to build). Lower Apple OS
layout core (Swift 6 language mode; Xcode 26 / Swift 6.2 to build). Lower Apple OS
floors are the top of the public roadmap.

## Feature space (complete, verified)
Expand Down
2 changes: 1 addition & 1 deletion tools/pi-canvas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ swift run --traits LinuxRaster,SDL pi-canvas --sdl <dir> # present via SDL2 (n
## What it shows

- **`InfiniteCanvas`** — diagrams placed at positions in an unbounded virtual
space. Each is rendered once to an RGBA raster (cached). On the Apple/CoreGraphics
space. Each is rendered once per zoom level to an RGBA raster (cached). On the Apple/CoreGraphics
path, zooming re-rasterizes for crispness rather than scaling a bitmap; on the
Linux/Silica path `silicaRaster` returns the natural render size and ignores the
zoom-scaled width, so zoomed cards are repositioned but not re-rasterized larger.
Expand Down
Loading