Support text show rules that match their own output (#3327)

This commit is contained in:
Laurenz 2024-02-05 10:42:14 +01:00 committed by GitHub
parent b224769c85
commit 92aba81a91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 81 additions and 51 deletions

View File

@ -14,8 +14,8 @@ use smallvec::smallvec;
use crate::diag::{SourceResult, StrResult}; use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, func, scope, ty, Dict, Element, Fields, Guard, IntoValue, Label, NativeElement, elem, func, scope, ty, Dict, Element, Fields, IntoValue, Label, NativeElement,
Recipe, Repr, Selector, Str, Style, StyleChain, Styles, Value, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
}; };
use crate::introspection::{Location, Meta, MetaElem}; use crate::introspection::{Location, Meta, MetaElem};
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; 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. /// Check whether a show rule recipe is disabled.
pub fn is_guarded(&self, guard: Guard) -> bool { pub fn is_guarded(&self, index: RecipeIndex) -> bool {
self.inner.lifecycle.contains(guard.0) self.inner.lifecycle.contains(index.0)
} }
/// Whether this content has already been prepared. /// Whether this content has already been prepared.
@ -157,8 +157,8 @@ impl Content {
} }
/// Disable a show rule recipe. /// Disable a show rule recipe.
pub fn guarded(mut self, guard: Guard) -> Self { pub fn guarded(mut self, index: RecipeIndex) -> Self {
self.make_mut().lifecycle.insert(guard.0); self.make_mut().lifecycle.insert(index.0);
self self
} }

View File

@ -336,7 +336,3 @@ pub enum Behaviour {
/// An element that does not have a visual representation. /// An element that does not have a visual representation.
Invisible, 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);

View File

