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(String),
50}
51
52impl Language {
53 pub fn new(s: impl Into<String>) -> 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),
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 Quoted,
120 Inline,
122 Inline1,
124 Inline2,
126 Block,
128 Block3,
130 Block4,
132 Block5,
134 Block6,
136}
137
138impl SyntaxHint {
139 pub fn is_quoted(&self) -> bool {
141 matches!(self, SyntaxHint::Quoted)
142 }
143
144 pub fn is_inline(&self) -> bool {
146 matches!(
147 self,
148 SyntaxHint::Inline | SyntaxHint::Inline1 | SyntaxHint::Inline2
149 )
150 }
151
152 pub fn is_block(&self) -> bool {
154 matches!(
155 self,
156 SyntaxHint::Block
157 | SyntaxHint::Block3
158 | SyntaxHint::Block4
159 | SyntaxHint::Block5
160 | SyntaxHint::Block6
161 )
162 }
163}
164
165#[derive(Debug, Clone, PartialEq)]
191pub struct Text {
192 pub content: String,
194 pub language: Language,
196 pub syntax_hint: Option<SyntaxHint>,
198}
199
200impl Text {
201 pub fn new(content: impl Into<String>, language: Language) -> Self {
203 Self {
204 content: content.into(),
205 language,
206 syntax_hint: None,
207 }
208 }
209
210 pub fn with_syntax_hint(
214 content: impl Into<String>,
215 language: Language,
216 syntax_hint: SyntaxHint,
217 ) -> Self {
218 let mut content = content.into();
219 if syntax_hint.is_block() && !content.ends_with('\n') {
220 content.push('\n');
221 }
222 Self {
223 content,
224 language,
225 syntax_hint: Some(syntax_hint),
226 }
227 }
228
229 pub fn plaintext(content: impl Into<String>) -> Self {
231 Self {
232 content: content.into(),
233 language: Language::Plaintext,
234 syntax_hint: Some(SyntaxHint::Quoted),
235 }
236 }
237
238 pub fn inline_implicit(content: impl Into<String>) -> Self {
240 Self {
241 content: content.into(),
242 language: Language::Implicit,
243 syntax_hint: Some(SyntaxHint::Inline1),
244 }
245 }
246
247 pub fn inline(content: impl Into<String>, language: impl Into<String>) -> Self {
249 Self {
250 content: content.into(),
251 language: Language::new(language),
252 syntax_hint: Some(SyntaxHint::Inline1),
253 }
254 }
255
256 pub fn block_implicit(content: impl Into<String>) -> Self {
258 let mut content = content.into();
259 if !content.ends_with('\n') {
260 content.push('\n');
261 }
262 Self {
263 content,
264 language: Language::Implicit,
265 syntax_hint: Some(SyntaxHint::Block3),
266 }
267 }
268
269 pub fn block(content: impl Into<String>, language: impl Into<String>) -> Self {
271 let mut content = content.into();
272 if !content.ends_with('\n') {
273 content.push('\n');
274 }
275 Self {
276 content,
277 language: Language::new(language),
278 syntax_hint: Some(SyntaxHint::Block3),
279 }
280 }
281
282 pub fn block_without_trailing_newline(
284 content: impl Into<String>,
285 language: impl Into<String>,
286 ) -> Self {
287 Self {
288 content: content.into(),
289 language: Language::new(language),
290 syntax_hint: Some(SyntaxHint::Block3),
291 }
292 }
293
294 pub fn as_str(&self) -> &str {
296 &self.content
297 }
298}
299
300#[derive(Debug, PartialEq, Eq, Clone, Error)]
302pub enum TextParseError {
303 #[error("Invalid escape sequence: {0}")]
305 InvalidEscapeSequence(char),
306 #[error("Invalid end of string after escape")]
308 InvalidEndOfStringAfterEscape,
309 #[error("Invalid unicode code point: {0}")]
311 InvalidUnicodeCodePoint(u32),
312 #[error("Newline in text binding")]
314 NewlineInTextBinding,
315 #[error(
317 "Invalid indent on code block at line {line}: actual {actual_indent} to be indented more than {expected_indent}"
318 )]
319 IndentError {
320 line: usize,
321 actual_indent: usize,
322 expected_indent: usize,
323 },
324}
325
326impl Text {
327 pub fn parse_quoted_string(s: &str) -> Result<Self, TextParseError> {
331 let content = parse_escape_sequences(s)?;
332 Ok(Text::plaintext(content))
333 }
334
335 pub fn parse_text_binding(s: &str) -> Result<Self, TextParseError> {
339 let stripped = s.strip_suffix('\n').unwrap_or(s);
340 let stripped = stripped.strip_suffix('\r').unwrap_or(stripped);
341 if stripped.contains(['\r', '\n']) {
342 return Err(TextParseError::NewlineInTextBinding);
343 }
344 let content = parse_escape_sequences(stripped.trim())?;
345 Ok(Text::plaintext(content))
346 }
347
348 pub fn parse_indented_block(
354 language: Language,
355 content: String,
356 syntax_hint: SyntaxHint,
357 ) -> Result<Self, TextParseError> {
358 let base_indent = if let Some(last_newline_pos) = content.rfind('\n') {
360 let trailing = &content[last_newline_pos + 1..];
361 if trailing.chars().all(|c| c == ' ') {
362 trailing.len()
363 } else {
364 0
365 }
366 } else {
367 0
368 };
369
370 let lines: Vec<&str> = content.lines().collect();
372 let line_count = if base_indent > 0 && !content.ends_with('\n') && lines.len() > 1 {
373 lines.len() - 1
374 } else {
375 lines.len()
376 };
377
378 let expected_whitespace_removals = base_indent * line_count;
379 let mut result = String::with_capacity(content.len() - expected_whitespace_removals);
380
381 for (line_number, line) in lines.iter().take(line_count).enumerate() {
382 if line.trim_start().is_empty() {
384 result.push('\n');
385 continue;
386 }
387
388 let actual_indent = line
389 .chars()
390 .take_while(|c| *c == ' ')
391 .take(base_indent)
392 .count();
393 if actual_indent < base_indent {
394 return Err(TextParseError::IndentError {
395 line: line_number + 1,
396 actual_indent,
397 expected_indent: base_indent,
398 });
399 }
400 result.push_str(&line[base_indent..]);
402 result.push('\n');
403 }
404
405 Ok(Self {
406 content: result,
407 language,
408 syntax_hint: Some(syntax_hint),
409 })
410 }
411}
412
413fn parse_escape_sequences(s: &str) -> Result<String, TextParseError> {
415 let mut result = String::with_capacity(s.len());
416 let mut chars = s.chars().peekable();
417
418 fn parse_unicode_escape(
419 chars: &mut Peekable<impl Iterator<Item = char>>,
420 ) -> Result<char, TextParseError> {
421 match chars.next() {
422 Some('{') => {}
423 Some(ch) => return Err(TextParseError::InvalidEscapeSequence(ch)),
424 None => return Err(TextParseError::InvalidEndOfStringAfterEscape),
425 }
426
427 let mut count = 0;
428 let mut code_point = 0;
429 while let Some(ch) = chars.peek()
430 && count < 6
431 {
433 if let Some(digit) = match ch {
434 '0'..='9' => Some(*ch as u32 - '0' as u32),
435 'a'..='f' => Some(*ch as u32 - 'a' as u32 + 10),
436 'A'..='F' => Some(*ch as u32 - 'A' as u32 + 10),
437 '_' | '-' => None,
438 _ => break,
439 } {
440 code_point = code_point * 16 + digit;
441 count += 1;
442 }
443 chars.next();
444 }
445
446 let Some(result) = core::char::from_u32(code_point) else {
447 return Err(TextParseError::InvalidUnicodeCodePoint(code_point));
448 };
449
450 match chars.next() {
451 Some('}') => {}
452 Some(ch) => return Err(TextParseError::InvalidEscapeSequence(ch)),
453 None => return Err(TextParseError::InvalidEndOfStringAfterEscape),
454 }
455
456 Ok(result)
457 }
458
459 while let Some(ch) = chars.next() {
460 match ch {
461 '\\' => match chars.next() {
462 Some('\\') => result.push('\\'),
463 Some('"') => result.push('"'),
464 Some('\'') => result.push('\''),
465 Some('n') => result.push('\n'),
466 Some('r') => result.push('\r'),
467 Some('t') => result.push('\t'),
468 Some('0') => result.push('\0'),
469 Some('u') => result.push(parse_unicode_escape(&mut chars)?),
470 Some(ch) => return Err(TextParseError::InvalidEscapeSequence(ch)),
471 None => return Err(TextParseError::InvalidEndOfStringAfterEscape),
472 },
473 _ => result.push(ch),
474 }
475 }
476
477 Ok(result)
478}
479
480pub use TextParseError as EureStringError;
482
483pub type EureString = Cow<'static, str>;
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn test_language_new_plaintext() {
494 assert_eq!(Language::new("plaintext"), Language::Plaintext);
495 assert_eq!(Language::new(""), Language::Plaintext);
496 }
497
498 #[test]
499 fn test_language_new_other() {
500 assert_eq!(Language::new("rust"), Language::Other("rust".into()));
501 assert_eq!(Language::new("sql"), Language::Other("sql".into()));
502 }
503
504 #[test]
505 fn test_language_as_str() {
506 assert_eq!(Language::Plaintext.as_str(), Some("plaintext"));
507 assert_eq!(Language::Implicit.as_str(), None);
508 assert_eq!(Language::Other("rust".into()).as_str(), Some("rust"));
509 }
510
511 #[test]
512 fn test_language_compatibility() {
513 assert!(Language::Implicit.is_compatible_with(&Language::Plaintext));
515 assert!(Language::Implicit.is_compatible_with(&Language::Other("rust".into())));
516
517 assert!(Language::Plaintext.is_compatible_with(&Language::Implicit));
519 assert!(Language::Other("rust".into()).is_compatible_with(&Language::Implicit));
520
521 assert!(Language::Plaintext.is_compatible_with(&Language::Plaintext));
523 assert!(Language::Other("rust".into()).is_compatible_with(&Language::Other("rust".into())));
524
525 assert!(!Language::Plaintext.is_compatible_with(&Language::Other("rust".into())));
527 assert!(!Language::Other("rust".into()).is_compatible_with(&Language::Plaintext));
528 assert!(!Language::Other("rust".into()).is_compatible_with(&Language::Other("sql".into())));
529 }
530
531 #[test]
532 fn test_text_plaintext() {
533 let text = Text::plaintext("hello");
534 assert_eq!(text.content, "hello");
535 assert_eq!(text.language, Language::Plaintext);
536 assert_eq!(text.syntax_hint, Some(SyntaxHint::Quoted));
537 }
538
539 #[test]
540 fn test_text_inline_implicit() {
541 let text = Text::inline_implicit("let a = 1");
542 assert_eq!(text.content, "let a = 1");
543 assert_eq!(text.language, Language::Implicit);
544 assert_eq!(text.syntax_hint, Some(SyntaxHint::Inline1));
545 }
546
547 #[test]
548 fn test_text_inline_with_language() {
549 let text = Text::inline("SELECT *", "sql");
550 assert_eq!(text.content, "SELECT *");
551 assert_eq!(text.language, Language::Other("sql".into()));
552 assert_eq!(text.syntax_hint, Some(SyntaxHint::Inline1));
553 }
554
555 #[test]
556 fn test_text_block_implicit() {
557 let text = Text::block_implicit("fn main() {}");
558 assert_eq!(text.content, "fn main() {}\n");
559 assert_eq!(text.language, Language::Implicit);
560 assert_eq!(text.syntax_hint, Some(SyntaxHint::Block3));
561 }
562
563 #[test]
564 fn test_text_block_with_language() {
565 let text = Text::block("fn main() {}", "rust");
566 assert_eq!(text.content, "fn main() {}\n");
567 assert_eq!(text.language, Language::Other("rust".into()));
568 assert_eq!(text.syntax_hint, Some(SyntaxHint::Block3));
569 }
570
571 #[test]
572 fn test_parse_quoted_string() {
573 let text = Text::parse_quoted_string("hello\\nworld").unwrap();
574 assert_eq!(text.content, "hello\nworld");
575 assert_eq!(text.language, Language::Plaintext);
576 }
577
578 #[test]
579 fn test_parse_text_binding() {
580 let text = Text::parse_text_binding(" hello world \n").unwrap();
581 assert_eq!(text.content, "hello world");
582 assert_eq!(text.language, Language::Plaintext);
583 }
584
585 #[test]
586 fn test_syntax_hint_is_quoted() {
587 assert!(SyntaxHint::Quoted.is_quoted());
588 assert!(!SyntaxHint::Inline1.is_quoted());
589 assert!(!SyntaxHint::Block3.is_quoted());
590 }
591
592 #[test]
593 fn test_syntax_hint_is_inline() {
594 assert!(SyntaxHint::Inline.is_inline());
595 assert!(SyntaxHint::Inline1.is_inline());
596 assert!(SyntaxHint::Inline2.is_inline());
597 assert!(!SyntaxHint::Quoted.is_inline());
598 assert!(!SyntaxHint::Block3.is_inline());
599 }
600
601 #[test]
602 fn test_syntax_hint_is_block() {
603 assert!(SyntaxHint::Block.is_block());
604 assert!(SyntaxHint::Block3.is_block());
605 assert!(SyntaxHint::Block4.is_block());
606 assert!(SyntaxHint::Block5.is_block());
607 assert!(SyntaxHint::Block6.is_block());
608 assert!(!SyntaxHint::Quoted.is_block());
609 assert!(!SyntaxHint::Inline1.is_block());
610 }
611
612 mod parse_indented_block_tests {
613 use super::*;
614 use alloc::string::ToString;
615
616 #[test]
617 fn test_parse_indented_block_single_line() {
618 let content = " hello\n ".to_string();
620 let result = Text::parse_indented_block(
621 Language::Other("text".into()),
622 content,
623 SyntaxHint::Block3,
624 )
625 .unwrap();
626 assert_eq!(result.language, Language::Other("text".into()));
627 assert_eq!(result.content, "hello\n");
628 }
629
630 #[test]
631 fn test_parse_indented_block_multiple_lines() {
632 let content = " line1\n line2\n line3\n ".to_string();
634 let result = Text::parse_indented_block(
635 Language::Other("text".into()),
636 content,
637 SyntaxHint::Block3,
638 )
639 .unwrap();
640 assert_eq!(result.content, "line1\nline2\nline3\n");
641 }
642
643 #[test]
644 fn test_parse_indented_block_with_empty_lines() {
645 let content = " line1\n\n line2\n ".to_string();
647 let result = Text::parse_indented_block(
648 Language::Other("text".into()),
649 content,
650 SyntaxHint::Block3,
651 )
652 .unwrap();
653 assert_eq!(result.content, "line1\n\nline2\n");
654 }
655
656 #[test]
657 fn test_parse_indented_block_whitespace_only_line() {
658 let content = " line1\n \n line2\n ".to_string();
660 let result = Text::parse_indented_block(
661 Language::Other("text".into()),
662 content,
663 SyntaxHint::Block3,
664 )
665 .unwrap();
666 assert_eq!(result.content, " line1\n\n line2\n");
667 }
668
669 #[test]
670 fn test_parse_indented_block_empty_content() {
671 let content = " ".to_string();
673 let result = Text::parse_indented_block(
674 Language::Other("text".into()),
675 content,
676 SyntaxHint::Block3,
677 )
678 .unwrap();
679 assert_eq!(result.content, "\n");
681 }
682
683 #[test]
684 fn test_parse_indented_block_implicit_language() {
685 let content = " hello\n ".to_string();
686 let result =
687 Text::parse_indented_block(Language::Implicit, content, SyntaxHint::Block3)
688 .unwrap();
689 assert_eq!(result.language, Language::Implicit);
690 assert_eq!(result.content, "hello\n");
691 }
692
693 #[test]
694 fn test_parse_indented_block_insufficient_indent() {
695 let content = " line1\n line2\n ".to_string();
697 let result = Text::parse_indented_block(
698 Language::Other("text".into()),
699 content,
700 SyntaxHint::Block3,
701 );
702 assert_eq!(
703 result,
704 Err(TextParseError::IndentError {
705 line: 2,
706 actual_indent: 2,
707 expected_indent: 4,
708 })
709 );
710 }
711
712 #[test]
713 fn test_parse_indented_block_no_indent() {
714 let content = "line1\n line2\n ".to_string();
716 let result = Text::parse_indented_block(
717 Language::Other("text".into()),
718 content,
719 SyntaxHint::Block3,
720 );
721 assert_eq!(
722 result,
723 Err(TextParseError::IndentError {
724 line: 1,
725 actual_indent: 0,
726 expected_indent: 4,
727 })
728 );
729 }
730
731 #[test]
732 fn test_parse_indented_block_empty_string() {
733 let content = String::new();
734 let result = Text::parse_indented_block(
735 Language::Other("text".into()),
736 content,
737 SyntaxHint::Block3,
738 );
739 assert!(result.is_ok());
740 }
741
742 #[test]
743 fn test_parse_indented_block_zero_indent() {
744 let content = "line1\nline2\n".to_string();
746 let result = Text::parse_indented_block(
747 Language::Other("text".into()),
748 content,
749 SyntaxHint::Block3,
750 )
751 .unwrap();
752 assert_eq!(result.content, "line1\nline2\n");
753 }
754
755 #[test]
756 fn test_parse_indented_block_empty_line_only() {
757 let content = " \n ".to_string();
759 let result = Text::parse_indented_block(
760 Language::Other("text".into()),
761 content,
762 SyntaxHint::Block3,
763 )
764 .unwrap();
765 assert_eq!(result.content, "\n");
767 }
768
769 #[test]
770 fn test_parse_indented_block_whitespace_only_line_insufficient_indent() {
771 let content = " line1\n \n line2\n ".to_string();
773 let result = Text::parse_indented_block(
774 Language::Other("text".into()),
775 content,
776 SyntaxHint::Block3,
777 )
778 .unwrap();
779 assert_eq!(result.content, "line1\n\nline2\n");
781 }
782
783 #[test]
784 fn test_parse_indented_block_whitespace_only_line_no_indent() {
785 let content = " line1\n\n line2\n ".to_string();
787 let result = Text::parse_indented_block(
788 Language::Other("text".into()),
789 content,
790 SyntaxHint::Block3,
791 )
792 .unwrap();
793 assert_eq!(result.content, " line1\n\n line2\n");
795 }
796 }
797}