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

View File

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

View File

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

View File

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

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

View File

@ -13,6 +13,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]