feat(draw): d2 diagram template — compile d2 source to SVG (T-494)
The d2 drawing template compiles a diagram's source to SVG through the d2 binary (resolved via the D-104 path layer), then paints it with the same renderer the svg card uses. `clide draw --file x.d2` infers the type from the extension; `.svg` files render directly. Template handlers now return a DrawResult so a compile failure or an unresolved d2 surface as an honest userError with an install hint, not a generic "no SVG". Real d2 0.7.1 verified end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ void main() {
|
||||
|
||||
test('template doc lowers via the registry before publishing', () async {
|
||||
wire();
|
||||
registry.register('d2', (doc) async => '<svg data-d2="${doc.fields['source']}"/>');
|
||||
registry.register('d2', (doc) async => DrawOk('<svg data-d2="${doc.fields['source']}"/>'));
|
||||
files['c.json'] = '{"template":"d2","source":"a -> b"}';
|
||||
final r = await draw('c.json');
|
||||
expect(r.ok, isTrue, reason: r.error?.message);
|
||||
@@ -56,6 +56,24 @@ void main() {
|
||||
expect(r.data['template'], 'd2');
|
||||
});
|
||||
|
||||
test('a .d2 file is wrapped as a d2 template and lowered (T-494)', () async {
|
||||
wire();
|
||||
registry.register('d2', (doc) async => DrawOk('<svg data-d2="${doc.fields['source']}"/>'));
|
||||
files['pipeline.d2'] = 'a -> b';
|
||||
final r = await draw('pipeline.d2');
|
||||
expect(r.ok, isTrue, reason: r.error?.message);
|
||||
expect(published.single.data['svg'], '<svg data-d2="a -> b"/>');
|
||||
expect(r.data['template'], 'd2');
|
||||
});
|
||||
|
||||
test('a .svg file renders as a primitive card (T-494)', () async {
|
||||
wire();
|
||||
files['logo.svg'] = '<svg id="raw"/>';
|
||||
final r = await draw('logo.svg');
|
||||
expect(r.ok, isTrue, reason: r.error?.message);
|
||||
expect(published.single.data['svg'], '<svg id="raw"/>');
|
||||
});
|
||||
|
||||
test('a missing file → notFound, nothing published', () async {
|
||||
wire();
|
||||
final r = await draw('nope.json');
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:clide/src/draw/d2_template.dart';
|
||||
import 'package:clide/src/draw/draw_dispatch.dart';
|
||||
import 'package:clide/src/draw/draw_doc.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
DrawingCardDoc d2doc(Object? source) => DrawingCardDoc(template: 'd2', fields: {'template': 'd2', if (source != null) 'source': source});
|
||||
|
||||
group('d2TemplateHandler', () {
|
||||
test('compiles the source field via the injected compiler', () async {
|
||||
final h = d2TemplateHandler(compile: (s) async => DrawOk('<svg data-s="$s"/>'));
|
||||
final r = await h(d2doc('a -> b'));
|
||||
expect((r as DrawOk).svg, '<svg data-s="a -> b"/>');
|
||||
});
|
||||
|
||||
test('a missing source is an honest error', () async {
|
||||
final h = d2TemplateHandler(compile: (_) async => const DrawOk('x'));
|
||||
expect(await h(d2doc(null)), isA<DrawErr>());
|
||||
});
|
||||
|
||||
test('a blank source is an honest error', () async {
|
||||
final h = d2TemplateHandler(compile: (_) async => const DrawOk('x'));
|
||||
expect(await h(d2doc(' ')), isA<DrawErr>());
|
||||
});
|
||||
|
||||
test('propagates a compiler error message', () async {
|
||||
final h = d2TemplateHandler(compile: (_) async => const DrawErr('boom'));
|
||||
expect((await h(d2doc('a -> b')) as DrawErr).message, 'boom');
|
||||
});
|
||||
});
|
||||
|
||||
group('d2CompileViaBinary', () {
|
||||
Future<DrawResult> compile({String? bin = '/x/d2', D2Run? run}) =>
|
||||
d2CompileViaBinary('a -> b', resolveD2: () => bin, run: run ?? (exe, src) async => (code: 0, out: '<svg/>', err: 'success:'));
|
||||
|
||||
test('unresolved d2 hints how to install', () async {
|
||||
expect((await compile(bin: null) as DrawErr).message, contains('d2 not found'));
|
||||
});
|
||||
|
||||
test('a clean compile returns the SVG (stderr ignored)', () async {
|
||||
expect((await compile() as DrawOk).svg, '<svg/>');
|
||||
});
|
||||
|
||||
test('a non-zero exit is a compile error carrying stderr', () async {
|
||||
final r = await compile(run: (exe, src) async => (code: 1, out: '', err: 'line 2: syntax error'));
|
||||
expect((r as DrawErr).message, contains('syntax error'));
|
||||
});
|
||||
|
||||
test('empty output is an error', () async {
|
||||
final r = await compile(run: (exe, src) async => (code: 0, out: ' ', err: ''));
|
||||
expect(r, isA<DrawErr>());
|
||||
});
|
||||
|
||||
test('a spawn failure is reported honestly', () async {
|
||||
final r = await compile(run: (exe, src) async => throw 'ENOENT');
|
||||
expect((r as DrawErr).message, contains('could not run d2'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -37,15 +37,15 @@ void main() {
|
||||
});
|
||||
|
||||
test('template: a registered handler lowers the doc to SVG', () async {
|
||||
final reg = DrawingRegistry()..register('d2', (doc) async => '<svg data-src="${doc.fields['source']}"/>');
|
||||
final reg = DrawingRegistry()..register('d2', (doc) async => DrawOk('<svg data-src="${doc.fields['source']}"/>'));
|
||||
final r = await resolve(parseDrawingCardDoc({'template': 'd2', 'source': 'a -> b'})!, reg);
|
||||
expect((r as DrawOk).svg, '<svg data-src="a -> b"/>');
|
||||
});
|
||||
|
||||
test('template: a handler that returns null is an error', () async {
|
||||
final reg = DrawingRegistry()..register('d2', (_) async => null);
|
||||
test('template: a handler error propagates with its message', () async {
|
||||
final reg = DrawingRegistry()..register('d2', (_) async => const DrawErr('d2 not found'));
|
||||
final r = await resolve(parseDrawingCardDoc({'template': 'd2'})!, reg);
|
||||
expect(r, isA<DrawErr>());
|
||||
expect((r as DrawErr).message, contains('d2 not found'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user