Add self-contained Marp HTML export
New export target: a single offline .html rendered from the deck's Marp
Markdown. Bundles (inlines) marked, highlight.js, MathJax (tex-svg, no font
files) and mermaid, so code highlighting, LaTeX math and mermaid diagrams all
render in any browser with no network access.
- MarpHtmlService splits the deck on `---`, strips front-matter, and inlines
the vendored libraries (assets/web_export/) with a </script> breakout guard.
The asset loader is injectable for testing.
- ExportFormat.html wired through ExportService (no rasterization needed),
the export dialog (new button, skips slide rendering) and app_shell
(passes the generated Markdown). Export dialog is now scrollable.
Note: rendered with marked, not Marp Core, so theme fidelity differs from the
in-app preview / PDF / PPTX; the win is a portable, dependency-free deck.
Tests: slide splitting, library inlining, breakout escaping, and an
end-to-end .html export.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 01:37:46 +02:00
|
|
|
import 'dart:io';
|
2026-06-04 02:30:03 +02:00
|
|
|
import 'dart:typed_data';
|
Add self-contained Marp HTML export
New export target: a single offline .html rendered from the deck's Marp
Markdown. Bundles (inlines) marked, highlight.js, MathJax (tex-svg, no font
files) and mermaid, so code highlighting, LaTeX math and mermaid diagrams all
render in any browser with no network access.
- MarpHtmlService splits the deck on `---`, strips front-matter, and inlines
the vendored libraries (assets/web_export/) with a </script> breakout guard.
The asset loader is injectable for testing.
- ExportFormat.html wired through ExportService (no rasterization needed),
the export dialog (new button, skips slide rendering) and app_shell
(passes the generated Markdown). Export dialog is now scrollable.
Note: rendered with marked, not Marp Core, so theme fidelity differs from the
in-app preview / PDF / PPTX; the win is a portable, dependency-free deck.
Tests: slide splitting, library inlining, breakout escaping, and an
end-to-end .html export.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 01:37:46 +02:00
|
|
|
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
2026-06-04 02:30:03 +02:00
|
|
|
import 'package:ocideck/models/settings.dart';
|
Add self-contained Marp HTML export
New export target: a single offline .html rendered from the deck's Marp
Markdown. Bundles (inlines) marked, highlight.js, MathJax (tex-svg, no font
files) and mermaid, so code highlighting, LaTeX math and mermaid diagrams all
render in any browser with no network access.
- MarpHtmlService splits the deck on `---`, strips front-matter, and inlines
the vendored libraries (assets/web_export/) with a </script> breakout guard.
The asset loader is injectable for testing.
- ExportFormat.html wired through ExportService (no rasterization needed),
the export dialog (new button, skips slide rendering) and app_shell
(passes the generated Markdown). Export dialog is now scrollable.
Note: rendered with marked, not Marp Core, so theme fidelity differs from the
in-app preview / PDF / PPTX; the win is a portable, dependency-free deck.
Tests: slide splitting, library inlining, breakout escaping, and an
end-to-end .html export.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 01:37:46 +02:00
|
|
|
import 'package:ocideck/services/marp_html_service.dart';
|
|
|
|
|
|
|
|
|
|
/// Reads the vendored libraries straight from the repo (tests run at the root).
|
|
|
|
|
Future<String> _diskLoader(String asset) => File(asset).readAsString();
|
2026-06-04 02:30:03 +02:00
|
|
|
Future<Uint8List> _diskBytes(String asset) => File(asset).readAsBytes();
|
Add self-contained Marp HTML export
New export target: a single offline .html rendered from the deck's Marp
Markdown. Bundles (inlines) marked, highlight.js, MathJax (tex-svg, no font
files) and mermaid, so code highlighting, LaTeX math and mermaid diagrams all
render in any browser with no network access.
- MarpHtmlService splits the deck on `---`, strips front-matter, and inlines
the vendored libraries (assets/web_export/) with a </script> breakout guard.
The asset loader is injectable for testing.
- ExportFormat.html wired through ExportService (no rasterization needed),
the export dialog (new button, skips slide rendering) and app_shell
(passes the generated Markdown). Export dialog is now scrollable.
Note: rendered with marked, not Marp Core, so theme fidelity differs from the
in-app preview / PDF / PPTX; the win is a portable, dependency-free deck.
Tests: slide splitting, library inlining, breakout escaping, and an
end-to-end .html export.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 01:37:46 +02:00
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
group('marpSlides', () {
|
|
|
|
|
test('drops the YAML front-matter and splits on --- separators', () {
|
|
|
|
|
const md = '''
|
|
|
|
|
---
|
|
|
|
|
marp: true
|
|
|
|
|
theme: ocideck
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# Slide one
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Slide two
|
|
|
|
|
''';
|
|
|
|
|
final slides = MarpHtmlService.marpSlides(md);
|
|
|
|
|
expect(slides, hasLength(2));
|
|
|
|
|
expect(slides[0], contains('# Slide one'));
|
|
|
|
|
expect(slides[0], isNot(contains('marp: true')));
|
|
|
|
|
expect(slides[1], contains('## Slide two'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a deck without front-matter keeps every slide', () {
|
|
|
|
|
final slides = MarpHtmlService.marpSlides(
|
|
|
|
|
'# A\n\n---\n\n# B\n\n---\n\n# C',
|
|
|
|
|
);
|
|
|
|
|
expect(slides, hasLength(3));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('build() inlines the libraries and the slide content', () async {
|
|
|
|
|
final service = MarpHtmlService(loadAsset: _diskLoader);
|
|
|
|
|
const md = '''
|
|
|
|
|
---
|
|
|
|
|
marp: true
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# Titel
|
|
|
|
|
|
|
|
|
|
\$\$E=mc^2\$\$
|
|
|
|
|
|
|
|
|
|
```dart
|
|
|
|
|
void main() {}
|
|
|
|
|
```
|
|
|
|
|
''';
|
|
|
|
|
final html = await service.build(md);
|
|
|
|
|
|
|
|
|
|
expect(html, startsWith('<!doctype html>'));
|
|
|
|
|
// Slide payload is embedded for the in-browser renderer.
|
|
|
|
|
expect(html, contains('# Titel'));
|
|
|
|
|
expect(html, contains(r'E=mc^2'));
|
|
|
|
|
// Each engine is inlined (offline): marked, highlight.js, MathJax, mermaid.
|
|
|
|
|
expect(html, contains('marked'));
|
|
|
|
|
expect(html, contains('hljs'));
|
|
|
|
|
expect(html, contains('MathJax'));
|
|
|
|
|
expect(html, contains('mermaid'));
|
|
|
|
|
// Everything is inlined: there must be no external <script src=...> tags.
|
|
|
|
|
expect(html, isNot(contains('<script src')));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('build() neutralises a closing-script breakout in content', () async {
|
|
|
|
|
final service = MarpHtmlService(loadAsset: _diskLoader);
|
|
|
|
|
final html = await service.build('# X\n\nfoo </script> bar');
|
|
|
|
|
// The literal breakout must be escaped so it cannot terminate the payload.
|
|
|
|
|
expect(html, isNot(contains('foo </script> bar')));
|
|
|
|
|
expect(html, contains(r'<\/script'));
|
|
|
|
|
});
|
2026-06-04 02:30:03 +02:00
|
|
|
|
|
|
|
|
test('a theme colours the slides with the profile palette', () async {
|
|
|
|
|
final service = MarpHtmlService(
|
|
|
|
|
loadAsset: _diskLoader,
|
|
|
|
|
loadBytes: _diskBytes,
|
|
|
|
|
);
|
|
|
|
|
const theme = ThemeProfile(
|
|
|
|
|
slideBackgroundColor: '#102030',
|
|
|
|
|
textColor: '#EEF1F4',
|
|
|
|
|
accentColor: '#33CC99',
|
|
|
|
|
fontFamily: 'Arial',
|
|
|
|
|
);
|
|
|
|
|
final html = await service.build('# Titel', theme: theme);
|
|
|
|
|
|
|
|
|
|
expect(html, contains('background:#102030'));
|
|
|
|
|
expect(html, contains('color:#EEF1F4'));
|
|
|
|
|
expect(html, contains('#33CC99'));
|
|
|
|
|
expect(html, contains("'Arial'"));
|
|
|
|
|
// A system font is not embedded as base64.
|
|
|
|
|
expect(html, isNot(contains('data:font/ttf;base64,')));
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-08 13:51:29 +02:00
|
|
|
test('code blocks use the themed code colours in the export CSS', () async {
|
|
|
|
|
final service = MarpHtmlService(
|
|
|
|
|
loadAsset: _diskLoader,
|
|
|
|
|
loadBytes: _diskBytes,
|
|
|
|
|
);
|
|
|
|
|
const theme = ThemeProfile(
|
|
|
|
|
codeBackgroundColor: '#000000',
|
|
|
|
|
codeTextColor: '#33FF33',
|
|
|
|
|
);
|
|
|
|
|
final html = await service.build('```dart\nvoid main() {}\n```', theme: theme);
|
|
|
|
|
|
|
|
|
|
expect(html, contains('.slide pre{background:#000000;color:#33FF33'));
|
|
|
|
|
expect(html, contains('.slide pre code{color:#33FF33'));
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-04 02:30:03 +02:00
|
|
|
test('EB Garamond theme embeds the font for offline rendering', () async {
|
|
|
|
|
final service = MarpHtmlService(
|
|
|
|
|
loadAsset: _diskLoader,
|
|
|
|
|
loadBytes: _diskBytes,
|
|
|
|
|
);
|
|
|
|
|
const theme = ThemeProfile(fontFamily: 'EB Garamond');
|
|
|
|
|
final html = await service.build('# Titel', theme: theme);
|
|
|
|
|
|
|
|
|
|
expect(html, contains('@font-face'));
|
|
|
|
|
expect(html, contains('data:font/ttf;base64,'));
|
|
|
|
|
expect(html, contains("'EB Garamond'"));
|
|
|
|
|
});
|
2026-06-08 12:18:35 +02:00
|
|
|
|
|
|
|
|
test('pie chart SVG renders every series and label', () {
|
|
|
|
|
const slide = '''
|
|
|
|
|
```chart
|
|
|
|
|
{
|
|
|
|
|
"type": "pie",
|
|
|
|
|
"x": ["Team A", "Team B"],
|
|
|
|
|
"series": [
|
|
|
|
|
{"name": "Gereed", "color": "#10B981", "data": [70, 40]},
|
|
|
|
|
{"name": "Open", "color": "#EF4444", "data": [30, 60]}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
''';
|
|
|
|
|
|
|
|
|
|
final html = MarpHtmlService.renderChartBlocks(slide);
|
|
|
|
|
|
|
|
|
|
expect(html, contains('Team A'));
|
|
|
|
|
expect(html, contains('Team B'));
|
|
|
|
|
expect(html, contains('Gereed'));
|
|
|
|
|
expect(html, contains('Open'));
|
|
|
|
|
expect(html, contains('#003399'));
|
|
|
|
|
expect(html, contains('#FFCC00'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('pie chart SVG renders at most two series', () {
|
|
|
|
|
const slide = '''
|
|
|
|
|
```chart
|
|
|
|
|
{
|
|
|
|
|
"type": "pie",
|
|
|
|
|
"x": ["A", "B"],
|
|
|
|
|
"series": [
|
|
|
|
|
{"name": "Een", "data": [1, 2]},
|
|
|
|
|
{"name": "Twee", "data": [2, 3]},
|
|
|
|
|
{"name": "Drie", "data": [3, 4]}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
''';
|
|
|
|
|
|
|
|
|
|
final html = MarpHtmlService.renderChartBlocks(slide);
|
|
|
|
|
|
|
|
|
|
expect(html, contains('Een'));
|
|
|
|
|
expect(html, contains('Twee'));
|
|
|
|
|
expect(html, isNot(contains('Drie')));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('bar chart SVG draws optional min/max bound lines with labels', () {
|
|
|
|
|
const slide = '''
|
|
|
|
|
```chart
|
|
|
|
|
{
|
|
|
|
|
"type": "bar",
|
|
|
|
|
"x": ["Q1", "Q2"],
|
|
|
|
|
"series": [{"name": "Omzet", "data": [10, 14]}],
|
|
|
|
|
"minBound": 5,
|
|
|
|
|
"maxBound": 20
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
''';
|
|
|
|
|
|
|
|
|
|
final html = MarpHtmlService.renderChartBlocks(slide);
|
|
|
|
|
|
|
|
|
|
expect(html, contains('stroke-dasharray'));
|
|
|
|
|
expect(html, contains('min 5'));
|
|
|
|
|
expect(html, contains('max 20'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('pie chart SVG never draws bound lines', () {
|
|
|
|
|
const slide = '''
|
|
|
|
|
```chart
|
|
|
|
|
{
|
|
|
|
|
"type": "pie",
|
|
|
|
|
"x": ["A", "B"],
|
|
|
|
|
"series": [{"name": "Een", "data": [1, 2]}],
|
|
|
|
|
"minBound": 5,
|
|
|
|
|
"maxBound": 20
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
''';
|
|
|
|
|
|
|
|
|
|
final html = MarpHtmlService.renderChartBlocks(slide);
|
|
|
|
|
|
|
|
|
|
expect(html, isNot(contains('stroke-dasharray')));
|
|
|
|
|
expect(html, isNot(contains('min 5')));
|
|
|
|
|
});
|
2026-06-08 13:51:29 +02:00
|
|
|
|
|
|
|
|
test('radar chart SVG draws a polygon per series with axis labels', () {
|
|
|
|
|
const slide = '''
|
|
|
|
|
```chart
|
|
|
|
|
{
|
|
|
|
|
"type": "radar",
|
|
|
|
|
"x": ["Snelheid", "Kracht", "Uithouding"],
|
|
|
|
|
"series": [
|
|
|
|
|
{"name": "A", "color": "#2563EB", "data": [3, 4, 5]},
|
|
|
|
|
{"name": "B", "color": "#EF4444", "data": [5, 2, 3]}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
''';
|
|
|
|
|
|
|
|
|
|
final html = MarpHtmlService.renderChartBlocks(slide);
|
|
|
|
|
|
|
|
|
|
expect(html, contains('<polygon'));
|
|
|
|
|
expect(html, contains('Snelheid'));
|
|
|
|
|
expect(html, contains('Kracht'));
|
|
|
|
|
expect(html, contains('Uithouding'));
|
|
|
|
|
// Both series are drawn with their colours.
|
|
|
|
|
expect(html, contains('fill="#2563EB"'));
|
|
|
|
|
expect(html, contains('fill="#EF4444"'));
|
|
|
|
|
// The series legend is shown (not a pie legend).
|
|
|
|
|
expect(html, contains('A'));
|
|
|
|
|
expect(html, contains('B'));
|
|
|
|
|
});
|
Add self-contained Marp HTML export
New export target: a single offline .html rendered from the deck's Marp
Markdown. Bundles (inlines) marked, highlight.js, MathJax (tex-svg, no font
files) and mermaid, so code highlighting, LaTeX math and mermaid diagrams all
render in any browser with no network access.
- MarpHtmlService splits the deck on `---`, strips front-matter, and inlines
the vendored libraries (assets/web_export/) with a </script> breakout guard.
The asset loader is injectable for testing.
- ExportFormat.html wired through ExportService (no rasterization needed),
the export dialog (new button, skips slide rendering) and app_shell
(passes the generated Markdown). Export dialog is now scrollable.
Note: rendered with marked, not Marp Core, so theme fidelity differs from the
in-app preview / PDF / PPTX; the win is a portable, dependency-free deck.
Tests: slide splitting, library inlining, breakout escaping, and an
end-to-end .html export.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 01:37:46 +02:00
|
|
|
}
|