Text replacement show rules
This commit is contained in:
parent
e18a896a93
commit
507c5fc925
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -902,6 +902,7 @@ dependencies = [
|
||||
"pdf-writer",
|
||||
"pico-args",
|
||||
"pixglyph",
|
||||
"regex",
|
||||
"resvg",
|
||||
"roxmltree",
|
||||
"rustybuzz",
|
||||
|
@ -23,6 +23,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
typed-arena = "2"
|
||||
parking_lot = "0.12"
|
||||
unscanny = { git = "https://github.com/typst/unscanny" }
|
||||
regex = "1"
|
||||
|
||||
# Text and font handling
|
||||
hypher = "0.1"
|
||||
|
@ -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()?,
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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);
|
||||
|
@ -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()))
|
||||
}
|
@ -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::*;
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -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(®ex::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.
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
BIN
tests/ref/style/show-text.png
Normal file
BIN
tests/ref/style/show-text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
@ -41,16 +41,16 @@
|
||||
|
||||
---
|
||||
// Outset padding.
|
||||
#set raw(lang: "rust")
|
||||
#show node: raw as [
|
||||
#set text("IBM Plex Mono", 8pt)
|
||||
#h(.7em, weak: true)
|
||||
#rect(radius: 3pt, outset: (y: 3pt, x: 2.5pt), fill: rgb(239, 241, 243))[{node.text}]
|
||||
#h(.7em, weak: true)
|
||||
#set text(8pt)
|
||||
#h(5.6pt, weak: true)
|
||||
#rect(radius: 3pt, outset: (y: 3pt, x: 2.5pt), fill: rgb(239, 241, 243), node)
|
||||
#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"
|
||||
#rect(radius: (left: 10pt, cake: 5pt))
|
||||
|
||||
|
@ -62,6 +62,10 @@ Another text.
|
||||
// Error: 10-15 this function cannot be customized with show
|
||||
#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
|
||||
{show list as a}
|
||||
|
@ -43,7 +43,7 @@
|
||||
|
||||
---
|
||||
// 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: red, v)
|
||||
|
||||
|
58
tests/typ/style/show-text.typ
Normal file
58
tests/typ/style/show-text.typ
Normal 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
|
10
tests/typ/utility/regex.typ
Normal file
10
tests/typ/utility/regex.typ
Normal 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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user