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 | |
7 | use clap::Parser; |
8 | use futures::prelude::*; |
9 | use std::collections::HashMap; |
10 | use std::fmt::Display; |
11 | use tokio::io::AsyncWriteExt; |
12 | |
13 | #[derive (Debug, Parser)] |
14 | #[command(author, version, about, long_about = None)] |
15 | struct 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 | |
32 | mod figmatypes; |
33 | mod rendered; |
34 | |
35 | fn 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)] |
44 | struct Error(String); |
45 | impl std::error::Error for Error {} |
46 | impl Display for Error { |
47 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { |
48 | self.0.fmt(f) |
49 | } |
50 | } |
51 | |
52 | async 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 ] |
104 | async 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 | |