2026-06-02 23:28:39 +02:00
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
2026-06-07 11:42:44 +02:00
|
|
|
import 'package:ocideck/models/chart.dart';
|
2026-06-02 23:28:39 +02:00
|
|
|
import 'package:ocideck/models/deck.dart';
|
|
|
|
|
import 'package:ocideck/models/settings.dart';
|
|
|
|
|
import 'package:ocideck/models/slide.dart';
|
|
|
|
|
import 'package:ocideck/services/markdown_service.dart';
|
|
|
|
|
|
|
|
|
|
/// Serialize a single slide to markdown and parse it straight back.
|
|
|
|
|
Slide _roundTrip(Slide slide) {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(Deck(title: 'Demo', slides: [slide]));
|
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
|
expect(deck, isNotNull, reason: 'parseDeck returned null for:\n$markdown');
|
|
|
|
|
expect(
|
|
|
|
|
deck!.slides,
|
|
|
|
|
hasLength(1),
|
|
|
|
|
reason: 'Expected exactly one slide from:\n$markdown',
|
|
|
|
|
);
|
|
|
|
|
return deck.slides.single;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
group('markdown round-trip per slide type', () {
|
|
|
|
|
test('title slide keeps title, subtitle, image and size', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.title).copyWith(
|
|
|
|
|
title: 'Welkom',
|
|
|
|
|
subtitle: 'Een ondertitel',
|
|
|
|
|
imagePath: 'images/cover.png',
|
|
|
|
|
imageSize: 80,
|
|
|
|
|
imageCaption: 'Bron: archief',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.title);
|
|
|
|
|
expect(out.title, 'Welkom');
|
|
|
|
|
expect(out.subtitle, 'Een ondertitel');
|
|
|
|
|
expect(out.imagePath, 'images/cover.png');
|
|
|
|
|
expect(out.imageSize, 80);
|
|
|
|
|
expect(out.imageCaption, 'Bron: archief');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('section slide keeps title and subtitle', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(
|
|
|
|
|
SlideType.section,
|
|
|
|
|
).copyWith(title: 'Hoofdstuk 1', subtitle: 'De inleiding'),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.section);
|
|
|
|
|
expect(out.title, 'Hoofdstuk 1');
|
|
|
|
|
expect(out.subtitle, 'De inleiding');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('bullets slide keeps title and nested bullets', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(
|
|
|
|
|
title: 'Agenda',
|
|
|
|
|
bullets: [
|
|
|
|
|
'Punt een',
|
|
|
|
|
'\tSubpunt',
|
|
|
|
|
'\t\tDiep subpunt',
|
|
|
|
|
'\t\t\tNog dieper',
|
|
|
|
|
'\t\t\t\tExtra dieper',
|
|
|
|
|
'Punt twee',
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.bullets);
|
|
|
|
|
expect(out.title, 'Agenda');
|
|
|
|
|
expect(out.bullets, [
|
|
|
|
|
'Punt een',
|
|
|
|
|
'\tSubpunt',
|
|
|
|
|
'\t\tDiep subpunt',
|
|
|
|
|
'\t\t\tNog dieper',
|
|
|
|
|
'\t\t\t\tExtra dieper',
|
|
|
|
|
'Punt twee',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-08 21:48:06 +02:00
|
|
|
test('bullets slide keeps an optional subheading', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(
|
|
|
|
|
title: 'Agenda',
|
|
|
|
|
subtitle: 'Vandaag',
|
|
|
|
|
bullets: ['Punt een', 'Punt twee'],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.bullets);
|
|
|
|
|
expect(out.title, 'Agenda');
|
|
|
|
|
expect(out.subtitle, 'Vandaag');
|
|
|
|
|
expect(out.bullets, ['Punt een', 'Punt twee']);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-09 13:28:23 +02:00
|
|
|
test('numbered list style round-trips', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
slides: [
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(
|
|
|
|
|
title: 'Stappen',
|
|
|
|
|
bullets: ['Eerst', '\tDetail', 'Daarna'],
|
|
|
|
|
listStyle: ListStyle.numbered,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
final out = service.parseDeck(markdown)!.slides.single;
|
|
|
|
|
expect(out.listStyle, ListStyle.numbered);
|
|
|
|
|
expect(out.bullets, ['Eerst', '\tDetail', 'Daarna']);
|
|
|
|
|
expect(markdown, contains('1. Eerst'));
|
|
|
|
|
expect(markdown, contains('2. Daarna'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('checklist style and checked items round-trip', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
slides: [
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(
|
|
|
|
|
title: 'Taken',
|
|
|
|
|
bullets: ['[x] Gedaan', '[ ] Nog doen', '\t[x] Subtaak'],
|
|
|
|
|
listStyle: ListStyle.checklist,
|
|
|
|
|
showChecklistProgress: true,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
final out = service.parseDeck(markdown)!.slides.single;
|
|
|
|
|
expect(out.listStyle, ListStyle.checklist);
|
|
|
|
|
expect(out.showChecklistProgress, isTrue);
|
|
|
|
|
expect(out.bullets, ['[x] Gedaan', '[ ] Nog doen', '\t[x] Subtaak']);
|
|
|
|
|
expect(markdown, contains('- [x] Gedaan'));
|
|
|
|
|
expect(markdown, contains('- [ ] Nog doen'));
|
|
|
|
|
expect(markdown, contains('ocideck_checklist_progress: true'));
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
test('twoBullets slide keeps both bullet columns', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.twoBullets).copyWith(
|
|
|
|
|
title: 'Vergelijking',
|
|
|
|
|
bullets: ['Links punt', '\tLinks subpunt'],
|
|
|
|
|
bullets2: ['Rechts punt', '\t\tRechts diep'],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.twoBullets);
|
|
|
|
|
expect(out.title, 'Vergelijking');
|
|
|
|
|
expect(out.bullets, ['Links punt', '\tLinks subpunt']);
|
|
|
|
|
expect(out.bullets2, ['Rechts punt', '\t\tRechts diep']);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-08 21:48:06 +02:00
|
|
|
test('twoBullets slide keeps optional column headings', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.twoBullets).copyWith(
|
|
|
|
|
title: 'Vergelijking',
|
|
|
|
|
columnTitle1: 'Voordelen',
|
|
|
|
|
columnTitle2: 'Nadelen',
|
|
|
|
|
bullets: ['Snel'],
|
|
|
|
|
bullets2: ['Duur'],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.twoBullets);
|
|
|
|
|
expect(out.columnTitle1, 'Voordelen');
|
|
|
|
|
expect(out.columnTitle2, 'Nadelen');
|
|
|
|
|
expect(out.bullets, ['Snel']);
|
|
|
|
|
expect(out.bullets2, ['Duur']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('twoBullets without headings stays empty (no spurious comments)', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final md = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
slides: [
|
2026-06-09 13:28:23 +02:00
|
|
|
Slide.create(
|
|
|
|
|
SlideType.twoBullets,
|
|
|
|
|
).copyWith(bullets: ['A'], bullets2: ['B']),
|
2026-06-08 21:48:06 +02:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(md, isNot(contains('ocideck_two_bullets_left_title')));
|
|
|
|
|
final out = service.parseDeck(md)!.slides.single;
|
|
|
|
|
expect(out.columnTitle1, '');
|
|
|
|
|
expect(out.columnTitle2, '');
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-09 13:28:23 +02:00
|
|
|
test('two-column checklist export respects disabled strike-through', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
themeProfile: const ThemeProfile(checklistStrikeThrough: false),
|
|
|
|
|
slides: [
|
|
|
|
|
Slide.create(SlideType.twoBullets).copyWith(
|
|
|
|
|
bullets: ['[x] Klaar'],
|
|
|
|
|
bullets2: ['[ ] Open'],
|
|
|
|
|
listStyle: ListStyle.checklist,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(markdown, isNot(contains('text-decoration:line-through')));
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
test('bulletsImage slide keeps bullets, image, size and caption', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.bulletsImage).copyWith(
|
|
|
|
|
title: 'Profiel',
|
|
|
|
|
bullets: ['Eerste punt', '\tGenest punt'],
|
|
|
|
|
imagePath: 'images/portret.png',
|
|
|
|
|
imageSize: 45,
|
|
|
|
|
imageCaption: 'Een onderschrift',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.bulletsImage);
|
|
|
|
|
expect(out.title, 'Profiel');
|
|
|
|
|
expect(out.bullets, ['Eerste punt', '\tGenest punt']);
|
|
|
|
|
expect(out.imagePath, 'images/portret.png');
|
|
|
|
|
expect(out.imageSize, 45);
|
|
|
|
|
expect(out.imageCaption, 'Een onderschrift');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('twoImages slide keeps both images, split and captions', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.twoImages).copyWith(
|
|
|
|
|
title: 'Voor en na',
|
|
|
|
|
imagePath: 'images/a.png',
|
|
|
|
|
imagePath2: 'images/b.png',
|
|
|
|
|
imageSize: 60,
|
|
|
|
|
imageCaption: 'Links',
|
|
|
|
|
imageCaption2: 'Rechts',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.twoImages);
|
|
|
|
|
expect(out.title, 'Voor en na');
|
|
|
|
|
expect(out.imagePath, 'images/a.png');
|
|
|
|
|
expect(out.imagePath2, 'images/b.png');
|
|
|
|
|
expect(out.imageSize, 60);
|
|
|
|
|
expect(out.imageCaption, 'Links');
|
|
|
|
|
expect(out.imageCaption2, 'Rechts');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('image slide keeps title, image and size', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.image).copyWith(
|
|
|
|
|
title: 'Overzicht',
|
|
|
|
|
imagePath: 'images/big.png',
|
|
|
|
|
imageSize: 70,
|
|
|
|
|
imageCaption: 'Foto',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.image);
|
|
|
|
|
expect(out.title, 'Overzicht');
|
|
|
|
|
expect(out.imagePath, 'images/big.png');
|
|
|
|
|
expect(out.imageSize, 70);
|
|
|
|
|
expect(out.imageCaption, 'Foto');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('video slide keeps video and audio with autoplay flags', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.video).copyWith(
|
|
|
|
|
title: 'Film',
|
|
|
|
|
videoPath: 'media/movie.mp4',
|
|
|
|
|
videoAutoplay: true,
|
|
|
|
|
audioPath: 'media/narration.mp3',
|
|
|
|
|
audioAutoplay: true,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.video);
|
|
|
|
|
expect(out.title, 'Film');
|
|
|
|
|
expect(out.videoPath, 'media/movie.mp4');
|
|
|
|
|
expect(out.videoAutoplay, isTrue);
|
|
|
|
|
expect(out.audioPath, 'media/narration.mp3');
|
|
|
|
|
expect(out.audioAutoplay, isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('quote slide keeps quote, author and background image', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.quote).copyWith(
|
|
|
|
|
quote: 'Kennis is macht',
|
|
|
|
|
quoteAuthor: 'Francis Bacon',
|
|
|
|
|
imagePath: 'images/bg.png',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.quote);
|
|
|
|
|
expect(out.quote, 'Kennis is macht');
|
|
|
|
|
expect(out.quoteAuthor, 'Francis Bacon');
|
|
|
|
|
expect(out.imagePath, 'images/bg.png');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('table slide keeps title, header and rows', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.table).copyWith(
|
|
|
|
|
title: 'Vergelijking',
|
|
|
|
|
tableRows: [
|
|
|
|
|
['Functie', 'Gratis', 'Pro'],
|
|
|
|
|
['Export', 'Nee', 'Ja'],
|
|
|
|
|
['Support', 'Email', '24/7'],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.table);
|
|
|
|
|
expect(out.title, 'Vergelijking');
|
|
|
|
|
expect(out.tableRows, [
|
|
|
|
|
['Functie', 'Gratis', 'Pro'],
|
|
|
|
|
['Export', 'Nee', 'Ja'],
|
|
|
|
|
['Support', 'Email', '24/7'],
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('table slide escapes pipes and newlines in cells', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.table).copyWith(
|
|
|
|
|
title: 'Speciale tekens',
|
|
|
|
|
tableRows: [
|
|
|
|
|
['Kolom A', 'Kolom B'],
|
|
|
|
|
['waarde | met pipe', 'regel een\nregel twee'],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.table);
|
|
|
|
|
expect(out.tableRows, [
|
|
|
|
|
['Kolom A', 'Kolom B'],
|
|
|
|
|
['waarde | met pipe', 'regel een\nregel twee'],
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a new table slide starts with empty cells (nothing to delete)', () {
|
|
|
|
|
final slide = Slide.create(SlideType.table);
|
|
|
|
|
expect(
|
|
|
|
|
slide.tableRows.every((row) => row.every((c) => c.isEmpty)),
|
|
|
|
|
isTrue,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('table slide stays a table even with empty headers', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.table).copyWith(
|
|
|
|
|
tableRows: const [
|
|
|
|
|
['', ''],
|
|
|
|
|
['waarde', ''],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.table);
|
|
|
|
|
expect(out.tableRows.first, ['', '']);
|
|
|
|
|
expect(out.tableRows[1].first, 'waarde');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('free markdown slide keeps its raw content', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.freeMarkdown).copyWith(
|
|
|
|
|
customMarkdown: 'Vrije tekst met **opmaak**.\n\nTweede alinea.',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.freeMarkdown);
|
|
|
|
|
expect(
|
|
|
|
|
out.customMarkdown.trim(),
|
|
|
|
|
'Vrije tekst met **opmaak**.\n\nTweede alinea.',
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-06-06 20:41:24 +02:00
|
|
|
|
|
|
|
|
test('code slide keeps title, language and code body', () {
|
|
|
|
|
const code = 'void main() {\n print("Hallo");\n}';
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.code).copyWith(
|
|
|
|
|
title: 'Voorbeeld',
|
|
|
|
|
codeLanguage: 'dart',
|
|
|
|
|
customMarkdown: code,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.code);
|
|
|
|
|
expect(out.title, 'Voorbeeld');
|
|
|
|
|
expect(out.codeLanguage, 'dart');
|
|
|
|
|
expect(out.customMarkdown, code);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-07 11:42:44 +02:00
|
|
|
test('chart slide keeps its inline spec', () {
|
|
|
|
|
const block =
|
|
|
|
|
'{\n "type": "bar",\n "title": "Omzet",\n "x": ["Q1","Q2"],\n'
|
|
|
|
|
' "series": [\n {"name":"2025","data":[10,14]}\n ]\n}';
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.chart).copyWith(customMarkdown: block),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.chart);
|
|
|
|
|
final spec = ChartSpec.parse(out.customMarkdown);
|
|
|
|
|
expect(spec.type, ChartType.bar);
|
|
|
|
|
expect(spec.x, ['Q1', 'Q2']);
|
|
|
|
|
expect(spec.series.single.data, [10, 14]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('chart slide with a source keeps only the reference in markdown', () {
|
Add project docs, EUPL licence, and open-source licence check
Documentation & licensing:
- Add the EUPL-1.2 licence (LICENSE.md) and set the project licence; refresh
the README (name origin wink, updated feature list, documentation index).
- Add CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, CHANGELOG, AUTHORS, and
THIRD_PARTY_NOTICES, plus docs/ (ARCHITECTURE, BUILD, USER_GUIDE, SHORTCUTS,
LICENSE_COMPLIANCE) and .github/ (CI workflow, issue/PR templates).
- Bring docs/FILE_FORMAT.md in line with current behaviour (code & chart
slides, per-slide TLP comment, annotation .ink.json sidecar, chart data/ CSVs).
Open-source compliance:
- Add tool/check_licenses.dart and a `make licenses` target (wired into
check-full and CI) that verifies every resolved dependency uses a recognised
open-source licence. A scan of all 151 packages and bundled assets found only
OSI-approved licences.
Charts (Fase 1.1):
- Replace the chart CSV textarea with an in-app editable data grid (editable
series/labels/values, add/remove row & column, read-only when linked).
- Centralize the linked-CSV directory name (`data/`) in a shared constant.
Also normalize formatting repo-wide with `dart format` and fix one
curly-braces lint, so `make check` and CI are green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:19:56 +02:00
|
|
|
const block =
|
|
|
|
|
'{"type":"line","source":"data/omzet.csv",'
|
2026-06-07 11:42:44 +02:00
|
|
|
'"x":["Q1"],"series":[{"name":"2025","data":[10]}]}';
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final md = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
Add project docs, EUPL licence, and open-source licence check
Documentation & licensing:
- Add the EUPL-1.2 licence (LICENSE.md) and set the project licence; refresh
the README (name origin wink, updated feature list, documentation index).
- Add CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, CHANGELOG, AUTHORS, and
THIRD_PARTY_NOTICES, plus docs/ (ARCHITECTURE, BUILD, USER_GUIDE, SHORTCUTS,
LICENSE_COMPLIANCE) and .github/ (CI workflow, issue/PR templates).
- Bring docs/FILE_FORMAT.md in line with current behaviour (code & chart
slides, per-slide TLP comment, annotation .ink.json sidecar, chart data/ CSVs).
Open-source compliance:
- Add tool/check_licenses.dart and a `make licenses` target (wired into
check-full and CI) that verifies every resolved dependency uses a recognised
open-source licence. A scan of all 151 packages and bundled assets found only
OSI-approved licences.
Charts (Fase 1.1):
- Replace the chart CSV textarea with an in-app editable data grid (editable
series/labels/values, add/remove row & column, read-only when linked).
- Centralize the linked-CSV directory name (`data/`) in a shared constant.
Also normalize formatting repo-wide with `dart format` and fix one
curly-braces lint, so `make check` and CI are green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:19:56 +02:00
|
|
|
slides: [
|
|
|
|
|
Slide.create(SlideType.chart).copyWith(customMarkdown: block),
|
|
|
|
|
],
|
2026-06-07 11:42:44 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
// The stored markdown references the CSV but does not inline the data.
|
|
|
|
|
expect(md.contains('data/omzet.csv'), isTrue);
|
|
|
|
|
final out = service.parseDeck(md)!.slides.single;
|
|
|
|
|
final spec = ChartSpec.parse(out.customMarkdown);
|
|
|
|
|
expect(spec.source, 'data/omzet.csv');
|
|
|
|
|
expect(spec.hasInlineData, isFalse);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-06 20:41:24 +02:00
|
|
|
test('code slide without a language stays plain code', () {
|
|
|
|
|
const code = 'GET /api/v1/status HTTP/1.1\nHost: example.org';
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.code).copyWith(customMarkdown: code),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.code);
|
|
|
|
|
expect(out.codeLanguage, '');
|
|
|
|
|
expect(out.customMarkdown, code);
|
|
|
|
|
});
|
2026-06-02 23:28:39 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
group('markdown round-trip cross-cutting fields', () {
|
|
|
|
|
test('keeps speaker notes and advance duration', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(
|
|
|
|
|
title: 'Met notities',
|
|
|
|
|
bullets: ['Een punt'],
|
|
|
|
|
notes: 'Vergeet de demo niet te tonen.',
|
|
|
|
|
advanceDuration: 2.5,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.notes, 'Vergeet de demo niet te tonen.');
|
|
|
|
|
expect(out.advanceDuration, 2.5);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('keeps the skipped flag and does not leak it into notes', () {
|
|
|
|
|
final skipped = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(
|
|
|
|
|
title: 'Backup-slide',
|
|
|
|
|
bullets: ['Alleen indien nodig'],
|
|
|
|
|
notes: 'Reserve',
|
|
|
|
|
skipped: true,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(skipped.skipped, isTrue);
|
|
|
|
|
expect(skipped.notes, 'Reserve'); // <!-- skip --> mag geen notitie worden
|
|
|
|
|
|
|
|
|
|
final normal = _roundTrip(
|
|
|
|
|
Slide.create(
|
|
|
|
|
SlideType.bullets,
|
|
|
|
|
).copyWith(title: 'Gewoon', bullets: ['Punt']),
|
|
|
|
|
);
|
|
|
|
|
expect(normal.skipped, isFalse);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-06 22:34:42 +02:00
|
|
|
test('keeps the per-slide TLP classification', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(
|
|
|
|
|
SlideType.bullets,
|
|
|
|
|
).copyWith(title: 'Gevoelig', bullets: ['Geheim'], tlp: TlpLevel.amber),
|
|
|
|
|
);
|
|
|
|
|
expect(out.tlp, TlpLevel.amber);
|
|
|
|
|
|
|
|
|
|
final none = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(bullets: ['Open']),
|
|
|
|
|
);
|
|
|
|
|
expect(none.tlp, TlpLevel.none);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('keeps the per-slide TLP on a code slide', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.code).copyWith(
|
|
|
|
|
customMarkdown: 'secret_key = 42',
|
|
|
|
|
codeLanguage: 'python',
|
|
|
|
|
tlp: TlpLevel.red,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(out.type, SlideType.code);
|
|
|
|
|
expect(out.tlp, TlpLevel.red);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
test('keeps general presentation metadata in the front matter', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
author: 'Jan Jansen',
|
|
|
|
|
organization: 'Vigilis',
|
|
|
|
|
version: '1.2',
|
|
|
|
|
date: '2026-05-30',
|
|
|
|
|
description: 'Een korte: omschrijving met dubbele punt',
|
|
|
|
|
keywords: 'kwartaal, cijfers, 2026',
|
|
|
|
|
tlp: TlpLevel.amberStrict,
|
|
|
|
|
slides: [Slide.create(SlideType.title).copyWith(title: 'Een')],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
|
expect(deck, isNotNull);
|
2026-06-08 21:48:06 +02:00
|
|
|
expect(deck!.title, 'Demo');
|
|
|
|
|
expect(deck.author, 'Jan Jansen');
|
2026-06-02 23:28:39 +02:00
|
|
|
expect(deck.organization, 'Vigilis');
|
|
|
|
|
expect(deck.version, '1.2');
|
|
|
|
|
expect(deck.date, '2026-05-30');
|
|
|
|
|
expect(deck.description, 'Een korte: omschrijving met dubbele punt');
|
|
|
|
|
expect(deck.keywords, 'kwartaal, cijfers, 2026');
|
|
|
|
|
expect(deck.tlp, TlpLevel.amberStrict);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('TLP none is omitted from the front matter and round-trips', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
slides: [Slide.create(SlideType.title).copyWith(title: 'Een')],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(markdown.contains('tlp:'), isFalse);
|
|
|
|
|
expect(service.parseDeck(markdown)!.tlp, TlpLevel.none);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('per-slide logo visibility round-trips when a logo is configured', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
themeProfile: const ThemeProfile(logoPath: '/tmp/logo.png'),
|
|
|
|
|
slides: [
|
|
|
|
|
Slide.create(
|
|
|
|
|
SlideType.bullets,
|
|
|
|
|
).copyWith(title: 'Met logo', bullets: ['a']),
|
|
|
|
|
Slide.create(
|
|
|
|
|
SlideType.bullets,
|
|
|
|
|
).copyWith(title: 'Zonder logo', bullets: ['b'], showLogo: false),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
|
expect(deck, isNotNull);
|
|
|
|
|
expect(deck!.slides[0].showLogo, isTrue);
|
|
|
|
|
expect(deck.slides[1].showLogo, isFalse);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('per-slide footer visibility round-trips', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
themeProfile: const ThemeProfile(
|
|
|
|
|
footerText: 'Vertrouwelijk',
|
|
|
|
|
footerPosition: 'center',
|
|
|
|
|
),
|
|
|
|
|
slides: [
|
|
|
|
|
Slide.create(
|
|
|
|
|
SlideType.bullets,
|
|
|
|
|
).copyWith(title: 'Met footer', bullets: ['a']),
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(
|
|
|
|
|
title: 'Zonder footer',
|
|
|
|
|
bullets: ['b'],
|
|
|
|
|
showFooter: false,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
|
expect(deck, isNotNull);
|
2026-06-11 19:25:05 +02:00
|
|
|
expect(deck!.slides[0].showFooter, isTrue);
|
2026-06-02 23:28:39 +02:00
|
|
|
expect(deck.slides[1].showFooter, isFalse);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('slides default to showing the logo', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(title: 'x', bullets: ['y']),
|
|
|
|
|
);
|
|
|
|
|
expect(out.showLogo, isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('slides default to showing the footer', () {
|
|
|
|
|
final out = _roundTrip(
|
|
|
|
|
Slide.create(SlideType.bullets).copyWith(title: 'x', bullets: ['y']),
|
|
|
|
|
);
|
|
|
|
|
expect(out.showFooter, isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('preserves slide order and count for a multi-slide deck', () {
|
|
|
|
|
final service = MarkdownService();
|
|
|
|
|
final markdown = service.generateDeck(
|
|
|
|
|
Deck(
|
|
|
|
|
title: 'Demo',
|
|
|
|
|
slides: [
|
|
|
|
|
Slide.create(SlideType.title).copyWith(title: 'Een'),
|
|
|
|
|
Slide.create(
|
|
|
|
|
SlideType.bullets,
|
|
|
|
|
).copyWith(title: 'Twee', bullets: ['x']),
|
|
|
|
|
Slide.create(SlideType.bulletsImage).copyWith(
|
|
|
|
|
title: 'Drie',
|
|
|
|
|
bullets: ['y'],
|
|
|
|
|
imagePath: 'images/c.png',
|
|
|
|
|
),
|
|
|
|
|
Slide.create(SlideType.image).copyWith(imagePath: 'images/d.png'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
final deck = service.parseDeck(markdown);
|
|
|
|
|
expect(deck, isNotNull);
|
|
|
|
|
expect(deck!.slides.map((s) => s.type).toList(), [
|
|
|
|
|
SlideType.title,
|
|
|
|
|
SlideType.bullets,
|
|
|
|
|
SlideType.bulletsImage,
|
|
|
|
|
SlideType.image,
|
|
|
|
|
]);
|
|
|
|
|
expect(deck.slides[2].imagePath, 'images/c.png');
|
|
|
|
|
expect(deck.slides[3].imagePath, 'images/d.png');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|