Skip to main content

typst_library/model/
document.rs

1use ecow::EcoString;
2use typst_syntax::VirtualPath;
3
4use crate::diag::{HintedStrResult, bail, error};
5use crate::foundations::{
6    Array, BundlePath, Cast, Content, Datetime, OneOrMultiple, Packed, ShowFn, ShowSet,
7    Smart, StyleChain, Styles, Target, Value, cast, elem,
8};
9use crate::introspection::Locatable;
10use crate::text::{Locale, TextElem};
11
12/// Manages metadata and is used to add a document file to a bundle.
13///
14/// = Metadata <metadata>
15/// The document element is the single source of truth for document metadata.
16/// With it, you can specify the document's title, authors, date, etc. in one
17/// place. Typically, the element is used with a
18/// @reference:styling:set-rules[set rule] like this:
19///
20/// ```example
21/// #set document(title: [My doc])
22///
23/// Title is _not_ rendered, but
24/// embedded in PDF metadata.
25/// ```
26///
27/// By default, the metadata is embedded into the output, but not visibly
28/// rendered in the document. However, it becomes
29/// @reference:context[contextually available] to the full document and can be
30/// used by elements and templates. For instance, the built-in @title element
31/// automatically picks up the configured document title:
32///
33/// ```example
34/// #set document(title: [My doc])
35///
36/// #title()
37/// Title is now rendered _and_
38/// embedded in PDF metadata.
39/// ```
40///
41/// In a similar fashion to the `title` element, you can also access metadata
42/// yourself using a @reference:context[context expression].
43///
44/// ```example
45/// // In the document.
46/// #set document(
47///   keywords: ("Typst", "Metadata")
48/// )
49///
50/// // Somewhere in your template.
51/// _Keywords:_
52/// #context document.keywords.join(", ")
53/// ```
54///
55/// In single-document export formats, this function is only used with
56/// @reference:styling:set-rules[set rules]. Such set rules must only occur at
57/// the top level, not inside of any layout container. You can also explicitly
58/// create a `document` element, but
59/// @document:documents-in-bundle-export[this is only relevant in bundle export].
60///
61/// == Format-specific considerations <format-specific-considerations>
62/// Metadata is embedded into the output to varying extents:
63///
64/// - PDF export supports the full range of metadata and emits it into the PDF
65///   _document information dictionary_ as well as XMP metadata.
66///
67/// - HTML export only supports the `title`, `description`, `author`, and
68///   `keywords` properties. The `date` property is not supported as the HTML
69///   standard has no provision for it.
70///
71/// - SVG and PNG export do not have any metadata support at all.
72///
73/// = Documents in bundle export <documents-in-bundle-export>
74/// In @reference:bundle[bundle export], a document element represents a single
75/// file in the bundle output, in one of Typst's other export formats. When
76/// creating a document, you must provide an output path and some content. Typst
77/// will compile and export the provided content with the appropriate format. By
78/// default, the format is inferred from the file extension of the path you
79/// specified, but you can also configure the @document.format[`format`]
80/// explicitly.
81///
82/// ```typ
83/// #document("index.html", title: [Home])[
84///   #title()
85///   View #link(<list>)[my famous list].
86/// ]
87///
88/// #document("list.html", title: [My Famous List])[
89///   #title()
90///   - My
91///   - Famous
92///   - List
93/// ] <list>
94/// ```
95///
96/// == Metadata <metadata>
97/// Document elements pick up metadata from top-level `{set document}` rules
98/// within them. This means that documents written for single-document export
99/// can be used with explicit `document` elements while properly retaining
100/// metadata.
101///
102/// ```typ
103/// // Will pick up the title defined in `paper.typ`.
104/// #document("paper.pdf", include "paper.typ")
105/// ```
106///
107/// ```typ
108/// // paper.typ
109/// #set document(title: [My Paper])
110/// ...
111/// ```
112///
113/// Note that document set rules within a `document` override explicit arguments
114/// passed to the `document` element.
115///
116/// Moreover, properties configured as explicit arguments to `document` are made
117/// contextually available:
118///
119/// ```typ
120/// #document("index.html", title: [My title])[
121///   // Both of these will pick up `[My title]`
122///   #title()
123///   #context document.title
124/// ]
125/// ```
126#[elem(Locatable, ShowSet)]
127pub struct DocumentElem {
128    /// The path in the bundle at which the exported document will be placed.
129    ///
130    /// May contain interior slashes, in which case intermediate directories
131    /// will be automatically created.
132    ///
133    /// This property is only supported in the @reference:bundle[bundle] target.
134    #[required]
135    pub path: BundlePath,
136
137    /// Which format to export in.
138    ///
139    /// If `{auto}`, Typst attempts to infer the export format from the
140    /// @document.path[`path`'s] file extension.
141    ///
142    /// This property is only supported in the @reference:bundle[bundle] target.
143    pub format: Smart<DocumentFormat>,
144
145    /// The document's title. This is rendered as the title of the PDF viewer
146    /// window or the browser tab of the page.
147    ///
148    /// By default, the configured title is not visibly rendered in the
149    /// document. You can add the title to the document's contents by using the
150    /// @title element. It will automatically pick up the title configured here.
151    ///
152    /// Adding a title is important for accessibility, as it makes it easier to
153    /// navigate to your document and identify it among other open documents.
154    /// When exporting to PDF/UA, a title is required.
155    ///
156    /// While this can be arbitrary content, PDF viewers only support plain text
157    /// titles, so the conversion might be lossy.
158    pub title: Option<Content>,
159
160    /// The document's authors.
161    pub author: OneOrMultiple<EcoString>,
162
163    /// The document's description.
164    pub description: Option<Content>,
165
166    /// The document's keywords.
167    pub keywords: OneOrMultiple<EcoString>,
168
169    /// The document's creation date.
170    ///
171    /// If this is `{auto}` (default), Typst uses the current date and time.
172    /// Setting it to `{none}` prevents Typst from embedding any creation date
173    /// into the PDF metadata.
174    ///
175    /// The year component must be at least zero in order to be embedded into a
176    /// PDF.
177    ///
178    /// If you want to create byte-by-byte reproducible PDFs, set this to
179    /// something other than `{auto}`.
180    pub date: Smart<Option<Datetime>>,
181
182    /// The content that makes up the document.
183    ///
184    /// This property is only supported in the @reference:bundle[bundle] target.
185    #[required]
186    pub body: Content,
187}
188
189impl Packed<DocumentElem> {
190    /// Tries to determine the document's format based on the format that was
191    /// explicitly defined, or else the extension of the document's path.
192    pub fn determine_format(
193        &self,
194        styles: StyleChain,
195    ) -> HintedStrResult<DocumentFormat> {
196        self.format
197            .get(styles)
198            .custom()
199            .or_else(|| determine_format_from_path(self.path.as_ref()))
200            .ok_or_else(|| {
201                error!(
202                    "unknown document format";
203                    hint: "try specifying the `format` explicitly";
204                )
205            })
206    }
207}
208
209/// Derive the document format from the file extension of a path.
210fn determine_format_from_path(path: &VirtualPath) -> Option<DocumentFormat> {
211    match path.extension()? {
212        "pdf" => Some(PagedFormat::Pdf.into()),
213        "svg" => Some(PagedFormat::Svg.into()),
214        "png" => Some(PagedFormat::Png.into()),
215        "html" => Some(DocumentFormat::Html),
216        _ => None,
217    }
218}
219
220pub const DOCUMENT_UNSUPPORTED_RULE: ShowFn<DocumentElem> = |elem, _, _| {
221    bail!(
222        elem.span(),
223        "constructing a document is only supported in the bundle target";
224        // TODO: Support for CLI-specific hints would be nice.
225        hint: "try enabling the bundle target";
226        hint: "or use a `set document(..)` rule to configure metadata";
227    )
228};
229
230impl ShowSet for Packed<DocumentElem> {
231    fn show_set(&self, _: StyleChain) -> Styles {
232        // Here, we make explicit document properties contextually available.
233        // This is mostly relevant for bundle export as document elements are
234        // not directly supported in other targets.
235        //
236        // Making the properties available like this is inconsistent with normal
237        // elements, but consistent with `page` and necessary to make the
238        // `title` element work.
239        //
240        // Nonetheless, it's fairly hacky and the whole thing should probably be
241        // revisited at some point. Also see
242        // <https://github.com/typst/typst/issues/6721>.
243        let mut styles = Styles::new();
244        self.format.copy_into(&mut styles);
245        self.title.copy_into(&mut styles);
246        self.author.copy_into(&mut styles);
247        self.description.copy_into(&mut styles);
248        self.keywords.copy_into(&mut styles);
249        self.date.copy_into(&mut styles);
250        styles
251    }
252}
253
254/// Supported export formats for bundle documents.
255#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
256pub enum DocumentFormat {
257    /// One of the page formats.
258    Paged(PagedFormat),
259    /// The document format of the web.
260    Html,
261}
262
263impl DocumentFormat {
264    pub fn target(self) -> Target {
265        match self {
266            Self::Paged(_) => Target::Paged,
267            Self::Html => Target::Html,
268        }
269    }
270}
271
272impl From<PagedFormat> for DocumentFormat {
273    fn from(format: PagedFormat) -> Self {
274        Self::Paged(format)
275    }
276}
277
278cast! {
279    DocumentFormat,
280    self => match self {
281        Self::Paged(v) => v.into_value(),
282        Self::Html => "html".into_value(),
283    },
284    v: PagedFormat => Self::Paged(v),
285    /// The document format of the web.
286    "html" => Self::Html,
287}
288
289/// Supported paged export formats for bundle documents.
290#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
291pub enum PagedFormat {
292    /// High-fidelity document and graphics format, with focus on exact
293    /// reproduction in print.
294    Pdf,
295    /// Raster format for illustrations and transparent graphics.
296    Png,
297    /// The vector graphics format of the web.
298    Svg,
299}
300
301/// A list of authors.
302#[derive(Debug, Default, Clone, PartialEq, Hash)]
303pub struct Author(Vec<EcoString>);
304
305cast! {
306    Author,
307    self => self.0.into_value(),
308    v: EcoString => Self(vec![v]),
309    v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
310}
311
312/// A list of keywords.
313#[derive(Debug, Default, Clone, PartialEq, Hash)]
314pub struct Keywords(Vec<EcoString>);
315
316cast! {
317    Keywords,
318    self => self.0.into_value(),
319    v: EcoString => Self(vec![v]),
320    v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
321}
322
323/// A document resulting from compilation.
324pub trait Document {
325    /// Get the document's metadata.
326    fn info(&self) -> &DocumentInfo;
327}
328
329/// Details about the document.
330#[derive(Debug, Default, Clone, PartialEq, Hash)]
331pub struct DocumentInfo {
332    /// The document's title.
333    pub title: Option<EcoString>,
334    /// The document's author(s).
335    pub author: Vec<EcoString>,
336    /// The document's description.
337    pub description: Option<EcoString>,
338    /// The document's keywords.
339    pub keywords: Vec<EcoString>,
340    /// The document's creation date.
341    pub date: Smart<Option<Datetime>>,
342    /// The document's language, set from the first top-level set rule, e.g.
343    ///
344    /// ```typc
345    /// set text(lang: "...", region: "...")
346    /// ```
347    pub locale: Smart<Locale>,
348}
349
350impl DocumentInfo {
351    /// Populate this document info with details from the given styles.
352    ///
353    /// Document set rules are a bit special, so we need to do this manually.
354    pub fn populate(&mut self, styles: StyleChain) {
355        if styles.has(DocumentElem::title) {
356            self.title = styles
357                .get_ref(DocumentElem::title)
358                .as_ref()
359                .map(|content| content.plain_text());
360        }
361        if styles.has(DocumentElem::author) {
362            self.author = styles.get_cloned(DocumentElem::author).0;
363        }
364        if styles.has(DocumentElem::description) {
365            self.description = styles
366                .get_ref(DocumentElem::description)
367                .as_ref()
368                .map(|content| content.plain_text());
369        }
370        if styles.has(DocumentElem::keywords) {
371            self.keywords = styles.get_cloned(DocumentElem::keywords).0;
372        }
373        if styles.has(DocumentElem::date) {
374            self.date = styles.get(DocumentElem::date);
375        }
376    }
377
378    /// Populate this document info with locale details from the given styles.
379    pub fn populate_locale(&mut self, styles: StyleChain) {
380        if self.locale.is_custom() {
381            return;
382        }
383
384        let mut locale: Option<Locale> = None;
385        if styles.has(TextElem::lang) {
386            locale.get_or_insert_default().lang = styles.get(TextElem::lang);
387        }
388        if styles.has(TextElem::region) {
389            locale.get_or_insert_default().region = styles.get(TextElem::region);
390        }
391        self.locale = Smart::from(locale);
392    }
393}