New block spacing model
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)?,
|
||||
|
@ -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),
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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! {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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")?,
|
||||
|
@ -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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)?
|
||||
}
|
||||
})
|
||||
|
@ -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.
|
||||
/// 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 ABOVE: RawLength = RawLength::zero();
|
||||
/// The extra padding below the list.
|
||||
#[property(resolve)]
|
||||
pub const BELOW: RawLength = RawLength::zero();
|
||||
pub const SPACING: BlockSpacing = Ratio::one().into();
|
||||
|
||||
fn construct(_: &mut Context, args: &mut Args) -> TypResult<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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
});
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()))
|
||||
}
|
||||
|
@ -35,26 +35,37 @@ impl<'a, T> CollapsingBuilder<'a, T> {
|
||||
}
|
||||
|
||||
/// Can only exist when there is at least one supportive item to its left
|
||||
/// and to its right, with no destructive items or weak items in between to
|
||||
/// its left and no destructive items in between to its right. There may be
|
||||
/// and to its right, with no destructive items in between. There may be
|
||||
/// ignorant items in between in both directions.
|
||||
pub fn weak(&mut self, item: T, strength: u8, styles: StyleChain<'a>) {
|
||||
if self.last != Last::Destructive {
|
||||
if self.last == Last::Weak {
|
||||
if let Some(i) = self
|
||||
.staged
|
||||
.iter()
|
||||
.position(|(.., prev)| prev.map_or(false, |p| p < strength))
|
||||
{
|
||||
self.staged.remove(i);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.staged.push((item, styles, Some(strength)));
|
||||
self.last = Last::Weak;
|
||||
///
|
||||
/// Between weak items, there may be at least one per layer and among the
|
||||
/// candidates the strongest one (smallest `weakness`) wins. When tied,
|
||||
/// the one that compares larger through `PartialOrd` wins.
|
||||
pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8)
|
||||
where
|
||||
T: PartialOrd,
|
||||
{
|
||||
if self.last == Last::Destructive {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.last == Last::Weak {
|
||||
if let Some(i) =
|
||||
self.staged.iter().position(|(prev_item, _, prev_weakness)| {
|
||||
prev_weakness.map_or(false, |prev_weakness| {
|
||||
weakness < prev_weakness
|
||||
|| (weakness == prev_weakness && item > *prev_item)
|
||||
})
|
||||
})
|
||||
{
|
||||
self.staged.remove(i);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.staged.push((item, styles, Some(weakness)));
|
||||
self.last = Last::Weak;
|
||||
}
|
||||
|
||||
/// Forces nearby weak items to collapse.
|
||||
@ -90,8 +101,8 @@ impl<'a, T> CollapsingBuilder<'a, T> {
|
||||
/// Push the staged items, filtering out weak items if `supportive` is
|
||||
/// false.
|
||||
fn flush(&mut self, supportive: bool) {
|
||||
for (item, styles, strength) in self.staged.drain(..) {
|
||||
if supportive || strength.is_none() {
|
||||
for (item, styles, meta) in self.staged.drain(..) {
|
||||
if supportive || meta.is_none() {
|
||||
self.builder.push(item, styles);
|
||||
}
|
||||
}
|
||||
@ -103,3 +114,64 @@ impl<'a, T> Default for CollapsingBuilder<'a, T> {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::library::layout::FlowChild;
|
||||
use crate::library::prelude::*;
|
||||
|
||||
#[track_caller]
|
||||
fn test<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()]);
|
||||
}
|
||||
}
|
||||
|
@ -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::Parbreak => {}
|
||||
Content::Colbreak { weak } => {
|
||||
if *weak {
|
||||
self.0.weak(FlowChild::Colbreak, styles, 0);
|
||||
} else {
|
||||
self.0.destructive(FlowChild::Colbreak, styles);
|
||||
}
|
||||
}
|
||||
Content::Colbreak => {
|
||||
self.0.destructive(FlowChild::Colbreak, styles);
|
||||
}
|
||||
Content::Vertical(kind) => {
|
||||
let child = FlowChild::Spacing(*kind);
|
||||
if kind.is_fractional() {
|
||||
&Content::Vertical { amount, weak, generated } => {
|
||||
let child = FlowChild::Spacing(amount);
|
||||
let frac = amount.is_fractional();
|
||||
if weak {
|
||||
let weakness = 1 + u8::from(frac) + 2 * u8::from(generated);
|
||||
self.0.weak(child, styles, weakness);
|
||||
} else if frac {
|
||||
self.0.destructive(child, styles);
|
||||
} else {
|
||||
self.0.ignorant(child, styles);
|
||||
@ -512,6 +539,18 @@ impl<'a> FlowBuilder<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) {
|
||||
let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) {
|
||||
styles.get(ParNode::LEADING).into()
|
||||
} else {
|
||||
styles.get(ParNode::SPACING).into()
|
||||
};
|
||||
|
||||
self.0.weak(FlowChild::Spacing(amount), styles, 5);
|
||||
self.0.supportive(FlowChild::Node(par.pack()), styles);
|
||||
self.0.weak(FlowChild::Spacing(amount), styles, 5);
|
||||
}
|
||||
|
||||
fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) {
|
||||
let (flow, shared) = self.0.finish();
|
||||
let styles = if flow.is_empty() { styles } else { shared };
|
||||
@ -530,24 +569,34 @@ struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>);
|
||||
|
||||
impl<'a> ParBuilder<'a> {
|
||||
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
|
||||
// Weak par elements:
|
||||
// Weakness | Element
|
||||
// 0 | weak fractional spacing
|
||||
// 1 | weak spacing
|
||||
// 2 | space
|
||||
|
||||
match content {
|
||||
Content::Space => {
|
||||
self.0.weak(ParChild::Text(' '.into()), 0, styles);
|
||||
self.0.weak(ParChild::Text(' '.into()), styles, 2);
|
||||
}
|
||||
Content::Linebreak(justified) => {
|
||||
let c = if *justified { '\u{2028}' } else { '\n' };
|
||||
&Content::Linebreak { justified } => {
|
||||
let c = if justified { '\u{2028}' } else { '\n' };
|
||||
self.0.destructive(ParChild::Text(c.into()), styles);
|
||||
}
|
||||
Content::Horizontal(kind) => {
|
||||
let child = ParChild::Spacing(*kind);
|
||||
if kind.is_fractional() {
|
||||
&Content::Horizontal { amount, weak } => {
|
||||
let child = ParChild::Spacing(amount);
|
||||
let frac = amount.is_fractional();
|
||||
if weak {
|
||||
let weakness = u8::from(!frac);
|
||||
self.0.weak(child, styles, weakness);
|
||||
} else if frac {
|
||||
self.0.destructive(child, styles);
|
||||
} else {
|
||||
self.0.ignorant(child, styles);
|
||||
}
|
||||
}
|
||||
Content::Quote(double) => {
|
||||
self.0.supportive(ParChild::Quote(*double), styles);
|
||||
&Content::Quote { double } => {
|
||||
self.0.supportive(ParChild::Quote { double }, styles);
|
||||
}
|
||||
Content::Text(text) => {
|
||||
self.0.supportive(ParChild::Text(text.clone()), styles);
|
||||
@ -575,7 +624,7 @@ impl<'a> ParBuilder<'a> {
|
||||
.items()
|
||||
.find_map(|child| match child {
|
||||
ParChild::Spacing(_) => None,
|
||||
ParChild::Text(_) | ParChild::Quote(_) => Some(true),
|
||||
ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
|
||||
ParChild::Node(_) => Some(false),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
@ -585,10 +634,8 @@ impl<'a> ParBuilder<'a> {
|
||||
.items()
|
||||
.rev()
|
||||
.find_map(|child| match child {
|
||||
FlowChild::Leading => None,
|
||||
FlowChild::Parbreak => None,
|
||||
FlowChild::Spacing(_) => None,
|
||||
FlowChild::Node(node) => Some(node.is::<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 {
|
||||
if self.items.is_empty() {
|
||||
match content {
|
||||
Content::Space => {}
|
||||
Content::Item(_) => {}
|
||||
Content::Parbreak => self.attachable = false,
|
||||
_ => self.attachable = true,
|
||||
}
|
||||
}
|
||||
|
||||
match content {
|
||||
Content::Space if !self.items.is_empty() => {
|
||||
self.staged.push((content, styles));
|
||||
}
|
||||
Content::Parbreak if !self.items.is_empty() => {
|
||||
self.staged.push((content, styles));
|
||||
}
|
||||
Content::Item(item)
|
||||
if self
|
||||
.items
|
||||
@ -634,6 +685,9 @@ impl<'a> ListBuilder<'a> {
|
||||
self.items.push(item.clone(), styles);
|
||||
self.tight &= self.staged.drain(..).all(|(t, _)| *t != Content::Parbreak);
|
||||
}
|
||||
Content::Space | Content::Parbreak if !self.items.is_empty() => {
|
||||
self.staged.push((content, styles));
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
@ -647,10 +701,17 @@ impl<'a> ListBuilder<'a> {
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let start = 1;
|
||||
let tight = self.tight;
|
||||
let attached = tight && self.attachable;
|
||||
|
||||
let content = match kind {
|
||||
UNORDERED => Content::show(ListNode::<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![],
|
||||
}
|
||||
}
|
||||
|
@ -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(')')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(')')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(_) => {
|
||||
|
@ -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);
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 => {}
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
tests/ref/structure/attach.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@ -29,6 +29,10 @@
|
||||
// Error: 2-18 cannot apply '<=' to relative length and ratio
|
||||
{30% + 1pt <= 40%}
|
||||
|
||||
---
|
||||
// Error: 2-13 cannot apply '<=' to length and length
|
||||
{1em <= 10pt}
|
||||
|
||||
---
|
||||
// Special messages for +, -, * and /.
|
||||
// Error: 03-10 cannot add integer and string
|
||||
|
@ -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.
|
||||
|
@ -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.*
|
||||
]
|
||||
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -18,25 +18,17 @@
|
||||
#set page(width: 50pt, margins: 0pt)
|
||||
#stack(dir: btt, ..items)
|
||||
|
||||
---
|
||||
// Test RTL alignment.
|
||||
#set page(width: 50pt, margins: 5pt)
|
||||
#set text(8pt)
|
||||
#stack(dir: rtl,
|
||||
align(center, [A]),
|
||||
align(left, [B]),
|
||||
[C],
|
||||
)
|
||||
|
||||
---
|
||||
// Test spacing.
|
||||
#set page(width: 50pt, margins: 0pt)
|
||||
#set par(leading: 5pt)
|
||||
|
||||
#let x = square(size: 10pt, fill: eastern)
|
||||
#stack(dir: rtl, spacing: 5pt, x, x, x)
|
||||
#stack(dir: ltr, x, 20%, x, 20%, x)
|
||||
#stack(dir: ltr, spacing: 5pt, x, x, 7pt, 3pt, x)
|
||||
#stack(
|
||||
spacing: 5pt,
|
||||
stack(dir: rtl, spacing: 5pt, x, x, x),
|
||||
stack(dir: ltr, x, 20%, x, 20%, x),
|
||||
stack(dir: ltr, spacing: 5pt, x, x, 7pt, 3pt, x),
|
||||
)
|
||||
|
||||
---
|
||||
// Test overflow.
|
||||
@ -45,3 +37,15 @@
|
||||
rect(width: 40pt, height: 20pt, fill: conifer),
|
||||
rect(width: 30pt, height: 13pt, fill: forest),
|
||||
))
|
||||
|
||||
---
|
||||
// Test aligning things in RTL stack with align function & fr units.
|
||||
#set page(width: 50pt, margins: 5pt)
|
||||
#set text(8pt)
|
||||
#stack(dir: rtl, 1fr, [A], 1fr, [B], [C])
|
||||
#v(5pt)
|
||||
#stack(dir: rtl,
|
||||
align(center, [A]),
|
||||
align(left, [B]),
|
||||
[C],
|
||||
)
|
||||
|
56
tests/typ/structure/attach.typ
Normal 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
|
@ -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],
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
---
|
||||
|
@ -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.
|
||||
|
@ -3,6 +3,7 @@
|
||||
---
|
||||
#set heading(
|
||||
size: 10pt,
|
||||
around: 0.65em,
|
||||
fill: lvl => if even(lvl) { red } else { blue },
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
])
|
||||
|
@ -6,36 +6,45 @@
|
||||
To the right! Where the sunlight peeks behind the mountain.
|
||||
|
||||
---
|
||||
// Test that explicit paragraph break respects active styles.
|
||||
#set par(spacing: 0pt)
|
||||
[#set par(spacing: 100pt);First]
|
||||
|
||||
[#set par(spacing: 100pt);Second]
|
||||
#set par(spacing: 13.5pt)
|
||||
|
||||
Third
|
||||
|
||||
---
|
||||
// Test that paragraph spacing uses correct set rule.
|
||||
Hello
|
||||
|
||||
#set par(spacing: 100pt)
|
||||
World
|
||||
#set par(spacing: 0pt, leading: 0pt)
|
||||
|
||||
You
|
||||
|
||||
---
|
||||
// Test that paragraphs break due to incompatibility has correct spacing.
|
||||
A #set par(spacing: 0pt, leading: 0pt); B #parbreak() C
|
||||
|
||||
---
|
||||
// Test weird metrics.
|
||||
#set par(spacing: 1em, leading: 0pt)
|
||||
// Test changing leading and spacing.
|
||||
#set par(spacing: 1em, leading: 2pt)
|
||||
But, soft! what light through yonder window breaks?
|
||||
|
||||
It is the east, and Juliet is the sun.
|
||||
|
||||
---
|
||||
// Test that largest paragraph spacing wins.
|
||||
#set par(spacing: 2.5pt)
|
||||
[#set par(spacing: 15pt);First]
|
||||
[#set par(spacing: 7.5pt);Second]
|
||||
Third
|
||||
|
||||
Fourth
|
||||
|
||||
---
|
||||
// Test that paragraph spacing loses against block spacing.
|
||||
#set par(spacing: 100pt)
|
||||
#set table(around: 5pt)
|
||||
Hello
|
||||
#table(columns: 4, secondary: silver)[A][B][C][D]
|
||||
|
||||
---
|
||||
// While we're at it, test the larger block spacing wins.
|
||||
#set raw(around: 15pt)
|
||||
#set math(around: 7.5pt)
|
||||
#set list(around: 2.5pt)
|
||||
#set par(spacing: 0pt)
|
||||
|
||||
```rust
|
||||
fn main() {}
|
||||
```
|
||||
|
||||
$[ x + y = z ]$
|
||||
|
||||
- List
|
||||
|
||||
Paragraph
|
||||
|
||||
---
|
||||
// Error: 17-20 must be horizontal
|
||||
#set par(align: top)
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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" }]
|
||||
|