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('