From bd1e1508c6ba7daa127931230bfb0d377b5aea7a Mon Sep 17 00:00:00 2001 From: Remy Blank Date: Thu, 10 Oct 2024 21:06:07 +0200 Subject: [PATCH] tdoc.svg: Automatically scope styles to the image. --- docs/demo/python.md | 40 +++++++------ tdoc/common/python/__init__.py | 11 ++++ tdoc/common/python/svg.py | 106 +++++++++++++++------------------ 3 files changed, 79 insertions(+), 78 deletions(-) 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 '' + if styles: yield f'' + for child in self.children: yield from child + yield ''