Set Rules Episode VII: The Set Awakens
This commit is contained in:
parent
244ad386ec
commit
2a3d0f4b39
@ -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
101
src/eval/class.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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}");
|
||||
|
@ -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)
|
||||
|
||||
---
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Test include statements.
|
||||
|
||||
---
|
||||
#page(width: 200pt)
|
||||
#set page(width: 200pt)
|
||||
|
||||
= Document
|
||||
|
||||
|
@ -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)")
|
||||
}
|
||||
|
||||
---
|
||||
|
@ -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 \
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
]
|
||||
|
@ -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")
|
||||
|
@ -5,7 +5,7 @@
|
||||
#rect()
|
||||
|
||||
---
|
||||
#page(width: 150pt)
|
||||
#set page(width: 150pt)
|
||||
|
||||
// Fit to text.
|
||||
#rect(fill: conifer, padding: 3pt)[Textbox]
|
||||
|
@ -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?
|
||||
]
|
||||
|
@ -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)),
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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,
|
||||
|
@ -12,7 +12,7 @@ Apart
|
||||
---
|
||||
// Test block over multiple pages.
|
||||
|
||||
#page(height: 60pt)
|
||||
#set page(height: 60pt)
|
||||
|
||||
First!
|
||||
#block[
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
---
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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?]
|
||||
|
@ -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
|
||||
|
@ -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 \
|
||||
Lריווח #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)
|
||||
|
@ -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集
|
||||
由执行制作人戴蒙·林道夫和卡尔顿·库斯编剧,导演则是另一名执行制作人杰克·本德
|
||||
|
@ -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.])
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
2π = 𝛼 + 𝛽. ✅
|
||||
|
||||
---
|
||||
// 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")
|
||||
|
@ -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)))
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
ס \ טֶ
|
||||
|
@ -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)
|
||||
טֶקסט
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user