diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index daf6c2dd9..886148674 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -412,7 +412,7 @@ impl Content { /// Queries the content tree for all elements that match the given selector. /// /// Elements produced in `show` rules will not be included in the results. - pub fn query(&self, selector: Selector) -> Vec { + pub fn query(&self, selector: &Selector) -> Vec { let mut results = Vec::new(); self.traverse(&mut |element| -> ControlFlow<()> { if selector.matches(&element, None) { diff --git a/crates/typst-library/src/foundations/selector.rs b/crates/typst-library/src/foundations/selector.rs index bf5449d94..7d76ac209 100644 --- a/crates/typst-library/src/foundations/selector.rs +++ b/crates/typst-library/src/foundations/selector.rs @@ -93,6 +93,8 @@ pub enum Selector { Before { selector: Arc, end: Arc, inclusive: bool }, /// Matches all matches of `selector` after `start`. After { selector: Arc, start: Arc, inclusive: bool }, + /// Matches all children of `ancestor` matching `selector` + Within { selector: Arc, ancestor: Arc }, } impl Selector { @@ -139,7 +141,10 @@ impl Selector { } Self::Location(location) => target.location() == Some(*location), // Not supported here. - Self::Regex(_) | Self::Before { .. } | Self::After { .. } => false, + Self::Regex(_) + | Self::Before { .. } + | Self::After { .. } + | Self::Within { .. } => false, } } } @@ -221,6 +226,15 @@ impl Selector { inclusive, } } + + /// Returns a modified selector that only matches `self` if it is contained in an `ancestor`. + #[func] + pub fn within(self, ancestor: LocatableSelector) -> Selector { + Self::Within { + selector: Arc::new(self), + ancestor: Arc::new(ancestor.0), + } + } } impl From for Selector { @@ -266,6 +280,9 @@ impl Repr for Selector { inclusive_arg ) } + Self::Within { selector, ancestor } => { + eco_format!("{}.within({})", selector.repr(), ancestor.repr()) + } } } } @@ -352,7 +369,8 @@ impl FromValue for LocatableSelector { } } Selector::Before { selector, end: split, .. } - | Selector::After { selector, start: split, .. } => { + | Selector::After { selector, start: split, .. } + | Selector::Within { selector, ancestor: split } => { for selector in [selector, split] { validate(selector)?; } @@ -431,7 +449,8 @@ impl FromValue for ShowableSelector { | Selector::Location(_) | Selector::Can(_) | Selector::Before { .. } - | Selector::After { .. } => { + | Selector::After { .. } + | Selector::Within { .. } => { bail!("this selector cannot be used with show") } } diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index 9751dfcb8..112c42e04 100644 --- a/crates/typst-library/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -185,6 +185,11 @@ impl Introspector { } list } + Selector::Within { selector, ancestor } => self + .query(ancestor) + .iter() + .flat_map(|children| children.query(selector)) + .collect(), // Not supported here. Selector::Can(_) | Selector::Regex(_) => EcoVec::new(), }; diff --git a/tests/ref/query-within.png b/tests/ref/query-within.png new file mode 100644 index 000000000..2092df0a7 Binary files /dev/null and b/tests/ref/query-within.png differ diff --git a/tests/suite/introspection/query.typ b/tests/suite/introspection/query.typ index 94e82820e..67d54c44b 100644 --- a/tests/suite/introspection/query.typ +++ b/tests/suite/introspection/query.typ @@ -250,3 +250,27 @@ t("b") block(height: 1fr, metadata("b")) } + +--- query-within --- + +#let test-selector(selector, ref) = context { + test(query(selector).map(e => e.body), ref) +} + += A #strong[a] #label("strong") + +#strong[b] #label("strong") + +== B + +== C #strong[c] #label("strong") +> +#test-selector( + selector().within(heading), + ([a], [c]), +) + +#test-selector( + selector().within(heading.where(level: 2)), + ([c], ), +)