use crate::script::support::{into_option_string, into_vec_of_strings};
use crate::script::{DEFAULT_MARKERS, LuaValueExt};
use crate::support::html::decode_html_entities;
use crate::support::text::{self, EnsureOptions, LineBlockIter, LineBlockIterOptions, truncate_with_ellipsis};
use crate::types::Extrude;
use mlua::{FromLua, IntoLua, Lua, MultiValue, Table, Value};
pub fn replace_markers_with_default_parkers(
lua: &Lua,
(content_val, new_sections_val): (Value, Value),
) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.replace_markers")? else {
return Ok(Value::Nil);
};
let sections = into_vec_of_strings(new_sections_val, "new_sections")?;
let sections: Vec<&str> = sections.iter().map(|s| s.as_str()).collect();
let new_content = text::replace_markers(&content, §ions, DEFAULT_MARKERS)?;
lua.create_string(&new_content).map(Value::String)
}
pub fn aip_truncate(
lua: &Lua,
(content_val, max_len, ellipsis): (Value, usize, Option<String>),
) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.truncate")? else {
return Ok(Value::Nil);
};
let ellipsis_str = ellipsis.unwrap_or_default();
let res_cow = truncate_with_ellipsis(&content, max_len, &ellipsis_str);
lua.create_string(res_cow.as_ref() as &str).map(Value::String)
}
impl FromLua for EnsureOptions {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
let table = value.as_table().ok_or_else(|| {
mlua::Error::runtime(
"Ensure argument needs to be a table with the format {start = string, end = string} (both optional",
)
})?;
let prefix = table.get::<String>("prefix").ok();
let suffix = table.get::<String>("suffix").ok();
for (key, _value) in table.pairs::<Value, Value>().flatten() {
if let Some(k) = key.x_as_lua_str()
&& k != "prefix"
&& k != "suffix"
{
let msg = format!(
"Ensure argument contains invalid table property `{k}`. Can only contain `prefix` and/or `suffix`"
);
return Err(mlua::Error::RuntimeError(msg));
}
}
Ok(EnsureOptions { prefix, suffix })
}
}
pub fn ensure(lua: &Lua, (content_val, inst_val): (Value, Value)) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.ensure")? else {
return Ok(Value::Nil);
};
let inst = EnsureOptions::from_lua(inst_val, lua)?;
let res = crate::support::text::ensure(&content, inst);
let res = res.to_string();
res.into_lua(lua)
}
pub fn ensure_single_trailing_newline(lua: &Lua, content_val: Value) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.ensure_single_trailing_newline")? else {
return Ok(Value::Nil);
};
let res = crate::support::text::ensure_single_trailing_newline(content);
lua.create_string(&res).map(Value::String)
}
pub fn remove_first_line(lua: &Lua, content_val: Value) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.remove_first_line")? else {
return Ok(Value::Nil);
};
let res = remove_first_lines_impl(&content, 1).to_string();
lua.create_string(&res).map(Value::String)
}
pub fn remove_first_lines(lua: &Lua, (content_val, num_of_lines): (Value, i64)) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.remove_first_lines")? else {
return Ok(Value::Nil);
};
let res = remove_first_lines_impl(&content, num_of_lines as usize).to_string();
lua.create_string(&res).map(Value::String)
}
pub fn remove_first_lines_impl(content: &str, num_of_lines: usize) -> &str {
let mut start_idx = 0;
let mut newline_count = 0;
for (i, c) in content.char_indices() {
if c == '\n' {
newline_count += 1;
if newline_count == num_of_lines {
start_idx = i + 1;
break;
}
}
}
if newline_count < num_of_lines {
return "";
}
&content[start_idx..]
}
pub fn remove_last_line(lua: &Lua, content_val: Value) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.remove_last_line")? else {
return Ok(Value::Nil);
};
let res = remove_last_lines_impl(&content, 1).to_string();
lua.create_string(&res).map(Value::String)
}
pub fn remove_last_lines(lua: &Lua, (content_val, num_of_lines): (Value, i64)) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.remove_last_lines")? else {
return Ok(Value::Nil);
};
let res = remove_last_lines_impl(&content, num_of_lines as usize).to_string();
lua.create_string(&res).map(Value::String)
}
pub fn remove_last_lines_impl(content: &str, num_of_lines: usize) -> &str {
let mut end_idx = content.len();
let mut newline_count = 0;
for (i, c) in content.char_indices().rev() {
if c == '\n' {
newline_count += 1;
if newline_count == num_of_lines {
end_idx = i;
break;
}
}
}
if newline_count < num_of_lines {
return "";
}
&content[..end_idx]
}
pub fn escape_decode_if_needed(lua: &Lua, content_val: Value) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.escape_decode_if_needed")? else {
return Ok(Value::Nil);
};
if !content.contains("<") {
lua.create_string(&content).map(Value::String)
} else {
let res = decode_html_entities(&content);
lua.create_string(&res).map(Value::String)
}
}
pub fn escape_decode(lua: &Lua, content_val: Value) -> mlua::Result<Value> {
let Some(content) = into_option_string(content_val, "aip.text.escape_decode")? else {
return Ok(Value::Nil);
};
let res = decode_html_entities(&content);
lua.create_string(&res).map(Value::String)
}
pub fn extract_line_blocks(lua: &Lua, (content_val, options): (Value, Table)) -> mlua::Result<MultiValue> {
let Some(content) = into_option_string(content_val, "aip.text.extract_line_blocks")? else {
return Ok(MultiValue::from_vec(vec![Value::Nil, Value::Nil]));
};
let starts_with: Option<String> = options.get("starts_with")?;
let Some(starts_with_str) = starts_with else {
return Err(crate::Error::custom(
r#"aip.text.extract_line_blocks requires to options with {starts_with = ".."} "#,
)
.into());
};
let extrude_param: Option<String> = options.get("extrude").ok();
let return_extrude = matches!(extrude_param.as_deref(), Some("content"));
let first_opt: Option<i64> = options.get("first").ok();
let first_count: Option<usize> = first_opt.map(|n| n as usize);
let iter_options = LineBlockIterOptions {
starts_with: &starts_with_str,
extrude: if return_extrude { Some(Extrude::Content) } else { None },
};
let mut iterator = LineBlockIter::new(content.as_str(), iter_options);
let (blocks, extruded_content) = if let Some(n) = first_count {
let mut limited_blocks = Vec::new();
for _ in 0..n {
if let Some(block) = iterator.next() {
limited_blocks.push(block);
} else {
break;
}
}
let remains = if return_extrude {
let (_ignored, extruded) = iterator.collect_remains();
extruded
} else {
String::new()
};
(limited_blocks, remains)
} else {
iterator.collect_blocks_and_extruded_content()
};
let blocks_table = lua.create_table()?;
for block in blocks.iter() {
blocks_table.push(block.as_str())?;
}
let extruded_value = if return_extrude {
Value::String(lua.create_string(&extruded_content)?)
} else {
Value::Nil
};
Ok(MultiValue::from_vec(vec![Value::Table(blocks_table), extruded_value]))
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{assert_contains, eval_lua, setup_lua};
use crate::script::aip_modules::aip_text;
use value_ext::JsonValueExt as _;
#[tokio::test]
async fn test_lua_text_replace_markers_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.replace_markers(nil, {"new1", "new2"})"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_truncate_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.truncate(nil, 10, "...")"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_remove_first_line_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.remove_first_line(nil)"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_remove_first_lines_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.remove_first_lines(nil, 2)"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_remove_last_line_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.remove_last_line(nil)"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_remove_last_lines_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.remove_last_lines(nil, 2)"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_escape_decode_if_needed_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.escape_decode_if_needed(nil)"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_escape_decode_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.escape_decode(nil)"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_extract_line_blocks_simple() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let lua_code = r#"
local content = [[
> one
> two
Some line A
> 3
The end
]]
local a, b = aip.text.extract_line_blocks(content, { starts_with = ">", extrude = "content" })
return {blocks = a, extruded = b}
"#;
let res = eval_lua(&lua, lua_code)?;
let block = res.x_get_str("/blocks/0")?;
assert_eq!(block, "> one\n> two\n");
let block = res.x_get_str("/blocks/1")?;
assert_eq!(block, "> 3\n");
let content = res.x_get_str("/extruded")?;
assert_contains(content, "Some line A");
assert_contains(content, "The end");
Ok(())
}
#[tokio::test]
async fn test_lua_text_extract_line_blocks_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"
local blocks, extruded = aip.text.extract_line_blocks(nil, { starts_with = ">", extrude = "content" })
return {blocks, blocks}
"#;
let res = eval_lua(&lua, script)?;
let res = res.as_object().ok_or("Should be object")?;
assert!(res.is_empty(), "Should be empty");
Ok(())
}
#[tokio::test]
async fn test_lua_text_extract_line_blocks_with_first_extrude() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let lua_code = r#"
local content = [[
> one
> two
line1
> three
line2
> four
line3
]]
local a, b = aip.text.extract_line_blocks(content, { starts_with = ">", extrude = "content", first = 2 })
return { blocks = a, extruded = b }
"#;
let res = eval_lua(&lua, lua_code)?;
let block1 = res.x_get_str("/blocks/0")?;
assert_eq!(block1, "> one\n> two\n");
let block2 = res.x_get_str("/blocks/1")?;
assert_eq!(block2, "> three\n");
let extruded = res.x_get_str("/extruded")?;
assert_eq!(extruded, "line1\nline2\n> four\nline3\n");
Ok(())
}
#[tokio::test]
async fn test_lua_text_extract_line_blocks_with_first_no_extrude() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let lua_code = r#"
local content = [[
> one
> two
line1
> three
line2
> four
line3
]]
local a, b = aip.text.extract_line_blocks(content, { starts_with = ">", first = 2 })
return { blocks = a, extruded = b }
"#;
let res = eval_lua(&lua, lua_code)?;
let blocks = res
.get("blocks")
.ok_or("Should have blocks")?
.as_array()
.ok_or("Should be array")?;
assert_eq!(blocks.len(), 2, "should have only 2 blocks");
assert_eq!(blocks[0].as_str().ok_or("Should be str")?, "> one\n> two\n");
assert_eq!(blocks[1].as_str().ok_or("Should be str")?, "> three\n");
let extruded_val = res.get("extruded");
assert!(
extruded_val.is_none(),
"extruded should be nil when extrude option is not set"
);
Ok(())
}
#[tokio::test]
async fn test_lua_text_ensure_simple() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let data = [
(
"some- ! -path",
r#"{prefix = "./", suffix = ".md"}"#,
"./some- ! -path.md",
),
("some- ! -path", r#"{suffix = ".md"}"#, "some- ! -path.md"),
(" ~ some- ! -path", r#"{prefix = " ~ "}"#, " ~ some- ! -path"),
("~ some- ! -path", r#"{prefix = " ~ "}"#, " ~ ~ some- ! -path"),
];
for (content, arg, expected) in data {
let script = format!("return aip.text.ensure(\"{content}\", {arg})");
let res = eval_lua(&lua, &script)?;
assert_eq!(res.as_str().ok_or("Should have res")?, expected);
}
Ok(())
}
#[tokio::test]
async fn test_lua_text_ensure_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.ensure(nil, {prefix = "./", suffix = ".md"})"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
#[tokio::test]
async fn test_lua_text_ensure_single_trailing_newline_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"return aip.text.ensure_single_trailing_newline(nil)"#;
let res = eval_lua(&lua, script)?;
assert!(res.is_null(), "Expected null for nil content input");
Ok(())
}
}