Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ a streamlined variant of the [Keep a Changelog] spec. Notable changes are:
ignored.
- Release date is not tracked

## [2.6.1]

### Fixed

- Fixed search index to properly include content from `{=include=}` directives.
Included files are now searchable as part of their parent document.
- Fixed memory leak in mobile search focus trap. Event listeners are now
properly cleaned up when closing mobile search or when results update.
- Fixed double navigation issue when pressing Enter in search results. Added
debounce guard to prevent redundant page navigations.

## [2.6.0]

### Added
Expand Down Expand Up @@ -241,11 +252,12 @@ a streamlined variant of the [Keep a Changelog] spec. Notable changes are:

### Fixed

- Fixed silent failure in syntax highlighting initialization (ndg-commonmark)
- Prevented panic in theme name fallback logic (ndg-commonmark)
- Prevented panic on empty fence character extraction (ndg-commonmark)
- Fixed silent failure in syntax highlighting initialization in ndg-commonmark
- Prevented panic in theme name fallback logic in ndg-commonmark
- Prevented panic on empty fence character extraction in ndg-commonmark

[Unreleased]: https://github.com/feel-co/ndg/compare/v2.6.0...HEAD
[2.6.1]: https://github.com/feel-co/ndg/compare/v2.6.0...v2.6.1
[2.6.0]: https://github.com/feel-co/ndg/compare/v2.5.1...v2.6.0
[2.5.0]: https://github.com/feel-co/ndg/compare/v2.4.1...v2.5.0
[2.5.1]: https://github.com/feel-co/ndg/compare/v2.5.0...v2.5.1
90 changes: 90 additions & 0 deletions crates/ndg-config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,96 @@ impl Config {
search_cfg.max_heading_level = level;
}
},
"search.min_word_length" => {
let length = value.parse::<usize>().map_err(|_| {
ConfigError::Config(format!(
"Invalid value for 'search.min_word_length': '{value}'. \
Expected a positive integer"
))
})?;
if self.search.is_none() {
self.search = Some(search::SearchConfig::default());
}
if let Some(ref mut search_cfg) = self.search {
search_cfg.min_word_length = length;
}
},
"search.boost" => {
let boost = value.parse::<f32>().map_err(|_| {
ConfigError::Config(format!(
"Invalid value for 'search.boost': '{value}'. Expected a number"
))
})?;
if self.search.is_none() {
self.search = Some(search::SearchConfig::default());
}
if let Some(ref mut search_cfg) = self.search {
search_cfg.boost = Some(boost);
}
},
"search.boost_title" => {
let boost = value.parse::<f32>().map_err(|_| {
ConfigError::Config(format!(
"Invalid value for 'search.boost_title': '{value}'. Expected a \
number"
))
})?;
if self.search.is_none() {
self.search = Some(search::SearchConfig::default());
}
if let Some(ref mut search_cfg) = self.search {
search_cfg.boost_title = Some(boost);
}
},
"search.boost_content" => {
let boost = value.parse::<f32>().map_err(|_| {
ConfigError::Config(format!(
"Invalid value for 'search.boost_content': '{value}'. Expected \
a number"
))
})?;
if self.search.is_none() {
self.search = Some(search::SearchConfig::default());
}
if let Some(ref mut search_cfg) = self.search {
search_cfg.boost_content = Some(boost);
}
},
"search.boost_anchor" => {
let boost = value.parse::<f32>().map_err(|_| {
ConfigError::Config(format!(
"Invalid value for 'search.boost_anchor': '{value}'. Expected a \
number"
))
})?;
if self.search.is_none() {
self.search = Some(search::SearchConfig::default());
}
if let Some(ref mut search_cfg) = self.search {
search_cfg.boost_anchor = Some(boost);
}
},

// Nested search.stopwords.* - add individual stopwords
key if key.starts_with("search.stopwords.") => {
#[allow(
clippy::expect_used,
reason = "Guard condition ensures strip_prefix cannot fail"
)]
let _index = key.strip_prefix("search.stopwords.").expect(
"Key starts with 'search.stopwords.' prefix, strip_prefix cannot \
fail",
);
// Add stopword to the list
if self.search.is_none() {
self.search = Some(search::SearchConfig::default());
}
if let Some(ref mut search_cfg) = self.search
&& !search_cfg.stopwords.contains(&value.to_string())
{
search_cfg.stopwords.push(value.to_string());
}
},

// Nested sidebar.options.depth
"sidebar.options.depth" => {
Expand Down
62 changes: 62 additions & 0 deletions crates/ndg-config/src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,75 @@ pub struct SearchConfig {
/// - 3: H1, H2, H3 headings (default)
/// - 6: All headings H1-H6
pub max_heading_level: u8,

/// Minimum word length for indexing
///
/// Words shorter than this length will be excluded from the search index.
/// Default is 2, meaning single-character words are ignored.
pub min_word_length: usize,

/// List of stopwords to exclude from indexing
///
/// Common words that should be ignored during indexing.
/// Empty by default - users can add their own stopwords.
pub stopwords: Vec<String>,

/// Score multiplier for title matches
///
/// If not set, falls back to `boost` value. Default is 100.0 for fuzzy
/// matches and 20.0 for exact matches.
pub boost_title: Option<f32>,

/// Score multiplier for content matches
///
/// If not set, falls back to `boost` value. Default is 30.0 for fuzzy
/// matches and 2.0 for partial matches.
pub boost_content: Option<f32>,

/// Score multiplier for anchor/heading matches
///
/// If not set, falls back to `boost` value. This affects matching section
/// headings.
pub boost_anchor: Option<f32>,

/// Global score multiplier
///
/// Sets all boost values at once. Individual boost_* settings override
/// this for their respective categories.
pub boost: Option<f32>,
}

impl Default for SearchConfig {
fn default() -> Self {
Self {
enable: true,
max_heading_level: 3,
min_word_length: 2,
stopwords: Vec::new(),
boost_title: None,
boost_content: None,
boost_anchor: None,
boost: None,
}
}
}

impl SearchConfig {
/// Get the effective title boost value
#[must_use]
pub fn get_title_boost(&self) -> f32 {
self.boost_title.or(self.boost).unwrap_or(100.0)
}

/// Get the effective content boost value
#[must_use]
pub fn get_content_boost(&self) -> f32 {
self.boost_content.or(self.boost).unwrap_or(30.0)
}

/// Get the effective anchor boost value
#[must_use]
pub fn get_anchor_boost(&self) -> f32 {
self.boost_anchor.or(self.boost).unwrap_or(10.0)
}
}
Loading
Loading