microcad_lang_markdown/
markdown.rs1use 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 #[error("IO error: {0}")]
17 IoError(#[from] std::io::Error),
18
19 #[error("Parse error: {0}")]
20 ParseError(#[from] ParseError),
21}
22
23#[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 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 pub fn add_section(&mut self, section: Section) {
56 self.0.push(section)
57 }
58
59 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 pub fn code_blocks(&self) -> impl Iterator<Item = &CodeBlock> {
66 self.0
67 .iter() .flat_map(|section| section.content.iter()) .filter_map(|paragraph| {
70 if let Paragraph::CodeBlock(block) = paragraph {
71 Some(block)
72 } else {
73 None
74 }
75 })
76 }
77
78 pub fn code_blocks_mut(&mut self) -> impl Iterator<Item = &mut CodeBlock> {
80 self.0
81 .iter_mut() .flat_map(|section| section.content.iter_mut()) .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 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 let output = md.0[0].to_string();
133 assert!(output.starts_with("# Top\n\n"));
134}