summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-03-10 20:47:23 +0100
committerLaurenz <laurmaedje@gmail.com>2023-03-10 21:19:50 +0100
commita9fdff244aef859449a76e5f762ee7c343a8ddcc (patch)
tree172b543183296b4bc30b3008650f594688467914 /src
parent62f35602a87574dcc607f1637aeae1be574981ff (diff)
Expose content representation more
Diffstat (limited to 'src')
-rw-r--r--src/eval/args.rs16
-rw-r--r--src/eval/array.rs49
-rw-r--r--src/eval/dict.rs45
-rw-r--r--src/eval/func.rs56
-rw-r--r--src/eval/methods.rs12
-rw-r--r--src/eval/mod.rs16
-rw-r--r--src/eval/ops.rs1
-rw-r--r--src/eval/value.rs7
-rw-r--r--src/ide/tooltip.rs40
-rw-r--r--src/model/content.rs392
-rw-r--r--src/model/realize.rs20
-rw-r--r--src/model/styles.rs71
-rw-r--r--src/util/mod.rs76
13 files changed, 416 insertions, 385 deletions
diff --git a/src/eval/args.rs b/src/eval/args.rs
index 159e9a77..17bd9cb4 100644
--- a/src/eval/args.rs
+++ b/src/eval/args.rs
@@ -1,10 +1,11 @@
-use std::fmt::{self, Debug, Formatter, Write};
+use std::fmt::{self, Debug, Formatter};
-use ecow::EcoVec;
+use ecow::{eco_format, EcoVec};
use super::{Array, Cast, Dict, Str, Value};
use crate::diag::{bail, At, SourceResult};
use crate::syntax::{Span, Spanned};
+use crate::util::pretty_array;
/// Evaluated arguments to a function.
#[derive(Clone, PartialEq, Hash)]
@@ -171,14 +172,9 @@ impl Args {
impl Debug for Args {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_char('(')?;
- for (i, arg) in self.items.iter().enumerate() {
- arg.fmt(f)?;
- if i + 1 < self.items.len() {
- f.write_str(", ")?;
- }
- }
- f.write_char(')')
+ let pieces: Vec<_> =
+ self.items.iter().map(|arg| eco_format!("{arg:?}")).collect();
+ f.write_str(&pretty_array(&pieces, false))
}
}
diff --git a/src/eval/array.rs b/src/eval/array.rs
index 53bae06f..8da9b3d2 100644
--- a/src/eval/array.rs
+++ b/src/eval/array.rs
@@ -1,11 +1,12 @@
use std::cmp::Ordering;
-use std::fmt::{self, Debug, Formatter, Write};
+use std::fmt::{self, Debug, Formatter};
use std::ops::{Add, AddAssign};
use ecow::{eco_format, EcoString, EcoVec};
use super::{ops, Args, Func, Value, Vm};
use crate::diag::{bail, At, SourceResult, StrResult};
+use crate::util::pretty_array;
/// Create a new [`Array`] from values.
#[macro_export]
@@ -147,7 +148,6 @@ impl Array {
return Ok(Some(item.clone()));
}
}
-
Ok(None)
}
@@ -262,6 +262,14 @@ impl Array {
self.0.iter().cloned().rev().collect()
}
+ /// Split all values in the array.
+ pub fn split(&self, at: Value) -> Array {
+ self.as_slice()
+ .split(|value| *value == at)
+ .map(|subslice| Value::Array(subslice.iter().cloned().collect()))
+ .collect()
+ }
+
/// Join all values in the array, optionally with separator and last
/// separator (between the final two items).
pub fn join(&self, sep: Option<Value>, mut last: Option<Value>) -> StrResult<Value> {
@@ -332,31 +340,10 @@ impl Array {
}
}
-/// The out of bounds access error message.
-#[cold]
-fn out_of_bounds(index: i64, len: i64) -> EcoString {
- eco_format!("array index out of bounds (index: {}, len: {})", index, len)
-}
-
-/// The error message when the array is empty.
-#[cold]
-fn array_is_empty() -> EcoString {
- "array is empty".into()
-}
-
impl Debug for Array {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_char('(')?;
- for (i, value) in self.iter().enumerate() {
- value.fmt(f)?;
- if i + 1 < self.0.len() {
- f.write_str(", ")?;
- }
- }
- if self.len() == 1 {
- f.write_char(',')?;
- }
- f.write_char(')')
+ let pieces: Vec<_> = self.iter().map(|value| eco_format!("{value:?}")).collect();
+ f.write_str(&pretty_array(&pieces, self.len() == 1))
}
}
@@ -404,3 +391,15 @@ impl<'a> IntoIterator for &'a Array {
self.iter()
}
}
+
+/// The error message when the array is empty.
+#[cold]
+fn array_is_empty() -> EcoString {
+ "array is empty".into()
+}
+
+/// The out of bounds access error message.
+#[cold]
+fn out_of_bounds(index: i64, len: i64) -> EcoString {
+ eco_format!("array index out of bounds (index: {}, len: {})", index, len)
+}
diff --git a/src/eval/dict.rs b/src/eval/dict.rs
index 6c1934c9..ececce07 100644
--- a/src/eval/dict.rs
+++ b/src/eval/dict.rs
@@ -1,5 +1,5 @@
use std::collections::BTreeMap;
-use std::fmt::{self, Debug, Formatter, Write};
+use std::fmt::{self, Debug, Formatter};
use std::ops::{Add, AddAssign};
use std::sync::Arc;
@@ -8,7 +8,7 @@ use ecow::{eco_format, EcoString};
use super::{array, Array, Str, Value};
use crate::diag::StrResult;
use crate::syntax::is_ident;
-use crate::util::ArcExt;
+use crate::util::{pretty_array, ArcExt};
/// Create a new [`Dict`] from key-value pairs.
#[macro_export]
@@ -132,31 +132,24 @@ impl Dict {
}
}
-/// The missing key access error message.
-#[cold]
-fn missing_key(key: &str) -> EcoString {
- eco_format!("dictionary does not contain key {:?}", Str::from(key))
-}
-
impl Debug for Dict {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_char('(')?;
if self.is_empty() {
- f.write_char(':')?;
+ return f.write_str("(:)");
}
- for (i, (key, value)) in self.iter().enumerate() {
- if is_ident(key) {
- f.write_str(key)?;
- } else {
- write!(f, "{key:?}")?;
- }
- f.write_str(": ")?;
- value.fmt(f)?;
- if i + 1 < self.0.len() {
- f.write_str(", ")?;
- }
- }
- f.write_char(')')
+
+ let pieces: Vec<_> = self
+ .iter()
+ .map(|(key, value)| {
+ if is_ident(key) {
+ eco_format!("{key}: {value:?}")
+ } else {
+ eco_format!("{key:?}: {value:?}")
+ }
+ })
+ .collect();
+
+ f.write_str(&pretty_array(&pieces, false))
}
}
@@ -207,3 +200,9 @@ impl<'a> IntoIterator for &'a Dict {
self.iter()
}
}
+
+/// The missing key access error message.
+#[cold]
+fn missing_key(key: &str) -> EcoString {
+ eco_format!("dictionary does not contain key {:?}", Str::from(key))
+}
diff --git a/src/eval/func.rs b/src/eval/func.rs
index 79ae142c..8da5c6bc 100644
--- a/src/eval/func.rs
+++ b/src/eval/func.rs
@@ -13,7 +13,7 @@ use super::{
Vm,
};
use crate::diag::{bail, SourceResult, StrResult};
-use crate::model::{Content, NodeId, Selector, StyleMap};
+use crate::model::{NodeId, Selector, StyleMap};
use crate::syntax::ast::{self, AstNode, Expr};
use crate::syntax::{SourceId, Span, SyntaxNode};
use crate::util::hash128;
@@ -29,7 +29,7 @@ enum Repr {
/// A native Rust function.
Native(NativeFunc),
/// A function for a node.
- Node(NodeFunc),
+ Node(NodeId),
/// A user-defined closure.
Closure(Closure),
/// A nested function with pre-applied arguments.
@@ -156,13 +156,13 @@ impl Func {
/// Create a selector for this function's node type.
pub fn select(&self, fields: Option<Dict>) -> StrResult<Selector> {
- match &**self.0 {
- Repr::Node(node) => {
- if node.id == item!(text_id) {
+ match **self.0 {
+ Repr::Node(id) => {
+ if id == item!(text_id) {
Err("to select text, please use a string or regex instead")?;
}
- Ok(Selector::Node(node.id, fields))
+ Ok(Selector::Node(id, fields))
}
_ => Err("this function is not selectable")?,
}
@@ -172,8 +172,8 @@ impl Func {
impl Debug for Func {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self.name() {
- Some(name) => write!(f, "<function {name}>"),
- None => f.write_str("<function>"),
+ Some(name) => write!(f, "{name}"),
+ None => f.write_str("(..) => .."),
}
}
}
@@ -190,6 +190,16 @@ impl From<Repr> for Func {
}
}
+impl From<NodeId> for Func {
+ fn from(id: NodeId) -> Self {
+ Repr::Node(id).into()
+ }
+}
+
+cast_to_value! {
+ v: NodeId => Value::Func(v.into())
+}
+
/// A native Rust function.
pub struct NativeFunc {
/// The function's implementation.
@@ -223,36 +233,6 @@ where
}
}
-/// A function defined by a native Rust node.
-pub struct NodeFunc {
- /// The node's id.
- pub id: NodeId,
- /// The node's constructor.
- pub construct: fn(&Vm, &mut Args) -> SourceResult<Content>,
- /// The node's set rule.
- pub set: fn(&mut Args) -> SourceResult<StyleMap>,
- /// Details about the function.
- pub info: Lazy<FuncInfo>,
-}
-
-impl Hash for NodeFunc {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.id.hash(state);
- (self.construct as usize).hash(state);
- (self.set as usize).hash(state);
- }
-}
-
-impl From<NodeFunc> for Func {
- fn from(node: NodeFunc) -> Self {
- Repr::Node(node).into()
- }
-}
-
-cast_to_value! {
- v: NodeFunc => Value::Func(v.into())
-}
-
/// Details about a function.
#[derive(Debug, Clone)]
pub struct FuncInfo {
diff --git a/src/eval/methods.rs b/src/eval/methods.rs
index dcb1ca31..197a2f65 100644
--- a/src/eval/methods.rs
+++ b/src/eval/methods.rs
@@ -69,6 +69,13 @@ pub fn call(
_ => return missing(),
},
+ Value::Content(content) => match method {
+ "func" => Value::Func(content.id().into()),
+ "has" => Value::Bool(content.has(&args.expect::<EcoString>("field")?)),
+ "at" => content.at(&args.expect::<EcoString>("field")?).at(span)?.clone(),
+ _ => return missing(),
+ },
+
Value::Array(array) => match method {
"len" => Value::Int(array.len()),
"first" => array.first().at(span)?.clone(),
@@ -96,6 +103,7 @@ pub fn call(
"all" => Value::Bool(array.all(vm, args.expect("function")?)?),
"flatten" => Value::Array(array.flatten()),
"rev" => Value::Array(array.rev()),
+ "split" => Value::Array(array.split(args.expect("separator")?)),
"join" => {
let sep = args.eat()?;
let last = args.named("last")?;
@@ -107,7 +115,7 @@ pub fn call(
Value::Dict(dict) => match method {
"len" => Value::Int(dict.len()),
- "at" => dict.at(&args.expect::<Str>("key")?).cloned().at(span)?,
+ "at" => dict.at(&args.expect::<Str>("key")?).at(span)?.clone(),
"keys" => Value::Array(dict.keys()),
"values" => Value::Array(dict.values()),
"pairs" => Value::Array(dict.pairs()),
@@ -237,6 +245,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
("starts-with", true),
("trim", true),
],
+ "content" => &[("func", false), ("has", true), ("at", true)],
"array" => &[
("all", true),
("any", true),
@@ -248,6 +257,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
("flatten", false),
("fold", true),
("insert", true),
+ ("split", true),
("join", true),
("last", false),
("len", false),
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index fcfda263..145f961a 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -47,6 +47,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{
bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint,
};
+use crate::model::Unlabellable;
use crate::model::{Content, Label, Recipe, Selector, StyleMap, Transform};
use crate::syntax::ast::AstNode;
use crate::syntax::{
@@ -357,12 +358,12 @@ fn eval_markup(
}
let tail = eval_markup(vm, exprs)?;
- seq.push(tail.apply_recipe(vm.world, recipe)?)
+ seq.push(tail.styled_with_recipe(vm.world, recipe)?)
}
expr => match expr.eval(vm)? {
Value::Label(label) => {
if let Some(node) =
- seq.iter_mut().rev().find(|node| node.labellable())
+ seq.iter_mut().rev().find(|node| !node.can::<dyn Unlabellable>())
{
*node = mem::take(node).labelled(label);
}
@@ -786,7 +787,7 @@ fn eval_code(
}
let tail = eval_code(vm, exprs)?.display();
- Value::Content(tail.apply_recipe(vm.world, recipe)?)
+ Value::Content(tail.styled_with_recipe(vm.world, recipe)?)
}
_ => expr.eval(vm)?,
};
@@ -979,6 +980,10 @@ impl Eval for ast::FuncCall {
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let span = self.span();
+ if vm.depth >= MAX_CALL_DEPTH {
+ bail!(span, "maximum function call depth exceeded");
+ }
+
let callee = self.callee();
let in_math = in_math(&callee);
let callee_span = callee.span();
@@ -1042,11 +1047,6 @@ impl Eval for ast::FuncCall {
));
}
- // Finally, just a normal function call!
- if vm.depth >= MAX_CALL_DEPTH {
- bail!(span, "maximum function call depth exceeded");
- }
-
let callee = callee.cast::<Func>().at(callee_span)?;
let point = || Tracepoint::Call(callee.name().map(Into::into));
callee.call(vm, args).trace(vm.world, point, span)
diff --git a/src/eval/ops.rs b/src/eval/ops.rs
index 52b9b06a..5efb68c3 100644
--- a/src/eval/ops.rs
+++ b/src/eval/ops.rs
@@ -340,6 +340,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Symbol(a), Symbol(b)) => a == b,
(Str(a), Str(b)) => a == b,
(Label(a), Label(b)) => a == b,
+ (Content(a), Content(b)) => a == b,
(Array(a), Array(b)) => a == b,
(Dict(a), Dict(b)) => a == b,
(Func(a), Func(b)) => a == b,
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 9b9bc314..61af36f5 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -120,10 +120,7 @@ impl Value {
match self {
Self::Symbol(symbol) => symbol.clone().modified(&field).map(Self::Symbol),
Self::Dict(dict) => dict.at(&field).cloned(),
- Self::Content(content) => content
- .field(&field)
- .cloned()
- .ok_or_else(|| eco_format!("unknown field `{field}`")),
+ Self::Content(content) => content.at(&field).cloned(),
Self::Module(module) => module.get(&field).cloned(),
v => Err(eco_format!("cannot access fields on type {}", v.type_name())),
}
@@ -190,7 +187,7 @@ impl Debug for Value {
Self::Symbol(v) => Debug::fmt(v, f),
Self::Str(v) => Debug::fmt(v, f),
Self::Label(v) => Debug::fmt(v, f),
- Self::Content(_) => f.pad("[...]"),
+ Self::Content(v) => Debug::fmt(v, f),
Self::Array(v) => Debug::fmt(v, f),
Self::Dict(v) => Debug::fmt(v, f),
Self::Func(v) => Debug::fmt(v, f),
diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs
index ac0cb60b..4f281050 100644
--- a/src/ide/tooltip.rs
+++ b/src/ide/tooltip.rs
@@ -1,11 +1,15 @@
+use std::fmt::Write;
+
+use ecow::EcoString;
+
use if_chain::if_chain;
-use unicode_segmentation::UnicodeSegmentation;
use super::{analyze_expr, plain_docs_sentence, summarize_font_family};
use crate::eval::{CastInfo, Tracer, Value};
use crate::geom::{round_2, Length, Numeric};
use crate::syntax::ast;
use crate::syntax::{LinkedNode, Source, SyntaxKind};
+use crate::util::pretty_comma_list;
use crate::World;
/// Describe the item under the cursor.
@@ -60,31 +64,27 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option<Tool
return None;
}
- let mut tooltip = String::new();
- let mut iter = values.into_iter().enumerate();
- for (i, value) in (&mut iter).take(Tracer::MAX - 1) {
- if i > 0 && !tooltip.is_empty() {
- tooltip.push_str(", ");
- }
- let repr = value.repr();
- let repr = repr.as_str();
- let len = repr.len();
- if len <= 40 {
- tooltip.push_str(repr);
- } else {
- let mut graphemes = repr.graphemes(true);
- let r = graphemes.next_back().map_or(0, str::len);
- let l = graphemes.take(40).map(str::len).sum();
- tooltip.push_str(&repr[..l]);
- tooltip.push_str("...");
- tooltip.push_str(&repr[len - r..]);
+ let mut last = None;
+ let mut pieces: Vec<EcoString> = vec![];
+ let mut iter = values.iter();
+ for value in (&mut iter).take(Tracer::MAX - 1) {
+ if let Some((prev, count)) = &mut last {
+ if *prev == value {
+ *count += 1;
+ continue;
+ } else if *count > 1 {
+ write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
+ }
}
+ pieces.push(value.repr().into());
+ last = Some((value, 1));
}
if iter.next().is_some() {
- tooltip.push_str(", ...");
+ pieces.push("...".into());
}
+ let tooltip = pretty_comma_list(&pieces, false);
(!tooltip.is_empty()).then(|| Tooltip::Code(tooltip))
}
diff --git a/src/model/content.rs b/src/model/content.rs
index d845ce1e..b895553c 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -2,22 +2,24 @@ use std::any::TypeId;
use std::fmt::{self, Debug, Formatter, Write};
use std::hash::{Hash, Hasher};
use std::iter::{self, Sum};
-use std::ops::{Add, AddAssign};
+use std::ops::{Add, AddAssign, Deref};
use comemo::Tracked;
-use ecow::{EcoString, EcoVec};
+use ecow::{eco_format, EcoString, EcoVec};
+use once_cell::sync::Lazy;
use super::{node, Guard, Recipe, Style, StyleMap};
use crate::diag::{SourceResult, StrResult};
-use crate::eval::{cast_from_value, Args, Cast, NodeFunc, Value, Vm};
+use crate::eval::{cast_from_value, Args, FuncInfo, Str, Value, Vm};
use crate::syntax::Span;
+use crate::util::pretty_array;
use crate::World;
/// Composable representation of styled content.
#[derive(Clone, Hash)]
pub struct Content {
id: NodeId,
- span: Option<Span>,
+ span: Span,
fields: EcoVec<(EcoString, Value)>,
modifiers: EcoVec<Modifier>,
}
@@ -33,7 +35,7 @@ impl Content {
pub fn new<T: Node>() -> Self {
Self {
id: T::id(),
- span: None,
+ span: Span::detached(),
fields: EcoVec::new(),
modifiers: EcoVec::new(),
}
@@ -52,82 +54,67 @@ impl Content {
}
}
- /// Attach a span to the content.
- pub fn spanned(mut self, span: Span) -> Self {
- if let Some(styled) = self.to::<StyledNode>() {
- self = StyledNode::new(styled.map(), styled.body().spanned(span)).pack();
- }
- self.span = Some(span);
- self
- }
-
- /// Attach a label to the content.
- pub fn labelled(self, label: Label) -> Self {
- self.with_field("label", label)
+ /// The id of the contained node.
+ pub fn id(&self) -> NodeId {
+ self.id
}
- /// Style this content with a style entry.
- pub fn styled(self, style: impl Into<Style>) -> Self {
- self.styled_with_map(style.into().into())
+ /// Whether the contained node is of type `T`.
+ pub fn is<T>(&self) -> bool
+ where
+ T: Node + 'static,
+ {
+ self.id == NodeId::of::<T>()
}
- /// Style this content with a full style map.
- pub fn styled_with_map(self, styles: StyleMap) -> Self {
- if styles.is_empty() {
- self
- } else if let Some(styled) = self.to::<StyledNode>() {
- let mut map = styled.map();
- map.apply(styles);
- StyledNode::new(map, styled.body()).pack()
- } else {
- StyledNode::new(styles, self).pack()
- }
+ /// Cast to `T` if the contained node is of type `T`.
+ pub fn to<T>(&self) -> Option<&T>
+ where
+ T: Node + 'static,
+ {
+ self.is::<T>().then(|| unsafe { std::mem::transmute(self) })
}
- /// Style this content with a recipe, eagerly applying it if possible.
- pub fn apply_recipe(
- self,
- world: Tracked<dyn World>,
- recipe: Recipe,
- ) -> SourceResult<Self> {
- if recipe.selector.is_none() {
- recipe.apply(world, self)
- } else {
- Ok(self.styled(Style::Recipe(recipe)))
- }
+ /// Whether this content has the given capability.
+ pub fn can<C>(&self) -> bool
+ where
+ C: ?Sized + 'static,
+ {
+ (self.id.0.vtable)(TypeId::of::<C>()).is_some()
}
- /// Repeat this content `n` times.
- pub fn repeat(&self, n: i64) -> StrResult<Self> {
- let count = usize::try_from(n)
- .map_err(|_| format!("cannot repeat this content {} times", n))?;
-
- Ok(Self::sequence(vec![self.clone(); count]))
+ /// Cast to a trait object if this content has the given capability.
+ pub fn with<C>(&self) -> Option<&C>
+ where
+ C: ?Sized + 'static,
+ {
+ let vtable = (self.id.0.vtable)(TypeId::of::<C>())?;
+ let data = self as *const Self as *const ();
+ Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) })
}
-}
-impl Content {
- /// The id of the contained node.
- pub fn id(&self) -> NodeId {
- self.id
+ /// The node's span.
+ pub fn span(&self) -> Span {
+ self.span
}
- /// The node's human-readable name.
- pub fn name(&self) -> &'static str {
- self.id.name()
+ /// Attach a span to the content.
+ pub fn spanned(mut self, span: Span) -> Self {
+ self.span = span;
+ self
}
- /// The node's span.
- pub fn span(&self) -> Option<Span> {
- self.span
+ /// Access a field on the content.
+ pub fn field(&self, name: &str) -> Option<&Value> {
+ self.fields
+ .iter()
+ .find(|(field, _)| field == name)
+ .map(|(_, value)| value)
}
- /// The content's label.
- pub fn label(&self) -> Option<&Label> {
- match self.field("label")? {
- Value::Label(label) => Some(label),
- _ => None,
- }
+ /// List all fields on the content.
+ pub fn fields(&self) -> &[(EcoString, Value)] {
+ &self.fields
}
/// Attach a field to the content.
@@ -150,88 +137,88 @@ impl Content {
}
}
- /// Access a field on the content.
- pub fn field(&self, name: &str) -> Option<&Value> {
- static NONE: Value = Value::None;
- self.fields
- .iter()
- .find(|(field, _)| field == name)
- .map(|(_, value)| value)
- .or_else(|| (name == "label").then(|| &NONE))
+ /// Whether the content has the specified field.
+ pub fn has(&self, field: &str) -> bool {
+ self.field(field).is_some()
+ }
+
+ /// Borrow the value of the given field.
+ pub fn at(&self, field: &str) -> StrResult<&Value> {
+ self.field(field).ok_or_else(|| missing_field(field))
}
- #[track_caller]
- pub fn cast_required_field<T: Cast>(&self, name: &str) -> T {
- match self.field(name) {
- Some(value) => value.clone().cast().unwrap(),
- None => field_is_missing(name),
+ /// The content's label.
+ pub fn label(&self) -> Option<&Label> {
+ match self.field("label")? {
+ Value::Label(label) => Some(label),
+ _ => None,
}
}
- /// List all fields on the content.
- pub fn fields(&self) -> &[(EcoString, Value)] {
- &self.fields
+ /// Attach a label to the content.
+ pub fn labelled(self, label: Label) -> Self {
+ self.with_field("label", label)
}
- /// Whether the contained node is of type `T`.
- pub fn is<T>(&self) -> bool
- where
- T: Node + 'static,
- {
- self.id == NodeId::of::<T>()
+ /// Style this content with a style entry.
+ pub fn styled(self, style: impl Into<Style>) -> Self {
+ self.styled_with_map(style.into().into())
}
- /// Cast to `T` if the contained node is of type `T`.
- pub fn to<T>(&self) -> Option<&T>
- where
- T: Node + 'static,
- {
- self.is::<T>().then(|| unsafe { std::mem::transmute(self) })
+ /// Style this content with a full style map.
+ pub fn styled_with_map(self, styles: StyleMap) -> Self {
+ if styles.is_empty() {
+ self
+ } else if let Some(styled) = self.to::<StyledNode>() {
+ let mut map = styled.styles();
+ map.apply(styles);
+ StyledNode::new(map, styled.body()).pack()
+ } else {
+ StyledNode::new(styles, self).pack()
+ }
}
- /// Whether this content has the given capability.
- pub fn has<C>(&self) -> bool
- where
- C: ?Sized + 'static,
- {
- (self.id.0.vtable)(TypeId::of::<C>()).is_some()
+ /// Style this content with a recipe, eagerly applying it if possible.
+ pub fn styled_with_recipe(
+ self,
+ world: Tracked<dyn World>,
+ recipe: Recipe,
+ ) -> SourceResult<Self> {
+ if recipe.selector.is_none() {
+ recipe.apply(world, self)
+ } else {
+ Ok(self.styled(Style::Recipe(recipe)))
+ }
}
- /// Cast to a trait object if this content has the given capability.
- pub fn with<C>(&self) -> Option<&C>
- where
- C: ?Sized + 'static,
- {
- let vtable = (self.id.0.vtable)(TypeId::of::<C>())?;
- let data = self as *const Self as *const ();
- Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) })
+ /// Repeat this content `n` times.
+ pub fn repeat(&self, n: i64) -> StrResult<Self> {
+ let count = usize::try_from(n)
+ .map_err(|_| format!("cannot repeat this content {} times", n))?;
+
+ Ok(Self::sequence(vec![self.clone(); count]))
}
+}
+#[doc(hidden)]
+impl Content {
/// Disable a show rule recipe.
- #[doc(hidden)]
pub fn guarded(mut self, id: Guard) -> Self {
self.modifiers.push(Modifier::Guard(id));
self
}
/// Mark this content as prepared.
- #[doc(hidden)]
pub fn prepared(mut self) -> Self {
self.modifiers.push(Modifier::Prepared);
self
}
/// Whether this node was prepared.
- #[doc(hidden)]
pub fn is_prepared(&self) -> bool {
self.modifiers.contains(&Modifier::Prepared)
}
- /// Whether a label can be attached to the content.
- pub(crate) fn labellable(&self) -> bool {
- !self.has::<dyn Unlabellable>()
- }
-
/// Whether no show rule was executed for this node so far.
pub(super) fn is_pristine(&self) -> bool {
!self
@@ -257,32 +244,24 @@ impl Content {
impl Debug for Content {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- struct Pad<'a>(&'a str);
- impl Debug for Pad<'_> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad(self.0)
- }
+ let name = self.id.name;
+ if let Some(text) = item!(text_str)(self) {
+ f.write_char('[')?;
+ f.write_str(&text)?;
+ f.write_char(']')?;
+ return Ok(());
+ } else if name == "space" {
+ return f.write_str("[ ]");
}
- if let Some(styled) = self.to::<StyledNode>() {
- styled.map().fmt(f)?;
- styled.body().fmt(f)
- } else if let Some(seq) = self.to::<SequenceNode>() {
- f.debug_list().entries(&seq.children()).finish()
- } else if self.id.name() == "space" {
- ' '.fmt(f)
- } else if self.id.name() == "text" {
- self.field("text").unwrap().fmt(f)
- } else {
- f.write_str(self.name())?;
- if self.fields.is_empty() {
- return Ok(());
- }
- f.write_char(' ')?;
- f.debug_map()
- .entries(self.fields.iter().map(|(name, value)| (Pad(name), value)))
- .finish()
- }
+ let pieces: Vec<_> = self
+ .fields
+ .iter()
+ .map(|(name, value)| eco_format!("{name}: {value:?}"))
+ .collect();
+
+ f.write_str(name)?;
+ f.write_str(&pretty_array(&pieces, false))
}
}
@@ -292,6 +271,17 @@ impl Default for Content {
}
}
+impl PartialEq for Content {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ && self.fields.len() == other.fields.len()
+ && self
+ .fields
+ .iter()
+ .all(|(name, value)| other.field(name) == Some(value))
+ }
+}
+
impl Add for Content {
type Output = Self;
@@ -323,48 +313,6 @@ impl Sum for Content {
}
}
-/// A node with applied styles.
-///
-/// Display: Styled
-/// Category: special
-#[node]
-pub struct StyledNode {
- /// The styles.
- #[required]
- pub map: StyleMap,
-
- /// The styled content.
- #[required]
- pub body: Content,
-}
-
-cast_from_value! {
- StyleMap: "style map",
-}
-
-/// A sequence of nodes.
-///
-/// Combines other arbitrary content. So, when you write `[Hi] + [you]` in
-/// Typst, the two text nodes are combined into a single sequence node.
-///
-/// Display: Sequence
-/// Category: special
-#[node]
-pub struct SequenceNode {
- #[variadic]
- pub children: Vec<Content>,
-}
-
-/// A label for a node.
-#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub struct Label(pub EcoString);
-
-impl Debug for Label {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "<{}>", self.0)
- }
-}
-
/// A constructable, stylable content node.
pub trait Node: Construct + Set + Sized + 'static {
/// The node's ID.
@@ -372,33 +320,22 @@ pub trait Node: Construct + Set + Sized + 'static {
/// Pack a node into type-erased content.
fn pack(self) -> Content;
-
- /// The node's function.
- fn func() -> NodeFunc;
}
/// A unique identifier for a node.
#[derive(Copy, Clone)]
-pub struct NodeId(&'static NodeMeta);
+pub struct NodeId(pub &'static NodeMeta);
impl NodeId {
+ /// Get the id of a node.
pub fn of<T: Node>() -> Self {
T::id()
}
-
- pub fn from_meta(meta: &'static NodeMeta) -> Self {
- Self(meta)
- }
-
- /// The name of the identified node.
- pub fn name(self) -> &'static str {
- self.0.name
- }
}
impl Debug for NodeId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad(self.name())
+ f.pad(self.name)
}
}
@@ -416,10 +353,26 @@ impl PartialEq for NodeId {
}
}
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+impl Deref for NodeId {
+ type Target = NodeMeta;
+
+ fn deref(&self) -> &Self::Target {
+ self.0
+ }
+}
+
+/// Static node for a node.
pub struct NodeMeta {
+ /// The node's name.
pub name: &'static str,
+ /// The node's vtable for caspability dispatch.
pub vtable: fn(of: TypeId) -> Option<*const ()>,
+ /// The node's constructor.
+ pub construct: fn(&Vm, &mut Args) -> SourceResult<Content>,
+ /// The node's set rule.
+ pub set: fn(&mut Args) -> SourceResult<StyleMap>,
+ /// Details about the function.
+ pub info: Lazy<FuncInfo>,
}
/// A node's constructor function.
@@ -440,8 +393,51 @@ pub trait Set {
/// Indicates that a node cannot be labelled.
pub trait Unlabellable {}
+/// A label for a node.
+#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Label(pub EcoString);
+
+impl Debug for Label {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "<{}>", self.0)
+ }
+}
+
+/// A sequence of nodes.
+///
+/// Combines other arbitrary content. So, when you write `[Hi] + [you]` in
+/// Typst, the two text nodes are combined into a single sequence node.
+///
+/// Display: Sequence
+/// Category: special
+#[node]
+pub struct SequenceNode {
+ #[variadic]
+ pub children: Vec<Content>,
+}
+
+/// A node with applied styles.
+///
+/// Display: Styled
+/// Category: special
+#[node]
+pub struct StyledNode {
+ /// The styles.
+ #[required]
+ pub styles: StyleMap,
+
+ /// The styled content.
+ #[required]
+ pub body: Content,
+}
+
+cast_from_value! {
+ StyleMap: "style map",
+}
+
+/// The missing key access error message.
#[cold]
#[track_caller]
-fn field_is_missing(name: &str) -> ! {
- panic!("required field `{name}` is missing")
+fn missing_field(key: &str) -> EcoString {
+ eco_format!("content does not contain field {:?}", Str::from(key))
}
diff --git a/src/model/realize.rs b/src/model/realize.rs
index 502774bb..c4c67a4f 100644
--- a/src/model/realize.rs
+++ b/src/model/realize.rs
@@ -3,11 +3,11 @@ use crate::diag::SourceResult;
/// Whether the target is affected by show rules in the given style chain.
pub fn applicable(target: &Content, styles: StyleChain) -> bool {
- if target.has::<dyn Prepare>() && !target.is_prepared() {
+ if target.can::<dyn Prepare>() && !target.is_prepared() {
return true;
}
- if target.has::<dyn Show>() && target.is_pristine() {
+ if target.can::<dyn Show>() && target.is_pristine() {
return true;
}
@@ -51,7 +51,7 @@ pub fn realize(
if let Some(showable) = target.with::<dyn Show>() {
let guard = Guard::Base(target.id());
if realized.is_none() && !target.is_guarded(guard) {
- realized = Some(showable.show(vt, target, styles)?);
+ realized = Some(showable.show(vt, styles)?);
}
}
@@ -135,23 +135,13 @@ fn try_apply(
/// Preparations before execution of any show rule.
pub trait Prepare {
/// Prepare the node for show rule application.
- fn prepare(
- &self,
- vt: &mut Vt,
- this: Content,
- styles: StyleChain,
- ) -> SourceResult<Content>;
+ fn prepare(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>;
}
/// The base recipe for a node.
pub trait Show {
/// Execute the base recipe for this node.
- fn show(
- &self,
- vt: &mut Vt,
- this: &Content,
- styles: StyleChain,
- ) -> SourceResult<Content>;
+ fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>;
}
/// Post-process a node after it was realized.
diff --git a/src/model/styles.rs b/src/model/styles.rs
index 3239bb17..b803bc34 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -1,13 +1,14 @@
-use std::fmt::{self, Debug, Formatter};
+use std::fmt::{self, Debug, Formatter, Write};
use std::iter;
use comemo::Tracked;
-use ecow::EcoString;
+use ecow::{eco_format, EcoString};
use super::{Content, Label, Node, NodeId};
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value};
use crate::syntax::Span;
+use crate::util::pretty_array;
use crate::World;
/// A map of style properties.
@@ -79,10 +80,13 @@ impl PartialEq for StyleMap {
impl Debug for StyleMap {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- for entry in self.0.iter() {
- writeln!(f, "{:?}", entry)?;
+ if let [style] = self.0.as_slice() {
+ return style.fmt(f);
}
- Ok(())
+
+ let pieces: Vec<_> =
+ self.0.iter().map(|value| eco_format!("{value:?}")).collect();
+ f.write_str(&pretty_array(&pieces, false))
}
}
@@ -160,7 +164,7 @@ impl Property {
impl Debug for Property {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "#set {}({}: {:?})", self.node.name(), self.name, self.value)?;
+ write!(f, "set {}({}: {:?})", self.node.name, self.name, self.value)?;
Ok(())
}
}
@@ -203,12 +207,10 @@ impl Recipe {
Transform::Func(func) => {
let args = Args::new(self.span, [Value::Content(content.clone())]);
let mut result = func.call_detached(world, args);
- if let Some(span) = content.span() {
- // For selector-less show rules, a tracepoint makes no sense.
- if self.selector.is_some() {
- let point = || Tracepoint::Show(content.name().into());
- result = result.trace(world, point, span);
- }
+ // For selector-less show rules, a tracepoint makes no sense.
+ if self.selector.is_some() {
+ let point = || Tracepoint::Show(content.id().name.into());
+ result = result.trace(world, point, content.span());
}
Ok(result?.display())
}
@@ -219,12 +221,18 @@ impl Recipe {
impl Debug for Recipe {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "#show {:?}: {:?}", self.selector, self.transform)
+ f.write_str("show")?;
+ if let Some(selector) = &self.selector {
+ f.write_char(' ')?;
+ selector.fmt(f)?;
+ }
+ f.write_str(": ")?;
+ self.transform.fmt(f)
}
}
/// A selector in a show rule.
-#[derive(Debug, Clone, PartialEq, Hash)]
+#[derive(Clone, PartialEq, Hash)]
pub enum Selector {
/// Matches a specific type of node.
///
@@ -267,6 +275,23 @@ impl Selector {
}
}
+impl Debug for Selector {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Node(node, dict) => {
+ f.write_str(node.name)?;
+ if let Some(dict) = dict {
+ f.write_str(".where")?;
+ dict.fmt(f)?;
+ }
+ Ok(())
+ }
+ Self::Label(label) => label.fmt(f),
+ Self::Regex(regex) => regex.fmt(f),
+ }
+ }
+}
+
cast_from_value! {
Selector: "selector",
text: EcoString => Self::text(&text),
@@ -276,7 +301,7 @@ cast_from_value! {
}
/// A show rule transformation that can be applied to a match.
-#[derive(Debug, Clone, Hash)]
+#[derive(Clone, Hash)]
pub enum Transform {
/// Replacement content.
Content(Content),
@@ -286,6 +311,16 @@ pub enum Transform {
Style(StyleMap),
}
+impl Debug for Transform {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Content(content) => content.fmt(f),
+ Self::Func(func) => func.fmt(f),
+ Self::Style(styles) => styles.fmt(f),
+ }
+ }
+}
+
cast_from_value! {
Transform,
content: Content => Self::Content(content),
@@ -434,9 +469,9 @@ impl<'a> StyleChain<'a> {
.map(|property| property.value.clone()),
)
.map(move |value| {
- value.cast().unwrap_or_else(|err| {
- panic!("{} (for {}.{})", err, node.name(), name)
- })
+ value
+ .cast()
+ .unwrap_or_else(|err| panic!("{} (for {}.{})", err, node.name, name))
})
}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 54b9fe27..a3fad8ca 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -6,12 +6,12 @@ mod buffer;
pub use buffer::Buffer;
-use std::any::TypeId;
use std::fmt::{self, Debug, Formatter};
use std::hash::Hash;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
+use ecow::EcoString;
use siphasher::sip128::{Hasher128, SipHasher};
/// Turn a closure into a struct implementing [`Debug`].
@@ -133,35 +133,63 @@ impl PathExt for Path {
}
}
-/// An alternative type id that prints as something readable in debug mode.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct ReadableTypeId {
- id: TypeId,
- #[cfg(debug_assertions)]
- name: &'static str,
+/// Format something as a a comma-separated list that support horizontal
+/// formatting but falls back to vertical formatting if the pieces are too long.
+pub fn pretty_array(pieces: &[EcoString], trailing_comma: bool) -> String {
+ let list = pretty_comma_list(&pieces, trailing_comma);
+ let mut buf = String::new();
+ buf.push('(');
+ if list.contains('\n') {
+ buf.push('\n');
+ buf.push_str(&indent(&list, 2));
+ buf.push('\n');
+ } else {
+ buf.push_str(&list);
+ }
+ buf.push(')');
+ buf
}
-impl ReadableTypeId {
- /// The type id of the given type.
- pub fn of<T: 'static>() -> Self {
- Self {
- id: TypeId::of::<T>(),
- #[cfg(debug_assertions)]
- name: std::any::type_name::<T>(),
+/// Format something as a a comma-separated list that support horizontal
+/// formatting but falls back to vertical formatting if the pieces are too long.
+pub fn pretty_comma_list(pieces: &[EcoString], trailing_comma: bool) -> String {
+ const MAX_WIDTH: usize = 50;
+
+ let mut buf = String::new();
+ let len = pieces.iter().map(|s| s.len()).sum::<usize>()
+ + 2 * pieces.len().saturating_sub(1);
+
+ if len <= MAX_WIDTH {
+ for (i, piece) in pieces.iter().enumerate() {
+ if i > 0 {
+ buf.push_str(", ");
+ }
+ buf.push_str(piece);
+ }
+ if trailing_comma {
+ buf.push(',');
+ }
+ } else {
+ for piece in pieces {
+ buf.push_str(piece.trim());
+ buf.push_str(",\n");
}
}
+
+ buf
}
-impl Debug for ReadableTypeId {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- #[cfg(debug_assertions)]
- if let Some(part) = self.name.split("::").last() {
- f.pad(part)?;
+/// Indent a string by two spaces.
+pub fn indent(text: &str, amount: usize) -> String {
+ let mut buf = String::new();
+ for (i, line) in text.lines().enumerate() {
+ if i > 0 {
+ buf.push('\n');
}
-
- #[cfg(not(debug_assertions))]
- f.pad("ReadableTypeId")?;
-
- Ok(())
+ for _ in 0..amount {
+ buf.push(' ');
+ }
+ buf.push_str(line);
}
+ buf
}