diff --git a/api.oas3.yaml b/api.oas3.yaml index 85d8f46..c91d474 100644 --- a/api.oas3.yaml +++ b/api.oas3.yaml @@ -199,6 +199,69 @@ components: TitleAsset: $ref: "./schemas/titleasset.yaml#/TitleAsset" + SvgAsset: + $ref: "./schemas/svgasset.yaml#/SvgAsset" + + SvgShape: + $ref: "./schemas/svgshapes.yaml#/SvgShape" + + SvgRectangleShape: + $ref: "./schemas/svgshapes.yaml#/SvgRectangleShape" + + SvgCircleShape: + $ref: "./schemas/svgshapes.yaml#/SvgCircleShape" + + SvgEllipseShape: + $ref: "./schemas/svgshapes.yaml#/SvgEllipseShape" + + SvgLineShape: + $ref: "./schemas/svgshapes.yaml#/SvgLineShape" + + SvgPolygonShape: + $ref: "./schemas/svgshapes.yaml#/SvgPolygonShape" + + SvgStarShape: + $ref: "./schemas/svgshapes.yaml#/SvgStarShape" + + SvgArrowShape: + $ref: "./schemas/svgshapes.yaml#/SvgArrowShape" + + SvgHeartShape: + $ref: "./schemas/svgshapes.yaml#/SvgHeartShape" + + SvgCrossShape: + $ref: "./schemas/svgshapes.yaml#/SvgCrossShape" + + SvgRingShape: + $ref: "./schemas/svgshapes.yaml#/SvgRingShape" + + SvgPathShape: + $ref: "./schemas/svgshapes.yaml#/SvgPathShape" + + SvgFill: + $ref: "./schemas/svgproperties.yaml#/SvgFill" + + SvgSolidFill: + $ref: "./schemas/svgproperties.yaml#/SvgSolidFill" + + SvgLinearGradientFill: + $ref: "./schemas/svgproperties.yaml#/SvgLinearGradientFill" + + SvgRadialGradientFill: + $ref: "./schemas/svgproperties.yaml#/SvgRadialGradientFill" + + SvgGradientStop: + $ref: "./schemas/svgproperties.yaml#/SvgGradientStop" + + SvgStroke: + $ref: "./schemas/svgproperties.yaml#/SvgStroke" + + SvgShadow: + $ref: "./schemas/svgproperties.yaml#/SvgShadow" + + SvgTransform: + $ref: "./schemas/svgproperties.yaml#/SvgTransform" + Transition: $ref: "./schemas/transition.yaml#/Transition" diff --git a/package.json b/package.json index b7ca647..d8d5c61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shotstack/schemas", - "version": "1.3.5", + "version": "1.4.1", "description": "Centralized OpenAPI schemas and TypeScript types for Shotstack API", "type": "module", "main": "dist/index.js", diff --git a/schemas/asset.yaml b/schemas/asset.yaml index adbae25..364ee10 100644 --- a/schemas/asset.yaml +++ b/schemas/asset.yaml @@ -16,6 +16,7 @@ Asset: html: '#/components/schemas/HtmlAsset' title: '#/components/schemas/TitleAsset' shape: '#/components/schemas/ShapeAsset' + svg: '#/components/schemas/SvgAsset' text-to-image: '#/components/schemas/TextToImageAsset' image-to-video: '#/components/schemas/ImageToVideoAsset' oneOf: @@ -29,6 +30,7 @@ Asset: - $ref: './htmlasset.yaml#/HtmlAsset' - $ref: './titleasset.yaml#/TitleAsset' - $ref: './shapeasset.yaml#/ShapeAsset' + - $ref: './svgasset.yaml#/SvgAsset' - $ref: './texttoimageasset.yaml#/TextToImageAsset' - $ref: './imagetovideoasset.yaml#/ImageToVideoAsset' additionalProperties: false diff --git a/schemas/ingest/responses/sourceresponseattributes.yaml b/schemas/ingest/responses/sourceresponseattributes.yaml index e525865..5745453 100644 --- a/schemas/ingest/responses/sourceresponseattributes.yaml +++ b/schemas/ingest/responses/sourceresponseattributes.yaml @@ -51,7 +51,7 @@ example: 1920 height: description: The height in pixels of the ingested source file, if a video or image. - type: string + type: integer example: 1080 duration: description: The duration in seconds of the ingested source file, if a video or audio file. diff --git a/schemas/rotatetransformation.yaml b/schemas/rotatetransformation.yaml index 85a68ab..3fcfbd8 100644 --- a/schemas/rotatetransformation.yaml +++ b/schemas/rotatetransformation.yaml @@ -9,7 +9,7 @@ Rotate a clip by the specified angle in degrees. Use a number or an array of [Tween](./#tocs_tween) objects to create a custom animation. oneOf: - - type: integer + - type: number description: >- The angle to rotate the clip. Can be 0 to 360, or 0 to -360. Using a positive number rotates the clip clockwise, negative numbers diff --git a/schemas/svgasset.yaml b/schemas/svgasset.yaml new file mode 100644 index 0000000..eeccc15 --- /dev/null +++ b/schemas/svgasset.yaml @@ -0,0 +1,173 @@ +SvgAsset: + description: | + The SvgAsset is used to add scalable vector graphics (SVG) shapes to a video. + It provides two mutually exclusive ways to define shapes: + + **Option 1: Import SVG markup using `src`** + ```json + { + "type": "svg", + "src": "" + } + ``` + When using `src`, no other properties are allowed. The fill, stroke, and dimensions + are automatically extracted from the SVG markup. + + **Option 2: Define shapes programmatically using `shape`** + ```json + { + "type": "svg", + "shape": { "type": "circle", "radius": 50 }, + "fill": { "type": "solid", "color": "#FF0000" } + } + ``` + When using `shape`, you can customize fill, stroke, shadow, transform, and other properties. + The `src` property is not allowed in this mode. + + **Important:** You must provide either `src` OR `shape`, but not both. + These two modes are mutually exclusive. + + **Available Shapes (Option 2 only):** + - `rectangle` - Rectangles with optional rounded corners + - `circle` - Perfect circles + - `ellipse` - Ellipses/ovals with separate x and y radii + - `line` - Straight lines with configurable thickness + - `polygon` - Regular polygons (triangle, pentagon, hexagon, etc.) + - `star` - Multi-pointed stars + - `arrow` - Directional arrows + - `heart` - Heart shapes + - `cross` - Plus/cross shapes + - `ring` - Donut/ring shapes + - `path` - Custom shapes using SVG path data + + See [W3C SVG 2 Specification](https://www.w3.org/TR/SVG2/) for path data syntax. + type: object + properties: + type: + description: The asset type - set to `svg` for SVG shapes. + type: string + enum: + - svg + default: svg + example: svg + src: + description: | + Raw SVG markup string to import. When provided, the shape is extracted + automatically from the SVG content. + + **Supported elements:** ``, ``, ``, ``, + ``, ``, `` + + **Automatically extracted:** + - Path data (converted to a single combined path) + - Fill color (from `fill` attribute or `style`) + - Stroke color and width (from attributes or `style`) + - Dimensions (from `width`/`height` or `viewBox`) + - Opacity (from `opacity` attribute) + + **Important:** When using `src`, no other properties (shape, fill, stroke, etc.) + are allowed. All styling must be defined within the SVG markup itself. + type: string + minLength: 1 + maxLength: 500000 + example: '' + shape: + description: | + The shape definition using primitives. The `type` property within determines + the shape kind and its specific properties. + + **Important:** When using `shape`, the `src` property is not allowed. + $ref: "./svgshapes.yaml#/SvgShape" + fill: + description: | + Fill properties for the shape interior. + Can be a solid color or a gradient (linear/radial). + If omitted, the shape will have no fill (transparent interior). + + **Note:** Only allowed when using `shape`, not with `src`. + $ref: "./svgproperties.yaml#/SvgFill" + stroke: + description: | + Stroke (outline) properties for the shape. + If omitted, the shape will have no stroke (no outline). + + **Note:** Only allowed when using `shape`, not with `src`. + $ref: "./svgproperties.yaml#/SvgStroke" + shadow: + description: | + Drop shadow properties for the shape. + Creates a shadow effect behind the shape. + + **Note:** Only allowed when using `shape`, not with `src`. + $ref: "./svgproperties.yaml#/SvgShadow" + transform: + description: | + Transform properties for positioning, rotating, and scaling the shape. + The transform is applied relative to the transformation origin. + + **Note:** Only allowed when using `shape`, not with `src`. + $ref: "./svgproperties.yaml#/SvgTransform" + opacity: + description: | + The overall opacity of the entire shape (including fill, stroke, and shadow). + `1` is fully opaque, `0` is fully transparent. + This is applied on top of individual fill/stroke/shadow opacity values. + + **Note:** Only allowed when using `shape`, not with `src`. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + width: + description: | + The width of the bounding box in pixels. + If specified, the shape may be scaled to fit within this width. + If omitted, the shape uses its natural dimensions. + + **Note:** Only allowed when using `shape`, not with `src`. + type: integer + minimum: 1 + maximum: 4096 + example: 400 + height: + description: | + The height of the bounding box in pixels. + If specified, the shape may be scaled to fit within this height. + If omitted, the shape uses its natural dimensions. + + **Note:** Only allowed when using `shape`, not with `src`. + type: integer + minimum: 1 + maximum: 4096 + example: 300 + required: + - type + example: + type: svg + shape: + type: star + points: 5 + outerRadius: 100 + innerRadius: 50 + fill: + type: linear + angle: 45 + stops: + - offset: 0 + color: "#FFD700" + - offset: 1 + color: "#FF6B6B" + opacity: 1 + stroke: + color: "#2C3E50" + width: 3 + opacity: 1 + lineCap: round + lineJoin: round + transform: + x: 200 + y: 150 + rotation: 0 + scale: 1 + opacity: 1 diff --git a/schemas/svgproperties.yaml b/schemas/svgproperties.yaml new file mode 100644 index 0000000..1c70760 --- /dev/null +++ b/schemas/svgproperties.yaml @@ -0,0 +1,311 @@ +SvgFill: + description: | + Fill properties for SVG shapes. Supports solid colors and gradients. + The fill defines how the interior of a shape is painted. + oneOf: + - $ref: "#/SvgSolidFill" + - $ref: "#/SvgLinearGradientFill" + - $ref: "#/SvgRadialGradientFill" + discriminator: + propertyName: type + mapping: + solid: "#/components/schemas/SvgSolidFill" + linear: "#/components/schemas/SvgLinearGradientFill" + radial: "#/components/schemas/SvgRadialGradientFill" + +SvgSolidFill: + description: A solid color fill for SVG shapes. + type: object + properties: + type: + description: The fill type - set to `solid` for a single color fill. + type: string + enum: + - solid + default: solid + color: + description: | + The fill color using hexadecimal color notation (e.g., `#FF0000` for red). + Must be a 6-digit hex color code prefixed with `#`. + type: string + pattern: "^#[A-Fa-f0-9]{6}$" + default: "#000000" + example: "#3498db" + opacity: + description: | + The opacity of the fill where `1` is fully opaque and `0` is fully transparent. + Values between 0 and 1 create semi-transparent fills. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 0.8 + required: + - type + - color + +SvgGradientStop: + description: | + A color stop in a gradient. Each stop defines a color at a specific position + along the gradient vector. Gradients require at least 2 stops. + type: object + properties: + offset: + description: | + Position of the color stop along the gradient vector. + `0` represents the start and `1` represents the end of the gradient. + type: number + minimum: 0 + maximum: 1 + example: 0.5 + color: + description: The color at this stop using hexadecimal color notation. + type: string + pattern: "^#[A-Fa-f0-9]{6}$" + example: "#e74c3c" + required: + - offset + - color + +SvgLinearGradientFill: + description: | + A linear gradient fill that transitions colors along a straight line. + The gradient direction is controlled by the `angle` property. + type: object + properties: + type: + description: The fill type - set to `linear` for a linear gradient fill. + type: string + enum: + - linear + angle: + description: | + The angle of the gradient in degrees. `0` is horizontal (left to right), + `90` is vertical (bottom to top), `180` is right to left, etc. + type: number + minimum: 0 + maximum: 360 + default: 0 + example: 45 + stops: + description: | + Array of color stops that define the gradient colors and their positions. + Must have at least 2 stops. Offsets should increase from 0 to 1. + type: array + minItems: 2 + items: + $ref: "#/SvgGradientStop" + opacity: + description: | + The overall opacity of the gradient where `1` is fully opaque and `0` is fully transparent. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + required: + - type + - stops + +SvgRadialGradientFill: + description: | + A radial gradient fill that transitions colors radiating outward from a center point. + The gradient creates a circular or elliptical color transition. + type: object + properties: + type: + description: The fill type - set to `radial` for a radial gradient fill. + type: string + enum: + - radial + stops: + description: | + Array of color stops that define the gradient colors and their positions. + Must have at least 2 stops. Offset `0` is the center, `1` is the outer edge. + type: array + minItems: 2 + items: + $ref: "#/SvgGradientStop" + opacity: + description: | + The overall opacity of the gradient where `1` is fully opaque and `0` is fully transparent. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + required: + - type + - stops + +SvgStroke: + description: | + Stroke (outline) properties for SVG shapes. The stroke defines how the outline + of a shape is painted, including its color, width, and line style. + type: object + properties: + color: + description: The stroke color using hexadecimal color notation. + type: string + pattern: "^#[A-Fa-f0-9]{6}$" + default: "#000000" + example: "#2c3e50" + width: + description: | + The width of the stroke in pixels. Must be greater than 0. + Larger values create thicker outlines. + type: number + minimum: 0 + maximum: 100 + default: 1 + example: 2 + opacity: + description: The opacity of the stroke where `1` is opaque and `0` is transparent. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + lineCap: + description: | + The shape at the end of open paths (lines, polylines, unclosed paths). +
    +
  • `butt` - flat edge perpendicular to the line (default)
  • +
  • `round` - semicircular cap extending beyond the endpoint
  • +
  • `square` - rectangular cap extending beyond the endpoint
  • +
