Skip to main content

microcad_lang/syntax/
doc_block.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Documentation block syntax element
5
6use microcad_lang_base::{Refer, SrcRef, SrcReferrer, TreeDisplay, TreeState};
7
8/// Block of documentation comments, starting with `/// `.
9#[derive(Clone, Debug, Default)]
10pub struct DocBlock(pub Refer<Vec<String>>);
11
12impl DocBlock {
13    /// Create new doc block for builtin.
14    pub fn new_builtin(comment: &str) -> Self {
15        Self(Refer::none(
16            comment.lines().map(|s| format!("/// {s}")).collect(),
17        ))
18    }
19
20    /// Check if this doc block is empty.
21    pub fn is_empty(&self) -> bool {
22        self.0.is_empty()
23    }
24
25    /// Merge two doc blocks, e.g. for merging inner and outer docs
26    pub fn merge(a: &DocBlock, b: &DocBlock) -> DocBlock {
27        match (a.is_empty(), b.is_empty()) {
28            (true, true) => Self::default(),
29            (true, false) => b.clone(),
30            (false, true) => a.clone(),
31            _ => {
32                let merged =
33                    a.0.iter()
34                        .chain([String::default()].iter()) // Add an empty line
35                        .chain(b.0.iter())
36                        .cloned()
37                        .collect::<Vec<_>>();
38                Self(Refer::new(
39                    merged,
40                    SrcRef::merge(&a.src_ref(), &b.src_ref()),
41                ))
42            }
43        }
44    }
45
46    /// Remove `///` comment marks and return each line as string.
47    pub fn fetch_lines(&self) -> Vec<String> {
48        self.0
49            .iter()
50            .filter_map(|s| s.strip_prefix("/// ").or(s.strip_prefix("///")))
51            .map(|s| s.trim_end().to_string())
52            .collect::<Vec<_>>()
53    }
54}
55
56impl SrcReferrer for DocBlock {
57    fn src_ref(&self) -> SrcRef {
58        self.0.src_ref()
59    }
60}
61
62impl std::fmt::Display for DocBlock {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        write!(f, "{}", &self.0.value.join("\n"))
65    }
66}
67
68impl TreeDisplay for DocBlock {
69    fn tree_print(&self, f: &mut std::fmt::Formatter, depth: TreeState) -> std::fmt::Result {
70        writeln!(
71            f,
72            "{:depth$}DocBlock: '{}'",
73            "",
74            microcad_lang_base::shorten!(self.0.first().cloned().unwrap_or_default())
75        )
76    }
77}
78#[test]
79fn doc_block_merge() {
80    let doc_a = DocBlock(Refer::none(vec!["/// line 1".to_string()]));
81    let doc_b = DocBlock(Refer::none(vec!["/// line 2".to_string()]));
82    let empty = DocBlock::default();
83
84    // Test: Merge with empty blocks
85    assert!(DocBlock::merge(&empty, &empty).is_empty());
86
87    let merge_with_empty_left = DocBlock::merge(&empty, &doc_a);
88    assert_eq!(merge_with_empty_left.0.value, vec!["/// line 1"]);
89
90    let merge_with_empty_right = DocBlock::merge(&doc_a, &empty);
91    assert_eq!(merge_with_empty_right.0.value, vec!["/// line 1"]);
92
93    // Test: Merge two populated blocks
94    let merged = DocBlock::merge(&doc_a, &doc_b);
95
96    // The implementation adds an empty line (String::default()) between blocks
97    let expected = vec![
98        "/// line 1".to_string(),
99        "".to_string(),
100        "/// line 2".to_string(),
101    ];
102
103    assert_eq!(merged.0.value, expected);
104}
105
106#[test]
107fn doc_block_fetch_text() {
108    // Test 1: Standard space-separated doc comments
109    let doc1 = DocBlock(Refer::none(vec![
110        "/// Line one".to_string(),
111        "/// Line two ".to_string(), // Note the trailing space
112    ]));
113    assert_eq!(doc1.fetch_lines().join("\n"), "Line one\nLine two");
114
115    // Test 2: Mixed prefixes (with and without space)
116    let doc2 = DocBlock(Refer::none(vec![
117        "///Space".to_string(),
118        "///No space".to_string(),
119    ]));
120    assert_eq!(doc2.fetch_lines().join("\n"), "Space\nNo space");
121
122    // Test 3: Lines that don't start with '///' should be ignored
123    let doc3 = DocBlock(Refer::none(vec![
124        "/// Valid".to_string(),
125        "Invalid line".to_string(),
126        "/// Also valid".to_string(),
127    ]));
128    assert_eq!(doc3.fetch_lines().join("\n"), "Valid\nAlso valid");
129
130    // Test 4: Empty DocBlock
131    let doc_empty = DocBlock::default();
132    assert_eq!(doc_empty.fetch_lines().join("\n"), "");
133}