typst/src/model/recipe.rs
2022-05-13 13:52:52 +02:00

144 lines
4.1 KiB
Rust

use std::fmt::{self, Debug, Formatter};
use super::{Content, Interruption, NodeId, Show, ShowNode, StyleChain, StyleEntry};
use crate::diag::{At, TypResult};
use crate::eval::{Args, Func, Regex, Value};
use crate::library::structure::{EnumNode, ListNode};
use crate::syntax::Span;
use crate::Context;
/// A show rule recipe.
#[derive(Clone, PartialEq, Hash)]
pub struct Recipe {
/// The patterns to customize.
pub pattern: Pattern,
/// The function that defines the recipe.
pub func: Func,
/// The span to report all erros with.
pub span: Span,
}
impl Recipe {
/// Whether the recipe is applicable to the target.
pub fn applicable(&self, target: Target) -> bool {
match (&self.pattern, target) {
(Pattern::Node(id), Target::Node(node)) => *id == node.id(),
(Pattern::Regex(_), Target::Text(_)) => true,
_ => false,
}
}
/// Try to apply the recipe to the target.
pub fn apply(
&self,
ctx: &mut Context,
styles: StyleChain,
sel: Selector,
target: Target,
) -> TypResult<Option<Content>> {
let content = match (target, &self.pattern) {
(Target::Node(node), &Pattern::Node(id)) if node.id() == id => {
let node = node.unguard(sel);
self.call(ctx, || {
let dict = node.encode(styles);
Value::Content(Content::Show(node, Some(dict)))
})?
}
(Target::Text(text), Pattern::Regex(regex)) => {
let mut result = vec![];
let mut cursor = 0;
for mat in regex.find_iter(text) {
let start = mat.start();
if cursor < start {
result.push(Content::Text(text[cursor .. start].into()));
}
result.push(self.call(ctx, || Value::Str(mat.as_str().into()))?);
cursor = mat.end();
}
if result.is_empty() {
return Ok(None);
}
if cursor < text.len() {
result.push(Content::Text(text[cursor ..].into()));
}
Content::sequence(result)
}
_ => return Ok(None),
};
Ok(Some(content.styled_with_entry(StyleEntry::Guard(sel))))
}
/// Call the recipe function, with the argument if desired.
fn call<F>(&self, ctx: &mut Context, arg: F) -> TypResult<Content>
where
F: FnOnce() -> Value,
{
let args = if self.func.argc() == Some(0) {
Args::new(self.span)
} else {
Args::from_values(self.span, [arg()])
};
self.func.call(ctx, args)?.cast().at(self.span)
}
/// What kind of structure the property interrupts.
pub fn interruption(&self) -> Option<Interruption> {
if let Pattern::Node(id) = self.pattern {
if id == NodeId::of::<ListNode>() || id == NodeId::of::<EnumNode>() {
return Some(Interruption::List);
}
}
None
}
}
impl Debug for Recipe {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Recipe matching {:?} from {:?}", self.pattern, self.span)
}
}
/// A show rule pattern that may match a target.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Pattern {
/// Defines the appearence of some node.
Node(NodeId),
/// Defines text to be replaced.
Regex(Regex),
}
impl Pattern {
/// Define a simple text replacement pattern.
pub fn text(text: &str) -> Self {
Self::Regex(Regex::new(&regex::escape(text)).unwrap())
}
}
/// A target for a show rule recipe.
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Target<'a> {
/// A showable node.
Node(&'a ShowNode),
/// A slice of text.
Text(&'a str),
}
/// Identifies a show rule recipe.
#[derive(Debug, Copy, Clone, PartialEq, Hash)]
pub enum Selector {
/// The nth recipe from the top of the chain.
Nth(usize),
/// The base recipe for a kind of node.
Base(NodeId),
}