mant-ir 0.11.0

Normalized, source-neutral document intermediate representation for ManT
Documentation
use super::*;
use crate::{
    DefinitionItem, DefinitionLayout, EntryContentSlice, EntryFacts, EntryForm, EntryInlineRoot,
    EntryKind, EntryNameBinding, EntryNameEvidence, Inline, LayoutHint, ListItem, ListItemLayout,
    ListKind, NameCase, TableCell, TableRow,
};

fn facts(name: &str, root: EntryInlineRoot) -> EntryFacts {
    let form = EntryForm {
        parts: vec![EntryContentSlice {
            root,
            path: vec![0],
            bytes: None,
        }],
    };
    EntryFacts {
        id: name.into(),
        kind: EntryKind::Command,
        case: NameCase::Sensitive,
        names: vec![name.into()],
        forms: vec![form.clone()],
        name_bindings: vec![EntryNameBinding {
            name: 0,
            evidence: EntryNameEvidence::Declared,
            occurrences: vec![form],
        }],
        alias_groups: Vec::new(),
        alias_of: None,
        value_domain: None,
    }
}

fn definition(name: &str, children: Vec<Block>) -> DefinitionItem {
    DefinitionItem {
        terms: vec![vec![Inline::Code { value: name.into() }]],
        description: children,
        entry: Some(facts(name, EntryInlineRoot::Term { index: 0 })),
        layout: DefinitionLayout::default(),
        source: None,
    }
}

fn definitions(items: Vec<DefinitionItem>) -> Block {
    Block::DefinitionList {
        declaration_groups: Vec::new(),
        items,
        compact: true,
        layout: LayoutHint {
            indent_columns: 4,
            ..Default::default()
        },
        source: Some(SourceSpan {
            byte_range: None,
            line: 3,
            column: 1,
            end_line: Some(7),
            end_column: None,
        }),
    }
}

fn item(name: &str) -> ListItem {
    ListItem {
        entry: Some(facts(name, EntryInlineRoot::Block { index: 0 })),
        blocks: vec![Block::Paragraph {
            children: vec![Inline::Code { value: name.into() }],
            layout: LayoutHint::default(),
            source: None,
        }],
        layout: ListItemLayout {
            spacing_before_lines: Some(2),
        },
        source: None,
    }
}

#[test]
fn borrowed_serialization_matches_owned_excerpts_and_preserves_ordinals() {
    let blocks = vec![
        definitions(vec![
            definition("first", Vec::new()),
            definition("second", Vec::new()),
        ]),
        Block::List {
            kind: ListKind::Ordered { start: Some(7) },
            items: vec![item("third"), item("fourth")],
            compact: false,
            layout: LayoutHint::default(),
            source: None,
        },
    ];
    let entries = content_entries(&blocks);
    assert_eq!(entries.len(), 4);
    for entry in &entries {
        assert_eq!(
            serde_json::to_value(entry).unwrap(),
            serde_json::to_value(entry.content()).unwrap()
        );
        assert_eq!(
            entry.content().entry_owner().unwrap().facts(),
            entry.owner().facts()
        );
        assert_eq!(entry.names(), entry.owner().facts().unwrap().names);
    }
    let Block::List { kind, items, .. } = entries[3].content() else {
        panic!("list excerpt")
    };
    assert_eq!(kind, ListKind::Ordered { start: Some(8) });
    assert_eq!(items.len(), 1);
    assert_eq!(entries[3].item_index(), 1);
    assert_eq!(entries[0].source(), None);
}

