Add search feature using grep.

This commit is contained in:
2025-08-31 01:19:08 -05:00
parent 5d760b4cdf
commit f3e708922f
4 changed files with 121 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ extern crate git2;
extern crate regex; extern crate regex;
extern crate pathdiff; extern crate pathdiff;
extern crate chrono; extern crate chrono;
extern crate grep;
#[macro_use] extern crate toml; #[macro_use] extern crate toml;
use std::fs; use std::fs;
@@ -724,3 +725,55 @@ impl<'a> Linker<'a> {
} }
} }
} }
pub struct Searcher<'a> {
historian: &'a Historian
}
#[derive(Serialize)]
pub struct SearchResult {
pub page: Page,
pub matches: Vec<String>
}
impl<'a> Searcher<'a> {
pub fn new(historian: &Historian) -> Searcher {
Searcher {
historian
}
}
pub fn search(&self, root: &Page, query: &str) -> Vec<SearchResult> {
let mut searcher = grep::searcher::SearcherBuilder::new().build();
let matcher = grep::regex::RegexMatcherBuilder::new()
.fixed_strings(true)
.case_insensitive(true)
.build(query)
.unwrap();
let mut results: Vec<SearchResult> = vec![];
self.do_search(root, &mut results, &mut searcher, &matcher);
results
}
fn do_search(&self, root: &Page, results: &mut Vec<SearchResult>, searcher: &mut grep::searcher::Searcher, matcher: &grep::regex::RegexMatcher) {
for child in &root.children {
if let Some(child_page) = self.historian.resolve_to_page(&child.full_name) {
let mut matches: Vec<String> = vec![];
searcher.search_path(matcher, &child.path, grep::searcher::sinks::UTF8(|lnum, line| {
matches.push(line.to_owned());
Ok(true)
}));
self.do_search(&child_page, results, searcher, matcher);
if !matches.is_empty() {
results.push(SearchResult {
page: child_page,
matches
});
}
}
}
}
}

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
extern crate historian; extern crate historian;
use historian::{Historian, Page, Edit, PageRenderer, export_wiki, Linker}; use historian::{Historian, Page, Edit, PageRenderer, export_wiki, Linker, Searcher, SearchResult};
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
use rocket::serde::json::Json; use rocket::serde::json::Json;
@@ -71,6 +71,17 @@ async fn page<'r>(
.render())) .render()))
} }
} }
if key == "q" {
return PageResponder::Page(RawHtml(renderer.template("search.html")
.with_historian(&historian)
.with_page(&page)
.insert("results", &Searcher::new(&historian).search(&page, value))
.insert("options", &toml! {
dynamic = true
})
.render()))
}
} }
} }
@@ -191,7 +202,15 @@ struct Args {
/// Path to templates /// Path to templates
#[arg(long)] #[arg(long)]
template_path: Option<String> template_path: Option<String>,
/// Search the wiki
#[arg(long)]
search: Option<String>,
/// Search root
#[arg(long)]
search_root: Option<String>
} }
fn print_tree(historian: &Historian, page: &Page, prefix: &str) { fn print_tree(historian: &Historian, page: &Page, prefix: &str) {
@@ -203,6 +222,14 @@ fn print_tree(historian: &Historian, page: &Page, prefix: &str) {
} }
} }
fn print_result(result: &SearchResult) {
println!("{}", result.page.full_name);
for line in &result.matches {
println!("{}", line);
}
println!("---");
}
#[rocket::main] #[rocket::main]
async fn main() { async fn main() {
let args = Args::parse(); let args = Args::parse();
@@ -239,6 +266,16 @@ async fn main() {
return; return;
} }
if let Some(search) = args.search {
let searcher = Searcher::new(&historian);
let search_root = args.search_root.as_deref().unwrap_or("");
let page = historian.resolve_to_page(&search_root).expect("failed to find page");
for result in searcher.search(&page, &search) {
print_result(&result);
}
return;
}
rocket::build() rocket::build()
.manage(historian) .manage(historian)
.manage(renderer) .manage(renderer)

8
templates/search.css Normal file
View File

@@ -0,0 +1,8 @@
#results {
list-style-type: none;
padding: 0;
}
#results li {
border-bottom: 1px solid;
}

21
templates/search.html Normal file
View File

@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block styles %}
{{ super() }}
<link rel="StyleSheet" href="/search.css" type="text/css" media="screen" />
{% endblock styles %}
{% block content %}
<ul id="results">
{% for result in results %}
<li>
<h2><a href="{{ relative_root | safe }}{{ result.page.url }}">{{ result.page.full_name }}</a></h2>
{% for line in result.matches %}
<p>{{ line | safe }}</p>
{% endfor %}
</li>
{% endfor %}
</ul>
{% endblock content %}