Set Rules Episode VII: The Set Awakens

This commit is contained in:
Laurenz 2021-12-15 20:27:41 +01:00
parent 244ad386ec
commit 2a3d0f4b39
56 changed files with 678 additions and 451 deletions

View File

@ -1,5 +1,5 @@
// Configuration with `page` and `font` functions. // Configuration with `page` and `font` functions.
#page(width: 450pt, margins: 1cm) #set page(width: 450pt, margins: 1cm)
// There are variables and they can take normal values like strings, ... // There are variables and they can take normal values like strings, ...
#let city = "Berlin" #let city = "Berlin"

101
src/eval/class.rs Normal file
View File

@ -0,0 +1,101 @@
use std::fmt::{self, Debug, Formatter, Write};
use std::marker::PhantomData;
use std::rc::Rc;
use super::{Args, EvalContext, Node, Styles};
use crate::diag::TypResult;
use crate::util::EcoString;
/// A class of nodes.
#[derive(Clone)]
pub struct Class(Rc<Inner<dyn Bounds>>);
/// The unsized structure behind the [`Rc`].
struct Inner<T: ?Sized> {
name: EcoString,
dispatch: T,
}
impl Class {
/// Create a new class.
pub fn new<T>(name: EcoString) -> Self
where
T: Construct + Set + 'static,
{
Self(Rc::new(Inner {
name,
dispatch: Dispatch::<T>(PhantomData),
}))
}
/// The name of the class.
pub fn name(&self) -> &EcoString {
&self.0.name
}
/// Construct an instance of the class.
pub fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
self.0.dispatch.construct(ctx, args)
}
/// Execute the class's set rule.
pub fn set(&self, styles: &mut Styles, args: &mut Args) -> TypResult<()> {
self.0.dispatch.set(styles, args)
}
}
impl Debug for Class {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("<class ")?;
f.write_str(&self.0.name)?;
f.write_char('>')
}
}
impl PartialEq for Class {
fn eq(&self, other: &Self) -> bool {
// We cast to thin pointers for comparison.
std::ptr::eq(
Rc::as_ptr(&self.0) as *const (),
Rc::as_ptr(&other.0) as *const (),
)
}
}
/// Construct an instance of a class.
pub trait Construct {
/// Construct an instance of this class from the arguments.
///
/// This is passed only the arguments that remain after execution of the
/// class's set rule.
fn construct(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node>;
}
/// Set style properties of a class.
pub trait Set {
/// Parse the arguments and insert style properties of this class into the
/// given style map.
fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()>;
}
/// Zero-sized struct whose vtable contains the constructor and set rule of a
/// class.
struct Dispatch<T>(PhantomData<T>);
trait Bounds {
fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node>;
fn set(&self, styles: &mut Styles, args: &mut Args) -> TypResult<()>;
}
impl<T> Bounds for Dispatch<T>
where
T: Construct + Set,
{
fn construct(&self, ctx: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
T::construct(ctx, args)
}
fn set(&self, styles: &mut Styles, args: &mut Args) -> TypResult<()> {
T::set(styles, args)
}
}

View File

@ -9,6 +9,7 @@ mod value;
#[macro_use] #[macro_use]
mod styles; mod styles;
mod capture; mod capture;
mod class;
mod function; mod function;
mod node; mod node;
mod ops; mod ops;
@ -16,6 +17,7 @@ mod scope;
pub use array::*; pub use array::*;
pub use capture::*; pub use capture::*;
pub use class::*;
pub use dict::*; pub use dict::*;
pub use function::*; pub use function::*;
pub use node::*; pub use node::*;
@ -54,7 +56,7 @@ pub fn eval(ctx: &mut Context, source: SourceId, markup: &Markup) -> TypResult<M
pub struct Module { pub struct Module {
/// The top-level definitions that were bound in this module. /// The top-level definitions that were bound in this module.
pub scope: Scope, pub scope: Scope,
/// The node defined by this module. /// The module's layoutable contents.
pub node: Node, pub node: Node,
} }
@ -288,6 +290,7 @@ impl Eval for Expr {
Self::Unary(v) => v.eval(ctx), Self::Unary(v) => v.eval(ctx),
Self::Binary(v) => v.eval(ctx), Self::Binary(v) => v.eval(ctx),
Self::Let(v) => v.eval(ctx), Self::Let(v) => v.eval(ctx),
Self::Set(v) => v.eval(ctx),
Self::If(v) => v.eval(ctx), Self::If(v) => v.eval(ctx),
Self::While(v) => v.eval(ctx), Self::While(v) => v.eval(ctx),
Self::For(v) => v.eval(ctx), Self::For(v) => v.eval(ctx),
@ -474,9 +477,17 @@ impl Eval for CallExpr {
Ok(value) Ok(value)
} }
Value::Class(class) => {
let mut styles = Styles::new();
class.set(&mut styles, &mut args)?;
let node = class.construct(ctx, &mut args)?;
args.finish()?;
Ok(Value::Node(node.styled(styles)))
}
v => bail!( v => bail!(
self.callee().span(), self.callee().span(),
"expected function or collection, found {}", "expected callable or collection, found {}",
v.type_name(), v.type_name(),
), ),
} }
@ -643,6 +654,19 @@ impl Eval for LetExpr {
} }
} }
impl Eval for SetExpr {
type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let class = self.class();
let class = class.eval(ctx)?.cast::<Class>().at(class.span())?;
let mut args = self.args().eval(ctx)?;
class.set(&mut ctx.styles, &mut args)?;
args.finish()?;
Ok(Value::None)
}
}
impl Eval for IfExpr { impl Eval for IfExpr {
type Output = Value; type Output = Value;

View File

@ -36,6 +36,8 @@ pub enum Node {
Inline(PackedNode), Inline(PackedNode),
/// A block node. /// A block node.
Block(PackedNode), Block(PackedNode),
/// A page node.
Page(PackedNode),
/// A sequence of nodes (which may themselves contain sequences). /// A sequence of nodes (which may themselves contain sequences).
Sequence(Vec<(Self, Styles)>), Sequence(Vec<(Self, Styles)>),
} }
@ -214,6 +216,14 @@ impl Packer {
Node::Block(block) => { Node::Block(block) => {
self.push_block(block.styled(styles)); self.push_block(block.styled(styles));
} }
Node::Page(flow) => {
if self.top {
self.pagebreak();
self.pages.push(PageNode { child: flow, styles });
} else {
self.push_block(flow.styled(styles));
}
}
Node::Sequence(list) => { Node::Sequence(list) => {
// For a list of nodes, we apply the list's styles to each node // For a list of nodes, we apply the list's styles to each node
// individually. // individually.

View File

@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Formatter};
use std::iter; use std::iter;
use std::rc::Rc; use std::rc::Rc;
use super::{Args, EvalContext, Function, Value}; use super::{Args, Class, Construct, EvalContext, Function, Set, Value};
use crate::diag::TypResult; use crate::diag::TypResult;
use crate::util::EcoString; use crate::util::EcoString;
@ -88,15 +88,6 @@ impl Scope {
self.values.insert(var.into(), Rc::new(cell)); self.values.insert(var.into(), Rc::new(cell));
} }
/// Define a constant function.
pub fn def_func<F>(&mut self, name: impl Into<EcoString>, f: F)
where
F: Fn(&mut EvalContext, &mut Args) -> TypResult<Value> + 'static,
{
let name = name.into();
self.def_const(name.clone(), Function::new(Some(name), f));
}
/// Define a mutable variable with a value. /// Define a mutable variable with a value.
pub fn def_mut(&mut self, var: impl Into<EcoString>, value: impl Into<Value>) { pub fn def_mut(&mut self, var: impl Into<EcoString>, value: impl Into<Value>) {
self.values.insert(var.into(), Rc::new(RefCell::new(value.into()))); self.values.insert(var.into(), Rc::new(RefCell::new(value.into())));
@ -107,6 +98,24 @@ impl Scope {
self.values.insert(var.into(), slot); self.values.insert(var.into(), slot);
} }
/// Define a constant function.
pub fn def_func<F>(&mut self, name: &str, f: F)
where
F: Fn(&mut EvalContext, &mut Args) -> TypResult<Value> + 'static,
{
let name = EcoString::from(name);
self.def_const(name.clone(), Function::new(Some(name), f));
}
/// Define a constant class.
pub fn def_class<T>(&mut self, name: &str)
where
T: Construct + Set + 'static,
{
let name = EcoString::from(name);
self.def_const(name.clone(), Class::new::<T>(name));
}
/// Look up the value of a variable. /// Look up the value of a variable.
pub fn get(&self, var: &str) -> Option<&Slot> { pub fn get(&self, var: &str) -> Option<&Slot> {
self.values.get(var) self.values.get(var)

View File

@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::rc::Rc; use std::rc::Rc;
use super::{ops, Array, Dict, Function, Node}; use super::{ops, Array, Class, Dict, Function, Node};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::{Angle, Color, Fractional, Length, Linear, Relative, RgbaColor}; use crate::geom::{Angle, Color, Fractional, Length, Linear, Relative, RgbaColor};
use crate::layout::Layout; use crate::layout::Layout;
@ -46,6 +46,8 @@ pub enum Value {
Node(Node), Node(Node),
/// An executable function. /// An executable function.
Func(Function), Func(Function),
/// A class of nodes.
Class(Class),
/// A dynamic value. /// A dynamic value.
Dyn(Dynamic), Dyn(Dynamic),
} }
@ -86,6 +88,7 @@ impl Value {
Self::Dict(_) => Dict::TYPE_NAME, Self::Dict(_) => Dict::TYPE_NAME,
Self::Node(_) => Node::TYPE_NAME, Self::Node(_) => Node::TYPE_NAME,
Self::Func(_) => Function::TYPE_NAME, Self::Func(_) => Function::TYPE_NAME,
Self::Class(_) => Class::TYPE_NAME,
Self::Dyn(v) => v.type_name(), Self::Dyn(v) => v.type_name(),
} }
} }
@ -148,6 +151,7 @@ impl Debug for Value {
Self::Dict(v) => Debug::fmt(v, f), Self::Dict(v) => Debug::fmt(v, f),
Self::Node(_) => f.pad("<template>"), Self::Node(_) => f.pad("<template>"),
Self::Func(v) => Debug::fmt(v, f), Self::Func(v) => Debug::fmt(v, f),
Self::Class(v) => Debug::fmt(v, f),
Self::Dyn(v) => Debug::fmt(v, f), Self::Dyn(v) => Debug::fmt(v, f),
} }
} }
@ -394,6 +398,7 @@ primitive! { Array: "array", Array }
primitive! { Dict: "dictionary", Dict } primitive! { Dict: "dictionary", Dict }
primitive! { Node: "template", Node } primitive! { Node: "template", Node }
primitive! { Function: "function", Func } primitive! { Function: "function", Func }
primitive! { Class: "class", Class }
impl Cast<Value> for Value { impl Cast<Value> for Value {
fn is(_: &Value) -> bool { fn is(_: &Value) -> bool {

View File

@ -27,7 +27,9 @@ mod prelude {
pub use std::rc::Rc; pub use std::rc::Rc;
pub use crate::diag::{At, TypResult}; pub use crate::diag::{At, TypResult};
pub use crate::eval::{Args, EvalContext, Node, Property, Smart, Styles, Value}; pub use crate::eval::{
Args, Construct, EvalContext, Node, Property, Set, Smart, Styles, Value,
};
pub use crate::frame::*; pub use crate::frame::*;
pub use crate::geom::*; pub use crate::geom::*;
pub use crate::layout::*; pub use crate::layout::*;
@ -60,22 +62,24 @@ use crate::geom::*;
pub fn new() -> Scope { pub fn new() -> Scope {
let mut std = Scope::new(); let mut std = Scope::new();
// Text. // Classes.
std.def_func("font", font); std.def_class::<PageNode>("page");
std.def_func("par", par); std.def_class::<ParNode>("par");
std.def_func("parbreak", parbreak); std.def_class::<TextNode>("text");
// Text functions.
std.def_func("strike", strike); std.def_func("strike", strike);
std.def_func("underline", underline); std.def_func("underline", underline);
std.def_func("overline", overline); std.def_func("overline", overline);
std.def_func("link", link); std.def_func("link", link);
// Layout. // Layout functions.
std.def_func("page", page);
std.def_func("pagebreak", pagebreak);
std.def_func("h", h); std.def_func("h", h);
std.def_func("v", v); std.def_func("v", v);
std.def_func("box", box_); std.def_func("box", box_);
std.def_func("block", block); std.def_func("block", block);
std.def_func("pagebreak", pagebreak);
std.def_func("parbreak", parbreak);
std.def_func("stack", stack); std.def_func("stack", stack);
std.def_func("grid", grid); std.def_func("grid", grid);
std.def_func("pad", pad); std.def_func("pad", pad);
@ -85,14 +89,14 @@ pub fn new() -> Scope {
std.def_func("scale", scale); std.def_func("scale", scale);
std.def_func("rotate", rotate); std.def_func("rotate", rotate);
// Elements. // Element functions.
std.def_func("image", image); std.def_func("image", image);
std.def_func("rect", rect); std.def_func("rect", rect);
std.def_func("square", square); std.def_func("square", square);
std.def_func("ellipse", ellipse); std.def_func("ellipse", ellipse);
std.def_func("circle", circle); std.def_func("circle", circle);
// Utility. // Utility functions.
std.def_func("assert", assert); std.def_func("assert", assert);
std.def_func("type", type_); std.def_func("type", type_);
std.def_func("repr", repr); std.def_func("repr", repr);
@ -110,14 +114,14 @@ pub fn new() -> Scope {
std.def_func("len", len); std.def_func("len", len);
std.def_func("sorted", sorted); std.def_func("sorted", sorted);
// Colors. // Predefined colors.
std.def_const("white", RgbaColor::WHITE); std.def_const("white", RgbaColor::WHITE);
std.def_const("black", RgbaColor::BLACK); std.def_const("black", RgbaColor::BLACK);
std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF));
std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
// Arbitrary constants. // Other constants.
std.def_const("ltr", Dir::LTR); std.def_const("ltr", Dir::LTR);
std.def_const("rtl", Dir::RTL); std.def_const("rtl", Dir::RTL);
std.def_const("ttb", Dir::TTB); std.def_const("ttb", Dir::TTB);

View File

@ -6,53 +6,6 @@ use std::str::FromStr;
use super::prelude::*; use super::prelude::*;
use super::PadNode; use super::PadNode;
/// `page`: Configure pages.
pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
castable! {
Paper,
Expected: "string",
Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?,
}
let body: Option<Node> = args.find();
let mut map = Styles::new();
let styles = match body {
Some(_) => &mut map,
None => &mut ctx.styles,
};
if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
styles.set(PageNode::CLASS, paper.class());
styles.set(PageNode::WIDTH, Smart::Custom(paper.width()));
styles.set(PageNode::HEIGHT, Smart::Custom(paper.height()));
}
if let Some(width) = args.named("width")? {
styles.set(PageNode::CLASS, PaperClass::Custom);
styles.set(PageNode::WIDTH, width);
}
if let Some(height) = args.named("height")? {
styles.set(PageNode::CLASS, PaperClass::Custom);
styles.set(PageNode::HEIGHT, height);
}
let margins = args.named("margins")?;
set!(styles, PageNode::FLIPPED => args.named("flipped")?);
set!(styles, PageNode::LEFT => args.named("left")?.or(margins));
set!(styles, PageNode::TOP => args.named("top")?.or(margins));
set!(styles, PageNode::RIGHT => args.named("right")?.or(margins));
set!(styles, PageNode::BOTTOM => args.named("bottom")?.or(margins));
set!(styles, PageNode::FILL => args.named("fill")?);
Ok(match body {
Some(body) => Value::block(body.into_block().styled(map)),
None => Value::None,
})
}
/// `pagebreak`: Start a new page. /// `pagebreak`: Start a new page.
pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
Ok(Value::Node(Node::Pagebreak)) Ok(Value::Node(Node::Pagebreak))
@ -90,6 +43,45 @@ properties! {
FILL: Option<Paint> = None, FILL: Option<Paint> = None,
} }
impl Construct for PageNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
// TODO(set): Make sure it's really a page so that it doesn't merge
// with adjacent pages.
Ok(Node::Page(args.expect::<Node>("body")?.into_block()))
}
}
impl Set for PageNode {
fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> {
if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
styles.set(PageNode::CLASS, paper.class());
styles.set(PageNode::WIDTH, Smart::Custom(paper.width()));
styles.set(PageNode::HEIGHT, Smart::Custom(paper.height()));
}
if let Some(width) = args.named("width")? {
styles.set(PageNode::CLASS, PaperClass::Custom);
styles.set(PageNode::WIDTH, width);
}
if let Some(height) = args.named("height")? {
styles.set(PageNode::CLASS, PaperClass::Custom);
styles.set(PageNode::HEIGHT, height);
}
let margins = args.named("margins")?;
set!(styles, PageNode::FLIPPED => args.named("flipped")?);
set!(styles, PageNode::LEFT => args.named("left")?.or(margins));
set!(styles, PageNode::TOP => args.named("top")?.or(margins));
set!(styles, PageNode::RIGHT => args.named("right")?.or(margins));
set!(styles, PageNode::BOTTOM => args.named("bottom")?.or(margins));
set!(styles, PageNode::FILL => args.named("fill")?);
Ok(())
}
}
impl PageNode { impl PageNode {
/// Layout the page run into a sequence of frames, one per page. /// Layout the page run into a sequence of frames, one per page.
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
@ -182,6 +174,12 @@ impl Default for Paper {
} }
} }
castable! {
Paper,
Expected: "string",
Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?,
}
/// Defines default margins for a class of related papers. /// Defines default margins for a class of related papers.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum PaperClass { pub enum PaperClass {

