1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
3
4#![allow(non_snake_case)]
5#![allow(non_camel_case_types)]
6
7use clap::Parser;
8use futures::prelude::*;
9use std::collections::HashMap;
10use std::fmt::Display;
11use tokio::io::AsyncWriteExt;
12
13#[derive(Debug, Parser)]
14#[command(author, version, about, long_about = None)]
15struct Opt {
16 /// Figma access token
17 #[arg(short = 't', long = "token")]
18 token: String,
19 /// If present, load the specific node id
20 #[arg(short = 'n', long = "node")]
21 node_id: Option<String>,
22 /// If present, load the specific child node at the specified index
23 #[arg(long = "child")]
24 child_index: Option<usize>,
25 /// If set, don't connect to the network, but use the `figma_output/cache.json`
26 #[arg(long)]
27 read_from_cache: bool,
28 /// Figma file
29 file: String,
30}
31
32mod figmatypes;
33mod rendered;
34
35fn fill_hash<'x>(hash: &mut HashMap<&'x str, &'x figmatypes::Node>, node: &'x figmatypes::Node) {
36 let n: &NodeCommon = node.common();
37 hash.insert(&n.id, v:node);
38 for x: &Node in n.children.iter() {
39 fill_hash(hash, node:x);
40 }
41}
42
43#[derive(Debug)]
44struct Error(String);
45impl std::error::Error for Error {}
46impl Display for Error {
47 fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
48 self.0.fmt(f)
49 }
50}
51
52async fn load_from_network(opt: &Opt) -> Result<figmatypes::File, Box<dyn std::error::Error>> {
53 println!("Fetch document {}...", opt.file);
54 let full_doc = reqwest::Client::new()
55 .get(&format!("https://api.figma.com/v1/files/{}?geometry=paths", opt.file))
56 .header("X-Figma-Token", opt.token.clone())
57 .send()
58 .await?
59 .bytes()
60 .await?;
61
62 std::fs::create_dir_all("figma_output/images")?;
63 std::fs::write("figma_output/cache.json", &full_doc)?;
64
65 let r: figmatypes::File = serde_json::from_slice(&full_doc)?;
66
67 use serde::Deserialize;
68 #[derive(Deserialize)]
69 struct ImageResult {
70 meta: ImageResultMeta,
71 }
72 #[derive(Deserialize)]
73 struct ImageResultMeta {
74 images: HashMap<String, String>,
75 }
76
77 let i: ImageResult = reqwest::Client::new()
78 .get(&format!("https://api.figma.com/v1/files/{}/images", opt.file))
79 .header("X-Figma-Token", &opt.token)
80 .send()
81 .await?
82 .json()
83 .await?;
84
85 println!("Fetch {} images ...", i.meta.images.len());
86 let mut images = stream::iter(i.meta.images)
87 .map(|(k, v)| async move {
88 let mut resp = reqwest::Client::new().get(&v).send().await?.bytes_stream();
89 let mut file = tokio::fs::File::create(format!("figma_output/images/{}", k)).await?;
90 while let Some(bytes) = resp.next().await {
91 file.write_all(&(bytes?)).await?;
92 }
93 Result::<(), Box<dyn std::error::Error>>::Ok(())
94 })
95 .buffer_unordered(8);
96 while let Some(x) = images.next().await {
97 x?
98 }
99
100 Ok(r)
101}
102
103#[tokio::main]
104async fn main() -> Result<(), Box<dyn std::error::Error>> {
105 let opt = Opt::parse();
106
107 let r = if !opt.read_from_cache {
108 load_from_network(&opt).await?
109 } else {
110 let full_doc = std::fs::read("figma_output/cache.json")?;
111 serde_json::from_slice(&full_doc)?
112 };
113
114 let mut nodeHash = HashMap::new();
115 fill_hash(&mut nodeHash, &r.document);
116 let doc = rendered::Document { nodeHash };
117
118 if let figmatypes::Node::DOCUMENT(document) = &r.document {
119 if let figmatypes::Node::CANVAS { node, prototypeStartNodeID, backgroundColor, .. } =
120 &document.children[0]
121 {
122 let render_node = if let Some(node_id) = &opt.node_id {
123 doc.nodeHash
124 .get(node_id.as_str())
125 .ok_or_else(|| Error(format!("Could not find node id {}", node_id)))?
126 } else if let Some(child_index) = opt.child_index {
127 node.children
128 .get(child_index)
129 .ok_or_else(|| Error(format!("The index {} does not exist", child_index)))?
130 } else {
131 doc.nodeHash
132 .get(
133 prototypeStartNodeID
134 .as_ref()
135 .ok_or_else(|| Error("No start node in this project. Use '--node' to specify which node to render".into()))?
136 .as_str(),
137 )
138 .ok_or_else(|| Error("Start node not found".into()))?
139 };
140 let result = rendered::render(node.name.as_str(), render_node, *backgroundColor, &doc)?;
141
142 std::fs::write("figma_output/main.slint", result)?;
143 }
144 }
145
146 Ok(())
147}
148