use std::cell::OnceCell;
use serde_yaml::{Mapping, Value};
use crate::graph::Graph;
use crate::model::Key;
use crate::query::block_eval::BlockIndex;
use crate::query::document::{
FieldPath, Projection, ProjectionBase, ProjectionField, ProjectionSource, PseudoField,
};
use crate::query::frontmatter::{is_reserved_segment, strip_reserved};
use crate::retrieve::EdgeRef;
pub struct ProjectionContext<'a> {
pub graph: &'a Graph,
pub key: &'a Key,
blocks: OnceCell<BlockIndex>,
}
impl<'a> ProjectionContext<'a> {
pub fn new(graph: &'a Graph, key: &'a Key) -> Self {
ProjectionContext {
graph,
key,
blocks: OnceCell::new(),
}
}
fn block_index(&self) -> &BlockIndex {
self.blocks
.get_or_init(|| BlockIndex::build(self.graph, self.key))
}
}
pub fn apply_projection(ctx: &ProjectionContext<'_>, projection: &Projection) -> Mapping {
let over_defaults;
let fields: &[ProjectionField] = match projection.base {
ProjectionBase::Document => {
over_defaults = merge_over_defaults(&projection.fields);
&over_defaults
}
_ => &projection.fields,
};
let mut out = Mapping::new();
for field in fields {
let v = resolve_field(ctx, field);
out.insert(Value::String(field.output.clone()), v);
}
if projection.base != ProjectionBase::Empty {
merge_user_frontmatter(ctx, &mut out);
}
out
}
fn merge_over_defaults(fields: &[ProjectionField]) -> Vec<ProjectionField> {
let mut out = Projection::document_fields();
for f in fields {
match out.iter_mut().find(|d| d.output == f.output) {
Some(existing) => *existing = f.clone(),
None => out.push(f.clone()),
}
}
out
}
fn merge_user_frontmatter(ctx: &ProjectionContext<'_>, out: &mut Mapping) {
let Some(mut fm) = ctx.graph.frontmatter(ctx.key).cloned() else {
return;
};
strip_reserved(&mut fm);
for (k, v) in fm {
if !out.contains_key(&k) {
out.insert(k, v);
} else if let Some(s) = k.as_str() {
if matches!(s, "key" | "title") {
out.insert(k, v);
}
}
}
}
fn resolve_field(ctx: &ProjectionContext<'_>, field: &ProjectionField) -> Value {
match &field.source {
ProjectionSource::Pseudo(p) => resolve_pseudo(ctx, *p),
ProjectionSource::Frontmatter(path) => resolve_frontmatter(ctx, path),
ProjectionSource::ContentBlocks(pred) => {
Value::String(ctx.block_index().render_content(pred))
}
ProjectionSource::Blocks(pred) => Value::Sequence(
ctx.block_index()
.blocks_entries(pred)
.into_iter()
.map(Value::Mapping)
.collect(),
),
ProjectionSource::Matches(source) => Value::Sequence(
ctx.block_index()
.matches_entries(source)
.into_iter()
.map(Value::Mapping)
.collect(),
),
}
}
fn resolve_pseudo(ctx: &ProjectionContext<'_>, p: PseudoField) -> Value {
match p {
PseudoField::Key => Value::String(ctx.key.to_string()),
PseudoField::Title => Value::String(
ctx.graph
.get_key_title(ctx.key)
.unwrap_or_else(|| ctx.key.to_string()),
),
PseudoField::TitleSlug => {
let title = ctx
.graph
.get_key_title(ctx.key)
.unwrap_or_else(|| ctx.key.to_string());
Value::String(slugify(&title))
}
PseudoField::Content => Value::String(ctx.graph.to_markdown_skip_frontmatter(ctx.key)),
PseudoField::Frontmatter => {
let mut fm = ctx.graph.frontmatter(ctx.key).cloned().unwrap_or_default();
strip_reserved(&mut fm);
Value::Mapping(fm)
}
PseudoField::IncludedBy => {
edges_to_value(crate::query::edges::included_by(ctx.graph, ctx.key))
}
PseudoField::Includes => edges_to_value(crate::query::edges::includes(ctx.graph, ctx.key)),
PseudoField::ReferencedBy => {
edges_to_value(crate::query::edges::referenced_by(ctx.graph, ctx.key))
}
PseudoField::References => {
edges_to_value(crate::query::edges::references(ctx.graph, ctx.key))
}
}
}
fn resolve_frontmatter(ctx: &ProjectionContext<'_>, path: &FieldPath) -> Value {
let Some(mut fm) = ctx.graph.frontmatter(ctx.key).cloned() else {
return Value::Null;
};
strip_reserved(&mut fm);
let mut current = Value::Mapping(fm);
for segment in &path.0 {
if is_reserved_segment(segment) {
return Value::Null;
}
match current {
Value::Mapping(m) => match m.get(Value::String(segment.clone())) {
Some(v) => current = v.clone(),
None => return Value::Null,
},
_ => return Value::Null,
}
}
current
}
fn edges_to_value(edges: Vec<EdgeRef>) -> Value {
serde_yaml::to_value(&edges).unwrap_or(Value::Sequence(Vec::new()))
}
fn slugify(s: &str) -> String {
let mut out = String::new();
let mut prev_dash = true;
for c in s.chars() {
let lc = c.to_ascii_lowercase();
if lc.is_ascii_alphanumeric() {
out.push(lc);
prev_dash = false;
} else if !prev_dash {
out.push('-');
prev_dash = true;
}
}
while out.ends_with('-') {
out.pop();
}
out
}