1use crate::{
2 HasSpan, Parser, Span,
3 attributes::{Attrlist, AttrlistContext},
4 blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
5 span::MatchedItem,
6 strings::CowStr,
7 warnings::{MatchAndWarnings, Warning, WarningType},
8};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct MediaBlock<'src> {
13 type_: MediaType,
14 target: Span<'src>,
15 macro_attrlist: Attrlist<'src>,
16 source: Span<'src>,
17 title_source: Option<Span<'src>>,
18 title: Option<String>,
19 anchor: Option<Span<'src>>,
20 anchor_reftext: Option<Span<'src>>,
21 attrlist: Option<Attrlist<'src>>,
22}
23
24#[derive(Clone, Copy, Eq, PartialEq)]
26pub enum MediaType {
27 Image,
29
30 Video,
32
33 Audio,
35}
36
37impl std::fmt::Debug for MediaType {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 MediaType::Image => write!(f, "MediaType::Image"),
41 MediaType::Video => write!(f, "MediaType::Video"),
42 MediaType::Audio => write!(f, "MediaType::Audio"),
43 }
44 }
45}
46
47impl<'src> MediaBlock<'src> {
48 pub(crate) fn parse(
49 metadata: &BlockMetadata<'src>,
50 parser: &mut Parser,
51 ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
52 let line = metadata.block_start.take_normalized_line();
53
54 if !line.item.ends_with(']') {
56 return MatchAndWarnings {
57 item: None,
58 warnings: vec![],
59 };
60 }
61
62 let Some(name) = line.item.take_ident() else {
63 return MatchAndWarnings {
64 item: None,
65 warnings: vec![],
66 };
67 };
68
69 let type_ = match name.item.data() {
70 "image" => MediaType::Image,
71 "video" => MediaType::Video,
72 "audio" => MediaType::Audio,
73 _ => {
74 return MatchAndWarnings {
75 item: None,
76 warnings: vec![],
77 };
78 }
79 };
80
81 let Some(colons) = name.after.take_prefix("::") else {
82 return MatchAndWarnings {
83 item: None,
84 warnings: vec![Warning {
85 source: name.after,
86 warning: WarningType::MacroMissingDoubleColon,
87 }],
88 };
89 };
90
91 let target = colons.after.take_while(|c| c != '[');
93
94 if target.item.is_empty() {
95 return MatchAndWarnings {
96 item: None,
97 warnings: vec![Warning {
98 source: target.after,
99 warning: WarningType::MediaMacroMissingTarget,
100 }],
101 };
102 }
103
104 let Some(open_brace) = target.after.take_prefix("[") else {
105 return MatchAndWarnings {
106 item: None,
107 warnings: vec![Warning {
108 source: target.after,
109 warning: WarningType::MacroMissingAttributeList,
110 }],
111 };
112 };
113
114 let attrlist = open_brace.after.slice(0..open_brace.after.len() - 1);
115 let macro_attrlist = Attrlist::parse(attrlist, parser, AttrlistContext::Inline);
118
119 let source: Span = metadata.source.trim_remainder(line.after);
120 let source = source.slice(0..source.trim().len());
121
122 MatchAndWarnings {
123 item: Some(MatchedItem {
124 item: Self {
125 type_,
126 target: target.item,
127 macro_attrlist: macro_attrlist.item.item,
128 source,
129 title_source: metadata.title_source,
130 title: metadata.title.clone(),
131 anchor: metadata.anchor,
132 anchor_reftext: None,
133 attrlist: metadata.attrlist.clone(),
134 },
135
136 after: line.after.discard_empty_lines(),
137 }),
138 warnings: macro_attrlist.warnings,
139 }
140 }
141
142 pub fn type_(&self) -> MediaType {
144 self.type_
145 }
146
147 pub fn target(&'src self) -> Option<&'src Span<'src>> {
149 Some(&self.target)
150 }
151
152 pub fn macro_attrlist(&'src self) -> &'src Attrlist<'src> {
162 &self.macro_attrlist
163 }
164}
165
166impl<'src> IsBlock<'src> for MediaBlock<'src> {
167 fn content_model(&self) -> ContentModel {
168 ContentModel::Empty
169 }
170
171 fn raw_context(&self) -> CowStr<'src> {
172 match self.type_ {
173 MediaType::Audio => "audio",
174 MediaType::Image => "image",
175 MediaType::Video => "video",
176 }
177 .into()
178 }
179
180 fn title_source(&'src self) -> Option<Span<'src>> {
181 self.title_source
182 }
183
184 fn title(&self) -> Option<&str> {
185 self.title.as_deref()
186 }
187
188 fn anchor(&'src self) -> Option<Span<'src>> {
189 self.anchor
190 }
191
192 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
193 self.anchor_reftext
194 }
195
196 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
197 self.attrlist.as_ref()
198 }
199}
200
201impl<'src> HasSpan<'src> for MediaBlock<'src> {
202 fn span(&self) -> Span<'src> {
203 self.source
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 #![allow(clippy::unwrap_used)]
210
211 use std::ops::Deref;
212
213 use pretty_assertions_sorted::assert_eq;
214
215 use crate::{
216 Parser,
217 blocks::{ContentModel, IsBlock, MediaType, metadata::BlockMetadata},
218 content::SubstitutionGroup,
219 tests::prelude::*,
220 warnings::WarningType,
221 };
222
223 #[test]
224 fn impl_clone() {
225 let mut parser = Parser::default();
227
228 let b1 =
229 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::foo.jpg[]"), &mut parser)
230 .unwrap_if_no_warnings()
231 .unwrap()
232 .item;
233
234 let b2 = b1.clone();
235 assert_eq!(b1, b2);
236 }
237
238 #[test]
239 fn err_empty_source() {
240 let mut parser = Parser::default();
241 assert!(
242 crate::blocks::MediaBlock::parse(&BlockMetadata::new(""), &mut parser)
243 .unwrap_if_no_warnings()
244 .is_none()
245 );
246 }
247
248 #[test]
249 fn err_only_spaces() {
250 let mut parser = Parser::default();
251 assert!(
252 crate::blocks::MediaBlock::parse(&BlockMetadata::new(" "), &mut parser)
253 .unwrap_if_no_warnings()
254 .is_none()
255 );
256 }
257
258 #[test]
259 fn err_macro_name_not_ident() {
260 let mut parser = Parser::default();
261 let maw = crate::blocks::MediaBlock::parse(
262 &BlockMetadata::new("98xyz::bar[blah,blap]"),
263 &mut parser,
264 );
265
266 assert!(maw.item.is_none());
267 assert!(maw.warnings.is_empty());
268 }
269
270 #[test]
271 fn err_missing_double_colon() {
272 let mut parser = Parser::default();
273 let maw = crate::blocks::MediaBlock::parse(
274 &BlockMetadata::new("image:bar[blah,blap]"),
275 &mut parser,
276 );
277
278 assert!(maw.item.is_none());
279
280 assert_eq!(
281 maw.warnings,
282 vec![Warning {
283 source: Span {
284 data: ":bar[blah,blap]",
285 line: 1,
286 col: 6,
287 offset: 5,
288 },
289 warning: WarningType::MacroMissingDoubleColon,
290 }]
291 );
292 }
293
294 #[test]
295 fn err_missing_macro_attrlist() {
296 let mut parser = Parser::default();
297 let maw = crate::blocks::MediaBlock::parse(
298 &BlockMetadata::new("image::barblah,blap]"),
299 &mut parser,
300 );
301
302 assert!(maw.item.is_none());
303
304 assert_eq!(
305 maw.warnings,
306 vec![Warning {
307 source: Span {
308 data: "",
309 line: 1,
310 col: 21,
311 offset: 20,
312 },
313 warning: WarningType::MacroMissingAttributeList,
314 }]
315 );
316 }
317
318 #[test]
319 fn err_unknown_type() {
320 let mut parser = Parser::default();
321 assert!(
322 crate::blocks::MediaBlock::parse(&BlockMetadata::new("imagex::bar[]"), &mut parser)
323 .unwrap_if_no_warnings()
324 .is_none()
325 );
326 }
327
328 #[test]
329 fn err_no_attr_list() {
330 let mut parser = Parser::default();
331 assert!(
332 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar"), &mut parser)
333 .unwrap_if_no_warnings()
334 .is_none()
335 );
336 }
337
338 #[test]
339 fn err_attr_list_not_closed() {
340 let mut parser = Parser::default();
341 assert!(
342 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah"), &mut parser)
343 .unwrap_if_no_warnings()
344 .is_none()
345 );
346 }
347
348 #[test]
349 fn err_unexpected_after_attr_list() {
350 let mut parser = Parser::default();
351 assert!(
352 crate::blocks::MediaBlock::parse(
353 &BlockMetadata::new("image::bar[blah]bonus"),
354 &mut parser
355 )
356 .unwrap_if_no_warnings()
357 .is_none()
358 );
359 }
360
361 #[test]
362 fn simplest_block_macro() {
363 let mut parser = Parser::default();
364
365 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::[]"), &mut parser);
366 assert!(mi.item.is_none());
367
368 assert_eq!(
369 mi.warnings,
370 vec![Warning {
371 source: Span {
372 data: "[]",
373 line: 1,
374 col: 8,
375 offset: 7,
376 },
377 warning: WarningType::MediaMacroMissingTarget,
378 }]
379 );
380 }
381
382 #[test]
383 fn has_target() {
384 let mut parser = Parser::default();
385
386 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[]"), &mut parser)
387 .unwrap_if_no_warnings()
388 .unwrap();
389
390 assert_eq!(
391 mi.item,
392 MediaBlock {
393 type_: MediaType::Image,
394 target: Span {
395 data: "bar",
396 line: 1,
397 col: 8,
398 offset: 7,
399 },
400 macro_attrlist: Attrlist {
401 attributes: &[],
402 anchor: None,
403 source: Span {
404 data: "",
405 line: 1,
406 col: 12,
407 offset: 11,
408 }
409 },
410 source: Span {
411 data: "image::bar[]",
412 line: 1,
413 col: 1,
414 offset: 0,
415 },
416 title_source: None,
417 title: None,
418 anchor: None,
419 anchor_reftext: None,
420 attrlist: None,
421 }
422 );
423
424 assert_eq!(
425 mi.after,
426 Span {
427 data: "",
428 line: 1,
429 col: 13,
430 offset: 12
431 }
432 );
433
434 assert_eq!(mi.item.content_model(), ContentModel::Empty);
435 assert_eq!(mi.item.raw_context().deref(), "image");
436 assert!(mi.item.nested_blocks().next().is_none());
437 assert!(mi.item.title_source().is_none());
438 assert!(mi.item.title().is_none());
439 assert!(mi.item.anchor().is_none());
440 assert!(mi.item.anchor_reftext().is_none());
441 assert!(mi.item.attrlist().is_none());
442 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
443 }
444
445 #[test]
446 fn has_target_and_attrlist() {
447 let mut parser = Parser::default();
448
449 let mi =
450 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah]"), &mut parser)
451 .unwrap_if_no_warnings()
452 .unwrap();
453
454 assert_eq!(
455 mi.item,
456 MediaBlock {
457 type_: MediaType::Image,
458 target: Span {
459 data: "bar",
460 line: 1,
461 col: 8,
462 offset: 7,
463 },
464 macro_attrlist: Attrlist {
465 attributes: &[ElementAttribute {
466 name: None,
467 shorthand_items: &["blah"],
468 value: "blah"
469 }],
470 anchor: None,
471 source: Span {
472 data: "blah",
473 line: 1,
474 col: 12,
475 offset: 11,
476 }
477 },
478 source: Span {
479 data: "image::bar[blah]",
480 line: 1,
481 col: 1,
482 offset: 0,
483 },
484 title_source: None,
485 title: None,
486 anchor: None,
487 anchor_reftext: None,
488 attrlist: None,
489 }
490 );
491
492 assert_eq!(
493 mi.after,
494 Span {
495 data: "",
496 line: 1,
497 col: 17,
498 offset: 16
499 }
500 );
501 }
502
503 #[test]
504 fn audio() {
505 let mut parser = Parser::default();
506
507 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("audio::bar[]"), &mut parser)
508 .unwrap_if_no_warnings()
509 .unwrap();
510
511 assert_eq!(
512 mi.item,
513 MediaBlock {
514 type_: MediaType::Audio,
515 target: Span {
516 data: "bar",
517 line: 1,
518 col: 8,
519 offset: 7,
520 },
521 macro_attrlist: Attrlist {
522 attributes: &[],
523 anchor: None,
524 source: Span {
525 data: "",
526 line: 1,
527 col: 12,
528 offset: 11,
529 }
530 },
531 source: Span {
532 data: "audio::bar[]",
533 line: 1,
534 col: 1,
535 offset: 0,
536 },
537 title_source: None,
538 title: None,
539 anchor: None,
540 anchor_reftext: None,
541 attrlist: None,
542 }
543 );
544
545 assert_eq!(
546 mi.after,
547 Span {
548 data: "",
549 line: 1,
550 col: 13,
551 offset: 12
552 }
553 );
554
555 assert_eq!(mi.item.content_model(), ContentModel::Empty);
556 assert_eq!(mi.item.raw_context().deref(), "audio");
557 assert!(mi.item.nested_blocks().next().is_none());
558 assert!(mi.item.title_source().is_none());
559 assert!(mi.item.title().is_none());
560 assert!(mi.item.anchor().is_none());
561 assert!(mi.item.anchor_reftext().is_none());
562 assert!(mi.item.attrlist().is_none());
563 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
564 }
565
566 #[test]
567 fn video() {
568 let mut parser = Parser::default();
569
570 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("video::bar[]"), &mut parser)
571 .unwrap_if_no_warnings()
572 .unwrap();
573
574 assert_eq!(
575 mi.item,
576 MediaBlock {
577 type_: MediaType::Video,
578 target: Span {
579 data: "bar",
580 line: 1,
581 col: 8,
582 offset: 7,
583 },
584 macro_attrlist: Attrlist {
585 attributes: &[],
586 anchor: None,
587 source: Span {
588 data: "",
589 line: 1,
590 col: 12,
591 offset: 11,
592 }
593 },
594 source: Span {
595 data: "video::bar[]",
596 line: 1,
597 col: 1,
598 offset: 0,
599 },
600 title_source: None,
601 title: None,
602 anchor: None,
603 anchor_reftext: None,
604 attrlist: None,
605 }
606 );
607
608 assert_eq!(
609 mi.after,
610 Span {
611 data: "",
612 line: 1,
613 col: 13,
614 offset: 12
615 }
616 );
617
618 assert_eq!(mi.item.content_model(), ContentModel::Empty);
619 assert_eq!(mi.item.raw_context().deref(), "video");
620 assert!(mi.item.nested_blocks().next().is_none());
621 assert!(mi.item.title_source().is_none());
622 assert!(mi.item.title().is_none());
623 assert!(mi.item.anchor().is_none());
624 assert!(mi.item.anchor_reftext().is_none());
625 assert!(mi.item.attrlist().is_none());
626 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
627 }
628
629 #[test]
630 fn err_duplicate_comma() {
631 let mut parser = Parser::default();
632 let maw = crate::blocks::MediaBlock::parse(
633 &BlockMetadata::new("image::bar[blah,,blap]"),
634 &mut parser,
635 );
636
637 let mi = maw.item.unwrap().clone();
638
639 assert_eq!(
640 mi.item,
641 MediaBlock {
642 type_: MediaType::Image,
643 target: Span {
644 data: "bar",
645 line: 1,
646 col: 8,
647 offset: 7,
648 },
649 macro_attrlist: Attrlist {
650 attributes: &[
651 ElementAttribute {
652 name: None,
653 shorthand_items: &["blah"],
654 value: "blah"
655 },
656 ElementAttribute {
657 name: None,
658 shorthand_items: &[],
659 value: "blap"
660 }
661 ],
662 anchor: None,
663 source: Span {
664 data: "blah,,blap",
665 line: 1,
666 col: 12,
667 offset: 11,
668 }
669 },
670 source: Span {
671 data: "image::bar[blah,,blap]",
672 line: 1,
673 col: 1,
674 offset: 0,
675 },
676 title_source: None,
677 title: None,
678 anchor: None,
679 anchor_reftext: None,
680 attrlist: None,
681 }
682 );
683
684 assert_eq!(
685 mi.after,
686 Span {
687 data: "",
688 line: 1,
689 col: 23,
690 offset: 22
691 }
692 );
693
694 assert_eq!(
695 maw.warnings,
696 vec![Warning {
697 source: Span {
698 data: "blah,,blap",
699 line: 1,
700 col: 12,
701 offset: 11,
702 },
703 warning: WarningType::EmptyAttributeValue,
704 }]
705 );
706 }
707
708 mod media_type {
709 mod impl_debug {
710 use pretty_assertions_sorted::assert_eq;
711
712 use crate::blocks::MediaType;
713
714 #[test]
715 fn image() {
716 let media_type = MediaType::Image;
717 let debug_output = format!("{:?}", media_type);
718 assert_eq!(debug_output, "MediaType::Image");
719 }
720
721 #[test]
722 fn video() {
723 let media_type = MediaType::Video;
724 let debug_output = format!("{:?}", media_type);
725 assert_eq!(debug_output, "MediaType::Video");
726 }
727
728 #[test]
729 fn audio() {
730 let media_type = MediaType::Audio;
731 let debug_output = format!("{:?}", media_type);
732 assert_eq!(debug_output, "MediaType::Audio");
733 }
734 }
735 }
736}