use serde_yaml::{Mapping, Value};
use crate::graph::Graph;
use crate::model::Key;
use crate::query::document::{
FieldPath, Projection, ProjectionField, ProjectionMode, 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,
}
pub fn apply_projection(ctx: &ProjectionContext<'_>, projection: &Projection) -> Mapping {
let mut out = Mapping::new();
let effective: Vec<&ProjectionField> = match projection.mode {
ProjectionMode::Replace => projection.fields.iter().collect(),
ProjectionMode::Extend => {
let default = Projection::default_for_find();
let mut by_name: Vec<ProjectionField> = default.fields.clone();
for f in &projection.fields {
if let Some(existing) = by_name.iter_mut().find(|d| d.output == f.output) {
*existing = f.clone();
} else {
by_name.push(f.clone());
}
}
return write_with_user_fm(ctx, &by_name);
}
};
for field in &effective {
let v = resolve_field(ctx, field);
out.insert(Value::String(field.output.clone()), v);
}
if projection.mode == ProjectionMode::Replace
&& projection.fields.iter().any(|f| {
matches!(
&f.source,
ProjectionSource::Pseudo(PseudoField::Frontmatter)
)
})
{
return out;
}
if projection.mode == ProjectionMode::Replace {
return out;
}
out
}
fn write_with_user_fm(ctx: &ProjectionContext<'_>, fields: &[ProjectionField]) -> Mapping {
let mut out = Mapping::new();
for field in fields {
let v = resolve_field(ctx, field);
out.insert(Value::String(field.output.clone()), v);
}
if let Some(mut fm) = ctx.graph.frontmatter(ctx.key).cloned() {
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);
}
}
}
}
out
}
pub fn apply_projection_or_default(
ctx: &ProjectionContext<'_>,
projection: Option<&Projection>,
) -> Mapping {
match projection {
None => write_with_user_fm(ctx, &Projection::default_for_find().fields),
Some(p) if matches!(p.mode, ProjectionMode::Extend) => apply_projection(ctx, p),
Some(p) => apply_projection(ctx, p),
}
}
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),
}
}
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
}