Support text show rules that match their own output (#3327)
This commit is contained in:
parent
b224769c85
commit
92aba81a91
@ -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
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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 |
@ -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
|
||||
|
@ -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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user