1use std::sync::LazyLock;
2
3use regex::Regex;
4
5use crate::{
6 HasSpan, Parser, Span,
7 attributes::Attrlist,
8 blocks::{ContentModel, IsBlock},
9 content::{Content, SubstitutionGroup},
10 span::MatchedItem,
11 strings::CowStr,
12};
13
14#[derive(Clone, Debug, Eq, Hash, PartialEq)]
36pub struct Attribute<'src> {
37 name: Span<'src>,
38 value_source: Option<Span<'src>>,
39 value: InterpretedValue,
40 source: Span<'src>,
41}
42
43impl<'src> Attribute<'src> {
44 pub(crate) fn parse(source: Span<'src>, parser: &Parser) -> Option<MatchedItem<'src, Self>> {
45 let colon = source.take_prefix(":")?;
46
47 let mut unset = false;
48 let line = if colon.after.starts_with('!') {
49 unset = true;
50 colon.after.slice_from(1..)
51 } else {
52 colon.after
53 };
54
55 let name = line.take_user_attr_name()?;
56
57 let (name_item, after_name) = if name.item.data().ends_with('!') {
61 if unset {
62 return None;
63 }
64 unset = true;
65 let len = name.item.data().len();
66 (name.item.slice_to(..len - 1), name.after)
67 } else {
68 (name.item, name.after)
69 };
70
71 let line = after_name.take_prefix(":")?;
74
75 let (value, value_source, after) = if unset {
76 let extent = line
79 .after
80 .take_whitespace()
81 .after
82 .take_value_with_continuation();
83 (InterpretedValue::Unset, None, extent.after)
84 } else {
85 let first_line = line.after.take_normalized_line();
88
89 if first_line.item.is_empty() {
90 (InterpretedValue::Set, None, first_line.after)
93 } else {
94 let extent = line
101 .after
102 .take_required_whitespace()?
103 .after
104 .take_value_with_continuation();
105 (
106 InterpretedValue::from_raw_value(&extent.item, parser),
107 Some(extent.item),
108 extent.after,
109 )
110 }
111 };
112
113 let source = source.trim_remainder(after);
114 Some(MatchedItem {
115 item: Self {
116 name: name_item,
117 value_source,
118 value,
119 source: source.trim_trailing_whitespace(),
120 },
121 after,
122 })
123 }
124
125 pub fn name(&'src self) -> &'src Span<'src> {
127 &self.name
128 }
129
130 pub fn raw_value(&'src self) -> Option<Span<'src>> {
132 self.value_source
133 }
134
135 pub fn value(&'src self) -> &'src InterpretedValue {
137 &self.value
138 }
139}
140
141impl<'src> HasSpan<'src> for Attribute<'src> {
142 fn span(&self) -> Span<'src> {
143 self.source
144 }
145}
146
147impl<'src> IsBlock<'src> for Attribute<'src> {
148 fn content_model(&self) -> ContentModel {
149 ContentModel::Empty
150 }
151
152 fn raw_context(&self) -> CowStr<'src> {
153 "attribute".into()
154 }
155
156 fn title_source(&'src self) -> Option<Span<'src>> {
157 None
158 }
159
160 fn title(&self) -> Option<&str> {
161 None
162 }
163
164 fn anchor(&'src self) -> Option<Span<'src>> {
165 None
166 }
167
168 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
169 None
170 }
171
172 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
173 None
174 }
175}
176
177#[derive(Clone, Eq, Hash, PartialEq)]
183pub enum InterpretedValue {
184 Value(String),
186
187 Set,
190
191 Unset,
193}
194
195impl std::fmt::Debug for InterpretedValue {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 match self {
198 InterpretedValue::Value(value) => f
199 .debug_tuple("InterpretedValue::Value")
200 .field(value)
201 .finish(),
202
203 InterpretedValue::Set => write!(f, "InterpretedValue::Set"),
204 InterpretedValue::Unset => write!(f, "InterpretedValue::Unset"),
205 }
206 }
207}
208
209impl InterpretedValue {
210 fn from_raw_value(raw_value: &Span<'_>, parser: &Parser) -> Self {
211 let mut content = Content::from(*raw_value);
212
213 let folded = fold_continuation_value(raw_value.data());
217 if let Some(ref folded) = folded {
218 content.rendered = CowStr::Boxed(folded.clone().into_boxed_str());
219 }
220
221 let value = folded.as_deref().unwrap_or_else(|| raw_value.data());
229
230 if let Some(captures) = ATTRIBUTE_ENTRY_PASS_MACRO.captures(value) {
231 let inner = captures.get(2).map_or("", |m| m.as_str());
232
233 let rendered = match captures.get(1).map(|m| m.as_str()) {
234 None => inner.to_string(),
236
237 Some(subs) => {
238 let (group, _invalid) = SubstitutionGroup::from_custom_string(None, subs);
239 let mut inner_content = Content::from(Span::new(inner));
240 group.apply(&mut inner_content, parser, None);
241 inner_content.rendered().to_string()
242 }
243 };
244
245 return InterpretedValue::Value(rendered);
246 }
247
248 SubstitutionGroup::Header.apply(&mut content, parser, None);
249
250 InterpretedValue::Value(content.rendered.into_string())
251 }
252
253 pub(crate) fn as_maybe_str(&self) -> Option<&str> {
254 match self {
255 InterpretedValue::Value(value) => Some(value.as_ref()),
256 _ => None,
257 }
258 }
259}
260
261static ATTRIBUTE_ENTRY_PASS_MACRO: LazyLock<Regex> = LazyLock::new(|| {
267 #[allow(clippy::unwrap_used)]
268 Regex::new(r"(?s)\Apass:([a-z]+(?:,[a-z-]+)*)?\[(.*)\]\z").unwrap()
269});
270
271pub(crate) fn is_attribute_entry_pass_macro(value: &str) -> bool {
278 ATTRIBUTE_ENTRY_PASS_MACRO.is_match(value)
279}
280
281const ASCII_WHITESPACE: [char; 6] = [' ', '\t', '\n', '\r', '\x0C', '\x0B'];
290
291fn fold_continuation_value(data: &str) -> Option<String> {
310 const HARD_LINE_BREAK: &str = " +";
311
312 let mut lines = data
313 .split('\n')
314 .map(|line| line.strip_suffix('\r').unwrap_or(line));
315
316 let first = lines
321 .next()
322 .unwrap_or_default()
323 .trim_end_matches(ASCII_WHITESPACE);
324
325 let con: &str = if first.ends_with(" \\") {
328 " \\"
329 } else if first.ends_with(" +") {
330 " +"
331 } else {
332 return None;
333 };
334
335 let mut value = first[..first.len() - con.len()]
337 .trim_end_matches(ASCII_WHITESPACE)
338 .to_string();
339
340 for line in lines {
341 if line.is_empty() {
343 break;
344 }
345
346 let line = line.trim_matches(ASCII_WHITESPACE);
350
351 let keep_open = line.ends_with(con);
354 let piece = if keep_open {
355 line[..line.len() - con.len()].trim_end_matches(ASCII_WHITESPACE)
356 } else {
357 line
358 };
359
360 value.push_str(if value.ends_with(HARD_LINE_BREAK) {
363 "\n"
364 } else {
365 " "
366 });
367
368 value.push_str(piece);
369
370 if !keep_open {
371 break;
372 }
373 }
374
375 Some(value)
376}
377
378#[cfg(test)]
379mod tests {
380 #![allow(clippy::panic)]
381 #![allow(clippy::unwrap_used)]
382
383 use std::ops::Deref;
384
385 use crate::{blocks::ContentModel, tests::prelude::*};
386
387 #[test]
388 fn impl_clone() {
389 let h1 =
391 crate::document::Attribute::parse(crate::Span::new(":foo: bar"), &Parser::default())
392 .unwrap();
393 let h2 = h1.clone();
394 assert_eq!(h1, h2);
395 }
396
397 #[test]
398 fn whole_value_pass_macro_keeps_nested_brackets() {
399 let value = |src| {
404 crate::document::Attribute::parse(crate::Span::new(src), &Parser::default())
405 .unwrap()
406 .item
407 .value()
408 .clone()
409 };
410
411 assert_eq!(
412 value(":foo: pass:n[https://example.org/x[a *b* c]]"),
413 InterpretedValue::Value("<a href=\"https://example.org/x\">a <strong>b</strong> c</a>")
414 );
415
416 assert_eq!(
419 value(":foo: pass:[a[b] *c*]"),
420 InterpretedValue::Value("a[b] *c*")
421 );
422 }
423
424 #[test]
425 fn simple_value() {
426 let mi = crate::document::Attribute::parse(
427 crate::Span::new(":foo: bar\nblah"),
428 &Parser::default(),
429 )
430 .unwrap();
431
432 assert_eq!(
433 mi.item,
434 Attribute {
435 name: Span {
436 data: "foo",
437 line: 1,
438 col: 2,
439 offset: 1,
440 },
441 value_source: Some(Span {
442 data: "bar",
443 line: 1,
444 col: 7,
445 offset: 6,
446 }),
447 value: InterpretedValue::Value("bar"),
448 source: Span {
449 data: ":foo: bar",
450 line: 1,
451 col: 1,
452 offset: 0,
453 }
454 }
455 );
456
457 assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
458
459 assert_eq!(
460 mi.after,
461 Span {
462 data: "blah",
463 line: 2,
464 col: 1,
465 offset: 10
466 }
467 );
468 }
469
470 #[test]
471 fn no_value() {
472 let mi =
473 crate::document::Attribute::parse(crate::Span::new(":foo:\nblah"), &Parser::default())
474 .unwrap();
475
476 assert_eq!(
477 mi.item,
478 Attribute {
479 name: Span {
480 data: "foo",
481 line: 1,
482 col: 2,
483 offset: 1,
484 },
485 value_source: None,
486 value: InterpretedValue::Set,
487 source: Span {
488 data: ":foo:",
489 line: 1,
490 col: 1,
491 offset: 0,
492 }
493 }
494 );
495
496 assert_eq!(mi.item.value(), InterpretedValue::Set);
497
498 assert_eq!(
499 mi.after,
500 Span {
501 data: "blah",
502 line: 2,
503 col: 1,
504 offset: 6
505 }
506 );
507 }
508
509 #[test]
510 fn name_with_hyphens() {
511 let mi = crate::document::Attribute::parse(
512 crate::Span::new(":name-with-hyphen:"),
513 &Parser::default(),
514 )
515 .unwrap();
516
517 assert_eq!(
518 mi.item,
519 Attribute {
520 name: Span {
521 data: "name-with-hyphen",
522 line: 1,
523 col: 2,
524 offset: 1,
525 },
526 value_source: None,
527 value: InterpretedValue::Set,
528 source: Span {
529 data: ":name-with-hyphen:",
530 line: 1,
531 col: 1,
532 offset: 0,
533 }
534 }
535 );
536
537 assert_eq!(mi.item.value(), InterpretedValue::Set);
538
539 assert_eq!(
540 mi.after,
541 Span {
542 data: "",
543 line: 1,
544 col: 19,
545 offset: 18
546 }
547 );
548 }
549
550 #[test]
551 fn unset_prefix() {
552 let mi =
553 crate::document::Attribute::parse(crate::Span::new(":!foo:\nblah"), &Parser::default())
554 .unwrap();
555
556 assert_eq!(
557 mi.item,
558 Attribute {
559 name: Span {
560 data: "foo",
561 line: 1,
562 col: 3,
563 offset: 2,
564 },
565 value_source: None,
566 value: InterpretedValue::Unset,
567 source: Span {
568 data: ":!foo:",
569 line: 1,
570 col: 1,
571 offset: 0,
572 }
573 }
574 );
575
576 assert_eq!(mi.item.value(), InterpretedValue::Unset);
577
578 assert_eq!(
579 mi.after,
580 Span {
581 data: "blah",
582 line: 2,
583 col: 1,
584 offset: 7
585 }
586 );
587 }
588
589 #[test]
590 fn unset_postfix() {
591 let mi =
592 crate::document::Attribute::parse(crate::Span::new(":foo!:\nblah"), &Parser::default())
593 .unwrap();
594
595 assert_eq!(
596 mi.item,
597 Attribute {
598 name: Span {
599 data: "foo",
600 line: 1,
601 col: 2,
602 offset: 1,
603 },
604 value_source: None,
605 value: InterpretedValue::Unset,
606 source: Span {
607 data: ":foo!:",
608 line: 1,
609 col: 1,
610 offset: 0,
611 }
612 }
613 );
614
615 assert_eq!(mi.item.value(), InterpretedValue::Unset);
616
617 assert_eq!(
618 mi.after,
619 Span {
620 data: "blah",
621 line: 2,
622 col: 1,
623 offset: 7
624 }
625 );
626 }
627
628 #[test]
629 fn err_unset_prefix_and_postfix() {
630 assert!(
631 crate::document::Attribute::parse(
632 crate::Span::new(":!foo!:\nblah"),
633 &Parser::default()
634 )
635 .is_none()
636 );
637 }
638
639 #[test]
640 fn err_invalid_ident1() {
641 assert!(
642 crate::document::Attribute::parse(
643 crate::Span::new(":@invalid:\nblah"),
644 &Parser::default()
645 )
646 .is_none()
647 );
648 }
649
650 #[test]
651 fn err_missing_closing_colon() {
652 assert!(
655 crate::document::Attribute::parse(
656 crate::Span::new(":foo bar\nblah"),
657 &Parser::default()
658 )
659 .is_none()
660 );
661
662 assert!(
664 crate::document::Attribute::parse(crate::Span::new(":foo"), &Parser::default())
665 .is_none()
666 );
667 }
668
669 #[test]
670 fn name_captures_trailing_non_word_char() {
671 let mi = crate::document::Attribute::parse(
676 crate::Span::new(":invalid@:\nblah"),
677 &Parser::default(),
678 )
679 .unwrap();
680
681 assert_eq!(
682 mi.item,
683 Attribute {
684 name: Span {
685 data: "invalid@",
686 line: 1,
687 col: 2,
688 offset: 1,
689 },
690 value_source: None,
691 value: InterpretedValue::Set,
692 source: Span {
693 data: ":invalid@:",
694 line: 1,
695 col: 1,
696 offset: 0,
697 }
698 }
699 );
700
701 assert_eq!(
702 mi.after,
703 Span {
704 data: "blah",
705 line: 2,
706 col: 1,
707 offset: 11
708 }
709 );
710 }
711
712 #[test]
713 fn name_with_spaces() {
714 let mi = crate::document::Attribute::parse(
719 crate::Span::new(":Author Initials: SJR"),
720 &Parser::default(),
721 )
722 .unwrap();
723
724 assert_eq!(
725 mi.item,
726 Attribute {
727 name: Span {
728 data: "Author Initials",
729 line: 1,
730 col: 2,
731 offset: 1,
732 },
733 value_source: Some(Span {
734 data: "SJR",
735 line: 1,
736 col: 19,
737 offset: 18,
738 }),
739 value: InterpretedValue::Value("SJR"),
740 source: Span {
741 data: ":Author Initials: SJR",
742 line: 1,
743 col: 1,
744 offset: 0,
745 }
746 }
747 );
748 }
749
750 #[test]
751 fn err_invalid_ident3() {
752 assert!(
753 crate::document::Attribute::parse(
754 crate::Span::new(":-invalid:\nblah"),
755 &Parser::default()
756 )
757 .is_none()
758 );
759 }
760
761 #[test]
762 fn value_with_soft_wrap() {
763 let mi = crate::document::Attribute::parse(
764 crate::Span::new(":foo: bar \\\n blah"),
765 &Parser::default(),
766 )
767 .unwrap();
768
769 assert_eq!(
770 mi.item,
771 Attribute {
772 name: Span {
773 data: "foo",
774 line: 1,
775 col: 2,
776 offset: 1,
777 },
778 value_source: Some(Span {
779 data: "bar \\\n blah",
780 line: 1,
781 col: 7,
782 offset: 6,
783 }),
784 value: InterpretedValue::Value("bar blah"),
785 source: Span {
786 data: ":foo: bar \\\n blah",
787 line: 1,
788 col: 1,
789 offset: 0,
790 }
791 }
792 );
793
794 assert_eq!(mi.item.value(), InterpretedValue::Value("bar blah"));
795
796 assert_eq!(
797 mi.after,
798 Span {
799 data: "",
800 line: 2,
801 col: 6,
802 offset: 17
803 }
804 );
805 }
806
807 #[test]
808 fn bare_trailing_backslash_is_literal() {
809 let mi = crate::document::Attribute::parse(
813 crate::Span::new(":longpath: very/long/path/to/some/\\\nsubdirectory"),
814 &Parser::default(),
815 )
816 .unwrap();
817
818 assert_eq!(
819 mi.item,
820 Attribute {
821 name: Span {
822 data: "longpath",
823 line: 1,
824 col: 2,
825 offset: 1,
826 },
827 value_source: Some(Span {
828 data: "very/long/path/to/some/\\",
829 line: 1,
830 col: 12,
831 offset: 11,
832 }),
833 value: InterpretedValue::Value("very/long/path/to/some/\\"),
834 source: Span {
835 data: ":longpath: very/long/path/to/some/\\",
836 line: 1,
837 col: 1,
838 offset: 0,
839 }
840 }
841 );
842
843 assert_eq!(
844 mi.item.value(),
845 InterpretedValue::Value("very/long/path/to/some/\\")
846 );
847
848 assert_eq!(
850 mi.after,
851 Span {
852 data: "subdirectory",
853 line: 2,
854 col: 1,
855 offset: 36
856 }
857 );
858 }
859
860 #[test]
861 fn literal_trailing_backslash_on_final_line() {
862 let mi = crate::document::Attribute::parse(
865 crate::Span::new(":foo: bar \\\nbaz\\"),
866 &Parser::default(),
867 )
868 .unwrap();
869
870 assert_eq!(mi.item.value(), InterpretedValue::Value("bar baz\\"));
871 }
872
873 #[test]
874 fn value_with_hard_wrap() {
875 let mi = crate::document::Attribute::parse(
876 crate::Span::new(":foo: bar + \\\n blah"),
877 &Parser::default(),
878 )
879 .unwrap();
880
881 assert_eq!(
882 mi.item,
883 Attribute {
884 name: Span {
885 data: "foo",
886 line: 1,
887 col: 2,
888 offset: 1,
889 },
890 value_source: Some(Span {
891 data: "bar + \\\n blah",
892 line: 1,
893 col: 7,
894 offset: 6,
895 }),
896 value: InterpretedValue::Value("bar +\nblah"),
897 source: Span {
898 data: ":foo: bar + \\\n blah",
899 line: 1,
900 col: 1,
901 offset: 0,
902 }
903 }
904 );
905
906 assert_eq!(mi.item.value(), InterpretedValue::Value("bar +\nblah"));
907
908 assert_eq!(
909 mi.after,
910 Span {
911 data: "",
912 line: 2,
913 col: 6,
914 offset: 19
915 }
916 );
917 }
918
919 #[test]
920 fn single_line_trailing_hard_break_marker_is_stripped() {
921 let mi =
928 crate::document::Attribute::parse(crate::Span::new(":foo: bar +"), &Parser::default())
929 .unwrap();
930
931 assert_eq!(
932 mi.item,
933 Attribute {
934 name: Span {
935 data: "foo",
936 line: 1,
937 col: 2,
938 offset: 1,
939 },
940 value_source: Some(Span {
941 data: "bar +",
942 line: 1,
943 col: 7,
944 offset: 6,
945 }),
946 value: InterpretedValue::Value("bar"),
947 source: Span {
948 data: ":foo: bar +",
949 line: 1,
950 col: 1,
951 offset: 0,
952 }
953 }
954 );
955
956 assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
957
958 assert_eq!(
959 mi.after,
960 Span {
961 data: "",
962 line: 1,
963 col: 12,
964 offset: 11
965 }
966 );
967 }
968
969 #[test]
970 fn legacy_multi_line_value_is_fused() {
971 let mi = crate::document::Attribute::parse(
976 crate::Span::new(":foo: bar +\nblah"),
977 &Parser::default(),
978 )
979 .unwrap();
980
981 assert_eq!(
982 mi.item,
983 Attribute {
984 name: Span {
985 data: "foo",
986 line: 1,
987 col: 2,
988 offset: 1,
989 },
990 value_source: Some(Span {
991 data: "bar +\nblah",
992 line: 1,
993 col: 7,
994 offset: 6,
995 }),
996 value: InterpretedValue::Value("bar blah"),
997 source: Span {
998 data: ":foo: bar +\nblah",
999 line: 1,
1000 col: 1,
1001 offset: 0,
1002 }
1003 }
1004 );
1005
1006 assert_eq!(mi.item.value(), InterpretedValue::Value("bar blah"));
1007
1008 assert_eq!(
1009 mi.after,
1010 Span {
1011 data: "",
1012 line: 2,
1013 col: 5,
1014 offset: 16
1015 }
1016 );
1017 }
1018
1019 #[test]
1020 fn legacy_hard_break_marker_edge_cases() {
1021 let value = |src| {
1027 crate::document::Attribute::parse(crate::Span::new(src), &Parser::default())
1028 .unwrap()
1029 .item
1030 .value()
1031 .clone()
1032 };
1033
1034 assert_eq!(value(":foo: bar +\nx"), InterpretedValue::Value("bar x"));
1037
1038 assert_eq!(value(":foo: bar\t +\nx"), InterpretedValue::Value("bar x"));
1040
1041 assert_eq!(value(":foo: bar ++\nx"), InterpretedValue::Value("bar ++"));
1043
1044 assert_eq!(value(":foo: bar+\nx"), InterpretedValue::Value("bar+"));
1046
1047 assert_eq!(value(":foo: bar\t+\nx"), InterpretedValue::Value("bar\t+"));
1049
1050 assert_eq!(value(":foo: +\nx"), InterpretedValue::Value("+"));
1052
1053 assert_eq!(
1057 value(":foo: bar + +\nx"),
1058 InterpretedValue::Value("bar +\nx")
1059 );
1060
1061 assert_eq!(
1064 value(":foo: bar\u{00a0} +\nx"),
1065 InterpretedValue::Value("bar\u{00a0} x")
1066 );
1067 }
1068
1069 #[test]
1070 fn bare_marker_only_continuation_line_does_not_swallow_next_line() {
1071 let mi = crate::document::Attribute::parse(
1076 crate::Span::new(":foo: text +\n +\nmore"),
1077 &Parser::default(),
1078 )
1079 .unwrap();
1080
1081 assert_eq!(
1082 mi.item,
1083 Attribute {
1084 name: Span {
1085 data: "foo",
1086 line: 1,
1087 col: 2,
1088 offset: 1,
1089 },
1090 value_source: Some(Span {
1091 data: "text +\n +",
1092 line: 1,
1093 col: 7,
1094 offset: 6,
1095 }),
1096 value: InterpretedValue::Value("text +"),
1097 source: Span {
1098 data: ":foo: text +\n +",
1099 line: 1,
1100 col: 1,
1101 offset: 0,
1102 }
1103 }
1104 );
1105
1106 assert_eq!(
1108 mi.after,
1109 Span {
1110 data: "more",
1111 line: 3,
1112 col: 1,
1113 offset: 16,
1114 }
1115 );
1116 }
1117
1118 #[test]
1119 fn is_block() {
1120 let mut parser = Parser::default();
1121 let maw = crate::blocks::Block::parse(crate::Span::new(":foo: bar\nblah"), &mut parser);
1122
1123 let mi = maw.item.unwrap();
1124 let block = mi.item;
1125
1126 assert_eq!(
1127 block,
1128 Block::DocumentAttribute(Attribute {
1129 name: Span {
1130 data: "foo",
1131 line: 1,
1132 col: 2,
1133 offset: 1,
1134 },
1135 value_source: Some(Span {
1136 data: "bar",
1137 line: 1,
1138 col: 7,
1139 offset: 6,
1140 }),
1141 value: InterpretedValue::Value("bar"),
1142 source: Span {
1143 data: ":foo: bar",
1144 line: 1,
1145 col: 1,
1146 offset: 0,
1147 }
1148 })
1149 );
1150
1151 assert_eq!(block.content_model(), ContentModel::Empty);
1152 assert!(block.rendered_content().is_none());
1153 assert_eq!(block.raw_context().deref(), "attribute");
1154 assert!(block.child_blocks().next().is_none());
1155 assert!(block.title_source().is_none());
1156 assert!(block.title().is_none());
1157 assert!(block.anchor().is_none());
1158 assert!(block.anchor_reftext().is_none());
1159 assert!(block.attrlist().is_none());
1160 assert_eq!(block.substitution_group(), SubstitutionGroup::Normal);
1161
1162 assert_eq!(
1163 block.span(),
1164 Span {
1165 data: ":foo: bar",
1166 line: 1,
1167 col: 1,
1168 offset: 0,
1169 }
1170 );
1171
1172 let crate::blocks::Block::DocumentAttribute(attr) = block else {
1173 panic!("Wrong type");
1174 };
1175
1176 assert_eq!(attr.value(), InterpretedValue::Value("bar"));
1177
1178 assert_eq!(
1179 mi.after,
1180 Span {
1181 data: "blah",
1182 line: 2,
1183 col: 1,
1184 offset: 10
1185 }
1186 );
1187 }
1188
1189 #[test]
1190 fn affects_document_state() {
1191 let mut parser = Parser::default().with_intrinsic_attribute(
1192 "agreed",
1193 "yes",
1194 ModificationContext::Anywhere,
1195 );
1196
1197 let doc =
1198 parser.parse("We are agreed? {agreed}\n\n:agreed: no\n\nAre we still agreed? {agreed}");
1199
1200 let mut blocks = doc.child_blocks();
1201
1202 let block1 = blocks.next().unwrap();
1203
1204 assert_eq!(
1205 block1,
1206 &Block::Simple(SimpleBlock {
1207 content: Content {
1208 original: Span {
1209 data: "We are agreed? {agreed}",
1210 line: 1,
1211 col: 1,
1212 offset: 0,
1213 },
1214 rendered: "We are agreed? yes",
1215 },
1216 source: Span {
1217 data: "We are agreed? {agreed}",
1218 line: 1,
1219 col: 1,
1220 offset: 0,
1221 },
1222 style: SimpleBlockStyle::Paragraph,
1223 title_source: None,
1224 title: None,
1225 caption: None,
1226 number: None,
1227 anchor: None,
1228 anchor_reftext: None,
1229 attrlist: None,
1230 })
1231 );
1232
1233 let _ = blocks.next().unwrap();
1234
1235 let block3 = blocks.next().unwrap();
1236
1237 assert_eq!(
1238 block3,
1239 &Block::Simple(SimpleBlock {
1240 content: Content {
1241 original: Span {
1242 data: "Are we still agreed? {agreed}",
1243 line: 5,
1244 col: 1,
1245 offset: 38,
1246 },
1247 rendered: "Are we still agreed? no",
1248 },
1249 source: Span {
1250 data: "Are we still agreed? {agreed}",
1251 line: 5,
1252 col: 1,
1253 offset: 38,
1254 },
1255 style: SimpleBlockStyle::Paragraph,
1256 title_source: None,
1257 title: None,
1258 caption: None,
1259 number: None,
1260 anchor: None,
1261 anchor_reftext: None,
1262 attrlist: None,
1263 })
1264 );
1265
1266 let mut warnings = doc.warnings();
1267 assert!(warnings.next().is_none());
1268 }
1269
1270 #[test]
1271 fn block_enforces_permission() {
1272 let mut parser = Parser::default().with_intrinsic_attribute(
1273 "agreed",
1274 "yes",
1275 ModificationContext::ApiOnly,
1276 );
1277
1278 let doc = parser.parse("Hello\n\n:agreed: no\n\nAre we agreed? {agreed}");
1279
1280 let mut blocks = doc.child_blocks();
1281 let _ = blocks.next().unwrap();
1282 let _ = blocks.next().unwrap();
1283 let block3 = blocks.next().unwrap();
1284
1285 assert_eq!(
1286 block3,
1287 &Block::Simple(SimpleBlock {
1288 content: Content {
1289 original: Span {
1290 data: "Are we agreed? {agreed}",
1291 line: 5,
1292 col: 1,
1293 offset: 20,
1294 },
1295 rendered: "Are we agreed? yes",
1296 },
1297 source: Span {
1298 data: "Are we agreed? {agreed}",
1299 line: 5,
1300 col: 1,
1301 offset: 20,
1302 },
1303 style: SimpleBlockStyle::Paragraph,
1304 title_source: None,
1305 title: None,
1306 caption: None,
1307 number: None,
1308 anchor: None,
1309 anchor_reftext: None,
1310 attrlist: None,
1311 })
1312 );
1313
1314 let mut warnings = doc.warnings();
1315 let warning1 = warnings.next().unwrap();
1316
1317 assert_eq!(
1318 &warning1.source,
1319 Span {
1320 data: ":agreed: no",
1321 line: 3,
1322 col: 1,
1323 offset: 7,
1324 }
1325 );
1326
1327 assert_eq!(
1328 warning1.warning,
1329 WarningType::AttributeValueIsLocked("agreed".to_owned(),)
1330 );
1331
1332 assert!(warnings.next().is_none());
1333 }
1334
1335 mod fold_continuation_value {
1336 use super::super::fold_continuation_value;
1337
1338 #[test]
1339 fn no_marker_returns_none() {
1340 assert_eq!(fold_continuation_value("bar"), None);
1343 assert_eq!(fold_continuation_value("bar+"), None);
1344 assert_eq!(fold_continuation_value("bar ++"), None);
1345 }
1346
1347 #[test]
1348 fn modern_soft_wrap_folds_with_space() {
1349 assert_eq!(
1350 fold_continuation_value("bar \\\nblah"),
1351 Some("bar blah".to_string())
1352 );
1353 }
1354
1355 #[test]
1356 fn legacy_marker_folds_with_space() {
1357 assert_eq!(
1358 fold_continuation_value("This is the first +\nRuby implementation of +\nAsciiDoc."),
1359 Some("This is the first Ruby implementation of AsciiDoc.".to_string())
1360 );
1361 }
1362
1363 #[test]
1364 fn hard_line_break_joins_with_newline() {
1365 assert_eq!(
1368 fold_continuation_value("bar + \\\nblah"),
1369 Some("bar +\nblah".to_string())
1370 );
1371 }
1372
1373 #[test]
1374 fn blank_line_terminates_value() {
1375 assert_eq!(fold_continuation_value("a +\n\nb"), Some("a".to_string()));
1378 }
1379
1380 #[test]
1381 fn crlf_line_endings_are_normalized() {
1382 assert_eq!(
1383 fold_continuation_value("bar +\r\nblah"),
1384 Some("bar blah".to_string())
1385 );
1386 }
1387
1388 #[test]
1389 fn bare_marker_only_line_terminates_value() {
1390 assert_eq!(
1396 fold_continuation_value("text +\n +\nmore"),
1397 Some("text +".to_string())
1398 );
1399 }
1400
1401 #[test]
1402 fn trailing_whitespace_after_marker_still_continues() {
1403 assert_eq!(
1407 fold_continuation_value("a +\nb + \nmore"),
1408 Some("a b more".to_string())
1409 );
1410 }
1411 }
1412
1413 mod interpreted_value {
1414 mod impl_debug {
1415 use crate::document::InterpretedValue;
1416
1417 #[test]
1418 fn value_empty_string() {
1419 let interpreted_value = InterpretedValue::Value("".to_string());
1420 let debug_output = format!("{:?}", interpreted_value);
1421 assert_eq!(debug_output, "InterpretedValue::Value(\"\")");
1422 }
1423
1424 #[test]
1425 fn value_simple_string() {
1426 let interpreted_value = InterpretedValue::Value("hello".to_string());
1427 let debug_output = format!("{:?}", interpreted_value);
1428 assert_eq!(debug_output, "InterpretedValue::Value(\"hello\")");
1429 }
1430
1431 #[test]
1432 fn value_string_with_spaces() {
1433 let interpreted_value = InterpretedValue::Value("hello world".to_string());
1434 let debug_output = format!("{:?}", interpreted_value);
1435 assert_eq!(debug_output, "InterpretedValue::Value(\"hello world\")");
1436 }
1437
1438 #[test]
1439 fn value_string_with_special_chars() {
1440 let interpreted_value = InterpretedValue::Value("test!@#$%^&*()".to_string());
1441 let debug_output = format!("{:?}", interpreted_value);
1442 assert_eq!(debug_output, "InterpretedValue::Value(\"test!@#$%^&*()\")");
1443 }
1444
1445 #[test]
1446 fn value_string_with_quotes() {
1447 let interpreted_value = InterpretedValue::Value("value\"with'quotes".to_string());
1448 let debug_output = format!("{:?}", interpreted_value);
1449 assert_eq!(
1450 debug_output,
1451 "InterpretedValue::Value(\"value\\\"with'quotes\")"
1452 );
1453 }
1454
1455 #[test]
1456 fn value_string_with_newlines() {
1457 let interpreted_value = InterpretedValue::Value("line1\nline2\nline3".to_string());
1458 let debug_output = format!("{:?}", interpreted_value);
1459 assert_eq!(
1460 debug_output,
1461 "InterpretedValue::Value(\"line1\\nline2\\nline3\")"
1462 );
1463 }
1464
1465 #[test]
1466 fn value_string_with_backslashes() {
1467 let interpreted_value = InterpretedValue::Value("path\\to\\file".to_string());
1468 let debug_output = format!("{:?}", interpreted_value);
1469 assert_eq!(
1470 debug_output,
1471 "InterpretedValue::Value(\"path\\\\to\\\\file\")"
1472 );
1473 }
1474
1475 #[test]
1476 fn value_string_with_unicode() {
1477 let interpreted_value = InterpretedValue::Value("café 🚀 ñoño".to_string());
1478 let debug_output = format!("{:?}", interpreted_value);
1479 assert_eq!(debug_output, "InterpretedValue::Value(\"café 🚀 ñoño\")");
1480 }
1481
1482 #[test]
1483 fn set() {
1484 let interpreted_value = InterpretedValue::Set;
1485 let debug_output = format!("{:?}", interpreted_value);
1486 assert_eq!(debug_output, "InterpretedValue::Set");
1487 }
1488
1489 #[test]
1490 fn unset() {
1491 let interpreted_value = InterpretedValue::Unset;
1492 let debug_output = format!("{:?}", interpreted_value);
1493 assert_eq!(debug_output, "InterpretedValue::Unset");
1494 }
1495 }
1496 }
1497}