{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://sats.example/schema/template/v1.json",
  "title": "Sats template",
  "description": "Template format v1. Source of truth for engine, server and editor. All lengths are points (1/72 inch), origin top-left, y downward. Expression fields hold a bare expression; text content uses {{ expression }} interpolation. See docs/template-format.md and docs/expressions.md.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schemaVersion",
    "name",
    "page",
    "parts"
  ],
  "properties": {
    "schemaVersion": {
      "const": 1
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "description": {
      "type": "string",
      "maxLength": 2000
    },
    "locale": {
      "type": "string",
      "default": "nb-NO",
      "pattern": "^[a-z]{2}(-[A-Z]{2})?$"
    },
    "page": {
      "$ref": "#/$defs/PageSetup"
    },
    "fonts": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/FontFace"
      },
      "default": []
    },
    "styles": {
      "description": "Named text styles. 'base' is the default for all text.",
      "type": "object",
      "propertyNames": {
        "$ref": "#/$defs/Identifier"
      },
      "additionalProperties": {
        "$ref": "#/$defs/TextStyle"
      },
      "default": {}
    },
    "dataSchema": {
      "description": "Optional JSON Schema the render data must satisfy (the template's data contract).",
      "type": "object"
    },
    "sampleData": {
      "description": "Preview data used by the editor. Never used for real renders.",
      "type": "object"
    },
    "parts": {
      "description": "Document parts, rendered in order: flowing content or inserted PDFs.",
      "type": "array",
      "minItems": 1,
      "items": {
        "$ref": "#/$defs/Part"
      }
    }
  },
  "$defs": {
    "Identifier": {
      "type": "string",
      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$"
    },
    "Length": {
      "type": "number",
      "minimum": 0,
      "maximum": 14400
    },
    "Coordinate": {
      "type": "number",
      "minimum": -14400,
      "maximum": 14400
    },
    "Color": {
      "type": "string",
      "pattern": "^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
    },
    "Expression": {
      "description": "A bare expression, e.g. order.total > 0 or order.lines.",
      "type": "string",
      "minLength": 1,
      "maxLength": 2000
    },
    "Template": {
      "description": "Text with {{ expression }} interpolation, e.g. 'Order {{ order.no }}'.",
      "type": "string",
      "maxLength": 20000
    },
    "AssetRef": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "asset"
      ],
      "properties": {
        "asset": {
          "type": "string",
          "pattern": "^ast_[A-Za-z0-9]{8,40}$"
        }
      }
    },
    "BindingRef": {
      "description": "Resolved from data at render time: an asset id, an https URL or a data: URI.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "binding"
      ],
      "properties": {
        "binding": {
          "$ref": "#/$defs/Expression"
        }
      }
    },
    "Source": {
      "oneOf": [
        {
          "$ref": "#/$defs/AssetRef"
        },
        {
          "$ref": "#/$defs/BindingRef"
        }
      ]
    },
    "PageSetup": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "size"
      ],
      "properties": {
        "size": {
          "oneOf": [
            {
              "enum": [
                "A3",
                "A4",
                "A5",
                "Letter",
                "Legal"
              ]
            },
            {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "width",
                "height"
              ],
              "properties": {
                "width": {
                  "$ref": "#/$defs/Length"
                },
                "height": {
                  "$ref": "#/$defs/Length"
                }
              }
            }
          ]
        },
        "orientation": {
          "enum": [
            "portrait",
            "landscape"
          ],
          "default": "portrait"
        },
        "margin": {
          "$ref": "#/$defs/Box"
        }
      }
    },
    "Box": {
      "description": "Four sides in points. An absent side is 0.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "top": {
          "$ref": "#/$defs/Length"
        },
        "right": {
          "$ref": "#/$defs/Length"
        },
        "bottom": {
          "$ref": "#/$defs/Length"
        },
        "left": {
          "$ref": "#/$defs/Length"
        }
      }
    },
    "FontFace": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "family",
        "source"
      ],
      "properties": {
        "family": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "weight": {
          "type": "integer",
          "minimum": 100,
          "maximum": 900,
          "default": 400
        },
        "italic": {
          "type": "boolean",
          "default": false
        },
        "source": {
          "oneOf": [
            {
              "$ref": "#/$defs/AssetRef"
            },
            {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "builtin"
              ],
              "properties": {
                "builtin": {
                  "enum": [
                    "Inter",
                    "Noto Sans",
                    "Noto Serif",
                    "Noto Sans Mono"
                  ]
                }
              }
            }
          ]
        }
      }
    },
    "TextStyleProps": {
      "type": "object",
      "properties": {
        "extends": {
          "$ref": "#/$defs/Identifier"
        },
        "fontFamily": {
          "type": "string"
        },
        "fontSize": {
          "type": "number",
          "minimum": 2,
          "maximum": 400
        },
        "fontWeight": {
          "type": "integer",
          "minimum": 100,
          "maximum": 900
        },
        "italic": {
          "type": "boolean"
        },
        "underline": {
          "type": "boolean"
        },
        "color": {
          "$ref": "#/$defs/Color"
        },
        "align": {
          "enum": [
            "left",
            "center",
            "right",
            "justify"
          ]
        },
        "verticalAlign": {
          "enum": [
            "top",
            "middle",
            "bottom"
          ]
        },
        "lineHeight": {
          "type": "number",
          "minimum": 0.5,
          "maximum": 4,
          "description": "Multiple of font size."
        },
        "letterSpacing": {
          "type": "number",
          "minimum": -10,
          "maximum": 50
        }
      }
    },
    "TextStyle": {
      "description": "A named style. 'extends' inherits from another named style.",
      "allOf": [
        {
          "$ref": "#/$defs/TextStyleProps"
        }
      ],
      "unevaluatedProperties": false
    },
    "StyleRef": {
      "description": "A named style, or inline overrides on top of an optional named style ('ref').",
      "oneOf": [
        {
          "$ref": "#/$defs/Identifier"
        },
        {
          "type": "object",
          "allOf": [
            {
              "$ref": "#/$defs/TextStyleProps"
            }
          ],
          "properties": {
            "ref": {
              "$ref": "#/$defs/Identifier"
            },
            "extends": false
          },
          "unevaluatedProperties": false
        }
      ]
    },
    "Part": {
      "oneOf": [
        {
          "$ref": "#/$defs/FlowPart"
        },
        {
          "$ref": "#/$defs/PdfPart"
        }
      ]
    },
    "FlowPart": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "type",
        "id",
        "pageTemplates",
        "sections"
      ],
      "properties": {
        "type": {
          "const": "flow"
        },
        "id": {
          "$ref": "#/$defs/Identifier"
        },
        "visibleIf": {
          "$ref": "#/$defs/Expression"
        },
        "tocTitle": {
          "$ref": "#/$defs/Template"
        },
        "restartPageNumbers": {
          "type": "boolean",
          "default": false
        },
        "pageTemplates": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "default"
          ],
          "properties": {
            "default": {
              "$ref": "#/$defs/PageTemplate"
            },
            "first": {
              "$ref": "#/$defs/PageTemplate"
            },
            "last": {
              "$ref": "#/$defs/PageTemplate"
            }
          }
        },
        "sections": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Section"
          }
        }
      }
    },
    "PdfPart": {
      "description": "Inserts pages from an existing PDF. With 'repeat', one insert per item (e.g. a datasheet per product).",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "type",
        "id",
        "source"
      ],
      "properties": {
        "type": {
          "const": "pdf"
        },
        "id": {
          "$ref": "#/$defs/Identifier"
        },
        "visibleIf": {
          "$ref": "#/$defs/Expression"
        },
        "repeat": {
          "$ref": "#/$defs/Repeat"
        },
        "source": {
          "$ref": "#/$defs/Source"
        },
        "pages": {
          "type": "string",
          "pattern": "^(all|\\d+(-\\d+)?(,\\d+(-\\d+)?)*)$",
          "default": "all"
        },
        "fit": {
          "enum": [
            "keep",
            "scaleToPage"
          ],
          "default": "keep"
        },
        "countPages": {
          "type": "boolean",
          "default": true,
          "description": "Inserted pages count towards @page and @pageCount."
        },
        "overlay": {
          "description": "Optional page template drawn on top of every inserted page (e.g. page number footer).",
          "$ref": "#/$defs/PageTemplate"
        },
        "tocTitle": {
          "$ref": "#/$defs/Template"
        }
      }
    },
    "PageTemplate": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "background": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "source"
          ],
          "properties": {
            "source": {
              "$ref": "#/$defs/Source"
            },
            "page": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        },
        "header": {
          "$ref": "#/$defs/Band"
        },
        "footer": {
          "$ref": "#/$defs/Band"
        },
        "elements": {
          "description": "Page layer: positioned relative to the page corner (not the margins), drawn above the background and below the flow.",
          "type": "array",
          "items": {
            "$ref": "#/$defs/Element"
          },
          "default": []
        }
      }
    },
    "Band": {
      "description": "Header or footer inside the page margins. Coordinates are relative to the band.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "height"
      ],
      "properties": {
        "height": {
          "$ref": "#/$defs/Length"
        },
        "elements": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Element"
          },
          "default": []
        }
      }
    },
    "Repeat": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "over"
      ],
      "properties": {
        "over": {
          "$ref": "#/$defs/Expression"
        },
        "as": {
          "$ref": "#/$defs/Identifier",
          "default": "item"
        }
      }
    },
    "Section": {
      "description": "A horizontal band in the flow. Elements are positioned relative to the section's top-left corner. Height grows when a 'grow' text or a table needs more room.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "height",
        "elements"
      ],
      "properties": {
        "id": {
          "$ref": "#/$defs/Identifier"
        },
        "name": {
          "type": "string",
          "maxLength": 200
        },
        "height": {
          "$ref": "#/$defs/Length"
        },
        "repeat": {
          "$ref": "#/$defs/Repeat"
        },
        "visibleIf": {
          "$ref": "#/$defs/Expression"
        },
        "keepTogether": {
          "type": "boolean",
          "default": true
        },
        "keepWithNext": {
          "type": "boolean",
          "default": false
        },
        "breakBefore": {
          "type": "boolean",
          "default": false
        },
        "tocTitle": {
          "$ref": "#/$defs/Template"
        },
        "elements": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Element"
          }
        }
      }
    },
    "ElementBase": {
      "type": "object",
      "required": [
        "id",
        "type",
        "x",
        "y",
        "w",
        "h"
      ],
      "properties": {
        "id": {
          "$ref": "#/$defs/Identifier"
        },
        "type": {
          "type": "string"
        },
        "name": {
          "type": "string",
          "maxLength": 200
        },
        "x": {
          "$ref": "#/$defs/Coordinate"
        },
        "y": {
          "$ref": "#/$defs/Coordinate"
        },
        "w": {
          "$ref": "#/$defs/Length"
        },
        "h": {
          "$ref": "#/$defs/Length"
        },
        "rotation": {
          "type": "number",
          "minimum": -360,
          "maximum": 360,
          "default": 0
        },
        "visibleIf": {
          "$ref": "#/$defs/Expression"
        },
        "showOn": {
          "description": "Only for elements in page templates.",
          "enum": [
            "all",
            "first",
            "notFirst",
            "last",
            "notLast"
          ],
          "default": "all"
        },
        "locked": {
          "type": "boolean",
          "default": false
        }
      }
    },
    "Element": {
      "oneOf": [
        {
          "$ref": "#/$defs/TextElement"
        },
        {
          "$ref": "#/$defs/ImageElement"
        },
        {
          "$ref": "#/$defs/RectElement"
        },
        {
          "$ref": "#/$defs/LineElement"
        },
        {
          "$ref": "#/$defs/BarcodeElement"
        },
        {
          "$ref": "#/$defs/TableElement"
        },
        {
          "$ref": "#/$defs/TocElement"
        }
      ]
    },
    "TextElement": {
      "allOf": [
        {
          "$ref": "#/$defs/ElementBase"
        }
      ],
      "type": "object",
      "required": [
        "content"
      ],
      "properties": {
        "type": {
          "const": "text"
        },
        "content": {
          "$ref": "#/$defs/Template"
        },
        "style": {
          "$ref": "#/$defs/StyleRef"
        },
        "fit": {
          "enum": [
            "clip",
            "ellipsis",
            "shrink",
            "grow"
          ],
          "default": "clip"
        },
        "minFontSize": {
          "type": "number",
          "minimum": 2,
          "default": 6
        }
      },
      "unevaluatedProperties": false
    },
    "ImageElement": {
      "allOf": [
        {
          "$ref": "#/$defs/ElementBase"
        }
      ],
      "type": "object",
      "required": [
        "source"
      ],
      "properties": {
        "type": {
          "const": "image"
        },
        "source": {
          "$ref": "#/$defs/Source"
        },
        "fit": {
          "enum": [
            "contain",
            "cover",
            "stretch"
          ],
          "default": "contain"
        },
        "align": {
          "enum": [
            "topLeft",
            "top",
            "topRight",
            "left",
            "center",
            "right",
            "bottomLeft",
            "bottom",
            "bottomRight"
          ],
          "default": "center"
        }
      },
      "unevaluatedProperties": false
    },
    "RectElement": {
      "allOf": [
        {
          "$ref": "#/$defs/ElementBase"
        }
      ],
      "type": "object",
      "properties": {
        "type": {
          "const": "rect"
        },
        "fill": {
          "$ref": "#/$defs/Color"
        },
        "stroke": {
          "$ref": "#/$defs/Color"
        },
        "strokeWidth": {
          "type": "number",
          "minimum": 0,
          "default": 0
        },
        "radius": {
          "$ref": "#/$defs/Length"
        },
        "ellipse": {
          "type": "boolean",
          "default": false
        }
      },
      "unevaluatedProperties": false
    },
    "LineElement": {
      "description": "Draws from (x, y) to (x + w, y + h).",
      "allOf": [
        {
          "$ref": "#/$defs/ElementBase"
        }
      ],
      "type": "object",
      "properties": {
        "type": {
          "const": "line"
        },
        "stroke": {
          "$ref": "#/$defs/Color",
          "default": "#000000"
        },
        "strokeWidth": {
          "type": "number",
          "minimum": 0,
          "default": 0.75
        },
        "dash": {
          "type": "array",
          "items": {
            "type": "number",
            "minimum": 0
          },
          "maxItems": 8
        }
      },
      "unevaluatedProperties": false
    },
    "BarcodeElement": {
      "allOf": [
        {
          "$ref": "#/$defs/ElementBase"
        }
      ],
      "type": "object",
      "required": [
        "format",
        "value"
      ],
      "properties": {
        "type": {
          "const": "barcode"
        },
        "format": {
          "enum": [
            "qr",
            "code128",
            "ean13",
            "datamatrix"
          ]
        },
        "value": {
          "$ref": "#/$defs/Template"
        },
        "color": {
          "$ref": "#/$defs/Color",
          "default": "#000000"
        },
        "showText": {
          "type": "boolean",
          "default": false
        }
      },
      "unevaluatedProperties": false
    },
    "TableElement": {
      "description": "Rows from a list in data. The table may grow beyond h and split across pages between rows; h is the minimum height.",
      "allOf": [
        {
          "$ref": "#/$defs/ElementBase"
        }
      ],
      "type": "object",
      "required": [
        "rows",
        "columns"
      ],
      "properties": {
        "type": {
          "const": "table"
        },
        "rows": {
          "$ref": "#/$defs/Repeat"
        },
        "columns": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "width",
              "cell"
            ],
            "properties": {
              "id": {
                "$ref": "#/$defs/Identifier"
              },
              "width": {
                "description": "Points, or a fraction like '2fr' of the remaining width.",
                "oneOf": [
                  {
                    "$ref": "#/$defs/Length"
                  },
                  {
                    "type": "string",
                    "pattern": "^\\d+(\\.\\d+)?fr$"
                  }
                ]
              },
              "header": {
                "$ref": "#/$defs/Template"
              },
              "cell": {
                "$ref": "#/$defs/Template"
              },
              "footer": {
                "$ref": "#/$defs/Template"
              },
              "style": {
                "$ref": "#/$defs/StyleRef"
              },
              "wrap": {
                "type": "boolean",
                "default": true
              }
            }
          }
        },
        "headerStyle": {
          "$ref": "#/$defs/StyleRef"
        },
        "cellStyle": {
          "$ref": "#/$defs/StyleRef"
        },
        "footerStyle": {
          "$ref": "#/$defs/StyleRef"
        },
        "repeatHeader": {
          "type": "boolean",
          "default": true
        },
        "minRowHeight": {
          "$ref": "#/$defs/Length"
        },
        "padding": {
          "$ref": "#/$defs/Box"
        },
        "borders": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "color": {
              "$ref": "#/$defs/Color"
            },
            "width": {
              "type": "number",
              "minimum": 0
            },
            "horizontal": {
              "type": "boolean",
              "default": true
            },
            "vertical": {
              "type": "boolean",
              "default": false
            },
            "outer": {
              "type": "boolean",
              "default": false
            }
          }
        },
        "zebra": {
          "$ref": "#/$defs/Color"
        },
        "carryOver": {
          "description": "Running totals at page breaks.",
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "label": {
              "type": "string"
            },
            "broughtForwardLabel": {
              "type": "string"
            },
            "columns": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/Identifier"
              }
            }
          }
        }
      },
      "unevaluatedProperties": false
    },
    "TocElement": {
      "description": "Table of contents from parts and sections that have tocTitle.",
      "allOf": [
        {
          "$ref": "#/$defs/ElementBase"
        }
      ],
      "type": "object",
      "properties": {
        "type": {
          "const": "toc"
        },
        "style": {
          "$ref": "#/$defs/StyleRef"
        },
        "leader": {
          "enum": [
            "dots",
            "none"
          ],
          "default": "dots"
        },
        "links": {
          "type": "boolean",
          "default": true
        }
      },
      "unevaluatedProperties": false
    }
  }
}
