Text replacement show rules

This commit is contained in:
Laurenz 2022-05-03 16:59:13 +02:00
parent e18a896a93
commit 507c5fc925
18 changed files with 212 additions and 23 deletions

1
Cargo.lock generated
View File

@ -902,6 +902,7 @@ dependencies = [
"pdf-writer", "pdf-writer",
"pico-args", "pico-args",
"pixglyph", "pixglyph",
"regex",
"resvg", "resvg",
"roxmltree", "roxmltree",
"rustybuzz", "rustybuzz",

View File

@ -23,6 +23,7 @@ serde = { version = "1", features = ["derive"] }
typed-arena = "2" typed-arena = "2"
parking_lot = "0.12" parking_lot = "0.12"
unscanny = { git = "https://github.com/typst/unscanny" } unscanny = { git = "https://github.com/typst/unscanny" }
regex = "1"
# Text and font handling # Text and font handling
hypher = "0.1" hypher = "0.1"

View File

@ -1,8 +1,9 @@
//! Methods on values. //! Methods on values.
use super::{Args, StrExt, Value}; use super::{Args, Regex, StrExt, Value};
use crate::diag::{At, TypResult}; use crate::diag::{At, TypResult};
use crate::syntax::Span; use crate::syntax::Span;
use crate::util::EcoString;
use crate::Context; use crate::Context;
/// Call a method on a value. /// Call a method on a value.
@ -66,6 +67,19 @@ pub fn call(
_ => missing()?, _ => 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()?, _ => missing()?,
}; };

View File

@ -1,3 +1,7 @@
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use super::{Array, Value}; use super::{Array, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::util::EcoString; 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);
}
}

View File

