diff --git a/docs/demo/python.md b/docs/demo/python.md
index b92db64..f921f85 100644
--- a/docs/demo/python.md
+++ b/docs/demo/python.md
@@ -83,29 +83,31 @@ image creates an output block displaying the image.
:editable:
from tdoc import svg
-def paint_heart(g):
- g.path('M -40,-20 A 20,20 0,0,1 0,-20 A 20,20 0,0,1 40,-20 '
+def paint_heart(c):
+ c.path('M -40,-20 A 20,20 0,0,1 0,-20 A 20,20 0,0,1 40,-20 '
'Q 40,10 0,40 Q -40,10 -40,-20 z',
stroke='red', fill='transparent')
- g.path('M -40,30 -30,30 -30,40 '
+ c.path('M -40,30 -30,30 -30,40 '
'M -30,30 0,0 M 34,-34 45,-45'
'M 35,-45 45,-45 45,-35',
stroke=svg.Stroke('black', width=2), fill='transparent')
-img = svg.Image(400, 100, stroke='darkorange', fill='#c0c0ff', id='graphics')
-img.styles("""
-#graphics {
- width: 100%;
- height: 100%;
- polygon { fill: #c0ffc0; }
+img = svg.Image(400, 100, stroke='darkorange', fill='#c0c0ff',
+ style='width: 100%; height: 100%')
+img.styles = """
+.bold {
+ stroke: blue;
+ stroke-width: 2;
+ fill: #c0ffc0;
}
-""")
+"""
img.circle(20, 30, 10)
-img.ellipse(20, 70, 10, 20)
+img.ellipse(20, 70, 10, 20, klass='bold')
img.line(0, 0, 400, 100)
-img.polygon((200, 10), (230, 10), (240, 30))
-img.polyline((200, 10), (230, 10), (240, 30), fill='transparent',
- transform=svg.translate(x=50, y=10))
+g = img.group(transform=svg.translate(200, 10))
+g.polygon((0, 0), (30, 0), (40, 20), klass='bold')
+g.polyline((0, 0), (30, 0), (40, 20), fill='transparent',
+ transform=svg.translate(x=50, y=10))
img.rect(0, 0, 400, 100, fill='transparent')
img.text(50, 90, "Some text", fill='green')
paint_heart(img.group(transform=svg.translate(360, 30).rotate(20).scale(0.5)))
@@ -124,12 +126,12 @@ import asyncio
import random
img = svg.Image(400, 100, style='width: 100%; height: 100%')
-sym = img.symbol(id='heart')
+sym = img.symbol()
paint_heart(sym)
-hearts = [
- (img.use(href='#heart'),
- random.uniform(0, 100), random.uniform(0, 100), random.uniform(-180, 180))
- for _ in range(20)]
+hearts = [(img.use(href=f'#{sym.id}'),
+ random.uniform(0, 100), random.uniform(0, 100),
+ random.uniform(-180, 180))
+ for _ in range(20)]
def saw(value, amplitude):
return abs((value + amplitude) % (2 * amplitude) - amplitude)
diff --git a/tdoc/common/python/__init__.py b/tdoc/common/python/__init__.py
index 36f8c89..a37ad5d 100644
--- a/tdoc/common/python/__init__.py
+++ b/tdoc/common/python/__init__.py
@@ -52,6 +52,17 @@ def write(self, data, /):
line_buffering=True)
+_next_id = 0
+
+@public
+def new_id():
+ """Generate a unique ID, usable in id= attributes."""
+ global _next_id
+ v = _next_id
+ _next_id += 1
+ return f'tdoc-id-{v}'
+
+
@public
def render(html, name=''):
"""Render some HTML in an output block."""
diff --git a/tdoc/common/python/svg.py b/tdoc/common/python/svg.py
index 63b2fec..71f6315 100644
--- a/tdoc/common/python/svg.py
+++ b/tdoc/common/python/svg.py
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: MIT
import html
+import tdoc
def esc(v, quote=True):
@@ -128,25 +129,35 @@ def skew_y(*args, **kwargs): return Transform().skew_y(*args, **kwargs)
class _Element:
- _slots = ('stroke', 'fill', 'style', 'id')
+ _slots = ('_id', 'klass', 'style', 'stroke', 'fill')
- def __init__(self, stroke, fill, style, id):
+ def __init__(self, *, klass=None, style=None, stroke=None, fill=None):
+ self._id, self.klass, self.style = None, klass, style
self.stroke, self.fill = Stroke(stroke), Fill(fill)
- self.style, self.id = style, id
+
+ @property
+ def id(self):
+ if self._id is None: self._id = tdoc.new_id()
+ return self._id
+
+ @id.setter
+ def id(self, value):
+ self._id = value
def _attrs(self):
+ if (v := self._id) is not None: yield f' id="{esc(v)}"'
+ if (v := self.klass) is not None: yield f' class="{esc(v)}"'
+ if (v := self.style) is not None: yield f' style="{esc(v)}"'
yield from self.stroke
yield from self.fill
- if (v := self.style) is not None: yield f' style="{esc(v)}"'
- if (v := self.id) is not None: yield f' id="{esc(v)}"'
class _Shape(_Element):
_slots = _Element._slots + ('transform',)
- def __init__(self, stroke, fill, style, id, transform):
- super().__init__(stroke, fill, style, id)
+ def __init__(self, *, transform=None, **kwargs):
self.transform = transform
+ super().__init__(**kwargs)
def _attrs(self):
yield from super()._attrs()
@@ -156,10 +167,9 @@ def _attrs(self):
class Circle(_Shape):
__slots__ = _Shape._slots + ('x', 'y', 'r')
- def __init__(self, x, y, r, *, stroke=None, fill=None, style=None, id=None,
- transform=None):
+ def __init__(self, x, y, r, **kwargs):
self.x, self.y, self.r = x, y, r
- super().__init__(stroke, fill, style, id, transform)
+ super().__init__(**kwargs)
def __iter__(self):
yield f''
-class Styles:
- __slots__ = ('styles',)
-
- def __init__(self, styles):
- self.styles = styles
-
- def __iter__(self):
- yield f''
-
-
class _Container(_Shape):
_slots = _Shape._slots + ('children',)
@@ -345,9 +338,6 @@ def symbol(self, *args, **kwargs):
def use(self, *args, **kwargs):
return self.add(Use(*args, **kwargs))
- def styles(self, *args, **kwargs):
- return self.add(Styles(*args, **kwargs))
-
def __iter__(self):
yield f'<{self._tag}'
yield from self._attrs()
@@ -360,37 +350,35 @@ class Group(_Container):
__slots__ = _Container._slots
_tag = 'g'
- def __init__(self, *, stroke=None, fill=None, style=None, id=None,
- transform=None):
- super().__init__(stroke, fill, style, id, transform)
-
class Symbol(_Container):
__slots__ = _Container._slots
_tag = 'symbol'
- def __init__(self, *, stroke=None, fill=None, style=None, id=None,
- transform=None):
- super().__init__(stroke, fill, style, id, transform)
-
def _attrs(self):
yield from super()._attrs()
yield ' overflow="visible"'
class Image(_Container):
- __slots__ = _Container._slots + ('width', 'height')
- _tag = 'svg'
+ __slots__ = _Container._slots + ('width', 'height', 'styles')
- def __init__(self, width, height, *, stroke=Stroke.default,
- fill=Fill.default, style=None, id=None, transform=None):
- super().__init__(stroke, fill, style, id, transform)
- self.width, self.height = width, height
+ def __init__(self, width, height, *, styles=None, **kwargs):
+ self.width, self.height, self.styles = width, height, styles
+ super().__init__(**kwargs)
def _attrs(self):
yield ' xmlns="http://www.w3.org/2000/svg"'
yield ' xmlns:xlink="http://www.w3.org/1999/xlink"'
- yield ' version="2"'
yield f' viewBox="0 0 {esc(self.width)} {esc(self.height)}"'
yield f' width="{esc(self.width)}" height="{esc(self.height)}"'
yield from super()._attrs()
+
+ def __iter__(self):
+ if styles := self.styles: id = self.id # Force the allocation of an ID
+ yield ''