summaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 55f83cd724bff054aeeba7b6f68b2f927f1bf41c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process;

use anyhow::Context as _;
use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term::{self, termcolor, Config, Styles};
use same_file::is_same_file;
use termcolor::{ColorChoice, StandardStream, WriteColor};

use typst::diag::Error;
use typst::source::SourceStore;

fn main() {
    if let Err(error) = try_main() {
        print_error(error).unwrap();
        process::exit(1);
    }
}

/// The main compiler logic.
fn try_main() -> anyhow::Result<()> {
    let args = Args::from_env().unwrap_or_else(|_| {
        print_usage().unwrap();
        process::exit(2);
    });

    // Create a loader for fonts and files.
    let loader = typst::loading::FsLoader::new()
        .with_path("fonts")
        .with_system()
        .wrap();

    // Create the context which holds loaded source files, fonts, images and
    // cached artifacts.
    let mut ctx = typst::Context::new(loader);

    // Ensure that the source file is not overwritten.
    if is_same_file(&args.input, &args.output).unwrap_or(false) {
        anyhow::bail!("source and destination files are the same");
    }

    // Load the source file.
    let id = ctx.sources.load(&args.input).context("source file not found")?;

    // Typeset.
    match ctx.typeset(id) {
        // Export the PDF.
        Ok(document) => {
            let buffer = typst::export::pdf(&ctx, &document);
            fs::write(&args.output, buffer).context("failed to write PDF file")?;
        }

        // Print diagnostics.
        Err(errors) => {
            print_diagnostics(&ctx.sources, *errors)
                .context("failed to print diagnostics")?;
        }
    }

    Ok(())
}

struct Args {
    input: PathBuf,
    output: PathBuf,
}

impl Args {
    fn from_env() -> Result<Self, anyhow::Error> {
        let mut parser = pico_args::Arguments::from_env();

        // Parse free-standing arguments.
        let input = parser.free_from_str::<PathBuf>()?;
        let output = match parser.opt_free_from_str()? {
            Some(output) => output,
            None => {
                let name = input.file_name().context("source path is not a file")?;
                Path::new(name).with_extension("pdf")
            }
        };

        // Don't allow excess arguments.
        if !parser.finish().is_empty() {
            anyhow::bail!("too many arguments");
        }

        Ok(Self { input, output })
    }
}

/// Print a usage message.
fn print_usage() -> io::Result<()> {
    let mut w = StandardStream::stderr(ColorChoice::Always);
    let styles = Styles::default();

    w.set_color(&styles.header_help)?;
    write!(w, "usage")?;

    w.set_color(&styles.header_message)?;
    writeln!(w, ": typst <input.typ> [output.pdf]")
}

/// Print an error outside of a source file.
fn print_error(error: anyhow::Error) -> io::Result<()> {
    let mut w = StandardStream::stderr(ColorChoice::Always);
    let styles = Styles::default();

    for (i, cause) in error.chain().enumerate() {
        w.set_color(&styles.header_error)?;
        write!(w, "{}", if i == 0 { "error" } else { "cause" })?;

        w.set_color(&styles.header_message)?;
        writeln!(w, ": {}", cause)?;
    }

    w.reset()
}

/// Print diagnostics messages to the terminal.
fn print_diagnostics(
    sources: &SourceStore,
    errors: Vec<Error>,
) -> Result<(), codespan_reporting::files::Error> {
    let mut w = StandardStream::stderr(ColorChoice::Always);
    let config = Config { tab_width: 2, ..Default::default() };

    for error in errors {
        // The main diagnostic.
        let diag = Diagnostic::error().with_message(error.message).with_labels(vec![
            Label::primary(error.span.source, error.span.to_range()),
        ]);

        term::emit(&mut w, &config, sources, &diag)?;

        // Stacktrace-like helper diagnostics.
        for point in error.trace {
            let message = point.v.to_string();
            let help = Diagnostic::help().with_message(message).with_labels(vec![
                Label::primary(point.span.source, point.span.to_range()),
            ]);

            term::emit(&mut w, &config, sources, &help)?;
        }
    }

    Ok(())
}