nmd_core/
table_of_contents.rs1pub mod content_tree;
2
3use getset::{CopyGetters, Getters, Setters};
4use serde::Serialize;
5use crate::{codex::Codex, compilable_text::{compilable_text_part::CompilableTextPart, CompilableText}, compilation::{compilable::Compilable, compilation_configuration::{compilation_configuration_overlay::CompilationConfigurationOverLay, CompilationConfiguration}, compilation_error::CompilationError, compilation_outcome::CompilationOutcome}, dossier::document::chapter::heading::HeadingLevel, output_format::OutputFormat};
6use super::dossier::document::chapter::heading::Heading;
7
8
9pub const TOC_INDENTATION: &str = r#"<span class="toc-item-indentation"></span>"#;
10
11
12
13#[derive(Debug, Clone, Getters, CopyGetters, Setters, Serialize)]
14pub struct TableOfContents {
15
16 #[getset(get = "pub", set = "pub")]
17 title: String,
18
19 #[getset(get_copy = "pub", set = "pub")]
20 page_numbers: bool,
21
22 #[getset(get_copy = "pub", set = "pub")]
23 plain: bool,
24
25 #[getset(get_copy = "pub", set = "pub")]
26 maximum_heading_level: usize,
27
28 #[getset(get = "pub", set = "pub")]
29 headings: Vec<Heading>,
30}
31
32impl TableOfContents {
33 pub fn new(title: String, page_numbers: bool, plain: bool, maximum_heading_level: usize, headings: Vec<Heading>) -> Self {
34 Self {
35 title,
36 page_numbers,
37 plain,
38 maximum_heading_level,
39 headings,
40 }
41 }
42
43 fn min_headers_lv(headings: &Vec<Heading>) -> Result<u32, CompilationError> {
45 let mut m: u32 = 0;
46
47 for h in headings {
48
49 let level = if let HeadingLevel::Explicit(l) = h.level() {
50 *l
51 } else {
52
53 return Err(CompilationError::HeadingLevelNotInferable(h.title().to_string()))
54 };
55
56 m = m.min(level);
57 }
58
59 Ok(m)
60 }
61}
62
63impl Compilable for TableOfContents {
64
65 fn standard_compile(&mut self, format: &OutputFormat, codex: &Codex, compilation_configuration: &CompilationConfiguration, compilation_configuration_overlay: CompilationConfigurationOverLay) -> Result<CompilationOutcome, CompilationError> {
66
67 if self.headings().is_empty() {
68
69 return Ok(CompilationOutcome::empty());
70 }
71
72 if self.page_numbers() {
73 log::error!("table of contents with page numbers not already usable...");
74
75 unimplemented!("table of contents with page numbers not already usable...");
76 }
77
78 let min_heading_lv = Self::min_headers_lv(self.headings())?;
79
80 match format {
81 OutputFormat::Html => {
82 let mut outcome = CompilableText::new_empty();
83
84 let mut compiled_title = CompilableText::from(self.title.clone());
85
86 compiled_title.compile(format, codex, compilation_configuration, compilation_configuration_overlay.clone())?;
87
88 outcome.parts_mut().push(CompilableTextPart::new_fixed(String::from(r#"<section class="toc"><div class="toc-title">"#)));
89 outcome.parts_mut().append(compiled_title.parts_mut());
90 outcome.parts_mut().push(CompilableTextPart::new_fixed(String::from(r#"</div><ul class="toc-body">"#)));
91
92 let mut total_li = 0;
93
94 for heading in self.headings() {
95
96 let heading_lv: u32 = if let HeadingLevel::Explicit(l) = heading.level() {
97
98 *l
99
100 } else {
101
102 return Err(CompilationError::HeadingLevelNotInferable(heading.title().to_string()))
103 };
104
105 if heading_lv > self.maximum_heading_level() as u32 {
106 continue;
107 }
108
109 outcome.parts_mut().push(CompilableTextPart::new_fixed(String::from(r#"<li class="toc-item">"#)));
110
111 if !self.plain() {
112
113 outcome.parts_mut().push(CompilableTextPart::new_fixed(TOC_INDENTATION.repeat((heading_lv - min_heading_lv) as usize)));
114 }
115
116 outcome.parts_mut().push(CompilableTextPart::new_fixed(r#"<span class="toc-item-bullet"></span><span class="toc-item-content">"#.to_string()));
117
118 if let Some(id) = heading.resource_reference() {
119
120 outcome.parts_mut().push(CompilableTextPart::new_fixed(format!(r#"<a href="{}" class="link">"#, id.build())));
121
122 } else {
123 log::warn!("heading '{}' does not have a valid id", heading.title())
124 }
125
126 let compilation_configuration_overlay = compilation_configuration_overlay.clone();
127
128 let mut compiled_heading_title = CompilableText::from(heading.title().clone());
129
130 compiled_heading_title.compile(format, codex, compilation_configuration, compilation_configuration_overlay.clone())?;
131
132 outcome.parts_mut().append(compiled_heading_title.parts_mut());
133
134 if let Some(_) = heading.resource_reference() {
135
136 outcome.parts_mut().push(CompilableTextPart::new_fixed(String::from(r#"</a>"#)));
137 }
138
139 outcome.parts_mut().push(CompilableTextPart::new_fixed(String::from(r#"</span></li>"#)));
140
141 total_li += 1;
142
143 }
144
145 outcome.parts_mut().push(CompilableTextPart::new_fixed(String::from(r#"</ul></section>"#)));
146
147 log::info!("compiled table of contents ({} lines, {} skipped)", total_li, self.headings().len() - total_li);
148
149 Ok(CompilationOutcome::from(&outcome))
150 },
151 }
152 }
153
154}