use crate::script::support::into_option_string;
use mlua::{Lua, MultiValue, String as LuaString, Value};
pub fn split_first_line(lua: &Lua, (content_val, sep_lua_str): (Value, LuaString)) -> mlua::Result<MultiValue> {
let Some(content) = into_option_string(content_val, "aip.text.split_first_line")? else {
return Ok(MultiValue::from_vec(vec![Value::Nil, Value::Nil]));
};
let sep_str = sep_lua_str.to_str()?;
split_once_line(lua, content, &sep_str, true)
}
pub fn split_last_line(lua: &Lua, (content_val, sep_lua_str): (Value, LuaString)) -> mlua::Result<MultiValue> {
let Some(content) = into_option_string(content_val, "aip.text.split_last_line")? else {
return Ok(MultiValue::from_vec(vec![Value::Nil, Value::Nil]));
};
let sep_str = sep_lua_str.to_str()?;
split_once_line(lua, content, &sep_str, false)
}
fn split_once_line(lua: &Lua, content: String, sep: &str, find_first: bool) -> mlua::Result<MultiValue> {
let mut match_info: Option<(usize, usize)> = None;
let mut current_byte_offset = 0;
for line_str in content.lines() {
let line_content_start_idx = current_byte_offset;
let line_content_end_idx = line_content_start_idx + line_str.len();
if line_str == sep {
match_info = Some((line_content_start_idx, line_content_end_idx));
if find_first {
break;
}
}
current_byte_offset = line_content_end_idx + 1;
}
if let Some((sep_line_content_start_idx, sep_line_content_end_idx)) = match_info {
let first_part_str = if sep_line_content_start_idx == 0 {
""
} else {
&content[0..sep_line_content_start_idx.saturating_sub(1)]
};
let second_part_start_idx = sep_line_content_end_idx + 1;
let second_part_str = if second_part_start_idx > content.len() {
""
} else {
&content[second_part_start_idx..]
};
Ok(MultiValue::from_vec(vec![
Value::String(lua.create_string(first_part_str)?),
Value::String(lua.create_string(second_part_str)?),
]))
} else {
Ok(MultiValue::from_vec(vec![
Value::String(lua.create_string(&content)?),
Value::Nil,
]))
}
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{eval_lua, setup_lua};
use crate::script::aip_modules::aip_text;
use serde_json::Value as JsonValue;
fn get_returned_parts(res: JsonValue) -> Result<(Option<String>, Option<String>)> {
let arr = res.as_array().ok_or("Result should be an array")?;
let first_json = arr.first().ok_or("Missing first part")?;
let second = arr
.get(1)
.map(|v| v.as_str().map(|s| s.to_string()).ok_or("Should be string"))
.transpose()?;
let first = if first_json.is_null() {
None
} else {
Some(first_json.as_str().ok_or("First part not string")?.to_string())
};
Ok((first, second))
}
#[tokio::test]
async fn test_lua_text_split_first_line_simple() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let test_cases = vec![
("line1\nSEP\nline2", "SEP", Some("line1"), Some("line2")),
("SEP\nline2", "SEP", Some(""), Some("line2")),
("line1\nSEP", "SEP", Some("line1"), Some("")),
("SEP", "SEP", Some(""), Some("")),
("line1\nNO_MATCH\nline2", "SEP", Some("line1\nNO_MATCH\nline2"), None),
(
"line1\nSEP\nline2\nSEP\nline3",
"SEP",
Some("line1"),
Some("line2\nSEP\nline3"),
),
("", "SEP", Some(""), None), ("line1\n\nline2", "", Some("line1"), Some("line2")), ("\nline2", "", Some(""), Some("line2")), ("line1\n", "", Some("line1\n"), None), ("text without newlines", "SEP", Some("text without newlines"), None),
("SEP_LINE_NO_NEWLINE_END", "SEP_LINE_NO_NEWLINE_END", Some(""), Some("")),
];
for (content_str, sep_str, exp_first_opt, exp_second_opt) in test_cases {
let script = format!(
r#"
local content = {content_str:?}
local sep = {sep_str:?}
local first, second = aip.text.split_first_line(content, sep)
return {{first, second}}
"#,
);
let res_json = eval_lua(&lua, &script)?;
let (first, second) = get_returned_parts(res_json)?;
assert_eq!(
first,
exp_first_opt.map(String::from),
"Content: {content_str}, Sep: {sep_str}"
);
assert_eq!(
second,
exp_second_opt.map(String::from),
"Content: {content_str}, Sep: {sep_str}"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_text_split_first_line_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"
local first, second = aip.text.split_first_line(nil, "SEP")
return {first, second}
"#;
let res_json = eval_lua(&lua, script)?;
assert!(
res_json.as_object().ok_or("Should be object")?.is_empty(),
"Should be empty"
);
Ok(())
}
#[tokio::test]
async fn test_lua_text_split_last_line_simple() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let test_cases = vec![
("line1\nSEP\nline2", "SEP", Some("line1"), Some("line2")),
("SEP\nline2", "SEP", Some(""), Some("line2")),
("line1\nSEP", "SEP", Some("line1"), Some("")),
("SEP", "SEP", Some(""), Some("")),
("line1\nNO_MATCH\nline2", "SEP", Some("line1\nNO_MATCH\nline2"), None),
(
"line1\nSEP\nline2\nSEP\nline3",
"SEP",
Some("line1\nSEP\nline2"),
Some("line3"),
),
("SEP\nmiddle\nSEP", "SEP", Some("SEP\nmiddle"), Some("")),
("", "SEP", Some(""), None), ("line1\n\nline2", "", Some("line1"), Some("line2")), ("\nline2", "", Some(""), Some("line2")),
("line1\n", "", Some("line1\n"), None),
("text without newlines", "SEP", Some("text without newlines"), None),
("SEP_LINE_NO_NEWLINE_END", "SEP_LINE_NO_NEWLINE_END", Some(""), Some("")),
];
for (content_str, sep_str, exp_first_opt, exp_second_opt) in test_cases {
let script = format!(
r#"
local content = {content_str:?}
local sep = {sep_str:?}
local first, second = aip.text.split_last_line(content, sep)
return {{first, second}}
"#,
);
let res_json = eval_lua(&lua, &script)?;
let (first, second) = get_returned_parts(res_json)?;
assert_eq!(
first,
exp_first_opt.map(String::from),
"Content: {content_str:?}, Sep: {sep_str:?}"
);
assert_eq!(
second,
exp_second_opt.map(String::from),
"Content: {content_str:?}, Sep: {sep_str:?}"
);
}
Ok(())
}
#[tokio::test]
async fn test_lua_text_split_last_line_nil_content() -> Result<()> {
let lua = setup_lua(aip_text::init_module, "text").await?;
let script = r#"
local first, second = aip.text.split_last_line(nil, "SEP")
return {first, second}
"#;
let res_json = eval_lua(&lua, script)?;
assert!(
res_json.as_object().ok_or("Should be object")?.is_empty(),
"Should be empty"
);
Ok(())
}
}