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}