| 1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
| 3 | |
| 4 | // cspell:ignore slintdocs pipenv pipfile |
| 5 | |
| 6 | use anyhow::{Context, Result}; |
| 7 | use std::fs::create_dir_all; |
| 8 | use std::io::{BufWriter, Write}; |
| 9 | use std::path::Path; |
| 10 | use xshell::{cmd, Shell}; |
| 11 | |
| 12 | pub fn generate() -> Result<(), Box<dyn std::error::Error>> { |
| 13 | generate_enum_docs()?; |
| 14 | generate_builtin_struct_docs()?; |
| 15 | |
| 16 | let root: PathBuf = super::root_dir(); |
| 17 | |
| 18 | let docs_source_dir: PathBuf = root.join(path:"docs/astro" ); |
| 19 | |
| 20 | { |
| 21 | let sh: Shell = Shell::new()?; |
| 22 | let _p: PushDir<'_> = sh.push_dir(&docs_source_dir); |
| 23 | cmd!(sh, "pnpm install --frozen-lockfile --ignore-scripts" ).run()?; |
| 24 | cmd!(sh, "pnpm run build" ).run()?; |
| 25 | } |
| 26 | |
| 27 | Ok(()) |
| 28 | } |
| 29 | |
| 30 | fn write_individual_enum_files( |
| 31 | root_dir: &Path, |
| 32 | enums: &std::collections::BTreeMap<String, EnumDoc>, |
| 33 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 34 | let enums_dir = root_dir.join("docs/astro/src/content/collections/enums" ); |
| 35 | create_dir_all(&enums_dir).context(format!( |
| 36 | "Failed to create folder holding individual enum doc files {enums_dir:?}" |
| 37 | ))?; |
| 38 | |
| 39 | for (k, e) in enums { |
| 40 | let path = enums_dir.join(format!(" {k}.md" )); |
| 41 | let mut file = BufWriter::new( |
| 42 | std::fs::File::create(&path).context(format!("error creating {path:?}" ))?, |
| 43 | ); |
| 44 | |
| 45 | write!( |
| 46 | file, |
| 47 | r#"--- |
| 48 | title: {0} |
| 49 | description: {0} content |
| 50 | --- |
| 51 | |
| 52 | <!-- Generated with `cargo xtask slintdocs` from internal/commons/enums.rs --> |
| 53 | |
| 54 | ` {0}` |
| 55 | |
| 56 | {1} |
| 57 | "# , |
| 58 | k, e.description |
| 59 | )?; |
| 60 | for v in &e.values { |
| 61 | write!( |
| 62 | file, |
| 63 | r#"* **` {}`**: {} |
| 64 | "# , |
| 65 | v.key, v.description |
| 66 | )?; |
| 67 | } |
| 68 | } |
| 69 | Ok(()) |
| 70 | } |
| 71 | |
| 72 | pub struct EnumValueDoc { |
| 73 | key: String, |
| 74 | description: String, |
| 75 | } |
| 76 | |
| 77 | pub struct EnumDoc { |
| 78 | pub description: String, |
| 79 | pub values: Vec<EnumValueDoc>, |
| 80 | } |
| 81 | |
| 82 | pub fn extract_enum_docs() -> std::collections::BTreeMap<String, EnumDoc> { |
| 83 | let mut enums: std::collections::BTreeMap<String, EnumDoc> = std::collections::BTreeMap::new(); |
| 84 | |
| 85 | macro_rules! gen_enums { |
| 86 | ($( $(#[doc = $enum_doc:literal])* $(#[non_exhaustive])? enum $Name:ident { $( $(#[doc = $value_doc:literal])* $Value:ident,)* })*) => { |
| 87 | $( |
| 88 | let name = stringify!($Name).to_string(); |
| 89 | let mut description = String::new(); |
| 90 | $( description += &format!("{} \n" , $enum_doc); )* |
| 91 | |
| 92 | let mut values = Vec::new(); |
| 93 | |
| 94 | $( |
| 95 | let mut value_docs = String::new(); |
| 96 | $( |
| 97 | value_docs += $value_doc; |
| 98 | )* |
| 99 | values.push(EnumValueDoc { key: to_kebab_case(stringify!($Value)), description: value_docs }); |
| 100 | )* |
| 101 | |
| 102 | enums.insert(name, EnumDoc { description, values}); |
| 103 | )* |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | #[allow (unused)] // for 'has_val' |
| 108 | { |
| 109 | i_slint_common::for_each_enums!(gen_enums); |
| 110 | } |
| 111 | |
| 112 | enums |
| 113 | } |
| 114 | |
| 115 | pub fn generate_enum_docs() -> Result<(), Box<dyn std::error::Error>> { |
| 116 | let enums: BTreeMap = extract_enum_docs(); |
| 117 | |
| 118 | write_individual_enum_files(&super::root_dir(), &enums)?; |
| 119 | |
| 120 | Ok(()) |
| 121 | } |
| 122 | |
| 123 | pub struct StructFieldDoc { |
| 124 | key: String, |
| 125 | description: String, |
| 126 | type_name: String, |
| 127 | } |
| 128 | |
| 129 | pub struct StructDoc { |
| 130 | pub description: String, |
| 131 | pub fields: Vec<StructFieldDoc>, |
| 132 | } |
| 133 | |
| 134 | pub fn extract_builtin_structs() -> std::collections::BTreeMap<String, StructDoc> { |
| 135 | // `Point` should be in the documentation, but it's not inside of `for_each_builtin_structs`, |
| 136 | // so we manually create its entry first. |
| 137 | let mut structs = std::collections::BTreeMap::from([( |
| 138 | "Point" .to_string(), |
| 139 | StructDoc { |
| 140 | description: "This structure represents a point with x and y coordinate" .to_string(), |
| 141 | fields: vec![ |
| 142 | StructFieldDoc { |
| 143 | key: "x" .to_string(), |
| 144 | description: String::new(), |
| 145 | type_name: "length" .to_string(), |
| 146 | }, |
| 147 | StructFieldDoc { |
| 148 | key: "y" .to_string(), |
| 149 | description: String::new(), |
| 150 | type_name: "length" .to_string(), |
| 151 | }, |
| 152 | ], |
| 153 | }, |
| 154 | )]); |
| 155 | |
| 156 | macro_rules! map_type { |
| 157 | (i32) => { |
| 158 | stringify!(int) |
| 159 | }; |
| 160 | (f32) => { |
| 161 | stringify!(float) |
| 162 | }; |
| 163 | (SharedString) => { |
| 164 | stringify!(string) |
| 165 | }; |
| 166 | (Coord) => { |
| 167 | "length" |
| 168 | }; |
| 169 | (Image) => { |
| 170 | "image" |
| 171 | }; |
| 172 | ($pub_type:ident) => { |
| 173 | stringify!($pub_type) |
| 174 | }; |
| 175 | } |
| 176 | |
| 177 | macro_rules! gen_structs { |
| 178 | ($( |
| 179 | $(#[doc = $struct_doc:literal])* |
| 180 | $(#[non_exhaustive])? |
| 181 | $(#[derive(Copy, Eq)])? |
| 182 | struct $Name:ident { |
| 183 | @name = $inner_name:literal |
| 184 | export { |
| 185 | $( $(#[doc = $pub_doc:literal])* $pub_field:ident : $pub_type:ident, )* |
| 186 | } |
| 187 | private { |
| 188 | $( $(#[doc = $pri_doc:literal])* $pri_field:ident : $pri_type:ty, )* |
| 189 | } |
| 190 | } |
| 191 | )*) => { |
| 192 | $( |
| 193 | let name = stringify!($Name).to_string(); |
| 194 | let mut description = String::new(); |
| 195 | $(description += &format!("{} \n" , $struct_doc);)* |
| 196 | |
| 197 | let mut fields = Vec::new(); |
| 198 | $( |
| 199 | let key = stringify!($pub_field).to_string(); |
| 200 | let type_name = map_type!($pub_type).to_string(); |
| 201 | let mut f_description = String::new(); |
| 202 | $( |
| 203 | f_description += &format!("{}" , $pub_doc); |
| 204 | )* |
| 205 | fields.push(StructFieldDoc { key, description: f_description, type_name }); |
| 206 | )* |
| 207 | structs.insert(name, StructDoc { description, fields }); |
| 208 | )* |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | i_slint_common::for_each_builtin_structs!(gen_structs); |
| 213 | |
| 214 | // `StateInfo` should not be in the documentation, so remove it again |
| 215 | structs.remove("StateInfo" ); |
| 216 | // Experimental type |
| 217 | structs.remove("MenuEntry" ); |
| 218 | |
| 219 | structs |
| 220 | } |
| 221 | |
| 222 | fn write_individual_struct_files( |
| 223 | root_dir: &Path, |
| 224 | structs: std::collections::BTreeMap<String, StructDoc>, |
| 225 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 226 | let structs_dir = root_dir.join("docs/astro/src/content/collections/structs" ); |
| 227 | create_dir_all(&structs_dir).context(format!( |
| 228 | "Failed to create folder holding individual structs doc files {structs_dir:?}" |
| 229 | ))?; |
| 230 | |
| 231 | for (s, v) in &structs { |
| 232 | let path = structs_dir.join(format!(" {s}.md" )); |
| 233 | let mut file = BufWriter::new( |
| 234 | std::fs::File::create(&path).context(format!("error creating {path:?}" ))?, |
| 235 | ); |
| 236 | |
| 237 | write!( |
| 238 | file, |
| 239 | r#"--- |
| 240 | title: {0} |
| 241 | description: {0} content |
| 242 | --- |
| 243 | |
| 244 | <!-- Generated with `cargo xtask slintdocs` from internal/common/builtin_structs.rs --> |
| 245 | |
| 246 | ` {0}` |
| 247 | |
| 248 | {1} |
| 249 | "# , |
| 250 | s, v.description |
| 251 | )?; |
| 252 | |
| 253 | for f in &v.fields { |
| 254 | write!( |
| 255 | file, |
| 256 | r#"- **` {}`** (_ {}_): {} |
| 257 | "# , |
| 258 | f.key, f.type_name, f.description |
| 259 | )?; |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | Ok(()) |
| 264 | } |
| 265 | |
| 266 | pub fn generate_builtin_struct_docs() -> Result<(), Box<dyn std::error::Error>> { |
| 267 | let structs: BTreeMap = extract_builtin_structs(); |
| 268 | write_individual_struct_files(&super::root_dir(), structs) |
| 269 | } |
| 270 | |
| 271 | /// Convert a ascii pascal case string to kebab case |
| 272 | fn to_kebab_case(str: &str) -> String { |
| 273 | let mut result: Vec = Vec::with_capacity(str.len()); |
| 274 | for x: &u8 in str.as_bytes() { |
| 275 | if x.is_ascii_uppercase() { |
| 276 | if !result.is_empty() { |
| 277 | result.push(b'-' ); |
| 278 | } |
| 279 | result.push(x.to_ascii_lowercase()); |
| 280 | } else { |
| 281 | result.push(*x); |
| 282 | } |
| 283 | } |
| 284 | String::from_utf8(vec:result).unwrap() |
| 285 | } |
| 286 | |