diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 027db89d9..496238346 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -53,8 +53,7 @@ fn expand(stream: TokenStream2, mut impl_block: syn::ItemImpl) -> Result false, "showable" => true, @@ -244,10 +243,9 @@ fn process_const( /// A style property. struct Property { name: Ident, - hidden: bool, + skip: bool, referenced: bool, shorthand: Option, - variadic: bool, resolve: bool, fold: bool, } @@ -261,10 +259,9 @@ enum Shorthand { fn parse_property(item: &mut syn::ImplItemConst) -> Result { let mut property = Property { name: item.ident.clone(), - hidden: false, - referenced: false, + skip: false, shorthand: None, - variadic: false, + referenced: false, resolve: false, fold: false, }; @@ -279,7 +276,7 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result { while let Some(token) = stream.next() { match token { TokenTree::Ident(ident) => match ident.to_string().as_str() { - "hidden" => property.hidden = true, + "skip" => property.skip = true, "shorthand" => { let short = if let Some(TokenTree::Group(group)) = stream.peek() { let span = group.span(); @@ -296,7 +293,6 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result { property.shorthand = Some(short); } "referenced" => property.referenced = true, - "variadic" => property.variadic = true, "resolve" => property.resolve = true, "fold" => property.fold = true, _ => return Err(Error::new(ident.span(), "invalid attribute")), @@ -308,10 +304,10 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result { } let span = property.name.span(); - if property.shorthand.is_some() && property.variadic { + if property.skip && property.shorthand.is_some() { return Err(Error::new( span, - "shorthand and variadic are mutually exclusive", + "skip and shorthand are mutually exclusive", )); } @@ -326,26 +322,24 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result { } /// Auto-generate a `set` function from properties. -fn generate_set(properties: &[Property]) -> syn::ImplItemMethod { +fn generate_set( + properties: &[Property], + user: Option, +) -> syn::ImplItemMethod { + let user = user.map(|method| { + let block = &method.block; + quote! { (|| -> TypResult<()> { #block; Ok(()) } )()?; } + }); + let mut shorthands = vec![]; let sets: Vec<_> = properties .iter() - .filter(|p| !p.hidden) + .filter(|p| !p.skip) .map(|property| { let name = &property.name; let string = name.to_string().replace("_", "-").to_lowercase(); - let value = if property.variadic { - quote! { - match args.named(#string)? { - Some(value) => value, - None => { - let list: Vec<_> = args.all()?; - (!list.is_empty()).then(|| list) - } - } - } - } else if let Some(short) = &property.shorthand { + let value = if let Some(short) = &property.shorthand { match short { Shorthand::Positional => quote! { args.named_or_find(#string)? }, Shorthand::Named(named) => { @@ -357,7 +351,6 @@ fn generate_set(properties: &[Property]) -> syn::ImplItemMethod { quote! { args.named(#string)? } }; - quote! { styles.set_opt(Self::#name, #value); } }) .collect(); @@ -371,8 +364,9 @@ fn generate_set(properties: &[Property]) -> syn::ImplItemMethod { }); parse_quote! { - fn set(args: &mut Args) -> TypResult { + fn set(args: &mut Args, constructor: bool) -> TypResult { let mut styles = StyleMap::new(); + #user #(#bindings)* #(#sets)* Ok(styles) diff --git a/src/eval/args.rs b/src/eval/args.rs index f507e7145..9b21cfa2b 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -44,20 +44,6 @@ impl Args { Self { span, items } } - /// Consume and cast the first positional argument. - /// - /// Returns a `missing argument: {what}` error if no positional argument is - /// left. - pub fn expect(&mut self, what: &str) -> TypResult - where - T: Cast>, - { - match self.eat()? { - Some(v) => Ok(v), - None => bail!(self.span, "missing argument: {}", what), - } - } - /// Consume and cast the first positional argument if there is one. pub fn eat(&mut self) -> TypResult> where @@ -73,6 +59,20 @@ impl Args { Ok(None) } + /// Consume and cast the first positional argument. + /// + /// Returns a `missing argument: {what}` error if no positional argument is + /// left. + pub fn expect(&mut self, what: &str) -> TypResult + where + T: Cast>, + { + match self.eat()? { + Some(v) => Ok(v), + None => bail!(self.span, "missing argument: {}", what), + } + } + /// Find and consume the first castable positional argument. pub fn find(&mut self) -> TypResult> where diff --git a/src/eval/func.rs b/src/eval/func.rs index b72e9f184..73f2cac92 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -43,11 +43,11 @@ impl Func { Self(Arc::new(Repr::Native(Native { name, func: |ctx, args| { - let styles = T::set(args)?; + let styles = T::set(args, true)?; let content = T::construct(ctx, args)?; Ok(Value::Content(content.styled_with_map(styles.scoped()))) }, - set: Some(T::set), + set: Some(|args| T::set(args, false)), node: T::SHOWABLE.then(|| NodeId::of::()), }))) } @@ -165,7 +165,10 @@ pub trait Node: 'static { fn construct(ctx: &mut Context, args: &mut Args) -> TypResult; /// Parse the arguments into style properties for this node. - fn set(args: &mut Args) -> TypResult; + /// + /// When `constructor` is true, [`construct`](Self::construct) will run + /// after this invocation of `set`. + fn set(args: &mut Args, constructor: bool) -> TypResult; } /// A user-defined closure. diff --git a/src/eval/value.rs b/src/eval/value.rs index b5607cfdd..dd1839269 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -464,11 +464,19 @@ primitive! { f64: "float", Float, Int(v) => v as f64 } primitive! { RawLength: "length", Length } primitive! { Angle: "angle", Angle } primitive! { Ratio: "ratio", Ratio } -primitive! { Relative: "relative length", Relative, Length(v) => v.into(), Ratio(v) => v.into() } +primitive! { Relative: "relative length", + Relative, + Length(v) => v.into(), + Ratio(v) => v.into() +} primitive! { Fraction: "fraction", Fraction } primitive! { Color: "color", Color } primitive! { EcoString: "string", Str } -primitive! { Content: "content", Content, None => Content::new() } +primitive! { Content: "content", + Content, + None => Content::new(), + Str(text) => Content::Text(text) +} primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } primitive! { Func: "function", Func } diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 40b6e1e3e..bc768628b 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -24,19 +24,17 @@ impl ShapeNode { /// How to fill the shape. pub const FILL: Option = None; /// How to stroke the shape. - #[property(resolve, fold)] + #[property(skip, resolve, fold)] pub const STROKE: Smart>> = Smart::Auto; /// How much to pad the shape's content. #[property(resolve, fold)] pub const INSET: Sides>> = Sides::splat(Relative::zero()); - /// How much to extend the shape's dimensions beyond the allocated space. #[property(resolve, fold)] pub const OUTSET: Sides>> = Sides::splat(Relative::zero()); - /// How much to round the shape's corners. - #[property(resolve, fold)] + #[property(skip, resolve, fold)] pub const RADIUS: Sides>> = Sides::splat(Relative::zero()); fn construct(_: &mut Context, args: &mut Args) -> TypResult { @@ -57,14 +55,11 @@ impl ShapeNode { }; Ok(Content::inline( - Self(args.find()?).pack().sized(Spec::new(width, height)), + Self(args.eat()?).pack().sized(Spec::new(width, height)), )) } - fn set(args: &mut Args) -> TypResult { - let mut styles = StyleMap::new(); - styles.set_opt(Self::FILL, args.named("fill")?); - + fn set(...) { if is_round(S) { styles.set_opt( Self::STROKE, @@ -73,16 +68,8 @@ impl ShapeNode { ); } else { styles.set_opt(Self::STROKE, args.named("stroke")?); - } - - styles.set_opt(Self::INSET, args.named("inset")?); - styles.set_opt(Self::OUTSET, args.named("outset")?); - - if !is_round(S) { styles.set_opt(Self::RADIUS, args.named("radius")?); } - - Ok(styles) } } diff --git a/src/library/layout/container.rs b/src/library/layout/container.rs index 6689dd487..5264f2580 100644 --- a/src/library/layout/container.rs +++ b/src/library/layout/container.rs @@ -8,7 +8,7 @@ impl BoxNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { let width = args.named("width")?; let height = args.named("height")?; - let body: LayoutNode = args.find()?.unwrap_or_default(); + let body: LayoutNode = args.eat()?.unwrap_or_default(); Ok(Content::inline(body.sized(Spec::new(width, height)))) } } @@ -19,6 +19,6 @@ pub struct BlockNode; #[node] impl BlockNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - Ok(Content::Block(args.find()?.unwrap_or_default())) + Ok(Content::Block(args.eat()?.unwrap_or_default())) } } diff --git a/src/library/layout/pad.rs b/src/library/layout/pad.rs index 2be21bcb3..aff0e8b0b 100644 --- a/src/library/layout/pad.rs +++ b/src/library/layout/pad.rs @@ -12,7 +12,7 @@ pub struct PadNode { #[node] impl PadNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let all = args.find()?; + let all = args.named("rest")?.or(args.find()?); let x = args.named("x")?; let y = args.named("y")?; let left = args.named("left")?.or(x).or(all).unwrap_or_default(); diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index c8495e646..324ac285d 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -39,24 +39,11 @@ impl PageNode { Ok(Content::Page(Self(args.expect("body")?))) } - fn set(args: &mut Args) -> TypResult { - let mut styles = StyleMap::new(); - + fn set(...) { if let Some(paper) = args.named_or_find::("paper")? { styles.set(Self::WIDTH, Smart::Custom(paper.width().into())); styles.set(Self::HEIGHT, Smart::Custom(paper.height().into())); } - - styles.set_opt(Self::WIDTH, args.named("width")?); - styles.set_opt(Self::HEIGHT, args.named("height")?); - styles.set_opt(Self::MARGINS, args.named("margins")?); - styles.set_opt(Self::FLIPPED, args.named("flipped")?); - styles.set_opt(Self::FILL, args.named("fill")?); - styles.set_opt(Self::COLUMNS, args.named("columns")?); - styles.set_opt(Self::HEADER, args.named("header")?); - styles.set_opt(Self::FOOTER, args.named("footer")?); - - Ok(styles) } } diff --git a/src/library/text/link.rs b/src/library/text/link.rs index 423bcbfb3..9e9335298 100644 --- a/src/library/text/link.rs +++ b/src/library/text/link.rs @@ -22,7 +22,7 @@ impl LinkNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { Ok(Content::show(Self { url: args.expect::("url")?, - body: args.find()?, + body: args.eat()?, })) } } diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index 09c7fa173..80b036ac5 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -35,7 +35,7 @@ pub struct TextNode; #[node] impl TextNode { /// A prioritized sequence of font families. - #[property(referenced, variadic)] + #[property(skip, referenced)] pub const FAMILY: Vec = vec![FontFamily::new("IBM Plex Sans")]; /// Whether to allow font fallback when the primary font list contains no /// match. @@ -109,22 +109,22 @@ impl TextNode { pub const FEATURES: Vec<(Tag, u32)> = vec![]; /// Whether the font weight should be increased by 300. - #[property(hidden, fold)] + #[property(skip, fold)] pub const STRONG: Toggle = false; /// Whether the the font style should be inverted. - #[property(hidden, fold)] + #[property(skip, fold)] pub const EMPH: Toggle = false; /// A case transformation that should be applied to the text. - #[property(hidden)] + #[property(skip)] pub const CASE: Option = None; /// Whether small capital glyphs should be used. ("smcp") - #[property(hidden)] + #[property(skip)] pub const SMALLCAPS: bool = false; /// An URL the text should link to. - #[property(hidden, referenced)] + #[property(skip, referenced)] pub const LINK: Option = None; /// Decorative lines. - #[property(hidden, fold)] + #[property(skip, fold)] pub const DECO: Decoration = vec![]; fn construct(_: &mut Context, args: &mut Args) -> TypResult { @@ -133,6 +133,36 @@ impl TextNode { // styles all text in it. args.expect("body") } + + fn set(...) { + if let Some(family) = args.named("family")? { + styles.set(Self::FAMILY, family); + } else { + let mut count = 0; + let mut content = false; + for item in args.items.iter().filter(|item| item.name.is_none()) { + if EcoString::is(&item.value) { + count += 1; + } else if Content::is(&item.value) { + content = true; + } + } + + // Skip the final string if it's needed as the body. + if constructor && !content && count > 0 { + count -= 1; + } + + if count > 0 { + let mut list = Vec::with_capacity(count); + for _ in 0 .. count { + list.push(args.find()?.unwrap()); + } + + styles.set(Self::FAMILY, list); + } + } + } } /// A font family like "Arial". diff --git a/tests/ref/text/font.png b/tests/ref/text/font.png index 672513711..cce4897b2 100644 Binary files a/tests/ref/text/font.png and b/tests/ref/text/font.png differ diff --git a/tests/typ/style/show-node.typ b/tests/typ/style/show-node.typ index 07c30f19d..d0586c9d4 100644 --- a/tests/typ/style/show-node.typ +++ b/tests/typ/style/show-node.typ @@ -49,8 +49,8 @@ Some more text. Another text. --- -// Error: 18-22 expected content, found string -#show heading as "hi" +// Error: 18-22 expected content, found integer +#show heading as 1234 = Heading --- diff --git a/tests/typ/text/font.typ b/tests/typ/text/font.typ index b29091827..bfec3e1ce 100644 --- a/tests/typ/text/font.typ +++ b/tests/typ/text/font.typ @@ -38,6 +38,14 @@ Emoji: 🐪, 🌋, 🏞 #set text("PT Sans", "Twitter Color Emoji", fallback: false) 2π = 𝛼 + 𝛽. ✅ +--- +// Test string body. +#text("Text") \ +#text(red, "Text") \ +#text("Ubuntu", blue, "Text") \ +#text([Text], teal, "IBM Plex Serif") \ +#text(forest, "Latin Modern Roman", [Text]) \ + --- // Error: 11-16 unexpected argument #set text(false)