use crate::graph::PageStore;
use crate::parser::{slugify_page_name, PageId};
use std::collections::HashSet;
use super::parse::QueryExpr;
/// Evaluate a query expression against the PageStore.
/// Returns a list of matching page IDs.
pub fn evaluate(expr: &QueryExpr, store: &PageStore) -> Vec<PageId> {
let result_set = eval_set(expr, store);
let mut results: Vec<PageId> = result_set.into_iter().collect();
results.sort();
results
}
fn eval_set(expr: &QueryExpr, store: &PageStore) -> HashSet<PageId> {
match expr {
QueryExpr::Tag(tag) => {
let tag_lower = tag.to_lowercase();
let mut result = HashSet::new();
// Strictly match pages whose frontmatter tags contain this value
if let Some(ids) = store.tag_index.get(&tag_lower) {
result.extend(ids.iter().cloned());
}
result
}
QueryExpr::And(exprs) => {
if exprs.is_empty() {
return all_page_ids(store);
}
let mut result = eval_set(&exprs[0], store);
for expr in &exprs[1..] {
let other = eval_set(expr, store);
result = result.intersection(&other).cloned().collect();
}
result
}
QueryExpr::Or(exprs) => {
let mut result = HashSet::new();
for expr in exprs {
result.extend(eval_set(expr, store));
}
result
}
QueryExpr::Not(inner) => {
let excluded = eval_set(inner, store);
let all = all_page_ids(store);
all.difference(&excluded).cloned().collect()
}
QueryExpr::Property { key, value } => {
let mut result = HashSet::new();
for (id, page) in &store.pages {
match value {
Some(val) => {
if page
.meta
.properties
.get(key)
.map(|v| v.eq_ignore_ascii_case(val))
.unwrap_or(false)
{
result.insert(id.clone());
}
}
None => {
if page.meta.properties.contains_key(key) {
result.insert(id.clone());
}
}
}
}
result
}
QueryExpr::Namespace(ns) => {
let ns_slug = slugify_page_name(ns);
let mut result = HashSet::new();
if let Some(children) = store.namespace_tree.get(&ns_slug) {
result.extend(children.iter().cloned());
}
// Also include the namespace page itself
if store.pages.contains_key(&ns_slug) {
result.insert(ns_slug);
}
result
}
QueryExpr::Page(name) => {
let slug = slugify_page_name(name);
let mut result = HashSet::new();
if store.pages.contains_key(&slug) {
result.insert(slug);
}
result
}
QueryExpr::TextSearch(text) => {
let text_lower = text.to_lowercase();
let mut result = HashSet::new();
for (id, page) in &store.pages {
if page.content_md.to_lowercase().contains(&text_lower)
|| page.meta.title.to_lowercase().contains(&text_lower)
{
result.insert(id.clone());
}
}
result
}
}
}
fn all_page_ids(store: &PageStore) -> HashSet<PageId> {
store.pages.keys().cloned().collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::build_graph;
use crate::parser::{PageKind, PageMeta, ParsedPage};
use std::collections::HashMap;
use std::path::PathBuf;
fn make_page(name: &str, tags: Vec<&str>, props: Vec<(&str, &str)>) -> ParsedPage {
let mut properties = HashMap::new();
for (k, v) in props {
properties.insert(k.to_string(), v.to_string());
}
ParsedPage {
id: slugify_page_name(name),
meta: PageMeta {
title: name.to_string(),
properties,
tags: tags.into_iter().map(|s| s.to_string()).collect(),
public: Some(true),
aliases: vec![],
date: None,
icon: None,
menu_order: None,
stake: None,
},
kind: PageKind::Page,
source_path: PathBuf::from(format!("pages/{}.md", name)),
namespace: None,
content_md: format!("Content of {}", name),
outgoing_links: vec![],
}
}
#[test]
fn test_eval_tag() {
let store = build_graph(vec![
make_page("Page A", vec!["research"], vec![]),
make_page("Page B", vec!["research", "math"], vec![]),
make_page("Page C", vec!["math"], vec![]),
])
.unwrap();
let result = evaluate(&QueryExpr::Tag("research".to_string()), &store);
assert_eq!(result.len(), 2);
}
#[test]
fn test_eval_property_existence() {
let store = build_graph(vec![
make_page("Page A", vec![], vec![("supply", "yes")]),
make_page("Page B", vec![], vec![("supply", "no")]),
make_page("Page C", vec![], vec![]),
])
.unwrap();
let result = evaluate(
&QueryExpr::Property {
key: "supply".to_string(),
value: None,
},
&store,
);
assert_eq!(result.len(), 2);
}
#[test]
fn test_eval_property_value() {
let store = build_graph(vec![
make_page("Page A", vec![], vec![("supply", "yes")]),
make_page("Page B", vec![], vec![("supply", "no")]),
])
.unwrap();
let result = evaluate(
&QueryExpr::Property {
key: "supply".to_string(),
value: Some("yes".to_string()),
},
&store,
);
assert_eq!(result.len(), 1);
}
#[test]
fn test_eval_and() {
let store = build_graph(vec![
make_page("Page A", vec!["research", "math"], vec![]),
make_page("Page B", vec!["research"], vec![]),
make_page("Page C", vec!["math"], vec![]),
])
.unwrap();
let result = evaluate(
&QueryExpr::And(vec![
QueryExpr::Tag("research".to_string()),
QueryExpr::Tag("math".to_string()),
]),
&store,
);
assert_eq!(result.len(), 1);
assert_eq!(result[0], slugify_page_name("Page A"));
}
#[test]
fn test_eval_text_search() {
let store = build_graph(vec![
make_page("Page A", vec![], vec![]),
make_page("Page B", vec![], vec![]),
])
.unwrap();
let result = evaluate(&QueryExpr::TextSearch("Page A".to_string()), &store);
assert_eq!(result.len(), 1);
}
}
render/src/query/eval.rs
ฯ 0.0%