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 escaped: false,
162 })];
163 assert_eq!(
164 Section::id_from_title(inlines),
165 "this_is_a_title".to_string()
166 );
167 let inlines: &[InlineNode] = &[InlineNode::PlainText(Plain {
168 content: "This is a----title.".to_string(),
169 location: Location::default(),
170 escaped: false,
171 })];
172 assert_eq!(
173 Section::id_from_title(inlines),
174 "this_is_a_title".to_string()
175 );
176 }
177
178 #[test]
179 fn test_section_generate_id() {
180 let inlines: &[InlineNode] = &[InlineNode::PlainText(Plain {
181 content: "This is a b__i__g title.".to_string(),
182 location: Location::default(),
183 escaped: false,
184 })];
185 let metadata = BlockMetadata::default();
187 assert_eq!(
188 Section::generate_id(&metadata, inlines),
189 SafeId::Generated("this_is_a_big_title".to_string())
190 );
191
192 let metadata = BlockMetadata {
194 id: Some(Anchor {
195 id: "custom_id".to_string(),
196 xreflabel: None,
197 location: Location::default(),
198 }),
199 ..Default::default()
200 };
201 assert_eq!(
202 Section::generate_id(&metadata, inlines),
203 SafeId::Explicit("custom_id".to_string())
204 );
205
206 let metadata = BlockMetadata {
208 anchors: vec![Anchor {
209 id: "anchor_id".to_string(),
210 xreflabel: None,
211 location: Location::default(),
212 }],
213 ..Default::default()
214 };
215 assert_eq!(
216 Section::generate_id(&metadata, inlines),
217 SafeId::Explicit("anchor_id".to_string())
218 );
219
220 let metadata = BlockMetadata {
222 anchors: vec![
223 Anchor {
224 id: "first_anchor".to_string(),
225 xreflabel: None,
226 location: Location::default(),
227 },
228 Anchor {
229 id: "last_anchor".to_string(),
230 xreflabel: None,
231 location: Location::default(),
232 },
233 ],
234 ..Default::default()
235 };
236 assert_eq!(
237 Section::generate_id(&metadata, inlines),
238 SafeId::Explicit("last_anchor".to_string())
239 );
240
241 let metadata = BlockMetadata {
243 id: Some(Anchor {
244 id: "from_id".to_string(),
245 xreflabel: None,
246 location: Location::default(),
247 }),
248 anchors: vec![Anchor {
249 id: "from_anchors".to_string(),
250 xreflabel: None,
251 location: Location::default(),
252 }],
253 ..Default::default()
254 };
255 assert_eq!(
256 Section::generate_id(&metadata, inlines),
257 SafeId::Explicit("from_id".to_string())
258 );
259 }
260}