Cast content from string

This commit is contained in:
Laurenz 2022-05-06 10:05:11 +02:00
parent 49b8574b8d
commit bfaf5447a7
13 changed files with 106 additions and 89 deletions

View File

@ -53,8 +53,7 @@ fn expand(stream: TokenStream2, mut impl_block: syn::ItemImpl) -> Result<TokenSt
let construct = let construct =
construct.ok_or_else(|| Error::new(impl_block.span(), "missing constructor"))?; construct.ok_or_else(|| Error::new(impl_block.span(), "missing constructor"))?;
let set = set.unwrap_or_else(|| generate_set(&properties)); let set = generate_set(&properties, set);
let showable = match stream.to_string().as_str() { let showable = match stream.to_string().as_str() {
"" => false, "" => false,
"showable" => true, "showable" => true,
@ -244,10 +243,9 @@ fn process_const(
/// A style property. /// A style property.
struct Property { struct Property {
name: Ident, name: Ident,
hidden: bool, skip: bool,
referenced: bool, referenced: bool,
shorthand: Option<Shorthand>, shorthand: Option<Shorthand>,
variadic: bool,
resolve: bool, resolve: bool,
fold: bool, fold: bool,
} }
@ -261,10 +259,9 @@ enum Shorthand {
fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> { fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
let mut property = Property { let mut property = Property {
name: item.ident.clone(), name: item.ident.clone(),
hidden: false, skip: false,
referenced: false,
shorthand: None, shorthand: None,
variadic: false, referenced: false,
resolve: false, resolve: false,
fold: false, fold: false,
}; };
@ -279,7 +276,7 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
while let Some(token) = stream.next() { while let Some(token) = stream.next() {
match token { match token {
TokenTree::Ident(ident) => match ident.to_string().as_str() { TokenTree::Ident(ident) => match ident.to_string().as_str() {
"hidden" => property.hidden = true, "skip" => property.skip = true,
"shorthand" => { "shorthand" => {
let short = if let Some(TokenTree::Group(group)) = stream.peek() { let short = if let Some(TokenTree::Group(group)) = stream.peek() {
let span = group.span(); let span = group.span();
@ -296,7 +293,6 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
property.shorthand = Some(short); property.shorthand = Some(short);
} }
"referenced" => property.referenced = true, "referenced" => property.referenced = true,
"variadic" => property.variadic = true,
"resolve" => property.resolve = true, "resolve" => property.resolve = true,
"fold" => property.fold = true, "fold" => property.fold = true,
_ => return Err(Error::new(ident.span(), "invalid attribute")), _ => return Err(Error::new(ident.span(), "invalid attribute")),
@ -308,10 +304,10 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
} }
let span = property.name.span(); let span = property.name.span();
if property.shorthand.is_some() && property.variadic { if property.skip && property.shorthand.is_some() {
return Err(Error::new( return Err(Error::new(
span, 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<Property> {
} }
/// Auto-generate a `set` function from properties. /// Auto-generate a `set` function from properties.
fn generate_set(properties: &[Property]) -> syn::ImplItemMethod { fn generate_set(
properties: &[Property],
user: Option<syn::ImplItemMethod>,
) -> syn::ImplItemMethod {
let user = user.map(|method| {
let block = &method.block;
quote! { (|| -> TypResult<()> { #block; Ok(()) } )()?; }
});
let mut shorthands = vec![]; let mut shorthands = vec![];
let sets: Vec<_> = properties let sets: Vec<_> = properties
.iter() .iter()
.filter(|p| !p.hidden) .filter(|p| !p.skip)
.map(|property| { .map(|property| {
let name = &property.name; let name = &property.name;
let string = name.to_string().replace("_", "-").to_lowercase(); let string = name.to_string().replace("_", "-").to_lowercase();
let value = if property.variadic { let value = if let Some(short) = &property.shorthand {
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 {
match short { match short {
Shorthand::Positional => quote! { args.named_or_find(#string)? }, Shorthand::Positional => quote! { args.named_or_find(#string)? },
Shorthand::Named(named) => { Shorthand::Named(named) => {
@ -357,7 +351,6 @@ fn generate_set(properties: &[Property]) -> syn::ImplItemMethod {
quote! { args.named(#string)? } quote! { args.named(#string)? }
}; };
quote! { styles.set_opt(Self::#name, #value); } quote! { styles.set_opt(Self::#name, #value); }
}) })
.collect(); .collect();
@ -371,8 +364,9 @@ fn generate_set(properties: &[Property]) -> syn::ImplItemMethod {
}); });
parse_quote! { parse_quote! {
fn set(args: &mut Args) -> TypResult<StyleMap> { fn set(args: &mut Args, constructor: bool) -> TypResult<StyleMap> {
let mut styles = StyleMap::new(); let mut styles = StyleMap::new();
#user
#(#bindings)* #(#bindings)*
#(#sets)* #(#sets)*
Ok(styles) Ok(styles)

View File

@ -44,20 +44,6 @@ impl Args {
Self { span, items } 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<T>(&mut self, what: &str) -> TypResult<T>
where
T: Cast<Spanned<Value>>,
{
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. /// Consume and cast the first positional argument if there is one.
pub fn eat<T>(&mut self) -> TypResult<Option<T>> pub fn eat<T>(&mut self) -> TypResult<Option<T>>
where where
@ -73,6 +59,20 @@ impl Args {
Ok(None) Ok(None)
} }
/// Consume and cast the first positional argument.
///
/// Returns a `missing argument: {what}` error if no positional argument is
/// left.
pub fn expect<T>(&mut self, what: &str) -> TypResult<T>
where
T: Cast<Spanned<Value>>,
{
match self.eat()? {
Some(v) => Ok(v),
None => bail!(self.span, "missing argument: {}", what),
}
}
/// Find and consume the first castable positional argument. /// Find and consume the first castable positional argument.
pub fn find<T>(&mut self) -> TypResult<Option<T>> pub fn find<T>(&mut self) -> TypResult<Option<T>>
where where

View File

@ -43,11 +43,11 @@ impl Func {
Self(Arc::new(Repr::Native(Native { Self(Arc::new(Repr::Native(Native {
name, name,
func: |ctx, args| { func: |ctx, args| {
let styles = T::set(args)?; let styles = T::set(args, true)?;
let content = T::construct(ctx, args)?; let content = T::construct(ctx, args)?;
Ok(Value::Content(content.styled_with_map(styles.scoped()))) 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::<T>()), node: T::SHOWABLE.then(|| NodeId::of::<T>()),
}))) })))
} }
@ -165,7 +165,10 @@ pub trait Node: 'static {
fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Content>; fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Content>;
/// Parse the arguments into style properties for this node. /// Parse the arguments into style properties for this node.
fn set(args: &mut Args) -> TypResult<StyleMap>; ///
/// When `constructor` is true, [`construct`](Self::construct) will run
/// after this invocation of `set`.
fn set(args: &mut Args, constructor: bool) -> TypResult<StyleMap>;
} }
/// A user-defined closure. /// A user-defined closure.

View File

@ -464,11 +464,19 @@ primitive! { f64: "float", Float, Int(v) => v as f64 }
primitive! { RawLength: "length", Length } primitive! { RawLength: "length", Length }
primitive! { Angle: "angle", Angle } primitive! { Angle: "angle", Angle }
primitive! { Ratio: "ratio", Ratio } primitive! { Ratio: "ratio", Ratio }
primitive! { Relative<RawLength>: "relative length", Relative, Length(v) => v.into(), Ratio(v) => v.into() } primitive! { Relative<RawLength>: "relative length",
Relative,
Length(v) => v.into(),
Ratio(v) => v.into()
}
primitive! { Fraction: "fraction", Fraction } primitive! { Fraction: "fraction", Fraction }
primitive! { Color: "color", Color } primitive! { Color: "color", Color }
primitive! { EcoString: "string", Str } 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! { Array: "array", Array }
primitive! { Dict: "dictionary", Dict } primitive! { Dict: "dictionary", Dict }
primitive! { Func: "function", Func } primitive! { Func: "function", Func }

View File

@ -24,19 +24,17 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to fill the shape. /// How to fill the shape.
pub const FILL: Option<Paint> = None; pub const FILL: Option<Paint> = None;
/// How to stroke the shape. /// How to stroke the shape.
#[property(resolve, fold)] #[property(skip, resolve, fold)]
pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto; pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto;
/// How much to pad the shape's content. /// How much to pad the shape's content.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const INSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero()); pub const INSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
/// How much to extend the shape's dimensions beyond the allocated space. /// How much to extend the shape's dimensions beyond the allocated space.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const OUTSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero()); pub const OUTSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
/// How much to round the shape's corners. /// How much to round the shape's corners.
#[property(resolve, fold)] #[property(skip, resolve, fold)]
pub const RADIUS: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero()); pub const RADIUS: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
@ -57,14 +55,11 @@ impl<const S: ShapeKind> ShapeNode<S> {
}; };
Ok(Content::inline( 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<StyleMap> { fn set(...) {
let mut styles = StyleMap::new();
styles.set_opt(Self::FILL, args.named("fill")?);
if is_round(S) { if is_round(S) {
styles.set_opt( styles.set_opt(
Self::STROKE, Self::STROKE,
@ -73,16 +68,8 @@ impl<const S: ShapeKind> ShapeNode<S> {
); );
} else { } else {
styles.set_opt(Self::STROKE, args.named("stroke")?); 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")?); styles.set_opt(Self::RADIUS, args.named("radius")?);
} }
Ok(styles)
} }
} }

View File

@ -8,7 +8,7 @@ impl BoxNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let width = args.named("width")?; let width = args.named("width")?;
let height = args.named("height")?; 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)))) Ok(Content::inline(body.sized(Spec::new(width, height))))
} }
} }
@ -19,6 +19,6 @@ pub struct BlockNode;
#[node] #[node]
impl BlockNode { impl BlockNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
Ok(Content::Block(args.find()?.unwrap_or_default())) Ok(Content::Block(args.eat()?.unwrap_or_default()))
} }
} }

View File

@ -12,7 +12,7 @@ pub struct PadNode {
#[node] #[node]
impl PadNode { impl PadNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let all = args.find()?; let all = args.named("rest")?.or(args.find()?);
let x = args.named("x")?; let x = args.named("x")?;
let y = args.named("y")?; let y = args.named("y")?;
let left = args.named("left")?.or(x).or(all).unwrap_or_default(); let left = args.named("left")?.or(x).or(all).unwrap_or_default();

View File

@ -39,24 +39,11 @@ impl PageNode {
Ok(Content::Page(Self(args.expect("body")?))) Ok(Content::Page(Self(args.expect("body")?)))
} }
fn set(args: &mut Args) -> TypResult<StyleMap> { fn set(...) {
let mut styles = StyleMap::new();
if let Some(paper) = args.named_or_find::<Paper>("paper")? { if let Some(paper) = args.named_or_find::<Paper>("paper")? {
styles.set(Self::WIDTH, Smart::Custom(paper.width().into())); styles.set(Self::WIDTH, Smart::Custom(paper.width().into()));
styles.set(Self::HEIGHT, Smart::Custom(paper.height().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)
} }
} }

View File

@ -22,7 +22,7 @@ impl LinkNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
Ok(Content::show(Self { Ok(Content::show(Self {
url: args.expect::<EcoString>("url")?, url: args.expect::<EcoString>("url")?,
body: args.find()?, body: args.eat()?,
})) }))
} }
} }

View File

@ -35,7 +35,7 @@ pub struct TextNode;
#[node] #[node]
impl TextNode { impl TextNode {
/// A prioritized sequence of font families. /// A prioritized sequence of font families.
#[property(referenced, variadic)] #[property(skip, referenced)]
pub const FAMILY: Vec<FontFamily> = vec![FontFamily::new("IBM Plex Sans")]; pub const FAMILY: Vec<FontFamily> = vec![FontFamily::new("IBM Plex Sans")];
/// Whether to allow font fallback when the primary font list contains no /// Whether to allow font fallback when the primary font list contains no
/// match. /// match.
@ -109,22 +109,22 @@ impl TextNode {
pub const FEATURES: Vec<(Tag, u32)> = vec![]; pub const FEATURES: Vec<(Tag, u32)> = vec![];
/// Whether the font weight should be increased by 300. /// Whether the font weight should be increased by 300.
#[property(hidden, fold)] #[property(skip, fold)]
pub const STRONG: Toggle = false; pub const STRONG: Toggle = false;
/// Whether the the font style should be inverted. /// Whether the the font style should be inverted.
#[property(hidden, fold)] #[property(skip, fold)]
pub const EMPH: Toggle = false; pub const EMPH: Toggle = false;
/// A case transformation that should be applied to the text. /// A case transformation that should be applied to the text.
#[property(hidden)] #[property(skip)]
pub const CASE: Option<Case> = None; pub const CASE: Option<Case> = None;
/// Whether small capital glyphs should be used. ("smcp") /// Whether small capital glyphs should be used. ("smcp")
#[property(hidden)] #[property(skip)]
pub const SMALLCAPS: bool = false; pub const SMALLCAPS: bool = false;
/// An URL the text should link to. /// An URL the text should link to.
#[property(hidden, referenced)] #[property(skip, referenced)]
pub const LINK: Option<EcoString> = None; pub const LINK: Option<EcoString> = None;
/// Decorative lines. /// Decorative lines.
#[property(hidden, fold)] #[property(skip, fold)]
pub const DECO: Decoration = vec![]; pub const DECO: Decoration = vec![];
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
@ -133,6 +133,36 @@ impl TextNode {
// styles all text in it. // styles all text in it.
args.expect("body") 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". /// A font family like "Arial".

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -49,8 +49,8 @@ Some more text.
Another text. Another text.
--- ---
// Error: 18-22 expected content, found string // Error: 18-22 expected content, found integer
#show heading as "hi" #show heading as 1234
= Heading = Heading
--- ---

View File

@ -38,6 +38,14 @@ Emoji: 🐪, 🌋, 🏞
#set text("PT Sans", "Twitter Color Emoji", fallback: false) #set text("PT Sans", "Twitter Color Emoji", fallback: false)
= 𝛼 + 𝛽. = 𝛼 + 𝛽.
---
// 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 // Error: 11-16 unexpected argument
#set text(false) #set text(false)