diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 15925df73..027db89d9 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -53,38 +53,7 @@ fn expand(stream: TokenStream2, mut impl_block: syn::ItemImpl) -> Result value, - None => { - let list: Vec<_> = args.all()?; - (!list.is_empty()).then(|| list) - } - } - } - } else if property.shorthand { - quote! { args.named_or_find(#string)? } - } else { - quote! { args.named(#string)? } - }; - - quote! { styles.set_opt(Self::#name, #value); } - }); - - parse_quote! { - fn set(args: &mut Args) -> TypResult { - let mut styles = StyleMap::new(); - #(#sets)* - Ok(styles) - } - } - }); + let set = set.unwrap_or_else(|| generate_set(&properties)); let showable = match stream.to_string().as_str() { "" => false, @@ -204,7 +173,7 @@ fn process_const( }; } else if property.resolve { get = quote! { - let value = values.next().cloned().unwrap_or(#default); + let value = values.next().cloned().unwrap_or_else(|| #default); model::Resolve::resolve(value, chain) }; } else if property.fold { @@ -277,19 +246,24 @@ struct Property { name: Ident, hidden: bool, referenced: bool, - shorthand: bool, + shorthand: Option, variadic: bool, resolve: bool, fold: bool, } +enum Shorthand { + Positional, + Named(Ident), +} + /// Parse a style property attribute. fn parse_property(item: &mut syn::ImplItemConst) -> Result { let mut property = Property { name: item.ident.clone(), hidden: false, referenced: false, - shorthand: false, + shorthand: None, variadic: false, resolve: false, fold: false, @@ -301,11 +275,26 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result { .position(|attr| attr.path.get_ident().map_or(false, |name| name == "property")) { let attr = item.attrs.remove(idx); - for token in attr.parse_args::()? { + let mut stream = attr.parse_args::()?.into_iter().peekable(); + while let Some(token) = stream.next() { match token { TokenTree::Ident(ident) => match ident.to_string().as_str() { "hidden" => property.hidden = true, - "shorthand" => property.shorthand = true, + "shorthand" => { + let short = if let Some(TokenTree::Group(group)) = stream.peek() { + let span = group.span(); + let repr = group.to_string(); + let ident = repr.trim_matches(|c| matches!(c, '(' | ')')); + if !ident.chars().all(|c| c.is_ascii_alphabetic()) { + return Err(Error::new(span, "invalid args")); + } + stream.next(); + Shorthand::Named(Ident::new(ident, span)) + } else { + Shorthand::Positional + }; + property.shorthand = Some(short); + } "referenced" => property.referenced = true, "variadic" => property.variadic = true, "resolve" => property.resolve = true, @@ -319,7 +308,7 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result { } let span = property.name.span(); - if property.shorthand && property.variadic { + if property.shorthand.is_some() && property.variadic { return Err(Error::new( span, "shorthand and variadic are mutually exclusive", @@ -335,3 +324,58 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result { Ok(property) } + +/// Auto-generate a `set` function from properties. +fn generate_set(properties: &[Property]) -> syn::ImplItemMethod { + let mut shorthands = vec![]; + let sets: Vec<_> = properties + .iter() + .filter(|p| !p.hidden) + .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 { + match short { + Shorthand::Positional => quote! { args.named_or_find(#string)? }, + Shorthand::Named(named) => { + shorthands.push(named); + quote! { args.named(#string)?.or_else(|| #named.clone()) } + } + } + } else { + quote! { args.named(#string)? } + }; + + + quote! { styles.set_opt(Self::#name, #value); } + }) + .collect(); + + shorthands.sort(); + shorthands.dedup_by_key(|ident| ident.to_string()); + + let bindings = shorthands.into_iter().map(|ident| { + let string = ident.to_string(); + quote! { let #ident = args.named(#string)?; } + }); + + parse_quote! { + fn set(args: &mut Args) -> TypResult { + let mut styles = StyleMap::new(); + #(#bindings)* + #(#sets)* + Ok(styles) + } + } +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index bb9eb2fb1..5542de891 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -101,9 +101,9 @@ impl Eval for MarkupNode { Ok(match self { Self::Space => Content::Space, Self::Parbreak => Content::Parbreak, - Self::Linebreak(justified) => Content::Linebreak(*justified), + &Self::Linebreak { justified } => Content::Linebreak { justified }, Self::Text(text) => Content::Text(text.clone()), - Self::Quote(double) => Content::Quote(*double), + &Self::Quote { double } => Content::Quote { double }, Self::Strong(strong) => strong.eval(ctx, scp)?, Self::Emph(emph) => emph.eval(ctx, scp)?, Self::Raw(raw) => raw.eval(ctx, scp)?, diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 832bd354b..56bfdf2eb 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -316,9 +316,10 @@ pub fn compare(lhs: &Value, rhs: &Value) -> Option { (Bool(a), Bool(b)) => a.partial_cmp(b), (Int(a), Int(b)) => a.partial_cmp(b), (Float(a), Float(b)) => a.partial_cmp(b), - (Angle(a), Angle(b)) => a.partial_cmp(b), (Length(a), Length(b)) => a.partial_cmp(b), + (Angle(a), Angle(b)) => a.partial_cmp(b), (Ratio(a), Ratio(b)) => a.partial_cmp(b), + (Relative(a), Relative(b)) => a.partial_cmp(b), (Fraction(a), Fraction(b)) => a.partial_cmp(b), (Str(a), Str(b)) => a.partial_cmp(b), diff --git a/src/eval/raw.rs b/src/eval/raw.rs index 2d82fca8d..6545ea5ab 100644 --- a/src/eval/raw.rs +++ b/src/eval/raw.rs @@ -213,6 +213,8 @@ impl PartialOrd for RawLength { fn partial_cmp(&self, other: &Self) -> Option { if self.em.is_zero() && other.em.is_zero() { self.length.partial_cmp(&other.length) + } else if self.length.is_zero() && other.length.is_zero() { + self.em.partial_cmp(&other.em) } else { None } diff --git a/src/eval/value.rs b/src/eval/value.rs index 31287fa86..6ce815a4f 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -408,12 +408,25 @@ macro_rules! dynamic { /// Make a type castable from a value. macro_rules! castable { + ($type:ty: $inner:ty) => { + impl $crate::eval::Cast<$crate::eval::Value> for $type { + fn is(value: &$crate::eval::Value) -> bool { + <$inner>::is(value) + } + + fn cast(value: $crate::eval::Value) -> $crate::diag::StrResult { + <$inner>::cast(value).map(Self) + } + } + }; + ( $type:ty, Expected: $expected:expr, $($pattern:pat => $out:expr,)* $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)* ) => { + #[allow(unreachable_patterns)] impl $crate::eval::Cast<$crate::eval::Value> for $type { fn is(value: &$crate::eval::Value) -> bool { #[allow(unused_variables)] @@ -602,10 +615,14 @@ castable! { castable! { NonZeroUsize, Expected: "positive integer", - Value::Int(int) => Value::Int(int) - .cast::()? + Value::Int(int) => int .try_into() - .map_err(|_| "must be positive")?, + .and_then(|int: usize| int.try_into()) + .map_err(|_| if int <= 0 { + "must be positive" + } else { + "number too large" + })?, } castable! { diff --git a/src/geom/relative.rs b/src/geom/relative.rs index fc77fb9ff..f213ae522 100644 --- a/src/geom/relative.rs +++ b/src/geom/relative.rs @@ -68,6 +68,18 @@ impl From for Relative { } } +impl PartialOrd for Relative { + fn partial_cmp(&self, other: &Self) -> Option { + if self.rel.is_zero() && other.rel.is_zero() { + self.abs.partial_cmp(&other.abs) + } else if self.abs.is_zero() && other.abs.is_zero() { + self.rel.partial_cmp(&other.rel) + } else { + None + } + } +} + impl Neg for Relative { type Output = Self; diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index e4c832f0d..49c74c2f0 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -26,7 +26,6 @@ impl ShapeNode { /// How to stroke the shape. #[property(resolve, fold)] pub const STROKE: Smart> = Smart::Auto; - /// How much to pad the shape's content. pub const PADDING: Relative = Relative::zero(); diff --git a/src/library/graphics/transform.rs b/src/library/graphics/transform.rs index ea021cc19..a4aa20dba 100644 --- a/src/library/graphics/transform.rs +++ b/src/library/graphics/transform.rs @@ -13,8 +13,8 @@ pub struct MoveNode { #[node] impl MoveNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let dx = args.named("x")?.unwrap_or_default(); - let dy = args.named("y")?.unwrap_or_default(); + let dx = args.named("dx")?.unwrap_or_default(); + let dy = args.named("dy")?.unwrap_or_default(); Ok(Content::inline(Self { delta: Spec::new(dx, dy), child: args.expect("body")?, diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs index 2a4d039ea..c050d2a44 100644 --- a/src/library/layout/align.rs +++ b/src/library/layout/align.rs @@ -13,9 +13,15 @@ pub struct AlignNode { #[node] impl AlignNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let aligns: Spec<_> = args.find()?.unwrap_or_default(); - let body: LayoutNode = args.expect("body")?; - Ok(Content::block(body.aligned(aligns))) + let aligns: Spec> = args.find()?.unwrap_or_default(); + let body: Content = args.expect("body")?; + Ok(match (body, aligns) { + (Content::Block(node), _) => Content::Block(node.aligned(aligns)), + (other, Spec { x: Some(x), y: None }) => { + other.styled(ParNode::ALIGN, HorizontalAlign(x)) + } + (other, _) => Content::Block(other.pack().aligned(aligns)), + }) } } diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs index 4963043ef..8e5236946 100644 --- a/src/library/layout/columns.rs +++ b/src/library/layout/columns.rs @@ -106,7 +106,8 @@ pub struct ColbreakNode; #[node] impl ColbreakNode { - fn construct(_: &mut Context, _: &mut Args) -> TypResult { - Ok(Content::Colbreak) + fn construct(_: &mut Context, args: &mut Args) -> TypResult { + let weak = args.named("weak")?.unwrap_or(false); + Ok(Content::Colbreak { weak }) } } diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs index 6b43c8b7f..6193a68f6 100644 --- a/src/library/layout/flow.rs +++ b/src/library/layout/flow.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use super::{AlignNode, PlaceNode, Spacing}; use crate::library::prelude::*; use crate::library::text::ParNode; @@ -10,18 +12,14 @@ use crate::library::text::ParNode; pub struct FlowNode(pub StyleVec); /// A child of a flow node. -#[derive(Hash)] +#[derive(Hash, PartialEq)] pub enum FlowChild { - /// Leading between other children. - Leading, - /// A paragraph / block break. - Parbreak, - /// A column / region break. - Colbreak, /// Vertical spacing between other children. Spacing(Spacing), /// An arbitrary block-level node. Node(LayoutNode), + /// A column / region break. + Colbreak, } impl Layout for FlowNode { @@ -36,25 +34,15 @@ impl Layout for FlowNode { for (child, map) in self.0.iter() { let styles = map.chain(&styles); match child { - FlowChild::Leading => { - let amount = styles.get(ParNode::LEADING); - layouter.layout_spacing(amount.into(), styles); - } - FlowChild::Parbreak => { - let leading = styles.get(ParNode::LEADING); - let spacing = styles.get(ParNode::SPACING); - let amount = leading + spacing; - layouter.layout_spacing(amount.into(), styles); - } - FlowChild::Colbreak => { - layouter.finish_region(); - } FlowChild::Spacing(kind) => { layouter.layout_spacing(*kind, styles); } FlowChild::Node(ref node) => { layouter.layout_node(ctx, node, styles)?; } + FlowChild::Colbreak => { + layouter.finish_region(); + } } } @@ -72,11 +60,18 @@ impl Debug for FlowNode { impl Debug for FlowChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::Leading => f.pad("Leading"), - Self::Parbreak => f.pad("Parbreak"), - Self::Colbreak => f.pad("Colbreak"), Self::Spacing(kind) => write!(f, "{:?}", kind), Self::Node(node) => node.fmt(f), + Self::Colbreak => f.pad("Colbreak"), + } + } +} + +impl PartialOrd for FlowChild { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), + _ => None, } } } diff --git a/src/library/layout/pad.rs b/src/library/layout/pad.rs index e688e4231..2be21bcb3 100644 --- a/src/library/layout/pad.rs +++ b/src/library/layout/pad.rs @@ -13,12 +13,12 @@ pub struct PadNode { impl PadNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { let all = args.find()?; - let hor = args.named("horizontal")?; - let ver = args.named("vertical")?; - let left = args.named("left")?.or(hor).or(all).unwrap_or_default(); - let top = args.named("top")?.or(ver).or(all).unwrap_or_default(); - let right = args.named("right")?.or(hor).or(all).unwrap_or_default(); - let bottom = args.named("bottom")?.or(ver).or(all).unwrap_or_default(); + let x = args.named("x")?; + let y = args.named("y")?; + let left = args.named("left")?.or(x).or(all).unwrap_or_default(); + let top = args.named("top")?.or(y).or(all).unwrap_or_default(); + let right = args.named("right")?.or(x).or(all).unwrap_or_default(); + let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default(); let body: LayoutNode = args.expect("body")?; let padding = Sides::new(left, top, right, bottom); Ok(Content::block(body.padded(padding))) diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index 8e5801ea5..4307d2f92 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -165,8 +165,8 @@ pub struct PagebreakNode; #[node] impl PagebreakNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let soft = args.named("soft")?.unwrap_or(false); - Ok(Content::Pagebreak(soft)) + let weak = args.named("weak")?.unwrap_or(false); + Ok(Content::Pagebreak { weak }) } } diff --git a/src/library/layout/spacing.rs b/src/library/layout/spacing.rs index 3468af5ec..8a96e3785 100644 --- a/src/library/layout/spacing.rs +++ b/src/library/layout/spacing.rs @@ -1,4 +1,7 @@ +use std::cmp::Ordering; + use crate::library::prelude::*; +use crate::library::text::ParNode; /// Horizontal spacing. pub struct HNode; @@ -6,7 +9,9 @@ pub struct HNode; #[node] impl HNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - Ok(Content::Horizontal(args.expect("spacing")?)) + let amount = args.expect("spacing")?; + let weak = args.named("weak")?.unwrap_or(false); + Ok(Content::Horizontal { amount, weak }) } } @@ -16,7 +21,9 @@ pub struct VNode; #[node] impl VNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - Ok(Content::Vertical(args.expect("spacing")?)) + let amount = args.expect("spacing")?; + let weak = args.named("weak")?.unwrap_or(false); + Ok(Content::Vertical { amount, weak, generated: false }) } } @@ -25,7 +32,8 @@ impl VNode { pub enum Spacing { /// Spacing specified in absolute terms and relative to the parent's size. Relative(Relative), - /// Spacing specified as a fraction of the remaining free space in the parent. + /// Spacing specified as a fraction of the remaining free space in the + /// parent. Fractional(Fraction), } @@ -42,6 +50,16 @@ impl From for Spacing { } } +impl PartialOrd for Spacing { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Relative(a), Self::Relative(b)) => a.partial_cmp(b), + (Self::Fractional(a), Self::Fractional(b)) => a.partial_cmp(b), + _ => None, + } + } +} + castable! { Spacing, Expected: "relative length or fraction", @@ -50,3 +68,24 @@ castable! { Value::Relative(v) => Self::Relative(v), Value::Fraction(v) => Self::Fractional(v), } + +/// Spacing around and between block-level nodes, relative to paragraph spacing. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct BlockSpacing(Relative); + +castable!(BlockSpacing: Relative); + +impl Resolve for BlockSpacing { + type Output = Length; + + fn resolve(self, styles: StyleChain) -> Self::Output { + let whole = styles.get(ParNode::SPACING); + self.0.resolve(styles).relative_to(whole) + } +} + +impl From for BlockSpacing { + fn from(ratio: Ratio) -> Self { + Self(ratio.into()) + } +} diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs index f915c2151..bbfeeab04 100644 --- a/src/library/layout/stack.rs +++ b/src/library/layout/stack.rs @@ -1,5 +1,6 @@ use super::{AlignNode, Spacing}; use crate::library::prelude::*; +use crate::library::text::ParNode; /// Arrange nodes and spacing along an axis. #[derive(Debug, Hash)] @@ -180,7 +181,16 @@ impl<'a> StackLayouter<'a> { .downcast::() .and_then(|node| node.aligns.get(self.axis)) .map(|align| align.resolve(styles)) - .unwrap_or(self.dir.start().into()); + .unwrap_or_else(|| { + if let Some(Content::Styled(styled)) = node.downcast::() { + let map = &styled.1; + if map.contains(ParNode::ALIGN) { + return StyleChain::with_root(&styled.1).get(ParNode::ALIGN); + } + } + + self.dir.start().into() + }); let frames = node.layout(ctx, &self.regions, styles)?; let len = frames.len(); diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs index 345bb3f65..7b4998ca8 100644 --- a/src/library/math/mod.rs +++ b/src/library/math/mod.rs @@ -1,5 +1,6 @@ //! Mathematical formulas. +use crate::library::layout::BlockSpacing; use crate::library::prelude::*; use crate::library::text::FontFamily; @@ -19,6 +20,13 @@ impl MathNode { pub const FAMILY: Smart = Smart::Custom(FontFamily::new("Latin Modern Math")); + /// The spacing above display math. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option = Some(Ratio::one().into()); + /// The spacing below display math. + #[property(resolve, shorthand(around))] + pub const BELOW: Option = Some(Ratio::one().into()); + fn construct(_: &mut Context, args: &mut Args) -> TypResult { Ok(Content::show(Self { formula: args.expect("formula")?, @@ -36,7 +44,11 @@ impl Show for MathNode { } fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult { - Ok(Content::Text(self.formula.trim().into())) + let mut realized = Content::Text(self.formula.trim().into()); + if self.display { + realized = Content::block(realized); + } + Ok(realized) } fn finalize( @@ -50,12 +62,10 @@ impl Show for MathNode { map.set_family(family.clone(), styles); } - realized = realized.styled_with_map(map); - if self.display { - realized = Content::block(realized); + realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); } - Ok(realized) + Ok(realized.styled_with_map(map)) } } diff --git a/src/library/mod.rs b/src/library/mod.rs index c68915c8a..e90e5cc4f 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -88,7 +88,7 @@ pub fn new() -> Scope { std.def_fn("letter", utility::letter); std.def_fn("roman", utility::roman); std.def_fn("symbol", utility::symbol); - std.def_fn("lipsum", utility::lipsum); + std.def_fn("lorem", utility::lorem); // Predefined colors. std.def_const("black", Color::BLACK); diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index a6c879120..4f6c54f38 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -1,3 +1,4 @@ +use crate::library::layout::BlockSpacing; use crate::library::prelude::*; use crate::library::text::{FontFamily, TextNode, TextSize, Toggle}; @@ -6,7 +7,7 @@ use crate::library::text::{FontFamily, TextNode, TextSize, Toggle}; 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: usize, + pub level: NonZeroUsize, /// The heading's contents. pub body: Content, } @@ -22,8 +23,12 @@ impl HeadingNode { /// The size of text in the heading. #[property(referenced)] pub const SIZE: Leveled = Leveled::Mapping(|level| { - let upscale = (1.6 - 0.1 * level as f64).max(0.75); - TextSize(Em::new(upscale).into()) + let size = match level.get() { + 1 => 1.4, + 2 => 1.2, + _ => 1.0, + }; + TextSize(Em::new(size).into()) }); /// Whether text in the heading is strengthend. @@ -36,21 +41,24 @@ impl HeadingNode { #[property(referenced)] pub const UNDERLINE: Leveled = Leveled::Value(false); - /// The extra padding above the heading. - #[property(referenced)] - pub const ABOVE: Leveled = Leveled::Value(Length::zero().into()); - /// The extra padding below the heading. - #[property(referenced)] - pub const BELOW: Leveled = Leveled::Value(Length::zero().into()); - - /// Whether the heading is block-level. - #[property(referenced)] - pub const BLOCK: Leveled = Leveled::Value(true); + /// The spacing above the heading. + #[property(referenced, shorthand(around))] + pub const ABOVE: Leveled> = Leveled::Mapping(|level| { + let ratio = match level.get() { + 1 => 1.5, + _ => 1.2, + }; + Some(Ratio::new(ratio).into()) + }); + /// The spacing below the heading. + #[property(referenced, shorthand(around))] + pub const BELOW: Leveled> = + Leveled::Value(Some(Ratio::new(0.55).into())); fn construct(_: &mut Context, args: &mut Args) -> TypResult { Ok(Content::show(Self { body: args.expect("body")?, - level: args.named("level")?.unwrap_or(1), + level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()), })) } } @@ -58,13 +66,13 @@ impl HeadingNode { impl Show for HeadingNode { fn encode(&self) -> Dict { dict! { - "level" => Value::Int(self.level as i64), + "level" => Value::Int(self.level.get() as i64), "body" => Value::Content(self.body.clone()), } } fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult { - Ok(self.body.clone()) + Ok(Content::block(self.body.clone())) } fn finalize( @@ -103,11 +111,6 @@ impl Show for HeadingNode { } realized = realized.styled_with_map(map); - - if resolve!(Self::BLOCK) { - realized = Content::block(realized); - } - realized = realized.spaced( resolve!(Self::ABOVE).resolve(styles), resolve!(Self::BELOW).resolve(styles), @@ -123,19 +126,19 @@ pub enum Leveled { /// A bare value. Value(T), /// A simple mapping from a heading level to a value. - Mapping(fn(usize) -> T), + Mapping(fn(NonZeroUsize) -> T), /// A closure mapping from a heading level to a value. Func(Func, Span), } impl Leveled { /// Resolve the value based on the level. - pub fn resolve(&self, ctx: &mut Context, level: usize) -> TypResult { + pub fn resolve(&self, ctx: &mut Context, level: NonZeroUsize) -> TypResult { Ok(match self { Self::Value(value) => value.clone(), Self::Mapping(mapping) => mapping(level), Self::Func(func, span) => { - let args = Args::from_values(*span, [Value::Int(level as i64)]); + let args = Args::from_values(*span, [Value::Int(level.get() as i64)]); func.call(ctx, args)?.cast().at(*span)? } }) diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index ac705156e..4356ffb49 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use unscanny::Scanner; -use crate::library::layout::{GridNode, TrackSizing}; +use crate::library::layout::{BlockSpacing, GridNode, TrackSizing}; use crate::library::prelude::*; use crate::library::text::ParNode; use crate::library::utility::Numbering; @@ -12,9 +12,10 @@ use crate::library::utility::Numbering; pub struct ListNode { /// Where the list starts. pub start: usize, - /// If false, there is paragraph spacing between the items, if true - /// there is list spacing between the items. + /// If true, the items are separated by leading instead of list spacing. pub tight: bool, + /// If true, the spacing above the list is leading instead of above spacing. + pub attached: bool, /// The individual bulleted or numbered items. pub items: StyleVec, } @@ -38,10 +39,6 @@ impl ListNode { /// How the list is labelled. #[property(referenced)] pub const LABEL: Label = Label::Default; - - /// The spacing between the list items of a non-wide list. - #[property(resolve)] - pub const SPACING: RawLength = RawLength::zero(); /// The indentation of each item's label. #[property(resolve)] pub const INDENT: RawLength = RawLength::zero(); @@ -49,17 +46,21 @@ impl ListNode { #[property(resolve)] pub const BODY_INDENT: RawLength = Em::new(0.5).into(); - /// The extra padding above the list. + /// The spacing above the list. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option = Some(Ratio::one().into()); + /// The spacing below the list. + #[property(resolve, shorthand(around))] + pub const BELOW: Option = Some(Ratio::one().into()); + /// The spacing between the items of a wide (non-tight) list. #[property(resolve)] - pub const ABOVE: RawLength = RawLength::zero(); - /// The extra padding below the list. - #[property(resolve)] - pub const BELOW: RawLength = RawLength::zero(); + pub const SPACING: BlockSpacing = Ratio::one().into(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { Ok(Content::show(Self { start: args.named("start")?.unwrap_or(1), tight: args.named("tight")?.unwrap_or(true), + attached: args.named("attached")?.unwrap_or(false), items: args .all()? .into_iter() @@ -78,6 +79,7 @@ impl Show for ListNode { dict! { "start" => Value::Int(self.start as i64), "tight" => Value::Bool(self.tight), + "attached" => Value::Bool(self.attached), "items" => Value::Array( self.items .items() @@ -103,14 +105,12 @@ impl Show for ListNode { number += 1; } - let leading = styles.get(ParNode::LEADING); - let spacing = if self.tight { - styles.get(Self::SPACING) + let gutter = if self.tight { + styles.get(ParNode::LEADING) } else { - styles.get(ParNode::SPACING) + styles.get(Self::SPACING) }; - let gutter = leading + spacing; let indent = styles.get(Self::INDENT); let body_indent = styles.get(Self::BODY_INDENT); @@ -132,7 +132,19 @@ impl Show for ListNode { styles: StyleChain, realized: Content, ) -> TypResult { - Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))) + let mut above = styles.get(Self::ABOVE); + let mut below = styles.get(Self::BELOW); + + if self.attached { + if above.is_some() { + above = Some(styles.get(ParNode::LEADING)); + } + if below.is_some() { + below = Some(styles.get(ParNode::SPACING)); + } + } + + Ok(realized.spaced(above, below)) } } diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs index 191d3dd35..7b3f2ac57 100644 --- a/src/library/structure/table.rs +++ b/src/library/structure/table.rs @@ -1,4 +1,4 @@ -use crate::library::layout::{GridNode, TrackSizing}; +use crate::library::layout::{BlockSpacing, GridNode, TrackSizing}; use crate::library::prelude::*; /// A table of items. @@ -15,16 +15,24 @@ pub struct TableNode { #[node(showable)] impl TableNode { /// The primary cell fill color. + #[property(shorthand(fill))] pub const PRIMARY: Option = None; /// The secondary cell fill color. + #[property(shorthand(fill))] pub const SECONDARY: Option = None; /// How to stroke the cells. #[property(resolve, fold)] pub const STROKE: Option = Some(RawStroke::default()); - /// How much to pad the cells's content. pub const PADDING: Relative = Length::pt(5.0).into(); + /// The spacing above the table. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option = Some(Ratio::one().into()); + /// The spacing below the table. + #[property(resolve, shorthand(around))] + pub const BELOW: Option = Some(Ratio::one().into()); + fn construct(_: &mut Context, args: &mut Args) -> TypResult { let columns = args.named("columns")?.unwrap_or_default(); let rows = args.named("rows")?.unwrap_or_default(); @@ -40,16 +48,6 @@ impl TableNode { cells: args.all()?, })) } - - fn set(args: &mut Args) -> TypResult { - let mut styles = StyleMap::new(); - let fill = args.named("fill")?; - styles.set_opt(Self::PRIMARY, args.named("primary")?.or(fill)); - styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill)); - styles.set_opt(Self::STROKE, args.named("stroke")?); - styles.set_opt(Self::PADDING, args.named("padding")?); - Ok(styles) - } } impl Show for TableNode { @@ -99,4 +97,13 @@ impl Show for TableNode { cells, })) } + + fn finalize( + &self, + _: &mut Context, + styles: StyleChain, + realized: Content, + ) -> TypResult { + Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))) + } } diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs index 52f8ea80c..70040f9c2 100644 --- a/src/library/text/deco.rs +++ b/src/library/text/deco.rs @@ -24,7 +24,6 @@ impl DecoNode { /// tables if `auto`. #[property(shorthand, resolve, fold)] pub const STROKE: Smart = Smart::Auto; - /// Position of the line relative to the baseline, read from the font tables /// if `auto`. #[property(resolve)] @@ -32,7 +31,6 @@ impl DecoNode { /// Amount that the line will be longer or shorter than its associated text. #[property(resolve)] pub const EXTENT: RawLength = RawLength::zero(); - /// Whether the line skips sections in which it would collide /// with the glyphs. Does not apply to strikethrough. pub const EVADE: bool = true; diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index 3cfbc55d3..ecc0c546b 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -223,11 +223,7 @@ impl Fold for TextSize { } } -castable! { - TextSize, - Expected: "length", - Value::Length(v) => Self(v), -} +castable!(TextSize: RawLength); /// Specifies the bottom or top edge of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -290,11 +286,7 @@ impl Resolve for Smart { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Hyphenate(pub bool); -castable! { - Hyphenate, - Expected: "boolean", - Value::Bool(v) => Self(v), -} +castable!(Hyphenate: bool); impl Resolve for Smart { type Output = bool; diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 4694993ec..669d07ba8 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::sync::Arc; use unicode_bidi::{BidiInfo, Level}; @@ -15,12 +16,12 @@ use crate::util::{EcoString, MaybeShared}; pub struct ParNode(pub StyleVec); /// A uniformly styled atomic piece of a paragraph. -#[derive(Hash)] +#[derive(Hash, PartialEq)] pub enum ParChild { /// A chunk of text. Text(EcoString), - /// A smart quote, may be single (`false`) or double (`true`). - Quote(bool), + /// A single or double smart quote. + Quote { double: bool }, /// Horizontal spacing between other children. Spacing(Spacing), /// An arbitrary inline-level node. @@ -34,10 +35,12 @@ impl ParNode { pub const LEADING: RawLength = Em::new(0.65).into(); /// The extra spacing between paragraphs. #[property(resolve)] - pub const SPACING: RawLength = Em::new(0.55).into(); + pub const SPACING: RawLength = Em::new(1.2).into(); /// The indent the first line of a consecutive paragraph should have. #[property(resolve)] pub const INDENT: RawLength = RawLength::zero(); + /// Whether to allow paragraph spacing when there is paragraph indent. + pub const SPACING_AND_INDENT: bool = false; /// How to align text and inline objects in their line. #[property(resolve)] @@ -50,10 +53,13 @@ impl ParNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { // The paragraph constructor is special: It doesn't create a paragraph - // since that happens automatically through markup. Instead, it just - // lifts the passed body to the block level so that it won't merge with - // adjacent stuff and it styles the contained paragraphs. - Ok(Content::Block(args.expect("body")?)) + // node. Instead, it just ensures that the passed content lives is in a + // separate paragraph and styles it. + Ok(Content::sequence(vec![ + Content::Parbreak, + args.expect("body")?, + Content::Parbreak, + ])) } } @@ -91,13 +97,22 @@ impl Debug for ParChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Text(text) => write!(f, "Text({:?})", text), - Self::Quote(double) => write!(f, "Quote({})", double), + Self::Quote { double } => write!(f, "Quote({double})"), Self::Spacing(kind) => write!(f, "{:?}", kind), Self::Node(node) => node.fmt(f), } } } +impl PartialOrd for ParChild { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), + _ => None, + } + } +} + /// A horizontal alignment. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalAlign(pub RawAlign); @@ -169,7 +184,7 @@ pub struct LinebreakNode; impl LinebreakNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { let justified = args.named("justified")?.unwrap_or(false); - Ok(Content::Linebreak(justified)) + Ok(Content::Linebreak { justified }) } } @@ -432,7 +447,7 @@ fn collect<'a>( } Segment::Text(full.len() - prev) } - ParChild::Quote(double) => { + ParChild::Quote { double } => { let prev = full.len(); if styles.get(TextNode::SMART_QUOTES) { let lang = styles.get(TextNode::LANG); @@ -440,7 +455,7 @@ fn collect<'a>( let quotes = Quotes::from_lang(lang, region); let peeked = iter.peek().and_then(|(child, _)| match child { ParChild::Text(text) => text.chars().next(), - ParChild::Quote(_) => Some('"'), + ParChild::Quote { .. } => Some('"'), ParChild::Spacing(_) => Some(SPACING_REPLACE), ParChild::Node(_) => Some(NODE_REPLACE), }); diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index 13daa1b9c..ee6c6356c 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -4,6 +4,7 @@ use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet}; use syntect::parsing::SyntaxSet; use super::{FontFamily, Hyphenate, TextNode, Toggle}; +use crate::library::layout::BlockSpacing; use crate::library::prelude::*; use crate::source::SourceId; use crate::syntax::{self, RedNode}; @@ -26,13 +27,20 @@ pub struct RawNode { #[node(showable)] impl RawNode { + /// The language to syntax-highlight in. + #[property(referenced)] + pub const LANG: Option = None; + /// The raw text's font family. Just the normal text family if `none`. #[property(referenced)] pub const FAMILY: Smart = Smart::Custom(FontFamily::new("IBM Plex Mono")); - /// The language to syntax-highlight in. - #[property(referenced)] - pub const LANG: Option = None; + /// The spacing above block-level raw. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option = Some(Ratio::one().into()); + /// The spacing below block-level raw. + #[property(resolve, shorthand(around))] + pub const BELOW: Option = Some(Ratio::one().into()); fn construct(_: &mut Context, args: &mut Args) -> TypResult { Ok(Content::show(Self { @@ -59,7 +67,7 @@ impl Show for RawNode { .unwrap_or(Color::BLACK) .into(); - if matches!( + let mut realized = if matches!( lang.map(|s| s.to_lowercase()).as_deref(), Some("typ" | "typst") ) { @@ -72,7 +80,7 @@ impl Show for RawNode { seq.push(styled(&self.text[range], foreground, style)); }); - Ok(Content::sequence(seq)) + Content::sequence(seq) } else if let Some(syntax) = lang.and_then(|token| SYNTAXES.find_syntax_by_token(&token)) { @@ -80,7 +88,7 @@ impl Show for RawNode { let mut highlighter = HighlightLines::new(syntax, &THEME); for (i, line) in self.text.lines().enumerate() { if i != 0 { - seq.push(Content::Linebreak(false)); + seq.push(Content::Linebreak { justified: false }); } for (style, piece) in highlighter.highlight(line, &SYNTAXES) { @@ -88,10 +96,16 @@ impl Show for RawNode { } } - Ok(Content::sequence(seq)) + Content::sequence(seq) } else { - Ok(Content::Text(self.text.clone())) + Content::Text(self.text.clone()) + }; + + if self.block { + realized = Content::block(realized); } + + Ok(realized) } fn finalize( @@ -109,13 +123,11 @@ impl Show for RawNode { map.set_family(family.clone(), styles); } - realized = realized.styled_with_map(map); - if self.block { - realized = Content::block(realized); + realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); } - Ok(realized) + Ok(realized.styled_with_map(map)) } } diff --git a/src/library/utility/blind.rs b/src/library/utility/blind.rs index a4cfec90b..0075ab91f 100644 --- a/src/library/utility/blind.rs +++ b/src/library/utility/blind.rs @@ -3,7 +3,7 @@ use lipsum::lipsum_from_seed; use crate::library::prelude::*; /// Create blind text. -pub fn lipsum(_: &mut Context, args: &mut Args) -> TypResult { +pub fn lorem(_: &mut Context, args: &mut Args) -> TypResult { let words: usize = args.expect("number of words")?; Ok(Value::Str(lipsum_from_seed(words, 97).into())) } diff --git a/src/model/collapse.rs b/src/model/collapse.rs index 17933fe8f..258f577eb 100644 --- a/src/model/collapse.rs +++ b/src/model/collapse.rs @@ -35,26 +35,37 @@ impl<'a, T> CollapsingBuilder<'a, T> { } /// Can only exist when there is at least one supportive item to its left - /// and to its right, with no destructive items or weak items in between to - /// its left and no destructive items in between to its right. There may be + /// and to its right, with no destructive items in between. There may be /// ignorant items in between in both directions. - pub fn weak(&mut self, item: T, strength: u8, styles: StyleChain<'a>) { - if self.last != Last::Destructive { - if self.last == Last::Weak { - if let Some(i) = self - .staged - .iter() - .position(|(.., prev)| prev.map_or(false, |p| p < strength)) - { - self.staged.remove(i); - } else { - return; - } - } - - self.staged.push((item, styles, Some(strength))); - self.last = Last::Weak; + /// + /// Between weak items, there may be at least one per layer and among the + /// candidates the strongest one (smallest `weakness`) wins. When tied, + /// the one that compares larger through `PartialOrd` wins. + pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8) + where + T: PartialOrd, + { + if self.last == Last::Destructive { + return; } + + if self.last == Last::Weak { + if let Some(i) = + self.staged.iter().position(|(prev_item, _, prev_weakness)| { + prev_weakness.map_or(false, |prev_weakness| { + weakness < prev_weakness + || (weakness == prev_weakness && item > *prev_item) + }) + }) + { + self.staged.remove(i); + } else { + return; + } + } + + self.staged.push((item, styles, Some(weakness))); + self.last = Last::Weak; } /// Forces nearby weak items to collapse. @@ -90,8 +101,8 @@ impl<'a, T> CollapsingBuilder<'a, T> { /// Push the staged items, filtering out weak items if `supportive` is /// false. fn flush(&mut self, supportive: bool) { - for (item, styles, strength) in self.staged.drain(..) { - if supportive || strength.is_none() { + for (item, styles, meta) in self.staged.drain(..) { + if supportive || meta.is_none() { self.builder.push(item, styles); } } @@ -103,3 +114,64 @@ impl<'a, T> Default for CollapsingBuilder<'a, T> { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::library::layout::FlowChild; + use crate::library::prelude::*; + + #[track_caller] + fn test(builder: CollapsingBuilder, expected: &[T]) + where + T: Debug + PartialEq, + { + let result = builder.finish().0; + let items: Vec<_> = result.items().collect(); + let expected: Vec<_> = expected.iter().collect(); + assert_eq!(items, expected); + } + + fn node() -> FlowChild { + FlowChild::Node(Content::Text("Hi".into()).pack()) + } + + fn abs(pt: f64) -> FlowChild { + FlowChild::Spacing(Length::pt(pt).into()) + } + + #[test] + fn test_collapsing_weak() { + let mut builder = CollapsingBuilder::new(); + let styles = StyleChain::default(); + builder.weak(FlowChild::Colbreak, styles, 0); + builder.supportive(node(), styles); + builder.weak(abs(10.0), styles, 0); + builder.ignorant(FlowChild::Colbreak, styles); + builder.weak(abs(20.0), styles, 0); + builder.supportive(node(), styles); + builder.weak(abs(10.0), styles, 0); + builder.weak(abs(20.0), styles, 1); + builder.supportive(node(), styles); + test(builder, &[ + node(), + FlowChild::Colbreak, + abs(20.0), + node(), + abs(10.0), + node(), + ]); + } + + #[test] + fn test_collapsing_destructive() { + let mut builder = CollapsingBuilder::new(); + let styles = StyleChain::default(); + builder.supportive(node(), styles); + builder.weak(abs(10.0), styles, 0); + builder.destructive(FlowChild::Colbreak, styles); + builder.weak(abs(20.0), styles, 0); + builder.supportive(node(), styles); + test(builder, &[node(), FlowChild::Colbreak, node()]); + } +} diff --git a/src/model/content.rs b/src/model/content.rs index 6e1e2f1c5..31255a293 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -40,29 +40,32 @@ use crate::util::EcoString; pub enum Content { /// A word space. Space, - /// A forced line break. If `true`, the preceding line can still be - /// justified, if `false` not. - Linebreak(bool), + /// A forced line break. + Linebreak { justified: bool }, /// Horizontal spacing. - Horizontal(Spacing), + Horizontal { amount: Spacing, weak: bool }, /// Plain text. Text(EcoString), - /// A smart quote, may be single (`false`) or double (`true`). - Quote(bool), + /// A smart quote. + Quote { double: bool }, /// An inline-level node. Inline(LayoutNode), /// A paragraph break. Parbreak, /// A column break. - Colbreak, + Colbreak { weak: bool }, /// Vertical spacing. - Vertical(Spacing), + Vertical { + amount: Spacing, + weak: bool, + generated: bool, + }, /// A block-level node. Block(LayoutNode), /// A list / enum item. Item(ListItem), /// A page break. - Pagebreak(bool), + Pagebreak { weak: bool }, /// A page node. Page(PageNode), /// A node that can be realized with styles. @@ -153,21 +156,28 @@ impl Content { Self::show(DecoNode::(self)) } - /// Add vertical spacing above and below the node. - pub fn spaced(self, above: Length, below: Length) -> Self { - if above.is_zero() && below.is_zero() { + /// Add weak vertical spacing above and below the node. + pub fn spaced(self, above: Option, below: Option) -> Self { + if above.is_none() && below.is_none() { return self; } let mut seq = vec![]; - if !above.is_zero() { - seq.push(Content::Vertical(above.into())); + if let Some(above) = above { + seq.push(Content::Vertical { + amount: above.into(), + weak: true, + generated: true, + }); } seq.push(self); - - if !below.is_zero() { - seq.push(Content::Vertical(below.into())); + if let Some(below) = below { + seq.push(Content::Vertical { + amount: below.into(), + weak: true, + generated: true, + }); } Self::sequence(seq) @@ -219,17 +229,21 @@ impl Debug for Content { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Space => f.pad("Space"), - Self::Linebreak(justified) => write!(f, "Linebreak({justified})"), - Self::Horizontal(kind) => write!(f, "Horizontal({kind:?})"), + Self::Linebreak { justified } => write!(f, "Linebreak({justified})"), + Self::Horizontal { amount, weak } => { + write!(f, "Horizontal({amount:?}, {weak})") + } Self::Text(text) => write!(f, "Text({text:?})"), - Self::Quote(double) => write!(f, "Quote({double})"), + Self::Quote { double } => write!(f, "Quote({double})"), Self::Inline(node) => node.fmt(f), Self::Parbreak => f.pad("Parbreak"), - Self::Colbreak => f.pad("Colbreak"), - Self::Vertical(kind) => write!(f, "Vertical({kind:?})"), + Self::Colbreak { weak } => write!(f, "Colbreak({weak})"), + Self::Vertical { amount, weak, generated } => { + write!(f, "Vertical({amount:?}, {weak}, {generated})") + } Self::Block(node) => node.fmt(f), Self::Item(item) => item.fmt(f), - Self::Pagebreak(soft) => write!(f, "Pagebreak({soft})"), + Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"), Self::Page(page) => page.fmt(f), Self::Show(node) => node.fmt(f), Self::Styled(styled) => { @@ -360,7 +374,7 @@ impl<'a, 'ctx> Builder<'a, 'ctx> { return Ok(()); } - let keep = matches!(content, Content::Pagebreak(false)); + let keep = matches!(content, Content::Pagebreak { weak: false }); self.interrupt(Interruption::Page, styles, keep)?; if let Some(doc) = &mut self.doc { @@ -419,10 +433,8 @@ impl<'a, 'ctx> Builder<'a, 'ctx> { if intr >= Interruption::Par { if !self.par.is_empty() { - self.flow.0.weak(FlowChild::Leading, 0, styles); mem::take(&mut self.par).finish(self); } - self.flow.0.weak(FlowChild::Leading, 0, styles); } if intr >= Interruption::Page { @@ -456,8 +468,8 @@ struct DocBuilder<'a> { impl<'a> DocBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) { match content { - Content::Pagebreak(soft) => { - self.keep_next = !soft; + Content::Pagebreak { weak } => { + self.keep_next = !weak; } Content::Page(page) => { self.pages.push(page.clone(), styles); @@ -483,16 +495,31 @@ struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>); impl<'a> FlowBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + // Weak flow elements: + // Weakness | Element + // 0 | weak colbreak + // 1 | weak fractional spacing + // 2 | weak spacing + // 3 | generated weak spacing + // 4 | generated weak fractional spacing + // 5 | par spacing + match content { - Content::Parbreak => { - self.0.weak(FlowChild::Parbreak, 1, styles); + Content::Parbreak => {} + Content::Colbreak { weak } => { + if *weak { + self.0.weak(FlowChild::Colbreak, styles, 0); + } else { + self.0.destructive(FlowChild::Colbreak, styles); + } } - Content::Colbreak => { - self.0.destructive(FlowChild::Colbreak, styles); - } - Content::Vertical(kind) => { - let child = FlowChild::Spacing(*kind); - if kind.is_fractional() { + &Content::Vertical { amount, weak, generated } => { + let child = FlowChild::Spacing(amount); + let frac = amount.is_fractional(); + if weak { + let weakness = 1 + u8::from(frac) + 2 * u8::from(generated); + self.0.weak(child, styles, weakness); + } else if frac { self.0.destructive(child, styles); } else { self.0.ignorant(child, styles); @@ -512,6 +539,18 @@ impl<'a> FlowBuilder<'a> { true } + fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) { + let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) { + styles.get(ParNode::LEADING).into() + } else { + styles.get(ParNode::SPACING).into() + }; + + self.0.weak(FlowChild::Spacing(amount), styles, 5); + self.0.supportive(FlowChild::Node(par.pack()), styles); + self.0.weak(FlowChild::Spacing(amount), styles, 5); + } + fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) { let (flow, shared) = self.0.finish(); let styles = if flow.is_empty() { styles } else { shared }; @@ -530,24 +569,34 @@ struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>); impl<'a> ParBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + // Weak par elements: + // Weakness | Element + // 0 | weak fractional spacing + // 1 | weak spacing + // 2 | space + match content { Content::Space => { - self.0.weak(ParChild::Text(' '.into()), 0, styles); + self.0.weak(ParChild::Text(' '.into()), styles, 2); } - Content::Linebreak(justified) => { - let c = if *justified { '\u{2028}' } else { '\n' }; + &Content::Linebreak { justified } => { + let c = if justified { '\u{2028}' } else { '\n' }; self.0.destructive(ParChild::Text(c.into()), styles); } - Content::Horizontal(kind) => { - let child = ParChild::Spacing(*kind); - if kind.is_fractional() { + &Content::Horizontal { amount, weak } => { + let child = ParChild::Spacing(amount); + let frac = amount.is_fractional(); + if weak { + let weakness = u8::from(!frac); + self.0.weak(child, styles, weakness); + } else if frac { self.0.destructive(child, styles); } else { self.0.ignorant(child, styles); } } - Content::Quote(double) => { - self.0.supportive(ParChild::Quote(*double), styles); + &Content::Quote { double } => { + self.0.supportive(ParChild::Quote { double }, styles); } Content::Text(text) => { self.0.supportive(ParChild::Text(text.clone()), styles); @@ -575,7 +624,7 @@ impl<'a> ParBuilder<'a> { .items() .find_map(|child| match child { ParChild::Spacing(_) => None, - ParChild::Text(_) | ParChild::Quote(_) => Some(true), + ParChild::Text(_) | ParChild::Quote { .. } => Some(true), ParChild::Node(_) => Some(false), }) .unwrap_or_default() @@ -585,10 +634,8 @@ impl<'a> ParBuilder<'a> { .items() .rev() .find_map(|child| match child { - FlowChild::Leading => None, - FlowChild::Parbreak => None, + FlowChild::Spacing(_) => None, FlowChild::Node(node) => Some(node.is::()), - FlowChild::Spacing(_) => Some(false), FlowChild::Colbreak => Some(false), }) .unwrap_or_default() @@ -596,8 +643,7 @@ impl<'a> ParBuilder<'a> { children.push_front(ParChild::Spacing(indent.into())); } - let node = ParNode(children).pack(); - parent.flow.0.supportive(FlowChild::Node(node), shared); + parent.flow.par(ParNode(children), shared, !indent.is_zero()); } fn is_empty(&self) -> bool { @@ -611,19 +657,24 @@ struct ListBuilder<'a> { items: StyleVecBuilder<'a, ListItem>, /// Whether the list contains no paragraph breaks. tight: bool, + /// Whether the list can be attached. + attachable: bool, /// Trailing content for which it is unclear whether it is part of the list. staged: Vec<(&'a Content, StyleChain<'a>)>, } impl<'a> ListBuilder<'a> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + if self.items.is_empty() { + match content { + Content::Space => {} + Content::Item(_) => {} + Content::Parbreak => self.attachable = false, + _ => self.attachable = true, + } + } + match content { - Content::Space if !self.items.is_empty() => { - self.staged.push((content, styles)); - } - Content::Parbreak if !self.items.is_empty() => { - self.staged.push((content, styles)); - } Content::Item(item) if self .items @@ -634,6 +685,9 @@ impl<'a> ListBuilder<'a> { self.items.push(item.clone(), styles); self.tight &= self.staged.drain(..).all(|(t, _)| *t != Content::Parbreak); } + Content::Space | Content::Parbreak if !self.items.is_empty() => { + self.staged.push((content, styles)); + } _ => return false, } @@ -647,10 +701,17 @@ impl<'a> ListBuilder<'a> { None => return Ok(()), }; + let start = 1; let tight = self.tight; + let attached = tight && self.attachable; + let content = match kind { - UNORDERED => Content::show(ListNode:: { start: 1, tight, items }), - ORDERED | _ => Content::show(ListNode:: { start: 1, tight, items }), + UNORDERED => { + Content::show(ListNode:: { start, tight, attached, items }) + } + ORDERED | _ => { + Content::show(ListNode:: { start, tight, attached, items }) + } }; let stored = parent.scratch.templates.alloc(content); @@ -660,6 +721,8 @@ impl<'a> ListBuilder<'a> { parent.accept(content, styles)?; } + parent.list.attachable = true; + Ok(()) } @@ -673,6 +736,7 @@ impl Default for ListBuilder<'_> { Self { items: StyleVecBuilder::default(), tight: true, + attachable: true, staged: vec![], } } diff --git a/src/model/layout.rs b/src/model/layout.rs index 78bfedc7b..511542862 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -1,7 +1,7 @@ //! Layouting infrastructure. use std::any::Any; -use std::fmt::{self, Debug, Formatter}; +use std::fmt::{self, Debug, Formatter, Write}; use std::hash::Hash; use std::sync::Arc; @@ -239,7 +239,9 @@ impl Default for LayoutNode { impl Debug for LayoutNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) + f.write_str("Layout(")?; + self.0.fmt(f)?; + f.write_char(')') } } diff --git a/src/model/show.rs b/src/model/show.rs index d1365eb20..16374deba 100644 --- a/src/model/show.rs +++ b/src/model/show.rs @@ -1,4 +1,4 @@ -use std::fmt::{self, Debug, Formatter}; +use std::fmt::{self, Debug, Formatter, Write}; use std::hash::Hash; use std::sync::Arc; @@ -87,7 +87,9 @@ impl Show for ShowNode { impl Debug for ShowNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) + f.write_str("Show(")?; + self.0.fmt(f)?; + f.write_char(')') } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 145f86d69..32d618851 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -214,8 +214,8 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) { | NodeKind::EnDash | NodeKind::EmDash | NodeKind::Ellipsis - | NodeKind::Quote(_) - | NodeKind::Linebreak(_) + | NodeKind::Quote { .. } + | NodeKind::Linebreak { .. } | NodeKind::Raw(_) | NodeKind::Math(_) | NodeKind::Escape(_) => { diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 4772be7f0..f095bd095 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -141,8 +141,8 @@ impl<'s> Tokens<'s> { '~' => NodeKind::NonBreakingSpace, '-' => self.hyph(), '.' if self.s.eat_if("..") => NodeKind::Ellipsis, - '\'' => NodeKind::Quote(false), - '"' => NodeKind::Quote(true), + '\'' => NodeKind::Quote { double: false }, + '"' => NodeKind::Quote { double: true }, '*' if !self.in_word() => NodeKind::Star, '_' if !self.in_word() => NodeKind::Underscore, '`' => self.raw(), @@ -266,7 +266,7 @@ impl<'s> Tokens<'s> { fn backslash(&mut self) -> NodeKind { let c = match self.s.peek() { Some(c) => c, - None => return NodeKind::Linebreak(false), + None => return NodeKind::Linebreak { justified: false }, }; match c { @@ -300,10 +300,10 @@ impl<'s> Tokens<'s> { } // Linebreaks. - c if c.is_whitespace() => NodeKind::Linebreak(false), + c if c.is_whitespace() => NodeKind::Linebreak { justified: false }, '+' => { self.s.expect(c); - NodeKind::Linebreak(true) + NodeKind::Linebreak { justified: true } } // Just the backslash. @@ -839,7 +839,7 @@ mod tests { t!(Markup[" /"]: "hello-world" => Text("hello-world")); // Test code symbols in text. - t!(Markup[" /"]: "a():\"b" => Text("a():"), Quote(true), Text("b")); + t!(Markup[" /"]: "a():\"b" => Text("a():"), Quote { double: true }, Text("b")); t!(Markup[" /"]: ";:,|/+" => Text(";:,|"), Text("/+")); t!(Markup[" /"]: "=-a" => Eq, Minus, Text("a")); t!(Markup[" "]: "#123" => Text("#"), Text("123")); @@ -893,8 +893,8 @@ mod tests { t!(Markup: "_" => Underscore); t!(Markup[""]: "===" => Eq, Eq, Eq); t!(Markup["a1/"]: "= " => Eq, Space(0)); - t!(Markup[" "]: r"\" => Linebreak(false)); - t!(Markup[" "]: r"\+" => Linebreak(true)); + t!(Markup[" "]: r"\" => Linebreak { justified: false }); + t!(Markup[" "]: r"\+" => Linebreak { justified: true }); t!(Markup: "~" => NonBreakingSpace); t!(Markup["a1/"]: "-?" => Shy); t!(Markup["a "]: r"a--" => Text("a"), EnDash); diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 5232b1f15..42a4235d3 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -2,6 +2,7 @@ //! //! The AST is rooted in the [`Markup`] node. +use std::num::NonZeroUsize; use std::ops::Deref; use super::{Green, GreenData, NodeKind, RedNode, RedRef, Span}; @@ -62,7 +63,9 @@ impl Markup { self.0.children().filter_map(|node| match node.kind() { NodeKind::Space(2 ..) => Some(MarkupNode::Parbreak), NodeKind::Space(_) => Some(MarkupNode::Space), - NodeKind::Linebreak(j) => Some(MarkupNode::Linebreak(*j)), + &NodeKind::Linebreak { justified } => { + Some(MarkupNode::Linebreak { justified }) + } NodeKind::Text(s) => Some(MarkupNode::Text(s.clone())), NodeKind::Escape(c) => Some(MarkupNode::Text((*c).into())), NodeKind::NonBreakingSpace => Some(MarkupNode::Text('\u{00A0}'.into())), @@ -70,7 +73,7 @@ impl Markup { NodeKind::EnDash => Some(MarkupNode::Text('\u{2013}'.into())), NodeKind::EmDash => Some(MarkupNode::Text('\u{2014}'.into())), NodeKind::Ellipsis => Some(MarkupNode::Text('\u{2026}'.into())), - NodeKind::Quote(d) => Some(MarkupNode::Quote(*d)), + &NodeKind::Quote { double } => Some(MarkupNode::Quote { double }), NodeKind::Strong => node.cast().map(MarkupNode::Strong), NodeKind::Emph => node.cast().map(MarkupNode::Emph), NodeKind::Raw(raw) => Some(MarkupNode::Raw(raw.as_ref().clone())), @@ -88,15 +91,14 @@ impl Markup { pub enum MarkupNode { /// Whitespace containing less than two newlines. Space, - /// A forced line break. If `true` (`\`), the preceding line can still be - /// justified, if `false` (`\+`) not. - Linebreak(bool), + /// A forced line break: `\` or `\+` if justified. + Linebreak { justified: bool }, /// A paragraph break: Two or more newlines. Parbreak, /// Plain text. Text(EcoString), - /// A smart quote: `'` (`false`) or `"` (true). - Quote(bool), + /// A smart quote: `'` or `"`. + Quote { double: bool }, /// Strong content: `*Strong*`. Strong(StrongNode), /// Emphasized content: `_Emphasized_`. @@ -176,8 +178,13 @@ impl HeadingNode { } /// The section depth (numer of equals signs). - pub fn level(&self) -> usize { - self.0.children().filter(|n| n.kind() == &NodeKind::Eq).count() + pub fn level(&self) -> NonZeroUsize { + self.0 + .children() + .filter(|n| n.kind() == &NodeKind::Eq) + .count() + .try_into() + .expect("heading is missing equals sign") } } diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index 9bee73ae3..8e62424f0 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -126,7 +126,7 @@ impl Category { _ => Some(Category::Operator), }, NodeKind::EnumNumbering(_) => Some(Category::List), - NodeKind::Linebreak(_) => Some(Category::Shortcut), + NodeKind::Linebreak { .. } => Some(Category::Shortcut), NodeKind::NonBreakingSpace => Some(Category::Shortcut), NodeKind::Shy => Some(Category::Shortcut), NodeKind::EnDash => Some(Category::Shortcut), @@ -206,7 +206,7 @@ impl Category { NodeKind::Markup(_) => None, NodeKind::Space(_) => None, NodeKind::Text(_) => None, - NodeKind::Quote(_) => None, + NodeKind::Quote { .. } => None, NodeKind::List => None, NodeKind::Enum => None, NodeKind::CodeBlock => None, diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index 2272b3e03..d21597ffd 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -588,9 +588,8 @@ pub enum NodeKind { Space(usize), /// A consecutive non-markup string. Text(EcoString), - /// A forced line break. If `true` (`\`), the preceding line can still be - /// justified, if `false` (`\+`) not. - Linebreak(bool), + /// A forced line break: `\` or `\+` if justified. + Linebreak { justified: bool }, /// A non-breaking space: `~`. NonBreakingSpace, /// A soft hyphen: `-?`. @@ -601,8 +600,8 @@ pub enum NodeKind { EmDash, /// An ellipsis: `...`. Ellipsis, - /// A smart quote: `'` (`false`) or `"` (true). - Quote(bool), + /// A smart quote: `'` or `"`. + Quote { double: bool }, /// A slash and the letter "u" followed by a hexadecimal unicode entity /// enclosed in curly braces: `\u{1F5FA}`. Escape(char), @@ -773,13 +772,13 @@ impl NodeKind { pub fn only_in_mode(&self) -> Option { match self { Self::Markup(_) - | Self::Linebreak(_) + | Self::Linebreak { .. } | Self::Text(_) | Self::NonBreakingSpace | Self::EnDash | Self::EmDash | Self::Ellipsis - | Self::Quote(_) + | Self::Quote { .. } | Self::Escape(_) | Self::Strong | Self::Emph @@ -867,16 +866,16 @@ impl NodeKind { Self::Markup(_) => "markup", Self::Space(2 ..) => "paragraph break", Self::Space(_) => "space", - Self::Linebreak(false) => "linebreak", - Self::Linebreak(true) => "justified linebreak", + Self::Linebreak { justified: false } => "linebreak", + Self::Linebreak { justified: true } => "justified linebreak", Self::Text(_) => "text", Self::NonBreakingSpace => "non-breaking space", Self::Shy => "soft hyphen", Self::EnDash => "en dash", Self::EmDash => "em dash", Self::Ellipsis => "ellipsis", - Self::Quote(false) => "single quote", - Self::Quote(true) => "double quote", + Self::Quote { double: false } => "single quote", + Self::Quote { double: true } => "double quote", Self::Escape(_) => "escape sequence", Self::Strong => "strong content", Self::Emph => "emphasized content", @@ -993,14 +992,14 @@ impl Hash for NodeKind { Self::From => {} Self::Markup(c) => c.hash(state), Self::Space(n) => n.hash(state), - Self::Linebreak(s) => s.hash(state), + Self::Linebreak { justified } => justified.hash(state), Self::Text(s) => s.hash(state), Self::NonBreakingSpace => {} Self::Shy => {} Self::EnDash => {} Self::EmDash => {} Self::Ellipsis => {} - Self::Quote(d) => d.hash(state), + Self::Quote { double } => double.hash(state), Self::Escape(c) => c.hash(state), Self::Strong => {} Self::Emph => {} diff --git a/tests/ref/code/include.png b/tests/ref/code/include.png index 001d7d1ea..d3d660328 100644 Binary files a/tests/ref/code/include.png and b/tests/ref/code/include.png differ diff --git a/tests/ref/coma.png b/tests/ref/coma.png index f9d308908..817c756ab 100644 Binary files a/tests/ref/coma.png and b/tests/ref/coma.png differ diff --git a/tests/ref/layout/align.png b/tests/ref/layout/align.png index 5dde0cef8..9b6e268cd 100644 Binary files a/tests/ref/layout/align.png and b/tests/ref/layout/align.png differ diff --git a/tests/ref/layout/pad.png b/tests/ref/layout/pad.png index c2906cef8..7c27bd268 100644 Binary files a/tests/ref/layout/pad.png and b/tests/ref/layout/pad.png differ diff --git a/tests/ref/layout/place.png b/tests/ref/layout/place.png index 7900f95f7..6fc117063 100644 Binary files a/tests/ref/layout/place.png and b/tests/ref/layout/place.png differ diff --git a/tests/ref/layout/stack-1.png b/tests/ref/layout/stack-1.png index 106cc7919..167fd84cf 100644 Binary files a/tests/ref/layout/stack-1.png and b/tests/ref/layout/stack-1.png differ diff --git a/tests/ref/math/basic.png b/tests/ref/math/basic.png index 2c1dd3244..381e92c46 100644 Binary files a/tests/ref/math/basic.png and b/tests/ref/math/basic.png differ diff --git a/tests/ref/structure/attach.png b/tests/ref/structure/attach.png new file mode 100644 index 000000000..7082a4758 Binary files /dev/null and b/tests/ref/structure/attach.png differ diff --git a/tests/ref/structure/heading.png b/tests/ref/structure/heading.png index 693ae7637..3e12b2cf4 100644 Binary files a/tests/ref/structure/heading.png and b/tests/ref/structure/heading.png differ diff --git a/tests/ref/structure/list.png b/tests/ref/structure/list.png index ac90f4b20..7a60e1e55 100644 Binary files a/tests/ref/structure/list.png and b/tests/ref/structure/list.png differ diff --git a/tests/ref/style/set-site.png b/tests/ref/style/set-site.png index 408e44bf0..024f7c3e9 100644 Binary files a/tests/ref/style/set-site.png and b/tests/ref/style/set-site.png differ diff --git a/tests/ref/style/show.png b/tests/ref/style/show.png index 0f1a16a32..9539e4965 100644 Binary files a/tests/ref/style/show.png and b/tests/ref/style/show.png differ diff --git a/tests/ref/text/bidi.png b/tests/ref/text/bidi.png index f31bdf0a6..6f4c8acb3 100644 Binary files a/tests/ref/text/bidi.png and b/tests/ref/text/bidi.png differ diff --git a/tests/ref/text/indent.png b/tests/ref/text/indent.png index 42f05cfb6..09d8e68df 100644 Binary files a/tests/ref/text/indent.png and b/tests/ref/text/indent.png differ diff --git a/tests/ref/text/par.png b/tests/ref/text/par.png index 19f28b813..ae00ab45f 100644 Binary files a/tests/ref/text/par.png and b/tests/ref/text/par.png differ diff --git a/tests/ref/text/raw.png b/tests/ref/text/raw.png index 64402daee..e13293a6f 100644 Binary files a/tests/ref/text/raw.png and b/tests/ref/text/raw.png differ diff --git a/tests/typ/code/ops-invalid.typ b/tests/typ/code/ops-invalid.typ index 1506d9c4f..4e7fdb040 100644 --- a/tests/typ/code/ops-invalid.typ +++ b/tests/typ/code/ops-invalid.typ @@ -29,6 +29,10 @@ // Error: 2-18 cannot apply '<=' to relative length and ratio {30% + 1pt <= 40%} +--- +// Error: 2-13 cannot apply '<=' to length and length +{1em <= 10pt} + --- // Special messages for +, -, * and /. // Error: 03-10 cannot add integer and string diff --git a/tests/typ/code/ops.typ b/tests/typ/code/ops.typ index 53cf488e5..1f2867bc8 100644 --- a/tests/typ/code/ops.typ +++ b/tests/typ/code/ops.typ @@ -151,6 +151,8 @@ #test(45deg < 1rad, true) #test(10% < 20%, true) #test(50% < 40% + 0pt, false) +#test(40% + 0pt < 50% + 0pt, true) +#test(1em < 2em, true) --- // Test assignment operators. diff --git a/tests/typ/coma.typ b/tests/typ/coma.typ index 92d5695f1..0e228d148 100644 --- a/tests/typ/coma.typ +++ b/tests/typ/coma.typ @@ -6,10 +6,11 @@ Sekretariat MA \ Dr. Max Mustermann \ Ola Nordmann, John Doe -#v(2mm) +#v(3mm) #align(center)[ - ==== 3. Übungsblatt Computerorientierte Mathematik II #v(1mm) - *Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(1mm) + #set par(leading: 3mm) + #text(1.2em)[*3. Übungsblatt Computerorientierte Mathematik II*] \ + *Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) \ *Alle Antworten sind zu beweisen.* ] diff --git a/tests/typ/graphics/transform.typ b/tests/typ/graphics/transform.typ index 82ee13904..e06793060 100644 --- a/tests/typ/graphics/transform.typ +++ b/tests/typ/graphics/transform.typ @@ -6,7 +6,7 @@ #let tex = [{ [T] h(-0.14 * size) - move(y: 0.22 * size)[E] + move(dy: 0.22 * size)[E] h(-0.12 * size) [X] }] @@ -14,11 +14,11 @@ #let xetex = { [X] h(-0.14 * size) - scale(x: -100%, move(y: 0.26 * size)[E]) + scale(x: -100%, move(dy: 0.26 * size)[E]) h(-0.14 * size) [T] h(-0.14 * size) - move(y: 0.26 * size)[E] + move(dy: 0.26 * size)[E] h(-0.12 * size) [X] } diff --git a/tests/typ/layout/pagebreak.typ b/tests/typ/layout/pagebreak.typ index df3b04238..349a2fc20 100644 --- a/tests/typ/layout/pagebreak.typ +++ b/tests/typ/layout/pagebreak.typ @@ -13,14 +13,14 @@ #pagebreak() --- -// Two text bodies separated with and surrounded by soft pagebreaks. +// Two text bodies separated with and surrounded by weak pagebreaks. // Should result in two aqua-colored pages. #set page(fill: aqua) -#pagebreak(soft: true) +#pagebreak(weak: true) First -#pagebreak(soft: true) +#pagebreak(weak: true) Second -#pagebreak(soft: true) +#pagebreak(weak: true) --- // Test a combination of pagebreaks, styled pages and pages with bodies. @@ -34,12 +34,12 @@ Third Fif[#set page();th] --- -// Test hard and soft pagebreak followed by page with body. +// Test hard and weak pagebreak followed by page with body. // Should result in three navy-colored pages. #set page(fill: navy) #set text(fill: white) First #pagebreak() #page[Second] -#pagebreak(soft: true) +#pagebreak(weak: true) #page[Third] diff --git a/tests/typ/layout/place.typ b/tests/typ/layout/place.typ index 527e0559e..95049bdc6 100644 --- a/tests/typ/layout/place.typ +++ b/tests/typ/layout/place.typ @@ -16,14 +16,14 @@ the line breaks still had to be inserted manually. place(right, dy: 1.5pt)[ABC], rect(fill: conifer, height: 10pt, width: 80%), rect(fill: forest, height: 10pt, width: 100%), + 10pt, + block[ + #place(center, dx: -7pt, dy: -5pt)[Hello] + #place(center, dx: 7pt, dy: 5pt)[Hello] + Hello #h(1fr) Hello + ] ) -#block[ - #place(center, dx: -7pt, dy: -5pt)[Hello] - #place(center, dx: 7pt, dy: 5pt)[Hello] - Hello #h(1fr) Hello -] - --- // Test how the placed node interacts with paragraph spacing around it. #set page("a8", height: 60pt) diff --git a/tests/typ/layout/spacing.typ b/tests/typ/layout/spacing.typ index 378c11b8b..eb0bd39e5 100644 --- a/tests/typ/layout/spacing.typ +++ b/tests/typ/layout/spacing.typ @@ -1,8 +1,8 @@ // Test the `h` and `v` functions. --- -// Linebreak and v(0pt) are equivalent. -#box[A \ B] #box[A #v(0pt) B] +// Linebreak and leading-sized weak spacing are equivalent. +#box[A \ B] #box[A #v(0.65em, weak: true) B] // Eating up soft spacing. Inv#h(0pt)isible diff --git a/tests/typ/layout/stack-1.typ b/tests/typ/layout/stack-1.typ index eee1f4d12..19a00de59 100644 --- a/tests/typ/layout/stack-1.typ +++ b/tests/typ/layout/stack-1.typ @@ -18,25 +18,17 @@ #set page(width: 50pt, margins: 0pt) #stack(dir: btt, ..items) ---- -// Test RTL alignment. -#set page(width: 50pt, margins: 5pt) -#set text(8pt) -#stack(dir: rtl, - align(center, [A]), - align(left, [B]), - [C], -) - --- // Test spacing. #set page(width: 50pt, margins: 0pt) -#set par(leading: 5pt) #let x = square(size: 10pt, fill: eastern) -#stack(dir: rtl, spacing: 5pt, x, x, x) -#stack(dir: ltr, x, 20%, x, 20%, x) -#stack(dir: ltr, spacing: 5pt, x, x, 7pt, 3pt, x) +#stack( + spacing: 5pt, + stack(dir: rtl, spacing: 5pt, x, x, x), + stack(dir: ltr, x, 20%, x, 20%, x), + stack(dir: ltr, spacing: 5pt, x, x, 7pt, 3pt, x), +) --- // Test overflow. @@ -45,3 +37,15 @@ rect(width: 40pt, height: 20pt, fill: conifer), rect(width: 30pt, height: 13pt, fill: forest), )) + +--- +// Test aligning things in RTL stack with align function & fr units. +#set page(width: 50pt, margins: 5pt) +#set text(8pt) +#stack(dir: rtl, 1fr, [A], 1fr, [B], [C]) +#v(5pt) +#stack(dir: rtl, + align(center, [A]), + align(left, [B]), + [C], +) diff --git a/tests/typ/structure/attach.typ b/tests/typ/structure/attach.typ new file mode 100644 index 000000000..c6d3c28c3 --- /dev/null +++ b/tests/typ/structure/attach.typ @@ -0,0 +1,56 @@ +// Test list attaching. + +--- +// Test basic attached list. +Attached to: +- the bottom +- of the paragraph + +Next paragraph. + +--- +// Test attached list without parbreak after it. +// Ensures the par spacing is used below by setting +// super high around spacing. +#set list(around: 100pt) +Hello +- A +World +- B + +--- +// Test non-attached list followed by attached list, +// separated by only word. +Hello + +- A + +World +- B + +--- +// Test not-attached tight list. +#set list(around: 15pt) +Hello +- A +World + +- B +- C + +More. + +--- +// Test that wide lists cannot be attached ... +#set list(around: 15pt, spacing: 15pt) +Hello +- A + +- B +World + +--- +// ... unless really forced to. +Hello +#list(attached: true, tight: false)[A][B] +World diff --git a/tests/typ/structure/enum.typ b/tests/typ/structure/enum.typ index c4d178ce5..b1045ee2e 100644 --- a/tests/typ/structure/enum.typ +++ b/tests/typ/structure/enum.typ @@ -39,7 +39,8 @@ // Test label closure. #enum( start: 4, - spacing: -3pt, + spacing: 0.65em - 3pt, + tight: false, label: n => text(fill: (red, green, blue)(mod(n, 3)), [#upper(letter(n))]), [Red], [Green], [Blue], ) diff --git a/tests/typ/structure/heading.typ b/tests/typ/structure/heading.typ index f4ba5f33e..de95c35b4 100644 --- a/tests/typ/structure/heading.typ +++ b/tests/typ/structure/heading.typ @@ -1,14 +1,13 @@ // Test headings. --- -// Different number of hashtags. +// Different number of equals signs. -// Valid levels. = Level 1 -=== Level 2 -====== Level 6 +== Level 2 +=== Level 3 -// At some point, it should stop shrinking. +// After three, it stops shrinking. =========== Level 11 --- diff --git a/tests/typ/structure/list.typ b/tests/typ/structure/list.typ index 52cd51beb..77e153c76 100644 --- a/tests/typ/structure/list.typ +++ b/tests/typ/structure/list.typ @@ -2,21 +2,16 @@ --- _Shopping list_ -#list[Apples][Potatoes][Juice] - ---- -Tightly -- surrounded -- by two -paragraphs. +#list(attached: true)[Apples][Potatoes][Juice] --- - First level. - Second level. - There are multiple paragraphs. + - Third level. + Still the same bullet point. - Still level 2. diff --git a/tests/typ/style/closure.typ b/tests/typ/style/closure.typ index 226264723..cd1f87df1 100644 --- a/tests/typ/style/closure.typ +++ b/tests/typ/style/closure.typ @@ -3,6 +3,7 @@ --- #set heading( size: 10pt, + around: 0.65em, fill: lvl => if even(lvl) { red } else { blue }, ) diff --git a/tests/typ/style/set-site.typ b/tests/typ/style/set-site.typ index 8ee8a5fd0..b49d1027d 100644 --- a/tests/typ/style/set-site.typ +++ b/tests/typ/style/set-site.typ @@ -11,7 +11,7 @@ Hello *{x}* #let fruit = [ - Apple - Orange - #list(body-indent: 10pt, [Pear]) + #list(body-indent: 20pt, [Pear]) ] - Fruit @@ -22,7 +22,7 @@ Hello *{x}* --- // Test that that par spacing and text style are respected from // the outside, but the more specific fill is respected. -#set par(spacing: 0pt) +#set par(spacing: 4pt) #set text(style: "italic", fill: eastern) #let x = [And the forest #parbreak() lay silent!] #text(fill: forest, x) diff --git a/tests/typ/style/show.typ b/tests/typ/style/show.typ index 9aabfb34b..a71b8df20 100644 --- a/tests/typ/style/show.typ +++ b/tests/typ/style/show.typ @@ -29,7 +29,7 @@ Some more text. Another text. --- -#set heading(size: 1em, strong: false, block: false) +#set heading(size: 1em, strong: false, around: none) #show _: heading as [B] A [= Heading] C diff --git a/tests/typ/text/indent.typ b/tests/typ/text/indent.typ index 1b48851bc..897e360cc 100644 --- a/tests/typ/text/indent.typ +++ b/tests/typ/text/indent.typ @@ -1,7 +1,7 @@ // Test paragraph indent. --- -#set par(indent: 12pt, leading: 5pt, spacing: 0pt) +#set par(indent: 12pt, leading: 5pt) #set heading(size: 10pt, above: 8pt) The first paragraph has no indent. @@ -26,3 +26,11 @@ starts a paragraph without indent. دع النص يمطر عليك ثم يصبح النص رطبًا وقابل للطرق ويبدو المستند رائعًا. + + +--- +// This is madness. +#set par(indent: 12pt, spacing-and-indent: true) +Why would anybody ever ... + +... want spacing and indent? diff --git a/tests/typ/text/knuth.typ b/tests/typ/text/knuth.typ index 5adeee91a..593497384 100644 --- a/tests/typ/text/knuth.typ +++ b/tests/typ/text/knuth.typ @@ -15,8 +15,9 @@ #let column(title, linebreaks, hyphenate) = { rect(width: 132pt, fill: rgb("eee"))[ - #strong(title) - #par(linebreaks: linebreaks, text(hyphenate: hyphenate, story)) + #set par(linebreaks: linebreaks) + #set text(hyphenate: hyphenate) + #strong(title) \ #story ] } diff --git a/tests/typ/text/link.typ b/tests/typ/text/link.typ index 99037ee33..ad5381877 100644 --- a/tests/typ/text/link.typ +++ b/tests/typ/text/link.typ @@ -25,11 +25,11 @@ You could also make the #set page(height: 60pt) #set link(underline: false) #let mylink = link("https://typst.app/")[LINK] -My cool #move(x: 0.7cm, y: 0.7cm, rotate(10deg, scale(200%, mylink))) +My cool #move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))) --- // Link containing a block. #link("https://example.com/", underline: false, block[ My cool rhino - #move(x: 10pt, image("../../res/rhino.png", width: 1cm)) + #move(dx: 10pt, image("../../res/rhino.png", width: 1cm)) ]) diff --git a/tests/typ/text/par.typ b/tests/typ/text/par.typ index 6b7c0f59a..64a2dd175 100644 --- a/tests/typ/text/par.typ +++ b/tests/typ/text/par.typ @@ -6,36 +6,45 @@ To the right! Where the sunlight peeks behind the mountain. --- -// Test that explicit paragraph break respects active styles. -#set par(spacing: 0pt) -[#set par(spacing: 100pt);First] - -[#set par(spacing: 100pt);Second] -#set par(spacing: 13.5pt) - -Third - ---- -// Test that paragraph spacing uses correct set rule. -Hello - -#set par(spacing: 100pt) -World -#set par(spacing: 0pt, leading: 0pt) - -You - ---- -// Test that paragraphs break due to incompatibility has correct spacing. -A #set par(spacing: 0pt, leading: 0pt); B #parbreak() C - ---- -// Test weird metrics. -#set par(spacing: 1em, leading: 0pt) +// Test changing leading and spacing. +#set par(spacing: 1em, leading: 2pt) But, soft! what light through yonder window breaks? It is the east, and Juliet is the sun. +--- +// Test that largest paragraph spacing wins. +#set par(spacing: 2.5pt) +[#set par(spacing: 15pt);First] +[#set par(spacing: 7.5pt);Second] +Third + +Fourth + +--- +// Test that paragraph spacing loses against block spacing. +#set par(spacing: 100pt) +#set table(around: 5pt) +Hello +#table(columns: 4, secondary: silver)[A][B][C][D] + +--- +// While we're at it, test the larger block spacing wins. +#set raw(around: 15pt) +#set math(around: 7.5pt) +#set list(around: 2.5pt) +#set par(spacing: 0pt) + +```rust +fn main() {} +``` + +$[ x + y = z ]$ + +- List + +Paragraph + --- // Error: 17-20 must be horizontal #set par(align: top) diff --git a/tests/typ/text/raw.typ b/tests/typ/text/raw.typ index 0e053a9b3..33b08568d 100644 --- a/tests/typ/text/raw.typ +++ b/tests/typ/text/raw.typ @@ -6,17 +6,17 @@ --- // Typst syntax inside. -`#let x = 1` \ -`#f(1)` +```typ #let x = 1``` \ +```typ #f(1)``` --- // Multiline block splits paragraphs. -First +Text +```rust +fn code() {} ``` -Second -``` -Third +Text --- // Lots of backticks inside. diff --git a/tests/typ/utility/blind.typ b/tests/typ/utility/blind.typ index 7d1cb9694..17452decf 100644 --- a/tests/typ/utility/blind.typ +++ b/tests/typ/utility/blind.typ @@ -2,14 +2,14 @@ --- // Test basic call. -#lipsum(19) +#lorem(19) --- // Test custom paragraphs with user code. #set text(8pt) { - let sentences = lipsum(59) + let sentences = lorem(59) .split(".") .filter(s => s != "") .map(s => s + ".") @@ -28,5 +28,5 @@ } --- -// Error: 8-10 missing argument: number of words -#lipsum() +// Error: 7-9 missing argument: number of words +#lorem() diff --git a/tools/support/typst.tmLanguage.json b/tools/support/typst.tmLanguage.json index ef7806c86..e1dd0ba14 100644 --- a/tools/support/typst.tmLanguage.json +++ b/tools/support/typst.tmLanguage.json @@ -75,7 +75,7 @@ { "name": "markup.heading.typst", "contentName": "entity.name.section.typst", - "begin": "^\\s*={1,6}\\s+", + "begin": "^\\s*=+\\s+", "end": "\n", "beginCaptures": { "0": { "name": "punctuation.definition.heading.typst" } }, "patterns": [{ "include": "#markup" }]