Skip to main content

microcad_lang_markdown/
markdown.rs

1// Copyright © 2026 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Generate a single markdown file for symbol.
5
6use std::str::FromStr;
7
8use derive_more::{Deref, DerefMut};
9use thiserror::Error;
10
11use crate::{CodeBlock, Paragraph, ParseError, Section};
12
13#[derive(Error, Debug)]
14pub enum MarkdownError {
15    /// IO error
16    #[error("IO error: {0}")]
17    IoError(#[from] std::io::Error),
18
19    #[error("Parse error: {0}")]
20    ParseError(#[from] ParseError),
21}
22
23/// Markdown struct, represented as a linear list of sections.
24#[derive(Debug, Default, Clone, Deref, DerefMut)]
25pub struct Markdown(Vec<Section>);
26
27impl Markdown {
28    pub fn new(sections: Vec<Section>) -> Self {
29        Self(sections)
30    }
31}
32
33impl std::str::FromStr for Markdown {
34    type Err = ParseError;
35
36    fn from_str(input: &str) -> Result<Self, Self::Err> {
37        crate::parse(input)
38    }
39}
40
41impl Markdown {
42    pub fn load(path: impl AsRef<std::path::Path>) -> Result<Self, MarkdownError> {
43        let input = std::fs::read_to_string(path)?;
44        Markdown::from_str(&input).map_err(|err| err.into())
45    }
46
47    /// Write markdown to file.
48    pub fn save(&self, path: impl AsRef<std::path::Path>) -> Result<(), MarkdownError> {
49        use std::io::Write;
50        let mut file = std::fs::File::create(path)?;
51        Ok(file.write_all(self.to_string().as_bytes())?)
52    }
53
54    /// Add a new section.
55    pub fn add_section(&mut self, section: Section) {
56        self.0.push(section)
57    }
58
59    /// Nest another markdown
60    pub fn nest(&mut self, md: Markdown, n: i64) {
61        self.0.extend(md.0.into_iter().map(|s| s.nested(n)));
62    }
63
64    /// Returns an iterator over all code blocks in the entire document.
65    pub fn code_blocks(&self) -> impl Iterator<Item = &CodeBlock> {
66        self.0
67            .iter() // Iterate over Vec<Section>
68            .flat_map(|section| section.content.iter()) // Flatten Paragraphs
69            .filter_map(|paragraph| {
70                if let Paragraph::CodeBlock(block) = paragraph {
71                    Some(block)
72                } else {
73                    None
74                }
75            })
76    }
77
78    /// Returns an mut iterator over all code blocks in the entire document.
79    pub fn code_blocks_mut(&mut self) -> impl Iterator<Item = &mut CodeBlock> {
80        self.0
81            .iter_mut() // Iterate over Vec<Section>
82            .flat_map(|section| section.content.iter_mut()) // Flatten Paragraphs
83            .filter_map(|paragraph| {
84                if let Paragraph::CodeBlock(block) = paragraph {
85                    Some(block)
86                } else {
87                    None
88                }
89            })
90    }
91}
92
93impl std::fmt::Display for Markdown {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        for (i, section) in self.0.iter().enumerate() {
96            // Add a newline between sections for readability,
97            // but not before the very first one.
98            if i > 0 && !section.is_empty() {
99                writeln!(f)?;
100            }
101            write!(f, "{section}")?;
102        }
103        Ok(())
104    }
105}
106
107#[test]
108fn test_heading_parsing_and_display() {
109    let input = "# Top\nContent\n\n## Sub\nMore content";
110    let md = Markdown::from_str(input).unwrap();
111    eprintln!("{md:#?}");
112
113    assert_eq!(md.0.len(), 2);
114    assert_eq!(md.0[0].level, 1);
115    assert_eq!(md.0[0].heading, "Top");
116    println!("{:#?}", md.0[0].content);
117
118    assert!(
119        md.0[0]
120            .content
121            .contains(&Paragraph::Text("Content".to_string()))
122    );
123    assert_eq!(md.0[1].level, 2);
124    assert_eq!(md.0[1].heading, "Sub");
125    assert!(
126        md.0[1]
127            .content
128            .contains(&Paragraph::Text("More content".to_string()))
129    );
130
131    // Verify formatting includes the double newline you added in Display
132    let output = md.0[0].to_string();
133    assert!(output.starts_with("# Top\n\n"));
134}