use std::path::Path;
use mlua::{Function, Lua, Result as LuaResult, Table, Value};
use super::vault_context::VaultContext;
use crate::index::NoteQuery;
use crate::types::validation::yaml_to_lua_table;
pub fn register_index_bindings(lua: &Lua) -> LuaResult<()> {
let mdv: Table = lua.globals().get("mdv")?;
mdv.set("current_note", create_current_note_fn(lua)?)?;
mdv.set("backlinks", create_backlinks_fn(lua)?)?;
mdv.set("outlinks", create_outlinks_fn(lua)?)?;
mdv.set("query", create_query_fn(lua)?)?;
mdv.set("find_project", create_find_project_fn(lua)?)?;
Ok(())
}
fn create_current_note_fn(lua: &Lua) -> LuaResult<Function> {
lua.create_function(|lua, ()| {
let ctx = lua
.app_data_ref::<VaultContext>()
.ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
let current = match &ctx.current_note {
Some(note) => note,
None => return Ok(Value::Nil),
};
let note_table = lua.create_table()?;
note_table.set("path", current.path.as_str())?;
note_table.set("type", current.note_type.as_str())?;
note_table.set("content", current.content.as_str())?;
if let Some(title) = ¤t.title {
note_table.set("title", title.as_str())?;
}
if let Some(fm) = ¤t.frontmatter {
let fm_table = yaml_to_lua_table(lua, fm)?;
note_table.set("frontmatter", fm_table)?;
}
Ok(Value::Table(note_table))
})
}
fn create_backlinks_fn(lua: &Lua) -> LuaResult<Function> {
lua.create_function(|lua, path: String| {
let ctx = lua
.app_data_ref::<VaultContext>()
.ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
let db = match &ctx.index_db {
Some(db) => db,
None => {
return Err(mlua::Error::runtime(
"Index database not available. Run 'mdv reindex' first.",
));
}
};
let resolved_path = resolve_note_path(&ctx.vault_root, &path);
let note = match db.get_note_by_path(Path::new(&resolved_path)) {
Ok(Some(n)) => n,
Ok(None) => {
return Ok(Value::Table(lua.create_table()?));
}
Err(e) => return Err(mlua::Error::runtime(format!("Index error: {}", e))),
};
let note_id = match note.id {
Some(id) => id,
None => return Ok(Value::Table(lua.create_table()?)),
};
let backlinks = db
.get_backlinks(note_id)
.map_err(|e| mlua::Error::runtime(format!("Index error: {}", e)))?;
let result = lua.create_table()?;
for (i, link) in backlinks.iter().enumerate() {
let link_table = lua.create_table()?;
if let Ok(Some(source_note)) = db.get_note_by_id(link.source_id) {
link_table
.set("source_path", source_note.path.to_string_lossy().to_string())?;
link_table.set("source_title", source_note.title)?;
link_table.set("source_type", source_note.note_type.as_str())?;
}
if let Some(text) = &link.link_text {
link_table.set("link_text", text.as_str())?;
}
if let Some(context) = &link.context {
link_table.set("context", context.as_str())?;
}
link_table.set("link_type", link.link_type.as_str())?;
result.set(i + 1, link_table)?;
}
Ok(Value::Table(result))
})
}
fn create_outlinks_fn(lua: &Lua) -> LuaResult<Function> {
lua.create_function(|lua, path: String| {
let ctx = lua
.app_data_ref::<VaultContext>()
.ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
let db = match &ctx.index_db {
Some(db) => db,
None => {
return Err(mlua::Error::runtime(
"Index database not available. Run 'mdv reindex' first.",
));
}
};
let resolved_path = resolve_note_path(&ctx.vault_root, &path);
let note = match db.get_note_by_path(Path::new(&resolved_path)) {
Ok(Some(n)) => n,
Ok(None) => {
return Ok(Value::Table(lua.create_table()?));
}
Err(e) => return Err(mlua::Error::runtime(format!("Index error: {}", e))),
};
let note_id = match note.id {
Some(id) => id,
None => return Ok(Value::Table(lua.create_table()?)),
};
let outlinks = db
.get_outgoing_links(note_id)
.map_err(|e| mlua::Error::runtime(format!("Index error: {}", e)))?;
let result = lua.create_table()?;
for (i, link) in outlinks.iter().enumerate() {
let link_table = lua.create_table()?;
link_table.set("target_path", link.target_path.as_str())?;
if let Some(target_id) = link.target_id {
if let Ok(Some(target_note)) = db.get_note_by_id(target_id) {
link_table.set("target_title", target_note.title)?;
link_table.set("target_type", target_note.note_type.as_str())?;
link_table.set("resolved", true)?;
} else {
link_table.set("resolved", false)?;
}
} else {
link_table.set("resolved", false)?;
}
if let Some(text) = &link.link_text {
link_table.set("link_text", text.as_str())?;
}
link_table.set("link_type", link.link_type.as_str())?;
result.set(i + 1, link_table)?;
}
Ok(Value::Table(result))
})
}
fn create_query_fn(lua: &Lua) -> LuaResult<Function> {
lua.create_function(|lua, opts: Option<Table>| {
let ctx = lua
.app_data_ref::<VaultContext>()
.ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
let db = match &ctx.index_db {
Some(db) => db,
None => {
return Err(mlua::Error::runtime(
"Index database not available. Run 'mdv reindex' first.",
));
}
};
let mut query = NoteQuery::default();
if let Some(opts) = opts {
if let Ok(type_str) = opts.get::<String>("type") {
query.note_type = Some(type_str.parse().unwrap_or_default());
}
if let Ok(prefix) = opts.get::<String>("path_prefix") {
query.path_prefix = Some(std::path::PathBuf::from(prefix));
}
if let Ok(limit) = opts.get::<i64>("limit") {
query.limit = Some(limit as u32);
}
if let Ok(offset) = opts.get::<i64>("offset") {
query.offset = Some(offset as u32);
}
}
let notes = db
.query_notes(&query)
.map_err(|e| mlua::Error::runtime(format!("Query error: {}", e)))?;
let result = lua.create_table()?;
for (i, note) in notes.iter().enumerate() {
let note_table = lua.create_table()?;
note_table.set("path", note.path.to_string_lossy().to_string())?;
note_table.set("type", note.note_type.as_str())?;
note_table.set("title", note.title.clone())?;
note_table.set("modified", note.modified.to_rfc3339())?;
if let Some(created) = note.created {
note_table.set("created", created.to_rfc3339())?;
}
if let Some(fm_json) = ¬e.frontmatter_json
&& let Ok(fm) = serde_json::from_str::<serde_json::Value>(fm_json)
{
let fm_yaml = json_to_yaml(&fm);
let fm_lua = yaml_to_lua_table(lua, &fm_yaml)?;
note_table.set("frontmatter", fm_lua)?;
}
result.set(i + 1, note_table)?;
}
Ok(Value::Table(result))
})
}
#[allow(clippy::collapsible_if)]
fn create_find_project_fn(lua: &Lua) -> LuaResult<Function> {
lua.create_function(|lua, id: String| {
let ctx = lua
.app_data_ref::<VaultContext>()
.ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
let db = match &ctx.index_db {
Some(db) => db,
None => {
return Err(mlua::Error::runtime(
"Index database not available. Run 'mdv reindex' first.",
));
}
};
let query = NoteQuery {
note_type: Some(crate::index::NoteType::Project),
..Default::default()
};
let notes = db
.query_notes(&query)
.map_err(|e| mlua::Error::runtime(format!("Query error: {}", e)))?;
for note in notes {
if let Some(fm_json) = ¬e.frontmatter_json {
if let Ok(fm) = serde_json::from_str::<serde_json::Value>(fm_json) {
if fm.get("project-id").and_then(|v| v.as_str()) == Some(id.as_str())
{
let note_table = lua.create_table()?;
note_table
.set("path", note.path.to_string_lossy().to_string())?;
note_table.set("type", note.note_type.as_str())?;
note_table.set("title", note.title.clone())?;
note_table.set("modified", note.modified.to_rfc3339())?;
if let Some(created) = note.created {
note_table.set("created", created.to_rfc3339())?;
}
let fm_yaml = json_to_yaml(&fm);
let fm_lua = yaml_to_lua_table(lua, &fm_yaml)?;
note_table.set("frontmatter", fm_lua)?;
return Ok(Value::Table(note_table));
}
}
}
}
Ok(Value::Nil)
})
}
fn resolve_note_path(_vault_root: &std::path::Path, path: &str) -> String {
if path.ends_with(".md") { path.to_string() } else { format!("{}.md", path) }
}
fn json_to_yaml(json: &serde_json::Value) -> serde_yaml::Value {
match json {
serde_json::Value::Null => serde_yaml::Value::Null,
serde_json::Value::Bool(b) => serde_yaml::Value::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
serde_yaml::Value::Number(i.into())
} else if let Some(f) = n.as_f64() {
serde_yaml::Value::Number(serde_yaml::Number::from(f))
} else {
serde_yaml::Value::Null
}
}
serde_json::Value::String(s) => serde_yaml::Value::String(s.clone()),
serde_json::Value::Array(arr) => {
serde_yaml::Value::Sequence(arr.iter().map(json_to_yaml).collect())
}
serde_json::Value::Object(obj) => {
let mut map = serde_yaml::Mapping::new();
for (k, v) in obj {
map.insert(serde_yaml::Value::String(k.clone()), json_to_yaml(v));
}
serde_yaml::Value::Mapping(map)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_note_path_with_extension() {
let vault_root = std::path::Path::new("/vault");
let result = resolve_note_path(vault_root, "notes/test.md");
assert_eq!(result, "notes/test.md");
}
#[test]
fn test_resolve_note_path_without_extension() {
let vault_root = std::path::Path::new("/vault");
let result = resolve_note_path(vault_root, "notes/test");
assert_eq!(result, "notes/test.md");
}
#[test]
fn test_json_to_yaml() {
let json = serde_json::json!({
"string": "value",
"number": 42,
"bool": true,
"array": [1, 2, 3]
});
let yaml = json_to_yaml(&json);
if let serde_yaml::Value::Mapping(map) = yaml {
assert!(map.contains_key(serde_yaml::Value::String("string".into())));
assert!(map.contains_key(serde_yaml::Value::String("number".into())));
} else {
panic!("Expected mapping");
}
}
}