1use crate::{
2 HasSpan, Parser, Span,
3 attributes::Attrlist,
4 blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
5 span::MatchedItem,
6 strings::CowStr,
7};
8
9#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct Break<'src> {
12 type_: BreakType,
13 source: Span<'src>,
14 title_source: Option<Span<'src>>,
15 title: Option<String>,
16 anchor: Option<Span<'src>>,
17 attrlist: Option<Attrlist<'src>>,
18}
19
20#[derive(Clone, Copy, Eq, PartialEq)]
22pub enum BreakType {
23 Thematic,
25
26 Page,
28}
29
30impl std::fmt::Debug for BreakType {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 BreakType::Thematic => write!(f, "BreakType::Thematic"),
34 BreakType::Page => write!(f, "BreakType::Page"),
35 }
36 }
37}
38
39impl<'src> Break<'src> {
40 pub(crate) fn parse(
41 metadata: &BlockMetadata<'src>,
42 _parser: &mut Parser,
43 ) -> Option<MatchedItem<'src, Self>> {
44 let line = metadata.block_start.take_normalized_line();
45
46 let type_ = match line.item.data() {
47 "'''" | "---" | "- - -" | "***" | "* * *" => BreakType::Thematic,
48 "<<<" => BreakType::Page,
49 _ => {
50 return None;
51 }
52 };
53
54 let source: Span = metadata.source.trim_remainder(line.after);
55 let source = source.slice(0..source.trim().len());
56
57 Some(MatchedItem {
58 item: Self {
59 type_,
60 source,
61 title_source: metadata.title_source,
62 title: metadata.title.clone(),
63 anchor: metadata.anchor,
64 attrlist: metadata.attrlist.clone(),
65 },
66
67 after: line.after.discard_empty_lines(),
68 })
69 }
70
71 pub fn type_(&self) -> BreakType {
73 self.type_
74 }
75}
76
77impl<'src> IsBlock<'src> for Break<'src> {
78 fn content_model(&self) -> ContentModel {
79 ContentModel::Empty
80 }
81
82 fn raw_context(&self) -> CowStr<'src> {
83 match self.type_ {
84 BreakType::Thematic => "thematic_break",
85 BreakType::Page => "page_break",
86 }
87 .into()
88 }
89
90 fn title_source(&'src self) -> Option<Span<'src>> {
91 self.title_source
92 }
93
94 fn title(&self) -> Option<&str> {
95 self.title.as_deref()
96 }
97
98 fn anchor(&'src self) -> Option<Span<'src>> {
99 self.anchor
100 }
101
102 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
103 None
104 }
105
106 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
107 self.attrlist.as_ref()
108 }
109}
110
111impl<'src> HasSpan<'src> for Break<'src> {
112 fn span(&self) -> Span<'src> {
113 self.source
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 #![allow(clippy::unwrap_used)]
120
121 use std::ops::Deref;
122
123 use crate::{
124 blocks::{BreakType, ContentModel, metadata::BlockMetadata},
125 tests::prelude::*,
126 };
127
128 #[test]
129 fn impl_clone() {
130 let mut parser = Parser::default();
132
133 let b1 = crate::blocks::Break::parse(&BlockMetadata::new("'''"), &mut parser)
134 .unwrap()
135 .item;
136
137 let b2 = b1.clone();
138 assert_eq!(b1, b2);
139 }
140
141 #[test]
142 fn err_empty_source() {
143 let mut parser = Parser::default();
144 assert!(crate::blocks::Break::parse(&BlockMetadata::new(""), &mut parser).is_none());
145 }
146
147 #[test]
148 fn err_only_spaces() {
149 let mut parser = Parser::default();
150 assert!(crate::blocks::Break::parse(&BlockMetadata::new(" "), &mut parser).is_none());
151 }
152
153 #[test]
154 fn err_unknown_break_pattern() {
155 let mut parser = Parser::default();
156 assert!(crate::blocks::Break::parse(&BlockMetadata::new("=="), &mut parser).is_none());
157 assert!(crate::blocks::Break::parse(&BlockMetadata::new("~~~"), &mut parser).is_none());
158 assert!(crate::blocks::Break::parse(&BlockMetadata::new("****"), &mut parser).is_none());
159 assert!(crate::blocks::Break::parse(&BlockMetadata::new(">>>"), &mut parser).is_none());
160 }
161
162 #[test]
163 fn thematic_break_triple_apostrophe() {
164 let mut parser = Parser::default();
165
166 let mi = crate::blocks::Break::parse(&BlockMetadata::new("'''"), &mut parser).unwrap();
167
168 assert_eq!(
169 mi.item,
170 Break {
171 type_: BreakType::Thematic,
172 source: Span {
173 data: "'''",
174 line: 1,
175 col: 1,
176 offset: 0,
177 },
178 title_source: None,
179 title: None,
180 anchor: None,
181 attrlist: None,
182 }
183 );
184
185 assert_eq!(
186 mi.after,
187 Span {
188 data: "",
189 line: 1,
190 col: 4,
191 offset: 3
192 }
193 );
194
195 assert_eq!(mi.item.content_model(), ContentModel::Empty);
196 assert_eq!(mi.item.raw_context().deref(), "thematic_break");
197 assert_eq!(mi.item.type_(), BreakType::Thematic);
198 assert!(mi.item.nested_blocks().next().is_none());
199 assert!(mi.item.title_source().is_none());
200 assert!(mi.item.title().is_none());
201 assert!(mi.item.anchor().is_none());
202 assert!(mi.item.anchor_reftext().is_none());
203 assert!(mi.item.attrlist().is_none());
204 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
205 }
206
207 #[test]
208 fn thematic_break_triple_hyphen() {
209 let mut parser = Parser::default();
210
211 let mi = crate::blocks::Break::parse(&BlockMetadata::new("---"), &mut parser).unwrap();
212
213 assert_eq!(
214 mi.item,
215 Break {
216 type_: BreakType::Thematic,
217 source: Span {
218 data: "---",
219 line: 1,
220 col: 1,
221 offset: 0,
222 },
223 title_source: None,
224 title: None,
225 anchor: None,
226 attrlist: None,
227 }
228 );
229
230 assert_eq!(
231 mi.after,
232 Span {
233 data: "",
234 line: 1,
235 col: 4,
236 offset: 3
237 }
238 );
239
240 assert_eq!(mi.item.content_model(), ContentModel::Empty);
241 assert_eq!(mi.item.raw_context().deref(), "thematic_break");
242 assert_eq!(mi.item.type_(), BreakType::Thematic);
243 }
244
245 #[test]
246 fn thematic_break_spaced_hyphen() {
247 let mut parser = Parser::default();
248
249 let mi = crate::blocks::Break::parse(&BlockMetadata::new("- - -"), &mut parser).unwrap();
250
251 assert_eq!(
252 mi.item,
253 Break {
254 type_: BreakType::Thematic,
255 source: Span {
256 data: "- - -",
257 line: 1,
258 col: 1,
259 offset: 0,
260 },
261 title_source: None,
262 title: None,
263 anchor: None,
264 attrlist: None,
265 }
266 );
267
268 assert_eq!(mi.item.type_(), BreakType::Thematic);
269 }
270
271 #[test]
272 fn thematic_break_triple_asterisk() {
273 let mut parser = Parser::default();
274
275 let mi = crate::blocks::Break::parse(&BlockMetadata::new("***"), &mut parser).unwrap();
276
277 assert_eq!(
278 mi.item,
279 Break {
280 type_: BreakType::Thematic,
281 source: Span {
282 data: "***",
283 line: 1,
284 col: 1,
285 offset: 0,
286 },
287 title_source: None,
288 title: None,
289 anchor: None,
290 attrlist: None,
291 }
292 );
293
294 assert_eq!(mi.item.content_model(), ContentModel::Empty);
295 assert_eq!(mi.item.raw_context().deref(), "thematic_break");
296 assert_eq!(mi.item.type_(), BreakType::Thematic);
297 }
298
299 #[test]
300 fn thematic_break_spaced_asterisk() {
301 let mut parser = Parser::default();
302
303 let mi = crate::blocks::Break::parse(&BlockMetadata::new("* * *"), &mut parser).unwrap();
304
305 assert_eq!(
306 mi.item,
307 Break {
308 type_: BreakType::Thematic,
309 source: Span {
310 data: "* * *",
311 line: 1,
312 col: 1,
313 offset: 0,
314 },
315 title_source: None,
316 title: None,
317 anchor: None,
318 attrlist: None,
319 }
320 );
321
322 assert_eq!(mi.item.type_(), BreakType::Thematic);
323 }
324
325 #[test]
326 fn page_break() {
327 let mut parser = Parser::default();
328
329 let mi = crate::blocks::Break::parse(&BlockMetadata::new("<<<"), &mut parser).unwrap();
330
331 assert_eq!(
332 mi.item,
333 Break {
334 type_: BreakType::Page,
335 source: Span {
336 data: "<<<",
337 line: 1,
338 col: 1,
339 offset: 0,
340 },
341 title_source: None,
342 title: None,
343 anchor: None,
344 attrlist: None,
345 }
346 );
347
348 assert_eq!(
349 mi.after,
350 Span {
351 data: "",
352 line: 1,
353 col: 4,
354 offset: 3
355 }
356 );
357
358 assert_eq!(mi.item.content_model(), ContentModel::Empty);
359 assert_eq!(mi.item.raw_context().deref(), "page_break");
360 assert_eq!(mi.item.type_(), BreakType::Page);
361 assert!(mi.item.nested_blocks().next().is_none());
362 assert!(mi.item.title_source().is_none());
363 assert!(mi.item.title().is_none());
364 assert!(mi.item.anchor().is_none());
365 assert!(mi.item.anchor_reftext().is_none());
366 assert!(mi.item.attrlist().is_none());
367 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
368 }
369
370 #[test]
371 fn thematic_break_with_trailing_whitespace() {
372 let mut parser = Parser::default();
373
374 let mi = crate::blocks::Break::parse(&BlockMetadata::new("''' "), &mut parser).unwrap();
375
376 assert_eq!(
377 mi.item,
378 Break {
379 type_: BreakType::Thematic,
380 source: Span {
381 data: "'''",
382 line: 1,
383 col: 1,
384 offset: 0,
385 },
386 title_source: None,
387 title: None,
388 anchor: None,
389 attrlist: None,
390 }
391 );
392
393 assert_eq!(mi.item.type_(), BreakType::Thematic);
394 }
395
396 #[test]
397 fn page_break_with_trailing_whitespace() {
398 let mut parser = Parser::default();
399
400 let mi = crate::blocks::Break::parse(&BlockMetadata::new("<<< "), &mut parser).unwrap();
401
402 assert_eq!(
403 mi.item,
404 Break {
405 type_: BreakType::Page,
406 source: Span {
407 data: "<<<",
408 line: 1,
409 col: 1,
410 offset: 0,
411 },
412 title_source: None,
413 title: None,
414 anchor: None,
415 attrlist: None,
416 }
417 );
418
419 assert_eq!(mi.item.type_(), BreakType::Page);
420 }
421
422 mod break_type {
423 mod impl_debug {
424 use crate::blocks::BreakType;
425
426 #[test]
427 fn thematic() {
428 let break_type = BreakType::Thematic;
429 let debug_output = format!("{:?}", break_type);
430 assert_eq!(debug_output, "BreakType::Thematic");
431 }
432
433 #[test]
434 fn page() {
435 let break_type = BreakType::Page;
436 let debug_output = format!("{:?}", break_type);
437 assert_eq!(debug_output, "BreakType::Page");
438 }
439 }
440 }
441}