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