pub fn replace_region(text: &str, tag: &str, body: &str) -> Option<String> {
let begin = format!("// day:{tag}-begin");
let end = format!("// day:{tag}-end");
let b = text.find(&begin)?;
let e = text.find(&end)?;
if e < b {
return None;
}
let after_begin = text[b..].find('\n').map(|p| b + p + 1)?;
let end_line_start = text[..e].rfind('\n').map(|p| p + 1).unwrap_or(0);
let mut out = String::with_capacity(text.len() + body.len());
out.push_str(&text[..after_begin]);
out.push_str(body);
out.push_str(&text[end_line_start..]);
Some(out)
}
pub fn ensure_region(text: &str, key: &str, tag: &str) -> Result<String, String> {
let begin = format!("// day:{tag}-begin");
if text.contains(&begin) {
return Ok(text.to_string());
}
let needle = format!("\"{key}\"");
let k = text
.find(&needle)
.ok_or_else(|| format!("module.json5 has no {needle} array"))?;
let open = text[k..]
.find('[')
.map(|p| k + p)
.ok_or_else(|| format!("{needle} is not an array"))?;
let close = array_end(text, open + 1).ok_or_else(|| format!("{needle} array is not closed"))?;
let prior = last_significant(&text[open + 1..close]);
let comma = matches!(prior, Some(c) if c != ',');
let indent = indent_of(text, close);
let entry_indent = format!("{indent} ");
let mut block = String::new();
if comma {
block.push(',');
}
block.push('\n');
block.push_str(&format!(
"{entry_indent}// day:{tag}-begin — generated by `day build` from [permissions] in \
Day.toml.\n{entry_indent}// Everything between these markers is rewritten every build; \
edit Day.toml, not here.\n{entry_indent}// day:{tag}-end\n{indent}"
));
let mut out = String::with_capacity(text.len() + block.len());
out.push_str(&text[..close]);
while out.ends_with(' ') || out.ends_with('\n') || out.ends_with('\t') {
out.pop();
}
out.push_str(&block);
out.push_str(&text[close..]);
Ok(out)
}
fn array_end(text: &str, from: usize) -> Option<usize> {
let b = text.as_bytes();
let mut i = from;
let mut depth = 1usize;
while i < b.len() {
match b[i] {
b'"' | b'\'' => {
let quote = b[i];
i += 1;
while i < b.len() && b[i] != quote {
if b[i] == b'\\' {
i += 1;
}
i += 1;
}
}
b'/' if i + 1 < b.len() && b[i + 1] == b'/' => {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
}
b'/' if i + 1 < b.len() && b[i + 1] == b'*' => {
i += 2;
while i + 1 < b.len() && !(b[i] == b'*' && b[i + 1] == b'/') {
i += 1;
}
i += 1;
}
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None
}
fn last_significant(s: &str) -> Option<char> {
let mut out = None;
let b = s.as_bytes();
let mut i = 0usize;
while i < b.len() {
match b[i] {
b'/' if i + 1 < b.len() && b[i + 1] == b'/' => {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
}
b'/' if i + 1 < b.len() && b[i + 1] == b'*' => {
i += 2;
while i + 1 < b.len() && !(b[i] == b'*' && b[i + 1] == b'/') {
i += 1;
}
i += 1;
}
c if !c.is_ascii_whitespace() => out = Some(c as char),
_ => {}
}
i += 1;
}
out
}
fn indent_of(text: &str, pos: usize) -> String {
let line_start = text[..pos].rfind('\n').map(|p| p + 1).unwrap_or(0);
text[line_start..pos]
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
const MODULE: &str = include_str!("../templates/app/platform/ohos/entry/src/main/module.json5");
#[test]
fn inserts_a_region_then_replaces_it() {
let once = ensure_region(MODULE, "requestPermissions", "permissions").expect("insert");
assert!(once.contains("// day:permissions-begin"));
assert!(once.contains("// day:permissions-end"));
assert!(once.contains("\"ohos.permission.INTERNET\""));
assert!(once.contains("Required even for the LOOPBACK dayscript engine socket"));
assert_eq!(
ensure_region(&once, "requestPermissions", "permissions").unwrap(),
once
);
let filled = replace_region(
&once,
"permissions",
" { \"name\": \"ohos.permission.CAMERA\" },\n",
)
.expect("replace");
assert!(filled.contains("ohos.permission.CAMERA"));
assert!(filled.contains("\"ohos.permission.INTERNET\""));
assert_eq!(
replace_region(
&filled,
"permissions",
" { \"name\": \"ohos.permission.CAMERA\" },\n"
)
.unwrap(),
filled
);
let changed = replace_region(&filled, "permissions", "").unwrap();
assert!(!changed.contains("ohos.permission.CAMERA"));
assert!(changed.contains("\"ohos.permission.INTERNET\""));
}
#[test]
fn replace_reports_missing_markers() {
assert!(replace_region(MODULE, "permissions", "x").is_none());
}
#[test]
fn array_end_skips_strings_and_comments() {
let s = r#"["a]b", // ] not this
/* ] nor this */ "c"]after"#;
let end = array_end(s, 1).expect("end");
assert_eq!(&s[end..end + 1], "]");
assert_eq!(&s[end..], "]after");
}
#[test]
fn adds_a_comma_only_when_needed() {
let with_entries = "{\n \"requestPermissions\": [\n { \"name\": \"a\" }\n ]\n}\n";
let out = ensure_region(with_entries, "requestPermissions", "permissions").unwrap();
assert!(
out.contains("{ \"name\": \"a\" },"),
"needs a separating comma:\n{out}"
);
let empty = "{\n \"requestPermissions\": [\n ]\n}\n";
let out = ensure_region(empty, "requestPermissions", "permissions").unwrap();
assert!(
!out.contains("[,"),
"an empty array must not gain a leading comma:\n{out}"
);
}
}