font_map_core/
codegen.rs

1//! Code generation utilities for fonts
2use proc_macro2::TokenStream;
3use quote::format_ident;
4use std::{collections::HashMap, vec};
5
6use crate::font::{Font, StringKind};
7
8mod docstring;
9use docstring::DocstringExt;
10
11mod to_ident;
12use to_ident::{to_categories, to_identifiers, ToIdentExt};
13
14mod category;
15use category::FontCategoryDesc;
16
17mod glyph;
18pub use glyph::GlyphDesc;
19
20#[cfg(feature = "codegen")]
21#[cfg_attr(docsrs, doc(cfg(feature = "codegen")))]
22pub use quote::quote;
23
24/// Describes a font used for code generation
25#[derive(Debug, Clone)]
26pub struct FontDesc {
27    identifier: String,
28    family: Option<String>,
29    comments: Vec<String>,
30    categories: Vec<FontCategoryDesc>,
31}
32impl FontDesc {
33    /// Describe the font from a `Font` instance, optionally skipping categories
34    pub fn from_font(identifier: &str, font: &Font, skip_categories: bool) -> Self {
35        let identifier = identifier.to_string();
36        let family = font.string(StringKind::FontFamily).map(ToString::to_string);
37        let mut comments = font.gen_docblock();
38
39        //
40        // Get initial categories
41        let mut categories = if skip_categories {
42            // If set, skip categorization all-together
43            let glyphs = to_identifiers(font.glyphs());
44            vec![FontCategoryDesc::new(&identifier, glyphs)]
45        } else {
46            // Otherwise, attempt a best-effort categorization
47            let raw_categories = to_categories(font.glyphs());
48            let mut categories = Vec::with_capacity(raw_categories.len());
49            for (name, glyphs) in raw_categories {
50                categories.push(FontCategoryDesc::new(&name, glyphs));
51            }
52
53            categories
54        };
55
56        //
57        // If we have just one, fall-back to single-cat generation
58        if categories.len() == 1 {
59            let category = &mut categories[0];
60            category.set_name(identifier.clone());
61            category.set_comments(comments.drain(..));
62            category.sort();
63
64            return Self {
65                identifier,
66                family,
67                comments,
68                categories,
69            };
70        }
71
72        //
73        // Extract (or create) the `Other` category
74        let mut other = categories
75            .iter()
76            .position(|c| c.name() == "Other")
77            .map_or_else(
78                || FontCategoryDesc::new("Other", HashMap::default()),
79                |idx| categories.swap_remove(idx),
80            );
81
82        //
83        // Extract all categories with < 3 glyphs and merge them with `Other`
84        categories = categories
85            .drain(..)
86            .filter_map(|category| {
87                if category.glyphs().len() > 2 {
88                    return Some(category);
89                }
90
91                let (name, glyphs) = category.into_inner();
92                for mut glyph in glyphs {
93                    let identifier = name.merge_identifiers(glyph.identifier());
94                    glyph.set_identifier(identifier);
95                    other.insert(glyph);
96                }
97                None
98            })
99            .collect();
100
101        //
102        // Update/Add Other
103        other.update_comments();
104        categories.push(other);
105
106        //
107        // Sort the categories by name
108        categories.sort_by(|a, b| a.name().cmp(b.name()));
109        categories.iter_mut().for_each(FontCategoryDesc::sort);
110
111        Self {
112            identifier,
113            family,
114            comments,
115            categories,
116        }
117    }
118
119    /// Returns true if this font has only one category
120    #[must_use]
121    pub fn is_single_category(&self) -> bool {
122        self.categories.len() == 1
123    }
124
125    /// Generate the code for the font
126    ///
127    /// Optionally, you can inject additional code into the generated font's impl
128    #[allow(clippy::needless_pass_by_value)]
129    #[allow(clippy::too_many_lines)]
130    #[must_use]
131    pub fn codegen(&self, extra_impl: Option<TokenStream>) -> TokenStream {
132        let identifier = format_ident!("{}", &self.identifier);
133        let outer_comments = &self.comments;
134        let font_family = self.family.iter();
135        let injection = extra_impl.iter();
136
137        if self.is_single_category() {
138            let category = &self.categories[0];
139
140            category.codegen(Some(quote! {
141                #(
142                    /// The family name for font
143                    pub const FONT_FAMILY: &str = #font_family;
144                )*
145
146                #(
147                    #injection
148                )*
149            }))
150        } else {
151            //
152            // Categories in a module, generate an outer wrapper enum
153            let mut categories = Vec::with_capacity(self.categories.len());
154            for category in &self.categories {
155                categories.push(category.codegen(None));
156            }
157
158            let mut variant_names = Vec::with_capacity(categories.len());
159            let mut variants = Vec::with_capacity(categories.len());
160            for category in &self.categories {
161                let name = format_ident!("{}", category.name());
162                let comments = category.comments();
163                let variant = quote! {
164                    #( #[doc = #comments] )*
165                    #name(categories :: #name),
166                };
167
168                variant_names.push(name);
169                variants.push(variant);
170            }
171
172            quote! {
173                /// Contains a set of enums for each of the sub-categories in this font
174                pub mod categories {
175                    #( #categories )*
176                }
177
178                #[allow(rustdoc::bare_urls)]
179                #( #[doc = #outer_comments] )*
180                #[doc = ""]
181                #[doc = "See the [`categories`] module for more information."]
182                #[derive(Debug, Clone, Copy)]
183                #[rustfmt::skip]
184                pub enum #identifier {
185                    #( #variants )*
186                }
187
188                #[rustfmt::skip]
189                #[allow(dead_code)]
190                impl #identifier {
191                    #(
192                        /// The family name for this glyph's font
193                        pub const FONT_FAMILY: &str = #font_family;
194                    )*
195
196                    /// Returns the postscript name of the glyph
197                    #[allow(clippy::too_many_lines)]
198                    #[allow(clippy::match_same_arms)]
199                    #[must_use]
200                    pub fn name(&self) -> &'static str {
201                        match self {
202                            #( Self :: #variant_names(inner) => inner.name(), )*
203                        }
204                    }
205
206                    #(
207                        #injection
208                    )*
209                }
210
211                #(
212                    impl From<categories :: #variant_names> for #identifier {
213                        fn from(value: categories :: #variant_names) -> Self {
214                            Self :: #variant_names(value)
215                        }
216                    }
217                )*
218
219                impl From<#identifier> for char {
220                    fn from(value: #identifier) -> Self {
221                        match value {
222                            #( #identifier :: #variant_names(inner) => char::from(inner), )*
223                        }
224                    }
225                }
226
227                impl From<&#identifier> for char {
228                    fn from(value: &#identifier) -> Self {
229                        (*value).into()
230                    }
231                }
232
233                impl From<#identifier> for u32 {
234                    fn from(value: #identifier) -> Self {
235                        match value {
236                            #( #identifier :: #variant_names(inner) => inner as u32, )*
237                        }
238                    }
239                }
240
241                impl From<&#identifier> for u32 {
242                    fn from(value: &#identifier) -> Self {
243                        (*value).into()
244                    }
245                }
246
247                impl std::fmt::Display for #identifier {
248                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249                        match self {
250                            #( #identifier :: #variant_names(inner) => inner.fmt(f), )*
251                        }
252                    }
253                }
254            }
255        }
256    }
257}
258
259impl From<&FontDesc> for TokenStream {
260    fn from(value: &FontDesc) -> Self {
261        value.codegen(None)
262    }
263}