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 pretty_assertions_sorted::assert_eq;
124
125    use crate::{
126        Parser,
127        blocks::{BreakType, ContentModel, IsBlock, metadata::BlockMetadata},
128        content::SubstitutionGroup,
129        tests::prelude::*,
130    };
131
132    #[test]
133    fn impl_clone() {
134        // Silly test to mark the #[derive(...)] line as covered.
135        let mut parser = Parser::default();
136
137        let b1 = crate::blocks::Break::parse(&BlockMetadata::new("'''"), &mut parser)
138            .unwrap()
139            .item;
140
141        let b2 = b1.clone();
142        assert_eq!(b1, b2);
143    }
144
145    #[test]
146    fn err_empty_source() {
147        let mut parser = Parser::default();
148        assert!(crate::blocks::Break::parse(&BlockMetadata::new(""), &mut parser).is_none());
149    }
150
151    #[test]
152    fn err_only_spaces() {
153        let mut parser = Parser::default();
154        assert!(crate::blocks::Break::parse(&BlockMetadata::new("    "), &mut parser).is_none());
155    }
156
157    #[test]
158    fn err_unknown_break_pattern() {
159        let mut parser = Parser::default();
160        assert!(crate::blocks::Break::parse(&BlockMetadata::new("=="), &mut parser).is_none());
161        assert!(crate::blocks::Break::parse(&BlockMetadata::new("~~~"), &mut parser).is_none());
162        assert!(crate::blocks::Break::parse(&BlockMetadata::new("****"), &mut parser).is_none());
163        assert!(crate::blocks::Break::parse(&BlockMetadata::new(">>>"), &mut parser).is_none());
164    }
165
166    #[test]
167    fn thematic_break_triple_apostrophe() {
168        let mut parser = Parser::default();
169
170        let mi = crate::blocks::Break::parse(&BlockMetadata::new("'''"), &mut parser).unwrap();
171
172        assert_eq!(
173            mi.item,
174            Break {
175                type_: BreakType::Thematic,
176                source: Span {
177                    data: "'''",
178                    line: 1,
179                    col: 1,
180                    offset: 0,
181                },
182                title_source: None,
183                title: None,
184                anchor: None,
185                attrlist: None,
186            }
187        );
188
189        assert_eq!(
190            mi.after,
191            Span {
192                data: "",
193                line: 1,
194                col: 4,
195                offset: 3
196            }
197        );
198
199        assert_eq!(mi.item.content_model(), ContentModel::Empty);
200        assert_eq!(mi.item.raw_context().deref(), "thematic_break");
201        assert_eq!(mi.item.type_(), BreakType::Thematic);
202        assert!(mi.item.nested_blocks().next().is_none());
203        assert!(mi.item.title_source().is_none());
204        assert!(mi.item.title().is_none());
205        assert!(mi.item.anchor().is_none());
206        assert!(mi.item.anchor_reftext().is_none());
207        assert!(mi.item.attrlist().is_none());
208        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
209    }
210
211    #[test]
212    fn thematic_break_triple_hyphen() {
213        let mut parser = Parser::default();
214
215        let mi = crate::blocks::Break::parse(&BlockMetadata::new("---"), &mut parser).unwrap();
216
217        assert_eq!(
218            mi.item,
219            Break {
220                type_: BreakType::Thematic,
221                source: Span {
222                    data: "---",
223                    line: 1,
224                    col: 1,
225                    offset: 0,
226                },
227                title_source: None,
228                title: None,
229                anchor: None,
230                attrlist: None,
231            }
232        );
233
234        assert_eq!(
235            mi.after,
236            Span {
237                data: "",
238                line: 1,
239                col: 4,
240                offset: 3
241            }
242        );
243
244        assert_eq!(mi.item.content_model(), ContentModel::Empty);
245        assert_eq!(mi.item.raw_context().deref(), "thematic_break");
246        assert_eq!(mi.item.type_(), BreakType::Thematic);
247    }
248
249    #[test]
250    fn thematic_break_spaced_hyphen() {
251        let mut parser = Parser::default();
252
253        let mi = crate::blocks::Break::parse(&BlockMetadata::new("- - -"), &mut parser).unwrap();
254
255        assert_eq!(
256            mi.item,
257            Break {
258                type_: BreakType::Thematic,
259                source: Span {
260                    data: "- - -",
261                    line: 1,
262                    col: 1,
263                    offset: 0,
264                },
265                title_source: None,
266                title: None,
267                anchor: None,
268                attrlist: None,
269            }
270        );
271
272        assert_eq!(mi.item.type_(), BreakType::Thematic);
273    }
274
275    #[test]
276    fn thematic_break_triple_asterisk() {
277        let mut parser = Parser::default();
278
279        let mi = crate::blocks::Break::parse(&BlockMetadata::new("***"), &mut parser).unwrap();
280
281        assert_eq!(
282            mi.item,
283            Break {
284                type_: BreakType::Thematic,
285                source: Span {
286                    data: "***",
287                    line: 1,
288                    col: 1,
289                    offset: 0,
290                },
291                title_source: None,
292                title: None,
293                anchor: None,
294                attrlist: None,
295            }
296        );
297
298        assert_eq!(mi.item.content_model(), ContentModel::Empty);
299        assert_eq!(mi.item.raw_context().deref(), "thematic_break");
300        assert_eq!(mi.item.type_(), BreakType::Thematic);
301    }
302
303    #[test]
304    fn thematic_break_spaced_asterisk() {
305        let mut parser = Parser::default();
306
307        let mi = crate::blocks::Break::parse(&BlockMetadata::new("* * *"), &mut parser).unwrap();
308
309        assert_eq!(
310            mi.item,
311            Break {
312                type_: BreakType::Thematic,
313                source: Span {
314                    data: "* * *",
315                    line: 1,
316                    col: 1,
317                    offset: 0,
318                },
319                title_source: None,
320                title: None,
321                anchor: None,
322                attrlist: None,
323            }
324        );
325
326        assert_eq!(mi.item.type_(), BreakType::Thematic);
327    }
328
329    #[test]
330    fn page_break() {
331        let mut parser = Parser::default();
332
333        let mi = crate::blocks::Break::parse(&BlockMetadata::new("<<<"), &mut parser).unwrap();
334
335        assert_eq!(
336            mi.item,
337            Break {
338                type_: BreakType::Page,
339                source: Span {
340                    data: "<<<",
341                    line: 1,
342                    col: 1,
343                    offset: 0,
344                },
345                title_source: None,
346                title: None,
347                anchor: None,
348                attrlist: None,
349            }
350        );
351
352        assert_eq!(
353            mi.after,
354            Span {
355                data: "",
356                line: 1,
357                col: 4,
358                offset: 3
359            }
360        );
361
362        assert_eq!(mi.item.content_model(), ContentModel::Empty);
363        assert_eq!(mi.item.raw_context().deref(), "page_break");
364        assert_eq!(mi.item.type_(), BreakType::Page);
365        assert!(mi.item.nested_blocks().next().is_none());
366        assert!(mi.item.title_source().is_none());
367        assert!(mi.item.title().is_none());
368        assert!(mi.item.anchor().is_none());
369        assert!(mi.item.anchor_reftext().is_none());
370        assert!(mi.item.attrlist().is_none());
371        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
372    }
373
374    #[test]
375    fn thematic_break_with_trailing_whitespace() {
376        let mut parser = Parser::default();
377
378        let mi = crate::blocks::Break::parse(&BlockMetadata::new("'''   "), &mut parser).unwrap();
379
380        assert_eq!(
381            mi.item,
382            Break {
383                type_: BreakType::Thematic,
384                source: Span {
385                    data: "'''",
386                    line: 1,
387                    col: 1,
388                    offset: 0,
389                },
390                title_source: None,
391                title: None,
392                anchor: None,
393                attrlist: None,
394            }
395        );
396
397        assert_eq!(mi.item.type_(), BreakType::Thematic);
398    }
399
400    #[test]
401    fn page_break_with_trailing_whitespace() {
402        let mut parser = Parser::default();
403
404        let mi = crate::blocks::Break::parse(&BlockMetadata::new("<<<   "), &mut parser).unwrap();
405
406        assert_eq!(
407            mi.item,
408            Break {
409                type_: BreakType::Page,
410                source: Span {
411                    data: "<<<",
412                    line: 1,
413                    col: 1,
414                    offset: 0,
415                },
416                title_source: None,
417                title: None,
418                anchor: None,
419                attrlist: None,
420            }
421        );
422
423        assert_eq!(mi.item.type_(), BreakType::Page);
424    }
425
426    mod break_type {
427        mod impl_debug {
428            use pretty_assertions_sorted::assert_eq;
429
430            use crate::blocks::BreakType;
431
432            #[test]
433            fn thematic() {
434                let break_type = BreakType::Thematic;
435                let debug_output = format!("{:?}", break_type);
436                assert_eq!(debug_output, "BreakType::Thematic");
437            }
438
439            #[test]
440            fn page() {
441                let break_type = BreakType::Page;
442                let debug_output = format!("{:?}", break_type);
443                assert_eq!(debug_output, "BreakType::Page");
444            }
445        }
446    }
447}