SVG2DrawIOLib 1.2.1__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/.gitignore +11 -0
  2. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/PKG-INFO +1 -1
  3. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/__about__.py +1 -1
  4. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/icon_analyzer.py +12 -12
  5. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/validator.py +9 -33
  6. svg2drawiolib-1.2.2/src/SVG2DrawIOLib/xml_utils.py +55 -0
  7. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_cli_validate.py +4 -2
  8. svg2drawiolib-1.2.2/tests/test_xml_utils.py +116 -0
  9. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/.github/workflows/ci.yml +0 -0
  10. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/.github/workflows/publish.yml +0 -0
  11. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/.github/workflows/release.yml +0 -0
  12. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/.pre-commit-config.yaml +0 -0
  13. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/ARCHITECTURE.md +0 -0
  14. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/CHANGELOG.md +0 -0
  15. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/CONTRIBUTING.md +0 -0
  16. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/LICENSE.txt +0 -0
  17. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/MANIFEST.in +0 -0
  18. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/Makefile +0 -0
  19. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/QUICKSTART.md +0 -0
  20. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/README.md +0 -0
  21. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/pyproject.toml +0 -0
  22. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/__init__.py +0 -0
  23. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/__init__.py +0 -0
  24. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/add.py +0 -0
  25. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/create.py +0 -0
  26. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/create_helpers.py +0 -0
  27. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/extract.py +0 -0
  28. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/helpers.py +0 -0
  29. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/inspect.py +0 -0
  30. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/list.py +0 -0
  31. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/remove.py +0 -0
  32. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/rename.py +0 -0
  33. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/split_paths.py +0 -0
  34. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/cli/validate.py +0 -0
  35. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/library_manager.py +0 -0
  36. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/models.py +0 -0
  37. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/path_splitter.py +0 -0
  38. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/src/SVG2DrawIOLib/svg_processor.py +0 -0
  39. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/__init__.py +0 -0
  40. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/conftest.py +0 -0
  41. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_cli.py +0 -0
  42. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_cli_create_helpers.py +0 -0
  43. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_cli_create_split.py +0 -0
  44. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_cli_extract.py +0 -0
  45. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_cli_helpers.py +0 -0
  46. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_cli_inspect.py +0 -0
  47. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_cli_rename.py +0 -0
  48. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_css_modes.py +0 -0
  49. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_library_manager.py +0 -0
  50. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_models.py +0 -0
  51. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_path_splitter.py +0 -0
  52. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_svg_processor.py +0 -0
  53. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_svg_processor_coverage.py +0 -0
  54. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/tests/test_viewbox_improvements.py +0 -0
  55. {svg2drawiolib-1.2.1 → svg2drawiolib-1.2.2}/uv.lock +0 -0
@@ -38,3 +38,14 @@ debug/
38
38
  # Temporary test files
39
39
  /test_*.xml
40
40
  /test_*.py
41
+
42
+ # web-ui (Next.js)
43
+ web-ui/node_modules/
44
+ web-ui/.next/
45
+ web-ui/out/
46
+ web-ui/.env.local
47
+ web-ui/.env*.local
48
+
49
+ # api (FastAPI)
50
+ api/__pycache__/
51
+ api/**/__pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SVG2DrawIOLib
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Generate DrawIO shape libraries from SVGs
5
5
  Project-URL: Homepage, https://github.com/jamesbconner/SVG2DrawIOLib
6
6
  Project-URL: Documentation, https://github.com/jamesbconner/SVG2DrawIOLib#readme
@@ -1,3 +1,3 @@
1
1
  """Package version information."""
2
2
 
3
- __version__ = "1.2.1"
3
+ __version__ = "1.2.2"
@@ -5,12 +5,12 @@ import binascii
5
5
  import logging
6
6
  import re
7
7
  import xml.etree.ElementTree as ET
8
- import zlib
9
8
  from dataclasses import dataclass
10
9
  from pathlib import Path
11
10
  from typing import Any
12
11
 
13
12
  from SVG2DrawIOLib.models import DrawIOIcon
13
+ from SVG2DrawIOLib.xml_utils import decode_drawio_xml
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -51,7 +51,7 @@ class IconAnalyzer:
51
51
  """Extract SVG content and style information from icon data.
52
52
 
53
53
  Args:
54
- xml_data: Base64-encoded, compressed mxGraphModel XML.
54
+ xml_data: Base64-encoded compressed mxGraphModel XML or URL-encoded plain text XML.
55
55
 
56
56
  Returns:
57
57
  Tuple of (svg_content, style_info).
@@ -60,11 +60,8 @@ class IconAnalyzer:
60
60
  ValueError: If the data cannot be decompressed or parsed.
61
61
  """
