Skip to main content

asciidoc_parser/blocks/
break.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::Attrlist,
4    blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
5    span::MatchedItem,
6    strings::CowStr,
7};
8
9/// A break block is used to represent a thematic or page break macro.
10#[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/// A break may be one of two different types.
21#[derive(Clone, Copy, Eq, PartialEq)]
22pub enum BreakType {
23    /// A thematic break (aka horizontal rule).
24    Thematic,
25
26    /// A hint to the converter to insert a page break.
27    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    /// Return the type of break detected.
72    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        // Silly test to mark the #[derive(...)] line as covered.
131        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}