From 2c6127dea611944abb09a0d38375ad7cf9baced0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 29 Jul 2021 13:21:25 +0200 Subject: [PATCH] Refactor state --- src/exec/context.rs | 18 ++-- src/exec/mod.rs | 18 ++-- src/exec/state.rs | 208 ++++++++++++++++++++++------------------- src/geom/gen.rs | 10 ++ src/geom/spec.rs | 10 ++ src/layout/par.rs | 4 +- src/layout/shaping.rs | 10 +- src/library/layout.rs | 84 +++++++++++------ src/library/text.rs | 96 +++++++++---------- tests/ref/text/par.png | Bin 4337 -> 3632 bytes tests/typ/text/par.typ | 7 +- tests/typeset.rs | 5 +- 12 files changed, 261 insertions(+), 209 deletions(-) diff --git a/src/exec/context.rs b/src/exec/context.rs index 3a3eb7027..4d3516924 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -3,13 +3,13 @@ use std::rc::Rc; use super::{Exec, ExecWithMap, State}; use crate::diag::{Diag, DiagSet, Pass}; -use crate::util::EcoString; use crate::eval::{ExprMap, Template}; use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size}; use crate::layout::{ LayoutNode, LayoutTree, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, }; use crate::syntax::{Span, SyntaxTree}; +use crate::util::EcoString; use crate::Context; /// The context for execution. @@ -76,10 +76,10 @@ impl ExecContext { /// Push text, but in monospace. pub fn push_monospace_text(&mut self, text: impl Into) { - let prev = Rc::clone(&self.state.text); - self.state.text_mut().monospace = true; + let prev = Rc::clone(&self.state.font); + self.state.font_mut().monospace = true; self.push_text(text); - self.state.text = prev; + self.state.font = prev; } /// Push a word space into the active paragraph. @@ -121,7 +121,7 @@ impl ExecContext { /// Apply a forced paragraph break. pub fn parbreak(&mut self) { - let amount = self.state.text.par_spacing(); + let amount = self.state.par_spacing(); self.stack.finish_par(&self.state); self.stack.push_soft(StackChild::Spacing(amount)); } @@ -148,7 +148,7 @@ impl ExecContext { ParChild::Text( text.into(), self.state.aligns.cross, - Rc::clone(&self.state.text), + Rc::clone(&self.state.font), ) } } @@ -187,7 +187,7 @@ struct StackBuilder { impl StackBuilder { fn new(state: &State) -> Self { Self { - dirs: Gen::new(state.dir, Dir::TTB), + dirs: state.dirs, children: vec![], last: Last::None, par: ParBuilder::new(state), @@ -237,8 +237,8 @@ impl ParBuilder { fn new(state: &State) -> Self { Self { aligns: state.aligns, - dir: state.dir, - line_spacing: state.text.line_spacing(), + dir: state.dirs.cross, + line_spacing: state.line_spacing(), children: vec![], last: Last::None, } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index ff4faa227..8bac76e82 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -9,12 +9,12 @@ pub use state::*; use std::fmt::Write; use crate::diag::Pass; -use crate::util::EcoString; use crate::eval::{ExprMap, Template, TemplateFunc, TemplateNode, TemplateTree, Value}; -use crate::geom::{Dir, Gen}; +use crate::geom::Gen; use crate::layout::{LayoutTree, StackChild, StackNode}; use crate::pretty::pretty; use crate::syntax::*; +use crate::util::EcoString; use crate::Context; /// Execute a template to produce a layout tree. @@ -57,8 +57,8 @@ impl ExecWithMap for SyntaxNode { Self::Space => ctx.push_word_space(), Self::Linebreak(_) => ctx.linebreak(), Self::Parbreak(_) => ctx.parbreak(), - Self::Strong(_) => ctx.state.text_mut().strong ^= true, - Self::Emph(_) => ctx.state.text_mut().emph ^= true, + Self::Strong(_) => ctx.state.font_mut().strong ^= true, + Self::Emph(_) => ctx.state.font_mut().emph ^= true, Self::Raw(n) => n.exec(ctx), Self::Heading(n) => n.exec_with_map(ctx, map), Self::List(n) => n.exec_with_map(ctx, map), @@ -87,10 +87,10 @@ impl ExecWithMap for HeadingNode { ctx.parbreak(); let snapshot = ctx.state.clone(); - let text = ctx.state.text_mut(); + let font = ctx.state.font_mut(); let upscale = 1.6 - 0.1 * self.level as f64; - text.size *= upscale; - text.strong = true; + font.size *= upscale; + font.strong = true; self.body.exec_with_map(ctx, map); ctx.state = snapshot; @@ -118,11 +118,11 @@ fn exec_item(ctx: &mut ExecContext, label: EcoString, body: &SyntaxTree, map: &E let label = ctx.exec_stack(|ctx| ctx.push_text(label)); let body = ctx.exec_tree_stack(body, map); let stack = StackNode { - dirs: Gen::new(Dir::TTB, ctx.state.dir), + dirs: Gen::new(ctx.state.dirs.main, ctx.state.dirs.cross), aspect: None, children: vec![ StackChild::Any(label.into(), Gen::default()), - StackChild::Spacing(ctx.state.text.size / 2.0), + StackChild::Spacing(ctx.state.font.size / 2.0), StackChild::Any(body.into(), Gen::default()), ], }; diff --git a/src/exec/state.rs b/src/exec/state.rs index 51bbe395c..ce30e0423 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -12,29 +12,52 @@ use crate::paper::{PaperClass, PAPER_A4}; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct State { /// The direction for text and other inline objects. - pub dir: Dir, + pub dirs: Gen, /// The current alignments of layouts in their parents. pub aligns: Gen, /// The current page settings. - pub page: PageState, - /// The current text settings. - pub text: Rc, + pub page: Rc, + /// The current paragraph settings. + pub par: Rc, + /// The current font settings. + pub font: Rc, } impl State { - /// Access the `text` state mutably. - pub fn text_mut(&mut self) -> &mut TextState { - Rc::make_mut(&mut self.text) + /// Access the `page` state mutably. + pub fn page_mut(&mut self) -> &mut PageState { + Rc::make_mut(&mut self.page) + } + + /// Access the `par` state mutably. + pub fn par_mut(&mut self) -> &mut ParState { + Rc::make_mut(&mut self.par) + } + + /// Access the `font` state mutably. + pub fn font_mut(&mut self) -> &mut FontState { + Rc::make_mut(&mut self.font) + } + + /// The resolved line spacing. + pub fn line_spacing(&self) -> Length { + self.par.line_spacing.resolve(self.font.size) + } + + /// The resolved paragraph spacing. + pub fn par_spacing(&self) -> Length { + self.par.par_spacing.resolve(self.font.size) } } impl Default for State { fn default() -> Self { Self { - dir: Dir::LTR, + dirs: Gen::new(Dir::LTR, Dir::TTB), aligns: Gen::splat(Align::Start), - page: PageState::default(), - text: Rc::new(TextState::default()), + page: Rc::new(PageState::default()), + par: Rc::new(ParState::default()), + font: Rc::new(FontState::default()), } } } @@ -75,15 +98,27 @@ impl Default for PageState { } } -/// Defines text properties. +/// Style paragraph properties. #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct TextState { - /// A list of font families with generic class definitions (the final - /// family list also depends on `monospace`). - pub families: Rc, - /// The selected font variant (the final variant also depends on `strong` - /// and `emph`). - pub variant: FontVariant, +pub struct ParState { + /// The spacing between paragraphs (dependent on scaled font size). + pub par_spacing: Linear, + /// The spacing between lines (dependent on scaled font size). + pub line_spacing: Linear, +} + +impl Default for ParState { + fn default() -> Self { + Self { + par_spacing: Relative::new(1.0).into(), + line_spacing: Relative::new(0.5).into(), + } + } +} + +/// Defines font properties. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FontState { /// Whether the strong toggle is active or inactive. This determines /// whether the next `*` adds or removes font weight. pub strong: bool, @@ -94,19 +129,18 @@ pub struct TextState { pub monospace: bool, /// The font size. pub size: Length, - /// The spacing between words (dependent on scaled font size). - // TODO: Don't ignore this. - pub word_spacing: Linear, - /// The spacing between lines (dependent on scaled font size). - pub line_spacing: Linear, - /// The spacing between paragraphs (dependent on scaled font size). - pub par_spacing: Linear, + /// The selected font variant (the final variant also depends on `strong` + /// and `emph`). + pub variant: FontVariant, /// The top end of the text bounding box. pub top_edge: VerticalFontMetric, /// The bottom end of the text bounding box. pub bottom_edge: VerticalFontMetric, /// Glyph color. pub fill: Paint, + /// A list of font families with generic class definitions (the final + /// family list also depends on `monospace`). + pub families: Rc, /// The specifications for a strikethrough line, if any. pub strikethrough: Option>, /// The specifications for a underline, if any. @@ -115,22 +149,7 @@ pub struct TextState { pub overline: Option>, } -impl TextState { - /// Access the `families` list mutably. - pub fn families_mut(&mut self) -> &mut FamilyList { - Rc::make_mut(&mut self.families) - } - - /// The resolved family iterator. - pub fn families(&self) -> impl Iterator + Clone { - let head = if self.monospace { - self.families.monospace.as_slice() - } else { - &[] - }; - head.iter().map(String::as_str).chain(self.families.iter()) - } - +impl FontState { /// The resolved variant with `strong` and `emph` factored in. pub fn variant(&self) -> FontVariant { let mut variant = self.variant; @@ -150,26 +169,39 @@ impl TextState { variant } - /// The resolved word spacing. - pub fn word_spacing(&self) -> Length { - self.word_spacing.resolve(self.size) + /// The resolved family iterator. + pub fn families(&self) -> impl Iterator + Clone { + let head = if self.monospace { + self.families.monospace.as_slice() + } else { + &[] + }; + + let core = self.families.list.iter().flat_map(move |family: &FontFamily| { + match family { + FontFamily::Named(name) => std::slice::from_ref(name), + FontFamily::Serif => &self.families.serif, + FontFamily::SansSerif => &self.families.sans_serif, + FontFamily::Monospace => &self.families.monospace, + } + }); + + head.iter() + .chain(core) + .chain(self.families.base.iter()) + .map(String::as_str) } - /// The resolved line spacing. - pub fn line_spacing(&self) -> Length { - self.line_spacing.resolve(self.size) - } - - /// The resolved paragraph spacing. - pub fn par_spacing(&self) -> Length { - self.par_spacing.resolve(self.size) + /// Access the `families` state mutably. + pub fn families_mut(&mut self) -> &mut FamilyState { + Rc::make_mut(&mut self.families) } } -impl Default for TextState { +impl Default for FontState { fn default() -> Self { Self { - families: Rc::new(FamilyList::default()), + families: Rc::new(FamilyState::default()), variant: FontVariant { style: FontStyle::Normal, weight: FontWeight::REGULAR, @@ -179,9 +211,6 @@ impl Default for TextState { emph: false, monospace: false, size: Length::pt(11.0), - word_spacing: Relative::new(0.25).into(), - line_spacing: Relative::new(0.5).into(), - par_spacing: Relative::new(1.0).into(), top_edge: VerticalFontMetric::CapHeight, bottom_edge: VerticalFontMetric::Baseline, fill: Paint::Color(Color::Rgba(RgbaColor::BLACK)), @@ -194,63 +223,46 @@ impl Default for TextState { /// Font family definitions. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct FamilyList { +pub struct FamilyState { /// The user-defined list of font families. - pub list: Vec, + pub list: Rc>, /// Definition of serif font families. - pub serif: Vec, + pub serif: Rc>, /// Definition of sans-serif font families. - pub sans_serif: Vec, + pub sans_serif: Rc>, /// Definition of monospace font families used for raw text. - pub monospace: Vec, - /// Base fonts that are tried if the list has no match. - pub base: Vec, + pub monospace: Rc>, + /// Base fonts that are tried as last resort. + pub base: Rc>, } -impl FamilyList { - /// Flat iterator over this map's family names. - pub fn iter(&self) -> impl Iterator + Clone { - self.list - .iter() - .flat_map(move |family: &FontFamily| { - match family { - FontFamily::Named(name) => std::slice::from_ref(name), - FontFamily::Serif => &self.serif, - FontFamily::SansSerif => &self.sans_serif, - FontFamily::Monospace => &self.monospace, - } - }) - .chain(&self.base) - .map(String::as_str) - } -} - -impl Default for FamilyList { +impl Default for FamilyState { fn default() -> Self { Self { - list: vec![FontFamily::Serif], - serif: vec!["eb garamond".into()], - sans_serif: vec!["pt sans".into()], - monospace: vec!["inconsolata".into()], - base: vec!["twitter color emoji".into(), "latin modern math".into()], + list: Rc::new(vec![FontFamily::Serif]), + serif: Rc::new(vec!["eb garamond".into()]), + sans_serif: Rc::new(vec!["pt sans".into()]), + monospace: Rc::new(vec!["inconsolata".into()]), + base: Rc::new(vec![ + "twitter color emoji".into(), + "latin modern math".into(), + ]), } } } -/// Describes a line that is positioned over, under or on top of text. +/// Defines a line that is positioned over, under or on top of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct LineState { - /// Stroke color of the line. - /// - /// Defaults to the text color if `None`. + /// Stroke color of the line, defaults to the text color if `None`. pub stroke: Option, - /// Thickness of the line's stroke. Calling functions should attempt to - /// read this value from the appropriate font tables if this is `None`. + /// Thickness of the line's strokes (dependent on scaled font size), read + /// from the font tables if `None`. pub thickness: Option, - /// Position of the line relative to the baseline. Calling functions should - /// attempt to read this value from the appropriate font tables if this is - /// `None`. + /// Position of the line relative to the baseline (dependent on scaled font + /// size), read from the font tables if `None`. pub offset: Option, - /// Amount that the line will be longer or shorter than its associated text. + /// Amount that the line will be longer or shorter than its associated text + /// (dependent on scaled font size). pub extent: Linear, } diff --git a/src/geom/gen.rs b/src/geom/gen.rs index 57dc277d4..075b86201 100644 --- a/src/geom/gen.rs +++ b/src/geom/gen.rs @@ -60,6 +60,16 @@ impl Gen { } } +impl Gen> { + /// Unwrap the individual fields. + pub fn unwrap_or(self, other: Gen) -> Gen { + Gen { + cross: self.cross.unwrap_or(other.cross), + main: self.main.unwrap_or(other.main), + } + } +} + impl Get for Gen { type Component = T; diff --git a/src/geom/spec.rs b/src/geom/spec.rs index f8f62f9f9..ead67f11b 100644 --- a/src/geom/spec.rs +++ b/src/geom/spec.rs @@ -75,6 +75,16 @@ impl Spec { } } +impl Spec> { + /// Unwrap the individual fields. + pub fn unwrap_or(self, other: Spec) -> Spec { + Spec { + horizontal: self.horizontal.unwrap_or(other.horizontal), + vertical: self.vertical.unwrap_or(other.vertical), + } + } +} + impl Get for Spec { type Component = T; diff --git a/src/layout/par.rs b/src/layout/par.rs index 03d7efd56..a88a0f0bf 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -5,7 +5,7 @@ use unicode_bidi::{BidiInfo, Level}; use xi_unicode::LineBreakIterator; use super::*; -use crate::exec::TextState; +use crate::exec::FontState; use crate::util::{EcoString, RangeExt, SliceExt}; type Range = std::ops::Range; @@ -29,7 +29,7 @@ pub enum ParChild { /// Spacing between other nodes. Spacing(Length), /// A run of text and how to align it in its line. - Text(EcoString, Align, Rc), + Text(EcoString, Align, Rc), /// Any child node and how to align it in its line. Any(LayoutNode, Align), } diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index 0cfb01a84..3ede51227 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -5,7 +5,7 @@ use std::ops::Range; use rustybuzz::UnicodeBuffer; use super::{Element, Frame, Glyph, LayoutContext, Text}; -use crate::exec::{LineState, TextState}; +use crate::exec::{FontState, LineState}; use crate::font::{Face, FaceId, FontVariant, LineMetrics}; use crate::geom::{Dir, Length, Point, Size}; use crate::layout::Geometry; @@ -23,7 +23,7 @@ pub struct ShapedText<'a> { /// The text direction. pub dir: Dir, /// The properties used for font selection. - pub state: &'a TextState, + pub state: &'a FontState, /// The font size. pub size: Size, /// The baseline from the top of the frame. @@ -185,7 +185,7 @@ pub fn shape<'a>( ctx: &mut LayoutContext, text: &'a str, dir: Dir, - state: &'a TextState, + state: &'a FontState, ) -> ShapedText<'a> { let mut glyphs = vec![]; if !text.is_empty() { @@ -346,7 +346,7 @@ fn shape_segment<'a>( fn measure( ctx: &mut LayoutContext, glyphs: &[ShapedGlyph], - state: &TextState, + state: &FontState, ) -> (Size, Length) { let mut width = Length::zero(); let mut top = Length::zero(); @@ -386,7 +386,7 @@ fn decorate( pos: Point, width: Length, face_id: FaceId, - state: &TextState, + state: &FontState, ) { let mut apply = |substate: &LineState, metrics: fn(&Face) -> &LineMetrics| { let metrics = metrics(ctx.fonts.get(face_id)); diff --git a/src/library/layout.rs b/src/library/layout.rs index eea4afb57..f3f3f81e3 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -24,45 +24,45 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { Value::template(move |ctx| { let snapshot = ctx.state.clone(); + let state = ctx.state.page_mut(); if let Some(paper) = paper { - ctx.state.page.class = paper.class; - ctx.state.page.size = paper.size(); + state.class = paper.class; + state.size = paper.size(); } if let Some(width) = width { - ctx.state.page.class = PaperClass::Custom; - ctx.state.page.size.width = width; + state.class = PaperClass::Custom; + state.size.width = width; } if let Some(height) = height { - ctx.state.page.class = PaperClass::Custom; - ctx.state.page.size.height = height; + state.class = PaperClass::Custom; + state.size.height = height; } if let Some(margins) = margins { - ctx.state.page.margins = Sides::splat(Some(margins)); + state.margins = Sides::splat(Some(margins)); } if let Some(left) = left { - ctx.state.page.margins.left = Some(left); + state.margins.left = Some(left); } if let Some(top) = top { - ctx.state.page.margins.top = Some(top); + state.margins.top = Some(top); } if let Some(right) = right { - ctx.state.page.margins.right = Some(right); + state.margins.right = Some(right); } if let Some(bottom) = bottom { - ctx.state.page.margins.bottom = Some(bottom); + state.margins.bottom = Some(bottom); } if flip.unwrap_or(false) { - let page = &mut ctx.state.page; - std::mem::swap(&mut page.size.width, &mut page.size.height); + std::mem::swap(&mut state.size.width, &mut state.size.height); } ctx.pagebreak(false, true, span); @@ -96,7 +96,7 @@ fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: GenAxis) -> Va Value::template(move |ctx| { if let Some(linear) = spacing { // TODO: Should this really always be font-size relative? - let amount = linear.resolve(ctx.state.text.size); + let amount = linear.resolve(ctx.state.font.size); ctx.push_spacing(axis, amount); } }) @@ -180,7 +180,7 @@ pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { /// `stack`: Stack children along an axis. pub fn stack(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let dir = args.named(ctx, "dir").unwrap_or(Dir::TTB); + let dir = args.named(ctx, "dir"); let children: Vec<_> = args.all().collect(); Value::template(move |ctx| { @@ -192,22 +192,26 @@ pub fn stack(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { }) .collect(); - ctx.push_into_stack(StackNode { - dirs: Gen::new(ctx.state.dir, dir), - aspect: None, - children, - }); + let mut dirs = Gen::new(None, dir).unwrap_or(ctx.state.dirs); + + // If the directions become aligned, fix up the cross direction since + // that's the one that is not user-defined. + if dirs.main.axis() == dirs.cross.axis() { + dirs.cross = ctx.state.dirs.main; + } + + ctx.push_into_stack(StackNode { dirs, aspect: None, children }); }) } /// `grid`: Arrange children into a grid. pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let columns = args.named::(ctx, "columns").unwrap_or_default(); - let rows = args.named::(ctx, "rows").unwrap_or_default(); + let columns = args.named(ctx, "columns").unwrap_or_default(); + let rows = args.named(ctx, "rows").unwrap_or_default(); let gutter_columns = args.named(ctx, "gutter-columns"); let gutter_rows = args.named(ctx, "gutter-rows"); - let gutter = args + let default = args .named(ctx, "gutter") .map(|v| vec![TrackSizing::Linear(v)]) .unwrap_or_default(); @@ -217,22 +221,40 @@ pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let children: Vec<_> = args.all().collect(); + let tracks = Gen::new(columns, rows); + let gutter = Gen::new( + gutter_columns.unwrap_or_else(|| default.clone()), + gutter_rows.unwrap_or(default), + ); + Value::template(move |ctx| { let children = children .iter() .map(|child| ctx.exec_template_stack(child).into()) .collect(); - let cross_dir = column_dir.unwrap_or(ctx.state.dir); - let main_dir = row_dir.unwrap_or(cross_dir.axis().other().dir(true)); + let mut dirs = Gen::new(column_dir, row_dir).unwrap_or(ctx.state.dirs); + + // If the directions become aligned, try to fix up the direction which + // is not user-defined. + if dirs.main.axis() == dirs.cross.axis() { + let target = if column_dir.is_some() { + &mut dirs.main + } else { + &mut dirs.cross + }; + + *target = if target.axis() == ctx.state.dirs.cross.axis() { + ctx.state.dirs.main + } else { + ctx.state.dirs.cross + }; + } ctx.push_into_stack(GridNode { - dirs: Gen::new(cross_dir, main_dir), - tracks: Gen::new(columns.clone(), rows.clone()), - gutter: Gen::new( - gutter_columns.as_ref().unwrap_or(&gutter).clone(), - gutter_rows.as_ref().unwrap_or(&gutter).clone(), - ), + dirs, + tracks: tracks.clone(), + gutter: gutter.clone(), children, }) }) diff --git a/src/library/text.rs b/src/library/text.rs index b56cbcd36..54e9794ad 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -1,17 +1,10 @@ -use crate::exec::{LineState, TextState}; +use crate::exec::{FontState, LineState}; use crate::layout::Paint; use super::*; /// `font`: Configure the font. pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let families: Vec<_> = args.all().collect(); - let list = if families.is_empty() { - args.named(ctx, "family") - } else { - Some(FontDef(families)) - }; - let size = args.eat::().or_else(|| args.named(ctx, "size")); let style = args.named(ctx, "style"); let weight = args.named(ctx, "weight"); @@ -19,20 +12,25 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let top_edge = args.named(ctx, "top-edge"); let bottom_edge = args.named(ctx, "bottom-edge"); let fill = args.named(ctx, "fill"); + + let families: Vec<_> = args.all().collect(); + let list = if families.is_empty() { + args.named(ctx, "family") + } else { + Some(FontDef(Rc::new(families))) + }; + let serif = args.named(ctx, "serif"); let sans_serif = args.named(ctx, "sans-serif"); let monospace = args.named(ctx, "monospace"); + let body = args.expect::