Better space coalescing logic 🌧
This creates a smaller state machine helper type for softness coalescing, which does not own the resulting nodes. While this creates a bit more duplication in stack and par builder, it makes it a lot easier to integrate additional logic into the paragraph builder. Furthermore: - Line breaks are now "hard", that is, not coalesced with each other. - Text nodes with equal style are now merged allowing for example `f{}i` to form a ligature.
@ -65,87 +65,66 @@ impl<'a> ExecContext<'a> {
|
||||
mem::replace(&mut self.stack, stack).build()
|
||||
}
|
||||
|
||||
/// Push any node into the active paragraph.
|
||||
pub fn push(&mut self, node: impl Into<AnyNode>) {
|
||||
let align = self.state.aligns.cross;
|
||||
self.stack.par.push(ParChild::Any(node.into(), align));
|
||||
}
|
||||
|
||||
/// Push a word space into the active paragraph.
|
||||
pub fn push_word_space(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
let amount = self.state.par.word_spacing.resolve(em);
|
||||
self.stack.par.push_soft(ParChild::Spacing(amount));
|
||||
}
|
||||
|
||||
/// Push text into the active paragraph.
|
||||
///
|
||||
/// The text is split into lines at newlines.
|
||||
pub fn push_text(&mut self, text: &str) {
|
||||
let mut scanner = Scanner::new(text);
|
||||
let mut line = String::new();
|
||||
let push = |this: &mut Self, text| {
|
||||
let props = this.state.font.resolve_props();
|
||||
let node = TextNode { text, props };
|
||||
let align = this.state.aligns.cross;
|
||||
this.stack.par.folder.push(ParChild::Text(node, align))
|
||||
};
|
||||
let mut text = String::new();
|
||||
|
||||
while let Some(c) = scanner.eat_merging_crlf() {
|
||||
if is_newline(c) {
|
||||
push(self, mem::take(&mut line));
|
||||
self.push_linebreak();
|
||||
self.stack.par.push_text(mem::take(&mut text), &self.state);
|
||||
self.linebreak();
|
||||
} else {
|
||||
line.push(c);
|
||||
text.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
push(self, line);
|
||||
}
|
||||
|
||||
/// Push a word space.
|
||||
pub fn push_word_space(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
let amount = self.state.par.word_spacing.resolve(em);
|
||||
self.push_spacing(GenAxis::Cross, amount, 1);
|
||||
}
|
||||
|
||||
/// Apply a forced line break.
|
||||
pub fn push_linebreak(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
let amount = self.state.par.leading.resolve(em);
|
||||
self.push_spacing(GenAxis::Main, amount, 2);
|
||||
}
|
||||
|
||||
/// Apply a forced paragraph break.
|
||||
pub fn push_parbreak(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
let amount = self.state.par.spacing.resolve(em);
|
||||
self.push_spacing(GenAxis::Main, amount, 1);
|
||||
self.stack.par.push_text(text, &self.state);
|
||||
}
|
||||
|
||||
/// Push spacing into paragraph or stack depending on `axis`.
|
||||
///
|
||||
/// The `softness` configures how the spacing interacts with surrounding
|
||||
/// spacing.
|
||||
pub fn push_spacing(&mut self, axis: GenAxis, amount: Length, softness: u8) {
|
||||
pub fn push_spacing(&mut self, axis: GenAxis, amount: Length) {
|
||||
match axis {
|
||||
GenAxis::Main => {
|
||||
let spacing = StackChild::Spacing(amount);
|
||||
self.stack.finish_par(&self.state);
|
||||
self.stack.folder.push_soft(spacing, softness);
|
||||
self.stack.parbreak(&self.state);
|
||||
self.stack.push_hard(StackChild::Spacing(amount));
|
||||
}
|
||||
GenAxis::Cross => {
|
||||
let spacing = ParChild::Spacing(amount);
|
||||
self.stack.par.folder.push_soft(spacing, softness);
|
||||
self.stack.par.push_hard(ParChild::Spacing(amount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push any node into the active paragraph.
|
||||
pub fn push_into_par(&mut self, node: impl Into<AnyNode>) {
|
||||
let align = self.state.aligns.cross;
|
||||
self.stack.par.folder.push(ParChild::Any(node.into(), align));
|
||||
/// Apply a forced line break.
|
||||
pub fn linebreak(&mut self) {
|
||||
self.stack.par.push_hard(ParChild::Linebreak);
|
||||
}
|
||||
|
||||
/// Push any node directly into the stack of paragraphs.
|
||||
///
|
||||
/// This finishes the active paragraph and starts a new one.
|
||||
pub fn push_into_stack(&mut self, node: impl Into<AnyNode>) {
|
||||
let aligns = self.state.aligns;
|
||||
self.stack.finish_par(&self.state);
|
||||
self.stack.folder.push(StackChild::Any(node.into(), aligns));
|
||||
/// Apply a forced paragraph break.
|
||||
pub fn parbreak(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
let amount = self.state.par.spacing.resolve(em);
|
||||
self.stack.parbreak(&self.state);
|
||||
self.stack.push_soft(StackChild::Spacing(amount));
|
||||
}
|
||||
|
||||
/// Finish the active page.
|
||||
pub fn finish_page(&mut self, keep: bool, hard: bool, source: Span) {
|
||||
/// Apply a forced page break.
|
||||
pub fn pagebreak(&mut self, keep: bool, hard: bool, source: Span) {
|
||||
if let Some(builder) = &mut self.page {
|
||||
let page = mem::replace(builder, PageBuilder::new(&self.state, hard));
|
||||
let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state));
|
||||
@ -158,7 +137,7 @@ impl<'a> ExecContext<'a> {
|
||||
/// Finish execution and return the created layout tree.
|
||||
pub fn finish(mut self) -> Pass<Tree> {
|
||||
assert!(self.page.is_some());
|
||||
self.finish_page(true, false, Span::default());
|
||||
self.pagebreak(true, false, Span::default());
|
||||
Pass::new(self.tree, self.diags)
|
||||
}
|
||||
}
|
||||
@ -189,7 +168,8 @@ impl PageBuilder {
|
||||
|
||||
struct StackBuilder {
|
||||
dirs: Gen<Dir>,
|
||||
folder: SoftFolder<StackChild>,
|
||||
children: Vec<StackChild>,
|
||||
last: Last<StackChild>,
|
||||
par: ParBuilder,
|
||||
}
|
||||
|
||||
@ -197,20 +177,36 @@ impl StackBuilder {
|
||||
fn new(state: &State) -> Self {
|
||||
Self {
|
||||
dirs: Gen::new(Dir::TTB, state.lang.dir),
|
||||
folder: SoftFolder::new(),
|
||||
children: vec![],
|
||||
last: Last::None,
|
||||
par: ParBuilder::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_par(&mut self, state: &State) {
|
||||
fn push_soft(&mut self, child: StackChild) {
|
||||
self.last.soft(child);
|
||||
}
|
||||
|
||||
fn push_hard(&mut self, child: StackChild) {
|
||||
self.last.hard();
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
fn parbreak(&mut self, state: &State) {
|
||||
let par = mem::replace(&mut self.par, ParBuilder::new(state));
|
||||
self.folder.extend(par.build());
|
||||
if let Some(par) = par.build() {
|
||||
self.children.extend(self.last.any());
|
||||
self.children.push(par);
|
||||
}
|
||||
}
|
||||
|
||||
fn build(self) -> StackNode {
|
||||
let Self { dirs, mut folder, par } = self;
|
||||
folder.extend(par.build());
|
||||
StackNode { dirs, children: folder.finish() }
|
||||
let Self { dirs, mut children, par, mut last } = self;
|
||||
if let Some(par) = par.build() {
|
||||
children.extend(last.any());
|
||||
children.push(par);
|
||||
}
|
||||
StackNode { dirs, children }
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,7 +214,8 @@ struct ParBuilder {
|
||||
aligns: Gen<Align>,
|
||||
dir: Dir,
|
||||
line_spacing: Length,
|
||||
folder: SoftFolder<ParChild>,
|
||||
children: Vec<ParChild>,
|
||||
last: Last<ParChild>,
|
||||
}
|
||||
|
||||
impl ParBuilder {
|
||||
@ -228,13 +225,43 @@ impl ParBuilder {
|
||||
aligns: state.aligns,
|
||||
dir: state.lang.dir,
|
||||
line_spacing: state.par.leading.resolve(em),
|
||||
folder: SoftFolder::new(),
|
||||
children: vec![],
|
||||
last: Last::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, child: ParChild) {
|
||||
self.children.extend(self.last.any());
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
fn push_text(&mut self, text: String, state: &State) {
|
||||
self.children.extend(self.last.any());
|
||||
|
||||
let align = state.aligns.cross;
|
||||
let props = state.font.resolve_props();
|
||||
|
||||
if let Some(ParChild::Text(prev, prev_align)) = self.children.last_mut() {
|
||||
if *prev_align == align && prev.props == props {
|
||||
prev.text.push_str(&text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.children.push(ParChild::Text(TextNode { text, props }, align));
|
||||
}
|
||||
|
||||
fn push_soft(&mut self, child: ParChild) {
|
||||
self.last.soft(child);
|
||||
}
|
||||
|
||||
fn push_hard(&mut self, child: ParChild) {
|
||||
self.last.hard();
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
fn build(self) -> Option<StackChild> {
|
||||
let Self { aligns, dir, line_spacing, folder } = self;
|
||||
let children = folder.finish();
|
||||
let Self { aligns, dir, line_spacing, children, .. } = self;
|
||||
(!children.is_empty()).then(|| {
|
||||
let node = ParNode { dir, line_spacing, children };
|
||||
StackChild::Any(node.into(), aligns)
|
||||
@ -242,54 +269,28 @@ impl ParBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// This is used to remove leading and trailing word/line/paragraph spacing
|
||||
/// as well as collapse sequences of spacings into just one.
|
||||
struct SoftFolder<N> {
|
||||
nodes: Vec<N>,
|
||||
last: Last<N>,
|
||||
}
|
||||
|
||||
/// Finite state machine for spacing coalescing.
|
||||
enum Last<N> {
|
||||
None,
|
||||
Hard,
|
||||
Soft(N, u8),
|
||||
Any,
|
||||
Soft(N),
|
||||
}
|
||||
|
||||
impl<N> SoftFolder<N> {
|
||||
fn new() -> Self {
|
||||
Self { nodes: vec![], last: Last::Hard }
|
||||
}
|
||||
|
||||
fn push(&mut self, node: N) {
|
||||
let last = mem::replace(&mut self.last, Last::None);
|
||||
if let Last::Soft(soft, _) = last {
|
||||
self.nodes.push(soft);
|
||||
}
|
||||
self.nodes.push(node);
|
||||
}
|
||||
|
||||
fn push_soft(&mut self, node: N, softness: u8) {
|
||||
if softness == 0 {
|
||||
self.last = Last::Hard;
|
||||
self.nodes.push(node);
|
||||
} else {
|
||||
match self.last {
|
||||
Last::Hard => {}
|
||||
Last::Soft(_, other) if softness >= other => {}
|
||||
_ => self.last = Last::Soft(node, softness),
|
||||
}
|
||||
impl<N> Last<N> {
|
||||
fn any(&mut self) -> Option<N> {
|
||||
match mem::replace(self, Self::Any) {
|
||||
Self::Soft(soft) => Some(soft),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Vec<N> {
|
||||
self.nodes
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> Extend<N> for SoftFolder<N> {
|
||||
fn extend<T: IntoIterator<Item = N>>(&mut self, iter: T) {
|
||||
for elem in iter {
|
||||
self.push(elem);
|
||||
fn soft(&mut self, soft: N) {
|
||||
if let Self::Any = self {
|
||||
*self = Self::Soft(soft);
|
||||
}
|
||||
}
|
||||
|
||||
fn hard(&mut self) {
|
||||
*self = Self::None;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ pub enum ParChild {
|
||||
Text(TextNode, Align),
|
||||
/// Any child node and how to align it in its line.
|
||||
Any(AnyNode, Align),
|
||||
/// A forced linebreak.
|
||||
Linebreak,
|
||||
}
|
||||
|
||||
/// A consecutive, styled run of text.
|
||||
@ -55,6 +57,7 @@ impl Layout for ParNode {
|
||||
layouter.push_frame(frame, align);
|
||||
}
|
||||
}
|
||||
ParChild::Linebreak => layouter.finish_line(),
|
||||
}
|
||||
}
|
||||
layouter.finish()
|
||||
|
@ -52,7 +52,7 @@ pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
if let Some(vertical) = vertical {
|
||||
ctx.state.aligns.main = vertical.to_align(Dir::TTB);
|
||||
if ctx.state.aligns.main != snapshot.aligns.main {
|
||||
ctx.push_linebreak();
|
||||
ctx.parbreak();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let loaded = ctx.env.resources.load(&path.v, ImageResource::parse);
|
||||
if let Some((res, img)) = loaded {
|
||||
let dimensions = img.buf.dimensions();
|
||||
ctx.push_into_par(ImageNode { res, dimensions, width, height });
|
||||
ctx.push(ImageNode { res, dimensions, width, height });
|
||||
} else {
|
||||
ctx.diag(error!(path.span, "failed to load image"));
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.push_parbreak();
|
||||
ctx.parbreak();
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ use crate::syntax::{HeadingNode, RawNode};
|
||||
/// A template that inserts a line break.
|
||||
pub fn linebreak(_: &mut EvalContext, _: &mut FuncArgs) -> Value {
|
||||
Value::template(Node::LINEBREAK, move |ctx| {
|
||||
ctx.push_linebreak();
|
||||
ctx.linebreak();
|
||||
})
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ pub fn linebreak(_: &mut EvalContext, _: &mut FuncArgs) -> Value {
|
||||
/// A template that inserts a paragraph break.
|
||||
pub fn parbreak(_: &mut EvalContext, _: &mut FuncArgs) -> Value {
|
||||
Value::template(Node::PARBREAK, move |ctx| {
|
||||
ctx.push_parbreak();
|
||||
ctx.parbreak();
|
||||
})
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ pub fn heading(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
body.exec(ctx);
|
||||
ctx.state = snapshot;
|
||||
|
||||
ctx.push_parbreak();
|
||||
ctx.parbreak();
|
||||
})
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ pub fn raw(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
|
||||
Value::template(Node::RAW, move |ctx| {
|
||||
if block {
|
||||
ctx.push_parbreak();
|
||||
ctx.parbreak();
|
||||
}
|
||||
|
||||
let snapshot = ctx.state.clone();
|
||||
@ -164,7 +164,7 @@ pub fn raw(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
ctx.state = snapshot;
|
||||
|
||||
if block {
|
||||
ctx.push_parbreak();
|
||||
ctx.parbreak();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -32,6 +32,6 @@ pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
|
||||
Value::template("pad", move |ctx| {
|
||||
let child = ctx.exec_group(&body).into();
|
||||
ctx.push_into_par(PadNode { padding, child });
|
||||
ctx.push(PadNode { padding, child });
|
||||
})
|
||||
}
|
||||
|
@ -83,13 +83,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
std::mem::swap(&mut page.size.width, &mut page.size.height);
|
||||
}
|
||||
|
||||
ctx.finish_page(false, true, span);
|
||||
ctx.pagebreak(false, true, span);
|
||||
|
||||
if let Some(body) = &body {
|
||||
// TODO: Restrict body to a single page?
|
||||
body.exec(ctx);
|
||||
ctx.state = snapshot;
|
||||
ctx.finish_page(true, false, span);
|
||||
ctx.pagebreak(true, false, span);
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -101,6 +101,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
pub fn pagebreak(_: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let span = args.span;
|
||||
Value::template("pagebreak", move |ctx| {
|
||||
ctx.finish_page(true, true, span);
|
||||
ctx.pagebreak(true, true, span);
|
||||
})
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
ctx.state.par.word_spacing = word_spacing;
|
||||
}
|
||||
|
||||
ctx.push_parbreak();
|
||||
ctx.parbreak();
|
||||
})
|
||||
}
|
||||
|
@ -63,13 +63,13 @@ fn rect_impl(
|
||||
let node = FixedNode { width, height, aspect, child };
|
||||
|
||||
if let Some(color) = fill {
|
||||
ctx.push_into_par(BackgroundNode {
|
||||
ctx.push(BackgroundNode {
|
||||
shape: BackgroundShape::Rect,
|
||||
fill: Fill::Color(color),
|
||||
child: node.into(),
|
||||
});
|
||||
} else {
|
||||
ctx.push_into_par(node);
|
||||
ctx.push(node);
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -146,13 +146,13 @@ fn ellipse_impl(
|
||||
};
|
||||
|
||||
if let Some(color) = fill {
|
||||
ctx.push_into_par(BackgroundNode {
|
||||
ctx.push(BackgroundNode {
|
||||
shape: BackgroundShape::Ellipse,
|
||||
fill: Fill::Color(color),
|
||||
child: node.into(),
|
||||
});
|
||||
} else {
|
||||
ctx.push_into_par(node);
|
||||
ctx.push(node);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ fn spacing_impl(
|
||||
Value::template(name, move |ctx| {
|
||||
if let Some(linear) = spacing {
|
||||
let amount = linear.resolve(ctx.state.font.resolve_size());
|
||||
ctx.push_spacing(axis, amount, 0);
|
||||
ctx.push_spacing(axis, amount);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
@ -14,8 +14,8 @@
|
||||
|
||||
// Dictionary is not traversed in insertion order.
|
||||
// Should output `age: 1, name: Typst,`.
|
||||
#for k, v in (name: "Typst", age: 2) [
|
||||
{k}: {v}, \
|
||||
#for k, v in (Name: "Typst", Age: 2) [
|
||||
{k}: {v}.
|
||||
]
|
||||
|
||||
// String.
|
||||
|
@ -7,7 +7,7 @@
|
||||
// Top-level paragraph fills page, boxed paragraph only when width is fixed.
|
||||
L #right[R] \
|
||||
#rect(width: 50pt)[L #right[R]] \
|
||||
#rect[L #right[R]] \
|
||||
#rect[L #right[R]]
|
||||
|
||||
// Pad inherits expansion behaviour.
|
||||
#pad[PL #right[PR]] \
|
||||
|
@ -60,7 +60,7 @@
|
||||
#args[a] \
|
||||
#args(a) \
|
||||
#args(a, [b]) \
|
||||
#args(a)[b] \
|
||||
#args(a)[b]
|
||||
|
||||
// Template can be argument or body depending on whitespace.
|
||||
#if "template" == type[b] [Sure ]
|
||||
|
@ -53,7 +53,7 @@ Emoji: 🐪, 🌋, 🏞
|
||||
#font(sans-serif: "PT Sans")
|
||||
#font(sans-serif)[Sans-serif.] \
|
||||
#font(monospace)[Monospace.] \
|
||||
#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.] \
|
||||
#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.]
|
||||
|
||||
---
|
||||
// Ref: false
|
||||
|
@ -10,17 +10,11 @@ Line \ Break
|
||||
// Directly before word does not work.
|
||||
No \Break
|
||||
|
||||
---
|
||||
// Leading line break.
|
||||
\ Leading
|
||||
\ Before
|
||||
|
||||
// Trailing before paragraph break.
|
||||
Trailing 1 \
|
||||
Multiple \ \ \
|
||||
|
||||
Trailing 2
|
||||
|
||||
// Trailing before end of document.
|
||||
Trailing 3 \
|
||||
Times
|
||||
|
||||
---
|
||||
#let linebreak() = [
|
||||
@ -28,4 +22,4 @@ Trailing 3 \
|
||||
#circle(radius: 2pt, fill: #000) \
|
||||
]
|
||||
|
||||
A \ B \ C \
|
||||
A \ B \ C
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Test the non breaking space.
|
||||
|
||||
---
|
||||
// Parsed correctly, but not actually doing anything at the moment.
|
||||
The non-breaking~space does not work.
|
||||
The non-breaking~space does work.
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
{name} \
|
||||
{ke-bab} \
|
||||
{α} \
|
||||
{α}
|
||||
|
||||
// Error: 2-3 unknown variable
|
||||
{_}
|
||||
@ -18,7 +18,7 @@
|
||||
// Literal values.
|
||||
{none} (empty) \
|
||||
{true} \
|
||||
{false} \
|
||||
{false}
|
||||
|
||||
---
|
||||
// Numerical values.
|
||||
@ -33,16 +33,16 @@
|
||||
{2.5rad} \
|
||||
{45deg} \
|
||||
// Not in monospace via repr.
|
||||
#repr(45deg) \
|
||||
#repr(45deg)
|
||||
|
||||
---
|
||||
// Colors.
|
||||
{#f7a20500} \
|
||||
{#f7a20500}
|
||||
|
||||
---
|
||||
// Strings and escaping.
|
||||
{"hi"} \
|
||||
{"a\n[]\"\u{1F680}string"} \
|
||||
{"a\n[]\"\u{1F680}string"}
|
||||
|
||||
---
|
||||
// Templates.
|
||||
@ -54,4 +54,4 @@
|
||||
|
||||
{rect} \
|
||||
{f} \
|
||||
{() => none} \
|
||||
{() => none}
|
||||
|
@ -7,7 +7,7 @@
|
||||
A#let;B \
|
||||
A#let x = 1;B #test(x, 1) \
|
||||
A #let x = 2;B #test(x, 2) \
|
||||
A#let x = 3; B #test(x, 3) \
|
||||
A#let x = 3; B #test(x, 3)
|
||||
|
||||
---
|
||||
// Spacing around if-else.
|
||||
@ -17,7 +17,7 @@ A#if true [B] C \
|
||||
A #if true{"B"}C \
|
||||
A #if true{"B"} C \
|
||||
A#if false [] #else [B]C \
|
||||
A#if true [B] #else [] C \
|
||||
A#if true [B] #else [] C
|
||||
|
||||
---
|
||||
// Spacing around while loop.
|
||||
@ -25,11 +25,11 @@ A#if true [B] #else [] C \
|
||||
#let c = true; A#while c [{c = false}B]C \
|
||||
#let c = true; A#while c [{c = false}B] C \
|
||||
#let c = true; A #while c { c = false; "B" }C \
|
||||
#let c = true; A #while c { c = false; "B" } C \
|
||||
#let c = true; A #while c { c = false; "B" } C
|
||||
|
||||
---
|
||||
// Spacing around for loop.
|
||||
|
||||
A#for _ in (none,) [B]C \
|
||||
A#for _ in (none,) [B] C \
|
||||
A #for _ in (none,) {"B"}C \
|
||||
A #for _ in (none,) {"B"}C
|
||||
|