obsidianmd / jsoncanvas

An open file format for infinite canvas data.
https://jsoncanvas.org
MIT License
2.47k stars 80 forks source link

Create JSON Schema to further define and constrain the spec. #10

Open mwadams opened 6 months ago

mwadams commented 6 months ago

I would suggest creating a JSON Schema to define the spec.

Here's a first pass at that.

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://github.com/obsidianmd/jsoncanvas/canvas.json",
    "title": "An open file format for infinite canvas data.",
    "description": "Infinite canvas tools are a way to view and organize information spatially, like a digital whiteboard. Infinite canvases encourage freedom and exploration, and have become a popular interface pattern across many apps.\nThe JSON Canvas format was created to provide longevity, readability, interoperability, and extensibility to data created with infinite canvas apps. The format is designed to be easy to parse and give users ownership over their data. JSON Canvas files use the .canvas extension.\nJSON Canvas was originally created for Obsidian. JSON Canvas can be implemented freely as an import, export, and storage format for any app or tool. This site, and all the resources associated with JSON Canvas are open source under the MIT license.",
    "type": "object",
    "properties": {
        "nodes": { "title": "An optional array of nodes.", "$ref": "#/$defs/nodeCollection"},
        "edges": { "title": "An optional array of edges.", "$ref": "#/$defs/edgeCollection"}
    },

    "$defs": {
        "nodeCollection": {
            "title": "An array of nodes.",
            "type": "array",
            "items": { "$ref": "#/$defs/node"}
        },
        "edgeCollection": {
            "title": "An array of edges.",
            "type": "array",
            "items": { "$ref": "#/$defs/edge"}
        },

        "node": {
            "title": "A node.",
            "description": "Nodes are objects within the canvas. Nodes may be text, files, links, or groups.",
            "oneOf": [
                { "$ref": "#/$defs/textNode" },
                { "$ref": "#/$defs/fileNode" },
                { "$ref": "#/$defs/linkNode" },
                { "$ref": "#/$defs/groupNode" }
            ]
        },

        "genericNode": {
            "title": "A generic node.",
            "type": "object",
            "required": ["id", "type", "x", "y", "width", "height"],
            "properties": {
                "id": {
                    "title": "A unique identifier for the node.",
                    "type": "string"
                },
                "type": { "title": "The node type (a discriminator).", "$ref": "#/$defs/nodeType"},
                "x": { "title": "The x position of the node in pixels.", "$ref": "#/$defs/coordinate" },
                "y": { "title": "The y position of the node in pixels.", "$ref": "#/$defs/coordinate" },
                "width": { "title": "The width of the node in pixels.", "$ref": "#/$defs/dimension" },
                "height": { "title": "The height of the node in pixels.", "$ref": "#/$defs/dimension" },
                "color": { "title": "The color of the node.", "$ref": "#/$defs/color" }
            }
        },

        "textNode": {
            "title": "Text type nodes store text.",
            "$ref": "#/$defs/genericNode",
            "required": ["type", "text"],
            "properties": {
                "type": { "$ref": "#/$defs/textType" },
                "text": { "title": "Text in plain text with Markdown syntax.", "type": "string"}
            }
        },

        "fileNode": {
            "title": "File type nodes reference other files or attachments, such as images, videos, etc.",
            "$ref": "#/$defs/genericNode",
            "required": ["type", "file"],
            "properties": {
                "type": { "$ref": "#/$defs/fileType" },
                "file": { "title": "The path to the file within the system.", "type": "string"},
                "subpath": { "title": "A subpath that may link to a heading or a block. Always starts with #.", "$ref": "#/$defs/subpathReference"}
            }
        },

        "linkNode": {
            "title": "Link type nodes reference a URL.",
            "$ref": "#/$defs/genericNode",
            "required": ["type", "url"],
            "properties": {
                "type": { "$ref": "#/$defs/linkType" },
                "url": { "title": "The URL referenced by the link", "type": "string", "format": "iri" }
            }
        },

        "groupNode": {
            "title": "Group type nodes are used as a visual container for nodes within it.",
            "$ref": "#/$defs/genericNode",
            "required": ["type"],
            "properties": {
                "type": { "$ref": "#/$defs/groupType" },
                "label": { "title": "A text label for the group.", "type": "string"},
                "background": { "title": "The path to the background image.", "type": "string"},
                "backgroundStyle": { "title": "The rendering style of the background image.", "$ref": "#/$defs/backgroundStyles"}
            }
        },

        "nodeType": {
            "type": "string",
            "enum": ["text", "file", "link", "group"]
        },

        "textType": {
            "title": "The type of a text node.",
            "const": "text"
        },

        "fileType": {
            "title": "The type of a file node.",
            "const": "file"
        },

        "linkType": {
            "title": "The type of a link node.",
            "const": "link"
        },

        "groupType": {
            "title": "The type of a group node.",
            "const": "text"
        },

        "edge": {
            "title": "Edges are lines that connect one node to another.",
            "type": "object",
            "required": ["id", "fromNode", "toNode"],
            "properties": {
                "id": {
                    "title": "A unique identifier for the edge.",
                    "type": "string"
                },
                "fromNode": {
                    "title": "The node id where the connection starts.",
                    "type": "string"
                },
                "fromSide": {
                    "title": "The side where this edge starts.",
                    "$ref": "#/$defs/side"
                },
                "fromEnd": {
                    "title": "The shape of the endpoint at the edge start",
                    "$ref": "#/$defs/endpointShape"
                },
                "toNode": {
                    "title": "The node id where the connection ends.",
                    "type": "string"
                },
                "toSide": {
                    "title": "The side where this edge ends.",
                    "$ref": "#/$defs/side"
                },
                "toEnd": {
                    "title": "The shape of the endpoint at the edge end",
                    "$ref": "#/$defs/endpointShape"
                },
                "color": {
                    "title": "The color of the line.",
                    "$ref": "#/$defs/color"
                },
                "label": {
                    "title": "The text label for the edge.",
                    "type": "string"
                }
            }
        },

        "side": {
            "title": "The side of a node.",
            "type": "string",
            "enum": ["top", "right", "bottom", "left"]
        },

        "endpointShape": {
            "title": "The shape of the endpoint of an edge.",
            "type": "string",
            "enum": ["none", "arrow"]
        },

        "coordinate": {
            "title": "A co-ordinate (x or y) in pixels.",
            "type": "integer"
        },

        "dimension": {
            "title": "A dimension (width or height) in pixels.",
            "type": "integer",
            "minimum": 1
        },

        "color": {
            "title": "The color type is used to encode color data for nodes and edges",
            "oneOf": [
                { "$ref": "#/$defs/hexColor" },
                { "$ref": "#/$defs/presetColor" }
            ]
        },

        "hexColor": {
            "title": "A color in hexadecimal format.",
            "type": "string",
            "pattern": "^#(?:[0-9a-fA-F]{3}){1,2}$"
        },

        "presetColor": {
            "title": "A preset color.",
            "description": "Six preset colors exist, mapped to the following numbers:\n1 red\n2 orange\n3 yellow\n4 green\n5 cyan\n6 purple",
            "type": "integer",
            "enum": [1, 2, 3, 4, 5, 6]
        },

        "subpathReference": {
            "title": "A subpath that may link to a heading or a block. Always starts with #.",
            "type": "string",
            "pattern": "#(?:.*)"
        },

        "backgroundStyles": {
            "title": "The rendering style of a background image.",
            "description": "Options are:\ncover - fills the entire width and height of the node.\nratio - maintains the aspect ratio of the background image.\nrepeat - repeats the image as a pattern in both x/y directions.",
            "type": "string",
            "enum": ["cover", "ratio", "repeat"]
        }
    }
}