#[test]
fn transparent_table_and_list_paths_keep_nested_semantic_coordinates() {
    use ContentBlockStep::{Block as B, DefinitionItem as D, ListItem as L, TableCell as T};
    let mut transparent = item("unused");
    transparent.entry = None;
    transparent.blocks = vec![Block::Table {
        rows: vec![TableRow {
            cells: vec![TableCell {
                blocks: vec![definitions(vec![
                    definition(
                        "parent",
                        vec![definitions(vec![definition("child", Vec::new())])],
                    ),
                    definition("sibling", Vec::new()),
                ])],
                column_span: 1,
                row_span: 1,
                alignment: None,
            }],
        }],
        layout: LayoutHint::default(),
        source: None,
    }];
    let blocks = vec![Block::List {
        kind: ListKind::Bullet,
        compact: true,
        items: vec![transparent],
        layout: LayoutHint::default(),
        source: None,
    }];
    let entries = content_entries(&blocks);
    let base = vec![
        B { index: 0 },
        L { index: 0 },
        B { index: 0 },
        T { row: 0, column: 0 },
        B { index: 0 },
    ];
    assert_eq!(
        entries
            .iter()
            .map(ContentEntry::indices)
            .collect::<Vec<_>>(),
        vec![&[1][..], &[1, 1][..], &[2][..]]
    );
    assert_eq!(entries[0].block_path(), base);
    let mut nested = base.clone();
    nested.extend([D { index: 0 }, B { index: 0 }]);
    assert_eq!(entries[1].block_path(), nested);
    assert_eq!(entries[2].block_path(), base);
    assert_eq!(entries[1].ancestors().len(), 1);
    assert_eq!(
        entries[1].ancestors()[0].facts().unwrap().id.as_str(),
        "parent"
    );
    assert!(entries[2].ancestors().is_empty());
    for entry in entries {
        let block = crate::resolve_content_block(&blocks, entry.block_path()).unwrap();
        assert!(std::ptr::eq(block, entry.container));
    }
}

#[test]
fn invalid_names_remain_addressable_and_location_only_scan_skips_names() {
    let valid = definition("valid", Vec::new());
    let mut invalid = definition(
        "invalid",
        vec![definitions(vec![definition("child", Vec::new())])],
    );
    invalid.entry.as_mut().unwrap().name_bindings.clear();
    invalid.source = Some(SourceSpan {
        byte_range: None,
        line: 8,
        column: 2,
        end_line: None,
        end_column: None,
    });
    let blocks = vec![definitions(vec![valid, invalid])];
    let detailed = content_entries(&blocks);
    let locations = content_entry_locations(&blocks);
    assert_eq!(detailed.len(), 3);
    assert_eq!(detailed[0].names(), ["valid"]);
    assert!(detailed[1].names().is_empty());
    assert_eq!(detailed[2].names(), ["child"]);
    assert_eq!(detailed[1].source().unwrap().line, 8);
    for (detail, location) in detailed.iter().zip(&locations) {
        assert!(location.names().is_empty());
        assert_eq!(detail.indices(), location.indices());
        assert_eq!(detail.block_path(), location.block_path());
        assert_eq!(detail.owner().facts(), location.owner().facts());
    }
    assert_eq!(locations[2].indices(), [2, 1]);
}

#[test]
fn borrowed_locations_and_semantic_index_share_root_and_section_owners() {
    let mut transparent = definition(
        "unused",
        vec![definitions(vec![definition(
            "parent",
            vec![definitions(vec![definition("child", Vec::new())])],
        )])],
    );
    transparent.entry = None;
    let blocks = vec![definitions(vec![transparent])];
    let document = crate::Document {
        heading: None,
        parser: None,
        source: crate::DocumentSource {
            format: crate::SourceFormat::Markdown,
            path: None,
        },
        meta: crate::DocumentMeta::default(),
        fragment_aliases: Vec::new(),
        diagnostics: Vec::new(),
        blocks: blocks.clone(),
        sections: vec![crate::Section {
            id: "section".into(),
            heading: crate::Heading {
                content: vec![Inline::Text {
                    value: "Section".into(),
                }],
                source: None,
            },
            fragment_aliases: Vec::new(),
            spacing_before_lines: 0,
            blocks,
            children: Vec::new(),
            source: None,
        }],
    };
    let index = crate::SemanticIndex::build(&document);
    for (section, blocks) in [
        (None, document.blocks.as_slice()),
        (Some(&[1][..]), document.sections[0].blocks.as_slice()),
    ] {
        for entry in content_entry_locations(blocks) {
            let path = crate::OutlinePath::nested_entry(section, entry.indices()).unwrap();
            assert_eq!(
                index.owner_at(&path),
                Some(&crate::ContentReveal::Owner {
                    sections: section.map_or(Vec::new(), |_| vec![0]),
                    blocks: entry.block_path().to_vec(),
                    item_index: u32::try_from(entry.item_index()).unwrap(),
                })
            );
        }
    }
}