From 392a4379ee6a347f0aafbec76b5c5a173a7b3838 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:26:15 +0000 Subject: [PATCH 1/3] Initial plan From fea4ccdd442e02f5db9e6ea303e0bf88d940c56f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:32:39 +0000 Subject: [PATCH 2/3] Add comprehensive SVG support documentation to README and spec Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- README.md | 499 +++++++++++++++++++++++++++++++++++++++++++++++++++++- spec.md | 482 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 979 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fd299c7..47d3dbf 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,18 @@ Output: - [Array Element Access](#array-element-access) - [Comments](#comments) - [Conditional Rendering](#conditional-rendering) +- [SVG Support](#svg-support) + - [Basic SVG](#basic-svg) + - [Multiple Shapes](#multiple-shapes) + - [SVG Paths](#svg-paths) + - [SVG Groups and Transform](#svg-groups-and-transform) + - [SVG with Data Binding](#svg-with-data-binding) + - [SVG Text](#svg-text) + - [SVG with Gradients](#svg-with-gradients) + - [Conditional SVG Elements](#conditional-svg-elements) + - [Interactive SVG with Data](#interactive-svg-with-data) + - [SVG Icons](#svg-icons) + - [SVG Animations](#svg-animations) - [Error Handling](#error-handling) - [Format Notes](#format-notes) - [Available Libraries](#available-libraries) @@ -82,12 +94,21 @@ This means the implementation is featherweight. ### Allowed Tags +**HTML tags:** `div`, `span`, `p`, `header`, `footer`, `main`, `section`, `article`, `h1`–`h6`, `strong`, `em`, `blockquote`, `code`, `pre`, `ul`, `ol`, `li`, `table`, `thead`, `tbody`, `tr`, `th`, `td`, `a`, `img`, `br`, `hr` +**SVG tags:** +`svg`, `g`, `defs`, `symbol`, `use`, +`circle`, `rect`, `ellipse`, `line`, `polyline`, `polygon`, `path`, +`text`, `tspan`, +`linearGradient`, `radialGradient`, `stop`, +`clipPath`, `mask`, `pattern`, +`animate`, `animateTransform` + **Special tags:** - `$comment` — Emits HTML comments. Cannot be nested inside another `$comment`. - `$if` — Conditional rendering based on data properties with comparison operators. See [Conditional Rendering](#conditional-rendering) below. @@ -96,12 +117,30 @@ This means the implementation is featherweight. | Tag(s) | Allowed Attributes | |----------------|---------------------------------------------| -| All | `id`, `class`, `style`, `title`, `aria-*`, `data-*`, `role` | +| All HTML | `id`, `class`, `style`, `title`, `aria-*`, `data-*`, `role` | | `a` | `href`, `target`, `rel` | | `img` | `src`, `alt`, `width`, `height` | | `table` | `summary` | | `th`, `td` | `scope`, `colspan`, `rowspan` | | `blockquote` | `cite` | +| All SVG | `id`, `class`, `style`, `data-*` | +| `svg` | `width`, `height`, `viewBox`, `preserveAspectRatio`, `xmlns` | +| `circle` | `cx`, `cy`, `r`, `fill`, `stroke`, `stroke-width`, `opacity`, `fill-opacity`, `stroke-opacity` | +| `rect` | `x`, `y`, `width`, `height`, `rx`, `ry`, `fill`, `stroke`, `stroke-width`, `opacity`, `fill-opacity`, `stroke-opacity` | +| `ellipse` | `cx`, `cy`, `rx`, `ry`, `fill`, `stroke`, `stroke-width`, `opacity`, `fill-opacity`, `stroke-opacity` | +| `line` | `x1`, `y1`, `x2`, `y2`, `stroke`, `stroke-width`, `stroke-linecap`, `opacity`, `stroke-opacity` | +| `polyline`, `polygon` | `points`, `fill`, `stroke`, `stroke-width`, `stroke-linejoin`, `opacity`, `fill-opacity`, `stroke-opacity` | +| `path` | `d`, `fill`, `stroke`, `stroke-width`, `stroke-linecap`, `stroke-linejoin`, `fill-rule`, `opacity`, `fill-opacity`, `stroke-opacity` | +| `text`, `tspan` | `x`, `y`, `dx`, `dy`, `text-anchor`, `font-family`, `font-size`, `font-weight`, `fill`, `stroke`, `opacity` | +| `g`, `defs`, `symbol` | `transform`, `opacity` | +| `use` | `href`, `xlink:href`, `x`, `y`, `width`, `height`, `transform` | +| `linearGradient`, `radialGradient` | `id`, `gradientUnits`, `gradientTransform` | +| `linearGradient` | `x1`, `y1`, `x2`, `y2` | +| `radialGradient` | `cx`, `cy`, `r`, `fx`, `fy` | +| `stop` | `offset`, `stop-color`, `stop-opacity` | +| `clipPath`, `mask`, `pattern` | `id`, `clipPathUnits`, `maskUnits`, `patternUnits`, `patternContentUnits` | +| `pattern` | `x`, `y`, `width`, `height`, `viewBox` | +| `animate`, `animateTransform` | `attributeName`, `from`, `to`, `dur`, `repeatCount`, `type`, `values` | ### Special Keys @@ -1003,6 +1042,464 @@ The `$if` tag follows JavaScript truthiness when no operators are provided: - **Truthy:** `true`, non-empty strings, non-zero numbers, objects, arrays - **Falsy:** `false`, `null`, `undefined`, `0`, `""`, `NaN` +## SVG Support + +Treebark supports SVG (Scalable Vector Graphics) elements, allowing you to create data-driven visualizations, icons, and graphics within your templates. + +### Basic SVG + +Create simple SVG graphics using the `svg` tag and shape elements: + +```json +{ + "svg": { + "width": "100", + "height": "100", + "viewBox": "0 0 100 100", + "$children": [ + { + "circle": { + "cx": "50", + "cy": "50", + "r": "40", + "fill": "#3498db", + "stroke": "#2c3e50", + "stroke-width": "2" + } + } + ] + } +} +``` + +Output: +```html + + + +``` + +### Multiple Shapes + +Combine multiple SVG elements to create complex graphics: + +```json +{ + "svg": { + "width": "200", + "height": "200", + "viewBox": "0 0 200 200", + "$children": [ + { + "rect": { + "x": "10", + "y": "10", + "width": "180", + "height": "180", + "fill": "#ecf0f1", + "stroke": "#34495e", + "stroke-width": "2" + } + }, + { + "circle": { + "cx": "100", + "cy": "100", + "r": "50", + "fill": "#e74c3c" + } + }, + { + "line": { + "x1": "50", + "y1": "50", + "x2": "150", + "y2": "150", + "stroke": "#2c3e50", + "stroke-width": "3" + } + } + ] + } +} +``` + +### SVG Paths + +Use the `path` element for complex shapes: + +```json +{ + "svg": { + "width": "100", + "height": "100", + "viewBox": "0 0 100 100", + "$children": [ + { + "path": { + "d": "M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z", + "fill": "#e74c3c", + "stroke": "#c0392b", + "stroke-width": "2" + } + } + ] + } +} +``` + +### SVG Groups and Transform + +Organize related elements using the `g` tag and apply transformations: + +```json +{ + "svg": { + "width": "200", + "height": "200", + "viewBox": "0 0 200 200", + "$children": [ + { + "g": { + "transform": "translate(100, 100) rotate(45)", + "$children": [ + { + "rect": { + "x": "-25", + "y": "-25", + "width": "50", + "height": "50", + "fill": "#9b59b6" + } + } + ] + } + } + ] + } +} +``` + +### SVG with Data Binding + +Create dynamic, data-driven visualizations: + +```json +{ + "svg": { + "width": "400", + "height": "200", + "viewBox": "0 0 400 200", + "$children": [ + { + "rect": { + "$bind": "bars", + "x": "{{x}}", + "y": "{{y}}", + "width": "50", + "height": "{{height}}", + "fill": "{{color}}" + } + } + ] + } +} +``` + +Data: +```json +{ + "bars": [ + { "x": "10", "y": "50", "height": "150", "color": "#3498db" }, + { "x": "80", "y": "80", "height": "120", "color": "#2ecc71" }, + { "x": "150", "y": "30", "height": "170", "color": "#e74c3c" }, + { "x": "220", "y": "100", "height": "100", "color": "#f39c12" } + ] +} +``` + +Output: +```html + + + + + + +``` + +### SVG Text + +Add text elements to your SVG: + +```json +{ + "svg": { + "width": "300", + "height": "100", + "viewBox": "0 0 300 100", + "$children": [ + { + "text": { + "x": "150", + "y": "50", + "text-anchor": "middle", + "font-family": "Arial, sans-serif", + "font-size": "24", + "fill": "#2c3e50", + "$children": ["Hello SVG"] + } + } + ] + } +} +``` + +### SVG with Gradients + +Create gradients for fills: + +```json +{ + "svg": { + "width": "200", + "height": "200", + "viewBox": "0 0 200 200", + "$children": [ + { + "defs": { + "$children": [ + { + "linearGradient": { + "id": "grad1", + "x1": "0%", + "y1": "0%", + "x2": "100%", + "y2": "100%", + "$children": [ + { + "stop": { + "offset": "0%", + "stop-color": "#3498db", + "stop-opacity": "1" + } + }, + { + "stop": { + "offset": "100%", + "stop-color": "#2ecc71", + "stop-opacity": "1" + } + } + ] + } + } + ] + } + }, + { + "rect": { + "x": "10", + "y": "10", + "width": "180", + "height": "180", + "fill": "url(#grad1)" + } + } + ] + } +} +``` + +### Conditional SVG Elements + +Use conditional rendering with SVG elements: + +```json +{ + "svg": { + "width": "200", + "height": "200", + "viewBox": "0 0 200 200", + "$children": [ + { + "$if": { + "$check": "showCircle", + "$then": { + "circle": { + "cx": "100", + "cy": "100", + "r": "50", + "fill": "#3498db" + } + }, + "$else": { + "rect": { + "x": "50", + "y": "50", + "width": "100", + "height": "100", + "fill": "#e74c3c" + } + } + } + } + ] + } +} +``` + +### Interactive SVG with Data + +Create interactive data visualizations: + +```json +{ + "div": { + "class": "chart-container", + "$children": [ + { "h3": "Sales Data" }, + { + "svg": { + "width": "500", + "height": "300", + "viewBox": "0 0 500 300", + "$children": [ + { + "g": { + "$bind": "dataPoints", + "$children": [ + { + "circle": { + "cx": "{{x}}", + "cy": "{{y}}", + "r": "{{radius}}", + "fill": "{{color}}", + "opacity": "0.7" + } + }, + { + "text": { + "x": "{{x}}", + "y": "{{../..labelY}}", + "text-anchor": "middle", + "font-size": "12", + "fill": "#2c3e50", + "$children": ["{{label}}"] + } + } + ] + } + } + ] + } + } + ] + } +} +``` + +Data: +```json +{ + "labelY": "290", + "dataPoints": [ + { "x": "50", "y": "100", "radius": "20", "color": "#3498db", "label": "Jan" }, + { "x": "150", "y": "80", "radius": "25", "color": "#2ecc71", "label": "Feb" }, + { "x": "250", "y": "120", "radius": "18", "color": "#e74c3c", "label": "Mar" }, + { "x": "350", "y": "60", "radius": "30", "color": "#f39c12", "label": "Apr" } + ] +} +``` + +### SVG Icons + +Create reusable icon components with the `symbol` and `use` tags: + +```json +{ + "svg": { + "width": "200", + "height": "200", + "viewBox": "0 0 200 200", + "$children": [ + { + "defs": { + "$children": [ + { + "symbol": { + "id": "star", + "viewBox": "0 0 24 24", + "$children": [ + { + "path": { + "d": "M12 2 L15 9 L22 10 L17 15 L18 22 L12 18 L6 22 L7 15 L2 10 L9 9 Z", + "fill": "currentColor" + } + } + ] + } + } + ] + } + }, + { + "use": { + "href": "#star", + "x": "50", + "y": "50", + "width": "100", + "height": "100", + "fill": "#f39c12" + } + } + ] + } +} +``` + +### SVG Animations + +Add basic animations to SVG elements: + +```json +{ + "svg": { + "width": "200", + "height": "200", + "viewBox": "0 0 200 200", + "$children": [ + { + "circle": { + "cx": "100", + "cy": "100", + "r": "50", + "fill": "#3498db", + "$children": [ + { + "animate": { + "attributeName": "r", + "from": "30", + "to": "70", + "dur": "2s", + "repeatCount": "indefinite" + } + } + ] + } + } + ] + } +} +``` + +**Note on SVG:** +- SVG elements follow the same security model as HTML elements +- All SVG attributes are sanitized to prevent XSS attacks +- Data binding and conditional rendering work seamlessly with SVG +- SVG elements can be mixed with HTML elements in the same template +- The `xmlns` attribute is automatically added to `` root elements when needed + ## Error Handling Treebark follows a **no-throw policy**: instead of throwing exceptions, errors and warnings are sent to a logger. This allows your application to continue rendering even when there are invalid tags, attributes, or other issues. diff --git a/spec.md b/spec.md index 51e60f6..672f02e 100644 --- a/spec.md +++ b/spec.md @@ -163,13 +163,33 @@ div: - Attributes are plain key/value pairs. - Values may contain interpolations. -- Allowed: +- Allowed HTML attributes: - Global: `id`, `class`, `style`, `title`, `aria-*`, `data-*`, `role` - `a`: `href`, `target`, `rel` - `img`: `src`, `alt`, `width`, `height` - `table`: `summary` - `th`/`td`: `scope`, `colspan`, `rowspan` - `blockquote`: `cite` +- Allowed SVG attributes: + - Global SVG: `id`, `class`, `style`, `data-*` + - `svg`: `width`, `height`, `viewBox`, `preserveAspectRatio`, `xmlns` + - Shape presentation: `fill`, `stroke`, `stroke-width`, `opacity`, `fill-opacity`, `stroke-opacity` + - `circle`: `cx`, `cy`, `r` + - `rect`: `x`, `y`, `width`, `height`, `rx`, `ry` + - `ellipse`: `cx`, `cy`, `rx`, `ry` + - `line`: `x1`, `y1`, `x2`, `y2`, `stroke-linecap` + - `polyline`, `polygon`: `points`, `stroke-linejoin` + - `path`: `d`, `stroke-linecap`, `stroke-linejoin`, `fill-rule` + - `text`, `tspan`: `x`, `y`, `dx`, `dy`, `text-anchor`, `font-family`, `font-size`, `font-weight` + - `g`, `defs`, `symbol`: `transform` + - `use`: `href`, `xlink:href`, `x`, `y`, `width`, `height`, `transform` + - Gradients: `gradientUnits`, `gradientTransform` + - `linearGradient`: `x1`, `y1`, `x2`, `y2` + - `radialGradient`: `cx`, `cy`, `r`, `fx`, `fy` + - `stop`: `offset`, `stop-color`, `stop-opacity` + - `clipPath`, `mask`, `pattern`: `clipPathUnits`, `maskUnits`, `patternUnits`, `patternContentUnits` + - `pattern`: `x`, `y`, `width`, `height`, `viewBox` + - `animate`, `animateTransform`: `attributeName`, `from`, `to`, `dur`, `repeatCount`, `type`, `values` - Blocked: event handlers (`on*`), dangerous protocols (`javascript:`). --- @@ -336,6 +356,14 @@ JavaScript allows both `array[0]` and `array["0"]` syntax. Since the path is spl `table`, `thead`, `tbody`, `tr`, `th`, `td`, `a`, `img` +**SVG tags:** +`svg`, `g`, `defs`, `symbol`, `use`, +`circle`, `rect`, `ellipse`, `line`, `polyline`, `polygon`, `path`, +`text`, `tspan`, +`linearGradient`, `radialGradient`, `stop`, +`clipPath`, `mask`, `pattern`, +`animate`, `animateTransform` + **Special tags:** `comment`, `if` @@ -921,3 +949,455 @@ Conditional attributes support all the same modifiers and operators as `$if` tag - The `$if` tag **cannot** have regular HTML attributes (like `class`, `id`, etc.) - Only special operators (`$<`, `$>`, `$<=`, `$>=`, `$=`, `$in`) and modifiers (`$not`, `$join`) are allowed - If you need a wrapper element with attributes, use a regular tag inside the `$if` tag's children + +--- + +## 14. SVG Support + +Treebark supports SVG (Scalable Vector Graphics) elements with the same security model and data binding capabilities as HTML elements. + +### SVG Tags + +SVG elements are organized into several categories: + +**Container elements:** +- `svg` - Root SVG element +- `g` - Group element for organizing and transforming +- `defs` - Container for reusable elements +- `symbol` - Define reusable graphic templates +- `use` - Reference and reuse defined elements + +**Shape elements:** +- `circle` - Circular shape +- `rect` - Rectangle shape +- `ellipse` - Elliptical shape +- `line` - Straight line +- `polyline` - Connected line segments +- `polygon` - Closed shape with straight sides +- `path` - Complex shapes using path commands + +**Text elements:** +- `text` - Text content +- `tspan` - Styled text spans within text + +**Gradient elements:** +- `linearGradient` - Linear color gradient +- `radialGradient` - Radial color gradient +- `stop` - Gradient color stops + +**Effect elements:** +- `clipPath` - Clipping region +- `mask` - Masking effect +- `pattern` - Fill pattern + +**Animation elements:** +- `animate` - Animate attribute values +- `animateTransform` - Animate transformations + +### Basic SVG Example + +```javascript +{ + template: { + svg: { + width: "100", + height: "100", + viewBox: "0 0 100 100", + $children: [ + { + circle: { + cx: "50", + cy: "50", + r: "40", + fill: "#3498db", + stroke: "#2c3e50", + "stroke-width": "2" + } + } + ] + } + } +} +``` +→ `` + +### SVG Attributes + +**Root SVG attributes:** +- `width`, `height` - Dimensions of the SVG viewport +- `viewBox` - Coordinate system and aspect ratio +- `preserveAspectRatio` - How to scale the content +- `xmlns` - XML namespace (automatically added when needed) + +**Common presentation attributes** (most shape elements): +- `fill` - Fill color (hex, rgb, named colors, gradients) +- `stroke` - Stroke color +- `stroke-width` - Stroke thickness +- `opacity` - Overall opacity (0-1) +- `fill-opacity` - Fill opacity (0-1) +- `stroke-opacity` - Stroke opacity (0-1) + +**Position and size** (varies by element): +- Circle: `cx`, `cy`, `r` +- Rectangle: `x`, `y`, `width`, `height`, `rx`, `ry` +- Ellipse: `cx`, `cy`, `rx`, `ry` +- Line: `x1`, `y1`, `x2`, `y2` +- Path: `d` (path data) +- Polyline/Polygon: `points` + +**Transform attributes:** +- `transform` - Apply transformations (translate, rotate, scale, skew, matrix) + +**Text attributes:** +- `x`, `y` - Text position +- `dx`, `dy` - Relative positioning offset +- `text-anchor` - Horizontal alignment (start, middle, end) +- `font-family`, `font-size`, `font-weight` - Typography + +**Gradient attributes:** +- `gradientUnits` - Coordinate system for gradient +- `gradientTransform` - Gradient transformation +- Linear: `x1`, `y1`, `x2`, `y2` (start and end points) +- Radial: `cx`, `cy`, `r` (center and radius), `fx`, `fy` (focal point) + +**Stop attributes:** +- `offset` - Position in gradient (0-100%) +- `stop-color` - Color at this stop +- `stop-opacity` - Opacity at this stop + +**Use attributes:** +- `href` or `xlink:href` - Reference to element to reuse +- `x`, `y` - Position offset +- `width`, `height` - Size override + +**Animation attributes:** +- `attributeName` - Attribute to animate +- `from` - Starting value +- `to` - Ending value +- `dur` - Duration (e.g., "2s", "500ms") +- `repeatCount` - Number of repetitions ("indefinite" for continuous) +- `type` - Type of transformation (for animateTransform) +- `values` - List of values for keyframe animation + +### SVG with Data Binding + +SVG elements support full data binding capabilities: + +```javascript +{ + template: { + svg: { + width: "400", + height: "200", + viewBox: "0 0 400 200", + $children: [ + { + rect: { + $bind: "bars", + x: "{{x}}", + y: "{{y}}", + width: "50", + height: "{{height}}", + fill: "{{color}}" + } + } + ] + } + }, + data: { + bars: [ + { x: "10", y: "50", height: "150", color: "#3498db" }, + { x: "80", y: "80", height: "120", color: "#2ecc71" }, + { x: "150", y: "30", height: "170", color: "#e74c3c" } + ] + } +} +``` + +### SVG with Conditional Rendering + +Conditional logic works seamlessly with SVG: + +```javascript +{ + template: { + svg: { + width: "200", + height: "200", + viewBox: "0 0 200 200", + $children: [ + { + $if: { + $check: "showCircle", + $then: { + circle: { + cx: "100", + cy: "100", + r: "50", + fill: "#3498db" + } + }, + $else: { + rect: { + x: "50", + y: "50", + width: "100", + height: "100", + fill: "#e74c3c" + } + } + } + } + ] + } + }, + data: { showCircle: true } +} +``` + +### SVG Groups and Transforms + +Group related elements and apply transformations: + +```javascript +{ + template: { + svg: { + width: "200", + height: "200", + viewBox: "0 0 200 200", + $children: [ + { + g: { + transform: "translate(100, 100) rotate(45)", + $children: [ + { + rect: { + x: "-25", + y: "-25", + width: "50", + height: "50", + fill: "#9b59b6" + } + } + ] + } + } + ] + } + } +} +``` + +### SVG Gradients + +Define and use gradients: + +```javascript +{ + template: { + svg: { + width: "200", + height: "200", + viewBox: "0 0 200 200", + $children: [ + { + defs: { + $children: [ + { + linearGradient: { + id: "grad1", + x1: "0%", + y1: "0%", + x2: "100%", + y2: "100%", + $children: [ + { + stop: { + offset: "0%", + "stop-color": "#3498db", + "stop-opacity": "1" + } + }, + { + stop: { + offset: "100%", + "stop-color": "#2ecc71", + "stop-opacity": "1" + } + } + ] + } + } + ] + } + }, + { + rect: { + x: "10", + y: "10", + width: "180", + height: "180", + fill: "url(#grad1)" + } + } + ] + } + } +} +``` + +### SVG Symbol and Use + +Define reusable symbols: + +```javascript +{ + template: { + svg: { + width: "200", + height: "200", + viewBox: "0 0 200 200", + $children: [ + { + defs: { + $children: [ + { + symbol: { + id: "star", + viewBox: "0 0 24 24", + $children: [ + { + path: { + d: "M12 2 L15 9 L22 10 L17 15 L18 22 L12 18 L6 22 L7 15 L2 10 L9 9 Z", + fill: "currentColor" + } + } + ] + } + } + ] + } + }, + { + use: { + href: "#star", + x: "50", + y: "50", + width: "100", + height: "100", + fill: "#f39c12" + } + } + ] + } + } +} +``` + +### SVG Parent Context Access + +Access parent data in nested SVG structures: + +```javascript +{ + template: { + svg: { + width: "500", + height: "300", + viewBox: "0 0 500 300", + $children: [ + { + g: { + $bind: "dataPoints", + $children: [ + { + circle: { + cx: "{{x}}", + cy: "{{y}}", + r: "{{radius}}", + fill: "{{color}}" + } + }, + { + text: { + x: "{{x}}", + y: "{{../..labelY}}", + "text-anchor": "middle", + $children: ["{{label}}"] + } + } + ] + } + } + ] + } + }, + data: { + labelY: "290", + dataPoints: [ + { x: "50", y: "100", radius: "20", color: "#3498db", label: "A" }, + { x: "150", y: "80", radius: "25", color: "#2ecc71", label: "B" } + ] + } +} +``` + +### Security Considerations + +SVG follows the same security model as HTML: +- All attributes are sanitized to prevent XSS attacks +- Dangerous patterns in URLs are blocked (e.g., `javascript:`) +- Event handlers (`onclick`, etc.) are not allowed +- The renderer uses secure methods to create and populate SVG elements +- `xlink:href` is supported for backwards compatibility but sanitized + +### Mixing HTML and SVG + +SVG elements can be embedded within HTML structures: + +```javascript +{ + template: { + div: { + class: "card", + $children: [ + { h2: "Chart" }, + { + svg: { + width: "200", + height: "100", + viewBox: "0 0 200 100", + $children: [ + { + rect: { + x: "10", + y: "10", + width: "180", + height: "80", + fill: "#ecf0f1" + } + } + ] + } + }, + { p: "Data visualization" } + ] + } + } +} +``` + +### SVG Void Elements + +Most SVG elements are container elements and can have children. The following SVG elements are treated as void elements (self-closing): +- Individual shapes without child content (`circle`, `rect`, `ellipse`, `line`, `polyline`, `polygon`) +- `use` (when not containing fallback content) + +However, some SVG elements commonly have children: +- `svg`, `g`, `defs`, `symbol` - Always containers +- `text` - Contains text or `tspan` elements +- `path` - Can contain animation elements +- Gradient elements - Contain `stop` elements + +The implementation determines whether to self-close based on the presence of `$children`. From 453186144e24e41dda1d330e2f4fba20e24f9aad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 06:24:12 +0000 Subject: [PATCH 3/3] Fix SVG data binding examples to use correct $bind/$children pattern Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- README.md | 34 ++++++++++++++++++++++++---------- spec.md | 18 ++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 47d3dbf..883de42 100644 --- a/README.md +++ b/README.md @@ -1192,13 +1192,19 @@ Create dynamic, data-driven visualizations: "viewBox": "0 0 400 200", "$children": [ { - "rect": { + "g": { "$bind": "bars", - "x": "{{x}}", - "y": "{{y}}", - "width": "50", - "height": "{{height}}", - "fill": "{{color}}" + "$children": [ + { + "rect": { + "x": "{{x}}", + "y": "{{y}}", + "width": "50", + "height": "{{height}}", + "fill": "{{color}}" + } + } + ] } } ] @@ -1221,10 +1227,18 @@ Data: Output: ```html - - - - + + + + + + + + + + + + ``` diff --git a/spec.md b/spec.md index 672f02e..0cfc5b6 100644 --- a/spec.md +++ b/spec.md @@ -1092,13 +1092,19 @@ SVG elements support full data binding capabilities: viewBox: "0 0 400 200", $children: [ { - rect: { + g: { $bind: "bars", - x: "{{x}}", - y: "{{y}}", - width: "50", - height: "{{height}}", - fill: "{{color}}" + $children: [ + { + rect: { + x: "{{x}}", + y: "{{y}}", + width: "50", + height: "{{height}}", + fill: "{{color}}" + } + } + ] } } ]