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
192 .trim_start_matches('\r')
193 .trim_end_matches(' ')
194 .trim_end_matches('\\')
195 .trim_end_matches(' ');
196
197 if line.ends_with('+') {
198 format!("{}\n", line.trim_end_matches('+').trim_end_matches(' '))
199 } else if count < last_count {
200 format!("{line} ")
201 } else {
202 line.to_string()
203 }
204 })
205 .collect();
206
207 content.rendered = CowStr::Boxed(value.join("").into_boxed_str());
208 }
209
210 SubstitutionGroup::Header.apply(&mut content, parser, None);
211
212 InterpretedValue::Value(content.rendered.into_string())
213 }
214
215 pub(crate) fn as_maybe_str(&self) -> Option<&str> {
216 match self {
217 InterpretedValue::Value(value) => Some(value.as_ref()),
218 _ => None,
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 #![allow(clippy::panic)]
226 #![allow(clippy::unwrap_used)]
227
228 use std::ops::Deref;
229
230 use pretty_assertions_sorted::assert_eq;
231
232 use crate::{
233 HasSpan, Parser,
234 blocks::{ContentModel, IsBlock},
235 content::SubstitutionGroup,
236 parser::ModificationContext,
237 tests::prelude::*,
238 warnings::WarningType,
239 };
240
241 #[test]
242 fn impl_clone() {
243 let h1 =
245 crate::document::Attribute::parse(crate::Span::new(":foo: bar"), &Parser::default())
246 .unwrap();
247 let h2 = h1.clone();
248 assert_eq!(h1, h2);
249 }
250
251 #[test]
252 fn simple_value() {
253 let mi = crate::document::Attribute::parse(
254 crate::Span::new(":foo: bar\nblah"),
255 &Parser::default(),
256 )
257 .unwrap();
258
259 assert_eq!(
260 mi.item,
261 Attribute {
262 name: Span {
263 data: "foo",
264 line: 1,
265 col: 2,
266 offset: 1,
267 },
268 value_source: Some(Span {
269 data: "bar",
270 line: 1,
271 col: 7,
272 offset: 6,
273 }),
274 value: InterpretedValue::Value("bar"),
275 source: Span {
276 data: ":foo: bar",
277 line: 1,
278 col: 1,
279 offset: 0,
280 }
281 }
282 );
283
284 assert_eq!(mi.item.value(), InterpretedValue::Value("bar"));
285
286 assert_eq!(
287 mi.after,
288 Span {
289 data: "blah",
290 line: 2,
291 col: 1,
292 offset: 10
293 }
294 );
295 }
296
297 #[test]
298 fn no_value() {
299 let mi =
300 crate::document::Attribute::parse(crate::Span::new(":foo:\nblah"), &Parser::default())
301 .unwrap();
302
303 assert_eq!(
304 mi.item,
305 Attribute {
306 name: Span {
307 data: "foo",
308 line: 1,
309 col: 2,
310 offset: 1,
311 },
312 value_source: None,
313 value: InterpretedValue::Set,
314 source: Span {
315 data: ":foo:",
316 line: 1,
317 col: 1,
318 offset: 0,
319 }
320 }
321 );
322
323 assert_eq!(mi.item.value(), InterpretedValue::Set);
324
325 assert_eq!(
326 mi.after,
327 Span {
328 data: "blah",
329 line: 2,
330 col: 1,
331 offset: 6
332 }
333 );
334 }
335
336 #[test]
337 fn name_with_hyphens() {
338 let mi = crate::document::Attribute::parse(
339 crate::Span::new(":name-with-hyphen:"),
340 &Parser::default(),
341 )
342 .unwrap();
343
344 assert_eq!(
345 mi.item,
346 Attribute {
347 name: Span {
348 data: "name-with-hyphen",
349 line: 1,
350 col: 2,
351 offset: 1,
352 },
353 value_source: None,
354 value: InterpretedValue::Set,
355 source: Span {
356 data: ":name-with-hyphen:",
357 line: 1,
358 col: 1,
359 offset: 0,
360 }
361 }
362 );
363
364 assert_eq!(mi.item.value(), InterpretedValue::Set);
365
366 assert_eq!(
367 mi.after,
368 Span {
369 data: "",
370 line: 1,
371 col: 19,
372 offset: 18
373 }
374 );
375 }
376
377 #[test]
378 fn unset_prefix() {
379 let mi =
380 crate::document::Attribute::parse(crate::Span::new(":!foo:\nblah"), &Parser::default())
381 .unwrap();
382
383 assert_eq!(
384 mi.item,
385 Attribute {
386 name: Span {
387 data: "foo",
388 line: 1,
389 col: 3,
390 offset: 2,
391 },
392 value_source: None,
393 value: InterpretedValue::Unset,
394 source: Span {
395 data: ":!foo:",
396 line: 1,
397 col: 1,
398 offset: 0,
399 }
400 }
401 );
402
403 assert_eq!(mi.item.value(), InterpretedValue::Unset);
404
405 assert_eq!(
406 mi.after,
407 Span {
408 data: "blah",
409 line: 2,
410 col: 1,
411 offset: 7
412 }
413 );
414 }
415
416 #[test]
417 fn unset_postfix() {
418 let mi =
419 crate::document::Attribute::parse(crate::Span::new(":foo!:\nblah"), &Parser::default())
420 .unwrap();
421
422 assert_eq!(
423 mi.item,
424 Attribute {
425 name: Span {
426 data: "foo",
427 line: 1,
428 col: 2,
429 offset: 1,
430 },
431 value_source: None,
432 value: InterpretedValue::Unset,
433 source: Span {
434 data: ":foo!:",
435 line: 1,
436 col: 1,
437 offset: 0,
438 }
439 }
440 );
441
442 assert_eq!(mi.item.value(), InterpretedValue::Unset);
443
444 assert_eq!(
445 mi.after,
446 Span {
447 data: "blah",
448 line: 2,
449 col: 1,
450 offset: 7
451 }
452 );
453 }
454
455 #[test]
456 fn err_unset_prefix_and_postfix() {
457 assert!(
458 crate::document::Attribute::parse(
459 crate::Span::new(":!foo!:\nblah"),
460 &Parser::default()
461 )
462 .is_none()
463 );
464 }
465
466 #[test]
467 fn err_invalid_ident1() {
468 assert!(
469 crate::document::Attribute::parse(
470 crate::Span::new(":@invalid:\nblah"),
471 &Parser::default()
472 )
473 .is_none()
474 );
475 }
476
477 #[test]
478 fn err_invalid_ident2() {
479 assert!(
480 crate::document::Attribute::parse(
481 crate::Span::new(":invalid@:\nblah"),
482 &Parser::default()
483 )
484 .is_none()
485 );
486 }
487
488 #[test]
489 fn err_invalid_ident3() {
490 assert!(
491 crate::document::Attribute::parse(
492 crate::Span::new(":-invalid:\nblah"),
493 &Parser::default()
494 )
495 .is_none()
496 );
497 }
498
499 #[test]
500 fn value_with_soft_wrap() {
501 let mi = crate::document::Attribute::parse(
502 crate::Span::new(":foo: bar \\\n blah"),
503 &Parser::default(),
504 )
505 .unwrap();
506
507 assert_eq!(
508 mi.item,
509 Attribute {
510 name: Span {
511 data: "foo",
512 line: 1,
513 col: 2,
514 offset: 1,
515 },
516 value_source: Some(Span {
517 data: "bar \\\n blah",
518 line: 1,
519 col: 7,
520 offset: 6,
521 }),
522 value: InterpretedValue::Value("bar blah"),
523 source: Span {
524 data: ":foo: bar \\\n blah",
525 line: 1,
526 col: 1,
527 offset: 0,
528 }
529 }
530 );
531
532 assert_eq!(mi.item.value(), InterpretedValue::Value("bar blah"));
533
534 assert_eq!(
535 mi.after,
536 Span {
537 data: "",
538 line: 2,
539 col: 6,
540 offset: 17
541 }
542 );
543 }
544
545 #[test]
546 fn value_with_hard_wrap() {
547 let mi = crate::document::Attribute::parse(
548 crate::Span::new(":foo: bar + \\\n blah"),
549 &Parser::default(),
550 )
551 .unwrap();
552
553 assert_eq!(
554 mi.item,
555 Attribute {
556 name: Span {
557 data: "foo",
558 line: 1,
559 col: 2,
560 offset: 1,
561 },
562 value_source: Some(Span {
563 data: "bar + \\\n blah",
564 line: 1,
565 col: 7,
566 offset: 6,
567 }),
568 value: InterpretedValue::Value("bar\nblah"),
569 source: Span {
570 data: ":foo: bar + \\\n blah",
571 line: 1,
572 col: 1,
573 offset: 0,
574 }
575 }
576 );
577
578 assert_eq!(mi.item.value(), InterpretedValue::Value("bar\nblah"));
579
580 assert_eq!(
581 mi.after,
582 Span {
583 data: "",
584 line: 2,
585 col: 6,
586 offset: 19
587 }
588 );
589 }
590
591 #[test]
592 fn is_block() {
593 let mut parser = Parser::default();
594 let maw = crate::blocks::Block::parse(crate::Span::new(":foo: bar\nblah"), &mut parser);
595
596 let mi = maw.item.unwrap();
597 let block = mi.item;
598
599 assert_eq!(
600 block,
601 Block::DocumentAttribute(Attribute {
602 name: Span {
603 data: "foo",
604 line: 1,
605 col: 2,
606 offset: 1,
607 },
608 value_source: Some(Span {
609 data: "bar",
610 line: 1,
611 col: 7,
612 offset: 6,
613 }),
614 value: InterpretedValue::Value("bar"),
615 source: Span {
616 data: ":foo: bar",
617 line: 1,
618 col: 1,
619 offset: 0,
620 }
621 })
622 );
623
624 assert_eq!(block.content_model(), ContentModel::Empty);
625 assert_eq!(block.raw_context().deref(), "attribute");
626 assert!(block.nested_blocks().next().is_none());
627 assert!(block.title_source().is_none());
628 assert!(block.title().is_none());
629 assert!(block.anchor().is_none());
630 assert!(block.anchor_reftext().is_none());
631 assert!(block.attrlist().is_none());
632 assert_eq!(block.substitution_group(), SubstitutionGroup::Normal);
633
634 assert_eq!(
635 block.span(),
636 Span {
637 data: ":foo: bar",
638 line: 1,
639 col: 1,
640 offset: 0,
641 }
642 );
643
644 let crate::blocks::Block::DocumentAttribute(attr) = block else {
645 panic!("Wrong type");
646 };
647
648 assert_eq!(attr.value(), InterpretedValue::Value("bar"));
649
650 assert_eq!(
651 mi.after,
652 Span {
653 data: "blah",
654 line: 2,
655 col: 1,
656 offset: 10
657 }
658 );
659 }
660
661 #[test]
662 fn affects_document_state() {
663 let mut parser = Parser::default().with_intrinsic_attribute(
664 "agreed",
665 "yes",
666 ModificationContext::Anywhere,
667 );
668
669 let doc =
670 parser.parse("We are agreed? {agreed}\n\n:agreed: no\n\nAre we still agreed? {agreed}");
671
672 let mut blocks = doc.nested_blocks();
673
674 let block1 = blocks.next().unwrap();
675
676 assert_eq!(
677 block1,
678 &Block::Simple(SimpleBlock {
679 content: Content {
680 original: Span {
681 data: "We are agreed? {agreed}",
682 line: 1,
683 col: 1,
684 offset: 0,
685 },
686 rendered: "We are agreed? yes",
687 },
688 source: Span {
689 data: "We are agreed? {agreed}",
690 line: 1,
691 col: 1,
692 offset: 0,
693 },
694 title_source: None,
695 title: None,
696 anchor: None,
697 anchor_reftext: None,
698 attrlist: None,
699 })
700 );
701
702 let _ = blocks.next().unwrap();
703
704 let block3 = blocks.next().unwrap();
705
706 assert_eq!(
707 block3,
708 &Block::Simple(SimpleBlock {
709 content: Content {
710 original: Span {
711 data: "Are we still agreed? {agreed}",
712 line: 5,
713 col: 1,
714 offset: 38,
715 },
716 rendered: "Are we still agreed? no",
717 },
718 source: Span {
719 data: "Are we still agreed? {agreed}",
720 line: 5,
721 col: 1,
722 offset: 38,
723 },
724 title_source: None,
725 title: None,
726 anchor: None,
727 anchor_reftext: None,
728 attrlist: None,
729 })
730 );
731
732 let mut warnings = doc.warnings();
733 assert!(warnings.next().is_none());
734 }
735
736 #[test]
737 fn block_enforces_permission() {
738 let mut parser = Parser::default().with_intrinsic_attribute(
739 "agreed",
740 "yes",
741 ModificationContext::ApiOnly,
742 );
743
744 let doc = parser.parse("Hello\n\n:agreed: no\n\nAre we agreed? {agreed}");
745
746 let mut blocks = doc.nested_blocks();
747 let _ = blocks.next().unwrap();
748 let _ = blocks.next().unwrap();
749 let block3 = blocks.next().unwrap();
750
751 assert_eq!(
752 block3,
753 &Block::Simple(SimpleBlock {
754 content: Content {
755 original: Span {
756 data: "Are we agreed? {agreed}",
757 line: 5,
758 col: 1,
759 offset: 20,
760 },
761 rendered: "Are we agreed? yes",
762 },
763 source: Span {
764 data: "Are we agreed? {agreed}",
765 line: 5,
766 col: 1,
767 offset: 20,
768 },
769 title_source: None,
770 title: None,
771 anchor: None,
772 anchor_reftext: None,
773 attrlist: None,
774 })
775 );
776
777 let mut warnings = doc.warnings();
778 let warning1 = warnings.next().unwrap();
779
780 assert_eq!(
781 &warning1.source,
782 Span {
783 data: ":agreed: no",
784 line: 3,
785 col: 1,
786 offset: 7,
787 }
788 );
789
790 assert_eq!(
791 warning1.warning,
792 WarningType::AttributeValueIsLocked("agreed".to_owned(),)
793 );
794
795 assert!(warnings.next().is_none());
796 }
797
798 mod interpreted_value {
799 mod impl_debug {
800 use pretty_assertions_sorted::assert_eq;
801
802 use crate::document::InterpretedValue;
803
804 #[test]
805 fn value_empty_string() {
806 let interpreted_value = InterpretedValue::Value("".to_string());
807 let debug_output = format!("{:?}", interpreted_value);
808 assert_eq!(debug_output, "InterpretedValue::Value(\"\")");
809 }
810
811 #[test]
812 fn value_simple_string() {
813 let interpreted_value = InterpretedValue::Value("hello".to_string());
814 let debug_output = format!("{:?}", interpreted_value);
815 assert_eq!(debug_output, "InterpretedValue::Value(\"hello\")");
816 }
817
818 #[test]
819 fn value_string_with_spaces() {
820 let interpreted_value = InterpretedValue::Value("hello world".to_string());
821 let debug_output = format!("{:?}", interpreted_value);
822 assert_eq!(debug_output, "InterpretedValue::Value(\"hello world\")");
823 }
824
825 #[test]
826 fn value_string_with_special_chars() {
827 let interpreted_value = InterpretedValue::Value("test!@#$%^&*()".to_string());
828 let debug_output = format!("{:?}", interpreted_value);
829 assert_eq!(debug_output, "InterpretedValue::Value(\"test!@#$%^&*()\")");
830 }
831
832 #[test]
833 fn value_string_with_quotes() {
834 let interpreted_value = InterpretedValue::Value("value\"with'quotes".to_string());
835 let debug_output = format!("{:?}", interpreted_value);
836 assert_eq!(
837 debug_output,
838 "InterpretedValue::Value(\"value\\\"with'quotes\")"
839 );
840 }
841
842 #[test]
843 fn value_string_with_newlines() {
844 let interpreted_value = InterpretedValue::Value("line1\nline2\nline3".to_string());
845 let debug_output = format!("{:?}", interpreted_value);
846 assert_eq!(
847 debug_output,
848 "InterpretedValue::Value(\"line1\\nline2\\nline3\")"
849 );
850 }
851
852 #[test]
853 fn value_string_with_backslashes() {
854 let interpreted_value = InterpretedValue::Value("path\\to\\file".to_string());
855 let debug_output = format!("{:?}", interpreted_value);
856 assert_eq!(
857 debug_output,
858 "InterpretedValue::Value(\"path\\\\to\\\\file\")"
859 );
860 }
861
862 #[test]
863 fn value_string_with_unicode() {
864 let interpreted_value = InterpretedValue::Value("café 🚀 ñoño".to_string());
865 let debug_output = format!("{:?}", interpreted_value);
866 assert_eq!(debug_output, "InterpretedValue::Value(\"café 🚀 ñoño\")");
867 }
868
869 #[test]
870 fn set() {
871 let interpreted_value = InterpretedValue::Set;
872 let debug_output = format!("{:?}", interpreted_value);
873 assert_eq!(debug_output, "InterpretedValue::Set");
874 }
875
876 #[test]
877 fn unset() {
878 let interpreted_value = InterpretedValue::Unset;
879 let debug_output = format!("{:?}", interpreted_value);
880 assert_eq!(debug_output, "InterpretedValue::Unset");
881 }
882 }
883 }
884}