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 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 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}