1use crate::{
2 HasSpan, Parser, Span,
3 attributes::{Attrlist, AttrlistContext},
4 blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
5 content::substitute_attributes_in_macro_target,
6 span::MatchedItem,
7 strings::CowStr,
8 warnings::{MatchAndWarnings, Warning, WarningType},
9};
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct MediaBlock<'src> {
14 type_: MediaType,
15 target: Span<'src>,
16 resolved_target: CowStr<'src>,
17 macro_attrlist: Attrlist<'src>,
18 source: Span<'src>,
19 title_source: Option<Span<'src>>,
20 title: Option<String>,
21 anchor: Option<Span<'src>>,
22 anchor_reftext: Option<Span<'src>>,
23 attrlist: Option<Attrlist<'src>>,
24}
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub(crate) enum TargetResolution {
30 Keep,
32
33 Drop,
36}
37
38#[derive(Clone, Copy, Eq, PartialEq)]
40pub enum MediaType {
41 Image,
43
44 Video,
46
47 Audio,
49}
50
51impl std::fmt::Debug for MediaType {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 MediaType::Image => write!(f, "MediaType::Image"),
55 MediaType::Video => write!(f, "MediaType::Video"),
56 MediaType::Audio => write!(f, "MediaType::Audio"),
57 }
58 }
59}
60
61impl<'src> MediaBlock<'src> {
62 pub(crate) fn parse(
63 metadata: &BlockMetadata<'src>,
64 parser: &mut Parser,
65 ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
66 let line = metadata.block_start.take_normalized_line();
67
68 if !line.item.ends_with(']') {
70 return MatchAndWarnings {
71 item: None,
72 warnings: vec![],
73 };
74 }
75
76 let Some(name) = line.item.take_ident() else {
77 return MatchAndWarnings {
78 item: None,
79 warnings: vec![],
80 };
81 };
82
83 let type_ = match name.item.data() {
84 "image" => MediaType::Image,
85 "video" => MediaType::Video,
86 "audio" => MediaType::Audio,
87 _ => {
88 return MatchAndWarnings {
89 item: None,
90 warnings: vec![],
91 };
92 }
93 };
94
95 let Some(colons) = name.after.take_prefix("::") else {
96 return MatchAndWarnings {
97 item: None,
98 warnings: vec![Warning {
99 source: name.after,
100 warning: WarningType::MacroMissingDoubleColon,
101 }],
102 };
103 };
104
105 let target = colons.after.take_while(|c| c != '[');
107
108 if target.item.is_empty() {
109 return MatchAndWarnings {
110 item: None,
111 warnings: vec![Warning {
112 source: target.after,
113 warning: WarningType::MediaMacroMissingTarget,
114 }],
115 };
116 }
117
118 let Some(open_brace) = target.after.take_prefix("[") else {
119 return MatchAndWarnings {
120 item: None,
121 warnings: vec![Warning {
122 source: target.after,
123 warning: WarningType::MacroMissingAttributeList,
124 }],
125 };
126 };
127
128 let attrlist = open_brace.after.slice(0..open_brace.after.len() - 1);
129 let macro_attrlist = Attrlist::parse(attrlist, parser, AttrlistContext::Inline);
132
133 let source: Span = metadata.source.trim_remainder(line.after);
134 let source = source.slice(0..source.trim().len());
135
136 MatchAndWarnings {
137 item: Some(MatchedItem {
138 item: Self {
139 type_,
140 target: target.item,
141 resolved_target: target.item.data().into(),
146 macro_attrlist: macro_attrlist.item.item,
147 source,
148 title_source: metadata.title_source,
149 title: metadata.title.clone(),
150 anchor: metadata.anchor,
151 anchor_reftext: metadata.anchor_reftext,
152 attrlist: metadata.attrlist.clone(),
153 },
154
155 after: line.after.discard_empty_lines(),
156 }),
157 warnings: macro_attrlist.warnings,
158 }
159 }
160
161 pub fn type_(&self) -> MediaType {
163 self.type_
164 }
165
166 pub fn target(&'src self) -> Option<&'src Span<'src>> {
172 Some(&self.target)
173 }
174
175 pub fn resolved_target(&self) -> &str {
183 self.resolved_target.as_ref()
184 }
185
186 pub(crate) fn resolve_target(&mut self, parser: &Parser) -> TargetResolution {
198 match substitute_attributes_in_macro_target(self.target, parser) {
199 Some(resolved) => {
200 self.resolved_target = resolved;
201 TargetResolution::Keep
202 }
203 None => TargetResolution::Drop,
204 }
205 }
206
207 pub fn macro_attrlist(&'src self) -> &'src Attrlist<'src> {
217 &self.macro_attrlist
218 }
219}
220
221impl<'src> IsBlock<'src> for MediaBlock<'src> {
222 fn content_model(&self) -> ContentModel {
223 ContentModel::Empty
224 }
225
226 fn raw_context(&self) -> CowStr<'src> {
227 match self.type_ {
228 MediaType::Audio => "audio",
229 MediaType::Image => "image",
230 MediaType::Video => "video",
231 }
232 .into()
233 }
234
235 fn title_source(&'src self) -> Option<Span<'src>> {
236 self.title_source
237 }
238
239 fn title(&self) -> Option<&str> {
240 self.title.as_deref()
241 }
242
243 fn anchor(&'src self) -> Option<Span<'src>> {
244 self.anchor
245 }
246
247 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
248 self.anchor_reftext
249 }
250
251 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
252 self.attrlist.as_ref()
253 }
254}
255
256impl<'src> HasSpan<'src> for MediaBlock<'src> {
257 fn span(&self) -> Span<'src> {
258 self.source
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 #![allow(clippy::unwrap_used)]
265
266 use std::ops::Deref;
267
268 use crate::{
269 blocks::{ContentModel, MediaType, metadata::BlockMetadata},
270 tests::prelude::*,
271 };
272
273 #[test]
274 fn impl_clone() {
275 let mut parser = Parser::default();
277
278 let b1 =
279 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::foo.jpg[]"), &mut parser)
280 .unwrap_if_no_warnings()
281 .unwrap()
282 .item;
283
284 let b2 = b1.clone();
285 assert_eq!(b1, b2);
286 }
287
288 #[test]
289 fn err_empty_source() {
290 let mut parser = Parser::default();
291 assert!(
292 crate::blocks::MediaBlock::parse(&BlockMetadata::new(""), &mut parser)
293 .unwrap_if_no_warnings()
294 .is_none()
295 );
296 }
297
298 #[test]
299 fn err_only_spaces() {
300 let mut parser = Parser::default();
301 assert!(
302 crate::blocks::MediaBlock::parse(&BlockMetadata::new(" "), &mut parser)
303 .unwrap_if_no_warnings()
304 .is_none()
305 );
306 }
307
308 #[test]
309 fn err_macro_name_not_ident() {
310 let mut parser = Parser::default();
311 let maw = crate::blocks::MediaBlock::parse(
312 &BlockMetadata::new("98xyz::bar[blah,blap]"),
313 &mut parser,
314 );
315
316 assert!(maw.item.is_none());
317 assert!(maw.warnings.is_empty());
318 }
319
320 #[test]
321 fn err_missing_double_colon() {
322 let mut parser = Parser::default();
323 let maw = crate::blocks::MediaBlock::parse(
324 &BlockMetadata::new("image:bar[blah,blap]"),
325 &mut parser,
326 );
327
328 assert!(maw.item.is_none());
329
330 assert_eq!(
331 maw.warnings,
332 vec![Warning {
333 source: Span {
334 data: ":bar[blah,blap]",
335 line: 1,
336 col: 6,
337 offset: 5,
338 },
339 warning: WarningType::MacroMissingDoubleColon,
340 }]
341 );
342 }
343
344 #[test]
345 fn err_missing_macro_attrlist() {
346 let mut parser = Parser::default();
347 let maw = crate::blocks::MediaBlock::parse(
348 &BlockMetadata::new("image::barblah,blap]"),
349 &mut parser,
350 );
351
352 assert!(maw.item.is_none());
353
354 assert_eq!(
355 maw.warnings,
356 vec![Warning {
357 source: Span {
358 data: "",
359 line: 1,
360 col: 21,
361 offset: 20,
362 },
363 warning: WarningType::MacroMissingAttributeList,
364 }]
365 );
366 }
367
368 #[test]
369 fn err_unknown_type() {
370 let mut parser = Parser::default();
371 assert!(
372 crate::blocks::MediaBlock::parse(&BlockMetadata::new("imagex::bar[]"), &mut parser)
373 .unwrap_if_no_warnings()
374 .is_none()
375 );
376 }
377
378 #[test]
379 fn err_no_attr_list() {
380 let mut parser = Parser::default();
381 assert!(
382 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar"), &mut parser)
383 .unwrap_if_no_warnings()
384 .is_none()
385 );
386 }
387
388 #[test]
389 fn err_attr_list_not_closed() {
390 let mut parser = Parser::default();
391 assert!(
392 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah"), &mut parser)
393 .unwrap_if_no_warnings()
394 .is_none()
395 );
396 }
397
398 #[test]
399 fn err_unexpected_after_attr_list() {
400 let mut parser = Parser::default();
401 assert!(
402 crate::blocks::MediaBlock::parse(
403 &BlockMetadata::new("image::bar[blah]bonus"),
404 &mut parser
405 )
406 .unwrap_if_no_warnings()
407 .is_none()
408 );
409 }
410
411 #[test]
412 fn simplest_block_macro() {
413 let mut parser = Parser::default();
414
415 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::[]"), &mut parser);
416 assert!(mi.item.is_none());
417
418 assert_eq!(
419 mi.warnings,
420 vec![Warning {
421 source: Span {
422 data: "[]",
423 line: 1,
424 col: 8,
425 offset: 7,
426 },
427 warning: WarningType::MediaMacroMissingTarget,
428 }]
429 );
430 }
431
432 #[test]
433 fn has_target() {
434 let mut parser = Parser::default();
435
436 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[]"), &mut parser)
437 .unwrap_if_no_warnings()
438 .unwrap();
439
440 assert_eq!(
441 mi.item,
442 MediaBlock {
443 type_: MediaType::Image,
444 target: Span {
445 data: "bar",
446 line: 1,
447 col: 8,
448 offset: 7,
449 },
450 macro_attrlist: Attrlist {
451 attributes: &[],
452 anchor: None,
453 source: Span {
454 data: "",
455 line: 1,
456 col: 12,
457 offset: 11,
458 }
459 },
460 source: Span {
461 data: "image::bar[]",
462 line: 1,
463 col: 1,
464 offset: 0,
465 },
466 title_source: None,
467 title: None,
468 anchor: None,
469 anchor_reftext: None,
470 attrlist: None,
471 }
472 );
473
474 assert_eq!(
475 mi.after,
476 Span {
477 data: "",
478 line: 1,
479 col: 13,
480 offset: 12
481 }
482 );
483
484 assert_eq!(mi.item.content_model(), ContentModel::Empty);
485 assert_eq!(mi.item.raw_context().deref(), "image");
486 assert!(mi.item.nested_blocks().next().is_none());
487 assert!(mi.item.title_source().is_none());
488 assert!(mi.item.title().is_none());
489 assert!(mi.item.anchor().is_none());
490 assert!(mi.item.anchor_reftext().is_none());
491 assert!(mi.item.attrlist().is_none());
492 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
493 }
494
495 #[test]
496 fn has_target_and_attrlist() {
497 let mut parser = Parser::default();
498
499 let mi =
500 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah]"), &mut parser)
501 .unwrap_if_no_warnings()
502 .unwrap();
503
504 assert_eq!(
505 mi.item,
506 MediaBlock {
507 type_: MediaType::Image,
508 target: Span {
509 data: "bar",
510 line: 1,
511 col: 8,
512 offset: 7,
513 },
514 macro_attrlist: Attrlist {
515 attributes: &[ElementAttribute {
516 name: None,
517 shorthand_items: &["blah"],
518 value: "blah"
519 }],
520 anchor: None,
521 source: Span {
522 data: "blah",
523 line: 1,
524 col: 12,
525 offset: 11,
526 }
527 },
528 source: Span {
529 data: "image::bar[blah]",
530 line: 1,
531 col: 1,
532 offset: 0,
533 },
534 title_source: None,
535 title: None,
536 anchor: None,
537 anchor_reftext: None,
538 attrlist: None,
539 }
540 );
541
542 assert_eq!(
543 mi.after,
544 Span {
545 data: "",
546 line: 1,
547 col: 17,
548 offset: 16
549 }
550 );
551 }
552
553 #[test]
554 fn audio() {
555 let mut parser = Parser::default();
556
557 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("audio::bar[]"), &mut parser)
558 .unwrap_if_no_warnings()
559 .unwrap();
560
561 assert_eq!(
562 mi.item,
563 MediaBlock {
564 type_: MediaType::Audio,
565 target: Span {
566 data: "bar",
567 line: 1,
568 col: 8,
569 offset: 7,
570 },
571 macro_attrlist: Attrlist {
572 attributes: &[],
573 anchor: None,
574 source: Span {
575 data: "",
576 line: 1,
577 col: 12,
578 offset: 11,
579 }
580 },
581 source: Span {
582 data: "audio::bar[]",
583 line: 1,
584 col: 1,
585 offset: 0,
586 },
587 title_source: None,
588 title: None,
589 anchor: None,
590 anchor_reftext: None,
591 attrlist: None,
592 }
593 );
594
595 assert_eq!(
596 mi.after,
597 Span {
598 data: "",
599 line: 1,
600 col: 13,
601 offset: 12
602 }
603 );
604
605 assert_eq!(mi.item.content_model(), ContentModel::Empty);
606 assert_eq!(mi.item.raw_context().deref(), "audio");
607 assert!(mi.item.nested_blocks().next().is_none());
608 assert!(mi.item.title_source().is_none());
609 assert!(mi.item.title().is_none());
610 assert!(mi.item.anchor().is_none());
611 assert!(mi.item.anchor_reftext().is_none());
612 assert!(mi.item.attrlist().is_none());
613 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
614 }
615
616 #[test]
617 fn video() {
618 let mut parser = Parser::default();
619
620 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("video::bar[]"), &mut parser)
621 .unwrap_if_no_warnings()
622 .unwrap();
623
624 assert_eq!(
625 mi.item,
626 MediaBlock {
627 type_: MediaType::Video,
628 target: Span {
629 data: "bar",
630 line: 1,
631 col: 8,
632 offset: 7,
633 },
634 macro_attrlist: Attrlist {
635 attributes: &[],
636 anchor: None,
637 source: Span {
638 data: "",
639 line: 1,
640 col: 12,
641 offset: 11,
642 }
643 },
644 source: Span {
645 data: "video::bar[]",
646 line: 1,
647 col: 1,
648 offset: 0,
649 },
650 title_source: None,
651 title: None,
652 anchor: None,
653 anchor_reftext: None,
654 attrlist: None,
655 }
656 );
657
658 assert_eq!(
659 mi.after,
660 Span {
661 data: "",
662 line: 1,
663 col: 13,
664 offset: 12
665 }
666 );
667
668 assert_eq!(mi.item.content_model(), ContentModel::Empty);
669 assert_eq!(mi.item.raw_context().deref(), "video");
670 assert!(mi.item.nested_blocks().next().is_none());
671 assert!(mi.item.title_source().is_none());
672 assert!(mi.item.title().is_none());
673 assert!(mi.item.anchor().is_none());
674 assert!(mi.item.anchor_reftext().is_none());
675 assert!(mi.item.attrlist().is_none());
676 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
677 }
678
679 #[test]
680 fn err_duplicate_comma() {
681 let mut parser = Parser::default();
682 let maw = crate::blocks::MediaBlock::parse(
683 &BlockMetadata::new("image::bar[blah,,blap]"),
684 &mut parser,
685 );
686
687 let mi = maw.item.unwrap().clone();
688
689 assert_eq!(
690 mi.item,
691 MediaBlock {
692 type_: MediaType::Image,
693 target: Span {
694 data: "bar",
695 line: 1,
696 col: 8,
697 offset: 7,
698 },
699 macro_attrlist: Attrlist {
700 attributes: &[
701 ElementAttribute {
702 name: None,
703 shorthand_items: &["blah"],
704 value: "blah"
705 },
706 ElementAttribute {
707 name: None,
708 shorthand_items: &[],
709 value: "blap"
710 }
711 ],
712 anchor: None,
713 source: Span {
714 data: "blah,,blap",
715 line: 1,
716 col: 12,
717 offset: 11,
718 }
719 },
720 source: Span {
721 data: "image::bar[blah,,blap]",
722 line: 1,
723 col: 1,
724 offset: 0,
725 },
726 title_source: None,
727 title: None,
728 anchor: None,
729 anchor_reftext: None,
730 attrlist: None,
731 }
732 );
733
734 assert_eq!(
735 mi.after,
736 Span {
737 data: "",
738 line: 1,
739 col: 23,
740 offset: 22
741 }
742 );
743
744 assert_eq!(
745 maw.warnings,
746 vec![Warning {
747 source: Span {
748 data: "blah,,blap",
749 line: 1,
750 col: 12,
751 offset: 11,
752 },
753 warning: WarningType::EmptyAttributeValue,
754 }]
755 );
756 }
757
758 mod target_resolution {
759 #![allow(clippy::indexing_slicing)]
760
761 use crate::{
762 blocks::{MediaBlock, media::TargetResolution, metadata::BlockMetadata},
763 parser::ModificationContext,
764 tests::prelude::*,
765 warnings::WarningType,
766 };
767
768 fn resolve<'i>(input: &'i str, parser: &mut Parser) -> Option<MediaBlock<'i>> {
772 let mut block = MediaBlock::parse(&BlockMetadata::new(input), parser)
773 .unwrap_if_no_warnings()
774 .unwrap()
775 .item;
776
777 match block.resolve_target(parser) {
778 TargetResolution::Keep => Some(block),
779 TargetResolution::Drop => None,
780 }
781 }
782
783 fn parser_with_mode(mode: &str) -> Parser {
784 Parser::default().with_intrinsic_attribute(
785 "attribute-missing",
786 mode,
787 ModificationContext::Anywhere,
788 )
789 }
790
791 #[test]
792 fn target_without_reference_is_unchanged() {
793 let mut p = Parser::default();
795 let block = resolve("image::foo.png[]", &mut p).unwrap();
796 assert_eq!(block.resolved_target(), "foo.png");
797 }
798
799 #[test]
800 fn resolves_a_defined_reference() {
801 let mut p = Parser::default().with_intrinsic_attribute(
802 "name",
803 "bar",
804 ModificationContext::Anywhere,
805 );
806 let block = resolve("image::pre-{name}-post.png[]", &mut p).unwrap();
807 assert_eq!(block.resolved_target(), "pre-bar-post.png");
808 }
809
810 #[test]
811 fn skip_leaves_a_missing_reference_in_place() {
812 let mut p = Parser::default();
814 let block = resolve("image::a{missing}b.png[]", &mut p).unwrap();
815 assert_eq!(block.resolved_target(), "a{missing}b.png");
816 assert!(p.take_substitution_warnings().is_empty());
817 }
818
819 #[test]
820 fn drop_removes_only_the_missing_reference() {
821 let mut p = parser_with_mode("drop");
822 let block = resolve("image::a{missing}b.png[]", &mut p).unwrap();
823 assert_eq!(block.resolved_target(), "ab.png");
824 }
825
826 #[test]
827 fn warn_leaves_the_reference_and_records_a_warning() {
828 let mut p = parser_with_mode("warn");
829 let block = resolve("image::a{missing}b.png[]", &mut p).unwrap();
830 assert_eq!(block.resolved_target(), "a{missing}b.png");
831
832 let warnings = p.take_substitution_warnings();
833 assert_eq!(warnings.len(), 1);
834 assert_eq!(
835 warnings[0].warning,
836 WarningType::SkippingReferenceToMissingAttribute("missing".to_string())
837 );
838 }
839
840 #[test]
841 fn drop_line_drops_the_whole_block() {
842 let mut p = parser_with_mode("drop-line");
843 assert!(resolve("image::a{missing}b.png[]", &mut p).is_none());
844 }
845
846 #[test]
847 fn drop_line_keeps_a_block_whose_reference_resolves() {
848 let mut p = parser_with_mode("drop-line").with_intrinsic_attribute(
849 "name",
850 "bar",
851 ModificationContext::Anywhere,
852 );
853 let block = resolve("image::{name}.png[]", &mut p).unwrap();
854 assert_eq!(block.resolved_target(), "bar.png");
855 }
856
857 #[test]
858 fn drop_line_drops_a_top_level_block_but_keeps_following_blocks() {
859 let doc = Parser::default().parse(
862 ":attribute-missing: drop-line\n\nimage::{unresolved}[]\n\nparagraph after\n",
863 );
864
865 assert_css(&doc, ".imageblock", 0);
866 assert_css(&doc, ".paragraph", 1);
867 }
868
869 #[test]
870 fn drop_line_drops_a_top_level_audio_block() {
871 let doc = Parser::default().parse(
874 ":attribute-missing: drop-line\n\naudio::{unresolved}[]\n\nparagraph after\n",
875 );
876
877 assert_css(&doc, ".audioblock", 0);
878 assert_css(&doc, ".paragraph", 1);
879 }
880
881 #[test]
882 fn escaped_missing_reference_never_drops_the_block() {
883 let mut p = parser_with_mode("drop-line");
888 let block = resolve("image::a\\{missing}b.png[]", &mut p).unwrap();
889 assert_eq!(block.resolved_target(), "a\\{missing}b.png");
890 assert!(p.take_substitution_warnings().is_empty());
891 }
892 }
893
894 mod media_type {
895 mod impl_debug {
896 use crate::blocks::MediaType;
897
898 #[test]
899 fn image() {
900 let media_type = MediaType::Image;
901 let debug_output = format!("{:?}", media_type);
902 assert_eq!(debug_output, "MediaType::Image");
903 }
904
905 #[test]
906 fn video() {
907 let media_type = MediaType::Video;
908 let debug_output = format!("{:?}", media_type);
909 assert_eq!(debug_output, "MediaType::Video");
910 }
911
912 #[test]
913 fn audio() {
914 let media_type = MediaType::Audio;
915 let debug_output = format!("{:?}", media_type);
916 assert_eq!(debug_output, "MediaType::Audio");
917 }
918 }
919 }
920}