62
62
  try:
63
- # Decode base64
64
- compressed = base64.b64decode(xml_data)
65
-
66
- # Decompress using raw DEFLATE (wbits=-15)
67
- decompressed = zlib.decompress(compressed, wbits=-15)
63
+ # Decode XML data (handles both compressed and URL-encoded formats)
64
+ decompressed = decode_drawio_xml(xml_data)
68
65
 
69
66
  # Parse mxGraphModel XML
70
67
  root = ET.fromstring(decompressed) # nosec B314 - User-provided library file
@@ -124,13 +121,16 @@ class IconAnalyzer:
124
121
  return svg_content, style_info
125
122
 
126
123
  except binascii.Error as e:
127
- raise ValueError(f"Failed to decode base64: {e}") from e
128
- except zlib.error as e:
129
- raise ValueError(f"Failed to decompress icon data: {e}") from e
130
- except ET.ParseError as e:
131
- raise ValueError(f"Failed to parse mxGraphModel XML: {e}") from e
124
+ # This can occur during SVG base64 decoding (line 93)
125
+ raise ValueError(f"Failed to decode SVG base64: {e}") from e
132
126
  except UnicodeDecodeError as e:
127
+ # This can occur during SVG UTF-8 decoding (line 94)
133
128
  raise ValueError(f"Failed to decode SVG UTF-8: {e}") from e
129
+ except ET.ParseError as e:
130
+ raise ValueError(f"Failed to parse mxGraphModel XML: {e}") from e
131
+ except ValueError:
132
+ # Re-raise ValueError from decode_drawio_xml or other sources
133
+ raise
134
134
 
135
135
  def extract_to_file(
136
136
  self,
@@ -6,10 +6,11 @@ import json
6
6
  import logging
7
7
  import re
8
8
  import xml.etree.ElementTree as ET
9
- import zlib
10
9
  from pathlib import Path
11
10
  from typing import Any
12
11
 
12
+ from SVG2DrawIOLib.xml_utils import decode_drawio_xml
13
+
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
@@ -165,7 +166,7 @@ class LibraryValidator:
165
166
  }
166
167
  )
167
168
 
168
- # Validate XML data (base64 encoded, compressed mxGraphModel)
169
+ # Validate XML data (can be base64+compressed or URL-encoded plain text)
169
170
  try:
170
171
  xml_data = item["xml"]
171
172
  if not isinstance(xml_data, str):
@@ -178,28 +179,15 @@ class LibraryValidator:
178
179
  )
179
180
  return issues
180
181
 
181
- # Try to decode base64
182
+ # Decode XML data (handles both compressed and URL-encoded formats)
182
183
  try:
183
- compressed = base64.b64decode(xml_data)
184
- except Exception as e:
184
+ decompressed = decode_drawio_xml(xml_data.encode("utf-8"))
185
+ except ValueError as e:
185
186
  issues.append(
186
187
  {
187
188
  "severity": "error",
188
189
  "icon": icon_name,
189
- "message": f"Failed to decode base64: {e}",
190
- }
191
- )
192
- return issues
193
-
194
- # Try to decompress
195
- try:
196
- decompressed = zlib.decompress(compressed, wbits=-15)
197
- except zlib.error as e:
198
- issues.append(
199
- {
200
- "severity": "error",
201
- "icon": icon_name,
202
- "message": f"Failed to decompress data: {e}",
190
+ "message": str(e),
203
191
  }
204
192
  )
205
193
  return issues
@@ -225,20 +213,8 @@ class LibraryValidator:
225
213
  )
226
214
  return issues
227
215
 
228
- # Try to recompress to verify round-trip
229
- try:
230
- co = zlib.compressobj(level=9, wbits=-15)
231
- _ = co.compress(decompressed) + co.flush()
232
- # Note: Recompressed data may differ slightly due to compression settings
233
- logger.debug(f"Icon '{icon_name}': round-trip compression successful")
234
- except Exception as e:
235
- issues.append(
236
- {
237
- "severity": "warning",
238
- "icon": icon_name,
239
- "message": f"Failed to recompress data: {e}",
240
- }
241
- )
216
+ # For compressed format, verify round-trip (optional validation)
217
+ # Note: We can't easily detect format after decode, so skip this for now
242
218
 
