diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-03-07 15:17:13 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-03-07 15:17:13 +0100 |
| commit | 25b5bd117529cd04bb789e1988eb3a3db8025a0e (patch) | |
| tree | 2fbb4650903123da047a1f1f11a0abda95286e12 /library/src/meta | |
| parent | 6ab7760822ccd24b4ef126d4737d41f1be15fe19 (diff) | |
Fully untyped model
Diffstat (limited to 'library/src/meta')
| -rw-r--r-- | library/src/meta/document.rs | 65 | ||||
| -rw-r--r-- | library/src/meta/heading.rs | 82 | ||||
| -rw-r--r-- | library/src/meta/link.rs | 62 | ||||
| -rw-r--r-- | library/src/meta/numbering.rs | 66 | ||||
| -rw-r--r-- | library/src/meta/outline.rs | 89 | ||||
| -rw-r--r-- | library/src/meta/reference.rs | 34 |
6 files changed, 199 insertions, 199 deletions
diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs index 1d349b89..0d03b496 100644 --- a/library/src/meta/document.rs +++ b/library/src/meta/document.rs @@ -1,7 +1,8 @@ +use typst::model::StyledNode; + use crate::layout::{LayoutRoot, PageNode}; use crate::prelude::*; -/// # Document /// The root element of a document and its metadata. /// /// All documents are automatically wrapped in a `document` element. The main @@ -11,33 +12,48 @@ use crate::prelude::*; /// The metadata set with this function is not rendered within the document. /// Instead, it is embedded in the compiled PDF file. /// -/// ## Category -/// meta -#[func] -#[capable(LayoutRoot)] -#[derive(Hash)] -pub struct DocumentNode(pub StyleVec<PageNode>); +/// Display: Document +/// Category: meta +#[node(LayoutRoot)] +pub struct DocumentNode { + /// The page runs. + #[variadic] + pub children: Vec<Content>, -#[node] -impl DocumentNode { /// The document's title. This is often rendered as the title of the /// PDF viewer window. - #[property(referenced)] - pub const TITLE: Option<EcoString> = None; + #[settable] + #[default] + pub title: Option<EcoString>, /// The document's authors. - #[property(referenced)] - pub const AUTHOR: Author = Author(vec![]); + #[settable] + #[default] + pub author: Author, } impl LayoutRoot for DocumentNode { /// Layout the document into a sequence of frames, one per page. fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { let mut pages = vec![]; - for (page, map) in self.0.iter() { - let number = 1 + pages.len(); - let fragment = page.layout(vt, number, styles.chain(map))?; - pages.extend(fragment); + + for mut child in self.children() { + let map; + let outer = styles; + let mut styles = outer; + if let Some(node) = child.to::<StyledNode>() { + map = node.map(); + styles = outer.chain(&map); + child = node.sub(); + } + + if let Some(page) = child.to::<PageNode>() { + let number = 1 + pages.len(); + let fragment = page.layout(vt, number, styles)?; + pages.extend(fragment); + } else if let Some(span) = child.span() { + bail!(span, "unexpected document child"); + } } Ok(Document { @@ -48,19 +64,16 @@ impl LayoutRoot for DocumentNode { } } -impl Debug for DocumentNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Document ")?; - self.0.fmt(f) - } -} - /// A list of authors. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Default, Clone, Hash)] pub struct Author(Vec<EcoString>); -castable! { +cast_from_value! { Author, v: EcoString => Self(vec![v]), v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), } + +cast_to_value! { + v: Author => v.0.into() +} diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index f108cad1..38890885 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -5,7 +5,6 @@ use crate::layout::{BlockNode, HNode, VNode}; use crate::prelude::*; use crate::text::{TextNode, TextSize}; -/// # Heading /// A section heading. /// /// With headings, you can structure your document into sections. Each heading @@ -39,28 +38,20 @@ use crate::text::{TextNode, TextSize}; /// one or multiple equals signs, followed by a space. The number of equals /// signs determines the heading's logical nesting depth. /// -/// ## Parameters -/// - title: `Content` (positional, required) -/// The heading's title. -/// -/// - level: `NonZeroUsize` (named) -/// The logical nesting depth of the heading, starting from one. -/// -/// ## Category -/// meta -#[func] -#[capable(Prepare, Show, Finalize)] -#[derive(Debug, Hash)] +/// Display: Heading +/// Category: meta +#[node(Prepare, Show, Finalize)] pub struct HeadingNode { - /// The logical nesting depth of the section, starting from one. In the - /// default style, this controls the text size of the heading. - pub level: NonZeroUsize, - /// The heading's contents. + /// The heading's title. + #[positional] + #[required] pub title: Content, -} -#[node] -impl HeadingNode { + /// The logical nesting depth of the heading, starting from one. + #[named] + #[default(NonZeroUsize::new(1).unwrap())] + pub level: NonZeroUsize, + /// How to number the heading. Accepts a /// [numbering pattern or function]($func/numbering). /// @@ -71,8 +62,9 @@ impl HeadingNode { /// == A subsection /// === A sub-subsection /// ``` - #[property(referenced)] - pub const NUMBERING: Option<Numbering> = None; + #[settable] + #[default] + pub numbering: Option<Numbering>, /// Whether the heading should appear in the outline. /// @@ -86,23 +78,9 @@ impl HeadingNode { /// This heading does not appear /// in the outline. /// ``` - pub const OUTLINED: bool = true; - - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self { - title: args.expect("title")?, - level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()), - } - .pack()) - } - - fn field(&self, name: &str) -> Option<Value> { - match name { - "level" => Some(Value::Int(self.level.get() as i64)), - "title" => Some(Value::Content(self.title.clone())), - _ => None, - } - } + #[settable] + #[default(true)] + pub outlined: bool, } impl Prepare for HeadingNode { @@ -121,7 +99,7 @@ impl Prepare for HeadingNode { } let numbers = node.field("numbers").unwrap(); - if numbers != Value::None { + if *numbers != Value::None { let heading = node.to::<Self>().unwrap(); counter.advance(heading); } @@ -136,38 +114,34 @@ impl Prepare for HeadingNode { this.push_field("numbers", numbers); let meta = Meta::Node(my_id, this.clone()); - Ok(this.styled(Meta::DATA, vec![meta])) + Ok(this.styled(MetaNode::DATA, vec![meta])) } } impl Show for HeadingNode { fn show(&self, _: &mut Vt, this: &Content, _: StyleChain) -> SourceResult<Content> { - let mut realized = self.title.clone(); + let mut realized = self.title(); let numbers = this.field("numbers").unwrap(); - if numbers != Value::None { - realized = numbers.display() - + HNode { amount: Em::new(0.3).into(), weak: true }.pack() + if *numbers != Value::None { + realized = numbers.clone().display() + + HNode::new(Em::new(0.3).into()).with_weak(true).pack() + realized; } - Ok(BlockNode { - body: realized, - width: Smart::Auto, - height: Smart::Auto, - } - .pack()) + Ok(BlockNode::new().with_body(realized).pack()) } } impl Finalize for HeadingNode { fn finalize(&self, realized: Content) -> Content { - let scale = match self.level.get() { + let level = self.level().get(); + let scale = match level { 1 => 1.4, 2 => 1.2, _ => 1.0, }; let size = Em::new(scale); - let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale; + let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale; let below = Em::new(0.75) / scale; let mut map = StyleMap::new(); @@ -191,7 +165,7 @@ impl HeadingCounter { /// Advance the counter and return the numbers for the given heading. pub fn advance(&mut self, heading: &HeadingNode) -> &[NonZeroUsize] { - let level = heading.level.get(); + let level = heading.level().get(); if self.0.len() >= level { self.0[level - 1] = self.0[level - 1].saturating_add(1); diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs index 61e75b8a..63a8ec98 100644 --- a/library/src/meta/link.rs +++ b/library/src/meta/link.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use crate::text::{Hyphenate, TextNode}; -/// # Link /// Link to a URL or another location in the document. /// /// The link function makes its positional `body` argument clickable and links @@ -50,67 +49,62 @@ use crate::text::{Hyphenate, TextNode}; /// The content that should become a link. If `dest` is an URL string, the /// parameter can be omitted. In this case, the URL will be shown as the link. /// -/// ## Category -/// meta -#[func] -#[capable(Show, Finalize)] -#[derive(Debug, Hash)] +/// Display: Link +/// Category: meta +#[node(Construct, Show, Finalize)] pub struct LinkNode { /// The destination the link points to. + #[positional] + #[required] pub dest: Destination, + /// How the link is represented. + #[positional] + #[default] pub body: Content, } impl LinkNode { /// Create a link node from a URL with its bare text. pub fn from_url(url: EcoString) -> Self { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); - } - let shorter = text.len() < url.len(); - let body = TextNode::packed(if shorter { text.into() } else { url.clone() }); - Self { dest: Destination::Url(url), body } + let body = body_from_url(&url); + Self::new(Destination::Url(url)).with_body(body) } } -#[node] -impl LinkNode { +impl Construct for LinkNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { let dest = args.expect::<Destination>("destination")?; - Ok(match dest { + let body = match &dest { Destination::Url(url) => match args.eat()? { - Some(body) => Self { dest: Destination::Url(url), body }, - None => Self::from_url(url), + Some(body) => body, + None => body_from_url(url), }, - Destination::Internal(_) => Self { dest, body: args.expect("body")? }, - } - .pack()) - } - - fn field(&self, name: &str) -> Option<Value> { - match name { - "dest" => Some(match &self.dest { - Destination::Url(url) => Value::Str(url.clone().into()), - Destination::Internal(loc) => Value::Dict(loc.encode()), - }), - "body" => Some(Value::Content(self.body.clone())), - _ => None, - } + Destination::Internal(_) => args.expect("body")?, + }; + Ok(Self::new(dest).with_body(body).pack()) } } impl Show for LinkNode { fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { - Ok(self.body.clone()) + Ok(self.body()) } } impl Finalize for LinkNode { fn finalize(&self, realized: Content) -> Content { realized - .styled(Meta::DATA, vec![Meta::Link(self.dest.clone())]) + .styled(MetaNode::DATA, vec![Meta::Link(self.dest())]) .styled(TextNode::HYPHENATE, Hyphenate(Smart::Custom(false))) } } + +fn body_from_url(url: &EcoString) -> Content { + let mut text = url.as_str(); + for prefix in ["mailto:", "tel:"] { + text = text.trim_start_matches(prefix); + } + let shorter = text.len() < url.len(); + TextNode::packed(if shorter { text.into() } else { url.clone() }) +} diff --git a/library/src/meta/numbering.rs b/library/src/meta/numbering.rs index 5b7cd92b..d3e1ee4d 100644 --- a/library/src/meta/numbering.rs +++ b/library/src/meta/numbering.rs @@ -3,7 +3,6 @@ use std::str::FromStr; use crate::prelude::*; use crate::text::Case; -/// # Numbering /// Apply a numbering to a sequence of numbers. /// /// A numbering defines how a sequence of numbers should be displayed as @@ -61,8 +60,8 @@ use crate::text::Case; /// /// - returns: any /// -/// ## Category -/// meta +/// Display: Numbering +/// Category: meta #[func] pub fn numbering(vm: &Vm, args: &mut Args) -> SourceResult<Value> { let numbering = args.expect::<Numbering>("pattern or function")?; @@ -99,12 +98,19 @@ impl Numbering { } } -castable! { +cast_from_value! { Numbering, - v: Str => Self::Pattern(v.parse()?), + v: NumberingPattern => Self::Pattern(v), v: Func => Self::Func(v), } +cast_to_value! { + v: Numbering => match v { + Numbering::Pattern(pattern) => pattern.into(), + Numbering::Func(func) => func.into(), + } +} + /// How to turn a number into text. /// /// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`, `I` @@ -173,12 +179,8 @@ impl FromStr for NumberingPattern { let mut handled = 0; for (i, c) in pattern.char_indices() { - let kind = match c.to_ascii_lowercase() { - '1' => NumberingKind::Arabic, - 'a' => NumberingKind::Letter, - 'i' => NumberingKind::Roman, - '*' => NumberingKind::Symbol, - _ => continue, + let Some(kind) = NumberingKind::from_char(c.to_ascii_lowercase()) else { + continue; }; let prefix = pattern[handled..i].into(); @@ -196,6 +198,27 @@ impl FromStr for NumberingPattern { } } +cast_from_value! { + NumberingPattern, + v: Str => v.parse()?, +} + +cast_to_value! { + v: NumberingPattern => { + let mut pat = EcoString::new(); + for (prefix, kind, case) in &v.pieces { + pat.push_str(prefix); + let mut c = kind.to_char(); + if *case == Case::Upper { + c = c.to_ascii_uppercase(); + } + pat.push(c); + } + pat.push_str(&v.suffix); + pat.into() + } +} + /// Different kinds of numberings. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] enum NumberingKind { @@ -206,6 +229,27 @@ enum NumberingKind { } impl NumberingKind { + /// Create a numbering kind from a lowercase character. + pub fn from_char(c: char) -> Option<Self> { + Some(match c { + '1' => NumberingKind::Arabic, + 'a' => NumberingKind::Letter, + 'i' => NumberingKind::Roman, + '*' => NumberingKind::Symbol, + _ => return None, + }) + } + + /// The lowercase character for this numbering kind. + pub fn to_char(self) -> char { + match self { + Self::Arabic => '1', + Self::Letter => 'a', + Self::Roman => 'i', + Self::Symbol => '*', + } + } + /// Apply the numbering to the given number. pub fn apply(self, n: NonZeroUsize, case: Case) -> EcoString { let mut n = n.get(); diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index d9eea0a9..7bc08bf9 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -1,11 +1,8 @@ use super::HeadingNode; -use crate::layout::{ - BoxNode, HNode, HideNode, ParbreakNode, RepeatNode, Sizing, Spacing, -}; +use crate::layout::{BoxNode, HNode, HideNode, ParbreakNode, RepeatNode}; use crate::prelude::*; use crate::text::{LinebreakNode, SpaceNode, TextNode}; -/// # Outline /// A section outline / table of contents. /// /// This function generates a list of all headings in the document, up to a @@ -23,27 +20,25 @@ use crate::text::{LinebreakNode, SpaceNode, TextNode}; /// #lorem(10) /// ``` /// -/// ## Category -/// meta -#[func] -#[capable(Prepare, Show)] -#[derive(Debug, Hash)] -pub struct OutlineNode; - -#[node] -impl OutlineNode { +/// Display: Outline +/// Category: meta +#[node(Prepare, Show)] +pub struct OutlineNode { /// The title of the outline. /// /// - When set to `{auto}`, an appropriate title for the [text /// language]($func/text.lang) will be used. This is the default. /// - When set to `{none}`, the outline will not have a title. /// - A custom title can be set by passing content. - #[property(referenced)] - pub const TITLE: Option<Smart<Content>> = Some(Smart::Auto); + #[settable] + #[default(Some(Smart::Auto))] + pub title: Option<Smart<Content>>, /// The maximum depth up to which headings are included in the outline. When /// this argument is `{none}`, all headings are included. - pub const DEPTH: Option<NonZeroUsize> = None; + #[settable] + #[default] + pub depth: Option<NonZeroUsize>, /// Whether to indent the subheadings to align the start of their numbering /// with the title of their parents. This will only have an effect if a @@ -62,7 +57,9 @@ impl OutlineNode { /// == Products /// #lorem(10) /// ``` - pub const INDENT: bool = false; + #[settable] + #[default(false)] + pub indent: bool, /// Content to fill the space between the title and the page number. Can be /// set to `none` to disable filling. The default is `{repeat[.]}`. @@ -72,12 +69,9 @@ impl OutlineNode { /// /// = A New Beginning /// ``` - #[property(referenced)] - pub const FILL: Option<Content> = Some(RepeatNode(TextNode::packed(".")).pack()); - - fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> { - Ok(Self.pack()) - } + #[settable] + #[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))] + pub fill: Option<Content>, } impl Prepare for OutlineNode { @@ -91,7 +85,7 @@ impl Prepare for OutlineNode { .locate(Selector::node::<HeadingNode>()) .into_iter() .map(|(_, node)| node) - .filter(|node| node.field("outlined").unwrap() == Value::Bool(true)) + .filter(|node| *node.field("outlined").unwrap() == Value::Bool(true)) .map(|node| Value::Content(node.clone())) .collect(); @@ -107,7 +101,7 @@ impl Show for OutlineNode { _: &Content, styles: StyleChain, ) -> SourceResult<Content> { - let mut seq = vec![ParbreakNode.pack()]; + let mut seq = vec![ParbreakNode::new().pack()]; if let Some(title) = styles.get(Self::TITLE) { let body = title.clone().unwrap_or_else(|| { TextNode::packed(match styles.get(TextNode::LANG) { @@ -117,7 +111,7 @@ impl Show for OutlineNode { }); seq.push( - HeadingNode { title: body, level: NonZeroUsize::new(1).unwrap() } + HeadingNode::new(body) .pack() .styled(HeadingNode::NUMBERING, None) .styled(HeadingNode::OUTLINED, false), @@ -129,26 +123,26 @@ impl Show for OutlineNode { let mut ancestors: Vec<&Content> = vec![]; for (_, node) in vt.locate(Selector::node::<HeadingNode>()) { - if node.field("outlined").unwrap() != Value::Bool(true) { + if *node.field("outlined").unwrap() != Value::Bool(true) { continue; } let heading = node.to::<HeadingNode>().unwrap(); if let Some(depth) = depth { - if depth < heading.level { + if depth < heading.level() { continue; } } while ancestors.last().map_or(false, |last| { - last.to::<HeadingNode>().unwrap().level >= heading.level + last.to::<HeadingNode>().unwrap().level() >= heading.level() }) { ancestors.pop(); } // Adjust the link destination a bit to the topleft so that the // heading is fully visible. - let mut loc = node.field("loc").unwrap().cast::<Location>().unwrap(); + let mut loc = node.field("loc").unwrap().clone().cast::<Location>().unwrap(); loc.pos -= Point::splat(Abs::pt(10.0)); // Add hidden ancestors numberings to realize the indent. @@ -156,21 +150,21 @@ impl Show for OutlineNode { let hidden: Vec<_> = ancestors .iter() .map(|node| node.field("numbers").unwrap()) - .filter(|numbers| *numbers != Value::None) - .map(|numbers| numbers.display() + SpaceNode.pack()) + .filter(|&numbers| *numbers != Value::None) + .map(|numbers| numbers.clone().display() + SpaceNode::new().pack()) .collect(); if !hidden.is_empty() { - seq.push(HideNode(Content::sequence(hidden)).pack()); - seq.push(SpaceNode.pack()); + seq.push(HideNode::new(Content::sequence(hidden)).pack()); + seq.push(SpaceNode::new().pack()); } } // Format the numbering. - let mut start = heading.title.clone(); + let mut start = heading.title(); let numbers = node.field("numbers").unwrap(); - if numbers != Value::None { - start = numbers.display() + SpaceNode.pack() + start; + if *numbers != Value::None { + start = numbers.clone().display() + SpaceNode::new().pack() + start; }; // Add the numbering and section name. @@ -178,30 +172,27 @@ impl Show for OutlineNode { // Add filler symbols between the section name and page number. if let Some(filler) = styles.get(Self::FILL) { - seq.push(SpaceNode.pack()); + seq.push(SpaceNode::new().pack()); seq.push( - BoxNode { - body: filler.clone(), - width: Sizing::Fr(Fr::one()), - height: Smart::Auto, - } - .pack(), + BoxNode::new() + .with_body(filler.clone()) + .with_width(Fr::one().into()) + .pack(), ); - seq.push(SpaceNode.pack()); + seq.push(SpaceNode::new().pack()); } else { - let amount = Spacing::Fr(Fr::one()); - seq.push(HNode { amount, weak: false }.pack()); + seq.push(HNode::new(Fr::one().into()).pack()); } // Add the page number and linebreak. let end = TextNode::packed(eco_format!("{}", loc.page)); seq.push(end.linked(Destination::Internal(loc))); - seq.push(LinebreakNode { justify: false }.pack()); + seq.push(LinebreakNode::new().pack()); ancestors.push(node); } - seq.push(ParbreakNode.pack()); + seq.push(ParbreakNode::new().pack()); Ok(Content::sequence(seq)) } diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index e64751f7..55051b5e 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use crate::text::TextNode; -/// # Reference /// A reference to a label. /// /// *Note: This function is currently unimplemented.* @@ -16,33 +15,18 @@ use crate::text::TextNode; /// created by typing an `@` followed by the name of the label (e.g. `[= /// Introduction <intro>]` can be referenced by typing `[@intro]`). /// -/// ## Parameters -/// - target: `Label` (positional, required) -/// The label that should be referenced. -/// -/// ## Category -/// meta -#[func] -#[capable(Show)] -#[derive(Debug, Hash)] -pub struct RefNode(pub EcoString); - -#[node] -impl RefNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self(args.expect("target")?).pack()) - } - - fn field(&self, name: &str) -> Option<Value> { - match name { - "target" => Some(Value::Str(self.0.clone().into())), - _ => None, - } - } +/// Display: Reference +/// Category: meta +#[node(Show)] +pub struct RefNode { + /// The label that should be referenced. + #[positional] + #[required] + pub target: EcoString, } impl Show for RefNode { fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { - Ok(TextNode::packed(eco_format!("@{}", self.0))) + Ok(TextNode::packed(eco_format!("@{}", self.target()))) } } |