View File

@ -9,8 +9,37 @@ use super::prelude::*;
use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode}; use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode};
use crate::util::{EcoString, RangeExt, RcExt, SliceExt}; use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
/// `par`: Configure paragraphs. /// `parbreak`: Start a new paragraph.
pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
Ok(Value::Node(Node::Parbreak))
}
/// A node that arranges its children into a paragraph.
#[derive(Hash)]
pub struct ParNode(pub Vec<ParChild>);
properties! {
ParNode,
/// The direction for text and inline objects.
DIR: Dir = Dir::LTR,
/// How to align text and inline objects in their line.
ALIGN: Align = Align::Left,
/// The spacing between lines (dependent on scaled font size).
LEADING: Linear = Relative::new(0.65).into(),
/// The spacing between paragraphs (dependent on scaled font size).
SPACING: Linear = Relative::new(1.2).into(),
}
impl Construct for ParNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
// Lift to a block so that it doesn't merge with adjacent stuff.
Ok(Node::Block(args.expect::<Node>("body")?.into_block()))
}
}
impl Set for ParNode {
fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> {
let spacing = args.named("spacing")?; let spacing = args.named("spacing")?;
let leading = args.named("leading")?; let leading = args.named("leading")?;
@ -41,34 +70,13 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right }); align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
} }
set!(ctx.styles, ParNode::DIR => dir); set!(styles, ParNode::DIR => dir);
set!(ctx.styles, ParNode::ALIGN => align); set!(styles, ParNode::ALIGN => align);
set!(ctx.styles, ParNode::LEADING => leading); set!(styles, ParNode::LEADING => leading);
set!(ctx.styles, ParNode::SPACING => spacing); set!(styles, ParNode::SPACING => spacing);
Ok(Value::None) Ok(())
} }
/// `parbreak`: Start a new paragraph.
pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
Ok(Value::Node(Node::Parbreak))
}
/// A node that arranges its children into a paragraph.
#[derive(Hash)]
pub struct ParNode(pub Vec<ParChild>);
properties! {
ParNode,
/// The direction for text and inline objects.
DIR: Dir = Dir::LTR,
/// How to align text and inline objects in their line.
ALIGN: Align = Align::Left,
/// The spacing between lines (dependent on scaled font size).
LEADING: Linear = Relative::new(0.65).into(),
/// The spacing between paragraphs (dependent on scaled font size).
SPACING: Linear = Relative::new(1.2).into(),
} }
impl Layout for ParNode { impl Layout for ParNode {

View File

@ -15,54 +15,6 @@ use crate::font::{
use crate::geom::{Dir, Em, Length, Point, Size}; use crate::geom::{Dir, Em, Length, Point, Size};
use crate::util::{EcoString, SliceExt}; use crate::util::{EcoString, SliceExt};
/// `font`: Configure the font.
pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let body = args.find::<Node>();
let mut map = Styles::new();
let styles = match body {
Some(_) => &mut map,
None => &mut ctx.styles,
};
let list = args.named("family")?.or_else(|| {
let families: Vec<_> = args.all().collect();
(!families.is_empty()).then(|| families)
});
set!(styles, TextNode::FAMILY_LIST => list);
set!(styles, TextNode::SERIF_LIST => args.named("serif")?);
set!(styles, TextNode::SANS_SERIF_LIST => args.named("sans-serif")?);
set!(styles, TextNode::MONOSPACE_LIST => args.named("monospace")?);
set!(styles, TextNode::FALLBACK => args.named("fallback")?);
set!(styles, TextNode::STYLE => args.named("style")?);
set!(styles, TextNode::WEIGHT => args.named("weight")?);
set!(styles, TextNode::STRETCH => args.named("stretch")?);
set!(styles, TextNode::FILL => args.named("fill")?.or_else(|| args.find()));
set!(styles, TextNode::SIZE => args.named("size")?.or_else(|| args.find()));
set!(styles, TextNode::TRACKING => args.named("tracking")?.map(Em::new));
set!(styles, TextNode::TOP_EDGE => args.named("top-edge")?);
set!(styles, TextNode::BOTTOM_EDGE => args.named("bottom-edge")?);
set!(styles, TextNode::KERNING => args.named("kerning")?);
set!(styles, TextNode::SMALLCAPS => args.named("smallcaps")?);
set!(styles, TextNode::ALTERNATES => args.named("alternates")?);
set!(styles, TextNode::STYLISTIC_SET => args.named("stylistic-set")?);
set!(styles, TextNode::LIGATURES => args.named("ligatures")?);
set!(styles, TextNode::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?);
set!(styles, TextNode::HISTORICAL_LIGATURES => args.named("historical-ligatures")?);
set!(styles, TextNode::NUMBER_TYPE => args.named("number-type")?);
set!(styles, TextNode::NUMBER_WIDTH => args.named("number-width")?);
set!(styles, TextNode::NUMBER_POSITION => args.named("number-position")?);
set!(styles, TextNode::SLASHED_ZERO => args.named("slashed-zero")?);
set!(styles, TextNode::FRACTIONS => args.named("fractions")?);
set!(styles, TextNode::FEATURES => args.named("features")?);
Ok(match body {
Some(body) => Value::Node(body.styled(map)),
None => Value::None,
})
}
/// `strike`: Typeset striken-through text. /// `strike`: Typeset striken-through text.
pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
line_impl(args, LineKind::Strikethrough) line_impl(args, LineKind::Strikethrough)
@ -172,6 +124,53 @@ properties! {
FEATURES: Vec<(Tag, u32)> = vec![], FEATURES: Vec<(Tag, u32)> = vec![],
} }
impl Construct for TextNode {
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
// We don't need to do anything more here because the whole point of the
// text constructor is to apply the styles and that happens
// automatically during class construction.
args.expect::<Node>("body")
}
}
impl Set for TextNode {
fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> {
let list = args.named("family")?.or_else(|| {
let families: Vec<_> = args.all().collect();
(!families.is_empty()).then(|| families)
});
set!(styles, TextNode::FAMILY_LIST => list);
set!(styles, TextNode::SERIF_LIST => args.named("serif")?);
set!(styles, TextNode::SANS_SERIF_LIST => args.named("sans-serif")?);
set!(styles, TextNode::MONOSPACE_LIST => args.named("monospace")?);
set!(styles, TextNode::FALLBACK => args.named("fallback")?);
set!(styles, TextNode::STYLE => args.named("style")?);
set!(styles, TextNode::WEIGHT => args.named("weight")?);
set!(styles, TextNode::STRETCH => args.named("stretch")?);
set!(styles, TextNode::FILL => args.named("fill")?.or_else(|| args.find()));
set!(styles, TextNode::SIZE => args.named("size")?.or_else(|| args.find()));
set!(styles, TextNode::TRACKING => args.named("tracking")?.map(Em::new));
set!(styles, TextNode::TOP_EDGE => args.named("top-edge")?);
set!(styles, TextNode::BOTTOM_EDGE => args.named("bottom-edge")?);
set!(styles, TextNode::KERNING => args.named("kerning")?);
set!(styles, TextNode::SMALLCAPS => args.named("smallcaps")?);
set!(styles, TextNode::ALTERNATES => args.named("alternates")?);
set!(styles, TextNode::STYLISTIC_SET => args.named("stylistic-set")?);
set!(styles, TextNode::LIGATURES => args.named("ligatures")?);
set!(styles, TextNode::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?);
set!(styles, TextNode::HISTORICAL_LIGATURES => args.named("historical-ligatures")?);
set!(styles, TextNode::NUMBER_TYPE => args.named("number-type")?);
set!(styles, TextNode::NUMBER_WIDTH => args.named("number-width")?);
set!(styles, TextNode::NUMBER_POSITION => args.named("number-position")?);
set!(styles, TextNode::SLASHED_ZERO => args.named("slashed-zero")?);
set!(styles, TextNode::FRACTIONS => args.named("fractions")?);
set!(styles, TextNode::FEATURES => args.named("features")?);
Ok(())
}
}
impl Debug for TextNode { impl Debug for TextNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() { if f.alternate() {

View File

@ -110,12 +110,13 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
// Hashtag + keyword / identifier. // Hashtag + keyword / identifier.
NodeKind::Ident(_) NodeKind::Ident(_)
| NodeKind::Let | NodeKind::Let
| NodeKind::Set
| NodeKind::If | NodeKind::If
| NodeKind::While | NodeKind::While
| NodeKind::For | NodeKind::For
| NodeKind::Import | NodeKind::Import
| NodeKind::Include => { | NodeKind::Include => {
let stmt = matches!(token, NodeKind::Let | NodeKind::Import); let stmt = matches!(token, NodeKind::Let | NodeKind::Set | NodeKind::Import);
let group = if stmt { Group::Stmt } else { Group::Expr }; let group = if stmt { Group::Stmt } else { Group::Expr };
p.start_group(group); p.start_group(group);
@ -265,6 +266,7 @@ fn primary(p: &mut Parser, atomic: bool) -> ParseResult {
// Keywords. // Keywords.
Some(NodeKind::Let) => let_expr(p), Some(NodeKind::Let) => let_expr(p),
Some(NodeKind::Set) => set_expr(p),
Some(NodeKind::If) => if_expr(p), Some(NodeKind::If) => if_expr(p),
Some(NodeKind::While) => while_expr(p), Some(NodeKind::While) => while_expr(p),
Some(NodeKind::For) => for_expr(p), Some(NodeKind::For) => for_expr(p),
@ -507,45 +509,40 @@ fn block(p: &mut Parser) {
/// Parse a function call. /// Parse a function call.
fn call(p: &mut Parser, callee: Marker) -> ParseResult { fn call(p: &mut Parser, callee: Marker) -> ParseResult {
callee.perform(p, NodeKind::Call, |p| match p.peek_direct() { callee.perform(p, NodeKind::Call, |p| args(p, true, true))
Some(NodeKind::LeftParen | NodeKind::LeftBracket) => {
args(p, true);
Ok(())
}
_ => {
p.expected_at("argument list");
Err(())
}
})
} }
/// Parse the arguments to a function call. /// Parse the arguments to a function call.
fn args(p: &mut Parser, allow_template: bool) { fn args(p: &mut Parser, direct: bool, brackets: bool) -> ParseResult {
match if direct { p.peek_direct() } else { p.peek() } {
Some(NodeKind::LeftParen) => {}
Some(NodeKind::LeftBracket) if brackets => {}
_ => {
p.expected("argument list");
return Err(());
}
}
p.perform(NodeKind::CallArgs, |p| { p.perform(NodeKind::CallArgs, |p| {
if !allow_template || p.peek_direct() == Some(&NodeKind::LeftParen) { if p.at(&NodeKind::LeftParen) {
p.start_group(Group::Paren); p.start_group(Group::Paren);
collection(p); collection(p);
p.end_group(); p.end_group();
} }
while allow_template && p.peek_direct() == Some(&NodeKind::LeftBracket) { while brackets && p.peek_direct() == Some(&NodeKind::LeftBracket) {
template(p); template(p);
} }
}) });
Ok(())
} }
/// Parse a with expression. /// Parse a with expression.
fn with_expr(p: &mut Parser, marker: Marker) -> ParseResult { fn with_expr(p: &mut Parser, marker: Marker) -> ParseResult {
marker.perform(p, NodeKind::WithExpr, |p| { marker.perform(p, NodeKind::WithExpr, |p| {
p.eat_assert(&NodeKind::With); p.eat_assert(&NodeKind::With);
args(p, false, false)
if p.at(&NodeKind::LeftParen) {
args(p, false);
Ok(())
} else {
p.expected("argument list");
Err(())
}
}) })
} }
@ -587,6 +584,15 @@ fn let_expr(p: &mut Parser) -> ParseResult {
}) })
} }
/// Parse a set expression.
fn set_expr(p: &mut Parser) -> ParseResult {
p.perform(NodeKind::SetExpr, |p| {
p.eat_assert(&NodeKind::Set);
ident(p)?;
args(p, true, false)
})
}
/// Parse an if expresion. /// Parse an if expresion.
fn if_expr(p: &mut Parser) -> ParseResult { fn if_expr(p: &mut Parser) -> ParseResult {
p.perform(NodeKind::IfExpr, |p| { p.perform(NodeKind::IfExpr, |p| {
@ -612,8 +618,7 @@ fn while_expr(p: &mut Parser) -> ParseResult {
p.perform(NodeKind::WhileExpr, |p| { p.perform(NodeKind::WhileExpr, |p| {
p.eat_assert(&NodeKind::While); p.eat_assert(&NodeKind::While);
expr(p)?; expr(p)?;
body(p)?; body(p)
Ok(())
}) })
} }
@ -624,8 +629,7 @@ fn for_expr(p: &mut Parser) -> ParseResult {
for_pattern(p)?; for_pattern(p)?;
p.eat_expect(&NodeKind::In)?; p.eat_expect(&NodeKind::In)?;
expr(p)?; expr(p)?;
body(p)?; body(p)
Ok(())
}) })
} }
@ -664,9 +668,7 @@ fn import_expr(p: &mut Parser) -> ParseResult {
}; };
p.eat_expect(&NodeKind::From)?; p.eat_expect(&NodeKind::From)?;
expr(p)?; expr(p)
Ok(())
}) })
} }
@ -674,8 +676,7 @@ fn import_expr(p: &mut Parser) -> ParseResult {
fn include_expr(p: &mut Parser) -> ParseResult { fn include_expr(p: &mut Parser) -> ParseResult {
p.perform(NodeKind::IncludeExpr, |p| { p.perform(NodeKind::IncludeExpr, |p| {
p.eat_assert(&NodeKind::Include); p.eat_assert(&NodeKind::Include);
expr(p)?; expr(p)
Ok(())
}) })
} }

View File

@ -125,6 +125,7 @@ impl<'s> Parser<'s> {
} }
/// Eat, debug-asserting that the token is the given one. /// Eat, debug-asserting that the token is the given one.
#[track_caller]
pub fn eat_assert(&mut self, t: &NodeKind) { pub fn eat_assert(&mut self, t: &NodeKind) {
debug_assert_eq!(self.peek(), Some(t)); debug_assert_eq!(self.peek(), Some(t));
self.eat(); self.eat();
@ -199,6 +200,7 @@ impl<'s> Parser<'s> {
/// to `end_group`. /// to `end_group`.
/// ///
/// This panics if the current token does not start the given group. /// This panics if the current token does not start the given group.
#[track_caller]
pub fn start_group(&mut self, kind: Group) { pub fn start_group(&mut self, kind: Group) {
self.groups.push(GroupEntry { kind, prev_mode: self.tokens.mode() }); self.groups.push(GroupEntry { kind, prev_mode: self.tokens.mode() });
self.tokens.set_mode(match kind { self.tokens.set_mode(match kind {
@ -220,6 +222,7 @@ impl<'s> Parser<'s> {
/// End the parsing of a group. /// End the parsing of a group.
/// ///
/// This panics if no group was started. /// This panics if no group was started.
#[track_caller]
pub fn end_group(&mut self) { pub fn end_group(&mut self) {
let group_mode = self.tokens.mode(); let group_mode = self.tokens.mode();
let group = self.groups.pop().expect("no started group"); let group = self.groups.pop().expect("no started group");

View File

@ -527,6 +527,7 @@ fn keyword(ident: &str) -> Option<NodeKind> {
"or" => NodeKind::Or, "or" => NodeKind::Or,
"with" => NodeKind::With, "with" => NodeKind::With,
"let" => NodeKind::Let, "let" => NodeKind::Let,
"set" => NodeKind::Set,
"if" => NodeKind::If, "if" => NodeKind::If,
"else" => NodeKind::Else, "else" => NodeKind::Else,
"for" => NodeKind::For, "for" => NodeKind::For,

View File

@ -149,6 +149,11 @@ impl SourceFile {
Self::new(SourceId(0), Path::new(""), src.into()) Self::new(SourceId(0), Path::new(""), src.into())
} }
/// The root node of the untyped green tree.
pub fn root(&self) -> &Rc<GreenNode> {
&self.root
}
/// The file's abstract syntax tree. /// The file's abstract syntax tree.
pub fn ast(&self) -> TypResult<Markup> { pub fn ast(&self) -> TypResult<Markup> {
let red = RedNode::from_root(self.root.clone(), self.id); let red = RedNode::from_root(self.root.clone(), self.id);

View File

@ -211,6 +211,8 @@ pub enum Expr {
With(WithExpr), With(WithExpr),
/// A let expression: `let x = 1`. /// A let expression: `let x = 1`.
Let(LetExpr), Let(LetExpr),
/// A set expression: `set text(...)`.
Set(SetExpr),
/// An if-else expression: `if x { y } else { z }`. /// An if-else expression: `if x { y } else { z }`.
If(IfExpr), If(IfExpr),
/// A while loop expression: `while x { y }`. /// A while loop expression: `while x { y }`.
@ -238,6 +240,7 @@ impl TypedNode for Expr {
NodeKind::Closure => node.cast().map(Self::Closure), NodeKind::Closure => node.cast().map(Self::Closure),
NodeKind::WithExpr => node.cast().map(Self::With), NodeKind::WithExpr => node.cast().map(Self::With),
NodeKind::LetExpr => node.cast().map(Self::Let), NodeKind::LetExpr => node.cast().map(Self::Let),
NodeKind::SetExpr => node.cast().map(Self::Set),
NodeKind::IfExpr => node.cast().map(Self::If), NodeKind::IfExpr => node.cast().map(Self::If),
NodeKind::WhileExpr => node.cast().map(Self::While), NodeKind::WhileExpr => node.cast().map(Self::While),
NodeKind::ForExpr => node.cast().map(Self::For), NodeKind::ForExpr => node.cast().map(Self::For),
@ -262,6 +265,7 @@ impl TypedNode for Expr {
Self::Closure(v) => v.as_red(), Self::Closure(v) => v.as_red(),
Self::With(v) => v.as_red(), Self::With(v) => v.as_red(),
Self::Let(v) => v.as_red(), Self::Let(v) => v.as_red(),
Self::Set(v) => v.as_red(),
Self::If(v) => v.as_red(), Self::If(v) => v.as_red(),
Self::While(v) => v.as_red(), Self::While(v) => v.as_red(),
Self::For(v) => v.as_red(), Self::For(v) => v.as_red(),
@ -837,6 +841,25 @@ impl LetExpr {
} }
} }
node! {
/// A set expression: `set text(...)`.
SetExpr
}
impl SetExpr {
/// The class to set style properties for.
pub fn class(&self) -> Ident {
self.0.cast_first_child().expect("set expression is missing class")
}
/// The style properties to set.
pub fn args(&self) -> CallArgs {
self.0
.cast_first_child()
.expect("set expression is missing argument list")
}
}
node! { node! {
/// An import expression: `import a, b, c from "utils.typ"`. /// An import expression: `import a, b, c from "utils.typ"`.
ImportExpr ImportExpr

View File

@ -96,22 +96,23 @@ impl Category {
NodeKind::EnDash => Some(Category::Shortcut), NodeKind::EnDash => Some(Category::Shortcut),
NodeKind::EmDash => Some(Category::Shortcut), NodeKind::EmDash => Some(Category::Shortcut),
NodeKind::Escape(_) => Some(Category::Escape), NodeKind::Escape(_) => Some(Category::Escape),
NodeKind::Let => Some(Category::Keyword),
NodeKind::If => Some(Category::Keyword),
NodeKind::Else => Some(Category::Keyword),
NodeKind::For => Some(Category::Keyword),
NodeKind::In => Some(Category::Keyword),
NodeKind::While => Some(Category::Keyword),
NodeKind::Break => Some(Category::Keyword),
NodeKind::Continue => Some(Category::Keyword),
NodeKind::Return => Some(Category::Keyword),
NodeKind::Import => Some(Category::Keyword),
NodeKind::Include => Some(Category::Keyword),
NodeKind::From => Some(Category::Keyword),
NodeKind::Not => Some(Category::Keyword), NodeKind::Not => Some(Category::Keyword),
NodeKind::And => Some(Category::Keyword), NodeKind::And => Some(Category::Keyword),
NodeKind::Or => Some(Category::Keyword), NodeKind::Or => Some(Category::Keyword),
NodeKind::With => Some(Category::Keyword), NodeKind::With => Some(Category::Keyword),
NodeKind::Let => Some(Category::Keyword),
NodeKind::Set => Some(Category::Keyword),
NodeKind::If => Some(Category::Keyword),
NodeKind::Else => Some(Category::Keyword),
NodeKind::While => Some(Category::Keyword),
NodeKind::For => Some(Category::Keyword),
NodeKind::In => Some(Category::Keyword),
NodeKind::Break => Some(Category::Keyword),
NodeKind::Continue => Some(Category::Keyword),
NodeKind::Return => Some(Category::Keyword),
NodeKind::Import => Some(Category::Keyword),
NodeKind::From => Some(Category::Keyword),
NodeKind::Include => Some(Category::Keyword),
NodeKind::Plus => Some(Category::Operator), NodeKind::Plus => Some(Category::Operator),
NodeKind::Star => Some(Category::Operator), NodeKind::Star => Some(Category::Operator),
NodeKind::Slash => Some(Category::Operator), NodeKind::Slash => Some(Category::Operator),
@ -139,6 +140,7 @@ impl Category {
Some(Category::Function) Some(Category::Function)
} }
NodeKind::WithExpr => Some(Category::Function), NodeKind::WithExpr => Some(Category::Function),
NodeKind::SetExpr => Some(Category::Function),
NodeKind::Call => Some(Category::Function), NodeKind::Call => Some(Category::Function),
_ => Some(Category::Variable), _ => Some(Category::Variable),
}, },
@ -161,21 +163,22 @@ impl Category {
NodeKind::Array => None, NodeKind::Array => None,
NodeKind::Dict => None, NodeKind::Dict => None,
NodeKind::Named => None, NodeKind::Named => None,
NodeKind::Template => None,
NodeKind::Group => None, NodeKind::Group => None,
NodeKind::Block => None,
NodeKind::Unary => None, NodeKind::Unary => None,
NodeKind::Binary => None, NodeKind::Binary => None,
NodeKind::Call => None, NodeKind::Call => None,
NodeKind::CallArgs => None, NodeKind::CallArgs => None,
NodeKind::Spread => None,
NodeKind::Closure => None, NodeKind::Closure => None,
NodeKind::ClosureParams => None, NodeKind::ClosureParams => None,
NodeKind::Spread => None,
NodeKind::Template => None,
NodeKind::Block => None,
NodeKind::ForExpr => None,
NodeKind::WhileExpr => None,
NodeKind::IfExpr => None,
NodeKind::LetExpr => None,
NodeKind::WithExpr => None, NodeKind::WithExpr => None,
NodeKind::LetExpr => None,
NodeKind::SetExpr => None,
NodeKind::IfExpr => None,
NodeKind::WhileExpr => None,
NodeKind::ForExpr => None,
NodeKind::ForPattern => None, NodeKind::ForPattern => None,
NodeKind::ImportExpr => None, NodeKind::ImportExpr => None,
NodeKind::ImportItems => None, NodeKind::ImportItems => None,

View File

@ -83,19 +83,15 @@ impl Default for Green {
impl Debug for Green { impl Debug for Green {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{:?}: {}", self.kind(), self.len())?; match self {
if let Self::Node(n) = self { Self::Node(node) => node.fmt(f),
if !n.children.is_empty() { Self::Token(token) => token.fmt(f),
f.write_str(" ")?;
f.debug_list().entries(&n.children).finish()?;
} }
} }
Ok(())
}
} }
/// An inner node in the untyped green tree. /// An inner node in the untyped green tree.
#[derive(Debug, Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct GreenNode { pub struct GreenNode {
/// Node metadata. /// Node metadata.
data: GreenData, data: GreenData,
@ -145,8 +141,19 @@ impl From<Rc<GreenNode>> for Green {
} }
} }
impl Debug for GreenNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.data.fmt(f)?;
if !self.children.is_empty() {
f.write_str(" ")?;
f.debug_list().entries(&self.children).finish()?;
}
Ok(())
}
}
/// Data shared between inner and leaf nodes. /// Data shared between inner and leaf nodes.
#[derive(Debug, Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct GreenData { pub struct GreenData {
/// What kind of node this is (each kind would have its own struct in a /// What kind of node this is (each kind would have its own struct in a
/// strongly typed AST). /// strongly typed AST).
@ -178,6 +185,12 @@ impl From<GreenData> for Green {
} }
} }
impl Debug for GreenData {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{:?}: {}", self.kind, self.len())
}
}
/// A owned wrapper for a green node with span information. /// A owned wrapper for a green node with span information.
/// ///
/// Owned variant of [`RedRef`]. Can be [cast](Self::cast) to an AST node. /// Owned variant of [`RedRef`]. Can be [cast](Self::cast) to an AST node.
@ -465,6 +478,8 @@ pub enum NodeKind {
Auto, Auto,
/// The `let` keyword. /// The `let` keyword.
Let, Let,
/// The `set` keyword.
Set,
/// The `if` keyword. /// The `if` keyword.
If, If,
/// The `else` keyword. /// The `else` keyword.
@ -552,8 +567,12 @@ pub enum NodeKind {
Dict, Dict,
/// A named pair: `thickness: 3pt`. /// A named pair: `thickness: 3pt`.
Named, Named,
/// A template expression: `[*Hi* there!]`.
Template,
/// A grouped expression: `(1 + 2)`. /// A grouped expression: `(1 + 2)`.
Group, Group,
/// A block expression: `{ let x = 1; x + 2 }`.
Block,
/// A unary operation: `-x`. /// A unary operation: `-x`.
Unary, Unary,
/// A binary operation: `a + b`. /// A binary operation: `a + b`.
@ -562,39 +581,37 @@ pub enum NodeKind {
Call, Call,
/// A function call's argument list: `(x, y)`. /// A function call's argument list: `(x, y)`.
CallArgs, CallArgs,
/// Spreaded arguments or a parameter sink: `..x`.
Spread,
/// A closure expression: `(x, y) => z`. /// A closure expression: `(x, y) => z`.
Closure, Closure,
/// A closure's parameters: `(x, y)`. /// A closure's parameters: `(x, y)`.
ClosureParams, ClosureParams,
/// A parameter sink: `..x`. /// A with expression: `f with (x, y: 1)`.
Spread, WithExpr,
/// A template expression: `[*Hi* there!]`.
Template,
/// A block expression: `{ let x = 1; x + 2 }`.
Block,
/// A for loop expression: `for x in y { ... }`.
ForExpr,
/// A while loop expression: `while x { ... }`.
WhileExpr,
/// An if expression: `if x { ... }`.
IfExpr,
/// A let expression: `let x = 1`. /// A let expression: `let x = 1`.
LetExpr, LetExpr,
/// The `with` expression: `with (1)`. /// A set expression: `set text(...)`.
WithExpr, SetExpr,
/// An if-else expression: `if x { y } else { z }`.
IfExpr,
/// A while loop expression: `while x { ... }`.
WhileExpr,
/// A for loop expression: `for x in y { ... }`.
ForExpr,
/// A for loop's destructuring pattern: `x` or `x, y`. /// A for loop's destructuring pattern: `x` or `x, y`.
ForPattern, ForPattern,
/// The import expression: `import x from "foo.typ"`. /// An import expression: `import a, b, c from "utils.typ"`.
ImportExpr, ImportExpr,
/// Items to import: `a, b, c`. /// Items to import: `a, b, c`.
ImportItems, ImportItems,
/// The include expression: `include "foo.typ"`. /// An include expression: `include "chapter1.typ"`.
IncludeExpr, IncludeExpr,
/// Two slashes followed by inner contents, terminated with a newline: /// A line comment, two slashes followed by inner contents, terminated with
/// `//<str>\n`. /// a newline: `//<str>\n`.
LineComment, LineComment,
/// A slash and a star followed by inner contents, terminated with a star /// A block comment, a slash and a star followed by inner contents,
/// and a slash: `/*<str>*/`. /// terminated with a star and a slash: `/*<str>*/`.
/// ///
/// The comment can contain nested block comments. /// The comment can contain nested block comments.
BlockComment, BlockComment,
@ -616,11 +633,6 @@ pub enum ErrorPos {
} }
impl NodeKind { impl NodeKind {
/// Whether this is some kind of parenthesis.
pub fn is_paren(&self) -> bool {
matches!(self, Self::LeftParen | Self::RightParen)
}
/// Whether this is some kind of bracket. /// Whether this is some kind of bracket.
pub fn is_bracket(&self) -> bool { pub fn is_bracket(&self) -> bool {
matches!(self, Self::LeftBracket | Self::RightBracket) matches!(self, Self::LeftBracket | Self::RightBracket)
@ -631,6 +643,11 @@ impl NodeKind {
matches!(self, Self::LeftBrace | Self::RightBrace) matches!(self, Self::LeftBrace | Self::RightBrace)
} }
/// Whether this is some kind of parenthesis.
pub fn is_paren(&self) -> bool {
matches!(self, Self::LeftParen | Self::RightParen)
}
/// Whether this is some kind of error. /// Whether this is some kind of error.
pub fn is_error(&self) -> bool { pub fn is_error(&self) -> bool {
matches!(self, NodeKind::Error(_, _) | NodeKind::Unknown(_)) matches!(self, NodeKind::Error(_, _) | NodeKind::Unknown(_))
@ -672,6 +689,7 @@ impl NodeKind {
Self::None => "`none`", Self::None => "`none`",
Self::Auto => "`auto`", Self::Auto => "`auto`",
Self::Let => "keyword `let`", Self::Let => "keyword `let`",
Self::Set => "keyword `set`",
Self::If => "keyword `if`", Self::If => "keyword `if`",
Self::Else => "keyword `else`", Self::Else => "keyword `else`",
Self::For => "keyword `for`", Self::For => "keyword `for`",
@ -712,21 +730,22 @@ impl NodeKind {
Self::Array => "array", Self::Array => "array",
Self::Dict => "dictionary", Self::Dict => "dictionary",
Self::Named => "named argument", Self::Named => "named argument",
Self::Template => "template",
Self::Group => "group", Self::Group => "group",
Self::Block => "block",
Self::Unary => "unary expression", Self::Unary => "unary expression",
Self::Binary => "binary expression", Self::Binary => "binary expression",
Self::Call => "call", Self::Call => "call",
Self::CallArgs => "call arguments", Self::CallArgs => "call arguments",
Self::Spread => "parameter sink",
Self::Closure => "closure", Self::Closure => "closure",
Self::ClosureParams => "closure parameters", Self::ClosureParams => "closure parameters",
Self::Spread => "parameter sink",
Self::Template => "template",
Self::Block => "block",
Self::ForExpr => "for-loop expression",
Self::WhileExpr => "while-loop expression",
Self::IfExpr => "`if` expression",
Self::LetExpr => "`let` expression",
Self::WithExpr => "`with` expression", Self::WithExpr => "`with` expression",
Self::LetExpr => "`let` expression",
Self::SetExpr => "`set` expression",
Self::IfExpr => "`if` expression",
Self::WhileExpr => "while-loop expression",
Self::ForExpr => "for-loop expression",
Self::ForPattern => "for-loop destructuring pattern", Self::ForPattern => "for-loop destructuring pattern",
Self::ImportExpr => "`import` expression", Self::ImportExpr => "`import` expression",
Self::ImportItems => "import items", Self::ImportItems => "import items",

View File

@ -225,6 +225,7 @@ impl Pretty for Expr {
Self::Closure(v) => v.pretty(p), Self::Closure(v) => v.pretty(p),
Self::With(v) => v.pretty(p), Self::With(v) => v.pretty(p),
Self::Let(v) => v.pretty(p), Self::Let(v) => v.pretty(p),
Self::Set(v) => v.pretty(p),
Self::If(v) => v.pretty(p), Self::If(v) => v.pretty(p),
Self::While(v) => v.pretty(p), Self::While(v) => v.pretty(p),
Self::For(v) => v.pretty(p), Self::For(v) => v.pretty(p),
@ -444,6 +445,16 @@ impl Pretty for LetExpr {
} }
} }
impl Pretty for SetExpr {
fn pretty(&self, p: &mut Printer) {
p.push_str("set ");
self.class().pretty(p);
p.push_str("(");
self.args().pretty(p);
p.push(')');
}
}
impl Pretty for IfExpr { impl Pretty for IfExpr {
fn pretty(&self, p: &mut Printer) { fn pretty(&self, p: &mut Printer) {
p.push_str("if "); p.push_str("if ");
@ -639,6 +650,7 @@ mod tests {
// Control flow. // Control flow.
roundtrip("#let x = 1 + 2"); roundtrip("#let x = 1 + 2");
roundtrip("#let f(x) = y"); roundtrip("#let f(x) = y");
roundtrip("#set text(size: 12pt)");
roundtrip("#if x [y] else [z]"); roundtrip("#if x [y] else [z]");
roundtrip("#if x {} else if y {} else {}"); roundtrip("#if x {} else if y {} else {}");
roundtrip("#while x {y}"); roundtrip("#while x {y}");

View File

@ -5,7 +5,8 @@
// Ref: true // Ref: true
// Ommitted space. // Ommitted space.
[#font(weight:"bold")Bold] #let f() = {}
[#f()*Bold*]
// Call return value of function with body. // Call return value of function with body.
#let f(x, body) = (y) => [#x] + body + [#y] #let f(x, body) = (y) => [#x] + body + [#y]
@ -44,25 +45,25 @@
} }
--- ---
// Error: 2-6 expected function or collection, found boolean // Error: 2-6 expected callable or collection, found boolean
{true()} {true()}
--- ---
#let x = "x" #let x = "x"
// Error: 1-3 expected function or collection, found string // Error: 1-3 expected callable or collection, found string
#x() #x()
--- ---
#let f(x) = x #let f(x) = x
// Error: 1-6 expected function or collection, found integer // Error: 1-6 expected callable or collection, found integer
#f(1)(2) #f(1)(2)
--- ---
#let f(x) = x #let f(x) = x
// Error: 1-6 expected function or collection, found template // Error: 1-6 expected callable or collection, found template
#f[1](2) #f[1](2)
--- ---

View File

@ -1,7 +1,7 @@
// Test include statements. // Test include statements.
--- ---
#page(width: 200pt) #set page(width: 200pt)
= Document = Document

View File

@ -4,14 +4,14 @@
--- ---
// Test standard argument overriding. // Test standard argument overriding.
{ {
let font(style: "normal", weight: "regular") = { let f(style: "normal", weight: "regular") = {
"(style: " + style + ", weight: " + weight + ")" "(style: " + style + ", weight: " + weight + ")"
} }
let myfont(..args) = font(weight: "bold", ..args) let myf(..args) = f(weight: "bold", ..args)
test(myfont(), "(style: normal, weight: bold)") test(myf(), "(style: normal, weight: bold)")
test(myfont(weight: "black"), "(style: normal, weight: black)") test(myf(weight: "black"), "(style: normal, weight: black)")
test(myfont(style: "italic"), "(style: italic, weight: bold)") test(myf(style: "italic"), "(style: italic, weight: bold)")
} }
--- ---

View File

@ -1,4 +1,4 @@
#page(width: 450pt, margins: 1cm) #set page(width: 450pt, margins: 1cm)
*Technische Universität Berlin* #h(1fr) *WiSe 2019/2020* \ *Technische Universität Berlin* #h(1fr) *WiSe 2019/2020* \
*Fakultät II, Institut for Mathematik* #h(1fr) Woche 3 \ *Fakultät II, Institut for Mathematik* #h(1fr) Woche 3 \

View File

@ -23,7 +23,7 @@ Center-aligned rect in auto-sized circle.
Rect in auto-sized circle. \ Rect in auto-sized circle. \
#circle(fill: forest, #circle(fill: forest,
rect(fill: conifer, stroke: white, padding: 4pt)[ rect(fill: conifer, stroke: white, padding: 4pt)[
#font(8pt) #set text(8pt)
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
] ]
) )
@ -38,7 +38,7 @@ Expanded by height.
--- ---
// Test relative sizing. // Test relative sizing.
#let centered(body) = align(center + horizon, body) #let centered(body) = align(center + horizon, body)
#font(fill: white) #set text(fill: white)
#rect(width: 100pt, height: 50pt, fill: rgb("aaa"), centered[ #rect(width: 100pt, height: 50pt, fill: rgb("aaa"), centered[
#circle(radius: 10pt, fill: eastern, centered[A]) // D=20pt #circle(radius: 10pt, fill: eastern, centered[A]) // D=20pt
#circle(height: 60%, fill: eastern, centered[B]) // D=30pt #circle(height: 60%, fill: eastern, centered[B]) // D=30pt

View File

@ -18,6 +18,6 @@ Rect in ellipse in fixed rect. \
Auto-sized ellipse. \ Auto-sized ellipse. \
#ellipse(fill: conifer, stroke: forest, thickness: 3pt, padding: 3pt)[ #ellipse(fill: conifer, stroke: forest, thickness: 3pt, padding: 3pt)[
#font(8pt) #set text(8pt)
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
] ]

View File

@ -7,7 +7,7 @@
#image("../../res/rhino.png") #image("../../res/rhino.png")
// Load an RGB JPEG image. // Load an RGB JPEG image.
#page(height: 60pt) #set page(height: 60pt)
#image("../../res/tiger.jpg") #image("../../res/tiger.jpg")
--- ---
@ -25,7 +25,7 @@
--- ---
// Test all three fit modes. // Test all three fit modes.
#page(height: 50pt, margins: 0pt) #set page(height: 50pt, margins: 0pt)
#grid( #grid(
columns: (1fr, 1fr, 1fr), columns: (1fr, 1fr, 1fr),
rows: 100%, rows: 100%,
@ -37,7 +37,7 @@
--- ---
// Does not fit to remaining height of page. // Does not fit to remaining height of page.
#page(height: 60pt) #set page(height: 60pt)
Stuff \ Stuff \
Stuff Stuff
#image("../../res/rhino.png") #image("../../res/rhino.png")

View File

@ -5,7 +5,7 @@
#rect() #rect()
--- ---
#page(width: 150pt) #set page(width: 150pt)
// Fit to text. // Fit to text.
#rect(fill: conifer, padding: 3pt)[Textbox] #rect(fill: conifer, padding: 3pt)[Textbox]

View File

@ -8,7 +8,7 @@
--- ---
// Test auto-sized square. // Test auto-sized square.
#square(fill: eastern, padding: 5pt)[ #square(fill: eastern, padding: 5pt)[
#font(fill: white, weight: "bold") #set text(fill: white, weight: "bold")
Typst Typst
] ]
@ -21,14 +21,14 @@
--- ---
// Test text overflowing height. // Test text overflowing height.
#page(width: 75pt, height: 100pt) #set page(width: 75pt, height: 100pt)
#square(fill: conifer)[ #square(fill: conifer)[
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
] ]
--- ---
// Test that square does not overflow page. // Test that square does not overflow page.
#page(width: 100pt, height: 75pt) #set page(width: 100pt, height: 75pt)
#square(fill: conifer)[ #square(fill: conifer)[
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
] ]

View File

@ -1,7 +1,7 @@
// Test alignment. // Test alignment.
--- ---
#page(height: 100pt) #set page(height: 100pt)
#stack(dir: ltr, #stack(dir: ltr,
align(left, square(size: 15pt, fill: eastern)), align(left, square(size: 15pt, fill: eastern)),
align(center, square(size: 20pt, fill: eastern)), align(center, square(size: 20pt, fill: eastern)),

View File

@ -3,14 +3,14 @@
--- ---
// Test relative width and height and size that is smaller // Test relative width and height and size that is smaller
// than default size. // than default size.
#page(width: 120pt, height: 70pt) #set page(width: 120pt, height: 70pt)
#square(width: 50%, align(bottom)[A]) #square(width: 50%, align(bottom)[A])
#square(height: 50%) #square(height: 50%)
#box(stack(square(size: 10pt), 5pt, square(size: 10pt, [B]))) #box(stack(square(size: 10pt), 5pt, square(size: 10pt, [B])))
--- ---
// Test alignment in automatically sized square and circle. // Test alignment in automatically sized square and circle.
#font(8pt) #set text(8pt)
#square(padding: 4pt)[ #square(padding: 4pt)[
Hey there, #align(center + bottom, rotate(180deg, [you!])) Hey there, #align(center + bottom, rotate(180deg, [you!]))
] ]
@ -23,19 +23,19 @@
--- ---
// Test square that is limited by region size. // Test square that is limited by region size.
#page(width: 20pt, height: 10pt, margins: 0pt) #set page(width: 20pt, height: 10pt, margins: 0pt)
#stack(dir: ltr, square(fill: forest), square(fill: conifer)) #stack(dir: ltr, square(fill: forest), square(fill: conifer))
--- ---
// Test different ways of sizing. // Test different ways of sizing.
#page(width: 120pt, height: 40pt) #set page(width: 120pt, height: 40pt)
#circle(radius: 5pt) #circle(radius: 5pt)
#circle(width: 10%) #circle(width: 10%)
#circle(height: 50%) #circle(height: 50%)
--- ---
// Test square that is overflowing due to its aspect ratio. // Test square that is overflowing due to its aspect ratio.
#page(width: 40pt, height: 20pt, margins: 5pt) #set page(width: 40pt, height: 20pt, margins: 5pt)
#square(width: 100%) #square(width: 100%)
#square(width: 100%)[Hello] #square(width: 100%)[Hello]

View File

@ -1,8 +1,8 @@
// Test placing a background image on a page. // Test placing a background image on a page.
--- ---
#page(paper: "a10", flipped: true) #set page(paper: "a10", flipped: true)
#font(fill: white) #set text(fill: white)
#place( #place(
dx: -10pt, dx: -10pt,
dy: -10pt, dy: -10pt,

View File

@ -12,7 +12,7 @@ Apart
--- ---
// Test block over multiple pages. // Test block over multiple pages.
#page(height: 60pt) #set page(height: 60pt)
First! First!
#block[ #block[

View File

@ -3,7 +3,7 @@
--- ---
#let cell(width, color) = rect(width: width, height: 2cm, fill: color) #let cell(width, color) = rect(width: width, height: 2cm, fill: color)
#page(width: 100pt, height: 140pt) #set page(width: 100pt, height: 140pt)
#grid( #grid(
columns: (auto, 1fr, 3fr, 0.25cm, 3%, 2mm + 10%), columns: (auto, 1fr, 3fr, 0.25cm, 3%, 2mm + 10%),
cell(0.5cm, rgb("2a631a")), cell(0.5cm, rgb("2a631a")),
@ -31,7 +31,7 @@
) )
--- ---
#page(height: 3cm, margins: 0pt) #set page(height: 3cm, margins: 0pt)
#grid( #grid(
columns: (1fr,), columns: (1fr,),
rows: (1fr, auto, 2fr), rows: (1fr, auto, 2fr),

View File

@ -1,7 +1,7 @@
// Test using the `grid` function to create a finance table. // Test using the `grid` function to create a finance table.
--- ---
#page(width: 12cm, height: 2.5cm) #set page(width: 12cm, height: 2.5cm)
#grid( #grid(
columns: 5, columns: 5,
column-gutter: (2fr, 1fr, 1fr), column-gutter: (2fr, 1fr, 1fr),

View File

@ -1,7 +1,7 @@
// Test grid cells that overflow to the next region. // Test grid cells that overflow to the next region.
--- ---
#page(width: 5cm, height: 3cm) #set page(width: 5cm, height: 3cm)
#grid( #grid(
columns: 2, columns: 2,
row-gutter: 8pt, row-gutter: 8pt,
@ -18,7 +18,7 @@
--- ---
// Test a column that starts overflowing right after another row/column did // Test a column that starts overflowing right after another row/column did
// that. // that.
#page(width: 5cm, height: 2cm) #set page(width: 5cm, height: 2cm)
#grid( #grid(
columns: 4 * (1fr,), columns: 4 * (1fr,),
row-gutter: 10pt, row-gutter: 10pt,
@ -32,7 +32,7 @@
--- ---
// Test two columns in the same row overflowing by a different amount. // Test two columns in the same row overflowing by a different amount.
#page(width: 5cm, height: 2cm) #set page(width: 5cm, height: 2cm)
#grid( #grid(
columns: 3 * (1fr,), columns: 3 * (1fr,),
row-gutter: 8pt, row-gutter: 8pt,
@ -48,7 +48,7 @@
--- ---
// Test grid within a grid, overflowing. // Test grid within a grid, overflowing.
#page(width: 5cm, height: 2.25cm) #set page(width: 5cm, height: 2.25cm)
#grid( #grid(
columns: 4 * (1fr,), columns: 4 * (1fr,),
row-gutter: 10pt, row-gutter: 10pt,
@ -62,7 +62,7 @@
--- ---
// Test partition of `fr` units before and after multi-region layout. // Test partition of `fr` units before and after multi-region layout.
#page(width: 5cm, height: 4cm) #set page(width: 5cm, height: 4cm)
#grid( #grid(
columns: 2 * (1fr,), columns: 2 * (1fr,),
rows: (1fr, 2fr, auto, 1fr, 1cm), rows: (1fr, 2fr, auto, 1fr, 1cm),

View File

@ -23,7 +23,7 @@
--- ---
// Test that all three kinds of rows use the correct bases. // Test that all three kinds of rows use the correct bases.
#page(height: 4cm, margins: 0cm) #set page(height: 4cm, margins: 0cm)
#grid( #grid(
rows: (1cm, 1fr, 1fr, auto), rows: (1cm, 1fr, 1fr, auto),
rect(height: 50%, width: 100%, fill: conifer), rect(height: 50%, width: 100%, fill: conifer),

View File

@ -1,7 +1,7 @@
--- ---
// Test that trailing linebreak doesn't overflow the region. // Test that trailing linebreak doesn't overflow the region.
#page(height: 2cm) #set page(height: 2cm)
#grid[ #grid[
Hello \ Hello \
Hello \ Hello \
@ -12,7 +12,7 @@
--- ---
// Test that broken cell expands vertically. // Test that broken cell expands vertically.
#page(height: 2.25cm) #set page(height: 2.25cm)
#grid( #grid(
columns: 2, columns: 2,
gutter: 10pt, gutter: 10pt,

View File

@ -19,7 +19,7 @@ Hi #box(pad(left: 10pt)[A]) there
--- ---
// Test that the pad node doesn't consume the whole region. // Test that the pad node doesn't consume the whole region.
#page(height: 6cm) #set page(height: 6cm)
#align(left)[Before] #align(left)[Before]
#pad(10pt, image("../../res/tiger.jpg")) #pad(10pt, image("../../res/tiger.jpg"))
#align(right)[After] #align(right)[After]

View File

@ -2,33 +2,33 @@
--- ---
// Set width and height. // Set width and height.
#page(width: 80pt, height: 80pt) #set page(width: 80pt, height: 80pt)
[#page(width: 40pt) High] [#set page(width: 40pt);High]
[#page(height: 40pt) Wide] [#set page(height: 40pt);Wide]
// Set all margins at once. // Set all margins at once.
[ [
#page(margins: 5pt) #set page(margins: 5pt)
#place(top + left)[TL] #place(top + left)[TL]
#place(bottom + right)[BR] #place(bottom + right)[BR]
] ]
// Set individual margins. // Set individual margins.
#page(height: 40pt) #set page(height: 40pt)
[#page(left: 0pt) #align(left)[Left]] [#set page(left: 0pt); #align(left)[Left]]
[#page(right: 0pt) #align(right)[Right]] [#set page(right: 0pt); #align(right)[Right]]
[#page(top: 0pt) #align(top)[Top]] [#set page(top: 0pt); #align(top)[Top]]
[#page(bottom: 0pt) #align(bottom)[Bottom]] [#set page(bottom: 0pt); #align(bottom)[Bottom]]
// Ensure that specific margins override general margins. // Ensure that specific margins override general margins.
[#page(margins: 0pt, left: 20pt) Overriden] [#set page(margins: 0pt, left: 20pt); Overriden]
// Flipped predefined paper. // Flipped predefined paper.
[#page(paper: "a11", flipped: true) Flipped A11] [#set page(paper: "a11", flipped: true);Flipped A11]
--- ---
#page(width: 80pt, height: 40pt, fill: eastern) #set page(width: 80pt, height: 40pt, fill: eastern)
#font(15pt, "Roboto", fill: white, smallcaps: true)[Typst] #text(15pt, "Roboto", fill: white, smallcaps: true)[Typst]
#page(width: 40pt, fill: none, margins: auto, top: 10pt) #set page(width: 40pt, fill: none, margins: auto, top: 10pt)
Hi Hi

View File

@ -3,7 +3,7 @@
--- ---
First of two First of two
#pagebreak() #pagebreak()
#page(height: 40pt) #set page(height: 40pt)
Second of two Second of two
--- ---
@ -12,7 +12,7 @@ A
#box[ #box[
B B
#pagebreak() #pagebreak()
#page("a4") #set page("a4")
] ]
C C
@ -23,13 +23,13 @@ D
--- ---
// Test a combination of pages with bodies and normal content. // Test a combination of pages with bodies and normal content.
#page(width: 80pt, height: 30pt) #set page(width: 80pt, height: 30pt)
Fi[#page(width: 80pt)rst] Fi[#set page(width: 80pt);rst]
[#page(width: 70pt) Second] [#set page(width: 70pt); Second]
#pagebreak() #pagebreak()
#pagebreak() #pagebreak()
Fourth Fourth
#page(height: 20pt)[] #page(height: 20pt)[]
Sixth Sixth
[#page() Seventh] [#set page(); Seventh]

View File

@ -1,7 +1,7 @@
// Test the `place` function. // Test the `place` function.
--- ---
#page("a8") #set page("a8")
#place(bottom + center)[© Typst] #place(bottom + center)[© Typst]
= Placement = Placement
@ -26,7 +26,7 @@ the line breaks still had to be inserted manually.
--- ---
// Test how the placed node interacts with paragraph spacing around it. // Test how the placed node interacts with paragraph spacing around it.
#page("a8", height: 60pt) #set page("a8", height: 60pt)
First First

View File

@ -20,8 +20,8 @@ Add #h(10pt) #h(10pt) up
--- ---
// Test that spacing has style properties. // Test that spacing has style properties.
A[#par(align: right)#h(1cm)]B A[#set par(align: right);#h(1cm)]B
[#page(height: 20pt)#v(1cm)] [#set page(height: 20pt);#v(1cm)]
B B
--- ---

View File

@ -15,13 +15,13 @@
#let items = for w in widths { (align(right, shaded(w)),) } #let items = for w in widths { (align(right, shaded(w)),) }
#page(width: 50pt, margins: 0pt) #set page(width: 50pt, margins: 0pt)
#stack(dir: btt, ..items) #stack(dir: btt, ..items)
--- ---
// Test RTL alignment. // Test RTL alignment.
#page(width: 50pt, margins: 5pt) #set page(width: 50pt, margins: 5pt)
#font(8pt) #set text(8pt)
#stack(dir: rtl, #stack(dir: rtl,
align(center, [A]), align(center, [A]),
align(left, [B]), align(left, [B]),
@ -30,8 +30,8 @@
--- ---
// Test spacing. // Test spacing.
#page(width: 50pt, margins: 0pt) #set page(width: 50pt, margins: 0pt)
#par(spacing: 5pt) #set par(spacing: 5pt)
#let x = square(size: 10pt, fill: eastern) #let x = square(size: 10pt, fill: eastern)
#stack(dir: rtl, spacing: 5pt, x, x, x) #stack(dir: rtl, spacing: 5pt, x, x, x)
@ -40,7 +40,7 @@
--- ---
// Test overflow. // Test overflow.
#page(width: 50pt, height: 30pt, margins: 0pt) #set page(width: 50pt, height: 30pt, margins: 0pt)
#box(stack( #box(stack(
rect(width: 40pt, height: 20pt, fill: conifer), rect(width: 40pt, height: 20pt, fill: conifer),
rect(width: 30pt, height: 13pt, fill: forest), rect(width: 30pt, height: 13pt, fill: forest),

View File

@ -1,7 +1,7 @@
// Test fr units in stacks. // Test fr units in stacks.
--- ---
#page(height: 3.5cm) #set page(height: 3.5cm)
#stack( #stack(
dir: ltr, dir: ltr,
spacing: 1fr, spacing: 1fr,
@ -15,8 +15,8 @@ from #h(1fr) the #h(1fr) wonderful
World! 🌍 World! 🌍
--- ---
#page(height: 2cm) #set page(height: 2cm)
#font(white) #set text(white)
#rect(fill: forest)[ #rect(fill: forest)[
#v(1fr) #v(1fr)
#h(1fr) Hi you! #h(5pt) #h(1fr) Hi you! #h(5pt)

View File

@ -23,13 +23,13 @@
[X] [X]
} }
#font("Latin Modern Math", size) #set text("Latin Modern Math", size)
Neither #tex, \ Neither #tex, \
nor #xetex! nor #xetex!
--- ---
// Test combination of scaling and rotation. // Test combination of scaling and rotation.
#page(height: 80pt) #set page(height: 80pt)
#align(center + horizon, #align(center + horizon,
rotate(20deg, scale(70%, image("../../res/tiger.jpg"))) rotate(20deg, scale(70%, image("../../res/tiger.jpg")))
) )
@ -43,7 +43,7 @@ nor #xetex!
--- ---
// Test setting scaling origin. // Test setting scaling origin.
#let r = rect(width: 100pt, height: 10pt, fill: forest) #let r = rect(width: 100pt, height: 10pt, fill: forest)
#page(height: 65pt) #set page(height: 65pt)
#scale(r, x: 50%, y: 200%, origin: left + top) #scale(r, x: 50%, y: 200%, origin: left + top)
#scale(r, x: 50%, origin: center) #scale(r, x: 50%, origin: center)
#scale(r, x: 50%, y: 200%, origin: right + bottom) #scale(r, x: 50%, y: 200%, origin: right + bottom)

View File

@ -1,4 +1,4 @@
// Test text baseline. // Test text baseline.
--- ---
Hi #font(150%)[You], #font(75%)[how are you?] Hi #text(150%)[You], #text(75%)[how are you?]

View File

@ -1,7 +1,7 @@
// Test simple text. // Test simple text.
--- ---
#page(width: 250pt, height: 120pt) #set page(width: 250pt, height: 120pt)
But, soft! what light through yonder window breaks? It is the east, and Juliet But, soft! what light through yonder window breaks? It is the east, and Juliet
is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and

View File

@ -2,55 +2,55 @@
--- ---
// Test reordering with different top-level paragraph directions. // Test reordering with different top-level paragraph directions.
#let text = [Text טֶקסט] #let content = [Text טֶקסט]
#font(serif, "Noto Serif Hebrew") #set text(serif, "Noto Serif Hebrew")
#par(lang: "he") {text} #par(lang: "he", content)
#par(lang: "de") {text} #par(lang: "de", content)
--- ---
// Test that consecutive, embedded LTR runs stay LTR. // Test that consecutive, embedded LTR runs stay LTR.
// Here, we have two runs: "A" and italic "B". // Here, we have two runs: "A" and italic "B".
#let text = [أنت A_B_مطرC] #let content = [أنت A_B_مطرC]
#font(serif, "Noto Sans Arabic") #set text(serif, "Noto Sans Arabic")
#par(lang: "ar") {text} #par(lang: "ar", content)
#par(lang: "de") {text} #par(lang: "de", content)
--- ---
// Test that consecutive, embedded RTL runs stay RTL. // Test that consecutive, embedded RTL runs stay RTL.
// Here, we have three runs: "גֶ", bold "שֶׁ", and "ם". // Here, we have three runs: "גֶ", bold "שֶׁ", and "ם".
#let text = [Aגֶ*שֶׁ*םB] #let content = [Aגֶ*שֶׁ*םB]
#font(serif, "Noto Serif Hebrew") #set text(serif, "Noto Serif Hebrew")
#par(lang: "he") {text} #par(lang: "he", content)
#par(lang: "de") {text} #par(lang: "de", content)
--- ---
// Test embedding up to level 4 with isolates. // Test embedding up to level 4 with isolates.
#font(serif, "Noto Serif Hebrew", "Twitter Color Emoji") #set text(serif, "Noto Serif Hebrew", "Twitter Color Emoji")
#par(dir: rtl) #set par(dir: rtl)
א\u{2066}A\u{2067}Bב\u{2069}? א\u{2066}A\u{2067}Bב\u{2069}?
--- ---
// Test hard line break (leads to two paragraphs in unicode-bidi). // Test hard line break (leads to two paragraphs in unicode-bidi).
#font("Noto Sans Arabic", serif) #set text("Noto Sans Arabic", serif)
#par(lang: "ar") #set par(lang: "ar")
Life المطر هو الحياة \ Life المطر هو الحياة \
الحياة تمطر is rain. الحياة تمطر is rain.
--- ---
// Test spacing. // Test spacing.
#font(serif, "Noto Serif Hebrew") #set text(serif, "Noto Serif Hebrew")
L #h(1cm) ריווחR \ L #h(1cm) ריווחR \
יווח #h(1cm) R יווח #h(1cm) R
--- ---
// Test inline object. // Test inline object.
#font("Noto Serif Hebrew", serif) #set text("Noto Serif Hebrew", serif)
#par(lang: "he") #set par(lang: "he")
קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים
--- ---
// Test setting a vertical direction. // Test setting a vertical direction.
// Ref: false // Ref: false
// Error: 11-14 must be horizontal // Error: 15-18 must be horizontal
#par(dir: ttb) #set par(dir: ttb)

View File

@ -1,7 +1,7 @@
// Test chinese text from Wikipedia. // Test chinese text from Wikipedia.
--- ---
#font("Noto Serif CJK SC") #set text("Noto Serif CJK SC")
是美国广播公司电视剧《迷失》第3季的第22和23集也是全剧的第71集和72集 是美国广播公司电视剧《迷失》第3季的第22和23集也是全剧的第71集和72集
由执行制作人戴蒙·林道夫和卡尔顿·库斯编剧,导演则是另一名执行制作人杰克·本德 由执行制作人戴蒙·林道夫和卡尔顿·库斯编剧,导演则是另一名执行制作人杰克·本德

View File

@ -13,7 +13,7 @@
#underline(red)[Critical information is conveyed here.] #underline(red)[Critical information is conveyed here.]
// Inherits font color. // Inherits font color.
#font(fill: red, underline[Change with the wind.]) #text(fill: red, underline[Change with the wind.])
// Both over- and underline. // Both over- and underline.
#overline(underline[Running amongst the wolves.]) #overline(underline[Running amongst the wolves.])

View File

@ -2,72 +2,73 @@
--- ---
// Test turning kerning off. // Test turning kerning off.
#font(kerning: true)[Tq] \ #text(kerning: true)[Tq] \
#font(kerning: false)[Tq] #text(kerning: false)[Tq]
--- ---
// Test smallcaps. // Test smallcaps.
#font("Roboto") #set text("Roboto")
#font(smallcaps: true)[Smallcaps] #text(smallcaps: true)[Smallcaps]
--- ---
// Test alternates and stylistic sets. // Test alternates and stylistic sets.
#font("IBM Plex Serif") #set text("IBM Plex Serif")
a vs #font(alternates: true)[a] \ a vs #text(alternates: true)[a] \
ß vs #font(stylistic-set: 5)[ß] ß vs #text(stylistic-set: 5)[ß]
--- ---
// Test ligatures. // Test ligatures.
fi vs. #font(ligatures: false)[No fi] \ fi vs. #text(ligatures: false)[No fi] \
--- ---
// Test number type. // Test number type.
#font("Roboto") #set text("Roboto")
#font(number-type: "old-style") 0123456789 \ #set text(number-type: "old-style")
#font(number-type: auto)[0123456789] 0123456789 \
#text(number-type: auto)[0123456789]
--- ---
// Test number width. // Test number width.
#font("Roboto") #set text("Roboto")
#font(number-width: "proportional")[0123456789] \ #text(number-width: "proportional")[0123456789] \
#font(number-width: "tabular")[3456789123] \ #text(number-width: "tabular")[3456789123] \
#font(number-width: "tabular")[0123456789] #text(number-width: "tabular")[0123456789]
--- ---
// Test number position. // Test number position.
#font("IBM Plex Sans") #set text("IBM Plex Sans")
#font(number-position: "normal")[C2H4] \ #text(number-position: "normal")[C2H4] \
#font(number-position: "subscript")[C2H4] \ #text(number-position: "subscript")[C2H4] \
#font(number-position: "superscript")[C2H4] #text(number-position: "superscript")[C2H4]
--- ---
// Test extra number stuff. // Test extra number stuff.
#font("IBM Plex Sans") #set text("IBM Plex Sans")
0 vs. #font(slashed-zero: true)[0] \ 0 vs. #text(slashed-zero: true)[0] \
1/2 vs. #font(fractions: true)[1/2] 1/2 vs. #text(fractions: true)[1/2]
--- ---
// Test raw features. // Test raw features.
#font("Roboto") #set text("Roboto")
#font(features: ("smcp",))[Smcp] \ #text(features: ("smcp",))[Smcp] \
fi vs. #font(features: (liga: 0))[No fi] fi vs. #text(features: (liga: 0))[No fi]
--- ---
// Error: 22-27 expected integer or none, found boolean // Error: 26-31 expected integer or none, found boolean
#font(stylistic-set: false) #set text(stylistic-set: false)
--- ---
// Error: 22-24 must be between 1 and 20 // Error: 26-28 must be between 1 and 20
#font(stylistic-set: 25) #set text(stylistic-set: 25)
--- ---
// Error: 20-21 expected string or auto, found integer // Error: 24-25 expected string or auto, found integer
#font(number-type: 2) #set text(number-type: 2)
--- ---
// Error: 20-31 expected "lining" or "old-style" // Error: 24-35 expected "lining" or "old-style"
#font(number-type: "different") #set text(number-type: "different")
--- ---
// Error: 17-22 expected array of strings or dictionary mapping tags to integers, found boolean // Error: 21-26 expected array of strings or dictionary mapping tags to integers, found boolean
#font(features: false) #set text(features: false)

View File

@ -2,57 +2,57 @@
--- ---
// Set same font size in three different ways. // Set same font size in three different ways.
#font(20pt)[A] #text(20pt)[A]
#font(200%)[A] #text(200%)[A]
#font(size: 15pt + 50%)[A] #text(size: 15pt + 50%)[A]
// Do nothing. // Do nothing.
#font()[Normal] #text()[Normal]
// Set style (is available). // Set style (is available).
#font(style: "italic")[Italic] #text(style: "italic")[Italic]
// Set weight (is available). // Set weight (is available).
#font(weight: "bold")[Bold] #text(weight: "bold")[Bold]
// Set stretch (not available, matching closest). // Set stretch (not available, matching closest).
#font(stretch: 50%)[Condensed] #text(stretch: 50%)[Condensed]
// Set family. // Set family.
#font(family: serif)[Serif] #text(family: serif)[Serif]
// Emoji. // Emoji.
Emoji: 🐪, 🌋, 🏞 Emoji: 🐪, 🌋, 🏞
// Math. // Math.
#font("Latin Modern Math")[ 𝛼 + 3𝛽 d𝑡] #text("Latin Modern Math")[ 𝛼 + 3𝛽 d𝑡]
// Colors. // Colors.
[ [
#font(fill: eastern) #set text(fill: eastern)
This is #font(rgb("FA644B"))[way more] colorful. This is #text(rgb("FA644B"))[way more] colorful.
] ]
// Disable font fallback beyond the user-specified list. // Disable font fallback beyond the user-specified list.
// Without disabling, Latin Modern Math would come to the rescue. // Without disabling, Latin Modern Math would come to the rescue.
#font("PT Sans", "Twitter Color Emoji", fallback: false) #set text("PT Sans", "Twitter Color Emoji", fallback: false)
= 𝛼 + 𝛽. = 𝛼 + 𝛽.
--- ---
// Test class definitions. // Test class definitions.
#font(sans-serif: "PT Sans") #set text(sans-serif: "PT Sans")
#font(family: sans-serif)[Sans-serif.] \ #text(family: sans-serif)[Sans-serif.] \
#font(monospace)[Monospace.] \ #text(monospace)[Monospace.] \
#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.] #text(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.]
--- ---
// Test top and bottom edge. // Test top and bottom edge.
#page(width: 150pt) #set page(width: 150pt)
#font(size: 8pt) #set text(size: 8pt)
#let try(top, bottom) = rect(fill: conifer)[ #let try(top, bottom) = rect(fill: conifer)[
#font(monospace, top-edge: top, bottom-edge: bottom) #set text(monospace, top-edge: top, bottom-edge: bottom)
From #top to #bottom From #top to #bottom
] ]
@ -64,33 +64,33 @@ Emoji: 🐪, 🌋, 🏞
#try(1pt + 27%, -18%) #try(1pt + 27%, -18%)
--- ---
// Error: 7-12 unexpected argument // Error: 11-16 unexpected argument
#font(false) #set text(false)
--- ---
// Error: 14-20 expected "normal", "italic" or "oblique" // Error: 18-24 expected "normal", "italic" or "oblique"
#font(style: "bold", weight: "thin") #set text(style: "bold", weight: "thin")
--- ---
// Error: 17-19 expected linear or string, found array // Error: 21-23 expected linear or string, found array
#font(top-edge: ()) #set text(top-edge: ())
--- ---
// Error: 17-19 unknown font metric // Error: 21-23 unknown font metric
#font(top-edge: "") #set text(top-edge: "")
--- ---
// Error: 14-15 expected string or array of strings, found integer // Error: 18-19 expected string or array of strings, found integer
#font(serif: 0) #set text(serif: 0)
--- ---
// Error: 19-23 unexpected argument // Error: 23-27 unexpected argument
#font(size: 10pt, 12pt) #set text(size: 10pt, 12pt)
--- ---
// Error: 28-35 unexpected argument // Error: 32-39 unexpected argument
#font(family: "Helvetica", "Arial") #set text(family: "Helvetica", "Arial")
--- ---
// Error: 7-27 unexpected argument // Error: 11-31 unexpected argument
#font(something: "invalid") #set text(something: "invalid")

View File

@ -12,12 +12,12 @@ Contact #link("mailto:hi@typst.app") or call #link("tel:123") for more informati
--- ---
// Styled with underline and color. // Styled with underline and color.
#let link(url, body) = link(url, font(fill: rgb("283663"), underline(body))) #let link(url, body) = link(url, text(fill: rgb("283663"), underline(body)))
You could also make the #link("https://html5zombo.com/")[link look way more typical.] You could also make the #link("https://html5zombo.com/")[link look way more typical.]
--- ---
// Transformed link. // Transformed link.
#page(height: 60pt) #set page(height: 60pt)
#let link = link("https://typst.app/")[LINK] #let link = link("https://typst.app/")[LINK]
My cool #move(x: 0.7cm, y: 0.7cm, rotate(10deg, scale(200%, link))) My cool #move(x: 0.7cm, y: 0.7cm, rotate(10deg, scale(200%, link)))

View File

@ -2,37 +2,37 @@
--- ---
// Test ragged-left. // Test ragged-left.
#par(align: right) #set par(align: right)
To the right! Where the sunlight peeks behind the mountain. To the right! Where the sunlight peeks behind the mountain.
--- ---
// Test that explicit paragraph break respects active styles. // Test that explicit paragraph break respects active styles.
#par(spacing: 7pt) #set par(spacing: 7pt)
[#par(spacing: 100pt) First] [#set par(spacing: 100pt);First]
[#par(spacing: 100pt) Second] [#set par(spacing: 100pt);Second]
#par(spacing: 20pt) #set par(spacing: 20pt)
Third Third
--- ---
// Test that paragraph break due to incompatibility respects // Test that paragraph break due to incompatibility respects
// spacing defined by the two adjacent paragraphs. // spacing defined by the two adjacent paragraphs.
#let a = [#par(spacing: 40pt) Hello] #let a = [#set par(spacing: 40pt);Hello]
#let b = [#par(spacing: 60pt) World] #let b = [#set par(spacing: 60pt);World]
{a}{b} {a}{b}
--- ---
// Test weird metrics. // Test weird metrics.
#par(spacing: 100%, leading: 0pt) #set par(spacing: 100%, leading: 0pt)
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
It is the east, and Juliet is the sun. It is the east, and Juliet is the sun.
--- ---
// Error: 13-16 must be horizontal // Error: 17-20 must be horizontal
#par(align: top) #set par(align: top)
--- ---
// Error: 13-29 expected alignment, found 2d alignment // Error: 17-33 expected alignment, found 2d alignment
#par(align: horizon + center) #set par(align: horizon + center)

View File

@ -7,11 +7,11 @@
Le fira Le fira
// This should just shape nicely. // This should just shape nicely.
#font("Noto Sans Arabic") #set text("Noto Sans Arabic")
دع النص يمطر عليك دع النص يمطر عليك
// This should form a three-member family. // This should form a three-member family.
#font("Twitter Color Emoji") #set text("Twitter Color Emoji")
👩‍👩‍👦 🤚🏿 👩‍👩‍👦 🤚🏿
// These two shouldn't be affected by a zero-width joiner. // These two shouldn't be affected by a zero-width joiner.
@ -20,7 +20,7 @@ Le fira
--- ---
// Test font fallback. // Test font fallback.
#font(sans-serif, "Noto Sans Arabic", "Twitter Color Emoji") #set text(sans-serif, "Noto Sans Arabic", "Twitter Color Emoji")
// Font fallback for emoji. // Font fallback for emoji.
A😀B A😀B
@ -40,6 +40,6 @@ A🐈中文B
--- ---
// Test reshaping. // Test reshaping.
#font("Noto Serif Hebrew") #set text("Noto Serif Hebrew")
#par(lang: "he") #set par(lang: "he")
ס \ טֶ ס \ טֶ

View File

@ -1,12 +1,12 @@
// Test tracking characters apart or together. // Test tracking characters apart or together.
--- ---
#font(tracking: -0.01) #set text(tracking: -0.01)
I saw Zoe yӛsterday, on the tram. I saw Zoe yӛsterday, on the tram.
--- ---
I'm in#font(tracking: 0.3)[ spaace]! I'm in#text(tracking: 0.3)[ spaace]!
--- ---
#font("Noto Serif Hebrew", tracking: 0.3) #set text("Noto Serif Hebrew", tracking: 0.3)
טֶקסט טֶקסט

View File

@ -30,11 +30,11 @@ A #for _ in (none,) {"B"}C
--- ---
// Test that a run consisting only of whitespace isn't trimmed. // Test that a run consisting only of whitespace isn't trimmed.
A[#font(serif) ]B A[#set text(serif); ]B
--- ---
// Test font change after space. // Test font change after space.
Left [#font(serif)Right]. Left [#set text(serif);Right].
--- ---
// Test that space at start of line is not trimmed. // Test that space at start of line is not trimmed.