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",
"pico-args",
"pixglyph",
"regex",
"resvg",
"roxmltree",
"rustybuzz",

View File

@ -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"

View File

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

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 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);
}
}

View File

@ -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)]

View File

@ -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);

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.
mod blind;
mod color;
mod math;
mod string;
pub use blind::*;
pub use color::*;
pub use math::*;
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::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)

View File

@ -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),
_ => {}
}

View File

@ -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.

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.
#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))

View File

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

View File

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

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