codexa 0.0.3

A Git-native content compiler for building typed public and private knowledge artifacts.
Documentation
use crate::model::DocumentLink;

pub fn parse_document_links(markdown: &str) -> Result<Vec<DocumentLink>, LinkError> {
    let mut links = Vec::new();
    let bytes = markdown.as_bytes();
    let mut cursor = 0;

    while cursor + 1 < bytes.len() {
        if bytes[cursor] == b'[' && bytes[cursor + 1] == b'[' {
            let start = cursor;
            cursor += 2;
            let rest = &markdown[cursor..];
            let Some(relative_end) = rest.find("]]") else {
                return Err(LinkError::Unclosed(start));
            };
            let end = cursor + relative_end;
            let inner = &markdown[cursor..end];
            let raw = markdown[start..end + 2].to_string();
            links.push(parse_inner(inner, raw)?);
            cursor = end + 2;
        } else {
            cursor += 1;
        }
    }

    Ok(links)
}

fn parse_inner(inner: &str, raw: String) -> Result<DocumentLink, LinkError> {
    let (target_and_heading, label) = match inner.split_once('|') {
        Some((left, right)) => (left.trim(), Some(right.trim().to_string())),
        None => (inner.trim(), None),
    };

    let (target_id, heading) = match target_and_heading.split_once('#') {
        Some((target, heading)) => (target.trim(), Some(heading.trim().to_string())),
        None => (target_and_heading, None),
    };

    if target_id.is_empty() {
        return Err(LinkError::EmptyTarget(raw));
    }

    Ok(DocumentLink {
        raw,
        target_id: target_id.to_string(),
        heading: heading.filter(|value| !value.is_empty()),
        label: label.filter(|value| !value.is_empty()),
    })
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinkError {
    Unclosed(usize),
    EmptyTarget(String),
}

impl std::fmt::Display for LinkError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Unclosed(offset) => write!(f, "unclosed logical link at byte {offset}"),
            Self::EmptyTarget(raw) => write!(f, "logical link has an empty target: {raw}"),
        }
    }
}

impl std::error::Error for LinkError {}

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

    #[test]
    fn parses_supported_link_shapes() {
        let links =
            parse_document_links("[[a.b]] [[a.b|Label]] [[a.b#Heading]] [[a.b#Heading|Label]]")
                .unwrap();
        assert_eq!(links.len(), 4);
        assert_eq!(links[3].target_id, "a.b");
        assert_eq!(links[3].heading.as_deref(), Some("Heading"));
        assert_eq!(links[3].label.as_deref(), Some("Label"));
    }
}