+ type: string + enum: + - butt + - round + - square + default: butt + example: round + lineJoin: + description: | + The shape at the corners where two lines meet. +
    +
  • `miter` - sharp corner (default)
  • +
  • `round` - rounded corner
  • +
  • `bevel` - flattened corner
  • +
+ type: string + enum: + - miter + - round + - bevel + default: miter + example: round + dashArray: + description: | + Pattern of dashes and gaps for the stroke. An array of numbers where + odd indices are dash lengths and even indices are gap lengths. + For example, `[10, 5]` creates 10px dashes with 5px gaps. + `[10, 5, 2, 5]` creates alternating 10px and 2px dashes with 5px gaps. + type: array + items: + type: number + minimum: 0 + example: [10, 5] + dashOffset: + description: | + Offset for the dash pattern. Positive values shift the pattern + forward along the path, negative values shift it backward. + type: number + default: 0 + example: 5 + +SvgShadow: + description: | + Drop shadow properties for SVG shapes. Creates a shadow effect behind the shape. + type: object + properties: + offsetX: + description: | + Horizontal offset of the shadow in pixels. + Positive values move the shadow to the right, negative to the left. + type: number + default: 0 + example: 4 + offsetY: + description: | + Vertical offset of the shadow in pixels. + Positive values move the shadow down, negative values move it up. + type: number + default: 0 + example: 4 + blur: + description: The blur radius of the shadow in pixels. Must be 0 or greater. + type: number + minimum: 0 + default: 0 + example: 8 + color: + description: The shadow color using hexadecimal color notation. + type: string + pattern: "^#[A-Fa-f0-9]{6}$" + default: "#000000" + example: "#000000" + opacity: + description: The opacity of the shadow where `1` is opaque and `0` is transparent. + type: number + minimum: 0 + maximum: 1 + default: 0.5 + example: 0.3 + +SvgTransform: + description: | + Transformation properties for positioning, rotating, and scaling SVG shapes. + type: object + properties: + x: + description: | + The x-coordinate position of the shape in pixels. + Relative to the top-left corner of the viewport. + type: number + default: 0 + example: 100 + y: + description: | + The y-coordinate position of the shape in pixels. + Relative to the top-left corner of the viewport. + type: number + default: 0 + example: 100 + rotation: + description: | + Rotation angle in degrees. Positive values rotate clockwise, + negative values rotate counter-clockwise. Range: -360 to 360. + type: number + minimum: -360 + maximum: 360 + default: 0 + example: 45 + scale: + description: | + Scale factor for the shape. `1` is original size, `2` is double size, + `0.5` is half size. Must be greater than 0. + type: number + minimum: 0.01 + maximum: 100 + default: 1 + example: 1.5 + originX: + description: | + The x-coordinate of the transformation origin as a value from 0 to 1. + `0` is the left edge, `0.5` is the center, `1` is the right edge. + type: number + minimum: 0 + maximum: 1 + default: 0.5 + example: 0.5 + originY: + description: | + The y-coordinate of the transformation origin as a value from 0 to 1. + `0` is the top edge, `0.5` is the center, `1` is the bottom edge. + type: number + minimum: 0 + maximum: 1 + default: 0.5 + example: 0.5 diff --git a/schemas/svgshapes.yaml b/schemas/svgshapes.yaml new file mode 100644 index 0000000..2e2978e --- /dev/null +++ b/schemas/svgshapes.yaml @@ -0,0 +1,385 @@ +SvgShape: + description: | + The shape definition for an SVG asset. Each shape type has its own specific + properties. The `type` field determines which shape is rendered. + oneOf: + - $ref: "#/SvgRectangleShape" + - $ref: "#/SvgCircleShape" + - $ref: "#/SvgEllipseShape" + - $ref: "#/SvgLineShape" + - $ref: "#/SvgPolygonShape" + - $ref: "#/SvgStarShape" + - $ref: "#/SvgArrowShape" + - $ref: "#/SvgHeartShape" + - $ref: "#/SvgCrossShape" + - $ref: "#/SvgRingShape" + - $ref: "#/SvgPathShape" + discriminator: + propertyName: type + mapping: + rectangle: "#/components/schemas/SvgRectangleShape" + circle: "#/components/schemas/SvgCircleShape" + ellipse: "#/components/schemas/SvgEllipseShape" + line: "#/components/schemas/SvgLineShape" + polygon: "#/components/schemas/SvgPolygonShape" + star: "#/components/schemas/SvgStarShape" + arrow: "#/components/schemas/SvgArrowShape" + heart: "#/components/schemas/SvgHeartShape" + cross: "#/components/schemas/SvgCrossShape" + ring: "#/components/schemas/SvgRingShape" + path: "#/components/schemas/SvgPathShape" + +SvgRectangleShape: + description: | + A rectangle shape with optional rounded corners. + The rectangle is defined by its width and height dimensions. + type: object + properties: + type: + description: The shape type - set to `rectangle`. + type: string + enum: + - rectangle + width: + description: The width of the rectangle in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 200 + height: + description: The height of the rectangle in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + cornerRadius: + description: | + The corner radius for rounded corners in pixels. + Set to `0` for sharp corners. The radius is automatically clamped + to half of the smallest dimension. + type: number + minimum: 0 + maximum: 2048 + default: 0 + example: 10 + required: + - type + - width + - height + +SvgCircleShape: + description: | + A perfect circle shape defined by its radius. + The circle is centered at the shape's position. + type: object + properties: + type: + description: The shape type - set to `circle`. + type: string + enum: + - circle + radius: + description: The radius of the circle in pixels. + type: number + minimum: 1 + maximum: 2048 + example: 50 + required: + - type + - radius + +SvgEllipseShape: + description: | + An ellipse (oval) shape with separate horizontal and vertical radii. + The ellipse is centered at the shape's position. + type: object + properties: + type: + description: The shape type - set to `ellipse`. + type: string + enum: + - ellipse + radiusX: + description: The horizontal radius (semi-major axis) in pixels. + type: number + minimum: 1 + maximum: 2048 + example: 80 + radiusY: + description: The vertical radius (semi-minor axis) in pixels. + type: number + minimum: 1 + maximum: 2048 + example: 50 + required: + - type + - radiusX + - radiusY + +SvgLineShape: + description: | + A straight line shape with a specified length and thickness. + The line is drawn horizontally by default and can be rotated using transform. + type: object + properties: + type: + description: The shape type - set to `line`. + type: string + enum: + - line + length: + description: The length of the line in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + thickness: + description: The thickness of the line in pixels. + type: number + minimum: 1 + maximum: 500 + example: 4 + required: + - type + - length + - thickness + +SvgPolygonShape: + description: | + A regular polygon shape with a specified number of sides. + Examples: triangle (3), square (4), pentagon (5), hexagon (6), etc. + The polygon is inscribed in a circle of the given radius. + type: object + properties: + type: + description: The shape type - set to `polygon`. + type: string + enum: + - polygon + sides: + description: | + The number of sides of the polygon. + Minimum 3 (triangle), maximum 100 for practical use. + type: integer + minimum: 3 + maximum: 100 + example: 6 + radius: + description: | + The radius of the circumscribed circle in pixels. + This determines the size of the polygon. + type: number + minimum: 1 + maximum: 2048 + example: 50 + required: + - type + - sides + - radius + +SvgStarShape: + description: | + A star shape with a specified number of points. + The star is defined by outer and inner radii, creating the characteristic + pointed appearance. + type: object + properties: + type: + description: The shape type - set to `star`. + type: string + enum: + - star + points: + description: | + The number of points on the star. + Minimum 3 for a triangle-like star, typically 5 for a classic star. + type: integer + minimum: 3 + maximum: 100 + example: 5 + outerRadius: + description: | + The outer radius in pixels - the distance from center to the tips of the points. + type: number + minimum: 1 + maximum: 2048 + example: 50 + innerRadius: + description: | + The inner radius in pixels - the distance from center to the inner vertices. + Should be smaller than outerRadius for a star effect. + type: number + minimum: 1 + maximum: 2048 + example: 25 + required: + - type + - points + - outerRadius + - innerRadius + +SvgArrowShape: + description: | + An arrow shape pointing to the right by default. + Use transform rotation to change direction. + type: object + properties: + type: + description: The shape type - set to `arrow`. + type: string + enum: + - arrow + length: + description: The total length of the arrow from tail to tip in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + headWidth: + description: The width of the arrow head (the widest part) in pixels. + type: number + minimum: 1 + maximum: 1000 + example: 40 + headLength: + description: The length of the arrow head portion in pixels. + type: number + minimum: 1 + maximum: 1000 + example: 30 + shaftWidth: + description: The width of the arrow shaft (body) in pixels. + type: number + minimum: 1 + maximum: 1000 + example: 20 + required: + - type + - length + - headWidth + - headLength + - shaftWidth + +SvgHeartShape: + description: | + A heart shape commonly used for love/like icons. + The heart is defined by a single size parameter. + type: object + properties: + type: + description: The shape type - set to `heart`. + type: string + enum: + - heart + size: + description: | + The size of the heart in pixels. + This determines both the width and height proportionally. + type: number + minimum: 1 + maximum: 4096 + example: 100 + required: + - type + - size + +SvgCrossShape: + description: | + A cross or plus shape with equal or different arm lengths. + Can be styled as a plus sign (+) or a cross (x with rotation). + type: object + properties: + type: + description: The shape type - set to `cross`. + type: string + enum: + - cross + width: + description: The total width of the cross in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + height: + description: The total height of the cross in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + thickness: + description: The thickness of the cross arms in pixels. + type: number + minimum: 1 + maximum: 500 + example: 20 + required: + - type + - width + - height + - thickness + +SvgRingShape: + description: | + A ring (donut/annulus) shape - a circle with a circular hole in the center. + The ring is defined by outer and inner radii. + type: object + properties: + type: + description: The shape type - set to `ring`. + type: string + enum: + - ring + outerRadius: + description: The outer radius of the ring in pixels. + type: number + minimum: 1 + maximum: 2048 + example: 50 + innerRadius: + description: | + The inner radius (hole) of the ring in pixels. + Must be smaller than outerRadius. + type: number + minimum: 0 + maximum: 2048 + example: 30 + required: + - type + - outerRadius + - innerRadius + +SvgPathShape: + description: | + A custom shape defined by SVG path data. + Supports all standard SVG path commands for creating complex shapes. + + **Path Commands:** + - `M x y` / `m dx dy` - Move to (absolute/relative) + - `L x y` / `l dx dy` - Line to + - `H x` / `h dx` - Horizontal line to + - `V y` / `v dy` - Vertical line to + - `C x1 y1 x2 y2 x y` / `c` - Cubic Bezier curve + - `S x2 y2 x y` / `s` - Smooth cubic Bezier + - `Q x1 y1 x y` / `q` - Quadratic Bezier curve + - `T x y` / `t` - Smooth quadratic Bezier + - `A rx ry angle large-arc sweep x y` / `a` - Elliptical arc + - `Z` / `z` - Close path + type: object + properties: + type: + description: The shape type - set to `path`. + type: string + enum: + - path + d: + description: | + The SVG path data string defining the shape. + Uses standard SVG path commands (M, L, C, Q, A, Z, etc.). + Example: `M 0 0 L 100 0 L 100 100 L 0 100 Z` draws a square. + type: string + minLength: 1 + maxLength: 100000 + example: "M 0 0 L 100 0 L 50 86.6 Z" + required: + - type + - d diff --git a/scripts/fix-discriminator.cjs b/scripts/fix-discriminator.cjs index 0bc421c..1d4b225 100644 --- a/scripts/fix-discriminator.cjs +++ b/scripts/fix-discriminator.cjs @@ -3,9 +3,7 @@ const path = require("path"); const zodGenPath = path.join(__dirname, "..", "dist", "zod", "zod.gen.ts"); -console.log( - "Fixing discriminator and adding coercion in generated Zod schemas..." -); +console.log("Fixing discriminator and adding z.coerce for number fields..."); let content = fs.readFileSync(zodGenPath, "utf8"); @@ -23,6 +21,7 @@ const newAssetSchema = `export const assetAssetSchema = z.discriminatedUnion("ty htmlassetHtmlAssetSchema, titleassetTitleAssetSchema, shapeassetShapeAssetSchema, + svgassetSvgAssetSchema, texttoimageassetTextToImageAssetSchema, imagetovideoassetImageToVideoAssetSchema, ]);`; @@ -34,64 +33,150 @@ if (assetUnionPattern.test(content)) { console.log("⚠ Could not find assetAssetSchema to replace"); } -const clipStartPattern = - /start: z\.union\(\[z\.number\(\), z\.enum\(\["auto"\]\)\]\)/g; -const clipStartReplacement = `start: z.union([z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()), z.enum(["auto"])])`; +const svgShapeUnionPattern = + /export const svgshapesSvgShapeSchema = z\.union\(\[[\s\S]*?\]\);/; + +const newSvgShapeSchema = `export const svgshapesSvgShapeSchema = z.discriminatedUnion("type", [ + svgshapesSvgRectangleShapeSchema, + svgshapesSvgCircleShapeSchema, + svgshapesSvgEllipseShapeSchema, + svgshapesSvgLineShapeSchema, + svgshapesSvgPolygonShapeSchema, + svgshapesSvgStarShapeSchema, + svgshapesSvgArrowShapeSchema, + svgshapesSvgHeartShapeSchema, + svgshapesSvgCrossShapeSchema, + svgshapesSvgRingShapeSchema, + svgshapesSvgPathShapeSchema, +]);`; -if (clipStartPattern.test(content)) { - content = content.replace(clipStartPattern, clipStartReplacement); - console.log("✓ Added coercion for clip start"); +if (svgShapeUnionPattern.test(content)) { + content = content.replace(svgShapeUnionPattern, newSvgShapeSchema); + console.log("✓ Fixed svgshapesSvgShapeSchema discriminator"); } else { - console.log("⚠ Could not find clip start pattern to add coercion"); + console.log("⚠ Could not find svgshapesSvgShapeSchema to replace"); } -const clipLengthPattern = - /length: z\.union\(\[z\.number\(\), z\.literal\("auto"\), z\.literal\("end"\)\]\)/g; -const clipLengthReplacement = `length: z.union([z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()), z.literal("auto"), z.literal("end")])`; +const svgFillUnionPattern = + /export const svgpropertiesSvgFillSchema = z\.union\(\[[\s\S]*?\]\);/; + +const newSvgFillSchema = `export const svgpropertiesSvgFillSchema = z.discriminatedUnion("type", [ + svgpropertiesSvgSolidFillSchema, + svgpropertiesSvgLinearGradientFillSchema, + svgpropertiesSvgRadialGradientFillSchema, +]);`; -if (clipLengthPattern.test(content)) { - content = content.replace(clipLengthPattern, clipLengthReplacement); - console.log("✓ Added coercion for clip length"); +if (svgFillUnionPattern.test(content)) { + content = content.replace(svgFillUnionPattern, newSvgFillSchema); + console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator"); } else { - console.log("⚠ Could not find clip length pattern to add coercion"); + console.log("⚠ Could not find svgpropertiesSvgFillSchema to replace"); } -const trimPattern = /trim: z\.optional\(z\.number\(\)\)/g; -const trimReplacement = `trim: z.optional(z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()))`; +const svgAssetPattern = + /export const svgassetSvgAssetSchema = z\.object\(\{[\s\S]*?\}\);/; + +const svgAssetSuperRefine = `export const svgassetSvgAssetSchema = z.object({ + type: z.enum(["svg"]), + src: z.optional(z.string().min(1).max(500000)), + shape: z.optional(svgshapesSvgShapeSchema), + fill: z.optional(svgpropertiesSvgFillSchema), + stroke: z.optional(svgpropertiesSvgStrokeSchema), + shadow: z.optional(svgpropertiesSvgShadowSchema), + transform: z.optional(svgpropertiesSvgTransformSchema), + opacity: z.optional(z.preprocess(((v: unknown) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), z.number().gte(0).lte(1))).default(1), + width: z.optional(z.preprocess(((v: unknown) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), z.number().int().gte(1).lte(4096))), + height: z.optional(z.preprocess(((v: unknown) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), z.number().int().gte(1).lte(4096))), +}).superRefine((data, ctx) => { + const hasShape = data.shape !== undefined; + const hasSrc = data.src !== undefined && data.src.trim() !== ""; + + if (!hasShape && !hasSrc) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Either 'src' or 'shape' must be provided", + path: [], + }); + } -const trimCount = (content.match(trimPattern) || []).length; -if (trimCount > 0) { - content = content.replace(trimPattern, trimReplacement); - console.log(`✓ Added coercion for trim (${trimCount} occurrences)`); -} + if (hasShape && hasSrc) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Provide either 'src' or 'shape', not both", + path: ["src"], + }); + } -const volumePattern = - /volume: z\.optional\(z\.number\(\)\.gte\(0\)\.lte\(1\)\)/g; -const volumeReplacement = `volume: z.optional(z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number().gte(0).lte(1)))`; + if (hasSrc) { + const disallowedProps = ["shape", "fill", "stroke", "shadow", "transform", "width", "height"]; + for (const prop of disallowedProps) { + if (data[prop] !== undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: \`'\${prop}' is not allowed when using 'src'. Only 'type' and 'src' are allowed in import mode\`, + path: [prop], + }); + } + } + } +});`; -const volumeCount = (content.match(volumePattern) || []).length; -if (volumeCount > 0) { - content = content.replace(volumePattern, volumeReplacement); - console.log(`✓ Added coercion for volume (${volumeCount} occurrences)`); +if (svgAssetPattern.test(content)) { + content = content.replace(svgAssetPattern, svgAssetSuperRefine); + console.log("✓ Added superRefine validation to svgassetSvgAssetSchema for mutual exclusivity"); +} else { + console.log("⚠ Could not find svgassetSvgAssetSchema to add superRefine validation"); } -const speedPattern = - /speed: z\.optional\(z\.number\(\)\.gte\(0\)\.lte\(10\)\)/g; -const speedReplacement = `speed: z.optional(z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number().gte(0).lte(10)))`; +// Coercion function that converts strings to numbers inside preprocess (doesn't rely on z.coerce) +const coerceNumber = `((v: unknown) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; })`; + +const plainNumberPattern = /z\.number\(\)(?!\.)/g; +const plainNumberCount = (content.match(plainNumberPattern) || []).length; +if (plainNumberCount > 0) { + content = content.replace( + plainNumberPattern, + `z.preprocess(${coerceNumber}, z.number())` + ); + console.log( + `✓ Added number coercion for plain z.number() (${plainNumberCount} occurrences)` + ); +} -const speedCount = (content.match(speedPattern) || []).length; -if (speedCount > 0) { - content = content.replace(speedPattern, speedReplacement); - console.log(`✓ Added coercion for speed (${speedCount} occurrences)`); +const chainedNumberPattern = /z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; +let chainedCount = 0; +content = content.replace(chainedNumberPattern, (match, chain) => { + chainedCount++; + return `z.preprocess(${coerceNumber}, z.number()${chain})`; +}); +if (chainedCount > 0) { + console.log( + `✓ Added number coercion for chained z.number() (${chainedCount} occurrences)` + ); } -const scalePattern = /scale: z\.optional\(z\.number\(\)\)/g; -const scaleReplacement = `scale: z.optional(z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()))`; +const plainIntPattern = /z\.int\(\)(?!\.)/g; +const plainIntCount = (content.match(plainIntPattern) || []).length; +if (plainIntCount > 0) { + content = content.replace( + plainIntPattern, + `z.preprocess(${coerceNumber}, z.number().int())` + ); + console.log( + `✓ Added number coercion for plain z.int() (${plainIntCount} occurrences)` + ); +} -const scaleCount = (content.match(scalePattern) || []).length; -if (scaleCount > 0) { - content = content.replace(scalePattern, scaleReplacement); - console.log(`✓ Added coercion for scale (${scaleCount} occurrences)`); +const chainedIntPattern = /z\.int\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; +let chainedIntCount = 0; +content = content.replace(chainedIntPattern, (match, chain) => { + chainedIntCount++; + return `z.preprocess(${coerceNumber}, z.number().int()${chain})`; +}); +if (chainedIntCount > 0) { + console.log( + `✓ Added number coercion for chained z.int() (${chainedIntCount} occurrences)` + ); } fs.writeFileSync(zodGenPath, content); @@ -114,6 +199,7 @@ if (fs.existsSync(zodGenCjsPath)) { exports.htmlassetHtmlAssetSchema, exports.titleassetTitleAssetSchema, exports.shapeassetShapeAssetSchema, + exports.svgassetSvgAssetSchema, exports.texttoimageassetTextToImageAssetSchema, exports.imagetovideoassetImageToVideoAssetSchema, ]);`; @@ -123,50 +209,129 @@ if (fs.existsSync(zodGenCjsPath)) { console.log("✓ Fixed assetAssetSchema discriminator in CJS"); } - const cjsCoercionPatterns = [ - { - pattern: - /start: zod_1\.z\.union\(\[zod_1\.z\.number\(\), zod_1\.z\.enum\(\["auto"\]\)\]\)/g, - replacement: `start: zod_1.z.union([zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number()), zod_1.z.enum(["auto"])])`, - name: "clip start", - }, - { - pattern: - /length: zod_1\.z\.union\(\[zod_1\.z\.number\(\), zod_1\.z\.literal\("auto"\), zod_1\.z\.literal\("end"\)\]\)/g, - replacement: `length: zod_1.z.union([zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number()), zod_1.z.literal("auto"), zod_1.z.literal("end")])`, - name: "clip length", - }, - { - pattern: /trim: zod_1\.z\.optional\(zod_1\.z\.number\(\)\)/g, - replacement: `trim: zod_1.z.optional(zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number()))`, - name: "trim", - }, - { - pattern: - /volume: zod_1\.z\.optional\(zod_1\.z\.number\(\)\.gte\(0\)\.lte\(1\)\)/g, - replacement: `volume: zod_1.z.optional(zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number().gte(0).lte(1)))`, - name: "volume", - }, - { - pattern: - /speed: zod_1\.z\.optional\(zod_1\.z\.number\(\)\.gte\(0\)\.lte\(10\)\)/g, - replacement: `speed: zod_1.z.optional(zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number().gte(0).lte(10)))`, - name: "speed", - }, - { - pattern: /scale: zod_1\.z\.optional\(zod_1\.z\.number\(\)\)/g, - replacement: `scale: zod_1.z.optional(zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number()))`, - name: "scale", - }, - ]; - - for (const { pattern, replacement, name } of cjsCoercionPatterns) { - const count = (cjsContent.match(pattern) || []).length; - if (count > 0) { - cjsContent = cjsContent.replace(pattern, replacement); - console.log(`✓ Added coercion for ${name} in CJS (${count} occurrences)`); + const cjsSvgShapeUnionPattern = + /exports\.svgshapesSvgShapeSchema = zod_1\.z\.union\(\[[\s\S]*?\]\);/; + + const newCjsSvgShapeSchema = `exports.svgshapesSvgShapeSchema = zod_1.z.discriminatedUnion("type", [ + exports.svgshapesSvgRectangleShapeSchema, + exports.svgshapesSvgCircleShapeSchema, + exports.svgshapesSvgEllipseShapeSchema, + exports.svgshapesSvgLineShapeSchema, + exports.svgshapesSvgPolygonShapeSchema, + exports.svgshapesSvgStarShapeSchema, + exports.svgshapesSvgArrowShapeSchema, + exports.svgshapesSvgHeartShapeSchema, + exports.svgshapesSvgCrossShapeSchema, + exports.svgshapesSvgRingShapeSchema, + exports.svgshapesSvgPathShapeSchema, +]);`; + + if (cjsSvgShapeUnionPattern.test(cjsContent)) { + cjsContent = cjsContent.replace( + cjsSvgShapeUnionPattern, + newCjsSvgShapeSchema + ); + console.log("✓ Fixed svgshapesSvgShapeSchema discriminator in CJS"); + } + + const cjsSvgFillUnionPattern = + /exports\.svgpropertiesSvgFillSchema = zod_1\.z\.union\(\[[\s\S]*?\]\);/; + + const newCjsSvgFillSchema = `exports.svgpropertiesSvgFillSchema = zod_1.z.discriminatedUnion("type", [ + exports.svgpropertiesSvgSolidFillSchema, + exports.svgpropertiesSvgLinearGradientFillSchema, + exports.svgpropertiesSvgRadialGradientFillSchema, +]);`; + + if (cjsSvgFillUnionPattern.test(cjsContent)) { + cjsContent = cjsContent.replace( + cjsSvgFillUnionPattern, + newCjsSvgFillSchema + ); + console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator in CJS"); + } + + const cjsSvgAssetPattern = + /exports\.svgassetSvgAssetSchema = zod_1\.z\.object\(\{[\s\S]*?\}\);/; + + const cjsSvgAssetSuperRefine = `exports.svgassetSvgAssetSchema = zod_1.z.object({ + type: zod_1.z.enum(["svg"]), + src: zod_1.z.optional(zod_1.z.string().min(1).max(500000)), + shape: zod_1.z.optional(exports.svgshapesSvgShapeSchema), + fill: zod_1.z.optional(exports.svgpropertiesSvgFillSchema), + stroke: zod_1.z.optional(exports.svgpropertiesSvgStrokeSchema), + shadow: zod_1.z.optional(exports.svgpropertiesSvgShadowSchema), + transform: zod_1.z.optional(exports.svgpropertiesSvgTransformSchema), + opacity: zod_1.z.optional(zod_1.z.preprocess(((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), zod_1.z.number().gte(0).lte(1))).default(1), + width: zod_1.z.optional(zod_1.z.preprocess(((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), zod_1.z.number().int().gte(1).lte(4096))), + height: zod_1.z.optional(zod_1.z.preprocess(((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), zod_1.z.number().int().gte(1).lte(4096))), +}).superRefine((data, ctx) => { + const hasShape = data.shape !== undefined; + const hasSrc = data.src !== undefined && data.src.trim() !== ""; + + if (!hasShape && !hasSrc) { + ctx.addIssue({ + code: zod_1.z.ZodIssueCode.custom, + message: "Either 'src' or 'shape' must be provided", + path: [], + }); + } + + if (hasShape && hasSrc) { + ctx.addIssue({ + code: zod_1.z.ZodIssueCode.custom, + message: "Provide either 'src' or 'shape', not both", + path: ["src"], + }); + } + + if (hasSrc) { + const disallowedProps = ["shape", "fill", "stroke", "shadow", "transform", "width", "height"]; + for (const prop of disallowedProps) { + if (data[prop] !== undefined) { + ctx.addIssue({ + code: zod_1.z.ZodIssueCode.custom, + message: "'" + prop + "' is not allowed when using 'src'. Only 'type' and 'src' are allowed in import mode", + path: [prop], + }); + } } } +});`; + + if (cjsSvgAssetPattern.test(cjsContent)) { + cjsContent = cjsContent.replace(cjsSvgAssetPattern, cjsSvgAssetSuperRefine); + console.log("✓ Added superRefine validation to svgassetSvgAssetSchema in CJS"); + } + + // Coercion function for CJS (without TypeScript type annotation) + const cjsCoerceNumber = `((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; })`; + + const cjsPlainNumberPattern = /zod_1\.z\.number\(\)(?!\.)/g; + const cjsPlainNumberCount = (cjsContent.match(cjsPlainNumberPattern) || []) + .length; + if (cjsPlainNumberCount > 0) { + cjsContent = cjsContent.replace( + cjsPlainNumberPattern, + `zod_1.z.preprocess(${cjsCoerceNumber}, zod_1.z.number())` + ); + console.log( + `✓ Added number coercion in CJS (${cjsPlainNumberCount} occurrences)` + ); + } + + const cjsChainedNumberPattern = + /zod_1\.z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; + let cjsChainedCount = 0; + cjsContent = cjsContent.replace(cjsChainedNumberPattern, (match, chain) => { + cjsChainedCount++; + return `zod_1.z.preprocess(${cjsCoerceNumber}, zod_1.z.number()${chain})`; + }); + if (cjsChainedCount > 0) { + console.log( + `✓ Added number coercion chains in CJS (${cjsChainedCount} occurrences)` + ); + } fs.writeFileSync(zodGenCjsPath, cjsContent); } @@ -183,31 +348,47 @@ if (fs.existsSync(zodGenJsPath)) { console.log("✓ Fixed assetAssetSchema discriminator in ESM JS"); } - const esmCoercionPatterns = [ - { - pattern: clipStartPattern, - replacement: clipStartReplacement, - name: "clip start", - }, - { - pattern: clipLengthPattern, - replacement: clipLengthReplacement, - name: "clip length", - }, - { pattern: trimPattern, replacement: trimReplacement, name: "trim" }, - { pattern: volumePattern, replacement: volumeReplacement, name: "volume" }, - { pattern: speedPattern, replacement: speedReplacement, name: "speed" }, - { pattern: scalePattern, replacement: scaleReplacement, name: "scale" }, - ]; - - for (const { pattern, replacement, name } of esmCoercionPatterns) { - const count = (jsContent.match(pattern) || []).length; - if (count > 0) { - jsContent = jsContent.replace(pattern, replacement); - console.log( - `✓ Added coercion for ${name} in ESM JS (${count} occurrences)` - ); - } + if (svgShapeUnionPattern.test(jsContent)) { + jsContent = jsContent.replace(svgShapeUnionPattern, newSvgShapeSchema); + console.log("✓ Fixed svgshapesSvgShapeSchema discriminator in ESM JS"); + } + + if (svgFillUnionPattern.test(jsContent)) { + jsContent = jsContent.replace(svgFillUnionPattern, newSvgFillSchema); + console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator in ESM JS"); + } + + if (svgAssetPattern.test(jsContent)) { + jsContent = jsContent.replace(svgAssetPattern, svgAssetSuperRefine); + console.log("✓ Added superRefine validation to svgassetSvgAssetSchema in ESM JS"); + } + + // Coercion function for ESM JS (without TypeScript type annotation) + const esmCoerceNumber = `((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; })`; + + const esmPlainNumberPattern = /z\.number\(\)(?!\.)/g; + const esmPlainNumberCount = (jsContent.match(esmPlainNumberPattern) || []) + .length; + if (esmPlainNumberCount > 0) { + jsContent = jsContent.replace( + esmPlainNumberPattern, + `z.preprocess(${esmCoerceNumber}, z.number())` + ); + console.log( + `✓ Added number coercion in ESM JS (${esmPlainNumberCount} occurrences)` + ); + } + + const esmChainedNumberPattern = /z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; + let esmChainedCount = 0; + jsContent = jsContent.replace(esmChainedNumberPattern, (match, chain) => { + esmChainedCount++; + return `z.preprocess(${esmCoerceNumber}, z.number()${chain})`; + }); + if (esmChainedCount > 0) { + console.log( + `✓ Added number coercion chains in ESM JS (${esmChainedCount} occurrences)` + ); } fs.writeFileSync(zodGenJsPath, jsContent);