@ -133,6 +133,7 @@ impl Styles {
self.0.iter().find_map(|entry| match &**entry { self.0.iter().find_map(|entry| match &**entry {
Style::Property(property) => property.is_of(elem).then_some(property.span), Style::Property(property) => property.is_of(elem).then_some(property.span),
Style::Recipe(recipe) => recipe.is_of(elem).then_some(Some(recipe.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), Property(Property),
/// A show rule recipe. /// A show rule recipe.
Recipe(Recipe), Recipe(Recipe),
/// Disables a specific show rule recipe.
Revocation(RecipeIndex),
} }
impl Style { impl Style {
@ -204,6 +207,7 @@ impl Debug for Style {
match self { match self {
Self::Property(property) => property.fmt(f), Self::Property(property) => property.fmt(f),
Self::Recipe(recipe) => recipe.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. /// A show rule transformation that can be applied to a match.
#[derive(Clone, PartialEq, Hash)] #[derive(Clone, PartialEq, Hash)]
pub enum Transformation { pub enum Transformation {
@ -522,13 +530,8 @@ impl<'a> StyleChain<'a> {
next(self.properties::<T>(func, id, inherent).cloned(), &default) 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. /// Iterate over all values for the given property in the chain.
pub fn properties<T: 'static>( fn properties<T: 'static>(
self, self,
func: Element, func: Element,
id: u8, id: u8,
@ -562,7 +565,7 @@ impl<'a> StyleChain<'a> {
} }
/// Iterate over the entries of the chain. /// 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() } 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. /// An iterator over the entries in a style chain.
struct Entries<'a> { pub struct Entries<'a> {
inner: std::slice::Iter<'a, Prehashed<Style>>, inner: std::slice::Iter<'a, Prehashed<Style>>,
links: Links<'a>, links: Links<'a>,
} }

View File

@ -14,9 +14,9 @@ use typed_arena::Arena;
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route}; use crate::engine::{Engine, Route};
use crate::foundations::{ use crate::foundations::{
Behave, Behaviour, Content, Guard, NativeElement, Packed, Recipe, Regex, Selector, Behave, Behaviour, Content, NativeElement, Packed, Recipe, RecipeIndex, Regex,
Show, ShowSet, StyleChain, StyleVec, StyleVecBuilder, Styles, Synthesize, Selector, Show, ShowSet, Style, StyleChain, StyleVec, StyleVecBuilder, Styles,
Transformation, Synthesize, Transformation,
}; };
use crate::introspection::{Locatable, Meta, MetaElem}; use crate::introspection::{Locatable, Meta, MetaElem};
use crate::layout::{ use crate::layout::{
@ -30,7 +30,7 @@ use crate::model::{
}; };
use crate::syntax::Span; use crate::syntax::Span;
use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; 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. /// Realize into an element that is capable of root-level layout.
#[typst_macros::time(name = "realize root")] #[typst_macros::time(name = "realize root")]
@ -129,7 +129,7 @@ struct Verdict<'a> {
/// An optional transformation step to apply to an element. /// An optional transformation step to apply to an element.
enum Step<'a> { enum Step<'a> {
/// A user-defined transformational show rule. /// A user-defined transformational show rule.
Recipe(&'a Recipe, Guard), Recipe(&'a Recipe, RecipeIndex),
/// The built-in show rule. /// The built-in show rule.
Builtin, Builtin,
} }
@ -143,6 +143,7 @@ fn verdict<'a>(
) -> Option<Verdict<'a>> { ) -> Option<Verdict<'a>> {
let mut target = target; let mut target = target;
let mut map = Styles::new(); let mut map = Styles::new();
let mut revoked = BitSet::new();
let mut step = None; let mut step = None;
let mut slot; let mut slot;
@ -162,9 +163,20 @@ fn verdict<'a>(
target = &slot; 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. // We're not interested in recipes that don't match.
if !recipe.applicable(target, styles) { if !recipe.applicable(target, styles) {
r += 1;
continue; continue;
} }
@ -180,14 +192,15 @@ fn verdict<'a>(
// applied to the `target` previously. For this purpose, show rules // applied to the `target` previously. For this purpose, show rules
// are indexed from the top of the chain as the chain might grow to // are indexed from the top of the chain as the chain might grow to
// the bottom. // the bottom.
let depth = *depth.get_or_init(|| styles.recipes().count()); let depth =
let guard = Guard(depth - i); *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, // If we find a matching, unguarded replacement show rule,
// remember it, but still continue searching for potential // remember it, but still continue searching for potential
// show-set styles that might change the verdict. // 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 // If we found a show rule and are already prepared, there is
// nothing else to do, so we can just break. // 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. // If we found no user-defined rule, also consider the built-in show rule.
@ -276,16 +291,16 @@ fn show(
engine: &mut Engine, engine: &mut Engine,
target: Content, target: Content,
recipe: &Recipe, recipe: &Recipe,
guard: Guard, index: RecipeIndex,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
match &recipe.selector { match &recipe.selector {
Some(Selector::Regex(regex)) => { Some(Selector::Regex(regex)) => {
// If the verdict picks this rule, the `target` is guaranteed // If the verdict picks this rule, the `target` is guaranteed
// to be a text element. // to be a text element.
let text = target.into_packed::<TextElem>().unwrap(); 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>, elem: &Packed<TextElem>,
regex: &Regex, regex: &Regex,
recipe: &Recipe, recipe: &Recipe,
guard: Guard, index: RecipeIndex,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
let make = |s: &str| { let make = |s: &str| {
let mut fresh = elem.clone(); let mut fresh = elem.clone();
@ -314,7 +329,7 @@ fn show_regex(
result.push(make(&text[cursor..start])); 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)?; let transformed = recipe.apply(engine, piece)?;
result.push(transformed); result.push(transformed);
cursor = m.end(); cursor = m.end();
@ -324,7 +339,18 @@ fn show_regex(
result.push(make(&text[cursor..])); 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. /// 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) { if !self.engine.route.within(Route::MAX_SHOW_RULE_DEPTH) {
bail!( bail!(
content.span(), "maximum show rule depth exceeded"; content.span(), "maximum show rule depth exceeded";
hint: "check whether the show rule matches its own output"; hint: "check whether the show rule matches its own output"
hint: "this is a current compiler limitation that will be resolved in the future",
); );
} }
let stored = self.scratch.content.alloc(realized); let stored = self.scratch.content.alloc(realized);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -52,20 +52,5 @@
// Test recursive show rules. // Test recursive show rules.
// Error: 22-25 maximum show rule depth exceeded // Error: 22-25 maximum show rule depth exceeded
// Hint: 22-25 check whether the show rule matches its own output // 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$ #show math.equation: $x$
$ 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

View File

@ -13,6 +13,27 @@ Die Zeitung Der Spiegel existiert.
TeX, LaTeX, LuaTeX and LuaLaTeX! 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. // Test that replacements happen exactly once.
#show "A": [BB] #show "A": [BB]