476 lines
12 KiB
Rust
476 lines
12 KiB
Rust
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, Deref};
|
|
|
|
use comemo::Tracked;
|
|
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, cast_to_value, Args, Cast, Func, FuncInfo, Str, Value, Vm,
|
|
};
|
|
use crate::syntax::Span;
|
|
use crate::util::pretty_array_like;
|
|
use crate::World;
|
|
|
|
/// Composable representation of styled content.
|
|
#[derive(Clone, Hash)]
|
|
pub struct Content {
|
|
id: NodeId,
|
|
span: Span,
|
|
fields: EcoVec<(EcoString, Value)>,
|
|
modifiers: EcoVec<Modifier>,
|
|
}
|
|
|
|
/// Modifiers that can be attached to content.
|
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
enum Modifier {
|
|
Synthesized,
|
|
Guard(Guard),
|
|
}
|
|
|
|
impl Content {
|
|
pub fn new<T: Node>() -> Self {
|
|
Self {
|
|
id: T::id(),
|
|
span: Span::detached(),
|
|
fields: EcoVec::new(),
|
|
modifiers: EcoVec::new(),
|
|
}
|
|
}
|
|
|
|
/// Create empty content.
|
|
pub fn empty() -> Self {
|
|
SequenceNode::new(vec![]).pack()
|
|
}
|
|
|
|
/// Create a new sequence node from multiples nodes.
|
|
pub fn sequence(seq: Vec<Self>) -> Self {
|
|
match seq.as_slice() {
|
|
[_] => seq.into_iter().next().unwrap(),
|
|
_ => SequenceNode::new(seq).pack(),
|
|
}
|
|
}
|
|
|
|
/// The id of the contained node.
|
|
pub fn id(&self) -> NodeId {
|
|
self.id
|
|
}
|
|
|
|
/// Whether the content is empty.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.to::<SequenceNode>()
|
|
.map_or(false, |seq| seq.children().is_empty())
|
|
}
|
|
|
|
/// Whether the contained node is of type `T`.
|
|
pub fn is<T>(&self) -> bool
|
|
where
|
|
T: Node + 'static,
|
|
{
|
|
self.id == NodeId::of::<T>()
|
|
}
|
|
|
|
/// 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) })
|
|
}
|
|
|
|
/// 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()
|
|
}
|
|
|
|
/// 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) })
|
|
}
|
|
|
|
/// The node's span.
|
|
pub fn span(&self) -> Span {
|
|
self.span
|
|
}
|
|
|
|
/// Attach a span to the content.
|
|
pub fn spanned(mut self, span: Span) -> Self {
|
|
self.span = span;
|
|
self
|
|
}
|
|
|
|
/// Access a field on the content.
|
|
pub fn field(&self, name: &str) -> Option<&Value> {
|
|
self.fields
|
|
.iter()
|
|
.find(|(field, _)| field == name)
|
|
.map(|(_, value)| value)
|
|
}
|
|
|
|
/// Access a field on the content as a specified type.
|
|
#[track_caller]
|
|
pub fn cast_field<T: Cast>(&self, name: &str) -> Option<T> {
|
|
match self.field(name) {
|
|
Some(value) => Some(value.clone().cast().unwrap()),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
/// Expect a field on the content to exist as a specified type.
|
|
#[track_caller]
|
|
pub fn expect_field<T: Cast>(&self, name: &str) -> T {
|
|
self.cast_field(name).unwrap()
|
|
}
|
|
|
|
/// List all fields on the content.
|
|
pub fn fields(&self) -> &[(EcoString, Value)] {
|
|
&self.fields
|
|
}
|
|
|
|
/// Attach a field to the content.
|
|
pub fn with_field(
|
|
mut self,
|
|
name: impl Into<EcoString>,
|
|
value: impl Into<Value>,
|
|
) -> Self {
|
|
self.push_field(name, value);
|
|
self
|
|
}
|
|
|
|
/// Attach a field to the content.
|
|
pub fn push_field(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) {
|
|
let name = name.into();
|
|
if let Some(i) = self.fields.iter().position(|(field, _)| *field == name) {
|
|
self.fields.make_mut()[i] = (name, value.into());
|
|
} else {
|
|
self.fields.push((name, value.into()));
|
|
}
|
|
}
|
|
|
|
/// 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))
|
|
}
|
|
|
|
/// The content's label.
|
|
pub fn label(&self) -> Option<&Label> {
|
|
match self.field("label")? {
|
|
Value::Label(label) => Some(label),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Attach a label to the content.
|
|
pub fn labelled(self, label: Label) -> Self {
|
|
self.with_field("label", label)
|
|
}
|
|
|
|
/// Style this content with a style entry.
|
|
pub fn styled(self, style: impl Into<Style>) -> Self {
|
|
self.styled_with_map(style.into().into())
|
|
}
|
|
|
|
/// 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()
|
|
}
|
|
}
|
|
|
|
/// 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)))
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
pub fn guarded(mut self, id: Guard) -> Self {
|
|
self.modifiers.push(Modifier::Guard(id));
|
|
self
|
|
}
|
|
|
|
/// Mark this content as prepared.
|
|
pub fn synthesized(mut self) -> Self {
|
|
self.modifiers.push(Modifier::Synthesized);
|
|
self
|
|
}
|
|
|
|
/// Whether this node was prepared.
|
|
pub fn is_synthesized(&self) -> bool {
|
|
self.modifiers.contains(&Modifier::Synthesized)
|
|
}
|
|
|
|
/// Whether no show rule was executed for this node so far.
|
|
pub(super) fn is_pristine(&self) -> bool {
|
|
!self
|
|
.modifiers
|
|
.iter()
|
|
.any(|modifier| matches!(modifier, Modifier::Guard(_)))
|
|
}
|
|
|
|
/// Check whether a show rule recipe is disabled.
|
|
pub(super) fn is_guarded(&self, id: Guard) -> bool {
|
|
self.modifiers.contains(&Modifier::Guard(id))
|
|
}
|
|
|
|
/// Copy the modifiers from another piece of content.
|
|
pub(super) fn copy_modifiers(&mut self, from: &Content) {
|
|
self.span = from.span;
|
|
self.modifiers = from.modifiers.clone();
|
|
if let Some(label) = from.label() {
|
|
self.push_field("label", label.clone())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Debug for Content {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
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("[ ]");
|
|
}
|
|
|
|
let pieces: Vec<_> = self
|
|
.fields
|
|
.iter()
|
|
.map(|(name, value)| eco_format!("{name}: {value:?}"))
|
|
.collect();
|
|
|
|
f.write_str(name)?;
|
|
f.write_str(&pretty_array_like(&pieces, false))
|
|
}
|
|
}
|
|
|
|
impl Default for Content {
|
|
fn default() -> Self {
|
|
Self::empty()
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
fn add(self, rhs: Self) -> Self::Output {
|
|
let lhs = self;
|
|
let seq = match (lhs.to::<SequenceNode>(), rhs.to::<SequenceNode>()) {
|
|
(Some(lhs), Some(rhs)) => {
|
|
lhs.children().into_iter().chain(rhs.children()).collect()
|
|
}
|
|
(Some(lhs), None) => {
|
|
lhs.children().into_iter().chain(iter::once(rhs)).collect()
|
|
}
|
|
(None, Some(rhs)) => iter::once(lhs).chain(rhs.children()).collect(),
|
|
(None, None) => vec![lhs, rhs],
|
|
};
|
|
SequenceNode::new(seq).pack()
|
|
}
|
|
}
|
|
|
|
impl AddAssign for Content {
|
|
fn add_assign(&mut self, rhs: Self) {
|
|
*self = std::mem::take(self) + rhs;
|
|
}
|
|
}
|
|
|
|
impl Sum for Content {
|
|
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
|
|
Self::sequence(iter.collect())
|
|
}
|
|
}
|
|
|
|
/// A constructable, stylable content node.
|
|
pub trait Node: Construct + Set + Sized + 'static {
|
|
/// The node's ID.
|
|
fn id() -> NodeId;
|
|
|
|
/// Pack a node into type-erased content.
|
|
fn pack(self) -> Content;
|
|
}
|
|
|
|
/// A unique identifier for a node.
|
|
#[derive(Copy, Clone)]
|
|
pub struct NodeId(pub &'static NodeMeta);
|
|
|
|
impl NodeId {
|
|
/// Get the id of a node.
|
|
pub fn of<T: Node>() -> Self {
|
|
T::id()
|
|
}
|
|
}
|
|
|
|
impl Debug for NodeId {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
f.pad(self.name)
|
|
}
|
|
}
|
|
|
|
impl Hash for NodeId {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
state.write_usize(self.0 as *const _ as usize);
|
|
}
|
|
}
|
|
|
|
impl Eq for NodeId {}
|
|
|
|
impl PartialEq for NodeId {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
std::ptr::eq(self.0, other.0)
|
|
}
|
|
}
|
|
|
|
impl Deref for NodeId {
|
|
type Target = NodeMeta;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
cast_from_value! {
|
|
NodeId,
|
|
v: Func => v.id().ok_or("this function is not an element")?
|
|
}
|
|
|
|
cast_to_value! {
|
|
v: NodeId => Value::Func(v.into())
|
|
}
|
|
|
|
/// 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.
|
|
pub trait Construct {
|
|
/// Construct a node from the arguments.
|
|
///
|
|
/// This is passed only the arguments that remain after execution of the
|
|
/// node's set rule.
|
|
fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content>;
|
|
}
|
|
|
|
/// A node's set rule.
|
|
pub trait Set {
|
|
/// Parse relevant arguments into style properties for this node.
|
|
fn set(args: &mut Args) -> SourceResult<StyleMap>;
|
|
}
|
|
|
|
/// 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 missing_field(key: &str) -> EcoString {
|
|
eco_format!("content does not contain field {:?}", Str::from(key))
|
|
}
|