mirror of
https://github.com/typst/typst
synced 2025-05-22 04:55:29 +08:00
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::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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
@ -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 |
@ -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
|
|
||||||
|
@ -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]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user