mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
* Add craft-figma-rest: a typed model of the Figma REST file JSON The neutral model for design files: the .fig and .sketch readers map onto it and an application's importer takes it. Typed records for the document tree, paints, effects, text styles, layout grids, constraints, export settings and the component and style metadata; every record keeps the fields it has no name for, so a file read and written loses nothing. The vectorNetwork extension is typed and documented with the others. The layer tree is converted level by level in both directions, and the depth and node limits are checked on the JSON before any typed value is built. * craft-figma-rest: Warnings and style_name for the readers that build this shape Moved from FigCraft's REST importer and .fig reader (crates/format/src/rest.rs Warnings, fig.rs style_name), which the .fig and Sketch mappings share.
115 lines
5.9 KiB
Markdown
115 lines
5.9 KiB
Markdown
# craft-figma-rest
|
|
|
|
A typed model of the Figma REST API file JSON: the response of `GET /v1/files/:key` (a `document`
|
|
with `CANVAS` pages, plus `components`, `componentSets` and `styles`) and of
|
|
`GET /v1/files/:key/nodes`. Read and written with serde. No `unsafe`.
|
|
|
|
This shape is the neutral model for design files in craft-libs: the `.fig` reader
|
|
([`craft-fig`](../fig/README.md)) and the `.sketch` reader ([`craft-sketch`](../sketch/README.md))
|
|
map what they read onto it, and an application's importer takes it. It is the file's own shape,
|
|
named as the API names it, and knows nothing of any application's document.
|
|
|
|
## Provenance
|
|
|
|
Written from the published REST API reference only (the documented node types and their
|
|
properties). No code or schema file of the API's owner or of any other product was copied.
|
|
|
|
## The model
|
|
|
|
- `File`: `name`, `lastModified`, `version`, `schemaVersion`, `document`, `components`,
|
|
`componentSets`, `styles`. `FileNodes` / `FileNode`: the `nodes` response.
|
|
- `Node`: a layer's `props` (`NodeProps`, read through the node: `node.fills`) and its `children`.
|
|
One record for every node type, since the API's types share most properties.
|
|
- `NodeProps` names: identity (`id`, `name`, `type`, `visible`, `locked`), geometry
|
|
(`absoluteBoundingBox`, `absoluteRenderBounds`, `relativeTransform`, `size`, `rotation`,
|
|
`constraints`), appearance (`fills`, `strokes` and the stroke properties, `fillGeometry`,
|
|
`strokeGeometry`, `cornerRadius`, `rectangleCornerRadii`, `cornerSmoothing`, `effects`,
|
|
`opacity`, `blendMode`, `isMask`, `maskType`), auto layout (`layoutMode` and the padding,
|
|
spacing, sizing and alignment properties), `layoutGrids`, `exportSettings`, `styles`, text
|
|
(`characters`, `style`, `characterStyleOverrides`, `styleOverrideTable`, `lineTypes`,
|
|
`lineIndentations`), `componentId`, `booleanOperation`, `arcData`.
|
|
- `Paint`, `ColorStop`, `Effect`, `TypeStyle`, `LayoutGrid`, `LayoutConstraint`, `ExportSetting`,
|
|
`Path`, `ArcData`, `Color`, `Vector`, `Rectangle`, `Transform`, and the metadata records
|
|
`Component`, `ComponentSet`, `Style`.
|
|
- `NodeType`, `PaintType`, `EffectType`, `BlendMode`, `StyleType`: the values known here and
|
|
`Other(String)` for the rest. Other enumerations are strings, with their values in the field's
|
|
documentation.
|
|
|
|
Every field is optional: the API leaves out what has its default.
|
|
|
|
### Not typed yet
|
|
|
|
Kept in `extra` (see below), as JSON: prototyping (`interactions`, `flowStartingPoints`,
|
|
`prototypeDevice`, transitions), variables (`boundVariables`, `explicitVariableModes`, the local
|
|
variables response), component properties (`componentPropertyDefinitions`,
|
|
`componentProperties`, `componentPropertyReferences`, `overrides`) and grid auto layout tracks.
|
|
|
|
## Nothing is lost
|
|
|
|
Every record has `extra`: the fields the model has no name for, in the JSON they came in, written
|
|
back on output. A file read and written keeps properties the API added after this model was
|
|
written. Two things change on a round trip: a field that was `null` is left out, and a whole
|
|
number written as `10` comes back as `10.0`.
|
|
|
|
## Extensions
|
|
|
|
Fields that are not in the REST API, added by the readers that map other files onto this shape so
|
|
that nothing those files hold is lost (`ext` documents each):
|
|
|
|
| Field | On | Holds |
|
|
|---|---|---|
|
|
| `vectorNetwork` | a `VECTOR` node | the editable network (`ext::VectorNetwork`, typed on `NodeProps`): vertices with corner radii, segments with tangents, regions with loops and winding |
|
|
| `vectorNetworkRef` | a `VECTOR` node | an index into a list of networks handed over beside the JSON |
|
|
| `pointCount`, `innerRadius` | `STAR`, `REGULAR_POLYGON` | points or sides; the star's inner radius as a fraction |
|
|
| `_letterSpacingPercent` | a text style | letter spacing as a percentage of the font size |
|
|
|
|
A consumer that doesn't know an extension ignores it; the public fields still describe the layer.
|
|
Fields starting with `_fig` are private handoffs between one reader and one importer and are not
|
|
part of this contract; they are kept like any unknown field.
|
|
|
|
## For readers
|
|
|
|
`Warnings` collects a reader's notes (each once, with a count, at most 200) and `style_name`
|
|
spells a font style the way this shape names it ("SemiBold" → "Semi Bold"), so the readers that
|
|
build this shape report and name things alike.
|
|
|
|
## Reading and writing
|
|
|
|
```rust
|
|
let file = craft_figma_rest::File::from_slice(&bytes)?; // or from_value(value, max_nodes)
|
|
for node in file.document.iter().flat_map(|d| d.walk()) { // parents first, no recursion
|
|
if node.kind == Some(craft_figma_rest::NodeType::Text) {
|
|
let text = node.characters.as_deref().unwrap_or_default();
|
|
}
|
|
}
|
|
let json = file.to_value()?;
|
|
```
|
|
|
|
Building one:
|
|
|
|
```rust
|
|
use craft_figma_rest::{Node, NodeProps, NodeType};
|
|
let rect = Node::new(NodeProps { id: Some("1:1".into()), kind: Some(NodeType::Rectangle), ..Default::default() });
|
|
let page = Node::with_children(NodeProps { kind: Some(NodeType::Canvas), ..Default::default() }, vec![rect]);
|
|
```
|
|
|
|
## Limits
|
|
|
|
Input is hostile. Nothing panics; every limit has a test.
|
|
|
|
| Limit | Value |
|
|
|---|---|
|
|
| JSON nesting (`MAX_DEPTH`): about 500 layers inside one another | 1024 levels |
|
|
| Nodes (`from_value` takes the limit; `from_slice` uses `DEFAULT_MAX_NODES`) | 200 000 |
|
|
| JSON text nesting (`from_slice`) | serde_json's 128 levels |
|
|
|
|
Both limits are checked on the JSON, without recursion, before any typed value is built.
|
|
`from_value` and `to_value` take the layer tree apart and put it together level by level, so a
|
|
tree at the depth limit is read, written and dropped within a 2 MiB stack (tested). The `Serialize`
|
|
and `Deserialize` implementations go through `serde_json::Value` as well, but their serializer
|
|
recurses once per level: use `to_value` / `from_value` for deep trees.
|
|
|
|
A field of the wrong type fails the read, and the error names the type expected. A reader that
|
|
must skip bad fields one by one (an importer taking files from anywhere) reads the
|
|
`serde_json::Value` instead.
|