@ -5,7 +5,7 @@ use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::sync::Arc; 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::diag::{with_alternative, StrResult};
use crate::geom::{ use crate::geom::{
Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides, Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides,
@ -641,6 +641,10 @@ dynamic! {
Dir: "direction", Dir: "direction",
} }
dynamic! {
Regex: "regular expression",
}
castable! { castable! {
usize, usize,
Expected: "non-negative integer", Expected: "non-negative integer",
@ -686,8 +690,10 @@ castable! {
castable! { castable! {
Pattern, Pattern,
Expected: "function", Expected: "function, string or regular expression",
Value::Func(func) => Pattern::Node(func.node()?), Value::Func(func) => Pattern::Node(func.node()?),
Value::Str(text) => Pattern::text(&text),
@regex: Regex => Pattern::Regex(regex.clone()),
} }
#[cfg(test)] #[cfg(test)]

View File

@ -83,6 +83,7 @@ pub fn new() -> Scope {
std.def_fn("cmyk", utility::cmyk); std.def_fn("cmyk", utility::cmyk);
std.def_fn("repr", utility::repr); std.def_fn("repr", utility::repr);
std.def_fn("str", utility::str); std.def_fn("str", utility::str);
std.def_fn("regex", utility::regex);
std.def_fn("lower", utility::lower); std.def_fn("lower", utility::lower);
std.def_fn("upper", utility::upper); std.def_fn("upper", utility::upper);
std.def_fn("letter", utility::letter); std.def_fn("letter", utility::letter);

View File

@ -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()))
}

View File

@ -1,11 +1,9 @@
//! Computational utility functions. //! Computational utility functions.
mod blind;
mod color; mod color;
mod math; mod math;
mod string; mod string;
pub use blind::*;
pub use color::*; pub use color::*;
pub use math::*; pub use math::*;
pub use string::*; pub use string::*;

View File

@ -1,3 +1,6 @@
use lipsum::lipsum_from_seed;
use crate::eval::Regex;
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::{Case, TextNode}; 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. /// Converts an integer into one or multiple letters.
pub fn letter(_: &mut Context, args: &mut Args) -> TypResult<Value> { pub fn letter(_: &mut Context, args: &mut Args) -> TypResult<Value> {
convert(Numbering::Letter, args) convert(Numbering::Letter, args)

View File

@ -366,12 +366,19 @@ impl<'a, 'ctx> Builder<'a, 'ctx> {
} }
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> TypResult<()> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> TypResult<()> {
// Handle special content kinds.
match content { match content {
Content::Empty => return Ok(()), 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::Show(node, _) => return self.show(node, styles),
Content::Styled(styled) => return self.styled(styled, styles), Content::Styled(styled) => return self.styled(styled, styles),
Content::Sequence(seq) => return self.sequence(seq, styles), Content::Sequence(seq) => return self.sequence(seq, styles),
_ => {} _ => {}
} }

View File

@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter};
use super::{Content, Interruption, NodeId, Show, ShowNode, StyleEntry}; use super::{Content, Interruption, NodeId, Show, ShowNode, StyleEntry};
use crate::diag::{At, TypResult}; 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::library::structure::{EnumNode, ListNode};
use crate::syntax::Span; use crate::syntax::Span;
use crate::Context; use crate::Context;
@ -23,6 +23,7 @@ impl Recipe {
pub fn applicable(&self, target: Target) -> bool { pub fn applicable(&self, target: Target) -> bool {
match (&self.pattern, target) { match (&self.pattern, target) {
(Pattern::Node(id), Target::Node(node)) => *id == node.id(), (Pattern::Node(id), Target::Node(node)) => *id == node.id(),
(Pattern::Regex(_), Target::Text(_)) => true,
_ => false, _ => 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), _ => return Ok(None),
}; };
@ -86,6 +112,15 @@ impl Debug for Recipe {
pub enum Pattern { pub enum Pattern {
/// Defines the appearence of some node. /// Defines the appearence of some node.
Node(NodeId), 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. /// A target for a show rule recipe.
@ -93,6 +128,8 @@ pub enum Pattern {
pub enum Target<'a> { pub enum Target<'a> {
/// A showable node. /// A showable node.
Node(&'a ShowNode), Node(&'a ShowNode),
/// A slice of text.
Text(&'a str),
} }
/// Identifies a show rule recipe. /// Identifies a show rule recipe.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -41,16 +41,16 @@
--- ---
// Outset padding. // Outset padding.
#set raw(lang: "rust")
#show node: raw as [ #show node: raw as [
#set text("IBM Plex Mono", 8pt) #set text(8pt)
#h(.7em, weak: true) #h(5.6pt, weak: true)
#rect(radius: 3pt, outset: (y: 3pt, x: 2.5pt), fill: rgb(239, 241, 243))[{node.text}] #rect(radius: 3pt, outset: (y: 3pt, x: 2.5pt), fill: rgb(239, 241, 243), node)
#h(.7em, weak: true) #h(5.6pt, weak: true)
] ]
Use the `*const ptr` pointer. Use the `*const T` pointer or the `&mut T` reference.
--- ---
// Error: 15-38 unexpected key "cake" // Error: 15-38 unexpected key "cake"
#rect(radius: (left: 10pt, cake: 5pt)) #rect(radius: (left: 10pt, cake: 5pt))

View File

@ -62,6 +62,10 @@ Another text.
// Error: 10-15 this function cannot be customized with show // Error: 10-15 this function cannot be customized with show
#show _: upper as {} #show _: upper as {}
---
// Error: 7-10 expected function, string or regular expression, found color
#show red as []
--- ---
// Error: 2-16 set, show and wrap are only allowed directly in markup // Error: 2-16 set, show and wrap are only allowed directly in markup
{show list as a} {show list as a}

View File

@ -43,7 +43,7 @@
--- ---
// Test multi-recursion with nested lists. // Test multi-recursion with nested lists.
#set rect(padding: 2pt) #set rect(inset: 2pt)
#show v: list as rect(stroke: blue, v) #show v: list as rect(stroke: blue, v)
#show v: list as rect(stroke: red, v) #show v: list as rect(stroke: red, v)

View File

@ -0,0 +1,58 @@
// Test text replacement show rules.
---
// Test classic example.
#set text("Roboto")
#show phrase: "Der Spiegel" as text(smallcaps: true, [#phrase])
Die Zeitung Der Spiegel existiert.
---
// Another classic example.
#show "TeX" as [T#h(-0.145em)#move(dy: 0.233em)[E]#h(-0.135em)X]
#show name: regex("(Lua)?(La)?TeX") as box(text("Latin Modern Roman")[#name])
TeX, LaTeX, LuaTeX and LuaLaTeX!
---
// Test out-of-order guarding.
#show "Good" as [Typst!]
#show "Typst" as [Fun!]
#show "Fun" as [Good!]
#show enum as []
Good \
Fun \
Typst \
---
// Test that replacements happen exactly once.
#show "A" as [BB]
#show "B" as [CC]
AA (8)
---
// Test caseless match and word boundaries.
#show regex("(?i)\bworld\b") as [🌍]
Treeworld, the World of worlds, is a world.
---
// This is a fun one.
#set par(justify: true)
#show letter: regex("\S") as rect(inset: 2pt)[#upper(letter)]
#lorem(5)
---
// See also: https://github.com/mTvare6/hello-world.rs
#show it: regex("(?i)rust") as [#it (🚀)]
Rust is memory-safe and blazingly fast. Let's rewrite everything in rust.
---
// Replace worlds but only in lists.
#show node: list as [
#show "World" as [🌎]
#node
]
World
- World

View File

@ -0,0 +1,10 @@
// Test regexes.
// Ref: false
---
{
let re = regex("(La)?TeX")
test(re.matches("La"), false)
test(re.matches("TeX"), true)
test(re.matches("LaTeX"), true)
}