243
219
  # Extract and validate SVG content
244
220
  svg_issues = self._validate_svg_content(root, icon_name)
@@ -0,0 +1,55 @@
1
+ """XML utilities - Shared functions for handling DrawIO XML data."""
2
+
3
+ import base64
4
+ import binascii
5
+ import html
6
+ import logging
7
+ import urllib.parse
8
+ import zlib
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def decode_drawio_xml(xml_data: bytes) -> bytes:
14
+ """Decode DrawIO XML data from either compressed or URL-encoded format.
15
+
16
+ DrawIO libraries can store XML in two formats:
17
+ 1. Compressed: base64-encoded, zlib-compressed (used by svg2drawiolib create)
18
+ 2. URL-encoded: HTML entity-escaped plain text (used by DrawIO native)
19
+
20
+ This function automatically detects the format and decodes accordingly.
21
+
22
+ Args:
23
+ xml_data: Raw XML data as bytes (either compressed or URL-encoded).
24
+
25
+ Returns:
26
+ Decompressed/decoded XML data as bytes.
27
+
28
+ Raises:
29
+ ValueError: If the data cannot be decoded in either format.
30
+ """
31
+ # Try compressed format first (base64 + zlib)
32
+ try:
33
+ compressed = base64.b64decode(xml_data)
34
+ decompressed = zlib.decompress(compressed, wbits=-15)
35
+ logger.debug("Detected compressed XML format")
36
+ return decompressed
37
+ except (binascii.Error, zlib.error):
38
+ # Not compressed format, try URL-encoded plain text
39
+ pass
40
+
41
+ # Try URL-encoded format (DrawIO native)
42
+ try:
43
+ # Decode from bytes to string
44
+ xml_str = xml_data.decode("utf-8")
45
+ # Unescape HTML entities (&lt; -> <, &gt; -> >, etc.)
46
+ unescaped = html.unescape(xml_str)
47
+ # URL decode if needed
48
+ decoded = urllib.parse.unquote(unescaped)
49
+ decompressed = decoded.encode("utf-8")
50
+ logger.debug("Detected URL-encoded XML format")
51
+ return decompressed
52
+ except Exception as e:
53
+ raise ValueError(
54
+ f"Failed to decode XML data (tried both compressed and URL-encoded formats): {e}"
55
+ ) from e
@@ -147,7 +147,8 @@ class TestValidateCommand:
147
147
 
148
148
  result = runner.invoke(cli, ["validate", str(library)])
149
149
  assert result.exit_code != 0
150
- assert "Failed to decode base64" in result.output
150
+ # With fallback to URL-encoded format, this now fails at XML parsing
151
+ assert "Failed to parse mxGraphModel XML" in result.output
151
152
 
152
153
  def test_validate_invalid_compression(self, runner: CliRunner, tmp_path: Path) -> None:
153
154
  """Test validate with data that can't be decompressed."""
@@ -161,7 +162,8 @@ class TestValidateCommand:
161
162
 
162
163
  result = runner.invoke(cli, ["validate", str(library)])
163
164
  assert result.exit_code != 0
164
- assert "Failed to decompress data" in result.output
165
+ # With fallback to URL-encoded format, this now fails at XML parsing
166
+ assert "Failed to parse mxGraphModel XML" in result.output
165
167
 
166
168
  def test_validate_invalid_mxgraphmodel(self, runner: CliRunner, tmp_path: Path) -> None:
167
169
  """Test validate with invalid mxGraphModel XML."""
