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.
#page(width: 450pt, margins: 1cm)
#set page(width: 450pt, margins: 1cm)
// There are variables and they can take normal values like strings, ...
#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]
mod styles;
mod capture;
mod class;
mod function;
mod node;
mod ops;
@ -16,6 +17,7 @@ mod scope;
pub use array::*;
pub use capture::*;
pub use class::*;
pub use dict::*;
pub use function::*;
pub use node::*;
@ -54,7 +56,7 @@ pub fn eval(ctx: &mut Context, source: SourceId, markup: &Markup) -> TypResult<M
pub struct Module {
/// The top-level definitions that were bound in this module.
pub scope: Scope,
/// The node defined by this module.
/// The module's layoutable contents.
pub node: Node,
}
@ -288,6 +290,7 @@ impl Eval for Expr {
Self::Unary(v) => v.eval(ctx),
Self::Binary(v) => v.eval(ctx),
Self::Let(v) => v.eval(ctx),
Self::Set(v) => v.eval(ctx),
Self::If(v) => v.eval(ctx),
Self::While(v) => v.eval(ctx),
Self::For(v) => v.eval(ctx),
@ -474,9 +477,17 @@ impl Eval for CallExpr {
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!(
self.callee().span(),
"expected function or collection, found {}",
"expected callable or collection, found {}",
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 {
type Output = Value;

View File

@ -36,6 +36,8 @@ pub enum Node {
Inline(PackedNode),
/// A block node.
Block(PackedNode),
/// A page node.
Page(PackedNode),
/// A sequence of nodes (which may themselves contain sequences).
Sequence(Vec<(Self, Styles)>),
}
@ -214,6 +216,14 @@ impl Packer {
Node::Block(block) => {
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) => {
// For a list of nodes, we apply the list's styles to each node
// individually.

View File

@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Formatter};
use std::iter;
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::util::EcoString;
@ -88,15 +88,6 @@ impl Scope {
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.
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())));
@ -107,6 +98,24 @@ impl Scope {
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.
pub fn get(&self, var: &str) -> Option<&Slot> {
self.values.get(var)

View File

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

View File

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

View File

@ -6,53 +6,6 @@ use std::str::FromStr;
use super::prelude::*;
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.
pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
Ok(Value::Node(Node::Pagebreak))
@ -90,6 +43,45 @@ properties! {
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 {
/// Layout the page run into a sequence of frames, one per page.
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.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum PaperClass {

View File

@ -9,46 +9,6 @@ use super::prelude::*;
use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode};
use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
/// `par`: Configure paragraphs.
pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let spacing = args.named("spacing")?;
let leading = args.named("leading")?;
let mut dir =
args.named("lang")?
.map(|iso: EcoString| match iso.to_lowercase().as_str() {
"ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
"en" | "fr" | "de" => Dir::LTR,
_ => Dir::LTR,
});
if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
if v.axis() != SpecAxis::Horizontal {
bail!(span, "must be horizontal");
}
dir = Some(v);
}
let mut align = None;
if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? {
if v.axis() != SpecAxis::Horizontal {
bail!(span, "must be horizontal");
}
align = Some(v);
}
if let (Some(dir), None) = (dir, align) {
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
}
set!(ctx.styles, ParNode::DIR => dir);
set!(ctx.styles, ParNode::ALIGN => align);
set!(ctx.styles, ParNode::LEADING => leading);
set!(ctx.styles, ParNode::SPACING => spacing);
Ok(Value::None)
}
/// `parbreak`: Start a new paragraph.
pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
Ok(Value::Node(Node::Parbreak))
@ -71,6 +31,54 @@ properties! {
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 leading = args.named("leading")?;
let mut dir =
args.named("lang")?
.map(|iso: EcoString| match iso.to_lowercase().as_str() {
"ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
"en" | "fr" | "de" => Dir::LTR,
_ => Dir::LTR,
});
if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
if v.axis() != SpecAxis::Horizontal {
bail!(span, "must be horizontal");
}
dir = Some(v);
}
let mut align = None;
if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? {
if v.axis() != SpecAxis::Horizontal {
bail!(span, "must be horizontal");
}
align = Some(v);
}
if let (Some(dir), None) = (dir, align) {
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
}
set!(styles, ParNode::DIR => dir);
set!(styles, ParNode::ALIGN => align);
set!(styles, ParNode::LEADING => leading);
set!(styles, ParNode::SPACING => spacing);
Ok(())
}
}
impl Layout for ParNode {
fn layout(
&self,

View File

@ -15,54 +15,6 @@ use crate::font::{
use crate::geom::{Dir, Em, Length, Point, Size};
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.
pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
line_impl(args, LineKind::Strikethrough)
@ -172,6 +124,53 @@ properties! {
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 {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() {

View File

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

View File

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

View File

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

View File

@ -149,6 +149,11 @@ impl SourceFile {
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.
pub fn ast(&self) -> TypResult<Markup> {
let red = RedNode::from_root(self.root.clone(), self.id);

View File

@ -211,6 +211,8 @@ pub enum Expr {
With(WithExpr),
/// A let expression: `let x = 1`.
Let(LetExpr),
/// A set expression: `set text(...)`.
Set(SetExpr),
/// An if-else expression: `if x { y } else { z }`.
If(IfExpr),
/// A while loop expression: `while x { y }`.
@ -238,6 +240,7 @@ impl TypedNode for Expr {
NodeKind::Closure => node.cast().map(Self::Closure),
NodeKind::WithExpr => node.cast().map(Self::With),
NodeKind::LetExpr => node.cast().map(Self::Let),
NodeKind::SetExpr => node.cast().map(Self::Set),
NodeKind::IfExpr => node.cast().map(Self::If),
NodeKind::WhileExpr => node.cast().map(Self::While),
NodeKind::ForExpr => node.cast().map(Self::For),
@ -262,6 +265,7 @@ impl TypedNode for Expr {
Self::Closure(v) => v.as_red(),
Self::With(v) => v.as_red(),
Self::Let(v) => v.as_red(),
Self::Set(v) => v.as_red(),
Self::If(v) => v.as_red(),
Self::While(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! {
/// An import expression: `import a, b, c from "utils.typ"`.
ImportExpr

View File

@ -96,22 +96,23 @@ impl Category {
NodeKind::EnDash => Some(Category::Shortcut),
NodeKind::EmDash => Some(Category::Shortcut),
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::And => Some(Category::Keyword),
NodeKind::Or => 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::Star => Some(Category::Operator),
NodeKind::Slash => Some(Category::Operator),
@ -139,6 +140,7 @@ impl Category {
Some(Category::Function)
}
NodeKind::WithExpr => Some(Category::Function),
NodeKind::SetExpr => Some(Category::Function),
NodeKind::Call => Some(Category::Function),
_ => Some(Category::Variable),
},
@ -161,21 +163,22 @@ impl Category {
NodeKind::Array => None,
NodeKind::Dict => None,
NodeKind::Named => None,
NodeKind::Template => None,
NodeKind::Group => None,
NodeKind::Block => None,
NodeKind::Unary => None,
NodeKind::Binary => None,
NodeKind::Call => None,
NodeKind::CallArgs => None,
NodeKind::Spread => None,
NodeKind::Closure => 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::LetExpr => None,
NodeKind::SetExpr => None,
NodeKind::IfExpr => None,
NodeKind::WhileExpr => None,
NodeKind::ForExpr => None,
NodeKind::ForPattern => None,
NodeKind::ImportExpr => None,
NodeKind::ImportItems => None,

View File

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

View File

@ -225,6 +225,7 @@ impl Pretty for Expr {
Self::Closure(v) => v.pretty(p),
Self::With(v) => v.pretty(p),
Self::Let(v) => v.pretty(p),
Self::Set(v) => v.pretty(p),
Self::If(v) => v.pretty(p),
Self::While(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 {
fn pretty(&self, p: &mut Printer) {
p.push_str("if ");
@ -639,6 +650,7 @@ mod tests {
// Control flow.
roundtrip("#let x = 1 + 2");
roundtrip("#let f(x) = y");
roundtrip("#set text(size: 12pt)");
roundtrip("#if x [y] else [z]");
roundtrip("#if x {} else if y {} else {}");
roundtrip("#while x {y}");

View File

@ -5,7 +5,8 @@
// Ref: true
// Ommitted space.
[#font(weight:"bold")Bold]
#let f() = {}
[#f()*Bold*]
// Call return value of function with body.
#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()}
---
#let x = "x"
// Error: 1-3 expected function or collection, found string
// Error: 1-3 expected callable or collection, found string
#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)
---
#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)
---

View File

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

View File

@ -4,14 +4,14 @@
---
// Test standard argument overriding.
{
let font(style: "normal", weight: "regular") = {
let f(style: "normal", weight: "regular") = {
"(style: " + style + ", weight: " + weight + ")"
}
let myfont(..args) = font(weight: "bold", ..args)
test(myfont(), "(style: normal, weight: bold)")
test(myfont(weight: "black"), "(style: normal, weight: black)")
test(myfont(style: "italic"), "(style: italic, weight: bold)")
let myf(..args) = f(weight: "bold", ..args)
test(myf(), "(style: normal, weight: bold)")
test(myf(weight: "black"), "(style: normal, weight: black)")
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* \
*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. \
#circle(fill: forest,
rect(fill: conifer, stroke: white, padding: 4pt)[
#font(8pt)
#set text(8pt)
But, soft! what light through yonder window breaks?
]
)
@ -38,7 +38,7 @@ Expanded by height.
---
// Test relative sizing.
#let centered(body) = align(center + horizon, body)
#font(fill: white)
#set text(fill: white)
#rect(width: 100pt, height: 50pt, fill: rgb("aaa"), centered[
#circle(radius: 10pt, fill: eastern, centered[A]) // D=20pt
#circle(height: 60%, fill: eastern, centered[B]) // D=30pt

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// Test alignment.
---
#page(height: 100pt)
#set page(height: 100pt)
#stack(dir: ltr,
align(left, square(size: 15pt, 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
// than default size.
#page(width: 120pt, height: 70pt)
#set page(width: 120pt, height: 70pt)
#square(width: 50%, align(bottom)[A])
#square(height: 50%)
#box(stack(square(size: 10pt), 5pt, square(size: 10pt, [B])))
---
// Test alignment in automatically sized square and circle.
#font(8pt)
#set text(8pt)
#square(padding: 4pt)[
Hey there, #align(center + bottom, rotate(180deg, [you!]))
]
@ -23,19 +23,19 @@
---
// 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))
---
// Test different ways of sizing.
#page(width: 120pt, height: 40pt)
#set page(width: 120pt, height: 40pt)
#circle(radius: 5pt)
#circle(width: 10%)
#circle(height: 50%)
---
// 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%)[Hello]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
---
// Test that trailing linebreak doesn't overflow the region.
#page(height: 2cm)
#set page(height: 2cm)
#grid[
Hello \
Hello \
@ -12,7 +12,7 @@
---
// Test that broken cell expands vertically.
#page(height: 2.25cm)
#set page(height: 2.25cm)
#grid(
columns: 2,
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.
#page(height: 6cm)
#set page(height: 6cm)
#align(left)[Before]
#pad(10pt, image("../../res/tiger.jpg"))
#align(right)[After]

View File

@ -2,33 +2,33 @@
---
// Set width and height.
#page(width: 80pt, height: 80pt)
[#page(width: 40pt) High]
[#page(height: 40pt) Wide]
#set page(width: 80pt, height: 80pt)
[#set page(width: 40pt);High]
[#set page(height: 40pt);Wide]
// Set all margins at once.
[
#page(margins: 5pt)
#set page(margins: 5pt)
#place(top + left)[TL]
#place(bottom + right)[BR]
]
// Set individual margins.
#page(height: 40pt)
[#page(left: 0pt) #align(left)[Left]]
[#page(right: 0pt) #align(right)[Right]]
[#page(top: 0pt) #align(top)[Top]]
[#page(bottom: 0pt) #align(bottom)[Bottom]]
#set page(height: 40pt)
[#set page(left: 0pt); #align(left)[Left]]
[#set page(right: 0pt); #align(right)[Right]]
[#set page(top: 0pt); #align(top)[Top]]
[#set page(bottom: 0pt); #align(bottom)[Bottom]]
// Ensure that specific margins override general margins.
[#page(margins: 0pt, left: 20pt) Overriden]
[#set page(margins: 0pt, left: 20pt); Overriden]
// 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)
#font(15pt, "Roboto", fill: white, smallcaps: true)[Typst]
#set page(width: 80pt, height: 40pt, fill: eastern)
#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

View File

@ -3,7 +3,7 @@
---
First of two
#pagebreak()
#page(height: 40pt)
#set page(height: 40pt)
Second of two
---
@ -12,7 +12,7 @@ A
#box[
B
#pagebreak()
#page("a4")
#set page("a4")
]
C
@ -23,13 +23,13 @@ D
---
// 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]
[#page(width: 70pt) Second]
Fi[#set page(width: 80pt);rst]
[#set page(width: 70pt); Second]
#pagebreak()
#pagebreak()
Fourth
#page(height: 20pt)[]
Sixth
[#page() Seventh]
[#set page(); Seventh]

View File

@ -1,7 +1,7 @@
// Test the `place` function.
---
#page("a8")
#set page("a8")
#place(bottom + center)[© Typst]
= 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.
#page("a8", height: 60pt)
#set page("a8", height: 60pt)
First

View File

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

View File

@ -15,13 +15,13 @@
#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)
---
// Test RTL alignment.
#page(width: 50pt, margins: 5pt)
#font(8pt)
#set page(width: 50pt, margins: 5pt)
#set text(8pt)
#stack(dir: rtl,
align(center, [A]),
align(left, [B]),
@ -30,8 +30,8 @@
---
// Test spacing.
#page(width: 50pt, margins: 0pt)
#par(spacing: 5pt)
#set page(width: 50pt, margins: 0pt)
#set par(spacing: 5pt)
#let x = square(size: 10pt, fill: eastern)
#stack(dir: rtl, spacing: 5pt, x, x, x)
@ -40,7 +40,7 @@
---
// Test overflow.
#page(width: 50pt, height: 30pt, margins: 0pt)
#set page(width: 50pt, height: 30pt, margins: 0pt)
#box(stack(
rect(width: 40pt, height: 20pt, fill: conifer),
rect(width: 30pt, height: 13pt, fill: forest),

View File

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

View File

@ -23,13 +23,13 @@
[X]
}
#font("Latin Modern Math", size)
#set text("Latin Modern Math", size)
Neither #tex, \
nor #xetex!
---
// Test combination of scaling and rotation.
#page(height: 80pt)
#set page(height: 80pt)
#align(center + horizon,
rotate(20deg, scale(70%, image("../../res/tiger.jpg")))
)
@ -43,7 +43,7 @@ nor #xetex!
---
// Test setting scaling origin.
#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%, origin: center)
#scale(r, x: 50%, y: 200%, origin: right + bottom)

View File

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

View File

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

View File

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

View File

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

View File

@ -2,57 +2,57 @@
---
// Set same font size in three different ways.
#font(20pt)[A]
#font(200%)[A]
#font(size: 15pt + 50%)[A]
#text(20pt)[A]
#text(200%)[A]
#text(size: 15pt + 50%)[A]
// Do nothing.
#font()[Normal]
#text()[Normal]
// Set style (is available).
#font(style: "italic")[Italic]
#text(style: "italic")[Italic]
// Set weight (is available).
#font(weight: "bold")[Bold]
#text(weight: "bold")[Bold]
// Set stretch (not available, matching closest).
#font(stretch: 50%)[Condensed]
#text(stretch: 50%)[Condensed]
// Set family.
#font(family: serif)[Serif]
#text(family: serif)[Serif]
// Emoji.
Emoji: 🐪, 🌋, 🏞
// Math.
#font("Latin Modern Math")[ 𝛼 + 3𝛽 d𝑡]
#text("Latin Modern Math")[ 𝛼 + 3𝛽 d𝑡]
// Colors.
[
#font(fill: eastern)
This is #font(rgb("FA644B"))[way more] colorful.
#set text(fill: eastern)
This is #text(rgb("FA644B"))[way more] colorful.
]
// Disable font fallback beyond the user-specified list.
// 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.
#font(sans-serif: "PT Sans")
#font(family: sans-serif)[Sans-serif.] \
#font(monospace)[Monospace.] \
#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.]
#set text(sans-serif: "PT Sans")
#text(family: sans-serif)[Sans-serif.] \
#text(monospace)[Monospace.] \
#text(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.]
---
// Test top and bottom edge.
#page(width: 150pt)
#font(size: 8pt)
#set page(width: 150pt)
#set text(size: 8pt)
#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
]
@ -64,33 +64,33 @@ Emoji: 🐪, 🌋, 🏞
#try(1pt + 27%, -18%)
---
// Error: 7-12 unexpected argument
#font(false)
// Error: 11-16 unexpected argument
#set text(false)
---
// Error: 14-20 expected "normal", "italic" or "oblique"
#font(style: "bold", weight: "thin")
// Error: 18-24 expected "normal", "italic" or "oblique"
#set text(style: "bold", weight: "thin")
---
// Error: 17-19 expected linear or string, found array
#font(top-edge: ())
// Error: 21-23 expected linear or string, found array
#set text(top-edge: ())
---
// Error: 17-19 unknown font metric
#font(top-edge: "")
// Error: 21-23 unknown font metric
#set text(top-edge: "")
---
// Error: 14-15 expected string or array of strings, found integer
#font(serif: 0)
// Error: 18-19 expected string or array of strings, found integer
#set text(serif: 0)
---
// Error: 19-23 unexpected argument
#font(size: 10pt, 12pt)
// Error: 23-27 unexpected argument
#set text(size: 10pt, 12pt)
---
// Error: 28-35 unexpected argument
#font(family: "Helvetica", "Arial")
// Error: 32-39 unexpected argument
#set text(family: "Helvetica", "Arial")
---
// Error: 7-27 unexpected argument
#font(something: "invalid")
// Error: 11-31 unexpected argument
#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.
#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.]
---
// Transformed link.
#page(height: 60pt)
#set page(height: 60pt)
#let link = link("https://typst.app/")[LINK]
My cool #move(x: 0.7cm, y: 0.7cm, rotate(10deg, scale(200%, link)))

View File

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

View File

@ -7,11 +7,11 @@
Le fira
// This should just shape nicely.
#font("Noto Sans Arabic")
#set text("Noto Sans Arabic")
دع النص يمطر عليك
// 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.
@ -20,7 +20,7 @@ Le fira
---
// 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.
A😀B
@ -40,6 +40,6 @@ A🐈中文B
---
// Test reshaping.
#font("Noto Serif Hebrew")
#par(lang: "he")
#set text("Noto Serif Hebrew")
#set par(lang: "he")
ס \ טֶ

View File

@ -1,12 +1,12 @@
// Test tracking characters apart or together.
---
#font(tracking: -0.01)
#set text(tracking: -0.01)
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.
A[#font(serif) ]B
A[#set text(serif); ]B
---
// Test font change after space.
Left [#font(serif)Right].
Left [#set text(serif);Right].
---
// Test that space at start of line is not trimmed.