ito-core 0.1.33

Core functionality and business logic for Ito
Documentation
use std::path::Path;

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum MarkerError {
    #[error("Invalid marker state in {file_path}. End marker appears before start marker.")]
    EndBeforeStart { file_path: String },

    #[error(
        "Invalid marker state in {file_path}. Found start: {found_start}, Found end: {found_end}"
    )]
    MissingMarker {
        file_path: String,
        found_start: bool,
        found_end: bool,
    },
}

#[derive(Debug, thiserror::Error)]
pub enum FsEditError {
    #[error(transparent)]
    Marker(#[from] MarkerError),

    #[error(transparent)]
    Io(#[from] std::io::Error),
}

fn is_marker_on_own_line(content: &str, marker_index: usize, marker_len: usize) -> bool {
    let bytes = content.as_bytes();

    let mut i = marker_index;
    while i > 0 {
        let c = bytes[i - 1];
        if c == b'\n' {
            break;
        }
        if c != b' ' && c != b'\t' && c != b'\r' {
            return false;
        }
        i -= 1;
    }

    let mut j = marker_index + marker_len;
    while j < bytes.len() {
        let c = bytes[j];
        if c == b'\n' {
            break;
        }
        if c != b' ' && c != b'\t' && c != b'\r' {
            return false;
        }
        j += 1;
    }

    true
}

fn find_marker_index(content: &str, marker: &str, from_index: usize) -> Option<usize> {
    let mut search_from = from_index;
    while let Some(rel) = content[search_from..].find(marker) {
        let idx = search_from + rel;
        if is_marker_on_own_line(content, idx, marker.len()) {
            return Some(idx);
        }
        search_from = idx + marker.len();
    }
    None
}

pub fn update_content_with_markers(
    file_path: &Path,
    existing: Option<&str>,
    new_block_content: &str,
    start_marker: &str,
    end_marker: &str,
) -> Result<String, MarkerError> {
    let Some(existing) = existing else {
        return Ok(format!(
            "{start}\n{body}\n{end}",
            start = start_marker,
            body = new_block_content,
            end = end_marker
        ));
    };

    let start = find_marker_index(existing, start_marker, 0);
    let end = match start {
        Some(start_idx) => find_marker_index(existing, end_marker, start_idx + start_marker.len()),
        None => find_marker_index(existing, end_marker, 0),
    };

    match (start, end) {
        (Some(start_idx), Some(end_idx)) => {
            if end_idx < start_idx {
                return Err(MarkerError::EndBeforeStart {
                    file_path: file_path.display().to_string(),
                });
            }
            let before = &existing[..start_idx];
            let after = &existing[end_idx + end_marker.len()..];
            Ok(format!(
                "{before}{start}\n{body}\n{end}{after}",
                before = before,
                start = start_marker,
                body = new_block_content,
                end = end_marker,
                after = after
            ))
        }
        (None, None) => Ok(format!(
            "{start}\n{body}\n{end}\n\n{rest}",
            start = start_marker,
            body = new_block_content,
            end = end_marker,
            rest = existing
        )),
        (Some(_), None) => Err(MarkerError::MissingMarker {
            file_path: file_path.display().to_string(),
            found_start: true,
            found_end: false,
        }),
        (None, Some(_)) => Err(MarkerError::MissingMarker {
            file_path: file_path.display().to_string(),
            found_start: false,
            found_end: true,
        }),
    }
}

pub fn update_file_with_markers(
    file_path: &Path,
    new_block_content: &str,
    start_marker: &str,
    end_marker: &str,
) -> Result<String, FsEditError> {
    let existing = std::fs::read_to_string(file_path).ok();
    let updated = update_content_with_markers(
        file_path,
        existing.as_deref(),
        new_block_content,
        start_marker,
        end_marker,
    )?;

    if let Some(parent) = file_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(file_path, &updated)?;
    Ok(updated)
}

#[cfg(test)]
#[path = "markers_tests.rs"]
mod markers_tests;