@@ -0,0 +1,116 @@
1
+ """Tests for XML utilities module."""
2
+
3
+ import base64
4
+ import zlib
5
+
6
+ from SVG2DrawIOLib.xml_utils import decode_drawio_xml
7
+
8
+
9
+ class TestDecodeDrawIOXML:
10
+ """Tests for decode_drawio_xml function."""
11
+
12
+ def test_decode_compressed_format(self) -> None:
13
+ """Test decoding base64-encoded, zlib-compressed XML."""
14
+ # Create test XML
15
+ xml_content = b"<mxGraphModel><root><mxCell id='0'/></root></mxGraphModel>"
16
+
17
+ # Compress and encode
18
+ co = zlib.compressobj(level=9, wbits=-15)
19
+ compressed = co.compress(xml_content) + co.flush()
20
+ encoded = base64.b64encode(compressed)
21
+
22
+ # Decode
23
+ result = decode_drawio_xml(encoded)
24
+
25
+ assert result == xml_content
26
+
27
+ def test_decode_url_encoded_format(self) -> None:
28
+ """Test decoding URL-encoded plain text XML."""
29
+ # Create test XML with HTML entities
30
+ xml_content = "&lt;mxGraphModel&gt;&lt;root&gt;&lt;mxCell id='0'/&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;"
31
+ expected = b"<mxGraphModel><root><mxCell id='0'/></root></mxGraphModel>"
32
+
33
+ # Decode
34
+ result = decode_drawio_xml(xml_content.encode("utf-8"))
35
+
36
+ assert result == expected
37
+
38
+ def test_decode_url_encoded_with_special_chars(self) -> None:
39
+ """Test decoding URL-encoded XML with special characters."""
40
+ # XML with URL-encoded special characters
41
+ xml_content = "&lt;mxGraphModel&gt;&lt;root&gt;&lt;mxCell id=&quot;0&quot;/&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;"
42
+ expected = b'<mxGraphModel><root><mxCell id="0"/></root></mxGraphModel>'
43
+
44
+ # Decode
45
+ result = decode_drawio_xml(xml_content.encode("utf-8"))
46
+
47
+ assert result == expected
48
+
49
+ def test_decode_invalid_data(self) -> None:
50
+ """Test that invalid data is decoded but may not be valid XML."""
51
+ # Data that's neither valid base64+compressed nor valid XML
52
+ # The function will decode it via URL-encoding fallback
53
+ invalid_data = b"this is not valid XML or compressed data!!!"
54
+
55
+ # Should decode without error (validation happens at XML parsing level)
56
+ result = decode_drawio_xml(invalid_data)
57
+ assert result == invalid_data # URL-decode doesn't change this string
58
+
59
+ def test_decode_valid_base64_but_not_compressed(self) -> None:
60
+ """Test data that's valid base64 but not compressed."""
61
+ # Valid base64 but not compressed data
62
+ data = base64.b64encode(b"not compressed data")
63
+
64
+ # Should fall back to URL-encoded format and decode
65
+ result = decode_drawio_xml(data)
66
+ # The base64 string gets URL-decoded (no change in this case)
67
+ assert isinstance(result, bytes)
68
+
69
+ def test_decode_empty_data(self) -> None:
70
+ """Test that empty data is handled."""
71
+ # Empty data should decode to empty
72
+ result = decode_drawio_xml(b"")
73
+ assert result == b""
74
+
75
+ def test_decode_compressed_with_complex_xml(self) -> None:
76
+ """Test decoding compressed XML with complex structure."""
77
+ # Complex XML with attributes and nested elements
78
+ xml_content = b"""<mxGraphModel>
79
+ <root>
80
+ <mxCell id="0"/>
81
+ <mxCell id="1" parent="0"/>
82
+ <mxCell id="2" value="test" style="shape=image;image=data:image/svg+xml,PHN2Zz4=" vertex="1" parent="1">
83
+ <mxGeometry width="100" height="100" as="geometry"/>
84
+ </mxCell>
85
+ </root>
86
+ </mxGraphModel>"""
87
+
88
+ # Compress and encode
89
+ co = zlib.compressobj(level=9, wbits=-15)
90
+ compressed = co.compress(xml_content) + co.flush()
91
+ encoded = base64.b64encode(compressed)
92
+
93
+ # Decode
94
+ result = decode_drawio_xml(encoded)
95
+
96
+ assert result == xml_content
97
+
98
+ def test_decode_url_encoded_with_complex_xml(self) -> None:
99
+ """Test decoding URL-encoded XML with complex structure."""
100
+ # Complex XML with HTML entities
101
+ xml_content = (
102
+ "&lt;mxGraphModel&gt;"
103
+ "&lt;root&gt;"
104
+ "&lt;mxCell id=&quot;0&quot;/&gt;"
105
+ "&lt;mxCell id=&quot;1&quot; parent=&quot;0&quot;/&gt;"
106
+ "&lt;/root&gt;"
107
+ "&lt;/mxGraphModel&gt;"
108
+ )
109
+ expected = (
110
+ b'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>'
111
+ )
112
+
113
+ # Decode
114
+ result = decode_drawio_xml(xml_content.encode("utf-8"))
115
+
116
+ assert result == expected
File without changes
File without changes
File without changes
File without changes
File without changes