diff options
| -rw-r--r-- | crates/typst/src/foundations/content.rs | 12 | ||||
| -rw-r--r-- | crates/typst/src/foundations/element.rs | 4 | ||||
| -rw-r--r-- | crates/typst/src/foundations/styles.rs | 19 | ||||
| -rw-r--r-- | crates/typst/src/realize/mod.rs | 61 | ||||
| -rw-r--r-- | tests/ref/compiler/show-text.png | bin | 38083 -> 45317 bytes | |||
| -rw-r--r-- | tests/typ/compiler/recursion.typ | 15 | ||||
| -rw-r--r-- | tests/typ/compiler/show-text.typ | 21 |
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 Binary files differindex d09c606b..2026cc35 100644 --- a/tests/ref/compiler/show-text.png +++ b/tests/ref/compiler/show-text.png 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] |
