typeduck-codex-extension-items 0.6.0

Support package for the standalone Codex Web runtime (codex-core-plugins)
Documentation
use super::RewriteProfile;

pub(super) fn rewrite_terms(content: &str, profile: RewriteProfile) -> String {
    let mut rewritten =
        replace_case_insensitive_with_boundaries(content, profile.doc_file_name, "AGENTS.md");
    for from in profile.term_variants {
        rewritten = replace_case_insensitive_with_boundaries(&rewritten, from, "Codex");
    }
    for from in profile.case_sensitive_term_variants {
        rewritten = replace_with_boundaries(&rewritten, from, "Codex");
    }
    rewritten
}

fn replace_with_boundaries(input: &str, needle: &str, replacement: &str) -> String {
    if needle.is_empty() {
        return input.to_string();
    }

    replace_with_boundaries_impl(input, needle, replacement, input)
}

fn replace_case_insensitive_with_boundaries(
    input: &str,
    needle: &str,
    replacement: &str,
) -> String {
    let needle_lower = needle.to_ascii_lowercase();
    if needle_lower.is_empty() {
        return input.to_string();
    }
    let haystack_lower = input.to_ascii_lowercase();
    replace_with_boundaries_impl(input, &needle_lower, replacement, &haystack_lower)
}

fn replace_with_boundaries_impl(
    input: &str,
    needle: &str,
    replacement: &str,
    searchable_input: &str,
) -> String {
    let bytes = input.as_bytes();
    let mut output = String::with_capacity(input.len());
    let mut last_emitted = 0usize;
    let mut search_start = 0usize;

    while let Some(relative_pos) = searchable_input[search_start..].find(needle) {
        let start = search_start + relative_pos;
        let end = start + needle.len();
        let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]);
        let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]);

        if boundary_before && boundary_after {
            output.push_str(&input[last_emitted..start]);
            output.push_str(replacement);
            last_emitted = end;
        }
        search_start = start + 1;
    }

    if last_emitted == 0 {
        return input.to_string();
    }
    output.push_str(&input[last_emitted..]);
    output
}

pub(super) fn yaml_string(value: &str) -> String {
    format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
}

pub(super) fn slugify_name(value: &str) -> String {
    let mut slug = String::new();
    let mut last_was_dash = false;
    for ch in value.chars() {
        if ch.is_ascii_alphanumeric() {
            slug.push(ch.to_ascii_lowercase());
            last_was_dash = false;
        } else if !last_was_dash {
            slug.push('-');
            last_was_dash = true;
        }
    }

    let slug = slug.trim_matches('-').to_string();
    if slug.is_empty() {
        "migrated".to_string()
    } else {
        slug
    }
}

fn is_word_byte(byte: u8) -> bool {
    byte.is_ascii_alphanumeric() || byte == b'_'
}