Skip to main content

mos_eval/
list.rs

1//! Lower parsed lists into semantic list nodes.
2
3use std::collections::BTreeMap;
4
5use mos_core::{AttrMap, AttrValue, Document, NodeId, NodeKind, NodeSpec, SourceSpan};
6use mos_parse::{Item, ListItem};
7
8use crate::inline::lower_inlines;
9
10/// Allocate a [`NodeKind::List`] under `parent` and recursively lower
11/// its [`ListItem`]s into [`NodeKind::ListItem`] children. The
12/// `ordered` flag is preserved as a `Bool` attribute so layout can pick
13/// the right marker style without re-walking the tree.
14pub(super) fn lower_list(
15    doc: &mut Document,
16    parent: NodeId,
17    ordered: bool,
18    items: &[ListItem],
19    span: &SourceSpan,
20) {
21    let mut attributes: AttrMap = BTreeMap::new();
22    attributes.insert("ordered".to_owned(), AttrValue::Bool(ordered));
23    let list_id = doc.alloc_child(
24        parent,
25        NodeSpec::new(NodeKind::List, span.clone()).with_attributes(attributes),
26    );
27    for item in items {
28        lower_list_item(doc, list_id, item);
29    }
30}
31
32fn lower_list_item(doc: &mut Document, parent: NodeId, item: &ListItem) {
33    let item_id = doc.alloc_child(parent, NodeSpec::new(NodeKind::ListItem, item.span.clone()));
34    lower_inlines(doc, item_id, &item.inlines);
35    for child in &item.children {
36        if let Item::List {
37            ordered,
38            items,
39            span,
40        } = child
41        {
42            lower_list(doc, item_id, *ordered, items, span);
43        }
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    #![allow(
50        clippy::unwrap_used,
51        clippy::expect_used,
52        clippy::panic,
53        reason = "tests panic loudly on setup failure; matches crate-wide test-module convention"
54    )]
55
56    use std::path::PathBuf;
57
58    use mos_core::{AttrValue, Node, NodeKind};
59
60    use crate::lower;
61
62    #[test]
63    fn lowers_unordered_list() {
64        let r = lower("- one\n- two\n", &PathBuf::from("test.mos"));
65        assert!(!r.has_errors(), "{:?}", r.diagnostics);
66        let list = r
67            .document
68            .nodes()
69            .find(|n| n.kind == NodeKind::List)
70            .expect("list node");
71        assert_eq!(
72            list.attributes.get("ordered"),
73            Some(&AttrValue::Bool(false))
74        );
75        assert_eq!(list.children.len(), 2);
76        let items: Vec<&Node> = list
77            .children
78            .iter()
79            .filter_map(|id| r.document.get(*id))
80            .collect();
81        assert!(items.iter().all(|n| n.kind == NodeKind::ListItem));
82        for (item, expected) in items.iter().zip(["one", "two"]) {
83            let text_child = item
84                .children
85                .iter()
86                .filter_map(|id| r.document.get(*id))
87                .find(|n| n.kind == NodeKind::Text)
88                .expect("text child");
89            assert_eq!(
90                text_child.attributes.get("text"),
91                Some(&AttrValue::Str(expected.to_owned()))
92            );
93        }
94    }
95
96    #[test]
97    fn lowers_ordered_list_flag() {
98        let r = lower("1. a\n2. b\n", &PathBuf::from("test.mos"));
99        assert!(!r.has_errors(), "{:?}", r.diagnostics);
100        let list = r
101            .document
102            .nodes()
103            .find(|n| n.kind == NodeKind::List)
104            .expect("list node");
105        assert_eq!(list.attributes.get("ordered"), Some(&AttrValue::Bool(true)));
106    }
107
108    #[test]
109    fn lowers_nested_list_as_listitem_child() {
110        let r = lower("- outer\n  - inner\n", &PathBuf::from("test.mos"));
111        assert!(!r.has_errors(), "{:?}", r.diagnostics);
112        let outer = r
113            .document
114            .nodes()
115            .find(|n| n.kind == NodeKind::List)
116            .expect("outer list");
117        let outer_item = r.document.get(outer.children[0]).unwrap();
118        let nested = outer_item
119            .children
120            .iter()
121            .filter_map(|id| r.document.get(*id))
122            .find(|n| n.kind == NodeKind::List)
123            .expect("nested list");
124        assert_eq!(nested.children.len(), 1);
125        let nested_item = r.document.get(nested.children[0]).unwrap();
126        assert_eq!(nested_item.kind, NodeKind::ListItem);
127    }
128}