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
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,9 @@ The north-star metric improved after onboarding changes.
SuperDeck uses GitHub-flavored Markdown plus custom builders:

- Headings, paragraphs, emphasis, lists, task lists, tables, blockquotes, links.
- Code blocks with highlighting for `dart`, `json`, `yaml`, `markdown`, `python`, and `mermaid`; unknown languages fall back to Dart highlighting.
- Code blocks with highlighting for `dart`, `json`, `yaml`, `markdown`, and
`python`; unknown languages fall back to Dart highlighting.
- Fenced `mermaid` blocks render supported diagrams directly in Flutter.
- GitHub alerts:

```markdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ Bare image keys with no scheme and no path separators, such as `slide-intro.png`

## Plugins

Use custom widgets for slide content that renders directly in Flutter. Use plugins when the capability needs shell actions or build-time transforms.
Use custom widgets for slide content that renders directly in Flutter. Use
plugins when the capability needs shell actions or build-time transforms.

Runtime plugin example: PDF export.

Expand All @@ -392,26 +393,22 @@ SuperDeckApp(
)
```

Build plugin example: Mermaid diagrams. Register `MermaidBuildPlugin` in a custom runner and run builds through that runner:
Mermaid diagrams render directly from fenced `mermaid` blocks. They do not
require a plugin, custom runner, browser, generated image, or cache:

```dart
import 'dart:io';
import 'package:superdeck_cli/runner.dart';
import 'package:superdeck_mermaid/superdeck_mermaid.dart';

Future<void> main(List<String> args) async {
final exitCode = await SuperDeckRunner(
plugins: [MermaidBuildPlugin()],
).run(args);
exit(exitCode);
}
```

