mirror of
https://github.com/typst/typst
synced 2025-05-13 12:36:23 +08:00
Set, show, wrap in code blocks
This commit is contained in:
parent
bfaf5447a7
commit
1927cc86da
@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Formatter, Write};
|
||||
use std::ops::{Add, AddAssign};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{Args, Func, Value};
|
||||
use super::{ops, Args, Func, Value};
|
||||
use crate::diag::{At, StrResult, TypResult};
|
||||
use crate::syntax::Spanned;
|
||||
use crate::util::ArcExt;
|
||||
@ -171,18 +171,14 @@ impl Array {
|
||||
let mut result = Value::None;
|
||||
for (i, value) in self.iter().cloned().enumerate() {
|
||||
if i > 0 {
|
||||
if i + 1 == len {
|
||||
if let Some(last) = last.take() {
|
||||
result = result.join(last)?;
|
||||
} else {
|
||||
result = result.join(sep.clone())?;
|
||||
}
|
||||
if i + 1 == len && last.is_some() {
|
||||
result = ops::join(result, last.take().unwrap())?;
|
||||
} else {
|
||||
result = result.join(sep.clone())?;
|
||||
result = ops::join(result, sep.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
result = result.join(value)?;
|
||||
result = ops::join(result, value)?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
|
@ -1,65 +0,0 @@
|
||||
use super::{ops, EvalResult, Value};
|
||||
use crate::diag::{At, TypError};
|
||||
use crate::syntax::Span;
|
||||
|
||||
/// A control flow event that occurred during evaluation.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Control {
|
||||
/// Stop iteration in a loop.
|
||||
Break(Value, Span),
|
||||
/// Skip the remainder of the current iteration in a loop.
|
||||
Continue(Value, Span),
|
||||
/// Stop execution of a function early, returning a value. The bool
|
||||
/// indicates whether this was an explicit return with value.
|
||||
Return(Value, bool, Span),
|
||||
/// Stop the execution because an error occurred.
|
||||
Err(TypError),
|
||||
}
|
||||
|
||||
impl From<TypError> for Control {
|
||||
fn from(error: TypError) -> Self {
|
||||
Self::Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Control> for TypError {
|
||||
fn from(control: Control) -> Self {
|
||||
match control {
|
||||
Control::Break(_, span) => {
|
||||
error!(span, "cannot break outside of loop")
|
||||
}
|
||||
Control::Continue(_, span) => {
|
||||
error!(span, "cannot continue outside of loop")
|
||||
}
|
||||
Control::Return(_, _, span) => {
|
||||
error!(span, "cannot return outside of function")
|
||||
}
|
||||
Control::Err(e) => e,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Join a value with an evaluated result.
|
||||
pub(super) fn join_result(
|
||||
prev: Value,
|
||||
result: EvalResult<Value>,
|
||||
result_span: Span,
|
||||
) -> EvalResult<Value> {
|
||||
match result {
|
||||
Ok(value) => Ok(ops::join(prev, value).at(result_span)?),
|
||||
Err(Control::Break(value, span)) => Err(Control::Break(
|
||||
ops::join(prev, value).at(result_span)?,
|
||||
span,
|
||||
)),
|
||||
Err(Control::Continue(value, span)) => Err(Control::Continue(
|
||||
ops::join(prev, value).at(result_span)?,
|
||||
span,
|
||||
)),
|
||||
Err(Control::Return(value, false, span)) => Err(Control::Return(
|
||||
ops::join(prev, value).at(result_span)?,
|
||||
false,
|
||||
span,
|
||||
)),
|
||||
other => other,
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{Args, Control, Eval, Scope, Scopes, Value};
|
||||
use super::{Args, Eval, Flow, Scope, Scopes, Value};
|
||||
use crate::diag::{StrResult, TypResult};
|
||||
use crate::model::{Content, NodeId, StyleMap};
|
||||
use crate::syntax::ast::Expr;
|
||||
@ -210,11 +210,19 @@ impl Closure {
|
||||
scp.top.def_mut(sink, args.take());
|
||||
}
|
||||
|
||||
// Backup the old control flow state.
|
||||
let prev_flow = ctx.flow.take();
|
||||
|
||||
// Evaluate the body.
|
||||
let value = match self.body.eval(ctx, &mut scp) {
|
||||
Err(Control::Return(value, _, _)) => value,
|
||||
other => other?,
|
||||
};
|
||||
let mut value = self.body.eval(ctx, &mut scp)?;
|
||||
|
||||
// Handle control flow.
|
||||
match std::mem::replace(&mut ctx.flow, prev_flow) {
|
||||
Some(Flow::Return(_, Some(explicit))) => value = explicit,
|
||||
Some(Flow::Return(_, None)) => {}
|
||||
Some(flow) => return Err(flow.forbidden())?,
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
285
src/eval/mod.rs
285
src/eval/mod.rs
@ -9,10 +9,8 @@ mod value;
|
||||
|
||||
mod args;
|
||||
mod capture;
|
||||
mod control;
|
||||
mod func;
|
||||
pub mod methods;
|
||||
mod module;
|
||||
pub mod ops;
|
||||
mod raw;
|
||||
mod scope;
|
||||
@ -22,10 +20,8 @@ pub use self::str::*;
|
||||
pub use args::*;
|
||||
pub use array::*;
|
||||
pub use capture::*;
|
||||
pub use control::*;
|
||||
pub use dict::*;
|
||||
pub use func::*;
|
||||
pub use module::*;
|
||||
pub use raw::*;
|
||||
pub use scope::*;
|
||||
pub use value::*;
|
||||
@ -35,10 +31,11 @@ use std::collections::BTreeMap;
|
||||
use parking_lot::{MappedRwLockWriteGuard, RwLockWriteGuard};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::diag::{At, StrResult, Trace, Tracepoint, TypResult};
|
||||
use crate::diag::{At, StrResult, Trace, Tracepoint, TypError, TypResult};
|
||||
use crate::geom::{Angle, Em, Fraction, Length, Ratio};
|
||||
use crate::library;
|
||||
use crate::model::{Content, Pattern, Recipe, StyleEntry, StyleMap};
|
||||
use crate::source::{SourceId, SourceStore};
|
||||
use crate::syntax::ast::*;
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::util::EcoString;
|
||||
@ -50,16 +47,60 @@ pub trait Eval {
|
||||
type Output;
|
||||
|
||||
/// Evaluate the expression to the output value.
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output>;
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output>;
|
||||
}
|
||||
|
||||
/// The result type for evaluating a syntactic construct.
|
||||
pub type EvalResult<T> = Result<T, Control>;
|
||||
/// An evaluated module, ready for importing or layouting.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Module {
|
||||
/// The top-level definitions that were bound in this module.
|
||||
pub scope: Scope,
|
||||
/// The module's layoutable contents.
|
||||
pub content: Content,
|
||||
/// The source file revisions this module depends on.
|
||||
pub deps: Vec<(SourceId, usize)>,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
/// Whether the module is still valid for the given sources.
|
||||
pub fn valid(&self, sources: &SourceStore) -> bool {
|
||||
self.deps.iter().all(|&(id, rev)| rev == sources.get(id).rev())
|
||||
}
|
||||
}
|
||||
|
||||
/// A control flow event that occurred during evaluation.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Flow {
|
||||
/// Stop iteration in a loop.
|
||||
Break(Span),
|
||||
/// Skip the remainder of the current iteration in a loop.
|
||||
Continue(Span),
|
||||
/// Stop execution of a function early, optionally returning an explicit
|
||||
/// value.
|
||||
Return(Span, Option<Value>),
|
||||
}
|
||||
|
||||
impl Flow {
|
||||
/// Return an error stating that this control flow is forbidden.
|
||||
pub fn forbidden(&self) -> TypError {
|
||||
match *self {
|
||||
Self::Break(span) => {
|
||||
error!(span, "cannot break outside of loop")
|
||||
}
|
||||
Self::Continue(span) => {
|
||||
error!(span, "cannot continue outside of loop")
|
||||
}
|
||||
Self::Return(span, _) => {
|
||||
error!(span, "cannot return outside of function")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Markup {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
eval_markup(ctx, scp, &mut self.nodes())
|
||||
}
|
||||
}
|
||||
@ -69,17 +110,25 @@ fn eval_markup(
|
||||
ctx: &mut Context,
|
||||
scp: &mut Scopes,
|
||||
nodes: &mut impl Iterator<Item = MarkupNode>,
|
||||
) -> EvalResult<Content> {
|
||||
) -> TypResult<Content> {
|
||||
let mut seq = Vec::with_capacity(nodes.size_hint().1.unwrap_or_default());
|
||||
|
||||
while let Some(node) = nodes.next() {
|
||||
seq.push(match node {
|
||||
MarkupNode::Expr(Expr::Set(set)) => {
|
||||
let styles = set.eval(ctx, scp)?;
|
||||
if ctx.flow.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
eval_markup(ctx, scp, nodes)?.styled_with_map(styles)
|
||||
}
|
||||
MarkupNode::Expr(Expr::Show(show)) => {
|
||||
let recipe = show.eval(ctx, scp)?;
|
||||
if ctx.flow.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
eval_markup(ctx, scp, nodes)?
|
||||
.styled_with_entry(StyleEntry::Recipe(recipe).into())
|
||||
}
|
||||
@ -88,8 +137,13 @@ fn eval_markup(
|
||||
scp.top.def_mut(wrap.binding().take(), tail);
|
||||
wrap.body().eval(ctx, scp)?.display()
|
||||
}
|
||||
|
||||
_ => node.eval(ctx, scp)?,
|
||||
});
|
||||
|
||||
if ctx.flow.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Content::sequence(seq))
|
||||
@ -98,7 +152,7 @@ fn eval_markup(
|
||||
impl Eval for MarkupNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
Ok(match self {
|
||||
Self::Space => Content::Space,
|
||||
Self::Parbreak => Content::Parbreak,
|
||||
@ -120,7 +174,7 @@ impl Eval for MarkupNode {
|
||||
impl Eval for StrongNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
Ok(Content::show(library::text::StrongNode(
|
||||
self.body().eval(ctx, scp)?,
|
||||
)))
|
||||
@ -130,7 +184,7 @@ impl Eval for StrongNode {
|
||||
impl Eval for EmphNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
Ok(Content::show(library::text::EmphNode(
|
||||
self.body().eval(ctx, scp)?,
|
||||
)))
|
||||
@ -140,7 +194,7 @@ impl Eval for EmphNode {
|
||||
impl Eval for RawNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let content = Content::show(library::text::RawNode {
|
||||
text: self.text.clone(),
|
||||
block: self.block,
|
||||
@ -155,7 +209,7 @@ impl Eval for RawNode {
|
||||
impl Eval for MathNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> TypResult<Self::Output> {
|
||||
Ok(Content::show(library::math::MathNode {
|
||||
formula: self.formula.clone(),
|
||||
display: self.display,
|
||||
@ -166,7 +220,7 @@ impl Eval for MathNode {
|
||||
impl Eval for HeadingNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
Ok(Content::show(library::structure::HeadingNode {
|
||||
body: self.body().eval(ctx, scp)?,
|
||||
level: self.level(),
|
||||
@ -177,7 +231,7 @@ impl Eval for HeadingNode {
|
||||
impl Eval for ListNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
Ok(Content::Item(library::structure::ListItem {
|
||||
kind: library::structure::UNORDERED,
|
||||
number: None,
|
||||
@ -189,7 +243,7 @@ impl Eval for ListNode {
|
||||
impl Eval for EnumNode {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
Ok(Content::Item(library::structure::ListItem {
|
||||
kind: library::structure::ORDERED,
|
||||
number: self.number(),
|
||||
@ -201,7 +255,14 @@ impl Eval for EnumNode {
|
||||
impl Eval for Expr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let forbidden = |name| {
|
||||
error!(
|
||||
self.span(),
|
||||
"{} is only allowed directly in code and content blocks", name
|
||||
)
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::Lit(v) => v.eval(ctx, scp),
|
||||
Self::Ident(v) => v.eval(ctx, scp),
|
||||
@ -217,11 +278,9 @@ impl Eval for Expr {
|
||||
Self::Unary(v) => v.eval(ctx, scp),
|
||||
Self::Binary(v) => v.eval(ctx, scp),
|
||||
Self::Let(v) => v.eval(ctx, scp),
|
||||
Self::Set(_) | Self::Show(_) | Self::Wrap(_) => {
|
||||
Err("set, show and wrap are only allowed directly in markup")
|
||||
.at(self.span())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Self::Set(_) => Err(forbidden("set")),
|
||||
Self::Show(_) => Err(forbidden("show")),
|
||||
Self::Wrap(_) => Err(forbidden("wrap")),
|
||||
Self::If(v) => v.eval(ctx, scp),
|
||||
Self::While(v) => v.eval(ctx, scp),
|
||||
Self::For(v) => v.eval(ctx, scp),
|
||||
@ -237,7 +296,7 @@ impl Eval for Expr {
|
||||
impl Eval for Lit {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> TypResult<Self::Output> {
|
||||
Ok(match self.kind() {
|
||||
LitKind::None => Value::None,
|
||||
LitKind::Auto => Value::Auto,
|
||||
@ -259,7 +318,7 @@ impl Eval for Lit {
|
||||
impl Eval for Ident {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, _: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, _: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
match scp.get(self) {
|
||||
Some(slot) => Ok(slot.read().clone()),
|
||||
None => bail!(self.span(), "unknown variable"),
|
||||
@ -270,23 +329,75 @@ impl Eval for Ident {
|
||||
impl Eval for CodeBlock {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
scp.enter();
|
||||
|
||||
let mut output = Value::None;
|
||||
for expr in self.exprs() {
|
||||
output = join_result(output, expr.eval(ctx, scp), expr.span())?;
|
||||
}
|
||||
|
||||
let output = eval_code(ctx, scp, &mut self.exprs())?;
|
||||
scp.exit();
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a stream of expressions.
|
||||
fn eval_code(
|
||||
ctx: &mut Context,
|
||||
scp: &mut Scopes,
|
||||
exprs: &mut impl Iterator<Item = Expr>,
|
||||
) -> TypResult<Value> {
|
||||
let mut output = Value::None;
|
||||
|
||||
while let Some(expr) = exprs.next() {
|
||||
let span = expr.span();
|
||||
let value = match expr {
|
||||
Expr::Set(set) => {
|
||||
let styles = set.eval(ctx, scp)?;
|
||||
if ctx.flow.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
let tail = to_content(eval_code(ctx, scp, exprs)?).at(span)?;
|
||||
Value::Content(tail.styled_with_map(styles))
|
||||
}
|
||||
Expr::Show(show) => {
|
||||
let recipe = show.eval(ctx, scp)?;
|
||||
let entry = StyleEntry::Recipe(recipe).into();
|
||||
if ctx.flow.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
let tail = to_content(eval_code(ctx, scp, exprs)?).at(span)?;
|
||||
Value::Content(tail.styled_with_entry(entry))
|
||||
}
|
||||
Expr::Wrap(wrap) => {
|
||||
let tail = to_content(eval_code(ctx, scp, exprs)?).at(span)?;
|
||||
scp.top.def_mut(wrap.binding().take(), Value::Content(tail));
|
||||
wrap.body().eval(ctx, scp)?
|
||||
}
|
||||
|
||||
_ => expr.eval(ctx, scp)?,
|
||||
};
|
||||
|
||||
output = ops::join(output, value).at(span)?;
|
||||
|
||||
if ctx.flow.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Extract content from a value.
|
||||
fn to_content(value: Value) -> StrResult<Content> {
|
||||
let ty = value.type_name();
|
||||
value
|
||||
.cast()
|
||||
.map_err(|_| format!("expected remaining block to yield content, found {ty}"))
|
||||
}
|
||||
|
||||
impl Eval for ContentBlock {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
scp.enter();
|
||||
let content = self.body().eval(ctx, scp)?;
|
||||
scp.exit();
|
||||
@ -297,7 +408,7 @@ impl Eval for ContentBlock {
|
||||
impl Eval for GroupExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
self.expr().eval(ctx, scp)
|
||||
}
|
||||
}
|
||||
@ -305,7 +416,7 @@ impl Eval for GroupExpr {
|
||||
impl Eval for ArrayExpr {
|
||||
type Output = Array;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let items = self.items();
|
||||
|
||||
let mut vec = Vec::with_capacity(items.size_hint().0);
|
||||
@ -327,7 +438,7 @@ impl Eval for ArrayExpr {
|
||||
impl Eval for DictExpr {
|
||||
type Output = Dict;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for item in self.items() {
|
||||
@ -357,7 +468,7 @@ impl Eval for DictExpr {
|
||||
impl Eval for UnaryExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let value = self.expr().eval(ctx, scp)?;
|
||||
let result = match self.op() {
|
||||
UnOp::Pos => ops::pos(value),
|
||||
@ -371,7 +482,7 @@ impl Eval for UnaryExpr {
|
||||
impl Eval for BinaryExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
match self.op() {
|
||||
BinOp::Add => self.apply(ctx, scp, ops::add),
|
||||
BinOp::Sub => self.apply(ctx, scp, ops::sub),
|
||||
@ -403,7 +514,7 @@ impl BinaryExpr {
|
||||
ctx: &mut Context,
|
||||
scp: &mut Scopes,
|
||||
op: fn(Value, Value) -> StrResult<Value>,
|
||||
) -> EvalResult<Value> {
|
||||
) -> TypResult<Value> {
|
||||
let lhs = self.lhs().eval(ctx, scp)?;
|
||||
|
||||
// Short-circuit boolean operations.
|
||||
@ -423,7 +534,7 @@ impl BinaryExpr {
|
||||
ctx: &mut Context,
|
||||
scp: &mut Scopes,
|
||||
op: fn(Value, Value) -> StrResult<Value>,
|
||||
) -> EvalResult<Value> {
|
||||
) -> TypResult<Value> {
|
||||
let rhs = self.rhs().eval(ctx, scp)?;
|
||||
let lhs = self.lhs();
|
||||
let mut location = lhs.access(ctx, scp)?;
|
||||
@ -436,7 +547,7 @@ impl BinaryExpr {
|
||||
impl Eval for FieldAccess {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let object = self.object().eval(ctx, scp)?;
|
||||
let span = self.field().span();
|
||||
let field = self.field().take();
|
||||
@ -462,7 +573,7 @@ impl Eval for FieldAccess {
|
||||
impl Eval for FuncCall {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let callee = self.callee().eval(ctx, scp)?;
|
||||
let args = self.args().eval(ctx, scp)?;
|
||||
|
||||
@ -486,7 +597,7 @@ impl Eval for FuncCall {
|
||||
impl Eval for MethodCall {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let span = self.span();
|
||||
let method = self.method();
|
||||
let point = || Tracepoint::Call(Some(method.to_string()));
|
||||
@ -507,7 +618,7 @@ impl Eval for MethodCall {
|
||||
impl Eval for CallArgs {
|
||||
type Output = Args;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for arg in self.items() {
|
||||
@ -559,7 +670,7 @@ impl Eval for CallArgs {
|
||||
impl Eval for ClosureExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
// The closure's name is defined by its let binding if there's one.
|
||||
let name = self.name().map(Ident::take);
|
||||
|
||||
@ -606,7 +717,7 @@ impl Eval for ClosureExpr {
|
||||
impl Eval for LetExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let value = match self.init() {
|
||||
Some(expr) => expr.eval(ctx, scp)?,
|
||||
None => Value::None,
|
||||
@ -619,7 +730,7 @@ impl Eval for LetExpr {
|
||||
impl Eval for SetExpr {
|
||||
type Output = StyleMap;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let target = self.target();
|
||||
let target = target.eval(ctx, scp)?.cast::<Func>().at(target.span())?;
|
||||
let args = self.args().eval(ctx, scp)?;
|
||||
@ -630,7 +741,7 @@ impl Eval for SetExpr {
|
||||
impl Eval for ShowExpr {
|
||||
type Output = Recipe;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
// Evaluate the target function.
|
||||
let pattern = self.pattern();
|
||||
let pattern = pattern.eval(ctx, scp)?.cast::<Pattern>().at(pattern.span())?;
|
||||
@ -666,7 +777,7 @@ impl Eval for ShowExpr {
|
||||
impl Eval for IfExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let condition = self.condition();
|
||||
if condition.eval(ctx, scp)?.cast::<bool>().at(condition.span())? {
|
||||
self.if_body().eval(ctx, scp)
|
||||
@ -681,19 +792,23 @@ impl Eval for IfExpr {
|
||||
impl Eval for WhileExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let mut output = Value::None;
|
||||
|
||||
let condition = self.condition();
|
||||
while condition.eval(ctx, scp)?.cast::<bool>().at(condition.span())? {
|
||||
let body = self.body();
|
||||
match join_result(output, body.eval(ctx, scp), body.span()) {
|
||||
Err(Control::Break(value, _)) => {
|
||||
output = value;
|
||||
let value = body.eval(ctx, scp)?;
|
||||
output = ops::join(output, value).at(body.span())?;
|
||||
|
||||
match ctx.flow {
|
||||
Some(Flow::Break(_)) => {
|
||||
ctx.flow = None;
|
||||
break;
|
||||
}
|
||||
Err(Control::Continue(value, _)) => output = value,
|
||||
other => output = other?,
|
||||
Some(Flow::Continue(_)) => ctx.flow = None,
|
||||
Some(Flow::Return(..)) => break,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -704,7 +819,7 @@ impl Eval for WhileExpr {
|
||||
impl Eval for ForExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
macro_rules! iter {
|
||||
(for ($($binding:ident => $value:ident),*) in $iter:expr) => {{
|
||||
let mut output = Value::None;
|
||||
@ -715,13 +830,17 @@ impl Eval for ForExpr {
|
||||
$(scp.top.def_mut(&$binding, $value);)*
|
||||
|
||||
let body = self.body();
|
||||
match join_result(output, body.eval(ctx, scp), body.span()) {
|
||||
Err(Control::Break(value, _)) => {
|
||||
output = value;
|
||||
let value = body.eval(ctx, scp)?;
|
||||
output = ops::join(output, value).at(body.span())?;
|
||||
|
||||
match ctx.flow {
|
||||
Some(Flow::Break(_)) => {
|
||||
ctx.flow = None;
|
||||
break;
|
||||
}
|
||||
Err(Control::Continue(value, _)) => output = value,
|
||||
other => output = other?,
|
||||
Some(Flow::Continue(_)) => ctx.flow = None,
|
||||
Some(Flow::Return(..)) => break,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -773,7 +892,7 @@ impl Eval for ForExpr {
|
||||
impl Eval for ImportExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let span = self.path().span();
|
||||
let path = self.path().eval(ctx, scp)?.cast::<EcoString>().at(span)?;
|
||||
let module = import(ctx, &path, span)?;
|
||||
@ -802,7 +921,7 @@ impl Eval for ImportExpr {
|
||||
impl Eval for IncludeExpr {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let span = self.path().span();
|
||||
let path = self.path().eval(ctx, scp)?.cast::<EcoString>().at(span)?;
|
||||
let module = import(ctx, &path, span)?;
|
||||
@ -833,30 +952,34 @@ fn import(ctx: &mut Context, path: &str, span: Span) -> TypResult<Module> {
|
||||
impl Eval for BreakExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
Err(Control::Break(Value::default(), self.span()))
|
||||
fn eval(&self, ctx: &mut Context, _: &mut Scopes) -> TypResult<Self::Output> {
|
||||
if ctx.flow.is_none() {
|
||||
ctx.flow = Some(Flow::Break(self.span()));
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for ContinueExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, _: &mut Context, _: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
Err(Control::Continue(Value::default(), self.span()))
|
||||
fn eval(&self, ctx: &mut Context, _: &mut Scopes) -> TypResult<Self::Output> {
|
||||
if ctx.flow.is_none() {
|
||||
ctx.flow = Some(Flow::Continue(self.span()));
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for ReturnExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult<Self::Output> {
|
||||
fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> TypResult<Self::Output> {
|
||||
let value = self.body().map(|body| body.eval(ctx, scp)).transpose()?;
|
||||
let explicit = value.is_some();
|
||||
Err(Control::Return(
|
||||
value.unwrap_or_default(),
|
||||
explicit,
|
||||
self.span(),
|
||||
))
|
||||
if ctx.flow.is_none() {
|
||||
ctx.flow = Some(Flow::Return(self.span(), value));
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
||||
|
||||
@ -867,7 +990,7 @@ pub trait Access {
|
||||
&self,
|
||||
ctx: &mut Context,
|
||||
scp: &'a mut Scopes,
|
||||
) -> EvalResult<Location<'a>>;
|
||||
) -> TypResult<Location<'a>>;
|
||||
}
|
||||
|
||||
impl Access for Expr {
|
||||
@ -875,7 +998,7 @@ impl Access for Expr {
|
||||
&self,
|
||||
ctx: &mut Context,
|
||||
scp: &'a mut Scopes,
|
||||
) -> EvalResult<Location<'a>> {
|
||||
) -> TypResult<Location<'a>> {
|
||||
match self {
|
||||
Expr::Ident(ident) => ident.access(ctx, scp),
|
||||
Expr::FuncCall(call) => call.access(ctx, scp),
|
||||
@ -889,7 +1012,7 @@ impl Access for Ident {
|
||||
&self,
|
||||
_: &mut Context,
|
||||
scp: &'a mut Scopes,
|
||||
) -> EvalResult<Location<'a>> {
|
||||
) -> TypResult<Location<'a>> {
|
||||
match scp.get(self) {
|
||||
Some(slot) => match slot.try_write() {
|
||||
Some(guard) => Ok(RwLockWriteGuard::map(guard, |v| v)),
|
||||
@ -905,7 +1028,7 @@ impl Access for FuncCall {
|
||||
&self,
|
||||
ctx: &mut Context,
|
||||
scp: &'a mut Scopes,
|
||||
) -> EvalResult<Location<'a>> {
|
||||
) -> TypResult<Location<'a>> {
|
||||
let args = self.args().eval(ctx, scp)?;
|
||||
let guard = self.callee().access(ctx, scp)?;
|
||||
try_map(guard, |value| {
|
||||
@ -928,9 +1051,9 @@ impl Access for FuncCall {
|
||||
type Location<'a> = MappedRwLockWriteGuard<'a, Value>;
|
||||
|
||||
/// Map a reader-writer lock with a function.
|
||||
fn try_map<F>(location: Location, f: F) -> EvalResult<Location>
|
||||
fn try_map<F>(location: Location, f: F) -> TypResult<Location>
|
||||
where
|
||||
F: FnOnce(&mut Value) -> EvalResult<&mut Value>,
|
||||
F: FnOnce(&mut Value) -> TypResult<&mut Value>,
|
||||
{
|
||||
let mut error = None;
|
||||
MappedRwLockWriteGuard::try_map(location, |value| match f(value) {
|
||||
|
@ -1,21 +0,0 @@
|
||||
use super::Scope;
|
||||
use crate::model::Content;
|
||||
use crate::source::{SourceId, SourceStore};
|
||||
|
||||
/// An evaluated module, ready for importing or layouting.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Module {
|
||||
/// The top-level definitions that were bound in this module.
|
||||
pub scope: Scope,
|
||||
/// The module's layoutable contents.
|
||||
pub content: Content,
|
||||
/// The source file revisions this module depends on.
|
||||
pub deps: Vec<(SourceId, usize)>,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
/// Whether the module is still valid for the given sources.
|
||||
pub fn valid(&self, sources: &SourceStore) -> bool {
|
||||
self.deps.iter().all(|&(id, rev)| rev == sources.get(id).rev())
|
||||
}
|
||||
}
|
@ -102,11 +102,6 @@ impl Value {
|
||||
T::cast(self)
|
||||
}
|
||||
|
||||
/// Join the value with another value.
|
||||
pub fn join(self, rhs: Self) -> StrResult<Self> {
|
||||
ops::join(self, rhs)
|
||||
}
|
||||
|
||||
/// Return the debug representation of the value.
|
||||
pub fn repr(&self) -> EcoString {
|
||||
format_eco!("{:?}", self)
|
||||
|
11
src/lib.rs
11
src/lib.rs
@ -58,7 +58,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::diag::TypResult;
|
||||
use crate::eval::{Eval, Module, Scope, Scopes};
|
||||
use crate::eval::{Eval, Flow, Module, Scope, Scopes};
|
||||
use crate::font::FontStore;
|
||||
use crate::frame::Frame;
|
||||
use crate::image::ImageStore;
|
||||
@ -88,6 +88,8 @@ pub struct Context {
|
||||
route: Vec<SourceId>,
|
||||
/// The dependencies of the current evaluation process.
|
||||
deps: Vec<(SourceId, usize)>,
|
||||
/// A control flow event that is currently happening.
|
||||
flow: Option<Flow>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@ -145,10 +147,16 @@ impl Context {
|
||||
let content = ast.eval(self, &mut scp);
|
||||
self.route.pop().unwrap();
|
||||
let deps = std::mem::replace(&mut self.deps, prev);
|
||||
let flow = self.flow.take();
|
||||
|
||||
// Assemble the module.
|
||||
let module = Module { scope: scp.top, content: content?, deps };
|
||||
|
||||
// Handle unhandled flow.
|
||||
if let Some(flow) = flow {
|
||||
return Err(flow.forbidden());
|
||||
}
|
||||
|
||||
// Save the evaluated module.
|
||||
self.modules.insert(id, module.clone());
|
||||
|
||||
@ -213,6 +221,7 @@ impl ContextBuilder {
|
||||
cache: HashMap::new(),
|
||||
route: vec![],
|
||||
deps: vec![],
|
||||
flow: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -246,7 +246,10 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) {
|
||||
| NodeKind::While
|
||||
| NodeKind::For
|
||||
| NodeKind::Import
|
||||
| NodeKind::Include => markup_expr(p),
|
||||
| NodeKind::Include
|
||||
| NodeKind::Break
|
||||
| NodeKind::Continue
|
||||
| NodeKind::Return => markup_expr(p),
|
||||
|
||||
// Code and content block.
|
||||
NodeKind::LeftBrace => code_block(p),
|
||||
@ -965,7 +968,7 @@ fn continue_expr(p: &mut Parser) -> ParseResult {
|
||||
fn return_expr(p: &mut Parser) -> ParseResult {
|
||||
p.perform(NodeKind::ReturnExpr, |p| {
|
||||
p.assert(NodeKind::Return);
|
||||
if !p.eof() {
|
||||
if !p.at(NodeKind::Comma) && !p.eof() {
|
||||
expr(p)?;
|
||||
}
|
||||
Ok(())
|
||||
|
BIN
tests/ref/code/break-continue.png
Normal file
BIN
tests/ref/code/break-continue.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 22 KiB |
@ -66,13 +66,28 @@
|
||||
|
||||
---
|
||||
// Test break outside of loop.
|
||||
|
||||
#let f() = {
|
||||
// Error: 3-8 cannot break outside of loop
|
||||
break
|
||||
}
|
||||
|
||||
#f()
|
||||
#for i in range(1) {
|
||||
f()
|
||||
}
|
||||
|
||||
---
|
||||
// Test break in function call.
|
||||
#let identity(x) = x
|
||||
#let out = for i in range(5) {
|
||||
"A"
|
||||
identity({
|
||||
"B"
|
||||
break
|
||||
})
|
||||
"C"
|
||||
}
|
||||
|
||||
#test(out, "AB")
|
||||
|
||||
---
|
||||
// Test continue outside of loop.
|
||||
@ -81,5 +96,41 @@
|
||||
#let x = { continue }
|
||||
|
||||
---
|
||||
// Error: 1-10 unexpected keyword `continue`
|
||||
// Error: 1-10 cannot continue outside of loop
|
||||
#continue
|
||||
|
||||
---
|
||||
// Ref: true
|
||||
// Should output `Hello World 🌎`.
|
||||
#for _ in range(10) {
|
||||
[Hello ]
|
||||
[World {
|
||||
[🌎]
|
||||
break
|
||||
}]
|
||||
}
|
||||
|
||||
---
|
||||
// Ref: true
|
||||
// Should output `Some` in red, `Some` in blue and `Last` in green.
|
||||
// Everything should be in smallcaps.
|
||||
#for color in (red, blue, green, yellow) [
|
||||
#set text("Roboto")
|
||||
#wrap body in text(fill: color, body)
|
||||
#smallcaps(if color != green [
|
||||
Some
|
||||
] else [
|
||||
Last
|
||||
#break
|
||||
])
|
||||
]
|
||||
|
||||
---
|
||||
// Ref: true
|
||||
// Test break in set rule.
|
||||
// Should output `Hi` in blue.
|
||||
#for i in range(10) {
|
||||
[Hello]
|
||||
set text(blue, ..break)
|
||||
[Not happening]
|
||||
}
|
||||
|
@ -35,10 +35,7 @@
|
||||
|
||||
#let f(text, caption: none) = {
|
||||
text
|
||||
if caption == none {
|
||||
[\.]
|
||||
return
|
||||
}
|
||||
if caption == none [\.#return]
|
||||
[, ]
|
||||
emph(caption)
|
||||
[\.]
|
||||
@ -55,3 +52,33 @@
|
||||
// Error: 3-9 cannot return outside of function
|
||||
return
|
||||
}
|
||||
|
||||
---
|
||||
// Test that the expression is evaluated to the end.
|
||||
#let y = 1
|
||||
#let identity(x, ..rest) = x
|
||||
#let f(x) = {
|
||||
identity(
|
||||
..return,
|
||||
x + 1,
|
||||
y = 2,
|
||||
)
|
||||
"nope"
|
||||
}
|
||||
|
||||
#test(f(1), 2)
|
||||
#test(y, 2)
|
||||
|
||||
---
|
||||
// Test value return from content.
|
||||
#let x = 3
|
||||
#let f() = [
|
||||
Hello 😀
|
||||
{ x = 1 }
|
||||
#return "nope"
|
||||
{ x = 2 }
|
||||
World
|
||||
]
|
||||
|
||||
#test(f(), "nope")
|
||||
#test(x, 1)
|
||||
|
@ -27,5 +27,15 @@ Hello *{x}*
|
||||
#text(fill: forest, x)
|
||||
|
||||
---
|
||||
// Error: 2-10 set, show and wrap are only allowed directly in markup
|
||||
{set f(x)}
|
||||
// Test that scoping works as expected.
|
||||
{
|
||||
if true {
|
||||
set text(blue)
|
||||
[Blue ]
|
||||
}
|
||||
[Not blue]
|
||||
}
|
||||
|
||||
---
|
||||
// Error: 11-25 set is only allowed directly in code and content blocks
|
||||
{ let x = set text(blue) }
|
||||
|
@ -48,6 +48,33 @@ Some more text.
|
||||
= Task 2
|
||||
Another text.
|
||||
|
||||
---
|
||||
// Test set and show in code blocks.
|
||||
#show node: heading as {
|
||||
set text(red)
|
||||
show "ding" as [🛎]
|
||||
node.body
|
||||
}
|
||||
|
||||
= Heading
|
||||
|
||||
---
|
||||
// Test that scoping works as expected.
|
||||
{
|
||||
let world = [ World ]
|
||||
show c: "W" as strong(c)
|
||||
world
|
||||
{
|
||||
set text(blue)
|
||||
wrap it in {
|
||||
show "o" as "Ø"
|
||||
it
|
||||
}
|
||||
world
|
||||
}
|
||||
world
|
||||
}
|
||||
|
||||
---
|
||||
// Error: 18-22 expected content, found integer
|
||||
#show heading as 1234
|
||||
@ -67,5 +94,5 @@ Another text.
|
||||
#show red as []
|
||||
|
||||
---
|
||||
// Error: 2-16 set, show and wrap are only allowed directly in markup
|
||||
{show list as a}
|
||||
// Error: 7-27 show is only allowed directly in code and content blocks
|
||||
{ 1 + show heading as none }
|
||||
|
@ -25,5 +25,12 @@ A [_B #wrap c in [*#c*]; C_] D
|
||||
Forest
|
||||
|
||||
---
|
||||
// Error: 6-17 set, show and wrap are only allowed directly in markup
|
||||
{1 + wrap x in y}
|
||||
{
|
||||
// Error: 3-24 expected remaining block to yield content, found integer
|
||||
wrap body in 2 * body
|
||||
2
|
||||
}
|
||||
|
||||
---
|
||||
// Error: 4-18 wrap is only allowed directly in code and content blocks
|
||||
{ (wrap body in 2) * body }
|
||||
|
Loading…
x
Reference in New Issue
Block a user