1use std::fmt::Display;
2
3use serde::ser::{Serialize, SerializeMap, Serializer};
4
5use crate::{Block, BlockMetadata, InlineNode, Location, model::inlines::converter};
6
7use super::title::Title;
8
9pub type SectionLevel = u8;
11
12#[derive(Clone, Debug, PartialEq)]
14#[non_exhaustive]
15pub struct Section {
16 pub metadata: BlockMetadata,
17 pub title: Title,
18 pub level: SectionLevel,
19 pub content: Vec<Block>,
20 pub location: Location,
21}
22
23impl Section {
24 #[must_use]
26 pub fn new(title: Title, level: SectionLevel, content: Vec<Block>, location: Location) -> Self {
27 Self {
28 metadata: BlockMetadata::default(),
29 title,
30 level,
31 content,
32 location,
33 }
34 }
35
36 #[must_use]
38 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
39 self.metadata = metadata;
40 self
41 }
42}
43
44#[derive(Clone, Debug, PartialEq)]
46#[non_exhaustive]
47pub enum SafeId {
48 Generated(String),
49 Explicit(String),
50}
51
52impl Display for SafeId {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 SafeId::Generated(id) => write!(f, "_{id}"),
56 SafeId::Explicit(id) => write!(f, "{id}"),
57 }
58 }
59}
60
61impl Section {
62 fn id_from_title(title: &[InlineNode]) -> String {
63 let title_text = converter::inlines_to_string(title);
65 let mut id = title_text
66 .to_lowercase()
67 .chars()
68 .filter_map(|c| {
69 if c.is_alphanumeric() {
70 Some(c)
71 } else if c.is_whitespace() || c == '-' || c == '.' {
72 Some('_')
73 } else {
74 None
75 }
76 })
77 .collect::<String>();
78
79 id = id.trim_end_matches('_').to_string();
81
82 let (collapsed, _) = id.chars().fold(
87 (String::with_capacity(id.len()), false), |(mut acc_string, last_was_underscore), current_char| {
89 if current_char == '_' {
90 if !last_was_underscore {
91 acc_string.push('_'); }
93 (acc_string, true) } else {
95 acc_string.push(current_char);
96 (acc_string, false) }
98 },
99 );
100 collapsed
101 }
102
103 #[must_use]
113 pub fn generate_id(metadata: &BlockMetadata, title: &[InlineNode]) -> SafeId {
114 if let Some(anchor) = &metadata.id {
116 return SafeId::Explicit(anchor.id.clone());
117 }
118 if let Some(anchor) = metadata.anchors.last() {
121 return SafeId::Explicit(anchor.id.clone());
122 }
123 let id = Self::id_from_title(title);
125 SafeId::Generated(id)
126 }
127}
128
129impl Serialize for Section {
130 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
131 where
132 S: Serializer,
133 {
134 let mut state = serializer.serialize_map(None)?;
135 state.serialize_entry("name", "section")?;
136 state.serialize_entry("type", "block")?;
137 state.serialize_entry("title", &self.title)?;
138 state.serialize_entry("level", &self.level)?;
139 if !self.metadata.is_default() {
140 state.serialize_entry("metadata", &self.metadata)?;
141 }
142 if !self.content.is_empty() {
143 state.serialize_entry("blocks", &self.content)?;
144 }
145 state.serialize_entry("location", &self.location)?;
146 state.end()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use crate::{Anchor, Plain};
153
154 use super::*;
155
156 #[test]
157 fn test_id_from_title() {
158 let inlines: &[InlineNode] = &[InlineNode::PlainText(Plain {
159 content: "This is a title.".to_string(),
160 location: Location::default(),
161 })];
162 assert_eq!(
163 Section::id_from_title(inlines),
164 "this_is_a_title".to_string()
165 );
166 let inlines: &[InlineNode] = &[InlineNode::PlainText(Plain {
167 content: "This is a----title.".to_string(),
168 location: Location::default(),
169 })];
170 assert_eq!(
171 Section::id_from_title(inlines),
172 "this_is_a_title".to_string()
173 );
174 }
175
176 #[test]
177 fn test_section_generate_id() {
178 let inlines: &[InlineNode] = &[InlineNode::PlainText(Plain {
179 content: "This is a b__i__g title.".to_string(),
180 location: Location::default(),
181 })];
182 let metadata = BlockMetadata::default();
184 assert_eq!(
185 Section::generate_id(&metadata, inlines),
186 SafeId::Generated("this_is_a_big_title".to_string())
187 );
188
189 let metadata = BlockMetadata {
191 id: Some(Anchor {
192 id: "custom_id".to_string(),
193 xreflabel: None,
194 location: Location::default(),
195 }),
196 ..Default::default()
197 };
198 assert_eq!(
199 Section::generate_id(&metadata, inlines),
200 SafeId::Explicit("custom_id".to_string())
201 );
202
203 let metadata = BlockMetadata {
205 anchors: vec![Anchor {
206 id: "anchor_id".to_string(),
207 xreflabel: None,
208 location: Location::default(),
209 }],
210 ..Default::default()
211 };
212 assert_eq!(
213 Section::generate_id(&metadata, inlines),
214 SafeId::Explicit("anchor_id".to_string())
215 );
216
217 let metadata = BlockMetadata {
219 anchors: vec![
220 Anchor {
221 id: "first_anchor".to_string(),
222 xreflabel: None,
223 location: Location::default(),
224 },
225 Anchor {
226 id: "last_anchor".to_string(),
227 xreflabel: None,
228 location: Location::default(),
229 },
230 ],
231 ..Default::default()
232 };
233 assert_eq!(
234 Section::generate_id(&metadata, inlines),
235 SafeId::Explicit("last_anchor".to_string())
236 );
237
238 let metadata = BlockMetadata {
240 id: Some(Anchor {
241 id: "from_id".to_string(),
242 xreflabel: None,
243 location: Location::default(),
244 }),
245 anchors: vec![Anchor {
246 id: "from_anchors".to_string(),
247 xreflabel: None,
248 location: Location::default(),
249 }],
250 ..Default::default()
251 };
252 assert_eq!(
253 Section::generate_id(&metadata, inlines),
254 SafeId::Explicit("from_id".to_string())
255 );
256 }
257}