From a36c86c90c4e3e807d75767cd9dad65379955f9f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 23:13:08 +0200 Subject: [PATCH] fix: exclude-selectors matches are excluded entirely, not just ignored --exclude-selectors marked a match as "ignored", the same as a bare data-pagefind-ignore, so metadata and filters inside the match were still collected. Excluding img.emoji, for example, still let the emoji become the page's auto-detected image. The status was also applied after the element had been processed. For a void element such as that is too late twice over: its metadata has already been read, and the parser has already popped back to the parent, so the status landed on the parent instead and wiped out the rest of the page. Match the selectors in a handler registered ahead of the main element handler, which now reads that flag and gives the node Excluded status up front. The built-in REMOVE_SELECTORS (head, nav, footer, ...) keep the old Ignored status, so metadata tagged inside them still counts. Fixes #692 --- docs/content/docs/config-options.md | 2 +- pagefind/src/fossick/parser.rs | 97 +++++++++++++++++++++++------ 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/docs/content/docs/config-options.md b/docs/content/docs/config-options.md index b3eedce..d6f063f 100644 --- a/docs/content/docs/config-options.md +++ b/docs/content/docs/config-options.md @@ -63,7 +63,7 @@ exclude_selectors: - "[id^='prefix-']" ``` -All children will also be ignored, so using a `#my_navigation *` selector is not required — in other words, the semantics are the same as the [data-pagefind-ignore](/docs/indexing/#removing-individual-elements-from-the-index) attribute. +All children will also be excluded, so using a `#my_navigation *` selector is not required. In other words, the semantics are the same as the [data-pagefind-ignore="all"](/docs/indexing/#removing-individual-elements-from-the-index) attribute: matched elements are skipped for metadata and filters as well as for content. Note that currently Pagefind only supports lists of options via configuration files. If using the `--exclude-selectors` CLI flag or the `PAGEFIND_EXCLUDE_SELECTORS` environment variable, only one selector may be supplied. The selector may be a comma-separated CSS selector though, so the above example can be passed as `--exclude-selectors "#my_navigation, blockquote > span, [id^='prefix-']"`. diff --git a/pagefind/src/fossick/parser.rs b/pagefind/src/fossick/parser.rs index a1f5d88..7793083 100644 --- a/pagefind/src/fossick/parser.rs +++ b/pagefind/src/fossick/parser.rs @@ -63,6 +63,9 @@ struct DomParserData { has_html_element: bool, has_old_bundle_reference: bool, has_default_ui_reference: bool, + // Set when an --exclude-selectors match fires, and taken by the handler that + // builds the node for that same element. + exclude_element: bool, } #[derive(Clone, Copy, Debug, PartialEq)] @@ -134,17 +137,28 @@ impl<'a> DomParser<'a> { pub fn new(options: &'a SearchOptions) -> Self { let data = Rc::new(RefCell::new(DomParserData::default())); let root = format!("{}, {} *", options.root_selector, options.root_selector); - let mut custom_exclusions = options.exclude_selectors.clone(); - custom_exclusions.extend(REMOVE_SELECTORS.iter().map(|s| s.to_string())); - let custom_exclusions = custom_exclusions - .iter() - .map(|e| format!("{} {}", options.root_selector, e)) - .collect::>() - .join(", "); + let scope_to_root = |selectors: &mut dyn Iterator| { + selectors + .map(|e| format!("{} {}", options.root_selector, e)) + .collect::>() + .join(", ") + }; + let user_exclusions = + scope_to_root(&mut options.exclude_selectors.iter().map(String::as_str)); + let auto_exclusions = scope_to_root(&mut REMOVE_SELECTORS.iter().copied()); let mut anchor_counter = 0; + // Registered ahead of the handler below so that the element's own status + // is known before any of its metadata is read. + let user_exclusion_handler = (!user_exclusions.is_empty()).then(|| { + enclose! { (data) element!(user_exclusions, move |_el| { + data.borrow_mut().exclude_element = true; + Ok(()) + })} + }); + let rewriter = HtmlRewriter::new( - vec![ + user_exclusion_handler.into_iter().chain(vec![ enclose! { (data) element!("html", move |el| { let mut data = data.borrow_mut(); data.has_html_element = true; @@ -154,6 +168,7 @@ impl<'a> DomParser<'a> { Ok(()) })}, enclose! { (data) element!(root, move |el| { + let excluded_by_selector = std::mem::take(&mut data.borrow_mut().exclude_element); let explicit_ignore_flag = el.get_attribute("data-pagefind-ignore").map(|attr| { match attr.to_ascii_lowercase().as_str() { "" | "index" | "true" => NodeStatus::Ignored, @@ -175,7 +190,9 @@ impl<'a> DomParser<'a> { let index_attrs: Option> = el.get_attribute("data-pagefind-index-attrs").map(|attr| attr.split(',').map(|a| a.trim().to_string()).collect()); let tag_name = el.tag_name(); - let status = if treat_as_body { + let status = if excluded_by_selector { + NodeStatus::Excluded + } else if treat_as_body { NodeStatus::Body } else if let Some(explicit_ignore_flag) = explicit_ignore_flag { explicit_ignore_flag @@ -495,8 +512,9 @@ impl<'a> DomParser<'a> { } Ok(()) })}, - // If we hit a selector that should be excluded, mark whatever the current node is as such - enclose! { (data) element!(custom_exclusions, move |_el| { + // Elements we always skip stay "ignored" rather than "excluded", + // so that metadata tagged inside a or a