liwe 0.17.0

IWE core library
Documentation
use pulldown_cmark::{Event, Parser, Tag};

use crate::markdown::reader::PARSER_OPTIONS;
use crate::model::{frontmatter_to_string, Frontmatter};
use crate::query::frontmatter::strip_reserved;

pub fn split_raw_frontmatter(content: &str) -> (Option<&str>, &str) {
    match leading_metadata_block_end(content) {
        Some(end) => (Some(&content[..end]), &content[end..]),
        None => (None, content),
    }
}

pub fn prepend_frontmatter(
    frontmatter: Option<Frontmatter>,
    rendered: &str,
) -> Result<String, String> {
    let mut mapping = match frontmatter {
        Some(mapping) => mapping,
        None => return Ok(rendered.to_string()),
    };

    strip_reserved(&mut mapping);
    if mapping.is_empty() {
        return Ok(rendered.to_string());
    }

    if leading_metadata_block_end(rendered).is_some() {
        return Err(
            "the document already begins with a frontmatter block, it would be written twice; \
             drop the frontmatter fields, or pass the complete document as content"
                .to_string(),
        );
    }

    Ok(format!(
        "---\n{}\n---\n\n{}",
        frontmatter_to_string(&mapping),
        rendered
    ))
}

fn leading_metadata_block_end(content: &str) -> Option<usize> {
    if let Some(end) = metadata_block_end(content) {
        return Some(end);
    }

    if content.is_empty() || content.ends_with('\n') {
        return None;
    }

    let terminated = format!("{}\n", content);
    metadata_block_end(&terminated).map(|end| end.min(content.len()))
}

fn metadata_block_end(content: &str) -> Option<usize> {
    match Parser::new_ext(content, PARSER_OPTIONS)
        .into_offset_iter()
        .next()
    {
        Some((Event::Start(Tag::MetadataBlock(_)), range)) if range.start == 0 => {
            Some(range.end + line_ending_len(&content[range.end..]))
        }
        _ => None,
    }
}

fn line_ending_len(rest: &str) -> usize {
    if rest.starts_with("\r\n") {
        2
    } else if rest.starts_with('\n') {
        1
    } else {
        0
    }
}

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

    fn mapping(yaml: &str) -> Frontmatter {
        match serde_yaml::from_str::<Value>(yaml).unwrap() {
            Value::Mapping(m) => m,
            _ => panic!("expected a mapping"),
        }
    }

    #[test]
    fn splits_dash_closed_block() {
        assert_eq!(
            split_raw_frontmatter("---\ntype: note\n---\n\nBody\n"),
            (Some("---\ntype: note\n---\n"), "\nBody\n")
        );
    }

    #[test]
    fn splits_dot_closed_block() {
        assert_eq!(
            split_raw_frontmatter("---\ntype: note\n...\n\nBody\n"),
            (Some("---\ntype: note\n...\n"), "\nBody\n")
        );
    }

    #[test]
    fn splits_crlf_block() {
        assert_eq!(
            split_raw_frontmatter("---\r\ntype: note\r\n---\r\n\r\nBody\r\n"),
            (Some("---\r\ntype: note\r\n---\r\n"), "\r\nBody\r\n")
        );
    }

    #[test]
    fn splits_block_without_trailing_newline() {
        assert_eq!(
            split_raw_frontmatter("---\ntype: note\n---"),
            (Some("---\ntype: note\n---"), "")
        );
    }

    #[test]
    fn keeps_lone_thematic_break() {
        assert_eq!(
            split_raw_frontmatter("---\n\nBody\n"),
            (None, "---\n\nBody\n")
        );
    }

    #[test]
    fn keeps_two_thematic_breaks() {
        assert_eq!(
            split_raw_frontmatter("---\n\n---\n\nBody\n"),
            (None, "---\n\n---\n\nBody\n")
        );
    }

    #[test]
    fn keeps_unterminated_block() {
        assert_eq!(
            split_raw_frontmatter("---\ntype: note\n\nBody\n"),
            (None, "---\ntype: note\n\nBody\n")
        );
    }

    #[test]
    fn keeps_block_below_the_first_line() {
        assert_eq!(
            split_raw_frontmatter("# Title\n\n---\ntype: note\n---\n"),
            (None, "# Title\n\n---\ntype: note\n---\n")
        );
    }

    #[test]
    fn keeps_empty_input() {
        assert_eq!(split_raw_frontmatter(""), (None, ""));
    }

    #[test]
    fn prepends_nothing_for_absent_mapping() {
        assert_eq!(
            prepend_frontmatter(None, "# Title\n"),
            Ok("# Title\n".to_string())
        );
    }

    #[test]
    fn prepends_nothing_for_empty_mapping() {
        assert_eq!(
            prepend_frontmatter(Some(Frontmatter::new()), "# Title\n"),
            Ok("# Title\n".to_string())
        );
    }

    #[test]
    fn prepends_nothing_when_all_keys_are_reserved() {
        assert_eq!(
            prepend_frontmatter(Some(mapping("_internal: 1\n$x: 2\n")), "# Title\n"),
            Ok("# Title\n".to_string())
        );
    }

    #[test]
    fn prepends_fenced_mapping() {
        assert_eq!(
            prepend_frontmatter(Some(mapping("type: note\ntags:\n- demo\n")), "# Title\n"),
            Ok("---\ntype: note\ntags:\n- demo\n---\n\n# Title\n".to_string())
        );
    }

    #[test]
    fn prepends_mapping_without_reserved_keys() {
        assert_eq!(
            prepend_frontmatter(Some(mapping("_internal: 1\ntype: note\n")), "# Title\n"),
            Ok("---\ntype: note\n---\n\n# Title\n".to_string())
        );
    }

    #[test]
    fn rejects_document_with_leading_block() {
        assert_eq!(
            prepend_frontmatter(
                Some(mapping("type: note\n")),
                "---\nother: 1\n---\n\n# Title\n"
            ),
            Err(
                "the document already begins with a frontmatter block, it would be written twice; \
                 drop the frontmatter fields, or pass the complete document as content"
                    .to_string()
            )
        );
    }
}