fea_rs/compile/
compiler.rs

1//! The main public API for compilation
2
3use std::{
4    io::Write,
5    path::{Path, PathBuf},
6};
7
8use crate::{
9    parse::{FileSystemResolver, SourceResolver},
10    DiagnosticSet, GlyphMap,
11};
12
13use super::{error::CompilerError, Compilation, FeatureProvider, Opts, VariationInfo};
14
15const DEFAULT_N_MESSAGES_TO_PRINT: usize = 100;
16
17/// A builder-style entry point for the compiler.
18///
19/// This is intended as the principal public API for this crate.
20///
21/// ```no_run
22/// # use fea_rs::{Compiler, compile::{NopFeatureProvider, NopVariationInfo}};
23/// # fn make_glyph_map() -> fea_rs::GlyphMap { todo!() }
24/// let glyph_map = make_glyph_map();
25/// let my_font_bytes = Compiler::<'_, NopFeatureProvider, NopVariationInfo>::new("path/to/features.fea", &glyph_map)
26///     .verbose(true)
27///     .compile_binary().unwrap();
28/// ```
29pub struct Compiler<'a, F: FeatureProvider, V: VariationInfo> {
30    root_path: PathBuf,
31    project_root: Option<PathBuf>,
32    glyph_map: &'a GlyphMap,
33    // variable fonts only
34    var_info: Option<&'a V>,
35    feature_writer: Option<&'a F>,
36    // this is not in `Opts` because it is specific to the compiler struct;
37    // if you're compiling manually you are responsible for handling warnings.
38    print_warnings: bool,
39    max_n_errors: usize,
40    opts: Opts,
41    resolver: Option<Box<dyn SourceResolver>>,
42}
43
44impl<'a, F: FeatureProvider, V: VariationInfo> Compiler<'a, F, V> {
45    /// Configure a new compilation run with a root source and a glyph map.
46    ///
47    /// In the general case, `root_path` will be a path to a feature file on disk;
48    /// however you may compile from memory by passing a custom [`SourceResolver`]
49    /// to the [`with_resolver`] method, in which case `root_path` can be any
50    /// identifier that your resolver will resolve.
51    ///
52    /// [`with_resolver`]: Self::with_resolver
53    pub fn new(root_path: impl Into<PathBuf>, glyph_map: &'a GlyphMap) -> Self {
54        Compiler {
55            root_path: root_path.into(),
56            glyph_map,
57            var_info: None,
58            feature_writer: None,
59            opts: Default::default(),
60            print_warnings: false,
61            resolver: Default::default(),
62            project_root: Default::default(),
63            max_n_errors: DEFAULT_N_MESSAGES_TO_PRINT,
64        }
65    }
66
67    /// Provide a custom `SourceResolver`, for mapping paths to their contents.
68    pub fn with_resolver(mut self, resolver: impl SourceResolver + 'static) -> Self {
69        self.resolver = Some(Box::new(resolver));
70        self
71    }
72
73    /// Provide [`VariationInfo`], necessary when compiling features for a variable font.
74    pub fn with_variable_info(mut self, var_info: &'a V) -> Self {
75        self.var_info = Some(var_info);
76        self
77    }
78
79    /// Provide [`FeatureProvider`] to provide additional features during compilation
80    pub fn with_feature_writer(mut self, feature_writer: &'a F) -> Self {
81        self.feature_writer = Some(feature_writer);
82        self
83    }
84
85    /// Specify verbosity.
86    ///
87    /// When verbose is true, we will print all warnings.
88    #[deprecated(since = "0.14.1", note = "use print_warnings method instead")]
89    pub fn verbose(self, verbose: bool) -> Self {
90        self.print_warnings(verbose)
91    }
92
93    /// Indicate whether or not warnings should be printed (default is `false`)
94    pub fn print_warnings(mut self, warnings: bool) -> Self {
95        self.print_warnings = warnings;
96        self
97    }
98
99    /// Specify an explicit project root.
100    ///
101    /// This is useful in cases where import resolution is based on an explicit
102    /// base directory, such as when dealing with certain source formats.
103    pub fn with_project_root(mut self, project_root: impl Into<PathBuf>) -> Self {
104        self.project_root = Some(project_root.into());
105        self
106    }
107
108    /// Specify additional compiler options.
109    pub fn with_opts(mut self, opts: Opts) -> Self {
110        self.opts = opts;
111        self
112    }
113
114    /// Parse, validate and compile this source.
115    ///
116    /// This returns a `Compilation` object that contains all of the features
117    /// and lookups generated during compilation. If you would like to go directly
118    /// to a binary font, you can use [`compile_binary`] instead.
119    ///
120    /// [`compile_binary`]: Self::compile_binary
121    pub fn compile(self) -> Result<Compilation, CompilerError> {
122        let resolver = self.resolver.unwrap_or_else(|| {
123            let project_root = self.project_root.unwrap_or_else(|| {
124                Path::new(&self.root_path)
125                    .parent()
126                    .map(PathBuf::from)
127                    .unwrap_or_default()
128            });
129            Box::new(FileSystemResolver::new(project_root))
130        });
131
132        let (tree, diagnostics) =
133            crate::parse::ParseContext::parse(self.root_path, Some(self.glyph_map), resolver)?
134                .generate_parse_tree();
135        print_warnings_return_errors(diagnostics, self.print_warnings, self.max_n_errors)
136            .map_err(CompilerError::ParseFail)?;
137        let diagnostics = super::validate(&tree, self.glyph_map, self.var_info);
138        print_warnings_return_errors(diagnostics, self.print_warnings, self.max_n_errors)
139            .map_err(CompilerError::ValidationFail)?;
140        let mut ctx = super::CompilationCtx::new(
141            self.glyph_map,
142            tree.source_map(),
143            self.var_info,
144            self.feature_writer,
145            self.opts,
146        );
147        ctx.compile(&tree.typed_root());
148
149        // we 'take' the errors here because it's easier for us to handle the
150        // warnings using our helper method.
151        let messages = std::mem::take(&mut ctx.errors);
152        let diagnostics = DiagnosticSet::new(messages, &tree, self.max_n_errors);
153        print_warnings_return_errors(diagnostics, self.print_warnings, self.max_n_errors)
154            .map_err(CompilerError::CompilationFail)?;
155        Ok(ctx.build().unwrap().0) // we've taken the errors, so this can't fail
156    }
157
158    /// Compile to a binary font.
159    pub fn compile_binary(self) -> Result<Vec<u8>, CompilerError> {
160        let glyph_map = self.glyph_map;
161        Ok(self.compile()?.to_binary(glyph_map)?)
162    }
163}
164
165fn print_warnings_return_errors(
166    mut diagnostics: DiagnosticSet,
167    print_warnings: bool,
168    max_to_print: usize,
169) -> Result<(), DiagnosticSet> {
170    diagnostics.set_max_to_print(max_to_print);
171    let warnings = diagnostics.split_off_warnings();
172    if let Some(warnings) = warnings {
173        if print_warnings {
174            // get around a CI check denying eprintln
175            let _ = writeln!(std::io::stderr(), "{}", warnings.display());
176        }
177    }
178
179    if diagnostics.is_empty() {
180        Ok(())
181    } else {
182        Err(diagnostics)
183    }
184}