mant-engine 0.11.0

Structured manual and Markdown document engine used by ManT
Documentation
//! Source-level rendering contracts paired with codec-only normalization tests.

use mant_ir::{
    DefinitionItem, Document, inline_plain_text,
    visit::{self, Visit},
};
use mant_loader::load_roff_bytes;
use mant_render::render_query_text;

fn row<'a>(text: &'a str, token: &str) -> &'a str {
    text.lines()
        .find(|line| line.trim() == token)
        .unwrap_or_else(|| panic!("missing standalone {token:?}: {text}"))
}

fn blank_rows_before(text: &str, token: &str) -> usize {
    let rows = text.lines().collect::<Vec<_>>();
    let index = rows.iter().position(|line| line.trim() == token).unwrap();
    rows[..index]
        .iter()
        .rev()
        .take_while(|line| line.is_empty())
        .count()
}

fn definition<'a>(document: &'a Document, term: &str) -> &'a DefinitionItem {
    struct Definitions<'a>(Vec<&'a DefinitionItem>);
    impl<'a> Visit<'a> for Definitions<'a> {
        fn visit_definition_item(&mut self, item: &'a DefinitionItem) {
            self.0.push(item);
            visit::walk_definition_item(self, item);
        }
    }
    let mut definitions = Definitions(Vec::new());
    definitions.visit_document(document);
    definitions
        .0
        .into_iter()
        .find(|item| {
            item.terms
                .iter()
                .any(|nodes| inline_plain_text(nodes) == term)
        })
        .unwrap_or_else(|| panic!("missing definition {term:?}: {document:?}"))
}

#[test]
fn spaced_relative_continuations_preserve_rows_columns_and_next_owner() {
    for origin in [0, 7] {
        for label in ["-a", "--long-option", "", "e\u{301}"] {
            let source = format!(
                ".TH PROBE 1\n.SH OPTIONS\n.PD 0\n.RS {origin}\n.IP \"{label}\" 4\nInitial.\n.RS 4\n.sp 1\n.sp 3\nFIRST_CONTINUATION\n.RE\n.RS 8\n.sp 2\n.IP --nested 4\nNESTED_BODY\n.RE\n.RS 4\n.sp 1\nLAST_CONTINUATION\n.RE\n.sp 2\n.IP --next 4\nOUTSIDE_BODY\n.RE\n"
            );
            let content = load_roff_bytes(source.as_bytes()).unwrap();
            let text = render_query_text(&content);
            for token in ["FIRST_CONTINUATION", "LAST_CONTINUATION"] {
                assert_eq!(
                    row(&text, token),
                    format!("{}{token}", " ".repeat(origin + 4)),
                    "{source}\n{text}"
                );
            }
            assert_eq!(blank_rows_before(&text, "FIRST_CONTINUATION"), 4, "{text}");
            assert_eq!(blank_rows_before(&text, "LAST_CONTINUATION"), 1, "{text}");
            let document = content.document.as_ref().unwrap();
            let owner = definition(document, label);
            let body = serde_json::to_string(&owner.description).unwrap();
            for token in ["FIRST_CONTINUATION", "NESTED_BODY", "LAST_CONTINUATION"] {
                assert!(body.contains(token), "{body}");
            }
            assert!(!body.contains("OUTSIDE_BODY"), "{body}");
            assert_eq!(owner.layout.inline_term, !label.starts_with("--"));
            assert!(
                serde_json::to_string(&definition(document, "--next").description)
                    .unwrap()
                    .contains("OUTSIDE_BODY")
            );
        }
    }
}

#[test]
fn hanging_paragraph_heads_keep_explicit_space_and_separate_body_rows() {
    for spacing in [0, 1, 3] {
        for head in ["--x", "--long-option-name"] {
            let source = format!(
                ".TH PROBE 1\n.SH OPTIONS\n.PD 0\n.PP\n.B {head}\n.sp {spacing}\n.RS 4\nDESCRIPTION_BODY\n.RE\n.PP\nOutside prose.\n"
            );
            let content = load_roff_bytes(source.as_bytes()).unwrap();
            let text = render_query_text(&content);
            assert_eq!(row(&text, head), head, "{source}\n{text}");
            assert_eq!(
                row(&text, "DESCRIPTION_BODY"),
                "    DESCRIPTION_BODY",
                "{source}\n{text}"
            );
            assert_eq!(
                blank_rows_before(&text, "DESCRIPTION_BODY"),
                spacing,
                "{source}\n{text}"
            );
            assert!(
                !definition(content.document.as_ref().unwrap(), head)
                    .layout
                    .inline_term
            );
        }
    }
}

#[test]
fn hanging_heads_keep_source_offsets_without_promoting_run_in_layout() {
    for origin in [0_i32, 2, 5, -2] {
        for offset in [2, 4, 12] {
            for head in ["--x", "--long-option-name"] {
                let source = format!(
                    ".TH PROBE 1\n.SH OPTIONS\n.PD 0\n.RS {origin}\n.PP\n.B {head}\n.RS {offset}\nDESCRIPTION_BODY\n.RE\n.RE\n"
                );
                let content = load_roff_bytes(source.as_bytes()).unwrap();
                let text = render_query_text(&content);
                assert_eq!(
                    row(&text, head),
                    format!(
                        "{}{head}",
                        " ".repeat(usize::try_from(origin.max(0)).unwrap())
                    ),
                    "{source}\n{text}"
                );
                assert_eq!(
                    row(&text, "DESCRIPTION_BODY"),
                    format!(
                        "{}DESCRIPTION_BODY",
                        " ".repeat(usize::try_from((origin + offset).max(0)).unwrap())
                    ),
                    "{source}\n{text}"
                );
                assert_eq!(
                    blank_rows_before(&text, "DESCRIPTION_BODY"),
                    0,
                    "{source}\n{text}"
                );
                assert!(
                    !definition(content.document.as_ref().unwrap(), head)
                        .layout
                        .inline_term
                );
            }
        }
    }
}