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