Add an opt-in option to lowercase filter values

Filter values that only differ in case, like `Mint` and `mint`, came
through as separate values. The new `--lowercase-filters` option
(`lowercase_filters`, `PAGEFIND_LOWERCASE_FILTERS`, and
`lowercaseFilters` / `lowercase_filters` in the Node and Python
wrappers) lowercases filter values when indexing and merges values on
a page that become equal. Filter names are left as they are. Off by
default.

Fixes #737
This commit is contained in:
dualfroz committed 2026-10-03 01:43:17 +02:00
1 parent 1308f3c1bb
commit 3a1d31147a
12 files changed
+176

No files matched your search

@@ -0,0 +1,51 @@
name: Filtering > Filter values can be lowercased
steps:
- step: I have the environment variable "PAGEFIND_SITE" set to "public"
- step: I have a "public/index.html" file with the content {html}
html: >-
<!DOCTYPE html><html lang="en"><head></head><body><p
data-results>Nothing</p></body></html>
- step: I have a "public/cheeka/index.html" file with the content {html}
html: >-
<!DOCTYPE html><html lang="en"><head></head><body><span
data-pagefind-filter="color">Black</span>
<span data-pagefind-filter="color">White</span>
<span data-pagefind-filter="color">white</span>
<h1>Cat</h1></body></html>
- step: I have a "public/theodore/index.html" file with the content {html}
html: >-
<!DOCTYPE html><html lang="en"><head></head><body><span
data-pagefind-filter="color">WHITE</span>
<h1>Cat</h1></body></html>
- macro: I run Pagefind with "--lowercase-filters"
- step: stdout should contain "Running Pagefind"
- step: I serve the directory "public"
- step: In my browser, I load "/"
- step: In my browser, I evaluate {js}
js: |-
let pagefind = await import("/pagefind/pagefind.js");
let filters = await pagefind.filters();
let strings = Object.entries(filters).map(([filter, values]) => {
values = Object.entries(values).map(([value, count]) => {
return `${value}(${count})`;
})
return `${filter}:[${values.join(", ")}]`;
});
let search = await pagefind.search("Cat", { filters: { color: "white" } });
let data = await Promise.all(search.results.map(result => result.data()));
document.querySelector('[data-results]').innerText = strings.join(' ')
+ ' ' + data.map(d => d.url).sort().join(', ');
- step: In my browser, the console should be empty
- step: In my browser, I evaluate {js}
js: >-
let val = await toolproof.querySelector("[data-results]");
toolproof.assert_eq(val.innerHTML, `color:[black(1), white(2)]
/cheeka/, /theodore/`);
@@ -0,0 +1,39 @@
name: Node API Base Tests > Pagefind lowercaseFilters config
platforms:
- linux
- mac
steps:
- ref: ./background.toolproof.yml
- step: I have a "public/index.js" file with the content {js}
js: |2-
import * as pagefind from "pagefind";
const run = async () => {
const { index } = await pagefind.createIndex({
lowercaseFilters: true,
});
await index.addHTMLFile({sourcePath: "page_a/index.html", content: "<html lang='en'><body><h1 data-pagefind-filter='color:White'>Cat</h1></body></html>"});
await index.addCustomRecord({
url: "/page_b/",
content: "Cat",
language: "en",
filters: { color: ["WHITE", "Black"] },
});
await index.writeFiles();
console.log(`Successfully wrote files`);
}
run();
- macro: I run Pagefind Node as "index.js" in "public"
- step: stdout should contain "Successfully wrote files"
- step: The file "public/pagefind/pagefind.js" should not be empty
- step: I serve the directory "public"
- step: In my browser, I load "/"
- step: In my browser, I evaluate {js}
js: |-
let pagefind = await import("/pagefind/pagefind.js");
let filters = await pagefind.filters();
toolproof.assert_eq(JSON.stringify(filters), `{"color":{"black":1,"white":2}}`);
- step: In my browser, the console should be empty
+54
View File
@@ -281,6 +281,9 @@ impl Fossicker {
let (content, word_data, anchors, word_count) = self.parse_digest(options);
self.tidy_meta_and_filters();
if options.lowercase_filters {
self.lowercase_filters();
}
let data = self.data.unwrap();
@@ -556,6 +559,22 @@ impl Fossicker {
}
}
/// Lowercases filter values, merging values on a page that only differed in case.
fn lowercase_filters(&mut self) {
if let Some(data) = self.data.as_mut() {
for values in data.filters.values_mut() {
let mut lowercased: Vec<String> = Vec::with_capacity(values.len());
for value in values.drain(..) {
let value = value.to_lowercase();
if !lowercased.contains(&value) {
lowercased.push(value);
}
}
*values = lowercased;
}
}
}
/// Parse words from metadata fields and return them with field IDs and positions.
fn parse_meta_words(
meta: &BTreeMap<String, String>,
@@ -1226,6 +1245,41 @@ mod tests {
}
}
#[tokio::test]
async fn lowercase_filters() {
let html = [
"<html><body>",
"<span data-pagefind-filter=\"Garnish\">Mint</span>",
"<span data-pagefind-filter=\"Garnish\">mint</span>",
"<span data-pagefind-filter=\"Garnish\">Basil</span>",
"<p>Hello World!</p>",
"</body></html>",
]
.concat();
let data = test_fossick(html.clone())
.fossick_sync(&test_opts())
.unwrap();
assert_eq!(
data.fragment.data.filters,
BTreeMap::from_iter([(
"Garnish".to_string(),
vec!["Mint".to_string(), "mint".to_string(), "Basil".to_string()]
)])
);
let mut opts = test_opts();
opts.lowercase_filters = true;
let data = test_fossick(html).fossick_sync(&opts).unwrap();
assert_eq!(
data.fragment.data.filters,
BTreeMap::from_iter([(
"Garnish".to_string(),
vec!["mint".to_string(), "basil".to_string()]
)])
);
}
#[cfg(not(target_os = "windows"))]
#[test]
fn building_url() {
+12
View File
@@ -96,6 +96,14 @@ pub(crate) struct PagefindInboundConfig {
#[clap(required = false)]
pub(crate) include_characters: Option<String>,
#[clap(
long,
help = "Lowercase filter values when indexing, so that values differing only in case are merged into one."
)]
#[clap(required = false)]
#[serde(default = "defaults::default_false")]
pub(crate) lowercase_filters: bool,
#[clap(
long,
help = "Serve the source directory after creating the search index"
@@ -199,6 +207,8 @@ pub struct PagefindServiceConfig {
#[patch(as_option)]
/// Include these characters when indexing and searching words.
pub(crate) include_characters: Option<String>,
/// Lowercase filter values when indexing, so that values differing only in case are merged into one.
pub(crate) lowercase_filters: Option<bool>,
}
mod defaults {
@@ -230,6 +240,7 @@ pub(crate) struct SearchOptions {
pub(crate) glob: String,
pub(crate) force_language: Option<String>,
pub(crate) include_characters: Vec<char>,
pub(crate) lowercase_filters: bool,
pub(crate) version: &'static str,
pub(crate) logger: Logger,
pub(crate) keep_index_url: bool,
@@ -311,6 +322,7 @@ impl SearchOptions {
glob: config.glob,
force_language: config.force_language,
include_characters,
lowercase_filters: config.lowercase_filters,
version: env!("CARGO_PKG_VERSION"),
logger: Logger::new(
log_level,