diff --git a/README.md b/README.md
index e2ffd8a..90b0940 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,29 @@ Visit: https://react.nodegui.org for docs.
- https://blog.logrocket.com/electron-alternatives-exploring-nodegui-and-react-nodegui/ - Electron alternatives: Exploring NodeGUI and React NodeGUI by [Siegfried Grimbeek](https://blog.logrocket.com/author/siegfriedgrimbeek/).
+## SVG
+
+React NodeGUI can render inline SVG trees by serializing SVG-like React components and rendering the result with Qt's SVG widget.
+
+```jsx
+import React from "react";
+import { Renderer, Svg, Rect, Circle, Path, Window } from "@nodegui/react-nodegui";
+
+const App = () => (
+
+
+
+);
+
+Renderer.render();
+```
+
+For complete SVG documents, pass `src`, `buffer`, or `content` to `Svg`.
+
**Talks/Podcasts**
- [NodeGui and React NodeGui at KarmaJS Nov 2019 meetup: https://www.youtube.com/watch?v=8jH5gaEEDv4](https://www.youtube.com/watch?v=8jH5gaEEDv4)
diff --git a/src/components/Svg/RNSvg.ts b/src/components/Svg/RNSvg.ts
new file mode 100644
index 0000000..8ce7732
--- /dev/null
+++ b/src/components/Svg/RNSvg.ts
@@ -0,0 +1,578 @@
+import {
+ QSvgWidget,
+ QWidgetSignals,
+} from "@nodegui/nodegui";
+import { Component } from "@nodegui/nodegui/dist/lib/core/Component";
+import { RNComponent, RNProps, RNWidget } from "../config";
+import { ViewProps, setViewProps } from "../View/RNView";
+
+type SvgPrimitive = string | number | boolean;
+type SvgStyle = string | Record;
+type SvgPropValue =
+ | SvgPrimitive
+ | SvgStyle
+ | null
+ | undefined
+ | Record;
+
+export interface SvgProps extends ViewProps {
+ src?: string;
+ buffer?: Buffer;
+ content?: string;
+ children?: unknown;
+ width?: SvgPrimitive;
+ height?: SvgPrimitive;
+ viewBox?: string;
+ preserveAspectRatio?: string;
+ [attribute: string]: SvgPropValue | unknown;
+}
+
+export interface SvgElementProps extends RNProps {
+ children?: unknown;
+ style?: SvgStyle;
+ id?: string;
+ className?: string;
+ fill?: SvgPrimitive;
+ stroke?: SvgPrimitive;
+ strokeWidth?: SvgPrimitive;
+ opacity?: SvgPrimitive;
+ transform?: string;
+ [attribute: string]: SvgPropValue | unknown;
+}
+
+export interface SvgRectProps extends SvgElementProps {
+ x?: SvgPrimitive;
+ y?: SvgPrimitive;
+ width?: SvgPrimitive;
+ height?: SvgPrimitive;
+ rx?: SvgPrimitive;
+ ry?: SvgPrimitive;
+}
+
+export interface SvgCircleProps extends SvgElementProps {
+ cx?: SvgPrimitive;
+ cy?: SvgPrimitive;
+ r?: SvgPrimitive;
+}
+
+export interface SvgEllipseProps extends SvgElementProps {
+ cx?: SvgPrimitive;
+ cy?: SvgPrimitive;
+ rx?: SvgPrimitive;
+ ry?: SvgPrimitive;
+}
+
+export interface SvgLineProps extends SvgElementProps {
+ x1?: SvgPrimitive;
+ y1?: SvgPrimitive;
+ x2?: SvgPrimitive;
+ y2?: SvgPrimitive;
+}
+
+export interface SvgPolygonProps extends SvgElementProps {
+ points?: string;
+}
+
+export interface SvgPolylineProps extends SvgElementProps {
+ points?: string;
+}
+
+export interface SvgPathProps extends SvgElementProps {
+ d?: string;
+}
+
+export interface SvgTextProps extends SvgElementProps {
+ x?: SvgPrimitive;
+ y?: SvgPrimitive;
+ dx?: SvgPrimitive;
+ dy?: SvgPrimitive;
+ textAnchor?: string;
+ fontFamily?: string;
+ fontSize?: SvgPrimitive;
+ fontWeight?: SvgPrimitive;
+}
+
+type DangerousHtml = {
+ __html?: string;
+};
+
+const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
+
+const WIDGET_PROP_NAMES = new Set([
+ "visible",
+ "styleSheet",
+ "geometry",
+ "id",
+ "mouseTracking",
+ "enabled",
+ "windowOpacity",
+ "windowTitle",
+ "windowState",
+ "cursor",
+ "windowIcon",
+ "minSize",
+ "maxSize",
+ "size",
+ "pos",
+ "on",
+ "attributes",
+ "windowFlags",
+]);
+
+const IGNORED_SVG_PROPS = new Set([
+ "children",
+ "key",
+ "ref",
+ "__self",
+ "__source",
+ "src",
+ "buffer",
+ "content",
+ "dangerouslySetInnerHTML",
+ "visible",
+ "styleSheet",
+ "geometry",
+ "mouseTracking",
+ "enabled",
+ "windowOpacity",
+ "windowTitle",
+ "windowState",
+ "cursor",
+ "windowIcon",
+ "minSize",
+ "maxSize",
+ "size",
+ "pos",
+ "on",
+ "attributes",
+ "windowFlags",
+]);
+
+const SVG_ATTRIBUTE_NAMES: Record = {
+ acceptCharset: "accept-charset",
+ accentHeight: "accent-height",
+ alignmentBaseline: "alignment-baseline",
+ arabicForm: "arabic-form",
+ baselineShift: "baseline-shift",
+ capHeight: "cap-height",
+ className: "class",
+ clipPath: "clip-path",
+ clipRule: "clip-rule",
+ colorInterpolation: "color-interpolation",
+ colorInterpolationFilters: "color-interpolation-filters",
+ dominantBaseline: "dominant-baseline",
+ enableBackground: "enable-background",
+ fillOpacity: "fill-opacity",
+ fillRule: "fill-rule",
+ floodColor: "flood-color",
+ floodOpacity: "flood-opacity",
+ fontFamily: "font-family",
+ fontSize: "font-size",
+ fontSizeAdjust: "font-size-adjust",
+ fontStretch: "font-stretch",
+ fontStyle: "font-style",
+ fontVariant: "font-variant",
+ fontWeight: "font-weight",
+ glyphName: "glyph-name",
+ horizAdvX: "horiz-adv-x",
+ horizOriginX: "horiz-origin-x",
+ imageRendering: "image-rendering",
+ letterSpacing: "letter-spacing",
+ lightingColor: "lighting-color",
+ markerEnd: "marker-end",
+ markerMid: "marker-mid",
+ markerStart: "marker-start",
+ overlinePosition: "overline-position",
+ overlineThickness: "overline-thickness",
+ paintOrder: "paint-order",
+ pointerEvents: "pointer-events",
+ shapeRendering: "shape-rendering",
+ stopColor: "stop-color",
+ stopOpacity: "stop-opacity",
+ strikethroughPosition: "strikethrough-position",
+ strikethroughThickness: "strikethrough-thickness",
+ strokeDasharray: "stroke-dasharray",
+ strokeDashoffset: "stroke-dashoffset",
+ strokeLinecap: "stroke-linecap",
+ strokeLinejoin: "stroke-linejoin",
+ strokeMiterlimit: "stroke-miterlimit",
+ strokeOpacity: "stroke-opacity",
+ strokeWidth: "stroke-width",
+ textAnchor: "text-anchor",
+ textDecoration: "text-decoration",
+ textRendering: "text-rendering",
+ underlinePosition: "underline-position",
+ underlineThickness: "underline-thickness",
+ unicodeBidi: "unicode-bidi",
+ unicodeRange: "unicode-range",
+ vectorEffect: "vector-effect",
+ vertAdvY: "vert-adv-y",
+ vertOriginX: "vert-origin-x",
+ vertOriginY: "vert-origin-y",
+ wordSpacing: "word-spacing",
+ writingMode: "writing-mode",
+ xHeight: "x-height",
+ xlinkActuate: "xlink:actuate",
+ xlinkArcrole: "xlink:arcrole",
+ xlinkHref: "xlink:href",
+ xlinkRole: "xlink:role",
+ xlinkShow: "xlink:show",
+ xlinkTitle: "xlink:title",
+ xlinkType: "xlink:type",
+ xmlBase: "xml:base",
+ xmlLang: "xml:lang",
+ xmlSpace: "xml:space",
+};
+
+const CASE_SENSITIVE_SVG_ATTRIBUTES = new Set([
+ "attributeName",
+ "baseFrequency",
+ "calcMode",
+ "clipPathUnits",
+ "diffuseConstant",
+ "edgeMode",
+ "filterUnits",
+ "gradientTransform",
+ "gradientUnits",
+ "kernelMatrix",
+ "kernelUnitLength",
+ "keyPoints",
+ "keySplines",
+ "keyTimes",
+ "lengthAdjust",
+ "limitingConeAngle",
+ "markerHeight",
+ "markerUnits",
+ "markerWidth",
+ "maskContentUnits",
+ "maskUnits",
+ "numOctaves",
+ "pathLength",
+ "patternContentUnits",
+ "patternTransform",
+ "patternUnits",
+ "pointsAtX",
+ "pointsAtY",
+ "pointsAtZ",
+ "preserveAlpha",
+ "preserveAspectRatio",
+ "primitiveUnits",
+ "refX",
+ "refY",
+ "repeatCount",
+ "repeatDur",
+ "requiredExtensions",
+ "requiredFeatures",
+ "specularConstant",
+ "specularExponent",
+ "spreadMethod",
+ "startOffset",
+ "stdDeviation",
+ "surfaceScale",
+ "systemLanguage",
+ "tableValues",
+ "targetX",
+ "targetY",
+ "viewBox",
+ "viewTarget",
+]);
+
+/**
+ * @ignore
+ */
+export class RNSvg extends QSvgWidget implements RNWidget {
+ static tagName = "svg";
+ private props: SvgProps = {};
+ private svgChildren: RNSvgElement[] = [];
+
+ setProps(newProps: SvgProps, oldProps: SvgProps): void {
+ this.props = newProps;
+
+ setViewProps(this, getWidgetProps(newProps), getWidgetProps(oldProps));
+ this.renderSvg();
+ }
+
+ appendInitialChild(child: any): void {
+ this.appendChild(child);
+ }
+
+ appendChild(child: any): void {
+ if (!isSvgElement(child)) {
+ return;
+ }
+
+ child.setSvgParent(this);
+ this.svgChildren.push(child);
+ this.renderSvg();
+ }
+
+ insertBefore(child: any, beforeChild: any): void {
+ if (!isSvgElement(child)) {
+ return;
+ }
+
+ child.setSvgParent(this);
+ const childIndex = this.svgChildren.indexOf(beforeChild);
+
+ if (childIndex === -1) {
+ this.svgChildren.push(child);
+ } else {
+ this.svgChildren.splice(childIndex, 0, child);
+ }
+
+ this.renderSvg();
+ }
+
+ removeChild(child: any): void {
+ const childIndex = this.svgChildren.indexOf(child);
+
+ if (childIndex === -1) {
+ return;
+ }
+
+ child.setSvgParent(null);
+ this.svgChildren.splice(childIndex, 1);
+ this.renderSvg();
+ }
+
+ requestRender(): void {
+ this.renderSvg();
+ }
+
+ toSvgString(): string {
+ return serializeElement("svg", this.props, this.svgChildren);
+ }
+
+ private renderSvg(): void {
+ if (this.props.buffer instanceof Buffer) {
+ this.load(this.props.buffer);
+ return;
+ }
+
+ if (typeof this.props.src === "string" && this.props.src) {
+ this.load(this.props.src);
+ return;
+ }
+
+ const svg = typeof this.props.content === "string"
+ ? this.props.content
+ : this.toSvgString();
+
+ this.load(Buffer.from(svg));
+ }
+}
+
+/**
+ * @ignore
+ */
+export class RNSvgElement extends Component implements RNComponent {
+ private props: SvgElementProps = {};
+ private svgChildren: RNSvgElement[] = [];
+ private svgParent: RNSvg | RNSvgElement | null = null;
+
+ constructor(private readonly svgTagName: string) {
+ super({ type: "native" });
+ }
+
+ setProps(newProps: SvgElementProps): void {
+ this.props = newProps;
+ this.requestRender();
+ }
+
+ appendInitialChild(child: any): void {
+ this.appendChild(child);
+ }
+
+ appendChild(child: any): void {
+ if (!isSvgElement(child)) {
+ return;
+ }
+
+ child.setSvgParent(this);
+ this.svgChildren.push(child);
+ this.requestRender();
+ }
+
+ insertBefore(child: any, beforeChild: any): void {
+ if (!isSvgElement(child)) {
+ return;
+ }
+
+ child.setSvgParent(this);
+ const childIndex = this.svgChildren.indexOf(beforeChild);
+
+ if (childIndex === -1) {
+ this.svgChildren.push(child);
+ } else {
+ this.svgChildren.splice(childIndex, 0, child);
+ }
+
+ this.requestRender();
+ }
+
+ removeChild(child: any): void {
+ const childIndex = this.svgChildren.indexOf(child);
+
+ if (childIndex === -1) {
+ return;
+ }
+
+ child.setSvgParent(null);
+ this.svgChildren.splice(childIndex, 1);
+ this.requestRender();
+ }
+
+ setSvgParent(parent: RNSvg | RNSvgElement | null): void {
+ this.svgParent = parent;
+ }
+
+ requestRender(): void {
+ if (this.svgParent) {
+ this.svgParent.requestRender();
+ }
+ }
+
+ toSvgString(): string {
+ return serializeElement(this.svgTagName, this.props, this.svgChildren);
+ }
+}
+
+export function createSvgElement(svgTagName: string, props: SvgElementProps): RNSvgElement {
+ const element = new RNSvgElement(svgTagName);
+ element.setProps(props);
+ return element;
+}
+
+function getWidgetProps(props: SvgProps): ViewProps {
+ return Object.keys(props || {}).reduce((widgetProps, key) => {
+ if (WIDGET_PROP_NAMES.has(key)) {
+ (widgetProps as Record)[key] = props[key];
+ }
+
+ return widgetProps;
+ }, {} as ViewProps);
+}
+
+function isSvgElement(value: unknown): value is RNSvgElement {
+ return value instanceof RNSvgElement;
+}
+
+function serializeElement(
+ tagName: string,
+ props: SvgProps | SvgElementProps,
+ children: RNSvgElement[]
+): string {
+ const attributes = serializeAttributes(tagName, props);
+ const innerSvg = serializeInnerSvg(props, children);
+
+ if (innerSvg) {
+ return `<${tagName}${attributes}>${innerSvg}${tagName}>`;
+ }
+
+ return `<${tagName}${attributes}/>`;
+}
+
+function serializeInnerSvg(
+ props: SvgProps | SvgElementProps,
+ children: RNSvgElement[]
+): string {
+ const dangerousHtml = props.dangerouslySetInnerHTML as DangerousHtml | undefined;
+
+ if (typeof dangerousHtml?.__html === "string") {
+ return dangerousHtml.__html;
+ }
+
+ return [
+ serializeTextChildren(props.children),
+ ...children.map(child => child.toSvgString()),
+ ].join("");
+}
+
+function serializeTextChildren(children: unknown): string {
+ if (typeof children === "string" || typeof children === "number") {
+ return escapeText(children);
+ }
+
+ if (Array.isArray(children)) {
+ return children
+ .filter(child => typeof child === "string" || typeof child === "number")
+ .map(child => escapeText(child as string | number))
+ .join("");
+ }
+
+ return "";
+}
+
+function serializeAttributes(tagName: string, props: SvgProps | SvgElementProps): string {
+ const attributes = Object.keys(props || {}).reduce((result, key) => {
+ if (IGNORED_SVG_PROPS.has(key) || key.startsWith("on")) {
+ return result;
+ }
+
+ const value = (props as Record)[key];
+
+ if (value === null || value === undefined || value === false) {
+ return result;
+ }
+
+ const attributeName = getSvgAttributeName(key);
+ const attributeValue = key === "style" && typeof value === "object"
+ ? serializeStyle(value as Record)
+ : String(value);
+
+ if (!attributeValue) {
+ return result;
+ }
+
+ result.push(value === true ? attributeName : `${attributeName}="${escapeAttribute(attributeValue)}"`);
+ return result;
+ }, [] as string[]);
+
+ if (tagName === "svg" && !("xmlns" in (props || {}))) {
+ attributes.unshift(`xmlns="${SVG_NAMESPACE}"`);
+ }
+
+ return attributes.length > 0 ? ` ${attributes.join(" ")}` : "";
+}
+
+function getSvgAttributeName(propName: string): string {
+ if (SVG_ATTRIBUTE_NAMES[propName]) {
+ return SVG_ATTRIBUTE_NAMES[propName];
+ }
+
+ if (
+ CASE_SENSITIVE_SVG_ATTRIBUTES.has(propName)
+ || propName.startsWith("data-")
+ || propName.startsWith("aria-")
+ ) {
+ return propName;
+ }
+
+ return propName.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`);
+}
+
+function serializeStyle(style: Record): string {
+ return Object.keys(style)
+ .reduce((result, key) => {
+ const value = style[key];
+
+ if (value === null || value === undefined || value === false) {
+ return result;
+ }
+
+ result.push(`${getSvgAttributeName(key)}:${String(value)}`);
+ return result;
+ }, [] as string[])
+ .join(";");
+}
+
+function escapeText(value: string | number): string {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">");
+}
+
+function escapeAttribute(value: string): string {
+ return escapeText(value).replace(/"/g, """);
+}
diff --git a/src/components/Svg/index.ts b/src/components/Svg/index.ts
new file mode 100644
index 0000000..5803dc0
--- /dev/null
+++ b/src/components/Svg/index.ts
@@ -0,0 +1,172 @@
+import { Fiber } from "react-reconciler";
+import { AppContainer } from "../../reconciler";
+import { ComponentConfig, RNComponent, RNProps, registerComponent } from "../config";
+import {
+ RNSvg,
+ RNSvgElement,
+ SvgCircleProps,
+ SvgElementProps,
+ SvgEllipseProps,
+ SvgLineProps,
+ SvgPathProps,
+ SvgPolygonProps,
+ SvgPolylineProps,
+ SvgProps,
+ SvgRectProps,
+ SvgTextProps,
+ createSvgElement,
+} from "./RNSvg";
+
+class SvgConfig extends ComponentConfig {
+ tagName = RNSvg.tagName;
+
+ shouldSetTextContent(nextProps: SvgProps): boolean {
+ return hasTextChildren(nextProps);
+ }
+
+ createInstance(
+ newProps: SvgProps,
+ rootInstance: AppContainer,
+ context: any,
+ workInProgress: Fiber
+ ): RNSvg {
+ const widget = new RNSvg();
+ widget.setProps(newProps, {});
+ return widget;
+ }
+
+ commitMount(
+ instance: RNSvg,
+ newProps: SvgProps,
+ internalInstanceHandle: any
+ ): void {
+ if (newProps.visible !== false) {
+ instance.show();
+ }
+ }
+
+ commitUpdate(
+ instance: RNSvg,
+ updatePayload: any,
+ oldProps: SvgProps,
+ newProps: SvgProps,
+ finishedWork: Fiber
+ ): void {
+ instance.setProps(newProps, oldProps);
+ }
+}
+
+class SvgElementConfig extends ComponentConfig {
+ constructor(
+ readonly tagName: string,
+ private readonly svgTagName = tagName
+ ) {
+ super();
+ }
+
+ shouldSetTextContent(nextProps: SvgElementProps): boolean {
+ return hasTextChildren(nextProps);
+ }
+
+ createInstance(
+ newProps: SvgElementProps,
+ rootInstance: AppContainer,
+ context: any,
+ workInProgress: Fiber
+ ): RNSvgElement {
+ return createSvgElement(this.svgTagName, newProps);
+ }
+
+ commitUpdate(
+ instance: RNComponent,
+ updatePayload: any,
+ oldProps: RNProps,
+ newProps: RNProps,
+ finishedWork: Fiber
+ ): void {
+ instance.setProps(newProps, oldProps);
+ }
+}
+
+function hasTextChildren(props: RNProps): boolean {
+ const children = (props as { children?: unknown }).children;
+ return typeof children === "string" || typeof children === "number";
+}
+
+function registerSvgElement(
+ tagName: string,
+ svgTagName = tagName
+) {
+ registeredSvgTags.add(tagName);
+ return registerComponent(new SvgElementConfig(tagName, svgTagName));
+}
+
+const registeredSvgTags = new Set();
+
+export const Svg = registerComponent(new SvgConfig());
+export const G = registerSvgElement("g");
+export const Group = G;
+export const Rect = registerSvgElement("rect");
+export const Circle = registerSvgElement("circle");
+export const Ellipse = registerSvgElement("ellipse");
+export const Line = registerSvgElement("line");
+export const Polygon = registerSvgElement("polygon");
+export const Polyline = registerSvgElement("polyline");
+export const Path = registerSvgElement("path");
+export const SvgText = registerSvgElement("svgText", "text");
+
+const svgElementTags = [
+ "a",
+ "animate",
+ "animateMotion",
+ "animateTransform",
+ "clipPath",
+ "defs",
+ "desc",
+ "feBlend",
+ "feColorMatrix",
+ "feComponentTransfer",
+ "feComposite",
+ "feConvolveMatrix",
+ "feDiffuseLighting",
+ "feDisplacementMap",
+ "feDistantLight",
+ "feDropShadow",
+ "feFlood",
+ "feFuncA",
+ "feFuncB",
+ "feFuncG",
+ "feFuncR",
+ "feGaussianBlur",
+ "feImage",
+ "feMerge",
+ "feMergeNode",
+ "feMorphology",
+ "feOffset",
+ "fePointLight",
+ "feSpecularLighting",
+ "feSpotLight",
+ "feTile",
+ "feTurbulence",
+ "filter",
+ "foreignObject",
+ "linearGradient",
+ "marker",
+ "mask",
+ "metadata",
+ "pattern",
+ "radialGradient",
+ "stop",
+ "style",
+ "switch",
+ "symbol",
+ "title",
+ "tspan",
+ "use",
+];
+
+for (const tagName of svgElementTags) {
+ if (!registeredSvgTags.has(tagName)) {
+ registerSvgElement(tagName);
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 78e18b8..6f06f19 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,6 +9,19 @@ export { Window } from "./components/Window";
export { Text } from "./components/Text";
export { Image } from "./components/Image";
export { AnimatedImage } from "./components/AnimatedImage";
+export {
+ Svg,
+ G,
+ Group,
+ Rect,
+ Circle,
+ Ellipse,
+ Line,
+ Polygon,
+ Polyline,
+ Path,
+ SvgText,
+} from "./components/Svg";
export { Button } from "./components/Button";
export { CheckBox } from "./components/CheckBox";
export { LineEdit } from "./components/LineEdit";