use crate::comments_meta::{self, CommentState};
use crate::envelope;
use crate::error::Result;
use crate::orgfile;
use crate::sexp::Sexp;
use crate::syncstate::SyncState;
const METADATA_HEADING: &str = "* GDOC_METADATA :noexport:";
#[derive(Debug, Clone)]
pub struct CleanOutcome {
pub removed: usize,
pub new_content: String,
}
pub fn clean(content: &str) -> Result<CleanOutcome> {
let (_, machine) = orgfile::split(content);
let Some(machine) = machine else {
return Ok(unchanged(content));
};
let removed = comments_meta::parse_entries(machine)
.iter()
.filter(|entry| entry.state == CommentState::Done)
.count();
if removed == 0 {
return Ok(unchanged(content));
}
let sync = SyncState::parse_block(machine)?;
let cleaned_active = comments_meta::clean_section(machine);
let region = format!(
"{METADATA_HEADING}\n{}{cleaned_active}",
sync.render_block()
);
let new_content = orgfile::replace_metadata(content, ®ion);
Ok(CleanOutcome {
removed,
new_content,
})
}
fn unchanged(content: &str) -> CleanOutcome {
CleanOutcome {
removed: 0,
new_content: content.to_owned(),
}
}
#[must_use]
pub fn envelope(outcome: &CleanOutcome) -> Sexp {
envelope::ok(
"clean",
vec![(
"removed",
Sexp::int(i64::try_from(outcome.removed).unwrap_or(i64::MAX)),
)],
)
}
#[cfg(test)]
mod tests {
use super::clean;
use crate::orgfile;
const WITH_DONE: &str = "#+GDOC_ID: DOC1\n#+TITLE: Spec\n\
* Intro\n:PROPERTIES:\n:CUSTOM_ID: sec-intro\n:END:\nBody prose.\n\n\
* GDOC_METADATA :noexport:\n** Sync State\n#+begin_src emacs-lisp\n\
(gdoc-sync-state 1 (positions (pos \"sec-intro\" 1 heading)) (collaborators))\n\
#+end_src\n** Active Comments\n\
*** TODO Alice: keep\n:PROPERTIES:\n:COMMENT_ID: C1\n:END:\n: operator note\n\n\
*** DONE Bob: drop\n:PROPERTIES:\n:COMMENT_ID: C2\n:END:\n";
#[test]
fn clean_removes_done_keeps_todo_and_preserves_body() {
let outcome = clean(WITH_DONE).expect("clean succeeds");
assert_eq!(outcome.removed, 1);
let written = &outcome.new_content;
assert!(written.contains(":COMMENT_ID: C1\n"));
assert!(written.contains(": operator note\n"));
assert!(!written.contains(":COMMENT_ID: C2\n"));
assert!(!written.contains("DONE"));
assert_eq!(orgfile::body(written), orgfile::body(WITH_DONE));
}
#[test]
fn clean_with_no_done_is_a_strict_noop() {
let todo_only = "#+GDOC_ID: DOC1\n* Intro\nBody.\n\n\
* GDOC_METADATA :noexport:\n** Sync State\n#+begin_src emacs-lisp\n\
(gdoc-sync-state 1 (positions) (collaborators))\n#+end_src\n\
** Active Comments\n*** TODO Alice: open\n:PROPERTIES:\n:COMMENT_ID: C1\n:END:\n";
let outcome = clean(todo_only).expect("clean succeeds");
assert_eq!(outcome.removed, 0);
assert_eq!(outcome.new_content, todo_only);
}
#[test]
fn clean_without_a_machine_region_is_a_noop() {
let body_only = "* Intro\nBody.\n";
let outcome = clean(body_only).expect("clean succeeds");
assert_eq!(outcome.removed, 0);
assert_eq!(outcome.new_content, body_only);
}
}