rustmotion 0.8.0

A CLI tool that renders motion design videos from JSON scenarios. No browser, no Node.js — just a single Rust binary.
Documentation
pub const START: &str = "<!-- rustmotion:start -->";
pub const END: &str = "<!-- rustmotion:end -->";

fn block_span(text: &str) -> Option<(usize, usize)> {
    let start = text.find(START)?;
    let end = text[start..].find(END)? + start + END.len();
    Some((start, end))
}

pub fn merge(existing: Option<&str>, body: &str) -> String {
    let block = format!("{START}\n{}\n{END}\n", body.trim_end());
    let Some(existing) = existing else {
        return block;
    };
    match block_span(existing) {
        Some((start, end)) => {
            let mut out = String::with_capacity(existing.len() + block.len());
            out.push_str(&existing[..start]);
            out.push_str(block.trim_end());
            out.push_str(&existing[end..]);
            out
        }
        None if existing.trim().is_empty() => block,
        None => {
            let mut out = existing.to_string();
            if !out.ends_with('\n') {
                out.push('\n');
            }
            out.push('\n');
            out.push_str(&block);
            out
        }
    }
}

pub fn strip(existing: &str) -> Option<String> {
    let Some((start, end)) = block_span(existing) else {
        return Some(existing.to_string());
    };
    let mut out = String::with_capacity(existing.len());
    out.push_str(&existing[..start]);
    out.push_str(&existing[end..]);
    if out.trim().is_empty() {
        None
    } else {
        Some(format!("{}\n", out.trim_end()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const BODY: &str = "# rustmotion\nGuidance.";

    #[test]
    fn a_project_claude_md_survives_install_and_uninstall() {
        let user = "# My Project\nBuild with `make`. Never delete this.\n";

        let installed = merge(Some(user), BODY);
        assert!(
            installed.contains("Never delete this."),
            "install destroyed the user's content: {installed}"
        );
        assert!(installed.contains(BODY));

        let uninstalled = strip(&installed).expect("a user-authored file is never removed");
        assert!(uninstalled.contains("Never delete this."));
        assert!(
            !uninstalled.contains("Guidance."),
            "uninstall left our block behind: {uninstalled}"
        );
        assert!(!uninstalled.contains(START));
    }

    #[test]
    fn reinstalling_replaces_the_block_instead_of_stacking_copies() {
        let once = merge(None, BODY);
        let twice = merge(Some(&once), "# rustmotion\nNewer guidance.");
        assert_eq!(twice.matches(START).count(), 1, "block duplicated: {twice}");
        assert!(twice.contains("Newer guidance."));
        assert!(!twice.contains("Guidance."));
    }

    #[test]
    fn a_file_we_alone_created_is_reported_as_removable() {
        let ours = merge(None, BODY);
        assert!(strip(&ours).is_none());
    }

    #[test]
    fn a_file_without_our_block_is_returned_untouched() {
        let user = "# Theirs\nnothing of ours here\n";
        assert_eq!(strip(user).as_deref(), Some(user));
    }

    #[test]
    fn content_around_the_block_is_preserved_on_both_sides() {
        let doc = format!("before\n\n{START}\nold\n{END}\n\nafter\n");
        let merged = merge(Some(&doc), BODY);
        assert!(merged.starts_with("before"));
        assert!(merged.trim_end().ends_with("after"));
        assert!(merged.contains("Guidance."));
        assert!(!merged.contains("\nold\n"));
    }
}