summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-12-15 20:27:41 +0100
committerLaurenz <laurmaedje@gmail.com>2021-12-15 20:27:41 +0100
commit2a3d0f4b390457174ed09347dd29e97ff9a783e4 (patch)
tree0e0634bff6b7f64131267f4cbe05651c1c91d900 /src
parent244ad386ec271ff86a2101eb4cc38d37a55552b9 (diff)
Set Rules Episode VII: The Set Awakens
Diffstat (limited to 'src')
-rw-r--r--src/eval/class.rs101
-rw-r--r--src/eval/mod.rs28
-rw-r--r--src/eval/node.rs10
-rw-r--r--src/eval/scope.rs29
-rw-r--r--src/eval/value.rs7
-rw-r--r--src/library/mod.rs28
-rw-r--r--src/library/page.rs92
-rw-r--r--src/library/par.rs88
-rw-r--r--src/library/text.rs95
-rw-r--r--src/parse/mod.rs65
-rw-r--r--src/parse/parser.rs3
-rw-r--r--src/parse/tokens.rs1
-rw-r--r--src/source.rs5
-rw-r--r--src/syntax/ast.rs23
-rw-r--r--src/syntax/highlight.rs29
-rw-r--r--src/syntax/mod.rs101
-rw-r--r--src/syntax/pretty.rs12
17 files changed, 471 insertions, 246 deletions
diff --git a/src/eval/class.rs b/src/eval/class.rs
new file mode 100644
index 00000000..45674933
--- /dev/null
+++ b/src/eval/class.rs
@@ -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)
+ }
+}
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 6dcff900..ae330134 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -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;
diff --git a/src/eval/node.rs b/src/eval/node.rs
index 5653beff..a04fe84b 100644
--- a/src/eval/node.rs
+++ b/src/eval/node.rs
@@ -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.
diff --git a/src/eval/scope.rs b/src/eval/scope.rs
index ffe2d63e..5178c819 100644
--- a/src/eval/scope.rs
+++ b/src/eval/scope.rs
@@ -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)
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 2cf82a26..0995ab75 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -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 {
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 9b6da6a9..1e2ee224 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -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);
diff --git a/src/library/page.rs b/src/library/page.rs
index 4a4fab03..0b0cc2d9 100644
--- a/src/library/page.rs
+++ b/src/library/page.rs
@@ -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 {
diff --git a/src/library/par.rs b/src/library/par.rs
index 6ea960bf..d63c2315 100644
--- a/src/library/par.rs
+++ b/src/library/par.rs
@@ -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,
diff --git a/src/library/text.rs b/src/library/text.rs
index 4ab378c2..4baf4bc5 100644
--- a/src/library/text.rs
+++ b/src/library/text.rs
@@ -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() {
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
index dbec0a5e..0a2f73f5 100644
--- a/src/parse/mod.rs
+++ b/src/parse/mod.rs
@@ -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)
})
}
diff --git a/src/parse/parser.rs b/src/parse/parser.rs
index 6598b1f2..503158a9 100644
--- a/src/parse/parser.rs
+++ b/src/parse/parser.rs
@@ -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");
diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs
index 07a6fe12..27ec046d 100644
--- a/src/parse/tokens.rs
+++ b/src/parse/tokens.rs
@@ -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,
diff --git a/src/source.rs b/src/source.rs
index 509b0a76..5fd85ed9 100644
--- a/src/source.rs
+++ b/src/source.rs
@@ -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);
diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs
index 8df25f59..9190953f 100644
--- a/src/syntax/ast.rs
+++ b/src/syntax/ast.rs
@@ -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(),
@@ -838,6 +842,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
}
diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs
index 22e6cf50..85fbef12 100644
--- a/src/syntax/highlight.rs
+++ b/src/syntax/highlight.rs
@@ -96,22 +96,23 @@ impl Category {
NodeKind::EnDash => Some(Category::Shortcut),
NodeKind::EmDash => Some(Category::Shortcut),
NodeKind::Escape(_) => Some(Category::Escape),
+ 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::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::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,
diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs
index e9011a4d..b9b00487 100644
--- a/src/syntax/mod.rs
+++ b/src/syntax/mod.rs
@@ -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",
diff --git a/src/syntax/pretty.rs b/src/syntax/pretty.rs
index c453fb56..62ecb8cd 100644
--- a/src/syntax/pretty.rs
+++ b/src/syntax/pretty.rs
@@ -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}");