feat: apply Pagefind attributes by selector through config rules
exclude_selectors is the only way the config can reach into a page, and it carries the semantics of data-pagefind-ignore="index". Every other control Pagefind has lives on an attribute, so reaching it from the config meant a new flag per attribute. Add a rules list that pairs a selector with the attributes to set on the elements it matches. The attributes are applied before every other handler, so the rest of the parser reads them exactly as if the page had carried them, and every documented attribute is reachable without new flags. An attribute already present on the element wins, so a rule never overrides markup. A selector that cannot be parsed is reported and skipped. Rules are config-file only: a list of selectors with attribute maps does not fit a CLI flag or an environment variable.
This commit is contained in:
1 parent
c036b250ba
commit
085385c3e4
3 files changed
+163
-3
No files matched your search
@@ -71,6 +71,30 @@ Note that currently Pagefind only supports lists of options via configuration fi
|
||||
|---------------------------|------------------------------|---------------------|
|
||||
| `--exclude-selectors <S>` | `PAGEFIND_EXCLUDE_SELECTORS` | `exclude_selectors` |
|
||||
|
||||
### Rules
|
||||
Applies Pagefind attributes to the elements a selector matches, so a site can be configured with the same vocabulary its HTML would use. For example, in `pagefind.yml`:
|
||||
|
||||
```yml
|
||||
rules:
|
||||
- selector: "#my_navigation"
|
||||
attributes:
|
||||
data-pagefind-ignore: all
|
||||
- selector: ".article-body"
|
||||
attributes:
|
||||
data-pagefind-body: ""
|
||||
data-pagefind-meta: "title[data-title]"
|
||||
```
|
||||
|
||||
Each rule behaves exactly as if the attributes had been written on the matching elements, so the documentation for [indexing attributes](/docs/indexing/) applies unchanged.
|
||||
|
||||
An attribute written in the page always wins over a rule setting the same attribute, so rules never override markup you control. A selector Pagefind cannot parse is reported and skipped, and indexing continues without it.
|
||||
|
||||
Rules are only available via configuration files, as a list of selectors with their attributes does not fit a CLI flag or an environment variable.
|
||||
|
||||
| CLI Flag | ENV Variable | Config Key |
|
||||
|----------|--------------|------------|
|
||||
| n/a | n/a | `rules` |
|
||||
|
||||
### Include characters
|
||||
Prevents Pagefind from stripping the provided characters when indexing content.
|
||||
Allows users to search for words including these characters.
|
||||
|
||||
@@ -154,7 +154,41 @@ impl<'a> DomParser<'a> {
|
||||
.join(", ");
|
||||
let mut anchor_counter = 0;
|
||||
|
||||
// Config rules run before every other handler, so the attributes they set are
|
||||
// read by the handlers below exactly as if the page had carried them itself.
|
||||
let rule_handlers = options
|
||||
.rules
|
||||
.iter()
|
||||
.filter_map(|rule| match rule.selector.parse::<Selector>() {
|
||||
Ok(_) => {
|
||||
let attributes = rule.attributes.clone();
|
||||
Some(element!(rule.selector.clone(), move |el| {
|
||||
for (name, value) in attributes.iter() {
|
||||
// An attribute written in the HTML is more specific than a
|
||||
// rule matching a whole selector, so the page wins.
|
||||
if el.has_attribute(name) {
|
||||
continue;
|
||||
}
|
||||
if el.set_attribute(name, value).is_err() {
|
||||
// Only an invalid attribute name can fail here, and that
|
||||
// is reported once when the rules are read.
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
Err(error) => {
|
||||
options.logger.warn(format!(
|
||||
"Ignoring the rule selector {:?}, which Pagefind can't parse: {error}. Indexing will continue without it.",
|
||||
rule.selector
|
||||
));
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let rewriter = HtmlRewriter::new(
|
||||
rule_handlers.into_iter().chain(
|
||||
vec![
|
||||
enclose! { (data) element!("html", move |el| {
|
||||
let mut data = data.borrow_mut();
|
||||
@@ -563,8 +597,7 @@ impl<'a> DomParser<'a> {
|
||||
}
|
||||
Ok(())
|
||||
})},
|
||||
]
|
||||
.into_iter()
|
||||
])
|
||||
.fold(Settings::new().with_strict(false), |settings, handler| {
|
||||
settings.append_element_content_handler(handler)
|
||||
}),
|
||||
@@ -771,6 +804,93 @@ mod tests {
|
||||
rewriter.wrap()
|
||||
}
|
||||
|
||||
fn test_parse_with_rules(
|
||||
mut input: Vec<&'static str>,
|
||||
rules: &[(&'static str, &[(&'static str, &'static str)])],
|
||||
) -> DomParserResult {
|
||||
use clap::CommandFactory;
|
||||
let config_args = vec![twelf::Layer::Clap(
|
||||
crate::PagefindInboundConfig::command().get_matches_from(vec![
|
||||
"pagefind",
|
||||
"--source",
|
||||
"not_important",
|
||||
]),
|
||||
)];
|
||||
let mut config =
|
||||
SearchOptions::load(crate::PagefindInboundConfig::with_layers(&config_args).unwrap())
|
||||
.unwrap();
|
||||
config.rules = rules
|
||||
.iter()
|
||||
.map(|(selector, attributes)| crate::options::PagefindRule {
|
||||
selector: selector.to_string(),
|
||||
attributes: attributes
|
||||
.iter()
|
||||
.map(|(name, value)| (name.to_string(), value.to_string()))
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut rewriter = DomParser::new(&config);
|
||||
input.insert(0, "<html><body>");
|
||||
input.push("</body></html>");
|
||||
for line in input {
|
||||
let _ = rewriter.write(line.as_bytes());
|
||||
}
|
||||
rewriter.wrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rules_apply_pagefind_attributes_by_selector() {
|
||||
let data = test_parse_with_rules(
|
||||
vec!["<div id='nav'>Dropped</div>", "<p>Kept</p>"],
|
||||
&[("#nav", &[("data-pagefind-ignore", "all")])],
|
||||
);
|
||||
|
||||
assert_eq!(data.digest, "Kept.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rules_can_pick_the_body() {
|
||||
let data = test_parse_with_rules(
|
||||
vec![
|
||||
"<div class='chrome'>Chrome</div>",
|
||||
"<div class='main'>Real content</div>",
|
||||
],
|
||||
&[(".main", &[("data-pagefind-body", "")])],
|
||||
);
|
||||
|
||||
assert_eq!(data.digest, "Real content.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_attribute_in_the_page_wins_over_a_rule() {
|
||||
let data = test_parse_with_rules(
|
||||
vec![
|
||||
"<p class='weighted' data-pagefind-weight='2'>Own weight</p>",
|
||||
"<p class='weighted'>Rule weight</p>",
|
||||
],
|
||||
&[(".weighted", &[("data-pagefind-weight", "5")])],
|
||||
);
|
||||
|
||||
// The first paragraph keeps the weight written in the page, the second one
|
||||
// takes the weight from the rule, so the rule applies without overriding.
|
||||
assert!(data.digest.contains("___PAGEFIND_WEIGHT___2 Own weight."));
|
||||
assert!(data.digest.contains("___PAGEFIND_WEIGHT___5 Rule weight."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_rule_selector_does_not_halt_indexing() {
|
||||
let data = test_parse_with_rules(
|
||||
vec!["<p>Kept</p>", "<div class='ad'>Dropped</div>"],
|
||||
&[
|
||||
(":not(.a b)", &[("data-pagefind-ignore", "all")]),
|
||||
(".ad", &[("data-pagefind-ignore", "all")]),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(data.digest, "Kept.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_exclude_selector_does_not_halt_indexing() {
|
||||
let data = test_parse_with_exclusions(
|
||||
|
||||
+17
-1
@@ -4,7 +4,7 @@ use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use rust_patch::Patch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{env, path::PathBuf};
|
||||
use std::{collections::BTreeMap, env, path::PathBuf};
|
||||
use twelf::config;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
@@ -22,6 +22,15 @@ use crate::{
|
||||
// No options should be added that are required.
|
||||
//
|
||||
|
||||
/// A selector paired with the Pagefind attributes to set on the elements it matches,
|
||||
/// so a site can be configured with the same vocabulary its HTML would use.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct PagefindRule {
|
||||
pub(crate) selector: String,
|
||||
#[serde(default)]
|
||||
pub(crate) attributes: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[config]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
@@ -74,6 +83,11 @@ pub(crate) struct PagefindInboundConfig {
|
||||
#[serde(default)]
|
||||
pub(crate) exclude_selectors: Vec<String>,
|
||||
|
||||
/// Not a CLI flag: a list of selectors with attribute maps does not fit an argument.
|
||||
#[clap(skip)]
|
||||
#[serde(default)]
|
||||
pub(crate) rules: Vec<PagefindRule>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "The file glob Pagefind uses to find HTML files. Defaults to \"**/*.{html}\""
|
||||
@@ -227,6 +241,7 @@ pub(crate) struct SearchOptions {
|
||||
pub(crate) bundle_output: PathBuf,
|
||||
pub(crate) root_selector: String,
|
||||
pub(crate) exclude_selectors: Vec<String>,
|
||||
pub(crate) rules: Vec<PagefindRule>,
|
||||
pub(crate) glob: String,
|
||||
pub(crate) force_language: Option<String>,
|
||||
pub(crate) include_characters: Vec<char>,
|
||||
@@ -308,6 +323,7 @@ impl SearchOptions {
|
||||
bundle_output,
|
||||
root_selector: config.root_selector,
|
||||
exclude_selectors: config.exclude_selectors,
|
||||
rules: config.rules,
|
||||
glob: config.glob,
|
||||
force_language: config.force_language,
|
||||
include_characters,
|
||||
|
||||
Reference in new issue
Block a user