From this we can generate object models to validate and operate over the files in a variety of different languages.

For example, I attach code generated by Corvus.JsonSchema to work with the object model in C#/dotnet.

Corvus.JsonSchema.Generated.zip

chainlist commented 6 months ago

This is a duplicate.

You can see my attempt made yesterday. Already done an MR. Maybe there is a few things to tweak like adding description though.

I think we can either work from yours or mine.

mwadams commented 6 months ago

The interesting thing about this approach with the documentation in the schema like this is that we could generate the human-readable text from the schema.

Mearman commented 6 months ago

I've been diving deep into JSON Schema and OpenAPI specs recently. I think building the JSON Schema from TypeScript source is good for maintainability. If there is an appetite for it I can transform this JSON to TS and set up the pipeline for programmatically generating the JSON Schema? also relates to https://github.com/obsidianmd/jsoncanvas/issues/4

mwadams commented 6 months ago

I've been diving deep into JSON Schema and OpenAPI specs recently. I think building the JSON Schema from TypeScript source is good for maintainability. If there is an appetite for it I can transform this JSON to TS and set up the pipeline for programmatically generating the JSON Schema? also relates to #4

Interesting idea!

Couple of questions:

Mearman commented 6 months ago

I've experimented with pretty much every tool I can find. So it's dealer's choice. I've found Draft-07 to be a lot more widely supported still, but I have enough reliable tooling to support 2020-12. What are @kepano and @ericaxu's thoughts?

mwadams commented 6 months ago

Over in json-schema land we are warmly encouraging people to adopt 2020-12. Most draft7 specs translate trivially and you will be well positioned for vNext. There is now plenty of high quality tooling for most languages.

Mearman commented 6 months ago

@mwadams for me the one downside is (as of writing) lack of support in VSCode for 2020-12