use crate::Result;
use crate::runtime::Runtime;
use crate::script::support::into_option_string;
use crate::support::W;
use crate::support::md::{self, MdRefIter};
use crate::types::{Extrude, MdBlock, MdRef};
use mlua::{IntoLua, Lua, LuaSerdeExt, MultiValue, Table, Value};
pub fn init_module(lua: &Lua, _runtime: &Runtime) -> Result<Table> {
let table = lua.create_table()?;
let extract_blocks_fn = lua.create_function(extract_blocks)?;
let outer_block_content_or_raw_fn = lua.create_function(outer_block_content_or_raw)?;
let extract_meta_fn = lua.create_function(extract_meta)?;
let extract_refs_fn = lua.create_function(extract_refs)?;
table.set("extract_blocks", extract_blocks_fn)?;
table.set("extract_meta", extract_meta_fn)?;
table.set("extract_refs", extract_refs_fn)?;
table.set("outer_block_content_or_raw", outer_block_content_or_raw_fn)?;
Ok(table)
}
fn extract_blocks(lua: &Lua, (md_content, options): (String, Option<Value>)) -> mlua::Result<MultiValue> {
let (lang, extrude): (Option<String>, Option<Extrude>) = match options {
Some(Value::String(string)) => (Some(string.to_string_lossy()), None),
Some(Value::Table(table)) => {
let lang = table.get::<Option<Value>>("lang")?;
let lang = lang
.map(|v| {
v.to_string()
.map_err(|_err| crate::Error::custom("md_extract_blocks lang options must be of type string"))
})
.transpose()?;
let extrude = Extrude::extract_from_table_value(&table)?;
(lang, extrude)
}
_ => (None, None),
};
let blocks_it = md::MdBlockIter::new(&md_content, lang.as_deref(), extrude);
let mut values = MultiValue::new();
match extrude {
Some(Extrude::Content) => {
let (blocks, content) = blocks_it.collect_blocks_and_extruded_content();
values.push_back(lua.to_value(&blocks)?); let content = lua.create_string(&content)?;
values.push_back(Value::String(content));
}
_ => {
let blocks: Vec<MdBlock> = blocks_it.collect();
values.push_back(lua.to_value(&blocks)?) }
}
Ok(values)
}
fn extract_meta(lua: &Lua, md_content: Value) -> mlua::Result<MultiValue> {
let Some(md_content) = into_option_string(md_content, "aip.md.extract_meta")? else {
return Ok(MultiValue::from_vec(vec![Value::Nil, Value::Nil]));
};
let (value, remain) = md::extract_meta(&md_content)?;
let lua_value = lua.to_value(&value)?;
let values = MultiValue::from_vec(vec![lua_value, W(remain).into_lua(lua)?]);
Ok(values)
}
fn outer_block_content_or_raw(_lua: &Lua, md_content: String) -> mlua::Result<String> {
let res = md::outer_block_content_or_raw(&md_content);
Ok(res.into_owned())
}
fn extract_refs(lua: &Lua, md_content: Value) -> mlua::Result<Value> {
let Some(md_content) = into_option_string(md_content, "aip.md.extract_refs")? else {
return Ok(Value::Table(lua.create_table()?));
};
let refs: Vec<MdRef> = MdRefIter::new(&md_content).collect();
let lua_value = lua.to_value(&refs)?;
Ok(lua_value)
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{assert_contains, assert_not_contains, eval_lua, run_reflective_agent, setup_lua};
use serde_json::Value;
use value_ext::JsonValueExt;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_extract_blocks_simple() -> Result<()> {
let fx_script = r#"
local file = aip.file.load("agent-script/agent-before-all-inputs-gen.aip")
return aip.md.extract_blocks(file.content, {lang = "lua"})
"#;
let res = run_reflective_agent(fx_script, None).await?;
assert!(res.is_array());
let blocks = res.as_array().ok_or("Res should be array")?;
assert_eq!(blocks.len(), 4, "Should have found 4 lua blocks");
let first_block = &blocks[0];
assert_eq!(first_block.x_get_str("lang")?, "lua");
assert!(first_block.x_get_str("content")?.contains("before_all_response"));
let second_block = &blocks[1];
assert_eq!(second_block.x_get_str("lang")?, "lua");
assert!(second_block.x_get_str("content")?.contains("Data with input"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_extract_blocks_with_lang_and_extruded_content() -> Result<()> {
let fx_script = r#"
local content = "This is some content\n"
content = content .. "\n```lua\n--some lua \n```\n"
content = content .. "and other block\n\n```rust\n//! some rust block \n```\n"
content = content .. "The end"
local blocks, extruded_content = aip.md.extract_blocks(content, {lang = "lua", extrude = "content"})
return {
blocks = blocks,
extruded_content = extruded_content
}
"#;
let res = run_reflective_agent(fx_script, None).await?;
let blocks = res.pointer("/blocks").ok_or("Should have blocks")?;
assert!(blocks.is_array());
let blocks = blocks.as_array().ok_or("Should be arr")?;
assert_eq!(blocks.len(), 1, "Should have found 1 lua blocks");
let first_block = &blocks[0];
assert_eq!(first_block.x_get_str("lang")?, "lua");
assert!(first_block.x_get_str("content")?.contains("some lua"));
let content = res.x_get_str("extruded_content")?;
assert_contains(content, "This is some content");
assert_contains(content, "and other block");
assert_contains(content, "```rust"); assert_contains(content, "```\n");
assert_contains(content, "The end");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_extract_blocks_with_all_lang_and_extruded_content() -> Result<()> {
let fx_script = r#"
local content = "This is some content\n"
content = content .. "\n```lua\n--some lua \n```\n"
content = content .. "and other block\n\n```rust\n//! some rust block \n```\n"
content = content .. "The end"
local blocks, extruded_content = aip.md.extract_blocks(content, {extrude = "content"})
return {
blocks = blocks,
extruded_content = extruded_content
}
"#;
let res = run_reflective_agent(fx_script, None).await?;
let blocks = res.pointer("/blocks").ok_or("Should have blocks")?;
assert!(blocks.is_array());
let blocks = blocks.as_array().ok_or("Should be arr")?;
assert_eq!(blocks.len(), 2, "Should have found 2 blocks, lua and rust");
let block1 = &blocks[0];
let block2 = &blocks[1];
if block1.x_get_str("lang")? == "lua" {
assert_eq!(block1.x_get_str("lang")?, "lua");
assert!(block1.x_get_str("content")?.contains("some lua"));
assert_eq!(block2.x_get_str("lang")?, "rust");
assert!(block2.x_get_str("content")?.contains("some rust"));
} else {
assert_eq!(block1.x_get_str("lang")?, "rust");
assert!(block1.x_get_str("content")?.contains("some rust"));
assert_eq!(block2.x_get_str("lang")?, "lua");
assert!(block2.x_get_str("content")?.contains("some lua"));
}
let content = res.x_get_str("extruded_content")?;
assert_contains(content, "This is some content");
assert_contains(content, "and other block");
assert_not_contains(content, "```lua");
assert_not_contains(content, "```rust");
assert_not_contains(content, "```"); assert_contains(content, "The end");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_extract_meta() -> Result<()> {
let lua = setup_lua(super::init_module, "md").await?;
let lua_code = r#"
local content = [[
Some content
```toml
#!meta
some = "stuff"
```
some more content
```toml
#!meta
# Another meta block
num = 123
```
And this is the end
]]
local meta, remain = aip.md.extract_meta(content)
return {
meta = meta,
remain = remain
}
"#;
let res: Value = eval_lua(&lua, lua_code)?;
let meta = res.get("meta").ok_or("Should have meta")?;
assert_eq!(meta.x_get_str("some")?, "stuff");
assert_eq!(meta.x_get_i64("num")?, 123);
let remain = res.x_get_str("remain")?;
assert_contains(remain, "Some content");
assert_contains(remain, "some more content");
assert_contains(remain, "And this is the end");
assert_not_contains(remain, "Another meta block");
assert_not_contains(remain, "num = 123");
assert_not_contains(remain, "#!meta");
assert_not_contains(remain, "```toml");
assert_not_contains(remain, "```");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_extract_refs_simple() -> Result<()> {
let lua = setup_lua(super::init_module, "md").await?;
let lua_code = r#"
local content = [[
Check out [this link](https://example.com) and [docs](docs/page.md).
Also see  for reference.
]]
return aip.md.extract_refs(content)
"#;
let res: Value = eval_lua(&lua, lua_code)?;
assert!(res.is_array());
let refs = res.as_array().ok_or("Res should be array")?;
assert_eq!(refs.len(), 3, "Should have found 3 refs");
let first = &refs[0];
assert_eq!(first.x_get_str("_type")?, "MdRef");
assert_eq!(first.x_get_str("target")?, "https://example.com");
assert_eq!(first.x_get_str("text")?, "this link");
assert_eq!(first.get("inline").and_then(|v| v.as_bool()), Some(false));
assert_eq!(first.x_get_str("kind")?, "Url");
let second = &refs[1];
assert_eq!(second.x_get_str("target")?, "docs/page.md");
assert_eq!(second.x_get_str("text")?, "docs");
assert_eq!(second.get("inline").and_then(|v| v.as_bool()), Some(false));
assert_eq!(second.x_get_str("kind")?, "File");
let third = &refs[2];
assert_eq!(third.x_get_str("target")?, "assets/photo.jpg");
assert_eq!(third.x_get_str("text")?, "image");
assert_eq!(third.get("inline").and_then(|v| v.as_bool()), Some(true));
assert_eq!(third.x_get_str("kind")?, "File");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_extract_refs_skip_code_blocks() -> Result<()> {
let lua = setup_lua(super::init_module, "md").await?;
let lua_code = r#"
local content = "Here is a [real link](https://real.com).\n"
content = content .. "\n```\n[not a link](https://fake.com)\n```\n"
content = content .. "\nAnd [another real](page.md)."
return aip.md.extract_refs(content)
"#;
let res: Value = eval_lua(&lua, lua_code)?;
assert!(res.is_array());
let refs = res.as_array().ok_or("Res should be array")?;
assert_eq!(refs.len(), 2, "Should have found 2 refs (skipping code block)");
assert_eq!(refs[0].x_get_str("target")?, "https://real.com");
assert_eq!(refs[1].x_get_str("target")?, "page.md");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_extract_refs_nil_input() -> Result<()> {
let lua = setup_lua(super::init_module, "md").await?;
let lua_code = r#"
return aip.md.extract_refs(nil)
"#;
let res: Value = eval_lua(&lua, lua_code)?;
assert!(res.is_array());
let refs = res.as_array().ok_or("Res should be array")?;
assert_eq!(refs.len(), 0, "Should return empty list for nil input");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_extract_refs_anchor() -> Result<()> {
let lua = setup_lua(super::init_module, "md").await?;
let lua_code = r#"
local content = [[
[go to section](#my-section)
]]
return aip.md.extract_refs(content)
"#;
let res: Value = eval_lua(&lua, lua_code)?;
assert!(res.is_array());
let refs = res.as_array().ok_or("Res should be array")?;
assert_eq!(refs.len(), 1, "Should have found 1 ref");
assert_eq!(refs[0].x_get_str("target")?, "#my-section");
assert_eq!(refs[0].x_get_str("kind")?, "Anchor");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lua_md_outer_block_content_or_raw() -> Result<()> {
let fx_script = r#"
local content = "```" .. [[rust
fn main() {
// Some nested blocks
let example = ```typescript
const x = 42;
```;
println!("Hello!");
}
]] .. "```"
return aip.md.outer_block_content_or_raw(content)
"#;
let res = run_reflective_agent(fx_script, None).await?;
let content = res.as_str().ok_or("Should have res")?;
assert!(content.contains("fn main()"));
assert!(content.contains("const x = 42"));
assert!(!content.contains("```rust")); assert!(content.contains("```typescript"));
let fx_script_raw = r#"
local content = [[Just some plain
text without any code blocks]]
return aip.md.outer_block_content_or_raw(content)
"#;
let res_raw = run_reflective_agent(fx_script_raw, None).await?;
let content_raw = res_raw.as_str().ok_or("Should have res")?;
assert_eq!(content_raw, "Just some plain\ntext without any code blocks");
Ok(())
}
}