1use crate::{
2 HasSpan, Parser, Span,
3 attributes::Attrlist,
4 blocks::{ContentModel, IsBlock},
5 content::{Content, SubstitutionGroup},
6 span::MatchedItem,
7 strings::CowStr,
8};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct Attribute<'src> {
33 name: Span<'src>,
34 value_source: Option<Span<'src>>,
35 value: InterpretedValue,
36 source: Span<'src>,
37}
38
39impl<'src> Attribute<'src> {
40 pub(crate) fn parse(source: Span<'src>, parser: &Parser) -> Option<MatchedItem<'src, Self>> {
41 let attr_line = source.take_line_with_continuation()?;
42 let colon = attr_line.item.take_prefix(":")?;
43
44 let mut unset = false;
45 let line = if colon.after.starts_with('!') {
46 unset = true;
47 colon.after.slice_from(1..)
48 } else {
49 colon.after
50 };
51
52 let name = line.take_user_attr_name()?;
53
54 let line = if name.after.starts_with('!') && !unset {
55 unset = true;
56 name.after.slice_from(1..)
57 } else {
58 name.after
59 };
60
61 let line = line.take_prefix(":")?;
62
63 let (value, value_source) = if unset {
64 (InterpretedValue::Unset, None)
66 } else if line.after.is_empty() {
67 (InterpretedValue::Set, None)
68 } else {
69 let raw_value = line.after.take_whitespace();
70 (
71 InterpretedValue::from_raw_value(&raw_value.after, parser),
72 Some(raw_value.after),
73 )
74 };
75
76 let source = source.trim_remainder(attr_line.after);
77 Some(MatchedItem {
78 item: Self {
79 name: name.item,
80 value_source,
81 value,
82 source: source.trim_trailing_whitespace(),
83 },
84 after: attr_line.after,
85 })
86 }
87
88 pub fn name(&'src self) -> &'src Span<'src> {
90 &self.name
91 }
92
93 pub fn raw_value(&'src self) -> Option<Span<'src>> {
95 self.value_source
96 }
97
98 pub fn value(&'src self) -> &'src InterpretedValue {
100 &self.value
101 }
102}
103
104impl<'src> HasSpan<'src> for Attribute<'src> {
105 fn span(&self) -> Span<'src> {
106 self.source
107 }
108}
109
110impl<'src> IsBlock<'src> for Attribute<'src> {
111 fn content_model(&self) -> ContentModel {
112 ContentModel::Empty
113 }
114
115 fn raw_context(&self) -> CowStr<'src> {
116 "attribute".into()
117 }
118
119 fn title_source(&'src self) -> Option<Span<'src>> {
120 None
121 }
122
123 fn title(&self) -> Option<&str> {
124 None
125 }
126
127 fn anchor(&'src self) -> Option<Span<'src>> {
128 None
129 }
130
131 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
132 None
133 }
134
135 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
136 None
137 }
138}
139
140#[derive(Clone, Eq, PartialEq)]
146pub enum InterpretedValue {
147 Value(String),
149
150 Set,
153
154 Unset,
156}
157
158impl std::fmt::Debug for InterpretedValue {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self {
161 InterpretedValue::Value(value) => f
162 .debug_tuple("InterpretedValue::Value")
163 .field(value)
164 .finish(),
165
166 InterpretedValue::Set => write!(f, "InterpretedValue::Set"),
167 InterpretedValue::Unset => write!(f, "InterpretedValue::Unset"),
168 }
169 }
170}
171
172impl InterpretedValue {
173 fn from_raw_value(raw_value: &Span<'_>, parser: &Parser) -> Self {
174 let data = raw_value.data();
175 let mut content = Content::from(*raw_value);
176
177 if data.contains('\n') {
178 let lines: Vec<&str> = data.lines().collect();
179 let last_count = lines.len() - 1;
180
181 let value: Vec<String> = lines
182 .iter()
183 .enumerate()
184 .map(|(count, line)| {
185 let line = if count > 0 {
186 line.trim_start_matches(' ')
187 } else {
188 line
189 };
190
191 let line = line.trim_start_matches('\r').trim_end_matches(' ');
192
193 let line = if count < last_count {
198 line.trim_end_matches('\\').trim_end_matches(' ')
199 } else {
200 line
201 };
202
203 if line.ends_with('+') {
204 format!("{}\n", line.trim_end_matches('+').trim_end_matches(' '))
205 } else if count < last_count {
206 format!("{line} ")
207 } else {
208 line.to_string()
209 }
210 })
211 .collect();
212
213 content.rendered = CowStr::Boxed(value.join("").into_boxed_str());
214 }
215
216 SubstitutionGroup::Header.apply(&mut content, parser, None);
217
218 InterpretedValue::Value(content.rendered.into_string())
219 }
220
221 pub(crate) fn as_maybe_str(&self) -> Option<&str> {
222 match self {
223 InterpretedValue::Value(value) => Some(value.as_ref()),
224 _ => None,
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 #![allow(clippy::panic)]
232 #![allow(clippy::unwrap_used)]
233
234 use std::ops::Deref;
235
236 use crate::{blocks::ContentModel, tests::prelude::*};
237
238 #[test]
239 fn impl_clone() {
240 let h1 =
242 crate::document::Attribute::parse(crate::Span::new(":foo: bar"), &Parser::default())
243 .unwrap();
244 let h2 = h1.clone();
245 assert_eq!(h1, h2);
246 }
247
248 #[test]
249 fn simple_value() {
250 let mi = crate::document::Attribute::parse(
251 crate::Span::new(":foo: bar\nblah"),
252 &Parser::default(),
253 )
254 .unwrap();
255
256 assert_eq!(
257 mi.item,
258 Attribute {
259 name: Span {
260 data: "foo",
261 line: 1,
262 col: 2,
263 offset: 1,
264 },
265 value_source: Some(Span {
266 data: "bar",
267 line: 1,
268 col: 7,
269 offset: 6,
270 }),
271 value: InterpretedValue::Value("bar"),
272 source: Span {
273 data: ":foo: bar",
274 line: 1,
275 col: 1,
276 offset: 0,
277 }
278 }
279 );
280
281 assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
282
283 assert_eq!(
284 mi.after,
285 Span {
286 data: "blah",
287 line: 2,
288 col: 1,
289 offset: 10
290 }
291 );
292 }
293
294 #[test]
295 fn no_value() {
296 let mi =
297 crate::document::Attribute::parse(crate::Span::new(":foo:\nblah"), &Parser::default())
298 .unwrap();
299
300 assert_eq!(
301 mi.item,
302 Attribute {
303 name: Span {
304 data: "foo",
305 line: 1,
306 col: 2,
307 offset: 1,
308 },
309 value_source: None,
310 value: InterpretedValue::Set,
311 source: Span {
312 data: ":foo:",
313 line: 1,
314 col: 1,
315 offset: 0,
316 }
317 }
318 );
319
320 assert_eq!(mi.item.value(), InterpretedValue::Set);
321
322 assert_eq!(
323 mi.after,
324 Span {
325 data: "blah",
326 line: 2,
327 col: 1,
328 offset: 6
329 }
330 );
331 }
332
333 #[test]
334 fn name_with_hyphens() {
335 let mi = crate::document::Attribute::parse(
336 crate::Span::new(":name-with-hyphen:"),
337 &Parser::default(),
338 )
339 .unwrap();
340
341 assert_eq!(
342 mi.item,
343 Attribute {
344 name: Span {
345 data: "name-with-hyphen",
346 line: 1,
347 col: 2,
348 offset: 1,
349 },
350 value_source: None,
351 value: InterpretedValue::Set,
352 source: Span {
353 data: ":name-with-hyphen:",
354 line: 1,
355 col: 1,
356 offset: 0,
357 }
358 }
359 );
360
361 assert_eq!(mi.item.value(), InterpretedValue::Set);
362
363 assert_eq!(
364 mi.after,
365 Span {
366 data: "",
367 line: 1,
368 col: 19,
369 offset: 18
370 }
371 );
372 }
373
374 #[test]
375 fn unset_prefix() {
376 let mi =
377 crate::document::Attribute::parse(crate::Span::new(":!foo:\nblah"), &Parser::default())
378 .unwrap();
379
380 assert_eq!(
381 mi.item,
382 Attribute {
383 name: Span {
384 data: "foo",
385 line: 1,
386 col: 3,
387 offset: 2,
388 },
389 value_source: None,
390 value: InterpretedValue::Unset,
391 source: Span {
392 data: ":!foo:",
393 line: 1,
394 col: 1,
395 offset: 0,
396 }
397 }
398 );
399
400 assert_eq!(mi.item.value(), InterpretedValue::Unset);
401
402 assert_eq!(
403 mi.after,
404 Span {
405 data: "blah",
406 line: 2,
407 col: 1,
408 offset: 7
409 }
410 );
411 }
412
413 #[test]
414 fn unset_postfix() {
415 let mi =
416 crate::document::Attribute::parse(crate::Span::new(":foo!:\nblah"), &Parser::default())
417 .unwrap();
418
419 assert_eq!(
420 mi.item,
421 Attribute {
422 name: Span {
423 data: "foo",
424 line: 1,
425 col: 2,
426 offset: 1,
427 },
428 value_source: None,
429 value: InterpretedValue::Unset,
430 source: Span {
431 data: ":foo!:",
432 line: 1,
433 col: 1,
434 offset: 0,
435 }
436 }
437 );
438
439 assert_eq!(mi.item.value(), InterpretedValue::Unset);
440
441 assert_eq!(
442 mi.after,
443 Span {
444 data: "blah",
445 line: 2,
446 col: 1,
447 offset: 7
448 }
449 );
450 }
451
452 #[test]
453 fn err_unset_prefix_and_postfix() {
454 assert!(
455 crate::document::Attribute::parse(
456 crate::Span::new(":!foo!:\nblah"),
457 &Parser::default()
458 )
459 .is_none()
460 );
461 }
462
463 #[test]
464 fn err_invalid_ident1() {
465 assert!(
466 crate::document::Attribute::parse(
467 crate::Span::new(":@invalid:\nblah"),
468 &Parser::default()
469 )
470 .is_none()
471 );
472 }
473
474 #[test]
475 fn err_invalid_ident2() {
476 assert!(
477 crate::document::Attribute::parse(
478 crate::Span::new(":invalid@:\nblah"),
479 &Parser::default()
480 )
481 .is_none()
482 );
483 }
484
485 #[test]
486 fn err_invalid_ident3() {
487 assert!(
488 crate::document::Attribute::parse(
489 crate::Span::new(":-invalid:\nblah"),
490 &Parser::default()
491 )
492 .is_none()
493 );
494 }
495
496 #[test]
497 fn value_with_soft_wrap() {
498 let mi = crate::document::Attribute::parse(
499 crate::Span::new(":foo: bar \\\n blah"),
500 &Parser::default(),
501 )
502 .unwrap();
503
504 assert_eq!(
505 mi.item,
506 Attribute {
507 name: Span {
508 data: "foo",
509 line: 1,
510 col: 2,
511 offset: 1,
512 },
513 value_source: Some(Span {
514 data: "bar \\\n blah",
515 line: 1,
516 col: 7,
517 offset: 6,
518 }),
519 value: InterpretedValue::Value("bar blah"),
520 source: Span {
521 data: ":foo: bar \\\n blah",
522 line: 1,
523 col: 1,
524 offset: 0,
525 }
526 }
527 );
528
529 assert_eq!(mi.item.value(), InterpretedValue::Value("bar blah"));
530
531 assert_eq!(
532 mi.after,
533 Span {
534 data: "",
535 line: 2,
536 col: 6,
537 offset: 17
538 }
539 );
540 }
541
542 #[test]
543 fn bare_trailing_backslash_is_literal() {
544 let mi = crate::document::Attribute::parse(
548 crate::Span::new(":longpath: very/long/path/to/some/\\\nsubdirectory"),
549 &Parser::default(),
550 )
551 .unwrap();
552
553 assert_eq!(
554 mi.item,
555 Attribute {
556 name: Span {
557 data: "longpath",
558 line: 1,
559 col: 2,
560 offset: 1,
561 },
562 value_source: Some(Span {
563 data: "very/long/path/to/some/\\",
564 line: 1,
565 col: 12,
566 offset: 11,
567 }),
568 value: InterpretedValue::Value("very/long/path/to/some/\\"),
569 source: Span {
570 data: ":longpath: very/long/path/to/some/\\",
571 line: 1,
572 col: 1,
573 offset: 0,
574 }
575 }
576 );
577
578 assert_eq!(
579 mi.item.value(),
580 InterpretedValue::Value("very/long/path/to/some/\\")
581 );
582
583 assert_eq!(
585 mi.after,
586 Span {
587 data: "subdirectory",
588 line: 2,
589 col: 1,
590 offset: 36
591 }
592 );
593 }
594
595 #[test]
596 fn literal_trailing_backslash_on_final_line() {
597 let mi = crate::document::Attribute::parse(
600 crate::Span::new(":foo: bar \\\nbaz\\"),
601 &Parser::default(),
602 )
603 .unwrap();
604
605 assert_eq!(mi.item.value(), InterpretedValue::Value("bar baz\\"));
606 }
607
608 #[test]
609 fn value_with_hard_wrap() {
610 let mi = crate::document::Attribute::parse(
611 crate::Span::new(":foo: bar + \\\n blah"),
612 &Parser::default(),
613 )
614 .unwrap();
615
616 assert_eq!(
617 mi.item,
618 Attribute {
619 name: Span {
620 data: "foo",
621 line: 1,
622 col: 2,
623 offset: 1,
624 },
625 value_source: Some(Span {
626 data: "bar + \\\n blah",
627 line: 1,
628 col: 7,
629 offset: 6,
630 }),
631 value: InterpretedValue::Value("bar\nblah"),
632 source: Span {
633 data: ":foo: bar + \\\n blah",
634 line: 1,
635 col: 1,
636 offset: 0,
637 }
638 }
639 );
640
641 assert_eq!(mi.item.value(), InterpretedValue::Value("bar\nblah"));
642
643 assert_eq!(
644 mi.after,
645 Span {
646 data: "",
647 line: 2,
648 col: 6,
649 offset: 19
650 }
651 );
652 }
653
654 #[test]
655 fn is_block() {
656 let mut parser = Parser::default();
657 let maw = crate::blocks::Block::parse(crate::Span::new(":foo: bar\nblah"), &mut parser);
658
659 let mi = maw.item.unwrap();
660 let block = mi.item;
661
662 assert_eq!(
663 block,
664 Block::DocumentAttribute(Attribute {
665 name: Span {
666 data: "foo",
667 line: 1,
668 col: 2,
669 offset: 1,
670 },
671 value_source: Some(Span {
672 data: "bar",
673 line: 1,
674 col: 7,
675 offset: 6,
676 }),
677 value: InterpretedValue::Value("bar"),
678 source: Span {
679 data: ":foo: bar",
680 line: 1,
681 col: 1,
682 offset: 0,
683 }
684 })
685 );
686
687 assert_eq!(block.content_model(), ContentModel::Empty);
688 assert!(block.rendered_content().is_none());
689 assert_eq!(block.raw_context().deref(), "attribute");
690 assert!(block.nested_blocks().next().is_none());
691 assert!(block.title_source().is_none());
692 assert!(block.title().is_none());
693 assert!(block.anchor().is_none());
694 assert!(block.anchor_reftext().is_none());
695 assert!(block.attrlist().is_none());
696 assert_eq!(block.substitution_group(), SubstitutionGroup::Normal);
697
698 assert_eq!(
699 block.span(),
700 Span {
701 data: ":foo: bar",
702 line: 1,
703 col: 1,
704 offset: 0,
705 }
706 );
707
708 let crate::blocks::Block::DocumentAttribute(attr) = block else {
709 panic!("Wrong type");
710 };
711
712 assert_eq!(attr.value(), InterpretedValue::Value("bar"));
713
714 assert_eq!(
715 mi.after,
716 Span {
717 data: "blah",
718 line: 2,
719 col: 1,
720 offset: 10
721 }
722 );
723 }
724
725 #[test]
726 fn affects_document_state() {
727 let mut parser = Parser::default().with_intrinsic_attribute(
728 "agreed",
729 "yes",
730 ModificationContext::Anywhere,
731 );
732
733 let doc =
734 parser.parse("We are agreed? {agreed}\n\n:agreed: no\n\nAre we still agreed? {agreed}");
735
736 let mut blocks = doc.nested_blocks();
737
738 let block1 = blocks.next().unwrap();
739
740 assert_eq!(
741 block1,
742 &Block::Simple(SimpleBlock {
743 content: Content {
744 original: Span {
745 data: "We are agreed? {agreed}",
746 line: 1,
747 col: 1,
748 offset: 0,
749 },
750 rendered: "We are agreed? yes",
751 },
752 source: Span {
753 data: "We are agreed? {agreed}",
754 line: 1,
755 col: 1,
756 offset: 0,
757 },
758 style: SimpleBlockStyle::Paragraph,
759 title_source: None,
760 title: None,
761 caption: None,
762 number: None,
763 anchor: None,
764 anchor_reftext: None,
765 attrlist: None,
766 })
767 );
768
769 let _ = blocks.next().unwrap();
770
771 let block3 = blocks.next().unwrap();
772
773 assert_eq!(
774 block3,
775 &Block::Simple(SimpleBlock {
776 content: Content {
777 original: Span {
778 data: "Are we still agreed? {agreed}",
779 line: 5,
780 col: 1,
781 offset: 38,
782 },
783 rendered: "Are we still agreed? no",
784 },
785 source: Span {
786 data: "Are we still agreed? {agreed}",
787 line: 5,
788 col: 1,
789 offset: 38,
790 },
791 style: SimpleBlockStyle::Paragraph,
792 title_source: None,
793 title: None,
794 caption: None,
795 number: None,
796 anchor: None,
797 anchor_reftext: None,
798 attrlist: None,
799 })
800 );
801
802 let mut warnings = doc.warnings();
803 assert!(warnings.next().is_none());
804 }
805
806 #[test]
807 fn block_enforces_permission() {
808 let mut parser = Parser::default().with_intrinsic_attribute(
809 "agreed",
810 "yes",
811 ModificationContext::ApiOnly,
812 );
813
814 let doc = parser.parse("Hello\n\n:agreed: no\n\nAre we agreed? {agreed}");
815
816 let mut blocks = doc.nested_blocks();
817 let _ = blocks.next().unwrap();
818 let _ = blocks.next().unwrap();
819 let block3 = blocks.next().unwrap();
820
821 assert_eq!(
822 block3,
823 &Block::Simple(SimpleBlock {
824 content: Content {
825 original: Span {
826 data: "Are we agreed? {agreed}",
827 line: 5,
828 col: 1,
829 offset: 20,
830 },
831 rendered: "Are we agreed? yes",
832 },
833 source: Span {
834 data: "Are we agreed? {agreed}",
835 line: 5,
836 col: 1,
837 offset: 20,
838 },
839 style: SimpleBlockStyle::Paragraph,
840 title_source: None,
841 title: None,
842 caption: None,
843 number: None,
844 anchor: None,
845 anchor_reftext: None,
846 attrlist: None,
847 })
848 );
849
850 let mut warnings = doc.warnings();
851 let warning1 = warnings.next().unwrap();
852
853 assert_eq!(
854 &warning1.source,
855 Span {
856 data: ":agreed: no",
857 line: 3,
858 col: 1,
859 offset: 7,
860 }
861 );
862
863 assert_eq!(
864 warning1.warning,
865 WarningType::AttributeValueIsLocked("agreed".to_owned(),)
866 );
867
868 assert!(warnings.next().is_none());
869 }
870
871 mod interpreted_value {
872 mod impl_debug {
873 use crate::document::InterpretedValue;
874
875 #[test]
876 fn value_empty_string() {
877 let interpreted_value = InterpretedValue::Value("".to_string());
878 let debug_output = format!("{:?}", interpreted_value);
879 assert_eq!(debug_output, "InterpretedValue::Value(\"\")");
880 }
881
882 #[test]
883 fn value_simple_string() {
884 let interpreted_value = InterpretedValue::Value("hello".to_string());
885 let debug_output = format!("{:?}", interpreted_value);
886 assert_eq!(debug_output, "InterpretedValue::Value(\"hello\")");
887 }
888
889 #[test]
890 fn value_string_with_spaces() {
891 let interpreted_value = InterpretedValue::Value("hello world".to_string());
892 let debug_output = format!("{:?}", interpreted_value);
893 assert_eq!(debug_output, "InterpretedValue::Value(\"hello world\")");
894 }
895
896 #[test]
897 fn value_string_with_special_chars() {
898 let interpreted_value = InterpretedValue::Value("test!@#$%^&*()".to_string());
899 let debug_output = format!("{:?}", interpreted_value);
900 assert_eq!(debug_output, "InterpretedValue::Value(\"test!@#$%^&*()\")");
901 }
902
903 #[test]
904 fn value_string_with_quotes() {
905 let interpreted_value = InterpretedValue::Value("value\"with'quotes".to_string());
906 let debug_output = format!("{:?}", interpreted_value);
907 assert_eq!(
908 debug_output,
909 "InterpretedValue::Value(\"value\\\"with'quotes\")"
910 );
911 }
912
913 #[test]
914 fn value_string_with_newlines() {
915 let interpreted_value = InterpretedValue::Value("line1\nline2\nline3".to_string());
916 let debug_output = format!("{:?}", interpreted_value);
917 assert_eq!(
918 debug_output,
919 "InterpretedValue::Value(\"line1\\nline2\\nline3\")"
920 );
921 }
922
923 #[test]
924 fn value_string_with_backslashes() {
925 let interpreted_value = InterpretedValue::Value("path\\to\\file".to_string());
926 let debug_output = format!("{:?}", interpreted_value);
927 assert_eq!(
928 debug_output,
929 "InterpretedValue::Value(\"path\\\\to\\\\file\")"
930 );
931 }
932
933 #[test]
934 fn value_string_with_unicode() {
935 let interpreted_value = InterpretedValue::Value("café 🚀 ñoño".to_string());
936 let debug_output = format!("{:?}", interpreted_value);
937 assert_eq!(debug_output, "InterpretedValue::Value(\"café 🚀 ñoño\")");
938 }
939
940 #[test]
941 fn set() {
942 let interpreted_value = InterpretedValue::Set;
943 let debug_output = format!("{:?}", interpreted_value);
944 assert_eq!(debug_output, "InterpretedValue::Set");
945 }
946
947 #[test]
948 fn unset() {
949 let interpreted_value = InterpretedValue::Unset;
950 let debug_output = format!("{:?}", interpreted_value);
951 assert_eq!(debug_output, "InterpretedValue::Unset");
952 }
953 }
954 }
955}