diff --git a/src/lib.rs b/src/lib.rs index eb6e8f722..06324c115 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ use std::any::Any; use std::collections::HashMap; use std::fmt::{self, Display, Formatter}; use std::hash::Hash; +use std::mem; use std::path::PathBuf; use std::sync::Arc; @@ -141,25 +142,26 @@ impl Context { let source = self.sources.get(id); let ast = source.ast()?; - let std = self.std.clone(); - let mut scp = Scopes::new(Some(&std)); + // Save the old context. + let prev_flow = self.flow.take(); + let prev_deps = mem::replace(&mut self.deps, vec![(id, source.rev())]); + self.route.push(id); // Evaluate the module. - let prev = std::mem::replace(&mut self.deps, vec![(id, source.rev())]); - self.route.push(id); - let content = ast.eval(self, &mut scp); + let std = self.std.clone(); + let mut scp = Scopes::new(Some(&std)); + let result = ast.eval(self, &mut scp); + + // Restore the old context and handle control flow. 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 { + let deps = mem::replace(&mut self.deps, prev_deps); + if let Some(flow) = mem::replace(&mut self.flow, prev_flow) { return Err(flow.forbidden()); } + // Assemble the module. + let module = Module { scope: scp.top, content: result?, deps }; + // Save the evaluated module. self.modules.insert(id, module.clone()); diff --git a/src/library/mod.rs b/src/library/mod.rs index 6a30badf8..ac0cbb92b 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -73,6 +73,7 @@ pub fn new() -> Scope { // Utility. std.def_fn("type", utility::type_); std.def_fn("assert", utility::assert); + std.def_fn("eval", utility::eval); std.def_fn("int", utility::int); std.def_fn("float", utility::float); std.def_fn("abs", utility::abs); diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index 13220242b..355315e4c 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -8,7 +8,11 @@ pub use color::*; pub use math::*; pub use string::*; +use std::mem; + +use crate::eval::{Eval, Scopes}; use crate::library::prelude::*; +use crate::source::SourceFile; /// The name of a value's type. pub fn type_(_: &mut Context, args: &mut Args) -> TypResult { @@ -23,3 +27,30 @@ pub fn assert(_: &mut Context, args: &mut Args) -> TypResult { } Ok(Value::None) } + +/// Evaluate a string as Typst markup. +pub fn eval(ctx: &mut Context, args: &mut Args) -> TypResult { + let Spanned { v: src, span } = args.expect::>("source")?; + + // Parse the source and set a synthetic span for all nodes. + let mut source = SourceFile::detached(src); + source.synthesize(span); + let ast = source.ast()?; + + // Save the old context, then detach it. + let prev_flow = ctx.flow.take(); + let prev_route = mem::take(&mut ctx.route); + + // Evaluate the source. + let std = ctx.std.clone(); + let mut scp = Scopes::new(Some(&std)); + let result = ast.eval(ctx, &mut scp); + + // Restore the old context and handle control flow. + ctx.route = prev_route; + if let Some(flow) = mem::replace(&mut ctx.flow, prev_flow) { + return Err(flow.forbidden()); + } + + Ok(Value::Content(result?)) +} diff --git a/src/model/property.rs b/src/model/property.rs index b35ffba98..ff6869708 100644 --- a/src/model/property.rs +++ b/src/model/property.rs @@ -131,13 +131,13 @@ impl Debug for KeyId { pub trait Key<'a>: Copy + 'static { /// The unfolded type which this property is stored as in a style map. For /// example, this is [`Toggle`](crate::geom::Length) for the - /// [`STRONG`](TextNode::STRONG) property. + /// [`STRONG`](crate::library::text::TextNode::STRONG) property. type Value: Debug + Clone + Hash + Sync + Send + 'static; /// The folded type of value that is returned when reading this property /// from a style chain. For example, this is [`bool`] for the - /// [`STRONG`](TextNode::STRONG) property. For non-copy, non-folding - /// properties this is a reference type. + /// [`STRONG`](crate::library::text::TextNode::STRONG) property. For + /// non-copy, non-folding properties this is a reference type. type Output; /// The name of the property, used for debug printing. @@ -274,8 +274,8 @@ impl Fold for Sides>>> { /// A scoped property barrier. /// -/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style -/// can still be read through a single barrier (the one of the node it +/// Barriers interact with [scoped](super::StyleMap::scoped) styles: A scoped +/// style can still be read through a single barrier (the one of the node it /// _should_ apply to), but a second barrier will make it invisible. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Barrier(NodeId); diff --git a/src/model/styles.rs b/src/model/styles.rs index 928133c74..5c36861ac 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -103,7 +103,8 @@ impl StyleMap { /// Mark all contained properties as _scoped_. This means that they only /// apply to the first descendant node (of their type) in the hierarchy and - /// not its children, too. This is used by [constructors](Node::construct). + /// not its children, too. This is used by + /// [constructors](crate::eval::Node::construct). pub fn scoped(mut self) -> Self { for entry in &mut self.0 { if let StyleEntry::Property(property) = entry { diff --git a/src/source.rs b/src/source.rs index 780e12a8b..deeaef4bf 100644 --- a/src/source.rs +++ b/src/source.rs @@ -12,7 +12,7 @@ use crate::diag::TypResult; use crate::loading::{FileHash, Loader}; use crate::parse::{is_newline, parse, reparse}; use crate::syntax::ast::Markup; -use crate::syntax::{self, Category, GreenNode, RedNode}; +use crate::syntax::{self, Category, GreenNode, RedNode, Span}; use crate::util::{PathExt, StrExt}; #[cfg(feature = "codespan-reporting")] @@ -23,6 +23,11 @@ use codespan_reporting::files::{self, Files}; pub struct SourceId(u32); impl SourceId { + /// Create a new source id for a file that is not part of a store. + pub const fn detached() -> Self { + Self(u32::MAX) + } + /// Create a source id from the raw underlying value. /// /// This should only be called with values returned by @@ -165,7 +170,12 @@ impl SourceFile { /// Create a source file without a real id and path, usually for testing. pub fn detached(src: impl Into) -> Self { - Self::new(SourceId(0), Path::new(""), src.into()) + Self::new(SourceId::detached(), Path::new(""), src.into()) + } + + /// Set a synthetic span for all nodes in this file. + pub fn synthesize(&mut self, span: Span) { + Arc::make_mut(&mut self.root).synthesize(Arc::new(span)); } /// The root node of the file's untyped green tree. diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index abe541b8c..29e2718b4 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -81,6 +81,14 @@ impl Green { Self::Token(data) => data.kind = kind, } } + + /// Set a synthetic span for the node and all its children. + pub fn synthesize(&mut self, span: Arc) { + match self { + Green::Node(n) => Arc::make_mut(n).synthesize(span), + Green::Token(t) => t.synthesize(span), + } + } } impl Default for Green { @@ -151,6 +159,14 @@ impl GreenNode { self.data().len() } + /// Set a synthetic span for the node and all its children. + pub fn synthesize(&mut self, span: Arc) { + self.data.synthesize(span.clone()); + for child in &mut self.children { + child.synthesize(span.clone()); + } + } + /// The node's children, mutably. pub(crate) fn children_mut(&mut self) -> &mut [Green] { &mut self.children @@ -214,12 +230,14 @@ pub struct GreenData { kind: NodeKind, /// The byte length of the node in the source. len: usize, + /// A synthetic span for the node, usually this is `None`. + span: Option>, } impl GreenData { /// Create new node metadata. pub fn new(kind: NodeKind, len: usize) -> Self { - Self { len, kind } + Self { len, kind, span: None } } /// The type of the node. @@ -231,6 +249,11 @@ impl GreenData { pub fn len(&self) -> usize { self.len } + + /// Set a synthetic span for the node. + pub fn synthesize(&mut self, span: Arc) { + self.span = Some(span) + } } impl From for Green { @@ -270,6 +293,11 @@ impl RedNode { } } + /// The node's metadata. + pub fn data(&self) -> &GreenData { + self.as_ref().data() + } + /// The type of the node. pub fn kind(&self) -> &NodeKind { self.as_ref().kind() @@ -340,6 +368,11 @@ impl<'a> RedRef<'a> { } } + /// The node's metadata. + pub fn data(self) -> &'a GreenData { + self.green.data() + } + /// The type of the node. pub fn kind(self) -> &'a NodeKind { self.green.kind() @@ -352,7 +385,10 @@ impl<'a> RedRef<'a> { /// The span of the node. pub fn span(self) -> Span { - Span::new(self.id, self.offset, self.offset + self.green.len()) + match self.data().span.as_deref() { + Some(&span) => span, + None => Span::new(self.id, self.offset, self.offset + self.len()), + } } /// Whether the node is a leaf node. @@ -368,11 +404,14 @@ impl<'a> RedRef<'a> { match self.kind() { NodeKind::Error(pos, msg) => { - let span = match pos { - ErrorPos::Start => self.span().at_start(), - ErrorPos::Full => self.span(), - ErrorPos::End => self.span().at_end(), - }; + let mut span = self.span(); + if self.data().span.is_none() { + span = match pos { + ErrorPos::Start => span.at_start(), + ErrorPos::Full => span, + ErrorPos::End => span.at_end(), + }; + } vec![Error::new(span, msg.to_string())] } diff --git a/tests/ref/utility/eval.png b/tests/ref/utility/eval.png new file mode 100644 index 000000000..38c1d64eb Binary files /dev/null and b/tests/ref/utility/eval.png differ diff --git a/tests/typ/utility/eval.typ b/tests/typ/utility/eval.typ new file mode 100644 index 000000000..86b1f0c47 --- /dev/null +++ b/tests/typ/utility/eval.typ @@ -0,0 +1,52 @@ +// Test the `eval` function. + +--- +#eval("_Hello" + " World!_") + +--- +// Error: 7-13 expected identifier +#eval("#let") + +--- +#set raw(around: none) +#show it: raw as text("IBM Plex Sans", eval(it.text)) + +Interacting +``` +#set text(blue) +Blue #move(dy: -0.15em)[🌊] +``` + +--- +// Error: 7-19 cannot continue outside of loop +#eval("{continue}") + +--- +// Error: 7-33 cannot access file system from here +#eval("#include \"../coma.typ\"") + +--- +// Error: 7-35 cannot access file system from here +#eval("#image(\"/res/tiger.jpg\")") + +--- +// Error: 23-30 cannot access file system from here +#show it: raw as eval(it.text) + +``` +#show strong as image("/res/tiger.jpg") +*No absolute tiger!* +``` + +--- +// Error: 23-30 cannot access file system from here +#show it: raw as eval(it.text) + +``` +#show emph as image("../../res/giraffe.jpg") +_No relative giraffe!_ +``` + +--- +// Error: 7-16 expected comma +#eval("{(1 2)}")