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 colon = source.take_prefix(":")?;
42
43 let mut unset = false;
44 let line = if colon.after.starts_with('!') {
45 unset = true;
46 colon.after.slice_from(1..)
47 } else {
48 colon.after
49 };
50
51 let name = line.take_user_attr_name()?;
52
53 let (name_item, after_name) = if name.item.data().ends_with('!') {
57 if unset {
58 return None;
59 }
60 unset = true;
61 let len = name.item.data().len();
62 (name.item.slice_to(..len - 1), name.after)
63 } else {
64 (name.item, name.after)
65 };
66
67 let line = after_name.take_prefix(":")?;
70
71 let (value, value_source, after) = if unset {
72 let extent = line
75 .after
76 .take_whitespace()
77 .after
78 .take_value_with_continuation();
79 (InterpretedValue::Unset, None, extent.after)
80 } else {
81 let first_line = line.after.take_normalized_line();
84
85 if first_line.item.is_empty() {
86 (InterpretedValue::Set, None, first_line.after)
89 } else {
90 let extent = line
97 .after
98 .take_required_whitespace()?
99 .after
100 .take_value_with_continuation();
101 (
102 InterpretedValue::from_raw_value(&extent.item, parser),
103 Some(extent.item),
104 extent.after,
105 )
106 }
107 };
108
109 let source = source.trim_remainder(after);
110 Some(MatchedItem {
111 item: Self {
112 name: name_item,
113 value_source,
114 value,
115 source: source.trim_trailing_whitespace(),
116 },
117 after,
118 })
119 }
120
121 pub fn name(&'src self) -> &'src Span<'src> {
123 &self.name
124 }
125
126 pub fn raw_value(&'src self) -> Option<Span<'src>> {
128 self.value_source
129 }
130
131 pub fn value(&'src self) -> &'src InterpretedValue {
133 &self.value
134 }
135}
136
137impl<'src> HasSpan<'src> for Attribute<'src> {
138 fn span(&self) -> Span<'src> {
139 self.source
140 }
141}
142
143impl<'src> IsBlock<'src> for Attribute<'src> {
144 fn content_model(&self) -> ContentModel {
145 ContentModel::Empty
146 }
147
148 fn raw_context(&self) -> CowStr<'src> {
149 "attribute".into()
150 }
151
152 fn title_source(&'src self) -> Option<Span<'src>> {
153 None
154 }
155
156 fn title(&self) -> Option<&str> {
157 None
158 }
159
160 fn anchor(&'src self) -> Option<Span<'src>> {
161 None
162 }
163
164 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
165 None
166 }
167
168 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
169 None
170 }
171}
172
173#[derive(Clone, Eq, PartialEq)]
179pub enum InterpretedValue {
180 Value(String),
182
183 Set,
186
187 Unset,
189}
190
191impl std::fmt::Debug for InterpretedValue {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 match self {
194 InterpretedValue::Value(value) => f
195 .debug_tuple("InterpretedValue::Value")
196 .field(value)
197 .finish(),
198
199 InterpretedValue::Set => write!(f, "InterpretedValue::Set"),
200 InterpretedValue::Unset => write!(f, "InterpretedValue::Unset"),
201 }
202 }
203}
204
205impl InterpretedValue {
206 fn from_raw_value(raw_value: &Span<'_>, parser: &Parser) -> Self {
207 let mut content = Content::from(*raw_value);
208
209 if let Some(folded) = fold_continuation_value(raw_value.data()) {
213 content.rendered = CowStr::Boxed(folded.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
229const ASCII_WHITESPACE: [char; 6] = [' ', '\t', '\n', '\r', '\x0C', '\x0B'];
238
239fn fold_continuation_value(data: &str) -> Option<String> {
258 const HARD_LINE_BREAK: &str = " +";
259
260 let mut lines = data
261 .split('\n')
262 .map(|line| line.strip_suffix('\r').unwrap_or(line));
263
264 let first = lines
269 .next()
270 .unwrap_or_default()
271 .trim_end_matches(ASCII_WHITESPACE);
272
273 let con: &str = if first.ends_with(" \\") {
276 " \\"
277 } else if first.ends_with(" +") {
278 " +"
279 } else {
280 return None;
281 };
282
283 let mut value = first[..first.len() - con.len()]
285 .trim_end_matches(ASCII_WHITESPACE)
286 .to_string();
287
288 for line in lines {
289 if line.is_empty() {
291 break;
292 }
293
294 let line = line.trim_matches(ASCII_WHITESPACE);
298
299 let keep_open = line.ends_with(con);
302 let piece = if keep_open {
303 line[..line.len() - con.len()].trim_end_matches(ASCII_WHITESPACE)
304 } else {
305 line
306 };
307
308 value.push_str(if value.ends_with(HARD_LINE_BREAK) {
311 "\n"
312 } else {
313 " "
314 });
315
316 value.push_str(piece);
317
318 if !keep_open {
319 break;
320 }
321 }
322
323 Some(value)
324}
325
326#[cfg(test)]
327mod tests {
328 #![allow(clippy::panic)]
329 #![allow(clippy::unwrap_used)]
330
331 use std::ops::Deref;
332
333 use crate::{blocks::ContentModel, tests::prelude::*};
334
335 #[test]
336 fn impl_clone() {
337 let h1 =
339 crate::document::Attribute::parse(crate::Span::new(":foo: bar"), &Parser::default())
340 .unwrap();
341 let h2 = h1.clone();
342 assert_eq!(h1, h2);
343 }
344
345 #[test]
346 fn simple_value() {
347 let mi = crate::document::Attribute::parse(
348 crate::Span::new(":foo: bar\nblah"),
349 &Parser::default(),
350 )
351 .unwrap();
352
353 assert_eq!(
354 mi.item,
355 Attribute {
356 name: Span {
357 data: "foo",
358 line: 1,
359 col: 2,
360 offset: 1,
361 },
362 value_source: Some(Span {
363 data: "bar",
364 line: 1,
365 col: 7,
366 offset: 6,
367 }),
368 value: InterpretedValue::Value("bar"),
369 source: Span {
370 data: ":foo: bar",
371 line: 1,
372 col: 1,
373 offset: 0,
374 }
375 }
376 );
377
378 assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
379
380 assert_eq!(
381 mi.after,
382 Span {
383 data: "blah",
384 line: 2,
385 col: 1,
386 offset: 10
387 }
388 );
389 }
390
391 #[test]
392 fn no_value() {
393 let mi =
394 crate::document::Attribute::parse(crate::Span::new(":foo:\nblah"), &Parser::default())
395 .unwrap();
396
397 assert_eq!(
398 mi.item,
399 Attribute {
400 name: Span {
401 data: "foo",
402 line: 1,
403 col: 2,
404 offset: 1,
405 },
406 value_source: None,
407 value: InterpretedValue::Set,
408 source: Span {
409 data: ":foo:",
410 line: 1,
411 col: 1,
412 offset: 0,
413 }
414 }
415 );
416
417 assert_eq!(mi.item.value(), InterpretedValue::Set);
418
419 assert_eq!(
420 mi.after,
421 Span {
422 data: "blah",
423 line: 2,
424 col: 1,
425 offset: 6
426 }
427 );
428 }
429
430 #[test]
431 fn name_with_hyphens() {
432 let mi = crate::document::Attribute::parse(
433 crate::Span::new(":name-with-hyphen:"),
434 &Parser::default(),
435 )
436 .unwrap();
437
438 assert_eq!(
439 mi.item,
440 Attribute {
441 name: Span {
442 data: "name-with-hyphen",
443 line: 1,
444 col: 2,
445 offset: 1,
446 },
447 value_source: None,
448 value: InterpretedValue::Set,
449 source: Span {
450 data: ":name-with-hyphen:",
451 line: 1,
452 col: 1,
453 offset: 0,
454 }
455 }
456 );
457
458 assert_eq!(mi.item.value(), InterpretedValue::Set);
459
460 assert_eq!(
461 mi.after,
462 Span {
463 data: "",
464 line: 1,
465 col: 19,
466 offset: 18
467 }
468 );
469 }
470
471 #[test]
472 fn unset_prefix() {
473 let mi =
474 crate::document::Attribute::parse(crate::Span::new(":!foo:\nblah"), &Parser::default())
475 .unwrap();
476
477 assert_eq!(
478 mi.item,
479 Attribute {
480 name: Span {
481 data: "foo",
482 line: 1,
483 col: 3,
484 offset: 2,
485 },
486 value_source: None,
487 value: InterpretedValue::Unset,
488 source: Span {
489 data: ":!foo:",
490 line: 1,
491 col: 1,
492 offset: 0,
493 }
494 }
495 );
496
497 assert_eq!(mi.item.value(), InterpretedValue::Unset);
498
499 assert_eq!(
500 mi.after,
501 Span {
502 data: "blah",
503 line: 2,
504 col: 1,
505 offset: 7
506 }
507 );
508 }
509
510 #[test]
511 fn unset_postfix() {
512 let mi =
513 crate::document::Attribute::parse(crate::Span::new(":foo!:\nblah"), &Parser::default())
514 .unwrap();
515
516 assert_eq!(
517 mi.item,
518 Attribute {
519 name: Span {
520 data: "foo",
521 line: 1,
522 col: 2,
523 offset: 1,
524 },
525 value_source: None,
526 value: InterpretedValue::Unset,
527 source: Span {
528 data: ":foo!:",
529 line: 1,
530 col: 1,
531 offset: 0,
532 }
533 }
534 );
535
536 assert_eq!(mi.item.value(), InterpretedValue::Unset);
537
538 assert_eq!(
539 mi.after,
540 Span {
541 data: "blah",
542 line: 2,
543 col: 1,
544 offset: 7
545 }
546 );
547 }
548
549 #[test]
550 fn err_unset_prefix_and_postfix() {
551 assert!(
552 crate::document::Attribute::parse(
553 crate::Span::new(":!foo!:\nblah"),
554 &Parser::default()
555 )
556 .is_none()
557 );
558 }
559
560 #[test]
561 fn err_invalid_ident1() {
562 assert!(
563 crate::document::Attribute::parse(
564 crate::Span::new(":@invalid:\nblah"),
565 &Parser::default()
566 )
567 .is_none()
568 );
569 }
570
571 #[test]
572 fn err_missing_closing_colon() {
573 assert!(
576 crate::document::Attribute::parse(
577 crate::Span::new(":foo bar\nblah"),
578 &Parser::default()
579 )
580 .is_none()
581 );
582
583 assert!(
585 crate::document::Attribute::parse(crate::Span::new(":foo"), &Parser::default())
586 .is_none()
587 );
588 }
589
590 #[test]
591 fn name_captures_trailing_non_word_char() {
592 let mi = crate::document::Attribute::parse(
597 crate::Span::new(":invalid@:\nblah"),
598 &Parser::default(),
599 )
600 .unwrap();
601
602 assert_eq!(
603 mi.item,
604 Attribute {
605 name: Span {
606 data: "invalid@",
607 line: 1,
608 col: 2,
609 offset: 1,
610 },
611 value_source: None,
612 value: InterpretedValue::Set,
613 source: Span {
614 data: ":invalid@:",
615 line: 1,
616 col: 1,
617 offset: 0,
618 }
619 }
620 );
621
622 assert_eq!(
623 mi.after,
624 Span {
625 data: "blah",
626 line: 2,
627 col: 1,
628 offset: 11
629 }
630 );
631 }
632
633 #[test]
634 fn name_with_spaces() {
635 let mi = crate::document::Attribute::parse(
640 crate::Span::new(":Author Initials: SJR"),
641 &Parser::default(),
642 )
643 .unwrap();
644
645 assert_eq!(
646 mi.item,
647 Attribute {
648 name: Span {
649 data: "Author Initials",
650 line: 1,
651 col: 2,
652 offset: 1,
653 },
654 value_source: Some(Span {
655 data: "SJR",
656 line: 1,
657 col: 19,
658 offset: 18,
659 }),
660 value: InterpretedValue::Value("SJR"),
661 source: Span {
662 data: ":Author Initials: SJR",
663 line: 1,
664 col: 1,
665 offset: 0,
666 }
667 }
668 );
669 }
670
671 #[test]
672 fn err_invalid_ident3() {
673 assert!(
674 crate::document::Attribute::parse(
675 crate::Span::new(":-invalid:\nblah"),
676 &Parser::default()
677 )
678 .is_none()
679 );
680 }
681
682 #[test]
683 fn value_with_soft_wrap() {
684 let mi = crate::document::Attribute::parse(
685 crate::Span::new(":foo: bar \\\n blah"),
686 &Parser::default(),
687 )
688 .unwrap();
689
690 assert_eq!(
691 mi.item,
692 Attribute {
693 name: Span {
694 data: "foo",
695 line: 1,
696 col: 2,
697 offset: 1,
698 },
699 value_source: Some(Span {
700 data: "bar \\\n blah",
701 line: 1,
702 col: 7,
703 offset: 6,
704 }),
705 value: InterpretedValue::Value("bar blah"),
706 source: Span {
707 data: ":foo: bar \\\n blah",
708 line: 1,
709 col: 1,
710 offset: 0,
711 }
712 }
713 );
714
715 assert_eq!(mi.item.value(), InterpretedValue::Value("bar blah"));
716
717 assert_eq!(
718 mi.after,
719 Span {
720 data: "",
721 line: 2,
722 col: 6,
723 offset: 17
724 }
725 );
726 }
727
728 #[test]
729 fn bare_trailing_backslash_is_literal() {
730 let mi = crate::document::Attribute::parse(
734 crate::Span::new(":longpath: very/long/path/to/some/\\\nsubdirectory"),
735 &Parser::default(),
736 )
737 .unwrap();
738
739 assert_eq!(
740 mi.item,
741 Attribute {
742 name: Span {
743 data: "longpath",
744 line: 1,
745 col: 2,
746 offset: 1,
747 },
748 value_source: Some(Span {
749 data: "very/long/path/to/some/\\",
750 line: 1,
751 col: 12,
752 offset: 11,
753 }),
754 value: InterpretedValue::Value("very/long/path/to/some/\\"),
755 source: Span {
756 data: ":longpath: very/long/path/to/some/\\",
757 line: 1,
758 col: 1,
759 offset: 0,
760 }
761 }
762 );
763
764 assert_eq!(
765 mi.item.value(),
766 InterpretedValue::Value("very/long/path/to/some/\\")
767 );
768
769 assert_eq!(
771 mi.after,
772 Span {
773 data: "subdirectory",
774 line: 2,
775 col: 1,
776 offset: 36
777 }
778 );
779 }
780
781 #[test]
782 fn literal_trailing_backslash_on_final_line() {
783 let mi = crate::document::Attribute::parse(
786 crate::Span::new(":foo: bar \\\nbaz\\"),
787 &Parser::default(),
788 )
789 .unwrap();
790
791 assert_eq!(mi.item.value(), InterpretedValue::Value("bar baz\\"));
792 }
793
794 #[test]
795 fn value_with_hard_wrap() {
796 let mi = crate::document::Attribute::parse(
797 crate::Span::new(":foo: bar + \\\n blah"),
798 &Parser::default(),
799 )
800 .unwrap();
801
802 assert_eq!(
803 mi.item,
804 Attribute {
805 name: Span {
806 data: "foo",
807 line: 1,
808 col: 2,
809 offset: 1,
810 },
811 value_source: Some(Span {
812 data: "bar + \\\n blah",
813 line: 1,
814 col: 7,
815 offset: 6,
816 }),
817 value: InterpretedValue::Value("bar +\nblah"),
818 source: Span {
819 data: ":foo: bar + \\\n blah",
820 line: 1,
821 col: 1,
822 offset: 0,
823 }
824 }
825 );
826
827 assert_eq!(mi.item.value(), InterpretedValue::Value("bar +\nblah"));
828
829 assert_eq!(
830 mi.after,
831 Span {
832 data: "",
833 line: 2,
834 col: 6,
835 offset: 19
836 }
837 );
838 }
839
840 #[test]
841 fn single_line_trailing_hard_break_marker_is_stripped() {
842 let mi =
849 crate::document::Attribute::parse(crate::Span::new(":foo: bar +"), &Parser::default())
850 .unwrap();
851
852 assert_eq!(
853 mi.item,
854 Attribute {
855 name: Span {
856 data: "foo",
857 line: 1,
858 col: 2,
859 offset: 1,
860 },
861 value_source: Some(Span {
862 data: "bar +",
863 line: 1,
864 col: 7,
865 offset: 6,
866 }),
867 value: InterpretedValue::Value("bar"),
868 source: Span {
869 data: ":foo: bar +",
870 line: 1,
871 col: 1,
872 offset: 0,
873 }
874 }
875 );
876
877 assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
878
879 assert_eq!(
880 mi.after,
881 Span {
882 data: "",
883 line: 1,
884 col: 12,
885 offset: 11
886 }
887 );
888 }
889
890 #[test]
891 fn legacy_multi_line_value_is_fused() {
892 let mi = crate::document::Attribute::parse(
897 crate::Span::new(":foo: bar +\nblah"),
898 &Parser::default(),
899 )
900 .unwrap();
901
902 assert_eq!(
903 mi.item,
904 Attribute {
905 name: Span {
906 data: "foo",
907 line: 1,
908 col: 2,
909 offset: 1,
910 },
911 value_source: Some(Span {
912 data: "bar +\nblah",
913 line: 1,
914 col: 7,
915 offset: 6,
916 }),
917 value: InterpretedValue::Value("bar blah"),
918 source: Span {
919 data: ":foo: bar +\nblah",
920 line: 1,
921 col: 1,
922 offset: 0,
923 }
924 }
925 );
926
927 assert_eq!(mi.item.value(), InterpretedValue::Value("bar blah"));
928
929 assert_eq!(
930 mi.after,
931 Span {
932 data: "",
933 line: 2,
934 col: 5,
935 offset: 16
936 }
937 );
938 }
939
940 #[test]
941 fn legacy_hard_break_marker_edge_cases() {
942 let value = |src| {
948 crate::document::Attribute::parse(crate::Span::new(src), &Parser::default())
949 .unwrap()
950 .item
951 .value()
952 .clone()
953 };
954
955 assert_eq!(value(":foo: bar +\nx"), InterpretedValue::Value("bar x"));
958
959 assert_eq!(value(":foo: bar\t +\nx"), InterpretedValue::Value("bar x"));
961
962 assert_eq!(value(":foo: bar ++\nx"), InterpretedValue::Value("bar ++"));
964
965 assert_eq!(value(":foo: bar+\nx"), InterpretedValue::Value("bar+"));
967
968 assert_eq!(value(":foo: bar\t+\nx"), InterpretedValue::Value("bar\t+"));
970
971 assert_eq!(value(":foo: +\nx"), InterpretedValue::Value("+"));
973
974 assert_eq!(
978 value(":foo: bar + +\nx"),
979 InterpretedValue::Value("bar +\nx")
980 );
981
982 assert_eq!(
985 value(":foo: bar\u{00a0} +\nx"),
986 InterpretedValue::Value("bar\u{00a0} x")
987 );
988 }
989
990 #[test]
991 fn bare_marker_only_continuation_line_does_not_swallow_next_line() {
992 let mi = crate::document::Attribute::parse(
997 crate::Span::new(":foo: text +\n +\nmore"),
998 &Parser::default(),
999 )
1000 .unwrap();
1001
1002 assert_eq!(
1003 mi.item,
1004 Attribute {
1005 name: Span {
1006 data: "foo",
1007 line: 1,
1008 col: 2,
1009 offset: 1,
1010 },
1011 value_source: Some(Span {
1012 data: "text +\n +",
1013 line: 1,
1014 col: 7,
1015 offset: 6,
1016 }),
1017 value: InterpretedValue::Value("text +"),
1018 source: Span {
1019 data: ":foo: text +\n +",
1020 line: 1,
1021 col: 1,
1022 offset: 0,
1023 }
1024 }
1025 );
1026
1027 assert_eq!(
1029 mi.after,
1030 Span {
1031 data: "more",
1032 line: 3,
1033 col: 1,
1034 offset: 16,
1035 }
1036 );
1037 }
1038
1039 #[test]
1040 fn is_block() {
1041 let mut parser = Parser::default();
1042 let maw = crate::blocks::Block::parse(crate::Span::new(":foo: bar\nblah"), &mut parser);
1043
1044 let mi = maw.item.unwrap();
1045 let block = mi.item;
1046
1047 assert_eq!(
1048 block,
1049 Block::DocumentAttribute(Attribute {
1050 name: Span {
1051 data: "foo",
1052 line: 1,
1053 col: 2,
1054 offset: 1,
1055 },
1056 value_source: Some(Span {
1057 data: "bar",
1058 line: 1,
1059 col: 7,
1060 offset: 6,
1061 }),
1062 value: InterpretedValue::Value("bar"),
1063 source: Span {
1064 data: ":foo: bar",
1065 line: 1,
1066 col: 1,
1067 offset: 0,
1068 }
1069 })
1070 );
1071
1072 assert_eq!(block.content_model(), ContentModel::Empty);
1073 assert!(block.rendered_content().is_none());
1074 assert_eq!(block.raw_context().deref(), "attribute");
1075 assert!(block.child_blocks().next().is_none());
1076 assert!(block.title_source().is_none());
1077 assert!(block.title().is_none());
1078 assert!(block.anchor().is_none());
1079 assert!(block.anchor_reftext().is_none());
1080 assert!(block.attrlist().is_none());
1081 assert_eq!(block.substitution_group(), SubstitutionGroup::Normal);
1082
1083 assert_eq!(
1084 block.span(),
1085 Span {
1086 data: ":foo: bar",
1087 line: 1,
1088 col: 1,
1089 offset: 0,
1090 }
1091 );
1092
1093 let crate::blocks::Block::DocumentAttribute(attr) = block else {
1094 panic!("Wrong type");
1095 };
1096
1097 assert_eq!(attr.value(), InterpretedValue::Value("bar"));
1098
1099 assert_eq!(
1100 mi.after,
1101 Span {
1102 data: "blah",
1103 line: 2,
1104 col: 1,
1105 offset: 10
1106 }
1107 );
1108 }
1109
1110 #[test]
1111 fn affects_document_state() {
1112 let mut parser = Parser::default().with_intrinsic_attribute(
1113 "agreed",
1114 "yes",
1115 ModificationContext::Anywhere,
1116 );
1117
1118 let doc =
1119 parser.parse("We are agreed? {agreed}\n\n:agreed: no\n\nAre we still agreed? {agreed}");
1120
1121 let mut blocks = doc.child_blocks();
1122
1123 let block1 = blocks.next().unwrap();
1124
1125 assert_eq!(
1126 block1,
1127 &Block::Simple(SimpleBlock {
1128 content: Content {
1129 original: Span {
1130 data: "We are agreed? {agreed}",
1131 line: 1,
1132 col: 1,
1133 offset: 0,
1134 },
1135 rendered: "We are agreed? yes",
1136 },
1137 source: Span {
1138 data: "We are agreed? {agreed}",
1139 line: 1,
1140 col: 1,
1141 offset: 0,
1142 },
1143 style: SimpleBlockStyle::Paragraph,
1144 title_source: None,
1145 title: None,
1146 caption: None,
1147 number: None,
1148 anchor: None,
1149 anchor_reftext: None,
1150 attrlist: None,
1151 })
1152 );
1153
1154 let _ = blocks.next().unwrap();
1155
1156 let block3 = blocks.next().unwrap();
1157
1158 assert_eq!(
1159 block3,
1160 &Block::Simple(SimpleBlock {
1161 content: Content {
1162 original: Span {
1163 data: "Are we still agreed? {agreed}",
1164 line: 5,
1165 col: 1,
1166 offset: 38,
1167 },
1168 rendered: "Are we still agreed? no",
1169 },
1170 source: Span {
1171 data: "Are we still agreed? {agreed}",
1172 line: 5,
1173 col: 1,
1174 offset: 38,
1175 },
1176 style: SimpleBlockStyle::Paragraph,
1177 title_source: None,
1178 title: None,
1179 caption: None,
1180 number: None,
1181 anchor: None,
1182 anchor_reftext: None,
1183 attrlist: None,
1184 })
1185 );
1186
1187 let mut warnings = doc.warnings();
1188 assert!(warnings.next().is_none());
1189 }
1190
1191 #[test]
1192 fn block_enforces_permission() {
1193 let mut parser = Parser::default().with_intrinsic_attribute(
1194 "agreed",
1195 "yes",
1196 ModificationContext::ApiOnly,
1197 );
1198
1199 let doc = parser.parse("Hello\n\n:agreed: no\n\nAre we agreed? {agreed}");
1200
1201 let mut blocks = doc.child_blocks();
1202 let _ = blocks.next().unwrap();
1203 let _ = blocks.next().unwrap();
1204 let block3 = blocks.next().unwrap();
1205
1206 assert_eq!(
1207 block3,
1208 &Block::Simple(SimpleBlock {
1209 content: Content {
1210 original: Span {
1211 data: "Are we agreed? {agreed}",
1212 line: 5,
1213 col: 1,
1214 offset: 20,
1215 },
1216 rendered: "Are we agreed? yes",
1217 },
1218 source: Span {
1219 data: "Are we agreed? {agreed}",
1220 line: 5,
1221 col: 1,
1222 offset: 20,
1223 },
1224 style: SimpleBlockStyle::Paragraph,
1225 title_source: None,
1226 title: None,
1227 caption: None,
1228 number: None,
1229 anchor: None,
1230 anchor_reftext: None,
1231 attrlist: None,
1232 })
1233 );
1234
1235 let mut warnings = doc.warnings();
1236 let warning1 = warnings.next().unwrap();
1237
1238 assert_eq!(
1239 &warning1.source,
1240 Span {
1241 data: ":agreed: no",
1242 line: 3,
1243 col: 1,
1244 offset: 7,
1245 }
1246 );
1247
1248 assert_eq!(
1249 warning1.warning,
1250 WarningType::AttributeValueIsLocked("agreed".to_owned(),)
1251 );
1252
1253 assert!(warnings.next().is_none());
1254 }
1255
1256 mod fold_continuation_value {
1257 use super::super::fold_continuation_value;
1258
1259 #[test]
1260 fn no_marker_returns_none() {
1261 assert_eq!(fold_continuation_value("bar"), None);
1264 assert_eq!(fold_continuation_value("bar+"), None);
1265 assert_eq!(fold_continuation_value("bar ++"), None);
1266 }
1267
1268 #[test]
1269 fn modern_soft_wrap_folds_with_space() {
1270 assert_eq!(
1271 fold_continuation_value("bar \\\nblah"),
1272 Some("bar blah".to_string())
1273 );
1274 }
1275
1276 #[test]
1277 fn legacy_marker_folds_with_space() {
1278 assert_eq!(
1279 fold_continuation_value("This is the first +\nRuby implementation of +\nAsciiDoc."),
1280 Some("This is the first Ruby implementation of AsciiDoc.".to_string())
1281 );
1282 }
1283
1284 #[test]
1285 fn hard_line_break_joins_with_newline() {
1286 assert_eq!(
1289 fold_continuation_value("bar + \\\nblah"),
1290 Some("bar +\nblah".to_string())
1291 );
1292 }
1293
1294 #[test]
1295 fn blank_line_terminates_value() {
1296 assert_eq!(fold_continuation_value("a +\n\nb"), Some("a".to_string()));
1299 }
1300
1301 #[test]
1302 fn crlf_line_endings_are_normalized() {
1303 assert_eq!(
1304 fold_continuation_value("bar +\r\nblah"),
1305 Some("bar blah".to_string())
1306 );
1307 }
1308
1309 #[test]
1310 fn bare_marker_only_line_terminates_value() {
1311 assert_eq!(
1317 fold_continuation_value("text +\n +\nmore"),
1318 Some("text +".to_string())
1319 );
1320 }
1321
1322 #[test]
1323 fn trailing_whitespace_after_marker_still_continues() {
1324 assert_eq!(
1328 fold_continuation_value("a +\nb + \nmore"),
1329 Some("a b more".to_string())
1330 );
1331 }
1332 }
1333
1334 mod interpreted_value {
1335 mod impl_debug {
1336 use crate::document::InterpretedValue;
1337
1338 #[test]
1339 fn value_empty_string() {
1340 let interpreted_value = InterpretedValue::Value("".to_string());
1341 let debug_output = format!("{:?}", interpreted_value);
1342 assert_eq!(debug_output, "InterpretedValue::Value(\"\")");
1343 }
1344
1345 #[test]
1346 fn value_simple_string() {
1347 let interpreted_value = InterpretedValue::Value("hello".to_string());
1348 let debug_output = format!("{:?}", interpreted_value);
1349 assert_eq!(debug_output, "InterpretedValue::Value(\"hello\")");
1350 }
1351
1352 #[test]
1353 fn value_string_with_spaces() {
1354 let interpreted_value = InterpretedValue::Value("hello world".to_string());
1355 let debug_output = format!("{:?}", interpreted_value);
1356 assert_eq!(debug_output, "InterpretedValue::Value(\"hello world\")");
1357 }
1358
1359 #[test]
1360 fn value_string_with_special_chars() {
1361 let interpreted_value = InterpretedValue::Value("test!@#$%^&*()".to_string());
1362 let debug_output = format!("{:?}", interpreted_value);
1363 assert_eq!(debug_output, "InterpretedValue::Value(\"test!@#$%^&*()\")");
1364 }
1365
1366 #[test]
1367 fn value_string_with_quotes() {
1368 let interpreted_value = InterpretedValue::Value("value\"with'quotes".to_string());
1369 let debug_output = format!("{:?}", interpreted_value);
1370 assert_eq!(
1371 debug_output,
1372 "InterpretedValue::Value(\"value\\\"with'quotes\")"
1373 );
1374 }
1375
1376 #[test]
1377 fn value_string_with_newlines() {
1378 let interpreted_value = InterpretedValue::Value("line1\nline2\nline3".to_string());
1379 let debug_output = format!("{:?}", interpreted_value);
1380 assert_eq!(
1381 debug_output,
1382 "InterpretedValue::Value(\"line1\\nline2\\nline3\")"
1383 );
1384 }
1385
1386 #[test]
1387 fn value_string_with_backslashes() {
1388 let interpreted_value = InterpretedValue::Value("path\\to\\file".to_string());
1389 let debug_output = format!("{:?}", interpreted_value);
1390 assert_eq!(
1391 debug_output,
1392 "InterpretedValue::Value(\"path\\\\to\\\\file\")"
1393 );
1394 }
1395
1396 #[test]
1397 fn value_string_with_unicode() {
1398 let interpreted_value = InterpretedValue::Value("café 🚀 ñoño".to_string());
1399 let debug_output = format!("{:?}", interpreted_value);
1400 assert_eq!(debug_output, "InterpretedValue::Value(\"café 🚀 ñoño\")");
1401 }
1402
1403 #[test]
1404 fn set() {
1405 let interpreted_value = InterpretedValue::Set;
1406 let debug_output = format!("{:?}", interpreted_value);
1407 assert_eq!(debug_output, "InterpretedValue::Set");
1408 }
1409
1410 #[test]
1411 fn unset() {
1412 let interpreted_value = InterpretedValue::Unset;
1413 let debug_output = format!("{:?}", interpreted_value);
1414 assert_eq!(debug_output, "InterpretedValue::Unset");
1415 }
1416 }
1417 }
1418}