1use std::{
2 backtrace::Backtrace,
3 fmt::Display,
4 io::{self, BufWriter, Write},
5 iter::Peekable,
6 path::Path,
7};
8
9use snafu::Snafu;
10
11use super::{
12 ParseContext,
13 module::ModuleKind,
14 section::{Section, SectionInheritParseError, SectionParseError, Sections, SectionsError},
15};
16use crate::{
17 config::{CommentedLine, CommentedLineIterator, Comments},
18 util::io::{FileError, create_file},
19};
20
21pub struct Delinks {
22 pub sections: Sections,
23 pub global_categories: Categories,
24 module_kind: ModuleKind,
25 pub files: Vec<DelinkFile>,
26}
27
28#[derive(Debug, Snafu)]
29pub enum DelinksParseError {
30 #[snafu(transparent)]
31 File { source: FileError },
32 #[snafu(transparent)]
33 Io { source: io::Error },
34 #[snafu(transparent)]
35 SectionParse { source: SectionParseError },
36 #[snafu(display("{context}: {error}"))]
37 Sections { context: ParseContext, error: Box<SectionsError> },
38 #[snafu(transparent)]
39 DelinkFileParse { source: DelinkFileParseError },
40}
41
42#[derive(Debug, Snafu)]
43pub enum DelinksWriteError {
44 #[snafu(transparent)]
45 File { source: FileError },
46 #[snafu(transparent)]
47 Io { source: io::Error },
48}
49
50impl Delinks {
51 pub fn new(sections: Sections, files: Vec<DelinkFile>, module_kind: ModuleKind) -> Self {
52 Self { sections, global_categories: Categories::new(), files, module_kind }
53 }
54
55 pub fn from_file<P: AsRef<Path>>(
56 path: P,
57 module_kind: ModuleKind,
58 ) -> Result<Self, DelinksParseError> {
59 let path = path.as_ref();
60
61 let mut context = ParseContext { file_path: path.to_str().unwrap().to_string(), row: 0 };
62 let mut lines = CommentedLine::read(path)?.peekable();
63
64 let mut sections: Sections = Sections::new();
65 let mut files = vec![];
66 let mut global_categories = Categories::new();
67
68 while let Some(line) = lines.next() {
69 let line = line?;
70 context.row = line.row;
71 if line.text.trim().is_empty() {
72 continue;
73 } else if let Some(delink_file) =
74 Self::try_parse_delink_file(&line, &mut lines, &mut context, §ions)?
75 {
76 files.push(delink_file);
77 break;
78 } else if let Some(new_categories) = Categories::try_parse(&line) {
79 global_categories.extend(new_categories);
80 } else {
81 let section = Section::parse(&line, &context)?;
82 sections
83 .add(section)
84 .map_err(|error| SectionsSnafu { context: context.clone(), error }.build())?;
85 }
86 }
87
88 while let Some(line) = lines.next() {
89 let line = line?;
90 context.row = line.row;
91
92 if line.text.trim().is_empty() {
93 continue;
94 } else if let Some(delink_file) =
95 Self::try_parse_delink_file(&line, &mut lines, &mut context, §ions)?
96 {
97 files.push(delink_file);
98 }
99 }
100
101 Ok(Self { sections, global_categories, files, module_kind })
102 }
103
104 fn try_parse_delink_file(
105 line: &CommentedLine,
106 lines: &mut Peekable<CommentedLineIterator>,
107 context: &mut ParseContext,
108 sections: &Sections,
109 ) -> Result<Option<DelinkFile>, DelinkFileParseError> {
110 if line.text.chars().next().is_some_and(|c| !c.is_whitespace()) {
111 let delink_file = DelinkFile::parse(line, lines, context, sections)?;
112 Ok(Some(delink_file))
113 } else {
114 Ok(None)
115 }
116 }
117
118 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), DelinksWriteError> {
119 let path = path.as_ref();
120
121 let file = create_file(path)?;
122 let mut writer = BufWriter::new(file);
123 write!(writer, "{self}")?;
124
125 Ok(())
126 }
127
128 pub fn module_kind(&self) -> ModuleKind {
129 self.module_kind
130 }
131}
132
133impl Display for Delinks {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 if !self.global_categories.categories.is_empty() {
136 writeln!(f, "{}", self.global_categories)?;
137 }
138 for section in self.sections.sorted_by_address() {
139 writeln!(f, "{section}")?;
140 }
141 for file in &self.files {
142 if file.gap {
143 continue;
144 }
145 writeln!(f)?;
146 write!(f, "{file}")?;
147 }
148 Ok(())
149 }
150}
151
152#[derive(Clone)]
153pub struct DelinkFile {
154 pub name: String,
155 pub sections: Sections,
156 pub migrated_sections: Sections,
157 pub complete: bool,
158 pub categories: Categories,
159 gap: bool,
160 migrated: bool,
161 pub comments: Comments,
162}
163
164#[derive(Debug, Snafu)]
165pub enum DelinkFileParseError {
166 #[snafu(display("{context}: expected file path to end with ':':\n{backtrace}"))]
167 MissingColon { context: ParseContext, backtrace: Backtrace },
168 #[snafu(transparent)]
169 Io { source: io::Error },
170 #[snafu(transparent)]
171 SectionInheritParse { source: SectionInheritParseError },
172 #[snafu(transparent)]
173 Sections { source: SectionsError },
174}
175
176pub struct DelinkFileOptions {
177 pub name: String,
178 pub sections: Sections,
179 pub complete: bool,
180 pub categories: Categories,
181 pub gap: bool,
182 pub migrated: bool,
183 pub comments: Comments,
184}
185
186impl DelinkFile {
187 pub fn new(options: DelinkFileOptions) -> Self {
188 let DelinkFileOptions { name, sections, complete, categories, gap, migrated, mut comments } =
189 options;
190 comments.remove_leading_blank_lines();
191 Self {
192 name,
193 sections,
194 migrated_sections: Sections::new(),
195 complete,
196 categories,
197 gap,
198 migrated,
199 comments,
200 }
201 }
202
203 pub fn parse(
204 first_line: &CommentedLine,
205 lines: &mut Peekable<CommentedLineIterator>,
206 context: &mut ParseContext,
207 inherit_sections: &Sections,
208 ) -> Result<Self, DelinkFileParseError> {
209 let name = first_line
210 .text
211 .trim()
212 .strip_suffix(':')
213 .ok_or_else(|| MissingColonSnafu { context: context.clone() }.build())?
214 .to_string();
215
216 let mut complete = false;
217 let mut sections = Sections::new();
218 let mut categories = Categories::new();
219
220 loop {
221 if let Some(Ok(next)) = lines.peek()
222 && next.text.chars().next().is_some_and(|c| !c.is_whitespace())
223 {
224 break;
225 }
226
227 let Some(line) = lines.next() else {
228 break;
229 };
230 let line = line?;
231 context.row = line.row;
232 let text = line.text.trim();
233 if text.is_empty() {
234 break;
235 } else if text == "complete" {
236 complete = true;
237 } else if let Some(new_categories) = Categories::try_parse(&line) {
238 categories.extend(new_categories);
239 } else {
240 let section = Section::parse_inherit(&line, context, inherit_sections)?;
241 sections.add(section)?;
242 }
243 }
244
245 Ok(DelinkFile::new(DelinkFileOptions {
246 name,
247 sections,
248 complete,
249 categories,
250 gap: false,
251 migrated: false,
252 comments: first_line.comments.clone(),
253 }))
254 }
255
256 pub fn split_file_ext(&self) -> (&str, &str) {
257 self.name.rsplit_once('.').unwrap_or((&self.name, ""))
258 }
259
260 pub fn gap(&self) -> bool {
261 self.gap
262 }
263
264 pub fn migrated(&self) -> bool {
265 self.migrated
266 }
267
268 pub fn migrate_section_by_name(&mut self, name: &str) -> Result<(), SectionsError> {
269 let Some(section) = self.sections.remove(name) else {
270 return Ok(());
271 };
272 self.migrated_sections.add(section)?;
273 Ok(())
274 }
275}
276
277impl Display for DelinkFile {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 write!(f, "{}", self.comments.display_pre_comments())?;
280 write!(f, "{}:", self.name)?;
281 writeln!(f, "{}", self.comments.display_post_comment())?;
282
283 if !self.categories.categories.is_empty() {
284 writeln!(f, "{}", self.categories)?;
285 }
286 if self.complete {
287 writeln!(f, " complete")?;
288 }
289 for section in self.sections.sorted_by_address() {
290 section.write_inherit(f)?;
291 writeln!(f)?;
292 }
293 Ok(())
294 }
295}
296
297#[derive(Clone)]
298pub struct Categories {
299 pub categories: Vec<String>,
300 comments: Comments,
301}
302
303impl Categories {
304 pub fn new() -> Self {
305 Self { categories: Vec::new(), comments: Comments::new() }
306 }
307
308 pub fn try_parse(line: &CommentedLine) -> Option<Self> {
309 let list = line.text.trim().strip_prefix("categories:")?;
310 let categories =
311 list.trim().split(',').map(|category| category.trim().to_string()).collect();
312 Some(Self { categories, comments: line.comments.clone() })
313 }
314
315 pub fn extend(&mut self, other: Categories) {
316 self.categories.extend(other.categories);
317 self.categories.sort_unstable();
318 self.categories.dedup();
319 }
320}
321
322impl Default for Categories {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328impl Display for Categories {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 write!(f, "{}", self.comments.display_pre_comments())?;
331 let mut iter = self.categories.iter();
332 if let Some(category) = iter.next() {
333 write!(f, " categories: {category}")?;
334 }
335 for category in iter {
336 write!(f, ", {category}")?;
337 }
338 write!(f, "{}", self.comments.display_post_comment())?;
339 Ok(())
340 }
341}