summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-02-05 10:42:14 +0100
committerGitHub <noreply@github.com>2024-02-05 09:42:14 +0000
commit92aba81a91364eb49f3a613cd2db3b2163202434 (patch)
tree28e471e3d882b480b190c3024f21548de73683d7
parentb224769c85767bccc8f21ab5ecaa01d93a5ea149 (diff)
Support text show rules that match their own output (#3327)
-rw-r--r--crates/typst/src/foundations/content.rs12
-rw-r--r--crates/typst/src/foundations/element.rs4
-rw-r--r--crates/typst/src/foundations/styles.rs19
-rw-r--r--crates/typst/src/realize/mod.rs61
-rw-r--r--tests/ref/compiler/show-text.pngbin38083 -> 45317 bytes
-rw-r--r--tests/typ/compiler/recursion.typ15
-rw-r--r--tests/typ/compiler/show-text.typ21
7 files changed, 81 insertions, 51 deletions
diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs
index 6f912086..0d764f11 100644
--- a/crates/typst/src/foundations/content.rs
+++ b/crates/typst/src/foundations/content.rs
@@ -14,8 +14,8 @@ use smallvec::smallvec;
use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
- elem, func, scope, ty, Dict, Element, Fields, Guard, IntoValue, Label, NativeElement,
- Recipe, Repr, Selector, Str, Style, StyleChain, Styles, Value,
+ elem, func, scope, ty, Dict, Element, Fields, IntoValue, Label, NativeElement,
+ Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
};
use crate::introspection::{Location, Meta, MetaElem};
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
@@ -142,8 +142,8 @@ impl Content {
}
/// Check whether a show rule recipe is disabled.
- pub fn is_guarded(&self, guard: Guard) -> bool {
- self.inner.lifecycle.contains(guard.0)
+ pub fn is_guarded(&self, index: RecipeIndex) -> bool {
+ self.inner.lifecycle.contains(index.0)
}
/// Whether this content has already been prepared.
@@ -157,8 +157,8 @@ impl Content {
}
/// Disable a show rule recipe.
- pub fn guarded(mut self, guard: Guard) -> Self {
- self.make_mut().lifecycle.insert(guard.0);
+ pub fn guarded(mut self, index: RecipeIndex) -> Self {
+ self.make_mut().lifecycle.insert(index.0);
self
}
diff --git a/crates/typst/src/foundations/element.rs b/crates/typst/src/foundations/element.rs
index 6d73a896..412e3089 100644
--- a/crates/typst/src/foundations/element.rs
+++ b/crates/typst/src/foundations/element.rs
@@ -336,7 +336,3 @@ pub enum Behaviour {
/// An element that does not have a visual representation.
Invisible,
}
-
-/// Guards content against being affected by the same show rule multiple times.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Guard(pub usize);
diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs
index 12ba2876..c6ee3c5e 100644
--- a/crates/typst/src/foundations/styles.rs
+++ b/crates/typst/src/foundations/styles.rs
@@ -133,6 +133,7 @@ impl Styles {
self.0.iter().find_map(|entry| match &**entry {
Style::Property(property) => property.is_of(elem).then_some(property.span),
Style::Recipe(recipe) => recipe.is_of(elem).then_some(Some(recipe.span)),
+ Style::Revocation(_) => None,
})
}
@@ -179,6 +180,8 @@ pub enum Style {
Property(Property),
/// A show rule recipe.
Recipe(Recipe),
+ /// Disables a specific show rule recipe.
+ Revocation(RecipeIndex),
}
impl Style {
@@ -204,6 +207,7 @@ impl Debug for Style {
match self {
Self::Property(property) => property.fmt(f),
Self::Recipe(recipe) => recipe.fmt(f),
+ Self::Revocation(guard) => guard.fmt(f),
}
}
}
@@ -413,6 +417,10 @@ impl Debug for Recipe {
}
}
+/// Identifies a show rule recipe from the top of the chain.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct RecipeIndex(pub usize);
+
/// A show rule transformation that can be applied to a match.
#[derive(Clone, PartialEq, Hash)]
pub enum Transformation {
@@ -522,13 +530,8 @@ impl<'a> StyleChain<'a> {
next(self.properties::<T>(func, id, inherent).cloned(), &default)
}
- /// Iterate over all style recipes in the chain.
- pub fn recipes(self) -> impl Iterator<Item = &'a Recipe> {
- self.entries().filter_map(Style::recipe)
- }
-
/// Iterate over all values for the given property in the chain.
- pub fn properties<T: 'static>(
+ fn properties<T: 'static>(
self,
func: Element,
id: u8,
@@ -562,7 +565,7 @@ impl<'a> StyleChain<'a> {
}
/// Iterate over the entries of the chain.
- fn entries(self) -> Entries<'a> {
+ pub fn entries(self) -> Entries<'a> {
Entries { inner: [].as_slice().iter(), links: self.links() }
}
@@ -646,7 +649,7 @@ impl Chainable for Styles {
}
/// An iterator over the entries in a style chain.
-struct Entries<'a> {
+pub struct Entries<'a> {
inner: std::slice::Iter<'a, Prehashed<Style>>,
links: Links<'a>,
}
diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs
index b7614b7e..7c0980f6 100644
--- a/crates/typst/src/realize/mod.rs
+++ b/crates/typst/src/realize/mod.rs
@@ -14,9 +14,9 @@ use typed_arena::Arena;
use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route};
use crate::foundations::{
- Behave, Behaviour, Content, Guard, NativeElement, Packed, Recipe, Regex, Selector,
- Show, ShowSet, StyleChain, StyleVec, StyleVecBuilder, Styles, Synthesize,
- Transformation,
+ Behave, Behaviour, Content, NativeElement, Packed, Recipe, RecipeIndex, Regex,
+ Selector, Show, ShowSet, Style, StyleChain, StyleVec, StyleVecBuilder, Styles,
+ Synthesize, Transformation,
};
use crate::introspection::{Locatable, Meta, MetaElem};
use crate::layout::{
@@ -30,7 +30,7 @@ use crate::model::{
};
use crate::syntax::Span;
use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
-use crate::util::hash128;
+use crate::util::{hash128, BitSet};
/// Realize into an element that is capable of root-level layout.
#[typst_macros::time(name = "realize root")]
@@ -129,7 +129,7 @@ struct Verdict<'a> {
/// An optional transformation step to apply to an element.
enum Step<'a> {
/// A user-defined transformational show rule.
- Recipe(&'a Recipe, Guard),
+ Recipe(&'a Recipe, RecipeIndex),
/// The built-in show rule.
Builtin,
}
@@ -143,6 +143,7 @@ fn verdict<'a>(
) -> Option<Verdict<'a>> {
let mut target = target;
let mut map = Styles::new();
+ let mut revoked = BitSet::new();
let mut step = None;
let mut slot;
@@ -162,9 +163,20 @@ fn verdict<'a>(
target = &slot;
}
- for (i, recipe) in styles.recipes().enumerate() {
+ let mut r = 0;
+ for entry in styles.entries() {
+ let recipe = match entry {
+ Style::Recipe(recipe) => recipe,
+ Style::Property(_) => continue,
+ Style::Revocation(index) => {
+ revoked.insert(index.0);
+ continue;
+ }
+ };
+
// We're not interested in recipes that don't match.
if !recipe.applicable(target, styles) {
+ r += 1;
continue;
}
@@ -180,14 +192,15 @@ fn verdict<'a>(
// applied to the `target` previously. For this purpose, show rules
// are indexed from the top of the chain as the chain might grow to
// the bottom.
- let depth = *depth.get_or_init(|| styles.recipes().count());
- let guard = Guard(depth - i);
+ let depth =
+ *depth.get_or_init(|| styles.entries().filter_map(Style::recipe).count());
+ let index = RecipeIndex(depth - r);
- if !target.is_guarded(guard) {
+ if !target.is_guarded(index) && !revoked.contains(index.0) {
// If we find a matching, unguarded replacement show rule,
// remember it, but still continue searching for potential
// show-set styles that might change the verdict.
- step = Some(Step::Recipe(recipe, guard));
+ step = Some(Step::Recipe(recipe, index));
// If we found a show rule and are already prepared, there is
// nothing else to do, so we can just break.
@@ -196,6 +209,8 @@ fn verdict<'a>(
}
}
}
+
+ r += 1;
}
// If we found no user-defined rule, also consider the built-in show rule.
@@ -276,16 +291,16 @@ fn show(
engine: &mut Engine,
target: Content,
recipe: &Recipe,
- guard: Guard,
+ index: RecipeIndex,
) -> SourceResult<Content> {
match &recipe.selector {
Some(Selector::Regex(regex)) => {
// If the verdict picks this rule, the `target` is guaranteed
// to be a text element.
let text = target.into_packed::<TextElem>().unwrap();
- show_regex(engine, &text, regex, recipe, guard)
+ show_regex(engine, &text, regex, recipe, index)
}
- _ => recipe.apply(engine, target.guarded(guard)),
+ _ => recipe.apply(engine, target.guarded(index)),
}
}
@@ -295,7 +310,7 @@ fn show_regex(
elem: &Packed<TextElem>,
regex: &Regex,
recipe: &Recipe,
- guard: Guard,
+ index: RecipeIndex,
) -> SourceResult<Content> {
let make = |s: &str| {
let mut fresh = elem.clone();
@@ -314,7 +329,7 @@ fn show_regex(
result.push(make(&text[cursor..start]));
}
- let piece = make(m.as_str()).guarded(guard);
+ let piece = make(m.as_str());
let transformed = recipe.apply(engine, piece)?;
result.push(transformed);
cursor = m.end();
@@ -324,7 +339,18 @@ fn show_regex(
result.push(make(&text[cursor..]));
}
- Ok(Content::sequence(result))
+ // In contrast to normal elements, which are guarded individually, for text
+ // show rules, we fully revoke the rule. This means that we can replace text
+ // with other text that rematches without running into infinite recursion
+ // problems.
+ //
+ // We do _not_ do this for all content because revoking e.g. a list show
+ // rule for all content resulting from that rule would be wrong: The list
+ // might contain nested lists. Moreover, replacing a normal element with one
+ // that rematches is bad practice: It can for instance also lead to
+ // surprising query results, so it's better to let the user deal with it.
+ // All these problems don't exist for text, so it's fine here.
+ Ok(Content::sequence(result).styled(Style::Revocation(index)))
}
/// Builds a document or a flow element from content.
@@ -384,8 +410,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
if !self.engine.route.within(Route::MAX_SHOW_RULE_DEPTH) {
bail!(
content.span(), "maximum show rule depth exceeded";
- hint: "check whether the show rule matches its own output";
- hint: "this is a current compiler limitation that will be resolved in the future",
+ hint: "check whether the show rule matches its own output"
);
}
let stored = self.scratch.content.alloc(realized);
diff --git a/tests/ref/compiler/show-text.png b/tests/ref/compiler/show-text.png
index d09c606b..2026cc35 100644
--- a/tests/ref/compiler/show-text.png
+++ b/tests/ref/compiler/show-text.png
Binary files differ
diff --git a/tests/typ/compiler/recursion.typ b/tests/typ/compiler/recursion.typ
index ef5ea7e6..f5f870b5 100644
--- a/tests/typ/compiler/recursion.typ
+++ b/tests/typ/compiler/recursion.typ
@@ -52,20 +52,5 @@
// Test recursive show rules.
// Error: 22-25 maximum show rule depth exceeded
// Hint: 22-25 check whether the show rule matches its own output
-// Hint: 22-25 this is a current compiler limitation that will be resolved in the future
#show math.equation: $x$
$ x $
-
----
-// Error: 18-21 maximum show rule depth exceeded
-// Hint: 18-21 check whether the show rule matches its own output
-// Hint: 18-21 this is a current compiler limitation that will be resolved in the future
-#show "hey": box[hey]
-hey
-
----
-// Error: 14-19 maximum show rule depth exceeded
-// Hint: 14-19 check whether the show rule matches its own output
-// Hint: 14-19 this is a current compiler limitation that will be resolved in the future
-#show "hey": "hey"
-hey
diff --git a/tests/typ/compiler/show-text.typ b/tests/typ/compiler/show-text.typ
index c279507b..7837bb46 100644
--- a/tests/typ/compiler/show-text.typ
+++ b/tests/typ/compiler/show-text.typ
@@ -14,6 +14,27 @@ Die Zeitung Der Spiegel existiert.
TeX, LaTeX, LuaTeX and LuaLaTeX!
---
+// Test direct cycle.
+#show "Hello": text(red)[Hello]
+Hello World!
+
+---
+// Test replacing text with raw text.
+#show "rax": `rax`
+The register rax.
+
+---
+// Test indirect cycle.
+#show "Good": [Typst!]
+#show "Typst": [Fun!]
+#show "Fun": [Good!]
+
+#set text(ligatures: false)
+Good \
+Fun \
+Typst \
+
+---
// Test that replacements happen exactly once.
#show "A": [BB]
#show "B": [CC]