diff --git a/lib/src/svg/svg_style.dart b/lib/src/svg/svg_style.dart
new file mode 100644
index 00000000..db15f6a7
--- /dev/null
+++ b/lib/src/svg/svg_style.dart
@@ -0,0 +1,144 @@
+/// Inline-style normalizer for the SVG renderer (T-320 / D-103).
+///
+/// d2 / graphviz style their output with a `');
+ final rect = only(r);
+ expect(rect.attrs['fill'], '#0D32B2');
+ expect(rect.attrs.containsKey('class'), isFalse);
+ });
+
+ test('removes ');
+ expect(r.children.whereType().map((e) => e.name), ['rect']);
+ });
+
+ test('cascade: class overrides presentation attr, style="" overrides class', () {
+ final r = norm('');
+ expect(only(r).attrs['fill'], 'blue');
+ });
+
+ test('class order: the later class wins', () {
+ final r = norm('');
+ expect(only(r).attrs['fill'], 'blue');
+ });
+
+ test('tag rule applies and a class overrides it', () {
+ final r = norm('');
+ expect(only(r).attrs['stroke'], 'red');
+ });
+
+ test('geometry attributes are left untouched', () {
+ final r = norm('');
+ final rect = only(r);
+ expect(rect.attrs['x'], '1');
+ expect(rect.attrs['transform'], 'translate(5,5)');
+ expect(rect.attrs['fill'], 'red');
+ });
+
+ test('nested elements are folded too', () {
+ final r = norm('');
+ expect(only(only(r)).attrs['fill'], 'red');
+ });
+ });
+}
diff --git a/test/svg/svg_xml_test.dart b/test/svg/svg_xml_test.dart
new file mode 100644
index 00000000..96031497
--- /dev/null
+++ b/test/svg/svg_xml_test.dart
@@ -0,0 +1,98 @@
+import 'package:clide/src/svg/svg_xml.dart';
+import 'package:test/test.dart';
+
+void main() {
+ XmlElement parse(String s) {
+ final root = parseXml(s);
+ expect(root, isNotNull, reason: 'expected an element root for: $s');
+ return root!;
+ }
+
+ group('parseXml', () {
+ test('element with attributes', () {
+ final e = parse('');
+ expect(e.name, 'rect');
+ expect(e.attrs, {'x': '1', 'y': '2', 'width': '3', 'height': '4'});
+ expect(e.children, isEmpty);
+ });
+
+ test('single-quoted and unquoted attribute values', () {
+ final e = parse("");
+ expect(e.attrs['fill'], 'red');
+ expect(e.attrs['opacity'], '0.5');
+ });
+
+ test('nested children preserve order', () {
+ final e = parse('');
+ expect(e.name, 'g');
+ expect(e.children.whereType().map((c) => c.name), ['rect', 'circle']);
+ });
+
+ test('text content is captured', () {
+ final e = parse('hello');
+ final t = e.children.single as XmlText;
+ expect(t.text, 'hello');
+ });
+
+ test('comments are skipped', () {
+ final e = parse('');
+ expect(e.children.whereType().map((c) => c.name), ['rect']);
+ expect(e.children.whereType(), isEmpty);
+ });
+
+ test('xml prolog and doctype are skipped', () {
+ final e = parse('');
+ expect(e.name, 'svg');
+ expect(e.attrs['width'], '10');
+ });
+
+ test('CDATA content is captured verbatim', () {
+ final e = parse('');
+ expect((e.children.single as XmlText).text, contains('.a { fill: red }'));
+ });
+
+ test('entities are decoded in text and attributes', () {
+ final e = parse('<tag> A');
+ expect(e.attrs['title'], 'a & b');
+ expect((e.children.single as XmlText).text, ' A');
+ });
+
+ test('style body is read as raw text, not markup', () {
+ // CSS with > and { } must not be parsed as elements.
+ final e = parse('');
+ expect(e.name, 'style');
+ final css = (e.children.single as XmlText).text;
+ expect(css, contains('.edge > .head'));
+ expect(css, contains('stroke:none'));
+ });
+
+ test('namespaced attribute names are kept verbatim', () {
+ final e = parse('');
+ expect(e.attrs['xlink:href'], 'a.png');
+ expect(e.attrs['href'], 'b.png');
+ });
+
+ test('mismatched close tag is tolerated, no throw', () {
+ final e = parse('');
+ expect(e.name, 'g');
+ expect(e.children.whereType().single.name, 'rect');
+ });
+
+ test('descendants walks the whole subtree', () {
+ final e = parse('');
+ final names = e.descendants().whereType().map((c) => c.name).toList();
+ expect(names, ['g', 'rect', 'circle']);
+ });
+
+ test('non-element root returns null, never throws', () {
+ expect(parseXml(''), isNull);
+ expect(parseXml(' '), isNull);
+ expect(parseXml('just text'), isNull);
+ });
+
+ test('unterminated tag does not throw', () {
+ // Should not hang or throw; returns whatever was understood.
+ expect(() => parseXml('