1use alloc::{borrow::Cow, string::String, vec::Vec};
7use core::iter::Peekable;
8use thiserror::Error;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
34pub enum Language {
35 #[default]
40 Plaintext,
41 Implicit,
46 Other(Cow<'static, str>),
50}
51
52impl Language {
53 pub fn new(s: impl Into<Cow<'static, str>>) -> Self {
61 let s = s.into();
62 if s == "plaintext" || s.is_empty() {
63 Language::Plaintext
64 } else {
65 Language::Other(s)
66 }
67 }
68
69 pub fn as_str(&self) -> Option<&str> {
71 match self {
72 Language::Plaintext => Some("plaintext"),
73 Language::Implicit => None,
74 Language::Other(s) => Some(s.as_ref()),
75 }
76 }
77
78 pub fn is_plaintext(&self) -> bool {
80 matches!(self, Language::Plaintext)
81 }
82
83 pub fn is_implicit(&self) -> bool {
85 matches!(self, Language::Implicit)
86 }
87
88 pub fn is_compatible_with(&self, expected: &Language) -> bool {
96 match (self, expected) {
97 (_, Language::Implicit) => true, (Language::Implicit, _) => true, (a, b) => a == b, }
101 }
102
103 pub fn is_other(&self, arg: &str) -> bool {
104 match self {
105 Language::Other(s) => s == arg,
106 _ => false,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117pub enum SyntaxHint {
118 Str,
121 LitStr,
123 LitStr1,
125 LitStr2,
127 LitStr3,
129
130 Inline,
133 Inline1,
135 Delim1,
137 Delim2,
139 Delim3,
141
142 Block,
145 Block3,
147 Block4,
149 Block5,
151 Block6,
153}
154
155impl SyntaxHint {
156 pub fn is_string(&self) -> bool {
158 matches!(
159 self,
160 SyntaxHint::Str
161 | SyntaxHint::LitStr
162 | SyntaxHint::LitStr1
163 | SyntaxHint::LitStr2
164 | SyntaxHint::LitStr3
165 )
166 }
167
168 pub fn is_escaped_string(&self) -> bool {
170 matches!(self, SyntaxHint::Str)
171 }
172
173 pub fn is_literal_string(&self) -> bool {
175 matches!(
176 self,
177 SyntaxHint::LitStr | SyntaxHint::LitStr1 | SyntaxHint::LitStr2 | SyntaxHint::LitStr3
178 )
179 }
180
181 pub fn is_inline(&self) -> bool {
183 matches!(
184 self,
185 SyntaxHint::Inline
186 | SyntaxHint::Inline1
187 | SyntaxHint::Delim1
188 | SyntaxHint::Delim2
189 | SyntaxHint::Delim3
190 )
191 }
192
193 pub fn is_block(&self) -> bool {
195 matches!(
196 self,
197 SyntaxHint::Block
198 | SyntaxHint::Block3
199 | SyntaxHint::Block4
200 | SyntaxHint::Block5
201 | SyntaxHint::Block6
202 )
203 }
204}
205
206#[derive(Debug, Clone)]
237pub struct Text {
238 pub content: String,
240 pub language: Language,
242 pub syntax_hint: Option<SyntaxHint>,
245}
246
247impl PartialEq for Text {
248 fn eq(&self, other: &Self) -> bool {
249 self.content == other.content && self.language == other.language
251 }
252}
253
254impl Text {
255 pub fn new(content: impl Into<String>, language: Language) -> Self {
257 Self {
258 content: content.into(),
259 language,
260 syntax_hint: None,
261 }
262 }
263
264 pub fn with_syntax_hint(
268 content: impl Into<String>,
269 language: Language,
270 syntax_hint: SyntaxHint,
271 ) -> Self {
272 let mut content = content.into();
273 if syntax_hint.is_block() && !content.ends_with('\n') {
274 content.push('\n');
275 }
276 Self {
277 content,
278 language,
279 syntax_hint: Some(syntax_hint),
280 }
281 }
282
283 pub fn plaintext(content: impl Into<String>) -> Self {
285 Self {
286 content: content.into(),
287 language: Language::Plaintext,
288 syntax_hint: Some(SyntaxHint::Str),
289 }
290 }
291
292 pub fn inline_implicit(content: impl Into<String>) -> Self {
294 Self {
295 content: content.into(),
296 language: Language::Implicit,
297 syntax_hint: Some(SyntaxHint::Inline1),
298 }
299 }
300
301 pub fn inline(content: impl Into<String>, language: impl Into<Cow<'static, str>>) -> Self {
303 Self {
304 content: content.into(),
305 language: Language::new(language),
306 syntax_hint: Some(SyntaxHint::Inline1),
307 }
308 }
309
310 pub fn block_implicit(content: impl Into<String>) -> Self {
312 let mut content = content.into();
313 if !content.ends_with('\n') {
314 content.push('\n');
315 }
316 Self {
317 content,
318 language: Language::Implicit,
319 syntax_hint: Some(SyntaxHint::Block3),
320 }
321 }
322
323 pub fn block(content: impl Into<String>, language: impl Into<Cow<'static, str>>) -> Self {
325 let mut content = content.into();
326 if !content.ends_with('\n') {
327 content.push('\n');
328 }
329 Self {
330 content,
331 language: Language::new(language),
332 syntax_hint: Some(SyntaxHint::Block3),
333 }
334 }
335
336 pub fn block_without_trailing_newline(
338 content: impl Into<String>,
339 language: impl Into<Cow<'static, str>>,
340 ) -> Self {
341 Self {
342 content: content.into(),
343 language: Language::new(language),
344 syntax_hint: Some(SyntaxHint::Block3),
345 }
346 }
347
348 pub fn as_str(&self) -> &str {
350 &self.content
351 }
352}
353
354#[derive(Debug, PartialEq, Eq, Clone, Error)]
356pub enum TextParseError {
357 #[error("Invalid escape sequence: {0}")]
359 InvalidEscapeSequence(char),
360 #[error("Invalid end of string after escape")]
362 InvalidEndOfStringAfterEscape,
363 #[error("Invalid unicode code point: {0}")]
365 InvalidUnicodeCodePoint(u32),
366 #[error("Newline in text binding")]
368 NewlineInTextBinding,
369 #[error(
371 "Invalid indent on code block at line {line}: actual {actual_indent} to be indented more than {expected_indent}"
372 )]
373 IndentError {
374 line: usize,
375 actual_indent: usize,
376 expected_indent: usize,
377 },
378}
379
380impl Text {
381 pub fn parse_quoted_string(s: &str) -> Result<Self, TextParseError> {
385 let content = parse_escape_sequences(s)?;
386 Ok(Text::plaintext(content))
387 }
388
389 pub fn parse_text_binding(s: &str) -> Result<Self, TextParseError> {
393 let stripped = s.strip_suffix('\n').unwrap_or(s);
394 let stripped = stripped.strip_suffix('\r').unwrap_or(stripped);
395 if stripped.contains(['\r', '\n']) {
396 return Err(TextParseError::NewlineInTextBinding);
397 }
398 let content = String::from(stripped.trim());
399 Ok(Text::plaintext(content))
400 }
401
402 pub fn parse_indented_block(
408 language: Language,
409 content: String,
410 syntax_hint: SyntaxHint,
411 ) -> Result<Self, TextParseError> {
412 let base_indent = if let Some(last_newline_pos) = content.rfind('\n') {
414 let trailing = &content[last_newline_pos + 1..];
415 if trailing.chars().all(|c| c == ' ') {
416 trailing.len()
417 } else {
418 0
419 }
420 } else {
421 0
422 };
423
424 let lines: Vec<&str> = content.lines().collect();
426 let line_count = if base_indent > 0 && !content.ends_with('\n') && lines.len() > 1 {
427 lines.len() - 1
428 } else {
429 lines.len()
430 };
431
432 let expected_whitespace_removals = base_indent * line_count;
433 let mut result = String::with_capacity(content.len() - expected_whitespace_removals);
434
435 for (line_number, line) in lines.iter().take(line_count).enumerate() {
436 if line.trim_start().is_empty() {
438 result.push('\n');
439 continue;
440 }
441
442 let actual_indent = line
443 .chars()
444 .take_while(|c| *c == ' ')
445 .take(base_indent)
446 .count();
447 if actual_indent < base_indent {
448 return Err(TextParseError::IndentError {
449 line: line_number + 1,
450 actual_indent,
451 expected_indent: base_indent,
452 });
453 }
454 result.push_str(&line[base_indent..]);
456 result.push('\n');
457 }
458
459 Ok(Self {
460 content: result,
461 language,
462 syntax_hint: Some(syntax_hint),
463 })
464 }
465}
466
467fn parse_escape_sequences(s: &str) -> Result<String, TextParseError> {
469 let mut result = String::with_capacity(s.len());
470 let mut chars = s.chars().peekable();
471
472 fn parse_unicode_escape(
473 chars: &mut Peekable<impl Iterator<Item = char>>,
474 ) -> Result<char, TextParseError> {
475 match chars.next() {
476 Some('{') => {}
477 Some(ch) => return Err(TextParseError::InvalidEscapeSequence(ch)),
478 None => return Err(TextParseError::InvalidEndOfStringAfterEscape),
479 }
480
481 let mut count = 0;
482 let mut code_point = 0;
483 while let Some(ch) = chars.peek()
484 && count < 6
485 {
487 if let Some(digit) = match ch {
488 '0'..='9' => Some(*ch as u32 - '0' as u32),
489 'a'..='f' => Some(*ch as u32 - 'a' as u32 + 10),
490 'A'..='F' => Some(*ch as u32 - 'A' as u32 + 10),
491 '_' | '-' => None,
492 _ => break,
493 } {
494 code_point = code_point * 16 + digit;
495 count += 1;
496 }
497 chars.next();
498 }
499
500 let Some(result) = core::char::from_u32(code_point) else {
501 return Err(TextParseError::InvalidUnicodeCodePoint(code_point));
502 };
503
504 match chars.next() {
505 Some('}') => {}
506 Some(ch) => return Err(TextParseError::InvalidEscapeSequence(ch)),
507 None => return Err(TextParseError::InvalidEndOfStringAfterEscape),
508 }
509
510 Ok(result)
511 }
512
513 while let Some(ch) = chars.next() {
514 match ch {
515 '\\' => match chars.next() {
516 Some('\\') => result.push('\\'),
517 Some('"') => result.push('"'),
518 Some('\'') => result.push('\''),
519 Some('n') => result.push('\n'),
520 Some('r') => result.push('\r'),
521 Some('t') => result.push('\t'),
522 Some('0') => result.push('\0'),
523 Some('u') => result.push(parse_unicode_escape(&mut chars)?),
524 Some(ch) => return Err(TextParseError::InvalidEscapeSequence(ch)),
525 None => return Err(TextParseError::InvalidEndOfStringAfterEscape),
526 },
527 _ => result.push(ch),
528 }
529 }
530
531 Ok(result)
532}
533
534pub use TextParseError as EureStringError;
536
537pub type EureString = Cow<'static, str>;
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_language_new_plaintext() {
548 assert_eq!(Language::new("plaintext"), Language::Plaintext);
549 assert_eq!(Language::new(""), Language::Plaintext);
550 }
551
552 #[test]
553 fn test_language_new_other() {
554 assert_eq!(Language::new("rust"), Language::Other("rust".into()));
555 assert_eq!(Language::new("sql"), Language::Other("sql".into()));
556 }
557
558 #[test]
559 fn test_language_as_str() {
560 assert_eq!(Language::Plaintext.as_str(), Some("plaintext"));
561 assert_eq!(Language::Implicit.as_str(), None);
562 assert_eq!(Language::Other("rust".into()).as_str(), Some("rust"));
563 }
564
565 #[test]
566 fn test_language_compatibility() {
567 assert!(Language::Implicit.is_compatible_with(&Language::Plaintext));
569 assert!(Language::Implicit.is_compatible_with(&Language::Other("rust".into())));
570
571 assert!(Language::Plaintext.is_compatible_with(&Language::Implicit));
573 assert!(Language::Other("rust".into()).is_compatible_with(&Language::Implicit));
574
575 assert!(Language::Plaintext.is_compatible_with(&Language::Plaintext));
577 assert!(Language::Other("rust".into()).is_compatible_with(&Language::Other("rust".into())));
578
579 assert!(!Language::Plaintext.is_compatible_with(&Language::Other("rust".into())));
581 assert!(!Language::Other("rust".into()).is_compatible_with(&Language::Plaintext));
582 assert!(!Language::Other("rust".into()).is_compatible_with(&Language::Other("sql".into())));
583 }
584
585 #[test]
586 fn test_text_plaintext() {
587 let text = Text::plaintext("hello");
588 assert_eq!(text.content, "hello");
589 assert_eq!(text.language, Language::Plaintext);
590 assert_eq!(text.syntax_hint, Some(SyntaxHint::Str));
591 }
592
593 #[test]
594 fn test_text_inline_implicit() {
595 let text = Text::inline_implicit("let a = 1");
596 assert_eq!(text.content, "let a = 1");
597 assert_eq!(text.language, Language::Implicit);
598 assert_eq!(text.syntax_hint, Some(SyntaxHint::Inline1));
599 }
600
601 #[test]
602 fn test_text_inline_with_language() {
603 let text = Text::inline("SELECT *", "sql");
604 assert_eq!(text.content, "SELECT *");
605 assert_eq!(text.language, Language::Other("sql".into()));
606 assert_eq!(text.syntax_hint, Some(SyntaxHint::Inline1));
607 }
608
609 #[test]
610 fn test_text_block_implicit() {
611 let text = Text::block_implicit("fn main() {}");
612 assert_eq!(text.content, "fn main() {}\n");
613 assert_eq!(text.language, Language::Implicit);
614 assert_eq!(text.syntax_hint, Some(SyntaxHint::Block3));
615 }
616
617 #[test]
618 fn test_text_block_with_language() {
619 let text = Text::block("fn main() {}", "rust");
620 assert_eq!(text.content, "fn main() {}\n");
621 assert_eq!(text.language, Language::Other("rust".into()));
622 assert_eq!(text.syntax_hint, Some(SyntaxHint::Block3));
623 }
624
625 #[test]
626 fn test_parse_quoted_string() {
627 let text = Text::parse_quoted_string("hello\\nworld").unwrap();
628 assert_eq!(text.content, "hello\nworld");
629 assert_eq!(text.language, Language::Plaintext);
630 }
631
632 #[test]
633 fn test_parse_text_binding() {
634 let text = Text::parse_text_binding(" hello world \n").unwrap();
635 assert_eq!(text.content, "hello world");
636 assert_eq!(text.language, Language::Plaintext);
637 }
638
639 #[test]
640 fn test_parse_text_binding_raw_backslashes() {
641 let text = Text::parse_text_binding(" \\b\\w+\\b \n").unwrap();
643 assert_eq!(text.content, "\\b\\w+\\b");
644 assert_eq!(text.language, Language::Plaintext);
645 }
646
647 #[test]
648 fn test_parse_text_binding_literal_backslash_n() {
649 let text = Text::parse_text_binding(" line1\\nline2 \n").unwrap();
651 assert_eq!(text.content, "line1\\nline2");
652 assert_eq!(text.language, Language::Plaintext);
653 }
654
655 #[test]
656 fn test_parse_text_binding_windows_path() {
657 let text = Text::parse_text_binding(" C:\\Users\\name\\file.txt \n").unwrap();
659 assert_eq!(text.content, "C:\\Users\\name\\file.txt");
660 }
661
662 #[test]
663 fn test_parse_text_binding_double_backslash() {
664 let text = Text::parse_text_binding(" \\\\ \n").unwrap();
666 assert_eq!(text.content, "\\\\");
667 }
668
669 #[test]
670 fn test_syntax_hint_is_string() {
671 assert!(SyntaxHint::Str.is_string());
673 assert!(SyntaxHint::LitStr.is_string());
675 assert!(SyntaxHint::LitStr1.is_string());
676 assert!(SyntaxHint::LitStr2.is_string());
677 assert!(SyntaxHint::LitStr3.is_string());
678 assert!(!SyntaxHint::Inline1.is_string());
680 assert!(!SyntaxHint::Block3.is_string());
681 }
682
683 #[test]
684 fn test_syntax_hint_is_escaped_string() {
685 assert!(SyntaxHint::Str.is_escaped_string());
686 assert!(!SyntaxHint::LitStr.is_escaped_string());
687 assert!(!SyntaxHint::Inline1.is_escaped_string());
688 }
689
690 #[test]
691 fn test_syntax_hint_is_literal_string() {
692 assert!(SyntaxHint::LitStr.is_literal_string());
693 assert!(SyntaxHint::LitStr1.is_literal_string());
694 assert!(SyntaxHint::LitStr2.is_literal_string());
695 assert!(SyntaxHint::LitStr3.is_literal_string());
696 assert!(!SyntaxHint::Str.is_literal_string());
697 assert!(!SyntaxHint::Inline1.is_literal_string());
698 }
699
700 #[test]
701 fn test_syntax_hint_is_inline() {
702 assert!(SyntaxHint::Inline.is_inline());
703 assert!(SyntaxHint::Inline1.is_inline());
704 assert!(SyntaxHint::Delim1.is_inline());
705 assert!(SyntaxHint::Delim2.is_inline());
706 assert!(SyntaxHint::Delim3.is_inline());
707 assert!(!SyntaxHint::Str.is_inline());
708 assert!(!SyntaxHint::Block3.is_inline());
709 }
710
711 #[test]
712 fn test_syntax_hint_is_block() {
713 assert!(SyntaxHint::Block.is_block());
714 assert!(SyntaxHint::Block3.is_block());
715 assert!(SyntaxHint::Block4.is_block());
716 assert!(SyntaxHint::Block5.is_block());
717 assert!(SyntaxHint::Block6.is_block());
718 assert!(!SyntaxHint::Str.is_block());
719 assert!(!SyntaxHint::Inline1.is_block());
720 }
721
722 mod parse_indented_block_tests {
723 use super::*;
724 use alloc::string::ToString;
725
726 #[test]
727 fn test_parse_indented_block_single_line() {
728 let content = " hello\n ".to_string();
730 let result = Text::parse_indented_block(
731 Language::Other("text".into()),
732 content,
733 SyntaxHint::Block3,
734 )
735 .unwrap();
736 assert_eq!(result.language, Language::Other("text".into()));
737 assert_eq!(result.content, "hello\n");
738 }
739
740 #[test]
741 fn test_parse_indented_block_multiple_lines() {
742 let content = " line1\n line2\n line3\n ".to_string();
744 let result = Text::parse_indented_block(
745 Language::Other("text".into()),
746 content,
747 SyntaxHint::Block3,
748 )
749 .unwrap();
750 assert_eq!(result.content, "line1\nline2\nline3\n");
751 }
752
753 #[test]
754 fn test_parse_indented_block_with_empty_lines() {
755 let content = " line1\n\n line2\n ".to_string();
757 let result = Text::parse_indented_block(
758 Language::Other("text".into()),
759 content,
760 SyntaxHint::Block3,
761 )
762 .unwrap();
763 assert_eq!(result.content, "line1\n\nline2\n");
764 }
765
766 #[test]
767 fn test_parse_indented_block_whitespace_only_line() {
768 let content = " line1\n \n line2\n ".to_string();
770 let result = Text::parse_indented_block(
771 Language::Other("text".into()),
772 content,
773 SyntaxHint::Block3,
774 )
775 .unwrap();
776 assert_eq!(result.content, " line1\n\n line2\n");
777 }
778
779 #[test]
780 fn test_parse_indented_block_empty_content() {
781 let content = " ".to_string();
783 let result = Text::parse_indented_block(
784 Language::Other("text".into()),
785 content,
786 SyntaxHint::Block3,
787 )
788 .unwrap();
789 assert_eq!(result.content, "\n");
791 }
792
793 #[test]
794 fn test_parse_indented_block_implicit_language() {
795 let content = " hello\n ".to_string();
796 let result =
797 Text::parse_indented_block(Language::Implicit, content, SyntaxHint::Block3)
798 .unwrap();
799 assert_eq!(result.language, Language::Implicit);
800 assert_eq!(result.content, "hello\n");
801 }
802
803 #[test]
804 fn test_parse_indented_block_insufficient_indent() {
805 let content = " line1\n line2\n ".to_string();
807 let result = Text::parse_indented_block(
808 Language::Other("text".into()),
809 content,
810 SyntaxHint::Block3,
811 );
812 assert_eq!(
813 result,
814 Err(TextParseError::IndentError {
815 line: 2,
816 actual_indent: 2,
817 expected_indent: 4,
818 })
819 );
820 }
821
822 #[test]
823 fn test_parse_indented_block_no_indent() {
824 let content = "line1\n line2\n ".to_string();
826 let result = Text::parse_indented_block(
827 Language::Other("text".into()),
828 content,
829 SyntaxHint::Block3,
830 );
831 assert_eq!(
832 result,
833 Err(TextParseError::IndentError {
834 line: 1,
835 actual_indent: 0,
836 expected_indent: 4,
837 })
838 );
839 }
840
841 #[test]
842 fn test_parse_indented_block_empty_string() {
843 let content = String::new();
844 let result = Text::parse_indented_block(
845 Language::Other("text".into()),
846 content,
847 SyntaxHint::Block3,
848 );
849 assert!(result.is_ok());
850 }
851
852 #[test]
853 fn test_parse_indented_block_zero_indent() {
854 let content = "line1\nline2\n".to_string();
856 let result = Text::parse_indented_block(
857 Language::Other("text".into()),
858 content,
859 SyntaxHint::Block3,
860 )
861 .unwrap();
862 assert_eq!(result.content, "line1\nline2\n");
863 }
864
865 #[test]
866 fn test_parse_indented_block_empty_line_only() {
867 let content = " \n ".to_string();
869 let result = Text::parse_indented_block(
870 Language::Other("text".into()),
871 content,
872 SyntaxHint::Block3,
873 )
874 .unwrap();
875 assert_eq!(result.content, "\n");
877 }
878
879 #[test]
880 fn test_parse_indented_block_whitespace_only_line_insufficient_indent() {
881 let content = " line1\n \n line2\n ".to_string();
883 let result = Text::parse_indented_block(
884 Language::Other("text".into()),
885 content,
886 SyntaxHint::Block3,
887 )
888 .unwrap();
889 assert_eq!(result.content, "line1\n\nline2\n");
891 }
892
893 #[test]
894 fn test_parse_indented_block_whitespace_only_line_no_indent() {
895 let content = " line1\n\n line2\n ".to_string();
897 let result = Text::parse_indented_block(
898 Language::Other("text".into()),
899 content,
900 SyntaxHint::Block3,
901 )
902 .unwrap();
903 assert_eq!(result.content, " line1\n\n line2\n");
905 }
906 }
907}