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 crate::{
214 blocks::{ContentModel, MediaType, metadata::BlockMetadata},
215 tests::prelude::*,
216 };
217
218 #[test]
219 fn impl_clone() {
220 let mut parser = Parser::default();
222
223 let b1 =
224 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::foo.jpg[]"), &mut parser)
225 .unwrap_if_no_warnings()
226 .unwrap()
227 .item;
228
229 let b2 = b1.clone();
230 assert_eq!(b1, b2);
231 }
232
233 #[test]
234 fn err_empty_source() {
235 let mut parser = Parser::default();
236 assert!(
237 crate::blocks::MediaBlock::parse(&BlockMetadata::new(""), &mut parser)
238 .unwrap_if_no_warnings()
239 .is_none()
240 );
241 }
242
243 #[test]
244 fn err_only_spaces() {
245 let mut parser = Parser::default();
246 assert!(
247 crate::blocks::MediaBlock::parse(&BlockMetadata::new(" "), &mut parser)
248 .unwrap_if_no_warnings()
249 .is_none()
250 );
251 }
252
253 #[test]
254 fn err_macro_name_not_ident() {
255 let mut parser = Parser::default();
256 let maw = crate::blocks::MediaBlock::parse(
257 &BlockMetadata::new("98xyz::bar[blah,blap]"),
258 &mut parser,
259 );
260
261 assert!(maw.item.is_none());
262 assert!(maw.warnings.is_empty());
263 }
264
265 #[test]
266 fn err_missing_double_colon() {
267 let mut parser = Parser::default();
268 let maw = crate::blocks::MediaBlock::parse(
269 &BlockMetadata::new("image:bar[blah,blap]"),
270 &mut parser,
271 );
272
273 assert!(maw.item.is_none());
274
275 assert_eq!(
276 maw.warnings,
277 vec![Warning {
278 source: Span {
279 data: ":bar[blah,blap]",
280 line: 1,
281 col: 6,
282 offset: 5,
283 },
284 warning: WarningType::MacroMissingDoubleColon,
285 }]
286 );
287 }
288
289 #[test]
290 fn err_missing_macro_attrlist() {
291 let mut parser = Parser::default();
292 let maw = crate::blocks::MediaBlock::parse(
293 &BlockMetadata::new("image::barblah,blap]"),
294 &mut parser,
295 );
296
297 assert!(maw.item.is_none());
298
299 assert_eq!(
300 maw.warnings,
301 vec![Warning {
302 source: Span {
303 data: "",
304 line: 1,
305 col: 21,
306 offset: 20,
307 },
308 warning: WarningType::MacroMissingAttributeList,
309 }]
310 );
311 }
312
313 #[test]
314 fn err_unknown_type() {
315 let mut parser = Parser::default();
316 assert!(
317 crate::blocks::MediaBlock::parse(&BlockMetadata::new("imagex::bar[]"), &mut parser)
318 .unwrap_if_no_warnings()
319 .is_none()
320 );
321 }
322
323 #[test]
324 fn err_no_attr_list() {
325 let mut parser = Parser::default();
326 assert!(
327 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar"), &mut parser)
328 .unwrap_if_no_warnings()
329 .is_none()
330 );
331 }
332
333 #[test]
334 fn err_attr_list_not_closed() {
335 let mut parser = Parser::default();
336 assert!(
337 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah"), &mut parser)
338 .unwrap_if_no_warnings()
339 .is_none()
340 );
341 }
342
343 #[test]
344 fn err_unexpected_after_attr_list() {
345 let mut parser = Parser::default();
346 assert!(
347 crate::blocks::MediaBlock::parse(
348 &BlockMetadata::new("image::bar[blah]bonus"),
349 &mut parser
350 )
351 .unwrap_if_no_warnings()
352 .is_none()
353 );
354 }
355
356 #[test]
357 fn simplest_block_macro() {
358 let mut parser = Parser::default();
359
360 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::[]"), &mut parser);
361 assert!(mi.item.is_none());
362
363 assert_eq!(
364 mi.warnings,
365 vec![Warning {
366 source: Span {
367 data: "[]",
368 line: 1,
369 col: 8,
370 offset: 7,
371 },
372 warning: WarningType::MediaMacroMissingTarget,
373 }]
374 );
375 }
376
377 #[test]
378 fn has_target() {
379 let mut parser = Parser::default();
380
381 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[]"), &mut parser)
382 .unwrap_if_no_warnings()
383 .unwrap();
384
385 assert_eq!(
386 mi.item,
387 MediaBlock {
388 type_: MediaType::Image,
389 target: Span {
390 data: "bar",
391 line: 1,
392 col: 8,
393 offset: 7,
394 },
395 macro_attrlist: Attrlist {
396 attributes: &[],
397 anchor: None,
398 source: Span {
399 data: "",
400 line: 1,
401 col: 12,
402 offset: 11,
403 }
404 },
405 source: Span {
406 data: "image::bar[]",
407 line: 1,
408 col: 1,
409 offset: 0,
410 },
411 title_source: None,
412 title: None,
413 anchor: None,
414 anchor_reftext: None,
415 attrlist: None,
416 }
417 );
418
419 assert_eq!(
420 mi.after,
421 Span {
422 data: "",
423 line: 1,
424 col: 13,
425 offset: 12
426 }
427 );
428
429 assert_eq!(mi.item.content_model(), ContentModel::Empty);
430 assert_eq!(mi.item.raw_context().deref(), "image");
431 assert!(mi.item.nested_blocks().next().is_none());
432 assert!(mi.item.title_source().is_none());
433 assert!(mi.item.title().is_none());
434 assert!(mi.item.anchor().is_none());
435 assert!(mi.item.anchor_reftext().is_none());
436 assert!(mi.item.attrlist().is_none());
437 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
438 }
439
440 #[test]
441 fn has_target_and_attrlist() {
442 let mut parser = Parser::default();
443
444 let mi =
445 crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah]"), &mut parser)
446 .unwrap_if_no_warnings()
447 .unwrap();
448
449 assert_eq!(
450 mi.item,
451 MediaBlock {
452 type_: MediaType::Image,
453 target: Span {
454 data: "bar",
455 line: 1,
456 col: 8,
457 offset: 7,
458 },
459 macro_attrlist: Attrlist {
460 attributes: &[ElementAttribute {
461 name: None,
462 shorthand_items: &["blah"],
463 value: "blah"
464 }],
465 anchor: None,
466 source: Span {
467 data: "blah",
468 line: 1,
469 col: 12,
470 offset: 11,
471 }
472 },
473 source: Span {
474 data: "image::bar[blah]",
475 line: 1,
476 col: 1,
477 offset: 0,
478 },
479 title_source: None,
480 title: None,
481 anchor: None,
482 anchor_reftext: None,
483 attrlist: None,
484 }
485 );
486
487 assert_eq!(
488 mi.after,
489 Span {
490 data: "",
491 line: 1,
492 col: 17,
493 offset: 16
494 }
495 );
496 }
497
498 #[test]
499 fn audio() {
500 let mut parser = Parser::default();
501
502 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("audio::bar[]"), &mut parser)
503 .unwrap_if_no_warnings()
504 .unwrap();
505
506 assert_eq!(
507 mi.item,
508 MediaBlock {
509 type_: MediaType::Audio,
510 target: Span {
511 data: "bar",
512 line: 1,
513 col: 8,
514 offset: 7,
515 },
516 macro_attrlist: Attrlist {
517 attributes: &[],
518 anchor: None,
519 source: Span {
520 data: "",
521 line: 1,
522 col: 12,
523 offset: 11,
524 }
525 },
526 source: Span {
527 data: "audio::bar[]",
528 line: 1,
529 col: 1,
530 offset: 0,
531 },
532 title_source: None,
533 title: None,
534 anchor: None,
535 anchor_reftext: None,
536 attrlist: None,
537 }
538 );
539
540 assert_eq!(
541 mi.after,
542 Span {
543 data: "",
544 line: 1,
545 col: 13,
546 offset: 12
547 }
548 );
549
550 assert_eq!(mi.item.content_model(), ContentModel::Empty);
551 assert_eq!(mi.item.raw_context().deref(), "audio");
552 assert!(mi.item.nested_blocks().next().is_none());
553 assert!(mi.item.title_source().is_none());
554 assert!(mi.item.title().is_none());
555 assert!(mi.item.anchor().is_none());
556 assert!(mi.item.anchor_reftext().is_none());
557 assert!(mi.item.attrlist().is_none());
558 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
559 }
560
561 #[test]
562 fn video() {
563 let mut parser = Parser::default();
564
565 let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("video::bar[]"), &mut parser)
566 .unwrap_if_no_warnings()
567 .unwrap();
568
569 assert_eq!(
570 mi.item,
571 MediaBlock {
572 type_: MediaType::Video,
573 target: Span {
574 data: "bar",
575 line: 1,
576 col: 8,
577 offset: 7,
578 },
579 macro_attrlist: Attrlist {
580 attributes: &[],
581 anchor: None,
582 source: Span {
583 data: "",
584 line: 1,
585 col: 12,
586 offset: 11,
587 }
588 },
589 source: Span {
590 data: "video::bar[]",
591 line: 1,
592 col: 1,
593 offset: 0,
594 },
595 title_source: None,
596 title: None,
597 anchor: None,
598 anchor_reftext: None,
599 attrlist: None,
600 }
601 );
602
603 assert_eq!(
604 mi.after,
605 Span {
606 data: "",
607 line: 1,
608 col: 13,
609 offset: 12
610 }
611 );
612
613 assert_eq!(mi.item.content_model(), ContentModel::Empty);
614 assert_eq!(mi.item.raw_context().deref(), "video");
615 assert!(mi.item.nested_blocks().next().is_none());
616 assert!(mi.item.title_source().is_none());
617 assert!(mi.item.title().is_none());
618 assert!(mi.item.anchor().is_none());
619 assert!(mi.item.anchor_reftext().is_none());
620 assert!(mi.item.attrlist().is_none());
621 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
622 }
623
624 #[test]
625 fn err_duplicate_comma() {
626 let mut parser = Parser::default();
627 let maw = crate::blocks::MediaBlock::parse(
628 &BlockMetadata::new("image::bar[blah,,blap]"),
629 &mut parser,
630 );
631
632 let mi = maw.item.unwrap().clone();
633
634 assert_eq!(
635 mi.item,
636 MediaBlock {
637 type_: MediaType::Image,
638 target: Span {
639 data: "bar",
640 line: 1,
641 col: 8,
642 offset: 7,
643 },
644 macro_attrlist: Attrlist {
645 attributes: &[
646 ElementAttribute {
647 name: None,
648 shorthand_items: &["blah"],
649 value: "blah"
650 },
651 ElementAttribute {
652 name: None,
653 shorthand_items: &[],
654 value: "blap"
655 }
656 ],
657 anchor: None,
658 source: Span {
659 data: "blah,,blap",
660 line: 1,
661 col: 12,
662 offset: 11,
663 }
664 },
665 source: Span {
666 data: "image::bar[blah,,blap]",
667 line: 1,
668 col: 1,
669 offset: 0,
670 },
671 title_source: None,
672 title: None,
673 anchor: None,
674 anchor_reftext: None,
675 attrlist: None,
676 }
677 );
678
679 assert_eq!(
680 mi.after,
681 Span {
682 data: "",
683 line: 1,
684 col: 23,
685 offset: 22
686 }
687 );
688
689 assert_eq!(
690 maw.warnings,
691 vec![Warning {
692 source: Span {
693 data: "blah,,blap",
694 line: 1,
695 col: 12,
696 offset: 11,
697 },
698 warning: WarningType::EmptyAttributeValue,
699 }]
700 );
701 }
702
703 mod media_type {
704 mod impl_debug {
705 use crate::blocks::MediaType;
706
707 #[test]
708 fn image() {
709 let media_type = MediaType::Image;
710 let debug_output = format!("{:?}", media_type);
711 assert_eq!(debug_output, "MediaType::Image");
712 }
713
714 #[test]
715 fn video() {
716 let media_type = MediaType::Video;
717 let debug_output = format!("{:?}", media_type);
718 assert_eq!(debug_output, "MediaType::Video");
719 }
720
721 #[test]
722 fn audio() {
723 let media_type = MediaType::Audio;
724 let debug_output = format!("{:?}", media_type);
725 assert_eq!(debug_output, "MediaType::Audio");
726 }
727 }
728 }
729}