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