mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
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",
|
"pdf-writer",
|
||||||
"pico-args",
|
"pico-args",
|
||||||
"pixglyph",
|
"pixglyph",
|
||||||
|
"regex",
|
||||||
"resvg",
|
"resvg",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"rustybuzz",
|
"rustybuzz",
|
||||||
|
@ -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"
|
||||||
|
@ -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()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)]
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
//! 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::*;
|
||||||
|
@ -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)
|
||||||
|
@ -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),
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(®ex::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 |
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.
|
// 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))
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
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