```bash
dart run tool/superdeck.dart build --watch
````markdown
```mermaid
flowchart LR
Draft[Draft] --> Review{Ready?}
Review -->|Yes| Present[Present]
Review -->|No| Draft
```
````

Plain fenced `mermaid` blocks are just code blocks unless the Mermaid build plugin transforms them into image assets.
Supported diagrams are flowcharts, sequence diagrams, pie charts, Gantt
charts, timelines, Kanban boards, radar charts, and XY charts. Unsupported or
invalid syntax displays an inline error in the slide. The diagram background is
transparent, and its colors follow the app's light or dark Flutter theme.

## DartPad Sharing

Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,6 @@ jobs:
cd demo/e2e
npx playwright install --with-deps chromium webkit

- name: Run Mermaid browser integration
timeout-minutes: 5
run: |
cd packages/plugins/mermaid
SUPERDECK_RUN_BROWSER_TESTS=1 fvm dart test test/integration/mermaid_build_plugin_integration_test.dart

- name: Run Playwright smoke tests
timeout-minutes: 5
run: |
Expand Down
199 changes: 9 additions & 190 deletions demo/integration_test/plugin_visual_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:superdeck/superdeck.dart';
import 'package:superdeck_builder/superdeck_builder.dart';
import 'package:superdeck_core/superdeck_core.dart';
import 'package:superdeck_mermaid/superdeck_mermaid.dart';
import 'package:superdeck_pdf/superdeck_pdf.dart';

import 'helpers/test_helpers.dart';
Expand All @@ -21,59 +17,22 @@ void main() {
group('plugin visual coverage', () {
setUpAll(TestApp.initialize);

testWidgets('Mermaid build plugin renders in the deck runtime', (
testWidgets('built-in Mermaid renderer paints in the deck runtime', (
tester,
) async {
_setReviewViewport(tester);
final fixtureRoot =
'build/integration_fixtures/plugin_visual/'
'mermaid_${DateTime.now().microsecondsSinceEpoch}';
final fixtureDir = Directory(fixtureRoot);
await fixtureDir.create(recursive: true);

final workspace = DeckWorkspace(
projectDir: Directory.current.path,
slidesPath: '$fixtureRoot/slides.md',
outputDir: '$fixtureRoot/.superdeck',
);

final builder = DeckBuilder(
workspace: workspace,
store: DeckBuildStore(workspace: workspace),
plugins: [MermaidBuildPlugin(generator: _FakeMermaidGenerator())],
);
FileDeckLoader? loader;
addTearDown(() async {
await tester.unmountTestApp();
await loader?.dispose();
await builder.dispose();
if (await fixtureDir.exists()) {
await fixtureDir.delete(recursive: true);
}
});

await workspace.slidesFile.writeAsString('''
# Plugin Diagram
final controller = await tester.pumpTestAppWithSlides([
makeSlide('mermaid-runtime', '''
# Runtime Diagram

```mermaid
graph TD
Markdown[slides.md] --> Plugin[MermaidBuildPlugin]
Plugin --> Asset[PNG asset]
Asset --> Runtime[SuperDeck runtime]
Draft[Draft slides] --> Review{Ready?}
Review -->|Yes| Present[Present]
Review -->|No| Draft
```
''');
await builder.build();
final generatedImage = await _singleGeneratedMermaidImage(workspace);
await _expectPngDimensions(generatedImage, width: 640, height: 360);
await _expectPngHasTransparency(generatedImage);

final fileLoader = FileDeckLoader(workspace: workspace);
loader = fileLoader;

final controller = await tester.pumpTestAppWithLoader(
fileLoader,
workspace: workspace,
);
'''),
]);

expect(controller.presentation.totalSlides.value, 1);
assertPresentationHealthy(tester, controller);
Expand Down Expand Up @@ -194,68 +153,6 @@ void _setReviewViewport(WidgetTester tester) {
});
}

class _FakeMermaidGenerator extends MermaidGenerator {
@override
Future<Uint8List> render(String syntax) async {
return _diagramPngBytes();
}

@override
Future<void> dispose() async {}
}

Future<File> _singleGeneratedMermaidImage(DeckWorkspace workspace) async {
final mermaidDir = Directory('${workspace.outputDir}/mermaid');
final images = await mermaidDir
.list()
.where((entity) => entity is File && entity.path.endsWith('.png'))
.cast<File>()
.toList();

expect(images, hasLength(1));

return images.single;
}

Future<void> _expectPngDimensions(
File file, {
required int width,
required int height,
}) async {
final codec = await ui.instantiateImageCodec(await file.readAsBytes());
final frame = await codec.getNextFrame();
final image = frame.image;

try {
expect(image.width, width);
expect(image.height, height);
} finally {
image.dispose();
codec.dispose();
}
}

Future<void> _expectPngHasTransparency(File file) async {
final codec = await ui.instantiateImageCodec(await file.readAsBytes());
final frame = await codec.getNextFrame();
final image = frame.image;

try {
final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
expect(bytes, isNotNull);

final pixels = bytes!.buffer.asUint8List();
final hasTransparentPixel = Iterable<int>.generate(
pixels.length ~/ 4,
).any((pixel) => pixels[pixel * 4 + 3] < 255);

expect(hasTransparentPixel, isTrue);
} finally {
image.dispose();
codec.dispose();
}
}

Future<void> _writePdfExportForReview(
Directory? outputDir,
Uint8List? pdf, {
Expand Down Expand Up @@ -290,81 +187,3 @@ void _writePdfExportArtifactIfRequested(Uint8List pdf) {
file.parent.createSync(recursive: true);
file.writeAsBytesSync(pdf, flush: true);
}

Future<Uint8List> _diagramPngBytes() async {
const width = 640.0;
const height = 360.0;
const strokeWidth = 2.0;
const arrowHeadLength = 12.0;
const arrowHeadRise = 8.0;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder, const Rect.fromLTWH(0, 0, width, height));

final nodePaint = Paint()..color = const Color(0xffececff);
final strokePaint = Paint()
..color = const Color(0xff9370db)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
final arrowPaint = Paint()
..color = const Color(0xff333333)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;

void drawNode(Rect rect, String label) {
canvas
..drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(18)),
nodePaint,
)
..drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(18)),
strokePaint,
);

final painter = TextPainter(
text: TextSpan(
text: label,
style: const TextStyle(
color: Color(0xff333333),
fontSize: 23,
fontWeight: FontWeight.w700,
),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
)..layout(maxWidth: rect.width - 28);

painter.paint(
canvas,
Offset(
rect.left + (rect.width - painter.width) / 2,
rect.top + (rect.height - painter.height) / 2,
),
);
}

void drawArrow(Offset start, Offset end) {
canvas.drawLine(start, end, arrowPaint);
final head = Path()
..moveTo(end.dx, end.dy)
..lineTo(end.dx - arrowHeadLength, end.dy - arrowHeadRise)
..moveTo(end.dx, end.dy)
..lineTo(end.dx - arrowHeadLength, end.dy + arrowHeadRise);
canvas.drawPath(head, arrowPaint);
}

drawNode(const Rect.fromLTWH(40, 130, 160, 92), 'slides.md');
drawNode(const Rect.fromLTWH(240, 130, 160, 92), 'Mermaid\nplugin');
drawNode(const Rect.fromLTWH(440, 130, 160, 92), 'PNG asset');
drawArrow(const Offset(200, 176), const Offset(240, 176));
drawArrow(const Offset(400, 176), const Offset(440, 176));

final picture = recorder.endRecording();
final image = await picture.toImage(width.toInt(), height.toInt());
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
image.dispose();
picture.dispose();

return byteData!.buffer.asUint8List();
}
1 change: 0 additions & 1 deletion demo/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ dev_dependencies:
ack_generator: 1.0.1
superdeck_builder: ^1.0.0
superdeck_cli: ^1.0.0
superdeck_mermaid: ^1.0.0
superdeck_pdf: ^1.0.0
flutter:
uses-material-design: true
Expand Down
Loading
Loading