summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-04-30 14:12:28 +0200
committerLaurenz <laurmaedje@gmail.com>2022-04-30 14:12:28 +0200
commitf9e115daf54c29358f890b137f50a33a781af680 (patch)
tree496de52246629ea8039db6beea94eb779ed2851d
parentf7c67cde72e6a67f45180856b332bae9863243bd (diff)
New block spacing model
-rw-r--r--macros/src/lib.rs120
-rw-r--r--src/eval/mod.rs4
-rw-r--r--src/eval/ops.rs3
-rw-r--r--src/eval/raw.rs2
-rw-r--r--src/eval/value.rs23
-rw-r--r--src/geom/relative.rs12
-rw-r--r--src/library/graphics/shape.rs1
-rw-r--r--src/library/graphics/transform.rs4
-rw-r--r--src/library/layout/align.rs12
-rw-r--r--src/library/layout/columns.rs5
-rw-r--r--src/library/layout/flow.rs41
-rw-r--r--src/library/layout/pad.rs12
-rw-r--r--src/library/layout/page.rs4
-rw-r--r--src/library/layout/spacing.rs45
-rw-r--r--src/library/layout/stack.rs12
-rw-r--r--src/library/math/mod.rs20
-rw-r--r--src/library/mod.rs2
-rw-r--r--src/library/structure/heading.rs51
-rw-r--r--src/library/structure/list.rs48
-rw-r--r--src/library/structure/table.rs31
-rw-r--r--src/library/text/deco.rs2
-rw-r--r--src/library/text/mod.rs12
-rw-r--r--src/library/text/par.rs39
-rw-r--r--src/library/text/raw.rs36
-rw-r--r--src/library/utility/blind.rs2
-rw-r--r--src/model/collapse.rs110
-rw-r--r--src/model/content.rs180
-rw-r--r--src/model/layout.rs6
-rw-r--r--src/model/show.rs6
-rw-r--r--src/parse/mod.rs4
-rw-r--r--src/parse/tokens.rs16
-rw-r--r--src/syntax/ast.rs25
-rw-r--r--src/syntax/highlight.rs4
-rw-r--r--src/syntax/mod.rs25
-rw-r--r--tests/ref/code/include.pngbin47947 -> 47545 bytes
-rw-r--r--tests/ref/coma.pngbin84510 -> 85923 bytes
-rw-r--r--tests/ref/layout/align.pngbin7832 -> 7901 bytes
-rw-r--r--tests/ref/layout/pad.pngbin53957 -> 53637 bytes
-rw-r--r--tests/ref/layout/place.pngbin44083 -> 43337 bytes
-rw-r--r--tests/ref/layout/stack-1.pngbin1076 -> 1522 bytes
-rw-r--r--tests/ref/math/basic.pngbin3619 -> 3639 bytes
-rw-r--r--tests/ref/structure/attach.pngbin0 -> 10993 bytes
-rw-r--r--tests/ref/structure/heading.pngbin23257 -> 22641 bytes
-rw-r--r--tests/ref/structure/list.pngbin22839 -> 20169 bytes
-rw-r--r--tests/ref/style/set-site.pngbin12569 -> 12397 bytes
-rw-r--r--tests/ref/style/show.pngbin19940 -> 19486 bytes
-rw-r--r--tests/ref/text/bidi.pngbin25815 -> 25981 bytes
-rw-r--r--tests/ref/text/indent.pngbin37966 -> 46160 bytes
-rw-r--r--tests/ref/text/par.pngbin11738 -> 30068 bytes
-rw-r--r--tests/ref/text/raw.pngbin22255 -> 22841 bytes
-rw-r--r--tests/typ/code/ops-invalid.typ4
-rw-r--r--tests/typ/code/ops.typ2
-rw-r--r--tests/typ/coma.typ7
-rw-r--r--tests/typ/graphics/transform.typ6
-rw-r--r--tests/typ/layout/pagebreak.typ12
-rw-r--r--tests/typ/layout/place.typ12
-rw-r--r--tests/typ/layout/spacing.typ4
-rw-r--r--tests/typ/layout/stack-1.typ32
-rw-r--r--tests/typ/structure/attach.typ56
-rw-r--r--tests/typ/structure/enum.typ3
-rw-r--r--tests/typ/structure/heading.typ9
-rw-r--r--tests/typ/structure/list.typ11
-rw-r--r--tests/typ/style/closure.typ1
-rw-r--r--tests/typ/style/set-site.typ4
-rw-r--r--tests/typ/style/show.typ2
-rw-r--r--tests/typ/text/indent.typ10
-rw-r--r--tests/typ/text/knuth.typ5
-rw-r--r--tests/typ/text/link.typ4
-rw-r--r--tests/typ/text/par.typ45
-rw-r--r--tests/typ/text/raw.typ12
-rw-r--r--tests/typ/utility/blind.typ8
-rw-r--r--tools/support/typst.tmLanguage.json2
72 files changed, 786 insertions, 384 deletions
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 15925df7..027db89d 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<TokenSt
let construct =
construct.ok_or_else(|| Error::new(impl_block.span(), "missing constructor"))?;
- let set = set.unwrap_or_else(|| {
- 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 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<Shorthand>,
variadic: bool,
resolve: bool,
fold: bool,
}
+enum Shorthand {
+ Positional,
+ Named(Ident),
+}
+
/// Parse a style property attribute.
fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
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<Property> {
.position(|attr| attr.path.get_ident().map_or(false, |name| name == "property"))
{
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 {
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<Property> {
}
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<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)
+ }
+ }
+}
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index bb9eb2fb..5542de89 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 832bd354..56bfdf2e 100644
--- a/src/eval/ops.rs
+++ b/src/eval/ops.rs
@@ -316,9 +316,10 @@ pub fn compare(lhs: &Value, rhs: &Value) -> Option<Ordering> {
(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 2d82fca8..6545ea5a 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<Ordering> {
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 31287fa8..6ce815a4 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<Self> {
+ <$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::<usize>()?
+ 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 fc77fb9f..f213ae52 100644
--- a/src/geom/relative.rs
+++ b/src/geom/relative.rs
@@ -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> {
type Output = Self;
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index e4c832f0..49c74c2f 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -26,7 +26,6 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to stroke the shape.
#[property(resolve, fold)]
pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto;
-
/// How much to pad the shape's content.
pub const PADDING: Relative<RawLength> = Relative::zero();
diff --git a/src/library/graphics/transform.rs b/src/library/graphics/transform.rs
index ea021cc1..a4aa20db 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<Content> {
- 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 2a4d039e..c050d2a4 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<Content> {
- let aligns: Spec<_> = args.find()?.unwrap_or_default();
- let body: LayoutNode = args.expect("body")?;
- Ok(Content::block(body.aligned(aligns)))
+ let aligns: Spec<Option<RawAlign>> = 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 4963043e..8e523694 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<Content> {
- Ok(Content::Colbreak)
+ fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
+ 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 6b43c8b7..6193a68f 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<FlowChild>);
/// 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<Ordering> {
+ 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 e688e423..2be21bcb 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<Content> {
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 8e5801ea..4307d2f9 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<Content> {
- 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 3468af5e..8a96e378 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<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]
impl VNode {
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 {
/// Spacing specified in absolute terms and relative to the parent's size.
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),
}
@@ -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! {
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<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())
+ }
+}
diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs
index f915c215..bbfeeab0 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::<AlignNode>()
.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::<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 len = frames.len();
diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs
index 345bb3f6..7b4998ca 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<FontFamily> =
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> {
Ok(Content::show(Self {
formula: args.expect("formula")?,
@@ -36,7 +44,11 @@ impl Show for MathNode {
}
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(
@@ -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 c68915c8..e90e5cc4 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 a6c87912..4f6c54f3 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<TextSize> = 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<bool> = Leveled::Value(false);
- /// The extra padding above the heading.
- #[property(referenced)]
- pub const ABOVE: Leveled<RawLength> = Leveled::Value(Length::zero().into());
- /// The extra padding below the heading.
- #[property(referenced)]
- pub const BELOW: Leveled<RawLength> = Leveled::Value(Length::zero().into());
-
- /// Whether the heading is block-level.
- #[property(referenced)]
- pub const BLOCK: Leveled<bool> = Leveled::Value(true);
+ /// The spacing above the heading.
+ #[property(referenced, shorthand(around))]
+ pub const ABOVE: Leveled<Option<BlockSpacing>> = 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<Option<BlockSpacing>> =
+ Leveled::Value(Some(Ratio::new(0.55).into()));
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
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<Content> {
- 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<T> {
/// 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<T: Cast + Clone> Leveled<T> {
/// 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 {
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 ac705156..4356ffb4 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<const L: ListKind = UNORDERED> {
/// 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<ListItem>,
}
@@ -38,10 +39,6 @@ impl<const L: ListKind> ListNode<L> {
/// 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<const L: ListKind> ListNode<L> {
#[property(resolve)]
pub const BODY_INDENT: RawLength = Em::new(0.5).into();
- /// The extra padding above the list.
- #[property(resolve)]
- pub const ABOVE: RawLength = RawLength::zero();
- /// The extra padding below 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)]
- pub const BELOW: RawLength = RawLength::zero();
+ pub const SPACING: BlockSpacing = Ratio::one().into();
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
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<const L: ListKind> Show for ListNode<L> {
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<const L: ListKind> Show for ListNode<L> {
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<const L: ListKind> Show for ListNode<L> {
styles: StyleChain,
realized: 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))
}
}
diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs
index 191d3dd3..7b3f2ac5 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<Paint> = None;
/// The secondary cell fill color.
+ #[property(shorthand(fill))]
pub const SECONDARY: Option<Paint> = None;
/// How to stroke the cells.
#[property(resolve, fold)]
pub const STROKE: Option<RawStroke> = Some(RawStroke::default());
-
/// How much to pad the cells's content.
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> {
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<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 {
@@ -99,4 +97,13 @@ impl Show for TableNode {
cells,
}))
}
+
+ fn finalize(
+ &self,
+ _: &mut Context,
+ styles: StyleChain,
+ realized: Content,
+ ) -> TypResult<Content> {
+ 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 52f8ea80..70040f9c 100644
--- a/src/library/text/deco.rs
+++ b/src/library/text/deco.rs
@@ -24,7 +24,6 @@ impl<const L: DecoLine> DecoNode<L> {
/// tables if `auto`.
#[property(shorthand, resolve, fold)]
pub const STROKE: Smart<RawStroke> = Smart::Auto;
-
/// Position of the line relative to the baseline, read from the font tables
/// if `auto`.
#[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.
#[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 3cfbc55d..ecc0c546 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<HorizontalDir> {
#[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<Hyphenate> {
type Output = bool;
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 4694993e..669d07ba 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<ParChild>);
/// 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<Content> {
// 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<Ordering> {
+ 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<Content> {
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 13daa1b9..ee6c6356 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<EcoString> = None;
+
/// The raw text's font family. Just the normal text family if `none`.
#[property(referenced)]
pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono"));
- /// The language to syntax-highlight in.
- #[property(referenced)]
- pub const LANG: Option<EcoString> = None;
+ /// The spacing above block-level raw.
+ #[property(resolve, shorthand(around))]
+ 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> {
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 a4cfec90..0075ab91 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<Value> {
+pub fn lorem(_: &mut Context, args: &mut Args) -> TypResult<Value> {
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 17933fe8..258f577e 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;
- }
- }
+ ///
+ /// 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;
+ }
- 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.
@@ -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<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()]);
+ }
+}
diff --git a/src/model/content.rs b/src/model/content.rs
index 6e1e2f1c..31255a29 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::<UNDERLINE>(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<Length>, below: Option<Length>) -> 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::Colbreak => {
- self.0.destructive(FlowChild::Colbreak, styles);
+ Content::Parbreak => {}
+ Content::Colbreak { weak } => {
+ if *weak {
+ self.0.weak(FlowChild::Colbreak, styles, 0);
+ } else {
+ 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::<ParNode>()),
- 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 {
- 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));
+ if self.items.is_empty() {
+ match content {
+ Content::Space => {}
+ Content::Item(_) => {}
+ Content::Parbreak => self.attachable = false,
+ _ => self.attachable = true,
}
+ }
+
+ match content {
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::<UNORDERED> { start: 1, tight, items }),
- ORDERED | _ => Content::show(ListNode::<ORDERED> { start: 1, tight, items }),
+ UNORDERED => {
+ Content::show(ListNode::<UNORDERED> { start, tight, attached, items })
+ }
+ ORDERED | _ => {
+ Content::show(ListNode::<ORDERED> { 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 78bfedc7..51154286 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 d1365eb2..16374deb 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 145f86d6..32d61885 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 4772be7f..f095bd09 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 5232b1f1..42a4235d 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 9bee73ae..8e62424f 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 2272b3e0..d21597ff 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<TokenMode> {
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 001d7d1e..d3d66032 100644
--- a/tests/ref/code/include.png
+++ b/tests/ref/code/include.png
Binary files differ
diff --git a/tests/ref/coma.png b/tests/ref/coma.png
index f9d30890..817c756a 100644
--- a/tests/ref/coma.png
+++ b/tests/ref/coma.png
Binary files differ
diff --git a/tests/ref/layout/align.png b/tests/ref/layout/align.png
index 5dde0cef..9b6e268c 100644
--- a/tests/ref/layout/align.png
+++ b/tests/ref/layout/align.png
Binary files differ
diff --git a/tests/ref/layout/pad.png b/tests/ref/layout/pad.png
index c2906cef..7c27bd26 100644
--- a/tests/ref/layout/pad.png
+++ b/tests/ref/layout/pad.png
Binary files differ
diff --git a/tests/ref/layout/place.png b/tests/ref/layout/place.png
index 7900f95f..6fc11706 100644
--- a/tests/ref/layout/place.png
+++ b/tests/ref/layout/place.png
Binary files differ
diff --git a/tests/ref/layout/stack-1.png b/tests/ref/layout/stack-1.png
index 106cc791..167fd84c 100644
--- a/tests/ref/layout/stack-1.png
+++ b/tests/ref/layout/stack-1.png
Binary files differ
diff --git a/tests/ref/math/basic.png b/tests/ref/math/basic.png
index 2c1dd324..381e92c4 100644
--- a/tests/ref/math/basic.png
+++ b/tests/ref/math/basic.png
Binary files differ
diff --git a/tests/ref/structure/attach.png b/tests/ref/structure/attach.png
new file mode 100644
index 00000000..7082a475
--- /dev/null
+++ b/tests/ref/structure/attach.png
Binary files differ
diff --git a/tests/ref/structure/heading.png b/tests/ref/structure/heading.png
index 693ae763..3e12b2cf 100644
--- a/tests/ref/structure/heading.png
+++ b/tests/ref/structure/heading.png
Binary files differ
diff --git a/tests/ref/structure/list.png b/tests/ref/structure/list.png
index ac90f4b2..7a60e1e5 100644
--- a/tests/ref/structure/list.png
+++ b/tests/ref/structure/list.png
Binary files differ
diff --git a/tests/ref/style/set-site.png b/tests/ref/style/set-site.png
index 408e44bf..024f7c3e 100644
--- a/tests/ref/style/set-site.png
+++ b/tests/ref/style/set-site.png
Binary files differ
diff --git a/tests/ref/style/show.png b/tests/ref/style/show.png
index 0f1a16a3..9539e496 100644
--- a/tests/ref/style/show.png
+++ b/tests/ref/style/show.png
Binary files differ
diff --git a/tests/ref/text/bidi.png b/tests/ref/text/bidi.png
index f31bdf0a..6f4c8acb 100644
--- a/tests/ref/text/bidi.png
+++ b/tests/ref/text/bidi.png
Binary files differ
diff --git a/tests/ref/text/indent.png b/tests/ref/text/indent.png
index 42f05cfb..09d8e68d 100644
--- a/tests/ref/text/indent.png
+++ b/tests/ref/text/indent.png
Binary files differ
diff --git a/tests/ref/text/par.png b/tests/ref/text/par.png
index 19f28b81..ae00ab45 100644
--- a/tests/ref/text/par.png
+++ b/tests/ref/text/par.png
Binary files differ
diff --git a/tests/ref/text/raw.png b/tests/ref/text/raw.png
index 64402dae..e13293a6 100644
--- a/tests/ref/text/raw.png
+++ b/tests/ref/text/raw.png
Binary files differ
diff --git a/tests/typ/code/ops-invalid.typ b/tests/typ/code/ops-invalid.typ
index 1506d9c4..4e7fdb04 100644
--- a/tests/typ/code/ops-invalid.typ
+++ b/tests/typ/code/ops-invalid.typ
@@ -30,6 +30,10 @@
{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
{(1 + "2", 40% - 1)}
diff --git a/tests/typ/code/ops.typ b/tests/typ/code/ops.typ
index 53cf488e..1f2867bc 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 92d5695f..0e228d14 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 82ee1390..e0679306 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 df3b0423..349a2fc2 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 527e0559..95049bdc 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 378c11b8..eb0bd39e 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 eee1f4d1..19a00de5 100644
--- a/tests/typ/layout/stack-1.typ
+++ b/tests/typ/layout/stack-1.typ
@@ -19,24 +19,16 @@
#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 00000000..c6d3c28c
--- /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 c4d178ce..b1045ee2 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 f4ba5f33..de95c35b 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 52cd51be..77e153c7 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 22626472..cd1f87df 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 8ee8a5fd..b49d1027 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 9aabfb34..a71b8df2 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 1b48851b..897e360c 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 5adeee91..59349738 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 99037ee3..ad538187 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 6b7c0f59..64a2dd17 100644
--- a/tests/typ/text/par.typ
+++ b/tests/typ/text/par.typ
@@ -6,35 +6,44 @@
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]
+// Test changing leading and spacing.
+#set par(spacing: 1em, leading: 2pt)
+But, soft! what light through yonder window breaks?
-[#set par(spacing: 100pt);Second]
-#set par(spacing: 13.5pt)
+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 uses correct set rule.
+// 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]
-#set par(spacing: 100pt)
-World
-#set par(spacing: 0pt, leading: 0pt)
+---
+// 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)
-You
+```rust
+fn main() {}
+```
----
-// Test that paragraphs break due to incompatibility has correct spacing.
-A #set par(spacing: 0pt, leading: 0pt); B #parbreak() C
+$[ x + y = z ]$
----
-// Test weird metrics.
-#set par(spacing: 1em, leading: 0pt)
-But, soft! what light through yonder window breaks?
+- List
-It is the east, and Juliet is the sun.
+Paragraph
---
// Error: 17-20 must be horizontal
diff --git a/tests/typ/text/raw.typ b/tests/typ/text/raw.typ
index 0e053a9b..33b08568 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 7d1cb969..17452dec 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 ef7806c8..e1dd0ba1 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" }]