1use crate::{
2 HasSpan, Parser, Span,
3 attributes::Attrlist,
4 blocks::{ChildBlocks, ContentModel, IsBlock, metadata::BlockMetadata},
5 content::Content,
6 span::MatchedItem,
7 strings::CowStr,
8};
9
10#[derive(Clone, Debug, Eq, Hash, PartialEq)]
12pub struct Break<'src> {
13 type_: BreakType,
14 source: Span<'src>,
15 title_source: Option<Span<'src>>,
16 title: Option<Content<'src>>,
17 anchor: Option<Span<'src>>,
18 attrlist: Option<Attrlist<'src>>,
19}
20
21#[derive(Clone, Copy, Eq, Hash, PartialEq)]
23pub enum BreakType {
24 Thematic,
26
27 Page,
29}
30
31impl std::fmt::Debug for BreakType {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 BreakType::Thematic => write!(f, "BreakType::Thematic"),
35 BreakType::Page => write!(f, "BreakType::Page"),
36 }
37 }
38}
39
40impl<'src> Break<'src> {
41 pub fn child_blocks(&'src self) -> ChildBlocks<'src> {
47 ChildBlocks::empty()
48 }
49
50 pub(crate) fn title_content_mut(&mut self) -> Option<&mut Content<'src>> {
58 self.title.as_mut()
59 }
60
61 pub(crate) fn parse(
62 metadata: &BlockMetadata<'src>,
63 _parser: &mut Parser,
64 ) -> Option<MatchedItem<'src, Self>> {
65 let line = metadata.block_start.take_normalized_line();
66 let data = line.item.data();
67
68 let type_ = match data {
69 "---" | "- - -" | "***" | "* * *" | "___" | "_ _ _" => BreakType::Thematic,
83 "<<<" => BreakType::Page,
84
85 _ if data.len() >= 3 && data.bytes().all(|b| b == b'\'') => BreakType::Thematic,
90 _ => {
91 return None;
92 }
93 };
94
95 let source: Span = metadata.source.trim_remainder(line.after);
96 let source = source.slice(0..source.trim().len());
97
98 Some(MatchedItem {
99 item: Self {
100 type_,
101 source,
102 title_source: metadata.title_source,
103 title: metadata.title.clone(),
104 anchor: metadata.anchor,
105 attrlist: metadata.attrlist.clone(),
106 },
107
108 after: line.after.discard_empty_lines(),
109 })
110 }
111
112 pub fn type_(&self) -> BreakType {
114 self.type_
115 }
116}
117
118impl<'src> IsBlock<'src> for Break<'src> {
119 fn content_model(&self) -> ContentModel {
120 ContentModel::Empty
121 }
122
123 fn raw_context(&self) -> CowStr<'src> {
124 match self.type_ {
125 BreakType::Thematic => "thematic_break",
126 BreakType::Page => "page_break",
127 }
128 .into()
129 }
130
131 fn title_source(&'src self) -> Option<Span<'src>> {
132 self.title_source
133 }
134
135 fn title(&self) -> Option<&str> {
136 self.title.as_ref().map(Content::rendered_str)
137 }
138
139 fn anchor(&'src self) -> Option<Span<'src>> {
140 self.anchor
141 }
142
143 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
144 None
145 }
146
147 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
148 self.attrlist.as_ref()
149 }
150}
151
152impl<'src> HasSpan<'src> for Break<'src> {
153 fn span(&self) -> Span<'src> {
154 self.source
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 #![allow(clippy::unwrap_used)]
161
162 use std::ops::Deref;
163
164 use crate::{
165 blocks::{BreakType, ContentModel, metadata::BlockMetadata},
166 tests::prelude::*,
167 };
168
169 #[test]
170 fn impl_clone() {
171 let mut parser = Parser::default();
173
174 let b1 = crate::blocks::Break::parse(&BlockMetadata::new("'''"), &mut parser)
175 .unwrap()
176 .item;
177
178 let b2 = b1.clone();
179 assert_eq!(b1, b2);
180 }
181
182 #[test]
183 fn err_empty_source() {
184 let mut parser = Parser::default();
185 assert!(crate::blocks::Break::parse(&BlockMetadata::new(""), &mut parser).is_none());
186 }
187
188 #[test]
189 fn err_only_spaces() {
190 let mut parser = Parser::default();
191 assert!(crate::blocks::Break::parse(&BlockMetadata::new(" "), &mut parser).is_none());
192 }
193
194 #[test]
195 fn err_unknown_break_pattern() {
196 let mut parser = Parser::default();
197 assert!(crate::blocks::Break::parse(&BlockMetadata::new("=="), &mut parser).is_none());
198 assert!(crate::blocks::Break::parse(&BlockMetadata::new("~~~"), &mut parser).is_none());
199 assert!(crate::blocks::Break::parse(&BlockMetadata::new("****"), &mut parser).is_none());
200 assert!(crate::blocks::Break::parse(&BlockMetadata::new(">>>"), &mut parser).is_none());
201 }
202
203 #[test]
204 fn thematic_break_triple_apostrophe() {
205 let mut parser = Parser::default();
206
207 let mi = crate::blocks::Break::parse(&BlockMetadata::new("'''"), &mut parser).unwrap();
208
209 assert_eq!(
210 mi.item,
211 Break {
212 type_: BreakType::Thematic,
213 source: Span {
214 data: "'''",
215 line: 1,
216 col: 1,
217 offset: 0,
218 },
219 title_source: None,
220 title: None,
221 anchor: None,
222 attrlist: None,
223 }
224 );
225
226 assert_eq!(
227 mi.after,
228 Span {
229 data: "",
230 line: 1,
231 col: 4,
232 offset: 3
233 }
234 );
235
236 assert_eq!(mi.item.content_model(), ContentModel::Empty);
237 assert_eq!(mi.item.raw_context().deref(), "thematic_break");
238 assert_eq!(mi.item.type_(), BreakType::Thematic);
239 assert!(mi.item.child_blocks().next().is_none());
240 assert!(mi.item.title_source().is_none());
241 assert!(mi.item.title().is_none());
242 assert!(mi.item.anchor().is_none());
243 assert!(mi.item.anchor_reftext().is_none());
244 assert!(mi.item.attrlist().is_none());
245 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
246 }
247
248 #[test]
249 fn thematic_break_extended_apostrophes() {
250 for line in ["''''", "'''''", "''''''"] {
253 let mut parser = Parser::default();
254 let mi = crate::blocks::Break::parse(&BlockMetadata::new(line), &mut parser).unwrap();
255 assert_eq!(
256 mi.item.type_(),
257 BreakType::Thematic,
258 "{line:?} should be a thematic break"
259 );
260 assert_eq!(mi.item.raw_context().deref(), "thematic_break");
261 }
262
263 let mut parser = Parser::default();
265 assert!(crate::blocks::Break::parse(&BlockMetadata::new("''"), &mut parser).is_none());
266 }
267
268 #[test]
269 fn thematic_break_triple_hyphen() {
270 let mut parser = Parser::default();
271
272 let mi = crate::blocks::Break::parse(&BlockMetadata::new("---"), &mut parser).unwrap();
273
274 assert_eq!(
275 mi.item,
276 Break {
277 type_: BreakType::Thematic,
278 source: Span {
279 data: "---",
280 line: 1,
281 col: 1,
282 offset: 0,
283 },
284 title_source: None,
285 title: None,
286 anchor: None,
287 attrlist: None,
288 }
289 );
290
291 assert_eq!(
292 mi.after,
293 Span {
294 data: "",
295 line: 1,
296 col: 4,
297 offset: 3
298 }
299 );
300
301 assert_eq!(mi.item.content_model(), ContentModel::Empty);
302 assert_eq!(mi.item.raw_context().deref(), "thematic_break");
303 assert_eq!(mi.item.type_(), BreakType::Thematic);
304 }
305
306 #[test]
307 fn thematic_break_spaced_hyphen() {
308 let mut parser = Parser::default();
309
310 let mi = crate::blocks::Break::parse(&BlockMetadata::new("- - -"), &mut parser).unwrap();
311
312 assert_eq!(
313 mi.item,
314 Break {
315 type_: BreakType::Thematic,
316 source: Span {
317 data: "- - -",
318 line: 1,
319 col: 1,
320 offset: 0,
321 },
322 title_source: None,
323 title: None,
324 anchor: None,
325 attrlist: None,
326 }
327 );
328
329 assert_eq!(mi.item.type_(), BreakType::Thematic);
330 }
331
332 #[test]
333 fn thematic_break_triple_asterisk() {
334 let mut parser = Parser::default();
335
336 let mi = crate::blocks::Break::parse(&BlockMetadata::new("***"), &mut parser).unwrap();
337
338 assert_eq!(
339 mi.item,
340 Break {
341 type_: BreakType::Thematic,
342 source: Span {
343 data: "***",
344 line: 1,
345 col: 1,
346 offset: 0,
347 },
348 title_source: None,
349 title: None,
350 anchor: None,
351 attrlist: None,
352 }
353 );
354
355 assert_eq!(mi.item.content_model(), ContentModel::Empty);
356 assert_eq!(mi.item.raw_context().deref(), "thematic_break");
357 assert_eq!(mi.item.type_(), BreakType::Thematic);
358 }
359
360 #[test]
361 fn thematic_break_spaced_asterisk() {
362 let mut parser = Parser::default();
363
364 let mi = crate::blocks::Break::parse(&BlockMetadata::new("* * *"), &mut parser).unwrap();
365
366 assert_eq!(
367 mi.item,
368 Break {
369 type_: BreakType::Thematic,
370 source: Span {
371 data: "* * *",
372 line: 1,
373 col: 1,
374 offset: 0,
375 },
376 title_source: None,
377 title: None,
378 anchor: None,
379 attrlist: None,
380 }
381 );
382
383 assert_eq!(mi.item.type_(), BreakType::Thematic);
384 }
385
386 #[test]
387 fn thematic_break_triple_underscore() {
388 let mut parser = Parser::default();
392
393 let mi = crate::blocks::Break::parse(&BlockMetadata::new("___"), &mut parser).unwrap();
394
395 assert_eq!(
396 mi.item,
397 Break {
398 type_: BreakType::Thematic,
399 source: Span {
400 data: "___",
401 line: 1,
402 col: 1,
403 offset: 0,
404 },
405 title_source: None,
406 title: None,
407 anchor: None,
408 attrlist: None,
409 }
410 );
411
412 assert_eq!(mi.item.content_model(), ContentModel::Empty);
413 assert_eq!(mi.item.raw_context().deref(), "thematic_break");
414 assert_eq!(mi.item.type_(), BreakType::Thematic);
415 }
416
417 #[test]
418 fn thematic_break_spaced_underscore() {
419 let mut parser = Parser::default();
420
421 let mi = crate::blocks::Break::parse(&BlockMetadata::new("_ _ _"), &mut parser).unwrap();
422
423 assert_eq!(
424 mi.item,
425 Break {
426 type_: BreakType::Thematic,
427 source: Span {
428 data: "_ _ _",
429 line: 1,
430 col: 1,
431 offset: 0,
432 },
433 title_source: None,
434 title: None,
435 anchor: None,
436 attrlist: None,
437 }
438 );
439
440 assert_eq!(mi.item.type_(), BreakType::Thematic);
441 }
442
443 #[test]
444 fn four_underscores_is_not_a_thematic_break() {
445 let mut parser = Parser::default();
449 assert!(crate::blocks::Break::parse(&BlockMetadata::new("____"), &mut parser).is_none());
450
451 let mut parser = Parser::default();
452 assert!(crate::blocks::Break::parse(&BlockMetadata::new("_____"), &mut parser).is_none());
453 }
454
455 #[test]
456 fn page_break() {
457 let mut parser = Parser::default();
458
459 let mi = crate::blocks::Break::parse(&BlockMetadata::new("<<<"), &mut parser).unwrap();
460
461 assert_eq!(
462 mi.item,
463 Break {
464 type_: BreakType::Page,
465 source: Span {
466 data: "<<<",
467 line: 1,
468 col: 1,
469 offset: 0,
470 },
471 title_source: None,
472 title: None,
473 anchor: None,
474 attrlist: None,
475 }
476 );
477
478 assert_eq!(
479 mi.after,
480 Span {
481 data: "",
482 line: 1,
483 col: 4,
484 offset: 3
485 }
486 );
487
488 assert_eq!(mi.item.content_model(), ContentModel::Empty);
489 assert_eq!(mi.item.raw_context().deref(), "page_break");
490 assert_eq!(mi.item.type_(), BreakType::Page);
491 assert!(mi.item.child_blocks().next().is_none());
492 assert!(mi.item.title_source().is_none());
493 assert!(mi.item.title().is_none());
494 assert!(mi.item.anchor().is_none());
495 assert!(mi.item.anchor_reftext().is_none());
496 assert!(mi.item.attrlist().is_none());
497 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
498 }
499
500 #[test]
501 fn thematic_break_with_trailing_whitespace() {
502 let mut parser = Parser::default();
503
504 let mi = crate::blocks::Break::parse(&BlockMetadata::new("''' "), &mut parser).unwrap();
505
506 assert_eq!(
507 mi.item,
508 Break {
509 type_: BreakType::Thematic,
510 source: Span {
511 data: "'''",
512 line: 1,
513 col: 1,
514 offset: 0,
515 },
516 title_source: None,
517 title: None,
518 anchor: None,
519 attrlist: None,
520 }
521 );
522
523 assert_eq!(mi.item.type_(), BreakType::Thematic);
524 }
525
526 #[test]
527 fn page_break_with_trailing_whitespace() {
528 let mut parser = Parser::default();
529
530 let mi = crate::blocks::Break::parse(&BlockMetadata::new("<<< "), &mut parser).unwrap();
531
532 assert_eq!(
533 mi.item,
534 Break {
535 type_: BreakType::Page,
536 source: Span {
537 data: "<<<",
538 line: 1,
539 col: 1,
540 offset: 0,
541 },
542 title_source: None,
543 title: None,
544 anchor: None,
545 attrlist: None,
546 }
547 );
548
549 assert_eq!(mi.item.type_(), BreakType::Page);
550 }
551
552 mod break_type {
553 mod impl_debug {
554 use crate::blocks::BreakType;
555
556 #[test]
557 fn thematic() {
558 let break_type = BreakType::Thematic;
559 let debug_output = format!("{:?}", break_type);
560 assert_eq!(debug_output, "BreakType::Thematic");
561 }
562
563 #[test]
564 fn page() {
565 let break_type = BreakType::Page;
566 let debug_output = format!("{:?}", break_type);
567 assert_eq!(debug_output, "BreakType::Page");
568 }
569 }
570 }
571}