New block spacing model

This commit is contained in:
Laurenz 2022-04-30 14:12:28 +02:00
parent f7c67cde72
commit f9e115daf5
72 changed files with 795 additions and 393 deletions

View File

@ -53,38 +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(|| { let set = set.unwrap_or_else(|| generate_set(&properties));
let sets = properties.into_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 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<StyleMap> {
let mut styles = StyleMap::new();
#(#sets)*
Ok(styles)
}
}
});
let showable = match stream.to_string().as_str() { let showable = match stream.to_string().as_str() {
"" => false, "" => false,
@ -204,7 +173,7 @@ fn process_const(
}; };
} else if property.resolve { } else if property.resolve {
get = quote! { get = quote! {
let value = values.next().cloned().unwrap_or(#default); let value = values.next().cloned().unwrap_or_else(|| #default);
model::Resolve::resolve(value, chain) model::Resolve::resolve(value, chain)
}; };
} else if property.fold { } else if property.fold {
@ -277,19 +246,24 @@ struct Property {
name: Ident, name: Ident,
hidden: bool, hidden: bool,
referenced: bool, referenced: bool,
shorthand: bool, shorthand: Option<Shorthand>,
variadic: bool, variadic: bool,
resolve: bool, resolve: bool,
fold: bool, fold: bool,
} }
enum Shorthand {
Positional,
Named(Ident),
}
/// Parse a style property attribute. /// Parse a style property attribute.
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, hidden: false,
referenced: false, referenced: false,
shorthand: false, shorthand: None,
variadic: false, variadic: false,
resolve: false, resolve: false,
fold: false, fold: false,
@ -301,11 +275,26 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
.position(|attr| attr.path.get_ident().map_or(false, |name| name == "property")) .position(|attr| attr.path.get_ident().map_or(false, |name| name == "property"))
{ {
let attr = item.attrs.remove(idx); let attr = item.attrs.remove(idx);
for token in attr.parse_args::<TokenStream2>()? { let mut stream = attr.parse_args::<TokenStream2>()?.into_iter().peekable();
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, "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, "referenced" => property.referenced = true,
"variadic" => property.variadic = true, "variadic" => property.variadic = true,
"resolve" => property.resolve = true, "resolve" => property.resolve = true,
@ -319,7 +308,7 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
} }
let span = property.name.span(); let span = property.name.span();
if property.shorthand && property.variadic { if property.shorthand.is_some() && property.variadic {
return Err(Error::new( return Err(Error::new(
span, span,
"shorthand and variadic are mutually exclusive", "shorthand and variadic are mutually exclusive",
@ -335,3 +324,58 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
Ok(property) 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<StyleMap> {
let mut styles = StyleMap::new();
#(#bindings)*
#(#sets)*
Ok(styles)
}
}
}

View File

@ -101,9 +101,9 @@ impl Eval for MarkupNode {
Ok(match self { Ok(match self {
Self::Space => Content::Space, Self::Space => Content::Space,
Self::Parbreak => Content::Parbreak, Self::Parbreak => Content::Parbreak,
Self::Linebreak(justified) => Content::Linebreak(*justified), &Self::Linebreak { justified } => Content::Linebreak { justified },
Self::Text(text) => Content::Text(text.clone()), 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::Strong(strong) => strong.eval(ctx, scp)?,
Self::Emph(emph) => emph.eval(ctx, scp)?, Self::Emph(emph) => emph.eval(ctx, scp)?,
Self::Raw(raw) => raw.eval(ctx, scp)?, Self::Raw(raw) => raw.eval(ctx, scp)?,

View File

@ -316,9 +316,10 @@ pub fn compare(lhs: &Value, rhs: &Value) -> Option<Ordering> {
(Bool(a), Bool(b)) => a.partial_cmp(b), (Bool(a), Bool(b)) => a.partial_cmp(b),
(Int(a), Int(b)) => a.partial_cmp(b), (Int(a), Int(b)) => a.partial_cmp(b),
(Float(a), Float(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), (Length(a), Length(b)) => a.partial_cmp(b),
(Angle(a), Angle(b)) => a.partial_cmp(b),
(Ratio(a), Ratio(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), (Fraction(a), Fraction(b)) => a.partial_cmp(b),
(Str(a), Str(b)) => a.partial_cmp(b), (Str(a), Str(b)) => a.partial_cmp(b),

View File

@ -213,6 +213,8 @@ impl PartialOrd for RawLength {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.em.is_zero() && other.em.is_zero() { if self.em.is_zero() && other.em.is_zero() {
self.length.partial_cmp(&other.length) self.length.partial_cmp(&other.length)
} else if self.length.is_zero() && other.length.is_zero() {
self.em.partial_cmp(&other.em)
} else { } else {
None None
} }

View File

@ -408,12 +408,25 @@ macro_rules! dynamic {
/// Make a type castable from a value. /// Make a type castable from a value.
macro_rules! castable { 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<Self> {
<$inner>::cast(value).map(Self)
}
}
};
( (
$type:ty, $type:ty,
Expected: $expected:expr, Expected: $expected:expr,
$($pattern:pat => $out:expr,)* $($pattern:pat => $out:expr,)*
$(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)* $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)*
) => { ) => {
#[allow(unreachable_patterns)]
impl $crate::eval::Cast<$crate::eval::Value> for $type { impl $crate::eval::Cast<$crate::eval::Value> for $type {
fn is(value: &$crate::eval::Value) -> bool { fn is(value: &$crate::eval::Value) -> bool {
#[allow(unused_variables)] #[allow(unused_variables)]
@ -602,10 +615,14 @@ castable! {
castable! { castable! {
NonZeroUsize, NonZeroUsize,
Expected: "positive integer", Expected: "positive integer",
Value::Int(int) => Value::Int(int) Value::Int(int) => int
.cast::<usize>()?
.try_into() .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! { castable! {

View File

@ -68,6 +68,18 @@ impl<T: Numeric> From<Ratio> for Relative<T> {
} }
} }
impl<T: Numeric + PartialOrd> PartialOrd for Relative<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
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<T: Numeric> Neg for Relative<T> { impl<T: Numeric> Neg for Relative<T> {
type Output = Self; type Output = Self;

View File

@ -26,7 +26,6 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to stroke the shape. /// How to stroke the shape.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto; pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto;
/// How much to pad the shape's content. /// How much to pad the shape's content.
pub const PADDING: Relative<RawLength> = Relative::zero(); pub const PADDING: Relative<RawLength> = Relative::zero();

View File

@ -13,8 +13,8 @@ pub struct MoveNode {
#[node] #[node]
impl MoveNode { impl MoveNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let dx = args.named("x")?.unwrap_or_default(); let dx = args.named("dx")?.unwrap_or_default();
let dy = args.named("y")?.unwrap_or_default(); let dy = args.named("dy")?.unwrap_or_default();
Ok(Content::inline(Self { Ok(Content::inline(Self {
delta: Spec::new(dx, dy), delta: Spec::new(dx, dy),
child: args.expect("body")?, child: args.expect("body")?,

View File

@ -13,9 +13,15 @@ pub struct AlignNode {
#[node] #[node]
impl AlignNode { impl AlignNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let aligns: Spec<_> = args.find()?.unwrap_or_default(); let aligns: Spec<Option<RawAlign>> = args.find()?.unwrap_or_default();
let body: LayoutNode = args.expect("body")?; let body: Content = args.expect("body")?;
Ok(Content::block(body.aligned(aligns))) 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)),
})
} }
} }

View File

@ -106,7 +106,8 @@ pub struct ColbreakNode;
#[node] #[node]
impl ColbreakNode { impl ColbreakNode {
fn construct(_: &mut Context, _: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
Ok(Content::Colbreak) let weak = args.named("weak")?.unwrap_or(false);
Ok(Content::Colbreak { weak })
} }
} }

View File

@ -1,3 +1,5 @@
use std::cmp::Ordering;
use super::{AlignNode, PlaceNode, Spacing}; use super::{AlignNode, PlaceNode, Spacing};
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::ParNode; use crate::library::text::ParNode;
@ -10,18 +12,14 @@ use crate::library::text::ParNode;
pub struct FlowNode(pub StyleVec<FlowChild>); pub struct FlowNode(pub StyleVec<FlowChild>);
/// A child of a flow node. /// A child of a flow node.
#[derive(Hash)] #[derive(Hash, PartialEq)]
pub enum FlowChild { pub enum FlowChild {
/// Leading between other children.
Leading,
/// A paragraph / block break.
Parbreak,
/// A column / region break.
Colbreak,
/// Vertical spacing between other children. /// Vertical spacing between other children.
Spacing(Spacing), Spacing(Spacing),
/// An arbitrary block-level node. /// An arbitrary block-level node.
Node(LayoutNode), Node(LayoutNode),
/// A column / region break.
Colbreak,
} }
impl Layout for FlowNode { impl Layout for FlowNode {
@ -36,25 +34,15 @@ impl Layout for FlowNode {
for (child, map) in self.0.iter() { for (child, map) in self.0.iter() {
let styles = map.chain(&styles); let styles = map.chain(&styles);
match child { 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) => { FlowChild::Spacing(kind) => {
layouter.layout_spacing(*kind, styles); layouter.layout_spacing(*kind, styles);
} }
FlowChild::Node(ref node) => { FlowChild::Node(ref node) => {
layouter.layout_node(ctx, node, styles)?; layouter.layout_node(ctx, node, styles)?;
} }
FlowChild::Colbreak => {
layouter.finish_region();
}
} }
} }
@ -72,11 +60,18 @@ impl Debug for FlowNode {
impl Debug for FlowChild { impl Debug for FlowChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::Leading => f.pad("Leading"),
Self::Parbreak => f.pad("Parbreak"),
Self::Colbreak => f.pad("Colbreak"),
Self::Spacing(kind) => write!(f, "{:?}", kind), Self::Spacing(kind) => write!(f, "{:?}", kind),
Self::Node(node) => node.fmt(f), Self::Node(node) => node.fmt(f),
Self::Colbreak => f.pad("Colbreak"),
}
}
}
impl PartialOrd for FlowChild {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b),
_ => None,
} }
} }
} }

View File

@ -13,12 +13,12 @@ pub struct PadNode {
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.find()?;
let hor = args.named("horizontal")?; let x = args.named("x")?;
let ver = args.named("vertical")?; let y = args.named("y")?;
let left = args.named("left")?.or(hor).or(all).unwrap_or_default(); let left = args.named("left")?.or(x).or(all).unwrap_or_default();
let top = args.named("top")?.or(ver).or(all).unwrap_or_default(); let top = args.named("top")?.or(y).or(all).unwrap_or_default();
let right = args.named("right")?.or(hor).or(all).unwrap_or_default(); let right = args.named("right")?.or(x).or(all).unwrap_or_default();
let bottom = args.named("bottom")?.or(ver).or(all).unwrap_or_default(); let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default();
let body: LayoutNode = args.expect("body")?; let body: LayoutNode = args.expect("body")?;
let padding = Sides::new(left, top, right, bottom); let padding = Sides::new(left, top, right, bottom);
Ok(Content::block(body.padded(padding))) Ok(Content::block(body.padded(padding)))

View File

@ -165,8 +165,8 @@ pub struct PagebreakNode;
#[node] #[node]
impl PagebreakNode { impl PagebreakNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let soft = args.named("soft")?.unwrap_or(false); let weak = args.named("weak")?.unwrap_or(false);
Ok(Content::Pagebreak(soft)) Ok(Content::Pagebreak { weak })
} }
} }

View File

@ -1,4 +1,7 @@
use std::cmp::Ordering;
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::ParNode;
/// Horizontal spacing. /// Horizontal spacing.
pub struct HNode; pub struct HNode;
@ -6,7 +9,9 @@ pub struct HNode;
#[node] #[node]
impl HNode { impl HNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
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] #[node]
impl VNode { impl VNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
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 { pub enum Spacing {
/// Spacing specified in absolute terms and relative to the parent's size. /// Spacing specified in absolute terms and relative to the parent's size.
Relative(Relative<RawLength>), Relative(Relative<RawLength>),
/// 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), Fractional(Fraction),
} }
@ -42,6 +50,16 @@ impl From<Length> for Spacing {
} }
} }
impl PartialOrd for Spacing {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
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! { castable! {
Spacing, Spacing,
Expected: "relative length or fraction", Expected: "relative length or fraction",
@ -50,3 +68,24 @@ castable! {
Value::Relative(v) => Self::Relative(v), Value::Relative(v) => Self::Relative(v),
Value::Fraction(v) => Self::Fractional(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<RawLength>);
castable!(BlockSpacing: Relative<RawLength>);
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<Ratio> for BlockSpacing {
fn from(ratio: Ratio) -> Self {
Self(ratio.into())
}
}

View File

@ -1,5 +1,6 @@
use super::{AlignNode, Spacing}; use super::{AlignNode, Spacing};
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::ParNode;
/// Arrange nodes and spacing along an axis. /// Arrange nodes and spacing along an axis.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -180,7 +181,16 @@ impl<'a> StackLayouter<'a> {
.downcast::<AlignNode>() .downcast::<AlignNode>()
.and_then(|node| node.aligns.get(self.axis)) .and_then(|node| node.aligns.get(self.axis))
.map(|align| align.resolve(styles)) .map(|align| align.resolve(styles))
.unwrap_or(self.dir.start().into()); .unwrap_or_else(|| {
if let Some(Content::Styled(styled)) = node.downcast::<Content>() {
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 frames = node.layout(ctx, &self.regions, styles)?;
let len = frames.len(); let len = frames.len();

View File

@ -1,5 +1,6 @@
//! Mathematical formulas. //! Mathematical formulas.
use crate::library::layout::BlockSpacing;
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::FontFamily; use crate::library::text::FontFamily;
@ -19,6 +20,13 @@ impl MathNode {
pub const FAMILY: Smart<FontFamily> = pub const FAMILY: Smart<FontFamily> =
Smart::Custom(FontFamily::new("Latin Modern Math")); Smart::Custom(FontFamily::new("Latin Modern Math"));
/// The spacing above display math.
#[property(resolve, shorthand(around))]
pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
/// The spacing below display math.
#[property(resolve, shorthand(around))]
pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
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 {
formula: args.expect("formula")?, formula: args.expect("formula")?,
@ -36,7 +44,11 @@ impl Show for MathNode {
} }
fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> { fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> {
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( fn finalize(
@ -50,12 +62,10 @@ impl Show for MathNode {
map.set_family(family.clone(), styles); map.set_family(family.clone(), styles);
} }
realized = realized.styled_with_map(map);
if self.display { 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))
} }
} }

View File

@ -88,7 +88,7 @@ pub fn new() -> Scope {
std.def_fn("letter", utility::letter); std.def_fn("letter", utility::letter);
std.def_fn("roman", utility::roman); std.def_fn("roman", utility::roman);
std.def_fn("symbol", utility::symbol); std.def_fn("symbol", utility::symbol);
std.def_fn("lipsum", utility::lipsum); std.def_fn("lorem", utility::lorem);
// Predefined colors. // Predefined colors.
std.def_const("black", Color::BLACK); std.def_const("black", Color::BLACK);

View File

@ -1,3 +1,4 @@
use crate::library::layout::BlockSpacing;
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::{FontFamily, TextNode, TextSize, Toggle}; use crate::library::text::{FontFamily, TextNode, TextSize, Toggle};
@ -6,7 +7,7 @@ use crate::library::text::{FontFamily, TextNode, TextSize, Toggle};
pub struct HeadingNode { pub struct HeadingNode {
/// The logical nesting depth of the section, starting from one. In the /// The logical nesting depth of the section, starting from one. In the
/// default style, this controls the text size of the heading. /// default style, this controls the text size of the heading.
pub level: usize, pub level: NonZeroUsize,
/// The heading's contents. /// The heading's contents.
pub body: Content, pub body: Content,
} }
@ -22,8 +23,12 @@ impl HeadingNode {
/// The size of text in the heading. /// The size of text in the heading.
#[property(referenced)] #[property(referenced)]
pub const SIZE: Leveled<TextSize> = Leveled::Mapping(|level| { pub const SIZE: Leveled<TextSize> = Leveled::Mapping(|level| {
let upscale = (1.6 - 0.1 * level as f64).max(0.75); let size = match level.get() {
TextSize(Em::new(upscale).into()) 1 => 1.4,
2 => 1.2,
_ => 1.0,
};
TextSize(Em::new(size).into())
}); });
/// Whether text in the heading is strengthend. /// Whether text in the heading is strengthend.
@ -36,21 +41,24 @@ impl HeadingNode {
#[property(referenced)] #[property(referenced)]
pub const UNDERLINE: Leveled<bool> = Leveled::Value(false); pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
/// The extra padding above the heading. /// The spacing above the heading.
#[property(referenced)] #[property(referenced, shorthand(around))]
pub const ABOVE: Leveled<RawLength> = Leveled::Value(Length::zero().into()); pub const ABOVE: Leveled<Option<BlockSpacing>> = Leveled::Mapping(|level| {
/// The extra padding below the heading. let ratio = match level.get() {
#[property(referenced)] 1 => 1.5,
pub const BELOW: Leveled<RawLength> = Leveled::Value(Length::zero().into()); _ => 1.2,
};
/// Whether the heading is block-level. Some(Ratio::new(ratio).into())
#[property(referenced)] });
pub const BLOCK: Leveled<bool> = Leveled::Value(true); /// The spacing below the heading.
#[property(referenced, shorthand(around))]
pub const BELOW: Leveled<Option<BlockSpacing>> =
Leveled::Value(Some(Ratio::new(0.55).into()));
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 {
body: args.expect("body")?, 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 { impl Show for HeadingNode {
fn encode(&self) -> Dict { fn encode(&self) -> Dict {
dict! { dict! {
"level" => Value::Int(self.level as i64), "level" => Value::Int(self.level.get() as i64),
"body" => Value::Content(self.body.clone()), "body" => Value::Content(self.body.clone()),
} }
} }
fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> { fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> {
Ok(self.body.clone()) Ok(Content::block(self.body.clone()))
} }
fn finalize( fn finalize(
@ -103,11 +111,6 @@ impl Show for HeadingNode {
} }
realized = realized.styled_with_map(map); realized = realized.styled_with_map(map);
if resolve!(Self::BLOCK) {
realized = Content::block(realized);
}
realized = realized.spaced( realized = realized.spaced(
resolve!(Self::ABOVE).resolve(styles), resolve!(Self::ABOVE).resolve(styles),
resolve!(Self::BELOW).resolve(styles), resolve!(Self::BELOW).resolve(styles),
@ -123,19 +126,19 @@ pub enum Leveled<T> {
/// A bare value. /// A bare value.
Value(T), Value(T),
/// A simple mapping from a heading level to a value. /// 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. /// A closure mapping from a heading level to a value.
Func(Func, Span), Func(Func, Span),
} }
impl<T: Cast + Clone> Leveled<T> { impl<T: Cast + Clone> Leveled<T> {
/// Resolve the value based on the level. /// Resolve the value based on the level.
pub fn resolve(&self, ctx: &mut Context, level: usize) -> TypResult<T> { pub fn resolve(&self, ctx: &mut Context, level: NonZeroUsize) -> TypResult<T> {
Ok(match self { Ok(match self {
Self::Value(value) => value.clone(), Self::Value(value) => value.clone(),
Self::Mapping(mapping) => mapping(level), Self::Mapping(mapping) => mapping(level),
Self::Func(func, span) => { 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)? func.call(ctx, args)?.cast().at(*span)?
} }
}) })

View File

@ -2,7 +2,7 @@ use std::fmt::Write;
use unscanny::Scanner; use unscanny::Scanner;
use crate::library::layout::{GridNode, TrackSizing}; use crate::library::layout::{BlockSpacing, GridNode, TrackSizing};
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::ParNode; use crate::library::text::ParNode;
use crate::library::utility::Numbering; use crate::library::utility::Numbering;
@ -12,9 +12,10 @@ use crate::library::utility::Numbering;
pub struct ListNode<const L: ListKind = UNORDERED> { pub struct ListNode<const L: ListKind = UNORDERED> {
/// Where the list starts. /// Where the list starts.
pub start: usize, pub start: usize,
/// If false, there is paragraph spacing between the items, if true /// If true, the items are separated by leading instead of list spacing.
/// there is list spacing between the items.
pub tight: bool, 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. /// The individual bulleted or numbered items.
pub items: StyleVec<ListItem>, pub items: StyleVec<ListItem>,
} }
@ -38,10 +39,6 @@ impl<const L: ListKind> ListNode<L> {
/// How the list is labelled. /// How the list is labelled.
#[property(referenced)] #[property(referenced)]
pub const LABEL: Label = Label::Default; 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. /// The indentation of each item's label.
#[property(resolve)] #[property(resolve)]
pub const INDENT: RawLength = RawLength::zero(); pub const INDENT: RawLength = RawLength::zero();
@ -49,17 +46,21 @@ impl<const L: ListKind> ListNode<L> {
#[property(resolve)] #[property(resolve)]
pub const BODY_INDENT: RawLength = Em::new(0.5).into(); 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<BlockSpacing> = Some(Ratio::one().into());
/// The spacing below the list.
#[property(resolve, shorthand(around))]
pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
/// The spacing between the items of a wide (non-tight) list.
#[property(resolve)] #[property(resolve)]
pub const ABOVE: RawLength = RawLength::zero(); pub const SPACING: BlockSpacing = Ratio::one().into();
/// The extra padding below the list.
#[property(resolve)]
pub const BELOW: RawLength = RawLength::zero();
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 {
start: args.named("start")?.unwrap_or(1), start: args.named("start")?.unwrap_or(1),
tight: args.named("tight")?.unwrap_or(true), tight: args.named("tight")?.unwrap_or(true),
attached: args.named("attached")?.unwrap_or(false),
items: args items: args
.all()? .all()?
.into_iter() .into_iter()
@ -78,6 +79,7 @@ impl<const L: ListKind> Show for ListNode<L> {
dict! { dict! {
"start" => Value::Int(self.start as i64), "start" => Value::Int(self.start as i64),
"tight" => Value::Bool(self.tight), "tight" => Value::Bool(self.tight),
"attached" => Value::Bool(self.attached),
"items" => Value::Array( "items" => Value::Array(
self.items self.items
.items() .items()
@ -103,14 +105,12 @@ impl<const L: ListKind> Show for ListNode<L> {
number += 1; number += 1;
} }
let leading = styles.get(ParNode::LEADING); let gutter = if self.tight {
let spacing = if self.tight { styles.get(ParNode::LEADING)
styles.get(Self::SPACING)
} else { } else {
styles.get(ParNode::SPACING) styles.get(Self::SPACING)
}; };
let gutter = leading + spacing;
let indent = styles.get(Self::INDENT); let indent = styles.get(Self::INDENT);
let body_indent = styles.get(Self::BODY_INDENT); let body_indent = styles.get(Self::BODY_INDENT);
@ -132,7 +132,19 @@ impl<const L: ListKind> Show for ListNode<L> {
styles: StyleChain, styles: StyleChain,
realized: Content, realized: Content,
) -> TypResult<Content> { ) -> TypResult<Content> {
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))
} }
} }

View File

@ -1,4 +1,4 @@
use crate::library::layout::{GridNode, TrackSizing}; use crate::library::layout::{BlockSpacing, GridNode, TrackSizing};
use crate::library::prelude::*; use crate::library::prelude::*;
/// A table of items. /// A table of items.
@ -15,16 +15,24 @@ pub struct TableNode {
#[node(showable)] #[node(showable)]
impl TableNode { impl TableNode {
/// The primary cell fill color. /// The primary cell fill color.
#[property(shorthand(fill))]
pub const PRIMARY: Option<Paint> = None; pub const PRIMARY: Option<Paint> = None;
/// The secondary cell fill color. /// The secondary cell fill color.
#[property(shorthand(fill))]
pub const SECONDARY: Option<Paint> = None; pub const SECONDARY: Option<Paint> = None;
/// How to stroke the cells. /// How to stroke the cells.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const STROKE: Option<RawStroke> = Some(RawStroke::default()); pub const STROKE: Option<RawStroke> = Some(RawStroke::default());
/// How much to pad the cells's content. /// How much to pad the cells's content.
pub const PADDING: Relative<RawLength> = Length::pt(5.0).into(); pub const PADDING: Relative<RawLength> = Length::pt(5.0).into();
/// The spacing above the table.
#[property(resolve, shorthand(around))]
pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
/// The spacing below the table.
#[property(resolve, shorthand(around))]
pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let columns = args.named("columns")?.unwrap_or_default(); let columns = args.named("columns")?.unwrap_or_default();
let rows = args.named("rows")?.unwrap_or_default(); let rows = args.named("rows")?.unwrap_or_default();
@ -40,16 +48,6 @@ impl TableNode {
cells: args.all()?, cells: args.all()?,
})) }))
} }
fn set(args: &mut Args) -> TypResult<StyleMap> {
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 { impl Show for TableNode {
@ -99,4 +97,13 @@ impl Show for TableNode {
cells, cells,
})) }))
} }
fn finalize(
&self,
_: &mut Context,
styles: StyleChain,
realized: Content,
) -> TypResult<Content> {
Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)))
}
} }

View File

@ -24,7 +24,6 @@ impl<const L: DecoLine> DecoNode<L> {
/// tables if `auto`. /// tables if `auto`.
#[property(shorthand, resolve, fold)] #[property(shorthand, resolve, fold)]
pub const STROKE: Smart<RawStroke> = Smart::Auto; pub const STROKE: Smart<RawStroke> = Smart::Auto;
/// Position of the line relative to the baseline, read from the font tables /// Position of the line relative to the baseline, read from the font tables
/// if `auto`. /// if `auto`.
#[property(resolve)] #[property(resolve)]
@ -32,7 +31,6 @@ impl<const L: DecoLine> DecoNode<L> {
/// Amount that the line will be longer or shorter than its associated text. /// Amount that the line will be longer or shorter than its associated text.
#[property(resolve)] #[property(resolve)]
pub const EXTENT: RawLength = RawLength::zero(); pub const EXTENT: RawLength = RawLength::zero();
/// Whether the line skips sections in which it would collide /// Whether the line skips sections in which it would collide
/// with the glyphs. Does not apply to strikethrough. /// with the glyphs. Does not apply to strikethrough.
pub const EVADE: bool = true; pub const EVADE: bool = true;

View File

@ -223,11 +223,7 @@ impl Fold for TextSize {
} }
} }
castable! { castable!(TextSize: RawLength);
TextSize,
Expected: "length",
Value::Length(v) => Self(v),
}
/// Specifies the bottom or top edge of text. /// Specifies the bottom or top edge of text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
@ -290,11 +286,7 @@ impl Resolve for Smart<HorizontalDir> {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Hyphenate(pub bool); pub struct Hyphenate(pub bool);
castable! { castable!(Hyphenate: bool);
Hyphenate,
Expected: "boolean",
Value::Bool(v) => Self(v),
}
impl Resolve for Smart<Hyphenate> { impl Resolve for Smart<Hyphenate> {
type Output = bool; type Output = bool;

View File

@ -1,3 +1,4 @@
use std::cmp::Ordering;
use std::sync::Arc; use std::sync::Arc;
use unicode_bidi::{BidiInfo, Level}; use unicode_bidi::{BidiInfo, Level};
@ -15,12 +16,12 @@ use crate::util::{EcoString, MaybeShared};
pub struct ParNode(pub StyleVec<ParChild>); pub struct ParNode(pub StyleVec<ParChild>);
/// A uniformly styled atomic piece of a paragraph. /// A uniformly styled atomic piece of a paragraph.
#[derive(Hash)] #[derive(Hash, PartialEq)]
pub enum ParChild { pub enum ParChild {
/// A chunk of text. /// A chunk of text.
Text(EcoString), Text(EcoString),
/// A smart quote, may be single (`false`) or double (`true`). /// A single or double smart quote.
Quote(bool), Quote { double: bool },
/// Horizontal spacing between other children. /// Horizontal spacing between other children.
Spacing(Spacing), Spacing(Spacing),
/// An arbitrary inline-level node. /// An arbitrary inline-level node.
@ -34,10 +35,12 @@ impl ParNode {
pub const LEADING: RawLength = Em::new(0.65).into(); pub const LEADING: RawLength = Em::new(0.65).into();
/// The extra spacing between paragraphs. /// The extra spacing between paragraphs.
#[property(resolve)] #[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. /// The indent the first line of a consecutive paragraph should have.
#[property(resolve)] #[property(resolve)]
pub const INDENT: RawLength = RawLength::zero(); 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. /// How to align text and inline objects in their line.
#[property(resolve)] #[property(resolve)]
@ -50,10 +53,13 @@ impl ParNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph // The paragraph constructor is special: It doesn't create a paragraph
// since that happens automatically through markup. Instead, it just // node. Instead, it just ensures that the passed content lives is in a
// lifts the passed body to the block level so that it won't merge with // separate paragraph and styles it.
// adjacent stuff and it styles the contained paragraphs. Ok(Content::sequence(vec![
Ok(Content::Block(args.expect("body")?)) Content::Parbreak,
args.expect("body")?,
Content::Parbreak,
]))
} }
} }
@ -91,13 +97,22 @@ impl Debug for ParChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::Text(text) => write!(f, "Text({:?})", text), 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::Spacing(kind) => write!(f, "{:?}", kind),
Self::Node(node) => node.fmt(f), Self::Node(node) => node.fmt(f),
} }
} }
} }
impl PartialOrd for ParChild {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b),
_ => None,
}
}
}
/// A horizontal alignment. /// A horizontal alignment.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalAlign(pub RawAlign); pub struct HorizontalAlign(pub RawAlign);
@ -169,7 +184,7 @@ pub struct LinebreakNode;
impl LinebreakNode { impl LinebreakNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let justified = args.named("justified")?.unwrap_or(false); 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) Segment::Text(full.len() - prev)
} }
ParChild::Quote(double) => { ParChild::Quote { double } => {
let prev = full.len(); let prev = full.len();
if styles.get(TextNode::SMART_QUOTES) { if styles.get(TextNode::SMART_QUOTES) {
let lang = styles.get(TextNode::LANG); let lang = styles.get(TextNode::LANG);
@ -440,7 +455,7 @@ fn collect<'a>(
let quotes = Quotes::from_lang(lang, region); let quotes = Quotes::from_lang(lang, region);
let peeked = iter.peek().and_then(|(child, _)| match child { let peeked = iter.peek().and_then(|(child, _)| match child {
ParChild::Text(text) => text.chars().next(), ParChild::Text(text) => text.chars().next(),
ParChild::Quote(_) => Some('"'), ParChild::Quote { .. } => Some('"'),
ParChild::Spacing(_) => Some(SPACING_REPLACE), ParChild::Spacing(_) => Some(SPACING_REPLACE),
ParChild::Node(_) => Some(NODE_REPLACE), ParChild::Node(_) => Some(NODE_REPLACE),
}); });

View File

@ -4,6 +4,7 @@ use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet};
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
use super::{FontFamily, Hyphenate, TextNode, Toggle}; use super::{FontFamily, Hyphenate, TextNode, Toggle};
use crate::library::layout::BlockSpacing;
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::source::SourceId; use crate::source::SourceId;
use crate::syntax::{self, RedNode}; use crate::syntax::{self, RedNode};
@ -26,13 +27,20 @@ pub struct RawNode {
#[node(showable)] #[node(showable)]
impl RawNode { impl RawNode {
/// The language to syntax-highlight in.
#[property(referenced)]
pub const LANG: Option<EcoString> = None;
/// The raw text's font family. Just the normal text family if `none`. /// The raw text's font family. Just the normal text family if `none`.
#[property(referenced)] #[property(referenced)]
pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono")); pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono"));
/// The language to syntax-highlight in. /// The spacing above block-level raw.
#[property(referenced)] #[property(resolve, shorthand(around))]
pub const LANG: Option<EcoString> = None; pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
/// The spacing below block-level raw.
#[property(resolve, shorthand(around))]
pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
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 {
@ -59,7 +67,7 @@ impl Show for RawNode {
.unwrap_or(Color::BLACK) .unwrap_or(Color::BLACK)
.into(); .into();
if matches!( let mut realized = if matches!(
lang.map(|s| s.to_lowercase()).as_deref(), lang.map(|s| s.to_lowercase()).as_deref(),
Some("typ" | "typst") Some("typ" | "typst")
) { ) {
@ -72,7 +80,7 @@ impl Show for RawNode {
seq.push(styled(&self.text[range], foreground, style)); seq.push(styled(&self.text[range], foreground, style));
}); });
Ok(Content::sequence(seq)) Content::sequence(seq)
} else if let Some(syntax) = } else if let Some(syntax) =
lang.and_then(|token| SYNTAXES.find_syntax_by_token(&token)) 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); let mut highlighter = HighlightLines::new(syntax, &THEME);
for (i, line) in self.text.lines().enumerate() { for (i, line) in self.text.lines().enumerate() {
if i != 0 { if i != 0 {
seq.push(Content::Linebreak(false)); seq.push(Content::Linebreak { justified: false });
} }
for (style, piece) in highlighter.highlight(line, &SYNTAXES) { for (style, piece) in highlighter.highlight(line, &SYNTAXES) {
@ -88,10 +96,16 @@ impl Show for RawNode {
} }
} }
Ok(Content::sequence(seq)) Content::sequence(seq)
} else { } else {
Ok(Content::Text(self.text.clone())) Content::Text(self.text.clone())
};
if self.block {
realized = Content::block(realized);
} }
Ok(realized)
} }
fn finalize( fn finalize(
@ -109,13 +123,11 @@ impl Show for RawNode {
map.set_family(family.clone(), styles); map.set_family(family.clone(), styles);
} }
realized = realized.styled_with_map(map);
if self.block { 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))
} }
} }

View File

@ -3,7 +3,7 @@ use lipsum::lipsum_from_seed;
use crate::library::prelude::*; use crate::library::prelude::*;
/// Create blind text. /// Create blind text.
pub fn lipsum(_: &mut Context, args: &mut Args) -> TypResult<Value> { pub fn lorem(_: &mut Context, args: &mut Args) -> TypResult<Value> {
let words: usize = args.expect("number of words")?; let words: usize = args.expect("number of words")?;
Ok(Value::Str(lipsum_from_seed(words, 97).into())) Ok(Value::Str(lipsum_from_seed(words, 97).into()))
} }

View File

@ -35,26 +35,37 @@ impl<'a, T> CollapsingBuilder<'a, T> {
} }
/// Can only exist when there is at least one supportive item to its left /// 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 /// and to its right, with no destructive items in between. There may be
/// its left and no destructive items in between to its right. There may be
/// ignorant items in between in both directions. /// ignorant items in between in both directions.
pub fn weak(&mut self, item: T, strength: u8, styles: StyleChain<'a>) { ///
if self.last != Last::Destructive { /// Between weak items, there may be at least one per layer and among the
if self.last == Last::Weak { /// candidates the strongest one (smallest `weakness`) wins. When tied,
if let Some(i) = self /// the one that compares larger through `PartialOrd` wins.
.staged pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8)
.iter() where
.position(|(.., prev)| prev.map_or(false, |p| p < strength)) T: PartialOrd,
{ {
self.staged.remove(i); if self.last == Last::Destructive {
} else { return;
return;
}
}
self.staged.push((item, styles, Some(strength)));
self.last = Last::Weak;
} }
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. /// 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 /// Push the staged items, filtering out weak items if `supportive` is
/// false. /// false.
fn flush(&mut self, supportive: bool) { fn flush(&mut self, supportive: bool) {
for (item, styles, strength) in self.staged.drain(..) { for (item, styles, meta) in self.staged.drain(..) {
if supportive || strength.is_none() { if supportive || meta.is_none() {
self.builder.push(item, styles); self.builder.push(item, styles);
} }
} }
@ -103,3 +114,64 @@ impl<'a, T> Default for CollapsingBuilder<'a, T> {
Self::new() Self::new()
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::library::layout::FlowChild;
use crate::library::prelude::*;
#[track_caller]
fn test<T>(builder: CollapsingBuilder<T>, 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()]);
}
}

View File

@ -40,29 +40,32 @@ use crate::util::EcoString;
pub enum Content { pub enum Content {
/// A word space. /// A word space.
Space, Space,
/// A forced line break. If `true`, the preceding line can still be /// A forced line break.
/// justified, if `false` not. Linebreak { justified: bool },
Linebreak(bool),
/// Horizontal spacing. /// Horizontal spacing.
Horizontal(Spacing), Horizontal { amount: Spacing, weak: bool },
/// Plain text. /// Plain text.
Text(EcoString), Text(EcoString),
/// A smart quote, may be single (`false`) or double (`true`). /// A smart quote.
Quote(bool), Quote { double: bool },
/// An inline-level node. /// An inline-level node.
Inline(LayoutNode), Inline(LayoutNode),
/// A paragraph break. /// A paragraph break.
Parbreak, Parbreak,
/// A column break. /// A column break.
Colbreak, Colbreak { weak: bool },
/// Vertical spacing. /// Vertical spacing.
Vertical(Spacing), Vertical {
amount: Spacing,
weak: bool,
generated: bool,
},
/// A block-level node. /// A block-level node.
Block(LayoutNode), Block(LayoutNode),
/// A list / enum item. /// A list / enum item.
Item(ListItem), Item(ListItem),
/// A page break. /// A page break.
Pagebreak(bool), Pagebreak { weak: bool },
/// A page node. /// A page node.
Page(PageNode), Page(PageNode),
/// A node that can be realized with styles. /// A node that can be realized with styles.
@ -153,21 +156,28 @@ impl Content {
Self::show(DecoNode::<UNDERLINE>(self)) Self::show(DecoNode::<UNDERLINE>(self))
} }
/// Add vertical spacing above and below the node. /// Add weak vertical spacing above and below the node.
pub fn spaced(self, above: Length, below: Length) -> Self { pub fn spaced(self, above: Option<Length>, below: Option<Length>) -> Self {
if above.is_zero() && below.is_zero() { if above.is_none() && below.is_none() {
return self; return self;
} }
let mut seq = vec![]; let mut seq = vec![];
if !above.is_zero() { if let Some(above) = above {
seq.push(Content::Vertical(above.into())); seq.push(Content::Vertical {
amount: above.into(),
weak: true,
generated: true,
});
} }
seq.push(self); seq.push(self);
if let Some(below) = below {
if !below.is_zero() { seq.push(Content::Vertical {
seq.push(Content::Vertical(below.into())); amount: below.into(),
weak: true,
generated: true,
});
} }
Self::sequence(seq) Self::sequence(seq)
@ -219,17 +229,21 @@ impl Debug for Content {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::Space => f.pad("Space"), Self::Space => f.pad("Space"),
Self::Linebreak(justified) => write!(f, "Linebreak({justified})"), Self::Linebreak { justified } => write!(f, "Linebreak({justified})"),
Self::Horizontal(kind) => write!(f, "Horizontal({kind:?})"), Self::Horizontal { amount, weak } => {
write!(f, "Horizontal({amount:?}, {weak})")
}
Self::Text(text) => write!(f, "Text({text:?})"), 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::Inline(node) => node.fmt(f),
Self::Parbreak => f.pad("Parbreak"), Self::Parbreak => f.pad("Parbreak"),
Self::Colbreak => f.pad("Colbreak"), Self::Colbreak { weak } => write!(f, "Colbreak({weak})"),
Self::Vertical(kind) => write!(f, "Vertical({kind:?})"), Self::Vertical { amount, weak, generated } => {
write!(f, "Vertical({amount:?}, {weak}, {generated})")
}
Self::Block(node) => node.fmt(f), Self::Block(node) => node.fmt(f),
Self::Item(item) => item.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::Page(page) => page.fmt(f),
Self::Show(node) => node.fmt(f), Self::Show(node) => node.fmt(f),
Self::Styled(styled) => { Self::Styled(styled) => {
@ -360,7 +374,7 @@ impl<'a, 'ctx> Builder<'a, 'ctx> {
return Ok(()); return Ok(());
} }
let keep = matches!(content, Content::Pagebreak(false)); let keep = matches!(content, Content::Pagebreak { weak: false });
self.interrupt(Interruption::Page, styles, keep)?; self.interrupt(Interruption::Page, styles, keep)?;
if let Some(doc) = &mut self.doc { if let Some(doc) = &mut self.doc {
@ -419,10 +433,8 @@ impl<'a, 'ctx> Builder<'a, 'ctx> {
if intr >= Interruption::Par { if intr >= Interruption::Par {
if !self.par.is_empty() { if !self.par.is_empty() {
self.flow.0.weak(FlowChild::Leading, 0, styles);
mem::take(&mut self.par).finish(self); mem::take(&mut self.par).finish(self);
} }
self.flow.0.weak(FlowChild::Leading, 0, styles);
} }
if intr >= Interruption::Page { if intr >= Interruption::Page {
@ -456,8 +468,8 @@ struct DocBuilder<'a> {
impl<'a> DocBuilder<'a> { impl<'a> DocBuilder<'a> {
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) {
match content { match content {
Content::Pagebreak(soft) => { Content::Pagebreak { weak } => {
self.keep_next = !soft; self.keep_next = !weak;
} }
Content::Page(page) => { Content::Page(page) => {
self.pages.push(page.clone(), styles); self.pages.push(page.clone(), styles);
@ -483,16 +495,31 @@ struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>);
impl<'a> FlowBuilder<'a> { impl<'a> FlowBuilder<'a> {
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { 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 { match content {
Content::Parbreak => { Content::Parbreak => {}
self.0.weak(FlowChild::Parbreak, 1, styles); Content::Colbreak { weak } => {
if *weak {
self.0.weak(FlowChild::Colbreak, styles, 0);
} else {
self.0.destructive(FlowChild::Colbreak, styles);
}
} }
Content::Colbreak => { &Content::Vertical { amount, weak, generated } => {
self.0.destructive(FlowChild::Colbreak, styles); let child = FlowChild::Spacing(amount);
} let frac = amount.is_fractional();
Content::Vertical(kind) => { if weak {
let child = FlowChild::Spacing(*kind); let weakness = 1 + u8::from(frac) + 2 * u8::from(generated);
if kind.is_fractional() { self.0.weak(child, styles, weakness);
} else if frac {
self.0.destructive(child, styles); self.0.destructive(child, styles);
} else { } else {
self.0.ignorant(child, styles); self.0.ignorant(child, styles);
@ -512,6 +539,18 @@ impl<'a> FlowBuilder<'a> {
true 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>) { fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) {
let (flow, shared) = self.0.finish(); let (flow, shared) = self.0.finish();
let styles = if flow.is_empty() { styles } else { shared }; let styles = if flow.is_empty() { styles } else { shared };
@ -530,24 +569,34 @@ struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>);
impl<'a> ParBuilder<'a> { impl<'a> ParBuilder<'a> {
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { 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 { match content {
Content::Space => { Content::Space => {
self.0.weak(ParChild::Text(' '.into()), 0, styles); self.0.weak(ParChild::Text(' '.into()), styles, 2);
} }
Content::Linebreak(justified) => { &Content::Linebreak { justified } => {
let c = if *justified { '\u{2028}' } else { '\n' }; let c = if justified { '\u{2028}' } else { '\n' };
self.0.destructive(ParChild::Text(c.into()), styles); self.0.destructive(ParChild::Text(c.into()), styles);
} }
Content::Horizontal(kind) => { &Content::Horizontal { amount, weak } => {
let child = ParChild::Spacing(*kind); let child = ParChild::Spacing(amount);
if kind.is_fractional() { 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); self.0.destructive(child, styles);
} else { } else {
self.0.ignorant(child, styles); self.0.ignorant(child, styles);
} }
} }
Content::Quote(double) => { &Content::Quote { double } => {
self.0.supportive(ParChild::Quote(*double), styles); self.0.supportive(ParChild::Quote { double }, styles);
} }
Content::Text(text) => { Content::Text(text) => {
self.0.supportive(ParChild::Text(text.clone()), styles); self.0.supportive(ParChild::Text(text.clone()), styles);
@ -575,7 +624,7 @@ impl<'a> ParBuilder<'a> {
.items() .items()
.find_map(|child| match child { .find_map(|child| match child {
ParChild::Spacing(_) => None, ParChild::Spacing(_) => None,
ParChild::Text(_) | ParChild::Quote(_) => Some(true), ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
ParChild::Node(_) => Some(false), ParChild::Node(_) => Some(false),
}) })
.unwrap_or_default() .unwrap_or_default()
@ -585,10 +634,8 @@ impl<'a> ParBuilder<'a> {
.items() .items()
.rev() .rev()
.find_map(|child| match child { .find_map(|child| match child {
FlowChild::Leading => None, FlowChild::Spacing(_) => None,
FlowChild::Parbreak => None,
FlowChild::Node(node) => Some(node.is::<ParNode>()), FlowChild::Node(node) => Some(node.is::<ParNode>()),
FlowChild::Spacing(_) => Some(false),
FlowChild::Colbreak => Some(false), FlowChild::Colbreak => Some(false),
}) })
.unwrap_or_default() .unwrap_or_default()
@ -596,8 +643,7 @@ impl<'a> ParBuilder<'a> {
children.push_front(ParChild::Spacing(indent.into())); children.push_front(ParChild::Spacing(indent.into()));
} }
let node = ParNode(children).pack(); parent.flow.par(ParNode(children), shared, !indent.is_zero());
parent.flow.0.supportive(FlowChild::Node(node), shared);
} }
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
@ -611,19 +657,24 @@ struct ListBuilder<'a> {
items: StyleVecBuilder<'a, ListItem>, items: StyleVecBuilder<'a, ListItem>,
/// Whether the list contains no paragraph breaks. /// Whether the list contains no paragraph breaks.
tight: bool, tight: bool,
/// Whether the list can be attached.
attachable: bool,
/// Trailing content for which it is unclear whether it is part of the list. /// Trailing content for which it is unclear whether it is part of the list.
staged: Vec<(&'a Content, StyleChain<'a>)>, staged: Vec<(&'a Content, StyleChain<'a>)>,
} }
impl<'a> ListBuilder<'a> { impl<'a> ListBuilder<'a> {
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { 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 { 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) Content::Item(item)
if self if self
.items .items
@ -634,6 +685,9 @@ impl<'a> ListBuilder<'a> {
self.items.push(item.clone(), styles); self.items.push(item.clone(), styles);
self.tight &= self.staged.drain(..).all(|(t, _)| *t != Content::Parbreak); 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, _ => return false,
} }
@ -647,10 +701,17 @@ impl<'a> ListBuilder<'a> {
None => return Ok(()), None => return Ok(()),
}; };
let start = 1;
let tight = self.tight; let tight = self.tight;
let attached = tight && self.attachable;
let content = match kind { let content = match kind {
UNORDERED => Content::show(ListNode::<UNORDERED> { start: 1, tight, items }), UNORDERED => {
ORDERED | _ => Content::show(ListNode::<ORDERED> { start: 1, tight, items }), Content::show(ListNode::<UNORDERED> { start, tight, attached, items })
}
ORDERED | _ => {
Content::show(ListNode::<ORDERED> { start, tight, attached, items })
}
}; };
let stored = parent.scratch.templates.alloc(content); let stored = parent.scratch.templates.alloc(content);
@ -660,6 +721,8 @@ impl<'a> ListBuilder<'a> {
parent.accept(content, styles)?; parent.accept(content, styles)?;
} }
parent.list.attachable = true;
Ok(()) Ok(())
} }
@ -673,6 +736,7 @@ impl Default for ListBuilder<'_> {
Self { Self {
items: StyleVecBuilder::default(), items: StyleVecBuilder::default(),
tight: true, tight: true,
attachable: true,
staged: vec![], staged: vec![],
} }
} }

View File

@ -1,7 +1,7 @@
//! Layouting infrastructure. //! Layouting infrastructure.
use std::any::Any; use std::any::Any;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter, Write};
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::sync::Arc;
@ -239,7 +239,9 @@ impl Default for LayoutNode {
impl Debug for LayoutNode { impl Debug for LayoutNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f) f.write_str("Layout(")?;
self.0.fmt(f)?;
f.write_char(')')
} }
} }

View File

@ -1,4 +1,4 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter, Write};
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::sync::Arc;
@ -87,7 +87,9 @@ impl Show for ShowNode {
impl Debug for ShowNode { impl Debug for ShowNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f) f.write_str("Show(")?;
self.0.fmt(f)?;
f.write_char(')')
} }
} }

View File

@ -214,8 +214,8 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
| NodeKind::EnDash | NodeKind::EnDash
| NodeKind::EmDash | NodeKind::EmDash
| NodeKind::Ellipsis | NodeKind::Ellipsis
| NodeKind::Quote(_) | NodeKind::Quote { .. }
| NodeKind::Linebreak(_) | NodeKind::Linebreak { .. }
| NodeKind::Raw(_) | NodeKind::Raw(_)
| NodeKind::Math(_) | NodeKind::Math(_)
| NodeKind::Escape(_) => { | NodeKind::Escape(_) => {

View File

@ -141,8 +141,8 @@ impl<'s> Tokens<'s> {
'~' => NodeKind::NonBreakingSpace, '~' => NodeKind::NonBreakingSpace,
'-' => self.hyph(), '-' => self.hyph(),
'.' if self.s.eat_if("..") => NodeKind::Ellipsis, '.' if self.s.eat_if("..") => NodeKind::Ellipsis,
'\'' => NodeKind::Quote(false), '\'' => NodeKind::Quote { double: false },
'"' => NodeKind::Quote(true), '"' => NodeKind::Quote { double: true },
'*' if !self.in_word() => NodeKind::Star, '*' if !self.in_word() => NodeKind::Star,
'_' if !self.in_word() => NodeKind::Underscore, '_' if !self.in_word() => NodeKind::Underscore,
'`' => self.raw(), '`' => self.raw(),
@ -266,7 +266,7 @@ impl<'s> Tokens<'s> {
fn backslash(&mut self) -> NodeKind { fn backslash(&mut self) -> NodeKind {
let c = match self.s.peek() { let c = match self.s.peek() {
Some(c) => c, Some(c) => c,
None => return NodeKind::Linebreak(false), None => return NodeKind::Linebreak { justified: false },
}; };
match c { match c {
@ -300,10 +300,10 @@ impl<'s> Tokens<'s> {
} }
// Linebreaks. // Linebreaks.
c if c.is_whitespace() => NodeKind::Linebreak(false), c if c.is_whitespace() => NodeKind::Linebreak { justified: false },
'+' => { '+' => {
self.s.expect(c); self.s.expect(c);
NodeKind::Linebreak(true) NodeKind::Linebreak { justified: true }
} }
// Just the backslash. // Just the backslash.
@ -839,7 +839,7 @@ mod tests {
t!(Markup[" /"]: "hello-world" => Text("hello-world")); t!(Markup[" /"]: "hello-world" => Text("hello-world"));
// Test code symbols in text. // 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[" /"]: ";:,|/+" => Text(";:,|"), Text("/+"));
t!(Markup[" /"]: "=-a" => Eq, Minus, Text("a")); t!(Markup[" /"]: "=-a" => Eq, Minus, Text("a"));
t!(Markup[" "]: "#123" => Text("#"), Text("123")); t!(Markup[" "]: "#123" => Text("#"), Text("123"));
@ -893,8 +893,8 @@ mod tests {
t!(Markup: "_" => Underscore); t!(Markup: "_" => Underscore);
t!(Markup[""]: "===" => Eq, Eq, Eq); t!(Markup[""]: "===" => Eq, Eq, Eq);
t!(Markup["a1/"]: "= " => Eq, Space(0)); t!(Markup["a1/"]: "= " => Eq, Space(0));
t!(Markup[" "]: r"\" => Linebreak(false)); t!(Markup[" "]: r"\" => Linebreak { justified: false });
t!(Markup[" "]: r"\+" => Linebreak(true)); t!(Markup[" "]: r"\+" => Linebreak { justified: true });
t!(Markup: "~" => NonBreakingSpace); t!(Markup: "~" => NonBreakingSpace);
t!(Markup["a1/"]: "-?" => Shy); t!(Markup["a1/"]: "-?" => Shy);
t!(Markup["a "]: r"a--" => Text("a"), EnDash); t!(Markup["a "]: r"a--" => Text("a"), EnDash);

View File

@ -2,6 +2,7 @@
//! //!
//! The AST is rooted in the [`Markup`] node. //! The AST is rooted in the [`Markup`] node.
use std::num::NonZeroUsize;
use std::ops::Deref; use std::ops::Deref;
use super::{Green, GreenData, NodeKind, RedNode, RedRef, Span}; use super::{Green, GreenData, NodeKind, RedNode, RedRef, Span};
@ -62,7 +63,9 @@ impl Markup {
self.0.children().filter_map(|node| match node.kind() { self.0.children().filter_map(|node| match node.kind() {
NodeKind::Space(2 ..) => Some(MarkupNode::Parbreak), NodeKind::Space(2 ..) => Some(MarkupNode::Parbreak),
NodeKind::Space(_) => Some(MarkupNode::Space), 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::Text(s) => Some(MarkupNode::Text(s.clone())),
NodeKind::Escape(c) => Some(MarkupNode::Text((*c).into())), NodeKind::Escape(c) => Some(MarkupNode::Text((*c).into())),
NodeKind::NonBreakingSpace => Some(MarkupNode::Text('\u{00A0}'.into())), NodeKind::NonBreakingSpace => Some(MarkupNode::Text('\u{00A0}'.into())),
@ -70,7 +73,7 @@ impl Markup {
NodeKind::EnDash => Some(MarkupNode::Text('\u{2013}'.into())), NodeKind::EnDash => Some(MarkupNode::Text('\u{2013}'.into())),
NodeKind::EmDash => Some(MarkupNode::Text('\u{2014}'.into())), NodeKind::EmDash => Some(MarkupNode::Text('\u{2014}'.into())),
NodeKind::Ellipsis => Some(MarkupNode::Text('\u{2026}'.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::Strong => node.cast().map(MarkupNode::Strong),
NodeKind::Emph => node.cast().map(MarkupNode::Emph), NodeKind::Emph => node.cast().map(MarkupNode::Emph),
NodeKind::Raw(raw) => Some(MarkupNode::Raw(raw.as_ref().clone())), NodeKind::Raw(raw) => Some(MarkupNode::Raw(raw.as_ref().clone())),
@ -88,15 +91,14 @@ impl Markup {
pub enum MarkupNode { pub enum MarkupNode {
/// Whitespace containing less than two newlines. /// Whitespace containing less than two newlines.
Space, Space,
/// A forced line break. If `true` (`\`), the preceding line can still be /// A forced line break: `\` or `\+` if justified.
/// justified, if `false` (`\+`) not. Linebreak { justified: bool },
Linebreak(bool),
/// A paragraph break: Two or more newlines. /// A paragraph break: Two or more newlines.
Parbreak, Parbreak,
/// Plain text. /// Plain text.
Text(EcoString), Text(EcoString),
/// A smart quote: `'` (`false`) or `"` (true). /// A smart quote: `'` or `"`.
Quote(bool), Quote { double: bool },
/// Strong content: `*Strong*`. /// Strong content: `*Strong*`.
Strong(StrongNode), Strong(StrongNode),
/// Emphasized content: `_Emphasized_`. /// Emphasized content: `_Emphasized_`.
@ -176,8 +178,13 @@ impl HeadingNode {
} }
/// The section depth (numer of equals signs). /// The section depth (numer of equals signs).
pub fn level(&self) -> usize { pub fn level(&self) -> NonZeroUsize {
self.0.children().filter(|n| n.kind() == &NodeKind::Eq).count() self.0
.children()
.filter(|n| n.kind() == &NodeKind::Eq)
.count()
.try_into()
.expect("heading is missing equals sign")
} }
} }

View File

@ -126,7 +126,7 @@ impl Category {
_ => Some(Category::Operator), _ => Some(Category::Operator),
}, },
NodeKind::EnumNumbering(_) => Some(Category::List), NodeKind::EnumNumbering(_) => Some(Category::List),
NodeKind::Linebreak(_) => Some(Category::Shortcut), NodeKind::Linebreak { .. } => Some(Category::Shortcut),
NodeKind::NonBreakingSpace => Some(Category::Shortcut), NodeKind::NonBreakingSpace => Some(Category::Shortcut),
NodeKind::Shy => Some(Category::Shortcut), NodeKind::Shy => Some(Category::Shortcut),
NodeKind::EnDash => Some(Category::Shortcut), NodeKind::EnDash => Some(Category::Shortcut),
@ -206,7 +206,7 @@ impl Category {
NodeKind::Markup(_) => None, NodeKind::Markup(_) => None,
NodeKind::Space(_) => None, NodeKind::Space(_) => None,
NodeKind::Text(_) => None, NodeKind::Text(_) => None,
NodeKind::Quote(_) => None, NodeKind::Quote { .. } => None,
NodeKind::List => None, NodeKind::List => None,
NodeKind::Enum => None, NodeKind::Enum => None,
NodeKind::CodeBlock => None, NodeKind::CodeBlock => None,

View File

@ -588,9 +588,8 @@ pub enum NodeKind {
Space(usize), Space(usize),
/// A consecutive non-markup string. /// A consecutive non-markup string.
Text(EcoString), Text(EcoString),
/// A forced line break. If `true` (`\`), the preceding line can still be /// A forced line break: `\` or `\+` if justified.
/// justified, if `false` (`\+`) not. Linebreak { justified: bool },
Linebreak(bool),
/// A non-breaking space: `~`. /// A non-breaking space: `~`.
NonBreakingSpace, NonBreakingSpace,
/// A soft hyphen: `-?`. /// A soft hyphen: `-?`.
@ -601,8 +600,8 @@ pub enum NodeKind {
EmDash, EmDash,
/// An ellipsis: `...`. /// An ellipsis: `...`.
Ellipsis, Ellipsis,
/// A smart quote: `'` (`false`) or `"` (true). /// A smart quote: `'` or `"`.
Quote(bool), Quote { double: bool },
/// A slash and the letter "u" followed by a hexadecimal unicode entity /// A slash and the letter "u" followed by a hexadecimal unicode entity
/// enclosed in curly braces: `\u{1F5FA}`. /// enclosed in curly braces: `\u{1F5FA}`.
Escape(char), Escape(char),
@ -773,13 +772,13 @@ impl NodeKind {
pub fn only_in_mode(&self) -> Option<TokenMode> { pub fn only_in_mode(&self) -> Option<TokenMode> {
match self { match self {
Self::Markup(_) Self::Markup(_)
| Self::Linebreak(_) | Self::Linebreak { .. }
| Self::Text(_) | Self::Text(_)
| Self::NonBreakingSpace | Self::NonBreakingSpace
| Self::EnDash | Self::EnDash
| Self::EmDash | Self::EmDash
| Self::Ellipsis | Self::Ellipsis
| Self::Quote(_) | Self::Quote { .. }
| Self::Escape(_) | Self::Escape(_)
| Self::Strong | Self::Strong
| Self::Emph | Self::Emph
@ -867,16 +866,16 @@ impl NodeKind {
Self::Markup(_) => "markup", Self::Markup(_) => "markup",
Self::Space(2 ..) => "paragraph break", Self::Space(2 ..) => "paragraph break",
Self::Space(_) => "space", Self::Space(_) => "space",
Self::Linebreak(false) => "linebreak", Self::Linebreak { justified: false } => "linebreak",
Self::Linebreak(true) => "justified linebreak", Self::Linebreak { justified: true } => "justified linebreak",
Self::Text(_) => "text", Self::Text(_) => "text",
Self::NonBreakingSpace => "non-breaking space", Self::NonBreakingSpace => "non-breaking space",
Self::Shy => "soft hyphen", Self::Shy => "soft hyphen",
Self::EnDash => "en dash", Self::EnDash => "en dash",
Self::EmDash => "em dash", Self::EmDash => "em dash",
Self::Ellipsis => "ellipsis", Self::Ellipsis => "ellipsis",
Self::Quote(false) => "single quote", Self::Quote { double: false } => "single quote",
Self::Quote(true) => "double quote", Self::Quote { double: true } => "double quote",
Self::Escape(_) => "escape sequence", Self::Escape(_) => "escape sequence",
Self::Strong => "strong content", Self::Strong => "strong content",
Self::Emph => "emphasized content", Self::Emph => "emphasized content",
@ -993,14 +992,14 @@ impl Hash for NodeKind {
Self::From => {} Self::From => {}
Self::Markup(c) => c.hash(state), Self::Markup(c) => c.hash(state),
Self::Space(n) => n.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::Text(s) => s.hash(state),
Self::NonBreakingSpace => {} Self::NonBreakingSpace => {}
Self::Shy => {} Self::Shy => {}
Self::EnDash => {} Self::EnDash => {}
Self::EmDash => {} Self::EmDash => {}
Self::Ellipsis => {} Self::Ellipsis => {}
Self::Quote(d) => d.hash(state), Self::Quote { double } => double.hash(state),
Self::Escape(c) => c.hash(state), Self::Escape(c) => c.hash(state),
Self::Strong => {} Self::Strong => {}
Self::Emph => {} Self::Emph => {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -29,6 +29,10 @@
// Error: 2-18 cannot apply '<=' to relative length and ratio // Error: 2-18 cannot apply '<=' to relative length and ratio
{30% + 1pt <= 40%} {30% + 1pt <= 40%}
---
// Error: 2-13 cannot apply '<=' to length and length
{1em <= 10pt}
--- ---
// Special messages for +, -, * and /. // Special messages for +, -, * and /.
// Error: 03-10 cannot add integer and string // Error: 03-10 cannot add integer and string

View File

@ -151,6 +151,8 @@
#test(45deg < 1rad, true) #test(45deg < 1rad, true)
#test(10% < 20%, true) #test(10% < 20%, true)
#test(50% < 40% + 0pt, false) #test(50% < 40% + 0pt, false)
#test(40% + 0pt < 50% + 0pt, true)
#test(1em < 2em, true)
--- ---
// Test assignment operators. // Test assignment operators.

View File

@ -6,10 +6,11 @@ Sekretariat MA \
Dr. Max Mustermann \ Dr. Max Mustermann \
Ola Nordmann, John Doe Ola Nordmann, John Doe
#v(2mm) #v(3mm)
#align(center)[ #align(center)[
==== 3. Übungsblatt Computerorientierte Mathematik II #v(1mm) #set par(leading: 3mm)
*Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(1mm) #text(1.2em)[*3. Übungsblatt Computerorientierte Mathematik II*] \
*Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) \
*Alle Antworten sind zu beweisen.* *Alle Antworten sind zu beweisen.*
] ]

View File

@ -6,7 +6,7 @@
#let tex = [{ #let tex = [{
[T] [T]
h(-0.14 * size) h(-0.14 * size)
move(y: 0.22 * size)[E] move(dy: 0.22 * size)[E]
h(-0.12 * size) h(-0.12 * size)
[X] [X]
}] }]
@ -14,11 +14,11 @@
#let xetex = { #let xetex = {
[X] [X]
h(-0.14 * size) 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) h(-0.14 * size)
[T] [T]
h(-0.14 * size) h(-0.14 * size)
move(y: 0.26 * size)[E] move(dy: 0.26 * size)[E]
h(-0.12 * size) h(-0.12 * size)
[X] [X]
} }

View File

@ -13,14 +13,14 @@
#pagebreak() #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. // Should result in two aqua-colored pages.
#set page(fill: aqua) #set page(fill: aqua)
#pagebreak(soft: true) #pagebreak(weak: true)
First First
#pagebreak(soft: true) #pagebreak(weak: true)
Second Second
#pagebreak(soft: true) #pagebreak(weak: true)
--- ---
// Test a combination of pagebreaks, styled pages and pages with bodies. // Test a combination of pagebreaks, styled pages and pages with bodies.
@ -34,12 +34,12 @@ Third
Fif[#set page();th] 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. // Should result in three navy-colored pages.
#set page(fill: navy) #set page(fill: navy)
#set text(fill: white) #set text(fill: white)
First First
#pagebreak() #pagebreak()
#page[Second] #page[Second]
#pagebreak(soft: true) #pagebreak(weak: true)
#page[Third] #page[Third]

View File

@ -16,14 +16,14 @@ the line breaks still had to be inserted manually.
place(right, dy: 1.5pt)[ABC], place(right, dy: 1.5pt)[ABC],
rect(fill: conifer, height: 10pt, width: 80%), rect(fill: conifer, height: 10pt, width: 80%),
rect(fill: forest, height: 10pt, width: 100%), 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. // Test how the placed node interacts with paragraph spacing around it.
#set page("a8", height: 60pt) #set page("a8", height: 60pt)

View File

@ -1,8 +1,8 @@
// Test the `h` and `v` functions. // Test the `h` and `v` functions.
--- ---
// Linebreak and v(0pt) are equivalent. // Linebreak and leading-sized weak spacing are equivalent.
#box[A \ B] #box[A #v(0pt) B] #box[A \ B] #box[A #v(0.65em, weak: true) B]
// Eating up soft spacing. // Eating up soft spacing.
Inv#h(0pt)isible Inv#h(0pt)isible

View File

@ -18,25 +18,17 @@
#set page(width: 50pt, margins: 0pt) #set page(width: 50pt, margins: 0pt)
#stack(dir: btt, ..items) #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. // Test spacing.
#set page(width: 50pt, margins: 0pt) #set page(width: 50pt, margins: 0pt)
#set par(leading: 5pt)
#let x = square(size: 10pt, fill: eastern) #let x = square(size: 10pt, fill: eastern)
#stack(dir: rtl, spacing: 5pt, x, x, x) #stack(
#stack(dir: ltr, x, 20%, x, 20%, x) spacing: 5pt,
#stack(dir: ltr, spacing: 5pt, x, x, 7pt, 3pt, x) 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. // Test overflow.
@ -45,3 +37,15 @@
rect(width: 40pt, height: 20pt, fill: conifer), rect(width: 40pt, height: 20pt, fill: conifer),
rect(width: 30pt, height: 13pt, fill: forest), 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],
)

View File

@ -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

View File

@ -39,7 +39,8 @@
// Test label closure. // Test label closure.
#enum( #enum(
start: 4, start: 4,
spacing: -3pt, spacing: 0.65em - 3pt,
tight: false,
label: n => text(fill: (red, green, blue)(mod(n, 3)), [#upper(letter(n))]), label: n => text(fill: (red, green, blue)(mod(n, 3)), [#upper(letter(n))]),
[Red], [Green], [Blue], [Red], [Green], [Blue],
) )

View File

@ -1,14 +1,13 @@
// Test headings. // Test headings.
--- ---
// Different number of hashtags. // Different number of equals signs.
// Valid levels.
= Level 1 = Level 1
=== Level 2 == Level 2
====== Level 6 === Level 3
// At some point, it should stop shrinking. // After three, it stops shrinking.
=========== Level 11 =========== Level 11
--- ---

View File

@ -2,21 +2,16 @@
--- ---
_Shopping list_ _Shopping list_
#list[Apples][Potatoes][Juice] #list(attached: true)[Apples][Potatoes][Juice]
---
Tightly
- surrounded
- by two
paragraphs.
--- ---
- First level. - First level.
- Second level. - Second level.
There are multiple paragraphs. There are multiple paragraphs.
- Third level. - Third level.
Still the same bullet point. Still the same bullet point.
- Still level 2. - Still level 2.

View File

@ -3,6 +3,7 @@
--- ---
#set heading( #set heading(
size: 10pt, size: 10pt,
around: 0.65em,
fill: lvl => if even(lvl) { red } else { blue }, fill: lvl => if even(lvl) { red } else { blue },
) )

View File

@ -11,7 +11,7 @@ Hello *{x}*
#let fruit = [ #let fruit = [
- Apple - Apple
- Orange - Orange
#list(body-indent: 10pt, [Pear]) #list(body-indent: 20pt, [Pear])
] ]
- Fruit - Fruit
@ -22,7 +22,7 @@ Hello *{x}*
--- ---
// Test that that par spacing and text style are respected from // Test that that par spacing and text style are respected from
// the outside, but the more specific fill is respected. // the outside, but the more specific fill is respected.
#set par(spacing: 0pt) #set par(spacing: 4pt)
#set text(style: "italic", fill: eastern) #set text(style: "italic", fill: eastern)
#let x = [And the forest #parbreak() lay silent!] #let x = [And the forest #parbreak() lay silent!]
#text(fill: forest, x) #text(fill: forest, x)

View File

@ -29,7 +29,7 @@ Some more text.
Another text. Another text.
--- ---
#set heading(size: 1em, strong: false, block: false) #set heading(size: 1em, strong: false, around: none)
#show _: heading as [B] #show _: heading as [B]
A [= Heading] C A [= Heading] C

View File

@ -1,7 +1,7 @@
// Test paragraph indent. // Test paragraph indent.
--- ---
#set par(indent: 12pt, leading: 5pt, spacing: 0pt) #set par(indent: 12pt, leading: 5pt)
#set heading(size: 10pt, above: 8pt) #set heading(size: 10pt, above: 8pt)
The first paragraph has no indent. 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?

View File

@ -15,8 +15,9 @@
#let column(title, linebreaks, hyphenate) = { #let column(title, linebreaks, hyphenate) = {
rect(width: 132pt, fill: rgb("eee"))[ rect(width: 132pt, fill: rgb("eee"))[
#strong(title) #set par(linebreaks: linebreaks)
#par(linebreaks: linebreaks, text(hyphenate: hyphenate, story)) #set text(hyphenate: hyphenate)
#strong(title) \ #story
] ]
} }

View File

@ -25,11 +25,11 @@ You could also make the
#set page(height: 60pt) #set page(height: 60pt)
#set link(underline: false) #set link(underline: false)
#let mylink = link("https://typst.app/")[LINK] #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 containing a block.
#link("https://example.com/", underline: false, block[ #link("https://example.com/", underline: false, block[
My cool rhino My cool rhino
#move(x: 10pt, image("../../res/rhino.png", width: 1cm)) #move(dx: 10pt, image("../../res/rhino.png", width: 1cm))
]) ])

View File

@ -6,36 +6,45 @@
To the right! Where the sunlight peeks behind the mountain. To the right! Where the sunlight peeks behind the mountain.
--- ---
// Test that explicit paragraph break respects active styles. // Test changing leading and spacing.
#set par(spacing: 0pt) #set par(spacing: 1em, leading: 2pt)
[#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)
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
It is the east, and Juliet is the sun. 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 // Error: 17-20 must be horizontal
#set par(align: top) #set par(align: top)

View File

@ -6,17 +6,17 @@
--- ---
// Typst syntax inside. // Typst syntax inside.
`#let x = 1` \ ```typ #let x = 1``` \
`#f(1)` ```typ #f(1)```
--- ---
// Multiline block splits paragraphs. // Multiline block splits paragraphs.
First Text
```rust
fn code() {}
``` ```
Second Text
```
Third
--- ---
// Lots of backticks inside. // Lots of backticks inside.

View File

@ -2,14 +2,14 @@
--- ---
// Test basic call. // Test basic call.
#lipsum(19) #lorem(19)
--- ---
// Test custom paragraphs with user code. // Test custom paragraphs with user code.
#set text(8pt) #set text(8pt)
{ {
let sentences = lipsum(59) let sentences = lorem(59)
.split(".") .split(".")
.filter(s => s != "") .filter(s => s != "")
.map(s => s + ".") .map(s => s + ".")
@ -28,5 +28,5 @@
} }
--- ---
// Error: 8-10 missing argument: number of words // Error: 7-9 missing argument: number of words
#lipsum() #lorem()

View File

@ -75,7 +75,7 @@
{ {
"name": "markup.heading.typst", "name": "markup.heading.typst",
"contentName": "entity.name.section.typst", "contentName": "entity.name.section.typst",
"begin": "^\\s*={1,6}\\s+", "begin": "^\\s*=+\\s+",
"end": "\n", "end": "\n",
"beginCaptures": { "0": { "name": "punctuation.definition.heading.typst" } }, "beginCaptures": { "0": { "name": "punctuation.definition.heading.typst" } },
"patterns": [{ "include": "#markup" }] "patterns": [{ "include": "#markup" }]