Skip to main content

typst/
lib.rs

1//! The compiler for the _Typst_ markup language.
2//!
3//! # Steps
4//! - **Parsing:**
5//!   The compiler first transforms a plain string into an iterator of [tokens].
6//!   This token stream is [parsed] into a [syntax tree]. The tree itself is
7//!   untyped, but the [AST] module provides a typed layer over it.
8//! - **Evaluation:**
9//!   The next step is to [evaluate] the markup. This produces a [module],
10//!   consisting of a scope of values that were exported by the code and
11//!   [content], a hierarchical, styled representation of what was written in
12//!   the source file. The elements of the content tree are well structured and
13//!   order-independent and thus much better suited for further processing than
14//!   the raw markup.
15//! - **Layouting:**
16//!   Next, the content is [laid out] into a
17//!   [`PagedDocument`](typst_layout::PagedDocument) containing one [frame] per
18//!   page with items at fixed positions.
19//! - **Exporting:**
20//!   These frames can finally be exported into an output format (currently PDF,
21//!   PNG, SVG, and HTML).
22//!
23//! [tokens]: typst_syntax::SyntaxKind
24//! [parsed]: typst_syntax::parse
25//! [syntax tree]: typst_syntax::SyntaxNode
26//! [AST]: typst_syntax::ast
27//! [evaluate]: typst_eval::eval
28//! [module]: crate::foundations::Module
29//! [content]: crate::foundations::Content
30//! [laid out]: typst_layout::layout_document
31//! [frame]: crate::layout::Frame
32
33pub extern crate comemo;
34pub extern crate ecow;
35
36pub use typst_library::*;
37#[doc(inline)]
38pub use typst_syntax as syntax;
39#[doc(inline)]
40pub use typst_utils as utils;
41
42use std::sync::LazyLock;
43
44use arrayvec::ArrayVec;
45use comemo::{Track, Tracked};
46use ecow::{EcoString, EcoVec, eco_format, eco_vec};
47use rustc_hash::FxHashSet;
48use typst_library::diag::{
49    FileError, SourceDiagnostic, SourceResult, Warned, bail, warning,
50};
51use typst_library::engine::{Engine, Route, Sink, Traced};
52use typst_library::foundations::{
53    NativeRuleMap, Output, StyleChain, Styles, Target, TargetElem, Value,
54};
55use typst_library::introspection::{
56    EmptyIntrospector, ITER_NAMES, Introspector, MAX_ITERS,
57};
58use typst_library::routines::Routines;
59use typst_syntax::{FileId, Span};
60use typst_timing::{TimingScope, timed};
61use typst_utils::Protected;
62
63/// Compiles sources into an output.
64///
65/// Supported outputs are
66/// - the `PagedDocument` (defined in `typst_layout`)
67/// - the `HtmlDocument` (defined in `typst_html`)
68///
69/// Returns the compilation output alongside warnings, if any. The contained
70/// result is
71/// - `Ok(output)` if there were no fatal errors.
72/// - `Err(errors)` if there were fatal errors.
73#[typst_macros::time]
74pub fn compile<T>(world: &dyn World) -> Warned<SourceResult<T>>
75where
76    T: Output,
77{
78    let mut sink = Sink::new();
79    let output = compile_impl::<T>(world.track(), Traced::default().track(), &mut sink)
80        .map_err(deduplicate);
81    Warned { output, warnings: sink.warnings() }
82}
83
84/// Compiles sources and returns all values and styles observed at the given
85/// `span` during compilation.
86#[typst_macros::time]
87pub fn trace<T>(world: &dyn World, span: Span) -> EcoVec<(Value, Option<Styles>)>
88where
89    T: Output,
90{
91    let mut sink = Sink::new();
92    let traced = Traced::new(span);
93    compile_impl::<T>(world.track(), traced.track(), &mut sink).ok();
94    sink.values()
95}
96
97/// The internal implementation of `compile` with a bit lower-level interface
98/// that is also used by `trace`.
99fn compile_impl<T: Output>(
100    world: Tracked<dyn World + '_>,
101    traced: Tracked<Traced>,
102    sink: &mut Sink,
103) -> SourceResult<T> {
104    let library = world.library();
105    match T::target() {
106        Target::Paged => {}
107        Target::Html => warn_or_error_for_html(&library.features, sink)?,
108        Target::Bundle => warn_or_error_for_bundle(&library.features, sink)?,
109    }
110
111    let base = StyleChain::new(&library.styles);
112    let target = TargetElem::target.set(T::target()).wrap();
113    let styles = base.chain(&target);
114    let empty_introspector = EmptyIntrospector;
115
116    // Fetch the main source file once.
117    let main = world.main();
118    let main = world
119        .source(main)
120        .map_err(|err| hint_invalid_main_file(world, err, main))?;
121
122    // First evaluate the main source file into a module.
123    let content = typst_eval::eval(
124        world,
125        library,
126        traced,
127        sink.track_mut(),
128        Route::default().track(),
129        &main,
130    )?
131    .content();
132
133    let mut history: ArrayVec<T, { MAX_ITERS - 1 }> = ArrayVec::new();
134    let mut document: T;
135
136    // Relayout until all introspections stabilize.
137    // If that doesn't happen within five attempts, we give up.
138    loop {
139        let _scope = TimingScope::new(ITER_NAMES[history.len()]);
140        let introspector = history
141            .last()
142            .map(|doc| doc.introspector())
143            .unwrap_or(&empty_introspector);
144        let constraint = comemo::Constraint::new();
145
146        let mut subsink = Sink::new();
147        let mut engine = Engine {
148            library,
149            world,
150            introspector: Protected::new(introspector.track_with(&constraint)),
151            traced,
152            sink: subsink.track_mut(),
153            route: Route::default(),
154        };
155
156        document = T::create(&mut engine, &content, styles)?;
157
158        if timed!("check stabilized", constraint.validate(document.introspector())) {
159            sink.extend_from_sink(subsink);
160            break;
161        }
162
163        if history.is_full() {
164            let mut introspectors =
165                [&empty_introspector as &dyn Introspector; MAX_ITERS + 1];
166            for i in 1..MAX_ITERS {
167                introspectors[i] = history[i - 1].introspector();
168            }
169            introspectors[MAX_ITERS] = document.introspector();
170
171            let warnings = typst_library::introspection::analyze(
172                world,
173                introspectors,
174                subsink.introspections(),
175            );
176
177            sink.extend_from_sink(subsink);
178            for warning in warnings {
179                sink.warn(warning);
180            }
181            break;
182        }
183
184        history.push(document);
185    }
186
187    // Promote delayed errors.
188    let delayed = sink.delayed();
189    if !delayed.is_empty() {
190        return Err(delayed);
191    }
192
193    Ok(document)
194}
195
196/// Deduplicate diagnostics.
197fn deduplicate(mut diags: EcoVec<SourceDiagnostic>) -> EcoVec<SourceDiagnostic> {
198    let mut unique = FxHashSet::default();
199    diags.retain(|diag| {
200        let hash = typst_utils::hash128(&(&diag.span, &diag.message));
201        unique.insert(hash)
202    });
203    diags
204}
205
206/// Adds useful hints when the main source file couldn't be read
207/// and returns the final diagnostic.
208fn hint_invalid_main_file(
209    world: Tracked<dyn World + '_>,
210    file_error: FileError,
211    input: FileId,
212) -> EcoVec<SourceDiagnostic> {
213    let is_utf8_error = matches!(file_error, FileError::InvalidUtf8);
214    let mut diagnostic =
215        SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));
216
217    // Attempt to provide helpful hints for UTF-8 errors. Perhaps the user
218    // mistyped the filename. For example, they could have written "file.pdf"
219    // instead of "file.typ".
220    if is_utf8_error {
221        match input.vpath().extension() {
222            // No hints if the file is already a .typ file. The file is indeed
223            // just invalid.
224            Some("typ") => return eco_vec![diagnostic],
225
226            Some(ext) => {
227                diagnostic.hint(eco_format!(
228                    "a file with the `.{ext}` extension is not usually a Typst file",
229                ));
230            }
231
232            None => {
233                diagnostic
234                    .hint("a file without an extension is not usually a Typst file");
235            }
236        };
237
238        if world.source(input.map(|p| p.with_extension("typ")).intern()).is_ok() {
239            diagnostic.hint("check if you meant to use the `.typ` extension instead");
240        }
241    }
242
243    eco_vec![diagnostic]
244}
245
246/// HTML export will warn or error depending on whether the feature flag is enabled.
247fn warn_or_error_for_html(features: &Features, sink: &mut Sink) -> SourceResult<()> {
248    const ISSUE: &str = "https://github.com/typst/typst/issues/5512";
249    if features.is_enabled(Feature::Html) {
250        sink.warn(warning!(
251            Span::detached(),
252            "html export is under active development and incomplete";
253            hint: "its behaviour may change at any time";
254            hint: "do not rely on this feature for production use cases";
255            hint: "see {ISSUE} for more information";
256        ));
257    } else {
258        bail!(
259            Span::detached(),
260            "html export is only available when `--features html` is passed";
261            hint: "html export is under active development and incomplete";
262            hint: "see {ISSUE} for more information";
263        );
264    }
265    Ok(())
266}
267
268/// Bundle export will warn or error depending on whether the feature flag is
269/// enabled.
270fn warn_or_error_for_bundle(features: &Features, sink: &mut Sink) -> SourceResult<()> {
271    if features.is_enabled(Feature::Bundle) {
272        sink.warn(warning!(
273            Span::detached(),
274            "bundle export is experimental";
275            hint: "its behaviour may change at any time";
276            hint: "do not rely on this feature for production use cases";
277        ));
278    } else {
279        bail!(
280            Span::detached(),
281            "bundle export is only available when `--features bundle` is passed";
282            hint: "bundle export is experimental";
283        );
284    }
285    Ok(())
286}
287
288/// Provides ways to construct a [`Library`].
289pub trait LibraryExt {
290    /// Creates the default library.
291    fn default() -> Library;
292
293    /// Creates a builder for configuring a library.
294    fn builder() -> LibraryBuilder;
295}
296
297impl LibraryExt for Library {
298    fn default() -> Library {
299        Self::builder().build()
300    }
301
302    fn builder() -> LibraryBuilder {
303        LibraryBuilder::from_routines(&ROUTINES)
304    }
305}
306
307/// Defines implementation of various Typst compiler routines as a table of
308/// function pointers.
309///
310/// This is essentially dynamic linking and done to allow for crate splitting.
311static ROUTINES: LazyLock<Routines> = LazyLock::new(|| Routines {
312    rules: || {
313        let mut rules = NativeRuleMap::new();
314        typst_layout::register(&mut rules);
315        typst_html::register(&mut rules);
316        rules
317    },
318    eval_string: typst_eval::eval_string,
319    eval_closure: typst_eval::eval_closure,
320    realize: typst_realize::realize,
321    layout_frame: typst_layout::layout_frame,
322    html_module: typst_html::module,
323    html_mathml_body: typst_html::html_mathml_body,
324    html_span_filled: typst_html::html_span_filled,
325});