From 6bafc6391061d4b589dea835705a08b25a4df9f8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 26 Nov 2022 23:42:40 +0100 Subject: [PATCH] Document metadata --- cli/src/main.rs | 4 +-- library/src/layout/mod.rs | 37 ++++++++++++++++------- library/src/lib.rs | 1 + library/src/prelude.rs | 2 +- library/src/structure/doc.rs | 32 -------------------- library/src/structure/document.rs | 47 ++++++++++++++++++++++++++++++ library/src/structure/mod.rs | 4 +-- src/{frame.rs => doc.rs} | 20 ++++++++++++- src/export/pdf/mod.rs | 30 ++++++++++++------- src/export/pdf/page.rs | 8 ++--- src/export/render.rs | 2 +- src/lib.rs | 22 +++++--------- src/model/cast.rs | 2 +- src/model/eval.rs | 6 +--- src/model/library.rs | 4 +-- src/model/styles.rs | 41 +++++++++++++++++--------- src/model/typeset.rs | 10 ++----- tests/ref/layout/page-box.png | Bin 756 -> 0 bytes tests/src/benches.rs | 4 +-- tests/src/tests.rs | 13 +++++---- tests/typ/layout/page-box.typ | 14 --------- tests/typ/style/document.typ | 30 +++++++++++++++++++ 22 files changed, 204 insertions(+), 129 deletions(-) delete mode 100644 library/src/structure/doc.rs create mode 100644 library/src/structure/document.rs rename src/{frame.rs => doc.rs} (97%) delete mode 100644 tests/ref/layout/page-box.png delete mode 100644 tests/typ/layout/page-box.typ create mode 100644 tests/typ/style/document.typ diff --git a/cli/src/main.rs b/cli/src/main.rs index 2825f74ca..5463c3ecf 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -236,8 +236,8 @@ fn compile_once(world: &mut SystemWorld, command: &CompileCommand) -> StrResult< match typst::compile(world, source) { // Export the PDF. - Ok(frames) => { - let buffer = typst::export::pdf(&frames); + Ok(document) => { + let buffer = typst::export::pdf(&document); fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?; status(command, Status::Success).unwrap(); } diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index 100e0611f..e63a072e6 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -29,7 +29,7 @@ use std::mem; use comemo::Tracked; use typed_arena::Arena; use typst::diag::SourceResult; -use typst::frame::Frame; +use typst::doc::Frame; use typst::geom::*; use typst::model::{ applicable, capability, realize, Content, Node, SequenceNode, Style, StyleChain, @@ -40,7 +40,7 @@ use typst::World; use crate::prelude::*; use crate::shared::BehavedBuilder; use crate::structure::{ - DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST, + DescNode, DocumentNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST, }; use crate::text::{ LinebreakNode, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, @@ -54,7 +54,7 @@ pub trait LayoutRoot { &self, world: Tracked, styles: StyleChain, - ) -> SourceResult>; + ) -> SourceResult; } impl LayoutRoot for Content { @@ -63,7 +63,7 @@ impl LayoutRoot for Content { &self, world: Tracked, styles: StyleChain, - ) -> SourceResult> { + ) -> SourceResult { let scratch = Scratch::default(); let (realized, styles) = realize_root(world, &scratch, self, styles)?; realized.with::().unwrap().layout_root(world, styles) @@ -245,7 +245,7 @@ fn realize_root<'a>( builder.accept(content, styles)?; builder.interrupt_page(Some(styles))?; let (pages, shared) = builder.doc.unwrap().pages.finish(); - Ok((DocNode(pages).pack(), shared)) + Ok((DocumentNode(pages).pack(), shared)) } /// Realize into a node that is capable of block-level layout. @@ -357,6 +357,10 @@ impl<'a> Builder<'a> { } } + if let Some(span) = content.span() { + bail!(span, "not allowed here"); + } + Ok(()) } @@ -378,13 +382,26 @@ impl<'a> Builder<'a> { map: &StyleMap, styles: Option>, ) -> SourceResult<()> { - if map.interrupts::() { + if let Some(Some(span)) = map.interruption::() { + if self.doc.is_none() { + bail!(span, "not allowed here"); + } + if !self.flow.0.is_empty() + || !self.par.0.is_empty() + || !self.list.items.is_empty() + { + bail!(span, "must appear before any content"); + } + } else if let Some(Some(span)) = map.interruption::() { + if self.doc.is_none() { + bail!(span, "not allowed here"); + } self.interrupt_page(styles)?; - } else if map.interrupts::() { + } else if map.interruption::().is_some() { self.interrupt_par()?; - } else if map.interrupts::() - || map.interrupts::() - || map.interrupts::() + } else if map.interruption::().is_some() + || map.interruption::().is_some() + || map.interruption::().is_some() { self.interrupt_list()?; } diff --git a/library/src/lib.rs b/library/src/lib.rs index 6107cf42d..cf8cb4903 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -44,6 +44,7 @@ fn scope() -> Scope { std.def_fn("smallcaps", text::smallcaps); // Structure. + std.def_node::("document"); std.def_node::("ref"); std.def_node::("heading"); std.def_node::("list"); diff --git a/library/src/prelude.rs b/library/src/prelude.rs index 6379e5792..bc0ec31d4 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -10,7 +10,7 @@ pub use comemo::Tracked; #[doc(no_inline)] pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult}; #[doc(no_inline)] -pub use typst::frame::*; +pub use typst::doc::*; #[doc(no_inline)] pub use typst::geom::*; #[doc(no_inline)] diff --git a/library/src/structure/doc.rs b/library/src/structure/doc.rs deleted file mode 100644 index e471a8523..000000000 --- a/library/src/structure/doc.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::layout::{LayoutRoot, PageNode}; -use crate::prelude::*; - -/// A sequence of page runs. -#[derive(Hash)] -pub struct DocNode(pub StyleVec); - -#[node(LayoutRoot)] -impl DocNode {} - -impl LayoutRoot for DocNode { - /// Layout the document into a sequence of frames, one per page. - fn layout_root( - &self, - world: Tracked, - styles: StyleChain, - ) -> SourceResult> { - let mut frames = vec![]; - for (page, map) in self.0.iter() { - let number = 1 + frames.len(); - frames.extend(page.layout(world, number, styles.chain(map))?); - } - Ok(frames) - } -} - -impl Debug for DocNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Doc ")?; - self.0.fmt(f) - } -} diff --git a/library/src/structure/document.rs b/library/src/structure/document.rs new file mode 100644 index 000000000..2e5761e04 --- /dev/null +++ b/library/src/structure/document.rs @@ -0,0 +1,47 @@ +use crate::layout::{LayoutRoot, PageNode}; +use crate::prelude::*; + +/// The root node of the model. +#[derive(Hash)] +pub struct DocumentNode(pub StyleVec); + +#[node(LayoutRoot)] +impl DocumentNode { + /// The document's title. + #[property(referenced)] + pub const TITLE: Option = None; + + /// The document's author. + #[property(referenced)] + pub const AUTHOR: Option = None; +} + +impl LayoutRoot for DocumentNode { + /// Layout the document into a sequence of frames, one per page. + fn layout_root( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult { + let mut pages = vec![]; + for (page, map) in self.0.iter() { + let number = 1 + pages.len(); + pages.extend(page.layout(world, number, styles.chain(map))?); + } + + Ok(Document { + metadata: Metadata { + title: styles.get(Self::TITLE).clone(), + author: styles.get(Self::AUTHOR).clone(), + }, + pages, + }) + } +} + +impl Debug for DocumentNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Document ")?; + self.0.fmt(f) + } +} diff --git a/library/src/structure/mod.rs b/library/src/structure/mod.rs index 8e13f76aa..a1c27eed1 100644 --- a/library/src/structure/mod.rs +++ b/library/src/structure/mod.rs @@ -1,12 +1,12 @@ //! Document structuring. -mod doc; +mod document; mod heading; mod list; mod reference; mod table; -pub use self::doc::*; +pub use self::document::*; pub use self::heading::*; pub use self::list::*; pub use self::reference::*; diff --git a/src/frame.rs b/src/doc.rs similarity index 97% rename from src/frame.rs rename to src/doc.rs index f7d05a1d6..f65d5ae6a 100644 --- a/src/frame.rs +++ b/src/doc.rs @@ -1,4 +1,4 @@ -//! Finished layouts. +//! Finished documents. use std::fmt::{self, Debug, Formatter, Write}; use std::num::NonZeroUsize; @@ -13,6 +13,24 @@ use crate::image::Image; use crate::model::{dict, Dict, Value}; use crate::util::EcoString; +/// A finished document with metadata and page frames. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Document { + /// The document's metadata. + pub metadata: Metadata, + /// The page frames. + pub pages: Vec, +} + +/// Document metadata. +#[derive(Debug, Default, Clone, Eq, PartialEq)] +pub struct Metadata { + /// The document's title. + pub title: Option, + /// The document's author. + pub author: Option, +} + /// A finished layout with elements at fixed positions. #[derive(Default, Clone, Eq, PartialEq)] pub struct Frame { diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs index 2547ddbf4..7a530f041 100644 --- a/src/export/pdf/mod.rs +++ b/src/export/pdf/mod.rs @@ -14,21 +14,21 @@ use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr}; use self::outline::{Heading, HeadingNode}; use self::page::Page; +use crate::doc::{Document, Lang, Metadata}; use crate::font::Font; -use crate::frame::{Frame, Lang}; use crate::geom::{Abs, Dir, Em}; use crate::image::Image; -/// Export a collection of frames into a PDF file. +/// Export a document into a PDF file. /// /// This creates one page per frame. In addition to the frames, you need to pass /// in the context used during compilation so that fonts and images can be /// included in the PDF. /// /// Returns the raw bytes making up the PDF file. -pub fn pdf(frames: &[Frame]) -> Vec { - let mut ctx = PdfContext::new(); - page::construct_pages(&mut ctx, frames); +pub fn pdf(document: &Document) -> Vec { + let mut ctx = PdfContext::new(&document.metadata); + page::construct_pages(&mut ctx, &document.pages); font::write_fonts(&mut ctx); image::write_images(&mut ctx); page::write_page_tree(&mut ctx); @@ -41,7 +41,8 @@ const SRGB: Name<'static> = Name(b"srgb"); const D65_GRAY: Name<'static> = Name(b"d65gray"); /// Context for exporting a whole PDF document. -pub struct PdfContext { +pub struct PdfContext<'a> { + metadata: &'a Metadata, writer: PdfWriter, pages: Vec, page_heights: Vec, @@ -57,11 +58,12 @@ pub struct PdfContext { heading_tree: Vec, } -impl PdfContext { - fn new() -> Self { +impl<'a> PdfContext<'a> { + fn new(metadata: &'a Metadata) -> Self { let mut alloc = Ref::new(1); let page_tree_ref = alloc.bump(); Self { + metadata, writer: PdfWriter::new(), pages: vec![], page_heights: vec![], @@ -117,7 +119,15 @@ fn write_catalog(ctx: &mut PdfContext) { }; // Write the document information. - ctx.writer.document_info(ctx.alloc.bump()).creator(TextStr("Typst")); + let mut info = ctx.writer.document_info(ctx.alloc.bump()); + if let Some(title) = &ctx.metadata.title { + info.title(TextStr(title)); + } + if let Some(author) = &ctx.metadata.author { + info.author(TextStr(author)); + } + info.creator(TextStr("Typst")); + info.finish(); // Write the document catalog. let mut catalog = ctx.writer.catalog(ctx.alloc.bump()); @@ -131,8 +141,6 @@ fn write_catalog(ctx: &mut PdfContext) { if let Some(lang) = lang { catalog.lang(TextStr(lang.as_str())); } - - catalog.finish(); } /// Compress data with the DEFLATE algorithm. diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index 3167989c4..7c4794253 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -5,8 +5,8 @@ use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; use super::{ deflate, AbsExt, EmExt, Heading, HeadingNode, PdfContext, RefExt, D65_GRAY, SRGB, }; +use crate::doc::{Destination, Element, Frame, Group, Role, Text}; use crate::font::Font; -use crate::frame::{Destination, Element, Frame, Group, Role, Text}; use crate::geom::{ self, Abs, Color, Em, Geometry, Numeric, Paint, Point, Ratio, Shape, Size, Stroke, Transform, @@ -155,8 +155,8 @@ pub struct Page { } /// An exporter for the contents of a single PDF page. -struct PageContext<'a> { - parent: &'a mut PdfContext, +struct PageContext<'a, 'b> { + parent: &'a mut PdfContext<'b>, page_ref: Ref, content: Content, state: State, @@ -177,7 +177,7 @@ struct State { stroke_space: Option>, } -impl<'a> PageContext<'a> { +impl PageContext<'_, '_> { fn save_state(&mut self) { self.saves.push(self.state.clone()); self.content.save_state(); diff --git a/src/export/render.rs b/src/export/render.rs index 7cff7ad83..14654b9b8 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -8,7 +8,7 @@ use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; use usvg::FitTo; -use crate::frame::{Element, Frame, Group, Text}; +use crate::doc::{Element, Frame, Group, Text}; use crate::geom::{ self, Abs, Geometry, Paint, PathElement, Shape, Size, Stroke, Transform, }; diff --git a/src/lib.rs b/src/lib.rs index 6e0a68a57..7b903d4eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,9 +11,8 @@ //! in the source file. The nodes of the content tree are well structured and //! order-independent and thus much better suited for further processing than //! the raw markup. -//! - **Typesetting:** Next, the content is [typeset] into a collection of -//! [`Frame`]s (one per page) with elements and fixed positions, ready for -//! exporting. +//! - **Typesetting:** Next, the content is [typeset] into a [document] +//! containing one [frame] per page with elements and fixed positions. //! - **Exporting:** These frames can finally be exported into an output format //! (currently supported are [PDF] and [raster images]). //! @@ -25,6 +24,8 @@ //! [module]: model::Module //! [content]: model::Content //! [typeset]: model::typeset +//! [document]: doc::Document +//! [frame]: doc::Frame //! [PDF]: export::pdf //! [raster images]: export::render @@ -38,9 +39,9 @@ pub mod geom; pub mod diag; #[macro_use] pub mod model; +pub mod doc; pub mod export; pub mod font; -pub mod frame; pub mod image; pub mod syntax; @@ -49,21 +50,14 @@ use std::path::Path; use comemo::{Prehashed, Track}; use crate::diag::{FileResult, SourceResult}; +use crate::doc::Document; use crate::font::{Font, FontBook}; -use crate::frame::Frame; use crate::model::{Library, Route}; use crate::syntax::{Source, SourceId}; use crate::util::Buffer; -/// Compile a source file into a collection of layouted frames. -/// -/// Returns either a vector of frames representing individual pages or -/// diagnostics in the form of a vector of error message with file and span -/// information. -pub fn compile( - world: &(dyn World + 'static), - source: &Source, -) -> SourceResult> { +/// Compile a source file into a fully layouted document. +pub fn compile(world: &(dyn World + 'static), source: &Source) -> SourceResult { // Evaluate the source file into a module. let route = Route::default(); let module = model::eval(world.track(), route.track(), source)?; diff --git a/src/model/cast.rs b/src/model/cast.rs index d0a4650a4..a4a3fe4ec 100644 --- a/src/model/cast.rs +++ b/src/model/cast.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use super::{Content, Regex, Selector, Transform, Value}; use crate::diag::{with_alternative, StrResult}; +use crate::doc::{Destination, Lang, Location, Region}; use crate::font::{FontStretch, FontStyle, FontWeight}; -use crate::frame::{Destination, Lang, Location, Region}; use crate::geom::{ Axes, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Rel, Sides, }; diff --git a/src/model/eval.rs b/src/model/eval.rs index da7036b7b..166dadde8 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -21,10 +21,6 @@ use crate::util::{format_eco, EcoString, PathExt}; use crate::World; /// Evaluate a source file and return the resulting module. -/// -/// Returns either a module containing a scope with top-level bindings and -/// layoutable contents or diagnostics in the form of a vector of error -/// messages with file and span information. #[comemo::memoize] pub fn eval( world: Tracked, @@ -934,7 +930,7 @@ impl Eval for ast::SetRule { let target = self.target(); let target = target.eval(vm)?.cast::().at(target.span())?; let args = self.args().eval(vm)?; - target.set(args) + Ok(target.set(args)?.spanned(self.span())) } } diff --git a/src/model/library.rs b/src/model/library.rs index 2ee09b270..518caca12 100644 --- a/src/model/library.rs +++ b/src/model/library.rs @@ -7,7 +7,7 @@ use once_cell::sync::OnceCell; use super::{Content, NodeId, Scope, StyleChain, StyleMap}; use crate::diag::SourceResult; -use crate::frame::Frame; +use crate::doc::Document; use crate::geom::{Abs, Dir}; use crate::util::{hash128, EcoString}; use crate::World; @@ -31,7 +31,7 @@ pub struct LangItems { world: Tracked, content: &Content, styles: StyleChain, - ) -> SourceResult>, + ) -> SourceResult, /// Access the em size. pub em: fn(StyleChain) -> Abs, /// Access the text direction. diff --git a/src/model/styles.rs b/src/model/styles.rs index f3cfb648c..80ec0d1ec 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -79,9 +79,25 @@ impl StyleMap { self } - /// Whether this map contains styles for the given `node.` - pub fn interrupts(&self) -> bool { - self.0.iter().any(|entry| entry.is_of(NodeId::of::())) + /// Add an origin span to all contained properties. + pub fn spanned(mut self, span: Span) -> Self { + for entry in &mut self.0 { + if let Style::Property(property) = entry { + property.origin = Some(span); + } + } + self + } + + /// Returns `Some(_)` with an optional span if this map contains styles for + /// the given `node`. + pub fn interruption(&self) -> Option> { + let node = NodeId::of::(); + self.0.iter().find_map(|entry| match entry { + Style::Property(property) => property.is_of(node).then(|| property.origin), + Style::Recipe(recipe) => recipe.is_of(node).then(|| Some(recipe.span)), + _ => None, + }) } } @@ -127,15 +143,6 @@ impl Style { _ => None, } } - - /// Whether this entry contains styles for the given `node.` - pub fn is_of(&self, node: NodeId) -> bool { - match self { - Self::Property(property) => property.is_of(node), - Self::Recipe(recipe) => recipe.is_of(node), - _ => false, - } - } } impl Debug for Style { @@ -162,6 +169,8 @@ pub struct Property { scoped: bool, /// The property's value. value: Arc>, + /// The span of the set rule the property stems from. + origin: Option, /// The name of the property. #[cfg(debug_assertions)] name: &'static str, @@ -175,6 +184,7 @@ impl Property { node: K::node(), value: Arc::new(Prehashed::new(value)), scoped: false, + origin: None, #[cfg(debug_assertions)] name: K::NAME, } @@ -330,8 +340,11 @@ impl Recipe { let args = Args::new(self.span, [Value::Content(content.clone())]); let mut result = func.call_detached(world, args); if let Some(span) = content.span() { - let point = || Tracepoint::Show(content.name().into()); - result = result.trace(world, point, span); + // For selector-less show rules, a tracepoint makes no sense. + if self.selector.is_some() { + let point = || Tracepoint::Show(content.name().into()); + result = result.trace(world, point, span); + } } Ok(result?.display()) } diff --git a/src/model/typeset.rs b/src/model/typeset.rs index ad2af3b20..451c6eb0f 100644 --- a/src/model/typeset.rs +++ b/src/model/typeset.rs @@ -2,16 +2,12 @@ use comemo::Tracked; use super::{Content, StyleChain}; use crate::diag::SourceResult; -use crate::frame::Frame; +use crate::doc::Document; use crate::World; -/// Typeset content into a collection of layouted frames. -/// -/// Returns either a vector of frames representing individual pages or -/// diagnostics in the form of a vector of error message with file and span -/// information. +/// Typeset content into a fully layouted document. #[comemo::memoize] -pub fn typeset(world: Tracked, content: &Content) -> SourceResult> { +pub fn typeset(world: Tracked, content: &Content) -> SourceResult { let library = world.library(); let styles = StyleChain::new(&library.styles); (library.items.layout)(world, content, styles) diff --git a/tests/ref/layout/page-box.png b/tests/ref/layout/page-box.png deleted file mode 100644 index c8d0320ca07c8dc9c43eb3834ac7c53c3c231b5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 756 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZyEuRZL*}~dQw$7DyFFbTLn>~)z2om85-4-v zPB?R9n$g2T4+|F=WQa8c$bT-i7)Jt9GBMw zKDMa~mfJ+0bd$Qeuw#kYsrvmAt6vhoqXi`t{7QP1DjXKOWe@T}s}HkMv^ zws6knrqGCY`@DB0$EO(kDHR8&D;%n*n`I^=U+b5;y6T+1q%Hrmx$J7Pl~eyaQNiko|0}=mlf9+2c8d9q z6ssnW$#!=xT-e~go&8+P%h;v&-d?nwY9#Y8{pBa$yoqlaS#~a{+T3hcdb6Nq%DY*S z(O+LPI0f0B$jZqY1z1_DnESV zMfZTh+uP3P1oqWm+^!|N`(ELyioYvna>!m|&Y!-4ZS(A~(`%Pp+Fx8Czu?@utGgDw zY+jjuDCW0`&AtsBKLT0NLL160>UD4df-moP (frames, vec![]), + Ok(document) => (document.pages, vec![]), Err(errors) => (vec![], *errors), }; diff --git a/tests/typ/layout/page-box.typ b/tests/typ/layout/page-box.typ deleted file mode 100644 index ed9d3e14b..000000000 --- a/tests/typ/layout/page-box.typ +++ /dev/null @@ -1,14 +0,0 @@ -// Test that you can't do page related stuff in a container. - ---- -A -#box[ - B - #pagebreak() - #set page("a4") -] -C - -// No consequences from the page("A4") call here. -#pagebreak() -D diff --git a/tests/typ/style/document.typ b/tests/typ/style/document.typ new file mode 100644 index 000000000..1fcb8109e --- /dev/null +++ b/tests/typ/style/document.typ @@ -0,0 +1,30 @@ +// Test document and page-level styles. + +--- +// This is okay. +// Ref: false +#set document(title: "Hello") + +--- +Hello + +// Error: 1-30 must appear before any content +#set document(title: "Hello") + +--- +#box[ + // Error: 3-32 not allowed here + #set document(title: "Hello") +] + +--- +#box[ + // Error: 3-18 not allowed here + #set page("a4") +] + +--- +#box[ + // Error: 3-15 not allowed here + #pagebreak() +]