summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/methods.rs16
-rw-r--r--src/eval/str.rs46
-rw-r--r--src/eval/value.rs10
-rw-r--r--src/library/mod.rs1
-rw-r--r--src/library/utility/blind.rs9
-rw-r--r--src/library/utility/mod.rs2
-rw-r--r--src/library/utility/string.rs15
-rw-r--r--src/model/content.rs9
-rw-r--r--src/model/recipe.rs39
9 files changed, 131 insertions, 16 deletions
diff --git a/src/eval/methods.rs b/src/eval/methods.rs
index b3674dff..363474fb 100644
--- a/src/eval/methods.rs
+++ b/src/eval/methods.rs
@@ -1,8 +1,9 @@
//! Methods on values.
-use super::{Args, StrExt, Value};
+use super::{Args, Regex, StrExt, Value};
use crate::diag::{At, TypResult};
use crate::syntax::Span;
+use crate::util::EcoString;
use crate::Context;
/// Call a method on a value.
@@ -66,6 +67,19 @@ pub fn call(
_ => missing()?,
},
+ Value::Dyn(dynamic) => {
+ if let Some(regex) = dynamic.downcast::<Regex>() {
+ match method {
+ "matches" => {
+ Value::Bool(regex.matches(&args.expect::<EcoString>("text")?))
+ }
+ _ => missing()?,
+ }
+ } else {
+ missing()?
+ }
+ }
+
_ => missing()?,
};
diff --git a/src/eval/str.rs b/src/eval/str.rs
index 3b4349a1..514bf318 100644
--- a/src/eval/str.rs
+++ b/src/eval/str.rs
@@ -1,3 +1,7 @@
+use std::fmt::{self, Debug, Formatter};
+use std::hash::{Hash, Hasher};
+use std::ops::Deref;
+
use super::{Array, Value};
use crate::diag::StrResult;
use crate::util::EcoString;
@@ -35,3 +39,45 @@ impl StrExt for EcoString {
}
}
}
+
+/// A regular expression.
+#[derive(Clone)]
+pub struct Regex(regex::Regex);
+
+impl Regex {
+ /// Create a new regex.
+ pub fn new(re: &str) -> StrResult<Self> {
+ regex::Regex::new(re).map(Self).map_err(|err| err.to_string())
+ }
+
+ /// Whether the regex matches the given `text`.
+ pub fn matches(&self, text: &str) -> bool {
+ self.0.is_match(text)
+ }
+}
+
+impl Deref for Regex {
+ type Target = regex::Regex;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Debug for Regex {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "regex({:?})", self.0.as_str())
+ }
+}
+
+impl PartialEq for Regex {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.as_str() == other.0.as_str()
+ }
+}
+
+impl Hash for Regex {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.as_str().hash(state);
+ }
+}
diff --git a/src/eval/value.rs b/src/eval/value.rs
index ba30348f..fc54cbce 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -5,7 +5,7 @@ use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::sync::Arc;
-use super::{ops, Args, Array, Dict, Func, RawLength};
+use super::{ops, Args, Array, Dict, Func, RawLength, Regex};
use crate::diag::{with_alternative, StrResult};
use crate::geom::{
Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides,
@@ -641,6 +641,10 @@ dynamic! {
Dir: "direction",
}
+dynamic! {
+ Regex: "regular expression",
+}
+
castable! {
usize,
Expected: "non-negative integer",
@@ -686,8 +690,10 @@ castable! {
castable! {
Pattern,
- Expected: "function",
+ Expected: "function, string or regular expression",
Value::Func(func) => Pattern::Node(func.node()?),
+ Value::Str(text) => Pattern::text(&text),
+ @regex: Regex => Pattern::Regex(regex.clone()),
}
#[cfg(test)]
diff --git a/src/library/mod.rs b/src/library/mod.rs
index e90e5cc4..eeda620d 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -83,6 +83,7 @@ pub fn new() -> Scope {
std.def_fn("cmyk", utility::cmyk);
std.def_fn("repr", utility::repr);
std.def_fn("str", utility::str);
+ std.def_fn("regex", utility::regex);
std.def_fn("lower", utility::lower);
std.def_fn("upper", utility::upper);
std.def_fn("letter", utility::letter);
diff --git a/src/library/utility/blind.rs b/src/library/utility/blind.rs
deleted file mode 100644
index 0075ab91..00000000
--- a/src/library/utility/blind.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use lipsum::lipsum_from_seed;
-
-use crate::library::prelude::*;
-
-/// Create blind text.
-pub fn lorem(_: &mut Context, args: &mut Args) -> TypResult<Value> {
- let words: usize = args.expect("number of words")?;
- Ok(Value::Str(lipsum_from_seed(words, 97).into()))
-}
diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs
index 4244ccbf..13220242 100644
--- a/src/library/utility/mod.rs
+++ b/src/library/utility/mod.rs
@@ -1,11 +1,9 @@
//! Computational utility functions.
-mod blind;
mod color;
mod math;
mod string;
-pub use blind::*;
pub use color::*;
pub use math::*;
pub use string::*;
diff --git a/src/library/utility/string.rs b/src/library/utility/string.rs
index 92d80be2..0b309b31 100644
--- a/src/library/utility/string.rs
+++ b/src/library/utility/string.rs
@@ -1,3 +1,6 @@
+use lipsum::lipsum_from_seed;
+
+use crate::eval::Regex;
use crate::library::prelude::*;
use crate::library::text::{Case, TextNode};
@@ -37,6 +40,18 @@ fn case(case: Case, args: &mut Args) -> TypResult<Value> {
})
}
+/// Create blind text.
+pub fn lorem(_: &mut Context, args: &mut Args) -> TypResult<Value> {
+ let words: usize = args.expect("number of words")?;
+ Ok(Value::Str(lipsum_from_seed(words, 97).into()))
+}
+
+/// Create a regular expression.
+pub fn regex(_: &mut Context, args: &mut Args) -> TypResult<Value> {
+ let Spanned { v, span } = args.expect::<Spanned<EcoString>>("regular expression")?;
+ Ok(Regex::new(&v).at(span)?.into())
+}
+
/// Converts an integer into one or multiple letters.
pub fn letter(_: &mut Context, args: &mut Args) -> TypResult<Value> {
convert(Numbering::Letter, args)
diff --git a/src/model/content.rs b/src/model/content.rs
index 70205acc..6956d380 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -366,12 +366,19 @@ impl<'a, 'ctx> Builder<'a, 'ctx> {
}
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> TypResult<()> {
- // Handle special content kinds.
match content {
Content::Empty => return Ok(()),
+ Content::Text(text) => {
+ if let Some(realized) = styles.apply(self.ctx, Target::Text(text))? {
+ let stored = self.scratch.templates.alloc(realized);
+ return self.accept(stored, styles);
+ }
+ }
+
Content::Show(node, _) => return self.show(node, styles),
Content::Styled(styled) => return self.styled(styled, styles),
Content::Sequence(seq) => return self.sequence(seq, styles),
+
_ => {}
}
diff --git a/src/model/recipe.rs b/src/model/recipe.rs
index f6adf4a5..48e7a22e 100644
--- a/src/model/recipe.rs
+++ b/src/model/recipe.rs
@@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter};
use super::{Content, Interruption, NodeId, Show, ShowNode, StyleEntry};
use crate::diag::{At, TypResult};
-use crate::eval::{Args, Func, Value};
+use crate::eval::{Args, Func, Regex, Value};
use crate::library::structure::{EnumNode, ListNode};
use crate::syntax::Span;
use crate::Context;
@@ -23,6 +23,7 @@ impl Recipe {
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,
}
}
@@ -43,6 +44,31 @@ impl Recipe {
})?
}
+ (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),
};
@@ -86,6 +112,15 @@ impl Debug for Recipe {
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.
@@ -93,6 +128,8 @@ pub enum Pattern {
pub enum Target<'a> {
/// A showable node.
Node(&'a ShowNode),
+ /// A slice of text.
+ Text(&'a str),
}
/// Identifies a show rule recipe.