SF.net SVN: docutils:[9921] trunk/docutils

milde--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 9921
          http://sourceforge.net/p/docutils/code/9921
Author:   milde
Date:     2024-09-04 19:33:39 +0000 (Wed, 04 Sep 2024)
Log Message:
-----------
Revise `HTMLTranslator.prepare_svg()`.

Change oder of method arguments (sort by importance).

Use a dictionary to update the `<svg style="..."> declarations keeping
exisisting declarations intact.

Simplify conversion of "align" attribute.

Add test cases.

Modified Paths:
--------------
    trunk/docutils/docutils/writers/_html_base.py
    trunk/docutils/test/test_writers/test_html5_polyglot.py
    trunk/docutils/test/test_writers/test_html5_polyglot_misc.py

Modified: trunk/docutils/docutils/writers/_html_base.py
===================================================================
--- trunk/docutils/docutils/writers/_html_base.py	2024-09-04 19:33:30 UTC (rev 9920)
+++ trunk/docutils/docutils/writers/_html_base.py	2024-09-04 19:33:39 UTC (rev 9921)
@@ -28,7 +28,7 @@
 import urllib.parse
 import urllib.request
 import warnings
-import xml.etree.ElementTree as ET  # TODO: lazy import in prepare_svg()?
+import xml.etree.ElementTree as ET
 from pathlib import Path
 from typing import TYPE_CHECKING
 
@@ -474,39 +474,36 @@
             return None
         return imgsize
 
-    def prepare_svg(self, node, imagedata, size_declaration):
-        # Edit `imagedata` for embedding as SVG image.
-        # Use ElementTree to add node attributes.
-        # ET also removes comments and preamble code.
+    def prepare_svg(self, code: str, node: nodes.Element, atts: dict) -> str:
+        # Parse SVG source `code` (ignoring comments and preamble code),
+        # add relevant attributes from `node` and `atts` to the root element.
         #
-        # Internal: interface and behaviour may change without notice.
-
-        # SVG namespace
+        # Internal auxiliary method called from `self.visit_image()`.
         svg_ns = {'': 'http://www.w3.org/2000/svg',
-                  'xlink': 'http://www.w3.org/1999/xlink'}
+                  'xlink': 'http://www.w3.org/1999/xlink'}  # SVG namespace
         # don't add SVG namespace to all elements
-        ET.register_namespace('', svg_ns[''])
-        ET.register_namespace('xlink', svg_ns['xlink'])
+        for key, value in svg_ns.items():
+            ET.register_namespace(key, value)
+        # Parse:
         try:
-            svg = ET.fromstring(imagedata)
+            svg = ET.fromstring(code)
         except ET.ParseError as err:
             self.messages.append(self.document.reporter.error(
                 f'Cannot parse SVG image "{node["uri"]}":\n  {err}',
                 base_node=node))
-            return imagedata
-        # apply image node attributes:
-        if size_declaration:  # append to style, replacing width & height
-            declarations = [d.strip() for d in svg.get('style', '').split(';')]
-            declarations = [d for d in declarations
-                            if d
-                            and not d.startswith('width')
-                            and not d.startswith('height')]
-            svg.set('style', '; '.join(declarations+[size_declaration]))
-        if node['classes'] or 'align' in node:
+            return code
+        # Apply image node attributes:
+        if 'style' in atts:
+            # update style declarations
+            style_att = f"{svg.get('style', '')}; {atts['style']}"
+            style_att = [d.partition(':') for d in style_att.split(';')
+                         if d.strip()]
+            style_att = dict((k.strip(), v.strip()) for k, p, v in style_att)
+            style_att = ' '.join(f'{k}: {v};' for k, v in style_att.items())
+            svg.set('style', style_att)
+        if 'classes' in atts or node['classes']:
             classes = svg.get('class', '').split()
-            classes += node.get('classes', [])
-            if 'align' in node:
-                classes.append(f'align-{node["align"]}')
+            classes += node['classes'] + atts.get('classes', [])
             svg.set('class', ' '.join(classes))
         if 'alt' in node and svg.find('title', svg_ns) is None:
             svg_title = ET.Element('title')
@@ -598,7 +595,7 @@
             infix = ' /'
         else:
             infix = ''
-        return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
+        return f"{''.join(prefix)}<{' '.join(parts)}{infix}>{suffix}"
 
     def emptytag(self, node, tagname, suffix='\n', **attributes):
         """Construct and return an XML-compatible empty tag."""
@@ -1173,14 +1170,11 @@
         mimetype = mimetypes.guess_type(uri)[0]
         element = ''  # the HTML element (including potential children)
         atts = {}  # attributes for the HTML tag
-        # alignment is handled by CSS rules
-        if 'align' in node:
-            atts['class'] = 'align-%s' % node['align']
-        # set size with "style" attribute (more universal, accepts dimensions)
         size_declaration = self.image_size(node)
         if size_declaration:
             atts['style'] = size_declaration
-
+        if 'align' in node:
+            atts['classes'] = [f"align-{node['align']}"]
         # ``:loading:`` option (embed, link, lazy), default from setting,
         # exception: only embed videos if told via directive option
         loading = 'link' if mimetype in self.videotypes else self.image_loading
@@ -1201,8 +1195,7 @@
             else:
                 self.settings.record_dependencies.add(imagepath)
                 if mimetype == 'image/svg+xml':
-                    element = self.prepare_svg(node, imagedata,
-                                               size_declaration)
+                    element = self.prepare_svg(imagedata, node, atts)
                 else:
                     data64 = base64.b64encode(imagedata).decode()
                     uri = f'data:{mimetype};base64,{data64}'

Modified: trunk/docutils/test/test_writers/test_html5_polyglot.py
===================================================================
--- trunk/docutils/test/test_writers/test_html5_polyglot.py	2024-09-04 19:33:30 UTC (rev 9920)
+++ trunk/docutils/test/test_writers/test_html5_polyglot.py	2024-09-04 19:33:39 UTC (rev 9921)
@@ -2,9 +2,10 @@
 
 # $Id$
 # Author: reggie dugard <[email protected]>
+# Maintainer: [email protected]
 # Copyright: This module has been placed in the public domain.
 
-"""Test HTML5 writer output ("fragment" part).
+"""Test HTML5 writer output ("fragment"/"body" part).
 
 This is the document body (not HTML <body>).
 """
@@ -89,7 +90,7 @@
                     self.assertEqual(case_expected, parts['body'])
 
 
-totest = {}
+totest = {}  # expected samples contain only the "body" part of the HMTL output
 
 totest['standard'] = ({}, [
 ["""\
@@ -147,6 +148,18 @@
 </div>
 """,
 ],
+[f"""\
+.. image:: {DATA_ROOT.as_uri()}/circle.svg
+   :loading: embed
+   :width: 50%
+   :height: 30
+   :align: left
+""",
+"""\
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" style="width: 50%; height: 30px;" class="align-left">
+  <circle cx="5" cy="5" r="4" fill="lightblue" />
+</svg>
+"""],
 ])
 
 

Modified: trunk/docutils/test/test_writers/test_html5_polyglot_misc.py
===================================================================
--- trunk/docutils/test/test_writers/test_html5_polyglot_misc.py	2024-09-04 19:33:30 UTC (rev 9920)
+++ trunk/docutils/test/test_writers/test_html5_polyglot_misc.py	2024-09-04 19:33:39 UTC (rev 9921)
@@ -247,6 +247,19 @@
     settings = frontend.get_default_settings(_html_base.Writer)
     document = utils.new_document('test data', settings)
     translator = _html_base.HTMLTranslator(document)
+    svg_sample = """\
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+     style="background: blue; width: 5ex" viewBox="0 0 10 10">
+  <circle cx="5" cy="5" r="4" fill="lightblue" />
+  <!-- comments are ignored -->
+</svg>
+"""
+    expected = """\
+<svg xmlns="http://www.w3.org/2000/svg" style="background: blue; width: 4em; height: 32px;" viewBox="0 0 10 10" class="test me">
+  <title>blue circle</title><circle cx="5" cy="5" r="4" fill="lightblue" />
+  \n\
+</svg>"""
 
     def test_image_size(self):
         image = nodes.image(height='3', width='4em')
@@ -256,6 +269,27 @@
         self.assertEqual(self.translator.image_size(image),
                          'width: 2em; height: 1.5px;')
 
+    def test_prepare_svg(self):
+        # Internal method: the test is no guaranty for stability,
+        # interface and behaviour may change without notice.
+        image = nodes.image(height='32', width='4em', alt='blue circle',
+                            align='left', classes=['test', 'me'])
+        atts = {'style': self.translator.image_size(image)}
+        rv = self.translator.prepare_svg(self.svg_sample, image, atts)
+        self.assertEqual(rv, self.expected)
 
+    def test_prepare_svg_syntax_variants(self):
+        # parsing "style" declarations must be robust:
+        svg_sample = '<svg style="width:3em"></svg>'
+
+        atts = {'style': 'colon:in:value'}
+        rv = self.translator.prepare_svg(svg_sample, nodes.image(), atts)
+        self.assertEqual(rv, '<svg style="width: 3em; colon: in:value;" />')
+
+        atts = {'style': 'no-colon;'}
+        rv = self.translator.prepare_svg(svg_sample, nodes.image(), atts)
+        self.assertEqual(rv, '<svg style="width: 3em; no-colon: ;" />')
+
+
 if __name__ == '__main__':
     unittest.main()

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.