1use super::error::{Error, Result};
4#[cfg(feature = "annotate")]
5use annotate_snippets::{
6 display_list::{DisplayList, FormatOptions},
7 snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
8};
9#[cfg(feature = "cli")]
10use clap::{Args, Parser, ValueEnum};
11use serde::{Deserialize, Serialize, Serializer};
12#[cfg(feature = "cli")]
13use std::path::PathBuf;
14
15#[cfg(feature = "cli")]
57pub fn parse_language_code(v: &str) -> Result<String> {
58 #[inline]
59 fn is_match(v: &str) -> bool {
60 let mut splits = v.split('-');
61
62 match splits.next() {
63 Some(s)
64 if (s.len() == 2 || s.len() == 3) && s.chars().all(|c| c.is_ascii_alphabetic()) => {
65 },
66 _ => return false,
67 }
68
69 match splits.next() {
70 Some(s) if s.len() != 2 || s.chars().any(|c| !c.is_ascii_alphabetic()) => return false,
71 Some(_) => (),
72 None => return true,
73 }
74 for s in splits {
75 if !s.chars().all(|c| c.is_ascii_alphabetic()) {
76 return false;
77 }
78 }
79 true
80 }
81
82 if v == "auto" || is_match(v) {
83 Ok(v.to_string())
84 } else {
85 Err(Error::InvalidValue(
86 "The value should be `\"auto\"` or match regex pattern: \
87 ^[a-zA-Z]{2,3}(-[a-zA-Z]{2}(-[a-zA-Z]+)*)?$"
88 .to_string(),
89 ))
90 }
91}
92
93pub(crate) fn serialize_option_vec_string<S>(
99 v: &Option<Vec<String>>,
100 serializer: S,
101) -> std::result::Result<S::Ok, S::Error>
102where
103 S: Serializer,
104{
105 match v {
106 Some(v) if v.len() == 1 => serializer.serialize_str(&v[0]),
107 Some(v) if v.len() > 1 => {
108 let size = v.iter().map(|s| s.len()).sum::<usize>() + v.len() - 1;
109 let mut string = String::with_capacity(size);
110
111 string.push_str(&v[0]);
112
113 for s in &v[1..] {
114 string.push(',');
115 string.push_str(s);
116 }
117
118 serializer.serialize_str(string.as_ref())
119 },
120 _ => serializer.serialize_none(),
121 }
122}
123
124#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
125#[non_exhaustive]
126#[serde(rename_all = "camelCase")]
127pub struct DataAnnotation {
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub interpret_as: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub markup: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub text: Option<String>,
138}
139
140impl Default for DataAnnotation {
141 fn default() -> Self {
142 Self {
143 interpret_as: None,
144 markup: None,
145 text: Some(String::new()),
146 }
147 }
148}
149
150impl DataAnnotation {
151 #[inline]
153 #[must_use]
154 pub fn new_text(text: String) -> Self {
155 Self {
156 interpret_as: None,
157 markup: None,
158 text: Some(text),
159 }
160 }
161
162 #[inline]
164 #[must_use]
165 pub fn new_markup(markup: String) -> Self {
166 Self {
167 interpret_as: None,
168 markup: Some(markup),
169 text: None,
170 }
171 }
172
173 #[inline]
175 #[must_use]
176 pub fn new_interpreted_markup(markup: String, interpret_as: String) -> Self {
177 Self {
178 interpret_as: Some(interpret_as),
179 markup: Some(markup),
180 text: None,
181 }
182 }
183}
184
185#[cfg(test)]
186mod data_annotation_tests {
187
188 use crate::check::DataAnnotation;
189
190 #[test]
191 fn test_text() {
192 let da = DataAnnotation::new_text("Hello".to_string());
193
194 assert_eq!(da.text.unwrap(), "Hello".to_string());
195 assert!(da.markup.is_none());
196 assert!(da.interpret_as.is_none());
197 }
198
199 #[test]
200 fn test_markup() {
201 let da = DataAnnotation::new_markup("<a>Hello</a>".to_string());
202
203 assert!(da.text.is_none());
204 assert_eq!(da.markup.unwrap(), "<a>Hello</a>".to_string());
205 assert!(da.interpret_as.is_none());
206 }
207
208 #[test]
209 fn test_interpreted_markup() {
210 let da =
211 DataAnnotation::new_interpreted_markup("<a>Hello</a>".to_string(), "Hello".to_string());
212
213 assert!(da.text.is_none());
214 assert_eq!(da.markup.unwrap(), "<a>Hello</a>".to_string());
215 assert_eq!(da.interpret_as.unwrap(), "Hello".to_string());
216 }
217}
218
219#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
221#[non_exhaustive]
222pub struct Data {
223 pub annotation: Vec<DataAnnotation>,
225}
226
227impl<T: Into<DataAnnotation>> FromIterator<T> for Data {
228 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
229 let annotation = iter.into_iter().map(std::convert::Into::into).collect();
230 Data { annotation }
231 }
232}
233
234impl Serialize for Data {
235 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
236 where
237 S: serde::Serializer,
238 {
239 let mut map = std::collections::HashMap::new();
240 map.insert("annotation", &self.annotation);
241
242 serializer.serialize_str(&serde_json::to_string(&map).unwrap())
243 }
244}
245
246#[cfg(feature = "cli")]
247impl std::str::FromStr for Data {
248 type Err = Error;
249
250 fn from_str(s: &str) -> Result<Self> {
251 let v: Self = serde_json::from_str(s)?;
252 Ok(v)
253 }
254}
255
256#[derive(Clone, Default, Deserialize, Debug, PartialEq, Eq, Serialize)]
261#[cfg_attr(feature = "cli", derive(ValueEnum))]
262#[serde(rename_all = "lowercase")]
263#[non_exhaustive]
264pub enum Level {
265 #[default]
267 Default,
268 Picky,
270}
271
272impl Level {
273 #[must_use]
285 pub fn is_default(&self) -> bool {
286 *self == Level::default()
287 }
288}
289
290#[must_use]
352pub fn split_len<'source>(s: &'source str, n: usize, pat: &str) -> Vec<&'source str> {
353 let mut vec: Vec<&'source str> = Vec::with_capacity(s.len() / n);
354 let mut splits = s.split_inclusive(pat);
355
356 let mut start = 0;
357 let mut i = 0;
358
359 if let Some(split) = splits.next() {
360 vec.push(split);
361 } else {
362 return Vec::new();
363 }
364
365 for split in splits {
366 let new_len = vec[i].len() + split.len();
367 if new_len < n {
368 vec[i] = &s[start..start + new_len];
369 } else {
370 vec.push(split);
371 start += vec[i].len();
372 i += 1;
373 }
374 }
375
376 vec
377}
378
379#[cfg_attr(feature = "cli", derive(Args))]
387#[derive(Clone, Deserialize, Debug, PartialEq, Eq, Serialize)]
388#[serde(rename_all = "camelCase")]
389#[non_exhaustive]
390pub struct CheckRequest {
391 #[cfg_attr(
393 feature = "cli",
394 clap(short = 't', long, conflicts_with = "data", allow_hyphen_values(true))
395 )]
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub text: Option<String>,
398 #[cfg_attr(feature = "cli", clap(short = 'd', long, conflicts_with = "text"))]
423 #[serde(skip_serializing_if = "Option::is_none")]
424 pub data: Option<Data>,
425 #[cfg_attr(
432 all(feature = "cli", feature = "cli", feature = "cli"),
433 clap(
434 short = 'l',
435 long,
436 default_value = "auto",
437 value_parser = parse_language_code
438 )
439 )]
440 pub language: String,
441 #[cfg_attr(
444 feature = "cli",
445 clap(short = 'u', long, requires = "api_key", env = "LANGUAGETOOL_USERNAME")
446 )]
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub username: Option<String>,
449 #[cfg_attr(
452 feature = "cli",
453 clap(short = 'k', long, requires = "username", env = "LANGUAGETOOL_API_KEY")
454 )]
455 #[serde(skip_serializing_if = "Option::is_none")]
456 pub api_key: Option<String>,
457 #[cfg_attr(feature = "cli", clap(long))]
460 #[serde(serialize_with = "serialize_option_vec_string")]
461 pub dicts: Option<Vec<String>>,
462 #[cfg_attr(feature = "cli", clap(long))]
465 #[serde(skip_serializing_if = "Option::is_none")]
466 pub mother_tongue: Option<String>,
467 #[cfg_attr(feature = "cli", clap(long, conflicts_with = "language"))]
477 #[serde(serialize_with = "serialize_option_vec_string")]
478 pub preferred_variants: Option<Vec<String>>,
479 #[cfg_attr(feature = "cli", clap(long))]
481 #[serde(serialize_with = "serialize_option_vec_string")]
482 pub enabled_rules: Option<Vec<String>>,
483 #[cfg_attr(feature = "cli", clap(long))]
485 #[serde(serialize_with = "serialize_option_vec_string")]
486 pub disabled_rules: Option<Vec<String>>,
487 #[cfg_attr(feature = "cli", clap(long))]
489 #[serde(serialize_with = "serialize_option_vec_string")]
490 pub enabled_categories: Option<Vec<String>>,
491 #[cfg_attr(feature = "cli", clap(long))]
493 #[serde(serialize_with = "serialize_option_vec_string")]
494 pub disabled_categories: Option<Vec<String>>,
495 #[cfg_attr(feature = "cli", clap(long))]
498 #[serde(skip_serializing_if = "is_false")]
499 pub enabled_only: bool,
500 #[cfg_attr(
503 feature = "cli",
504 clap(long, default_value = "default", ignore_case = true, value_enum)
505 )]
506 #[serde(skip_serializing_if = "Level::is_default")]
507 pub level: Level,
508}
509
510impl Default for CheckRequest {
511 #[inline]
512 fn default() -> CheckRequest {
513 CheckRequest {
514 text: Default::default(),
515 data: Default::default(),
516 language: "auto".to_string(),
517 username: Default::default(),
518 api_key: Default::default(),
519 dicts: Default::default(),
520 mother_tongue: Default::default(),
521 preferred_variants: Default::default(),
522 enabled_rules: Default::default(),
523 disabled_rules: Default::default(),
524 enabled_categories: Default::default(),
525 disabled_categories: Default::default(),
526 enabled_only: Default::default(),
527 level: Default::default(),
528 }
529 }
530}
531
532#[inline]
533fn is_false(b: &bool) -> bool {
534 !(*b)
535}
536
537impl CheckRequest {
538 #[must_use]
540 pub fn with_text(mut self, text: String) -> Self {
541 self.text = Some(text);
542 self.data = None;
543 self
544 }
545
546 #[must_use]
548 pub fn with_data(mut self, data: Data) -> Self {
549 self.data = Some(data);
550 self.text = None;
551 self
552 }
553
554 pub fn with_data_str(self, data: &str) -> serde_json::Result<Self> {
557 Ok(self.with_data(serde_json::from_str(data)?))
558 }
559
560 #[must_use]
562 pub fn with_language(mut self, language: String) -> Self {
563 self.language = language;
564 self
565 }
566
567 pub fn try_get_text(&self) -> Result<String> {
574 if let Some(ref text) = self.text {
575 Ok(text.clone())
576 } else if let Some(ref data) = self.data {
577 let mut text = String::new();
578 for da in data.annotation.iter() {
579 if let Some(ref t) = da.text {
580 text.push_str(t.as_str());
581 } else if let Some(ref t) = da.markup {
582 text.push_str(t.as_str());
583 } else {
584 return Err(Error::InvalidDataAnnotation(
585 "missing either text or markup field in {da:?}".to_string(),
586 ));
587 }
588 }
589 Ok(text)
590 } else {
591 Err(Error::InvalidRequest(
592 "missing either text or data field".to_string(),
593 ))
594 }
595 }
596
597 #[must_use]
605 pub fn get_text(&self) -> String {
606 self.try_get_text().unwrap()
607 }
608
609 pub fn try_split(&self, n: usize, pat: &str) -> Result<Vec<Self>> {
616 let text = self
617 .text
618 .as_ref()
619 .ok_or(Error::InvalidRequest("missing text field".to_string()))?;
620
621 Ok(split_len(text.as_str(), n, pat)
622 .iter()
623 .map(|text_fragment| self.clone().with_text(text_fragment.to_string()))
624 .collect())
625 }
626
627 #[must_use]
635 pub fn split(&self, n: usize, pat: &str) -> Vec<Self> {
636 self.try_split(n, pat).unwrap()
637 }
638}
639
640#[cfg(feature = "cli")]
643fn parse_filename(s: &str) -> Result<PathBuf> {
644 let path_buf: PathBuf = s.parse().unwrap();
645
646 if path_buf.is_file() {
647 Ok(path_buf)
648 } else {
649 Err(Error::InvalidFilename(s.to_string()))
650 }
651}
652
653#[cfg(feature = "cli")]
655#[derive(Debug, Parser)]
656pub struct CheckCommand {
657 #[cfg(feature = "cli")]
661 #[clap(short = 'r', long)]
662 pub raw: bool,
663 #[clap(short = 'm', long, hide = true)]
666 #[deprecated(
667 since = "2.0.0",
668 note = "Do not use this, it is only kept for backwards compatibility with v1"
669 )]
670 pub more_context: bool,
671 #[clap(long, default_value_t = 1500)]
673 pub max_length: usize,
674 #[clap(long, default_value = "\n\n")]
676 pub split_pattern: String,
677 #[clap(long, default_value_t = 5, allow_negative_numbers = true)]
679 pub max_suggestions: isize,
680 #[command(flatten)]
682 pub request: CheckRequest,
683 #[arg(conflicts_with_all(["text", "data"]), value_parser = parse_filename)]
685 pub filenames: Vec<PathBuf>,
686}
687
688#[cfg(test)]
689mod request_tests {
690
691 use crate::CheckRequest;
692
693 #[test]
694 fn test_with_text() {
695 let req = CheckRequest::default().with_text("hello".to_string());
696
697 assert_eq!(req.text.unwrap(), "hello".to_string());
698 assert!(req.data.is_none());
699 }
700
701 #[test]
702 fn test_with_data() {
703 let req = CheckRequest::default().with_text("hello".to_string());
704
705 assert_eq!(req.text.unwrap(), "hello".to_string());
706 assert!(req.data.is_none());
707 }
708}
709
710#[allow(clippy::derive_partial_eq_without_eq)]
714#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
715#[non_exhaustive]
716pub struct DetectedLanguage {
717 pub code: String,
719 #[cfg(feature = "unstable")]
721 pub confidence: Option<f64>,
722 pub name: String,
724 #[cfg(feature = "unstable")]
726 pub source: Option<String>,
727}
728
729#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
731#[serde(rename_all = "camelCase")]
732#[non_exhaustive]
733pub struct LanguageResponse {
734 pub code: String,
736 pub detected_language: DetectedLanguage,
738 pub name: String,
740}
741
742#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
744#[non_exhaustive]
745pub struct Context {
746 pub length: usize,
748 pub offset: usize,
750 pub text: String,
752}
753
754#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
756#[non_exhaustive]
757pub struct MoreContext {
758 pub line_number: usize,
760 pub line_offset: usize,
762}
763
764#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
766#[non_exhaustive]
767pub struct Replacement {
768 pub value: String,
770}
771
772impl From<String> for Replacement {
773 fn from(value: String) -> Self {
774 Self { value }
775 }
776}
777
778impl From<&str> for Replacement {
779 fn from(value: &str) -> Self {
780 value.to_string().into()
781 }
782}
783
784#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
786#[non_exhaustive]
787pub struct Category {
788 pub id: String,
790 pub name: String,
792}
793
794#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
796#[non_exhaustive]
797pub struct Url {
798 pub value: String,
800}
801
802#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
804#[serde(rename_all = "camelCase")]
805#[non_exhaustive]
806pub struct Rule {
807 pub category: Category,
809 pub description: String,
811 pub id: String,
813 #[cfg(feature = "unstable")]
815 pub is_premium: Option<bool>,
816 pub issue_type: String,
818 #[cfg(feature = "unstable")]
820 pub source_file: Option<String>,
821 pub sub_id: Option<String>,
823 pub urls: Option<Vec<Url>>,
825}
826
827#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
829#[serde(rename_all = "camelCase")]
830#[non_exhaustive]
831pub struct Type {
832 pub type_name: String,
834}
835
836#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
838#[serde(rename_all = "camelCase")]
839#[non_exhaustive]
840pub struct Match {
841 pub context: Context,
843 #[cfg(feature = "unstable")]
846 pub context_for_sure_match: isize,
847 #[cfg(feature = "unstable")]
850 pub ignore_for_incomplete_sentence: bool,
851 pub length: usize,
853 pub message: String,
855 #[serde(skip_serializing_if = "Option::is_none")]
857 pub more_context: Option<MoreContext>,
858 pub offset: usize,
860 pub replacements: Vec<Replacement>,
862 pub rule: Rule,
864 pub sentence: String,
866 pub short_message: String,
868 #[cfg(feature = "unstable")]
870 #[serde(rename = "type")]
871 pub type_: Type,
872}
873
874#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
876#[serde(rename_all = "camelCase")]
877#[non_exhaustive]
878pub struct Software {
879 pub api_version: usize,
881 pub build_date: String,
883 pub name: String,
885 pub premium: bool,
887 #[cfg(feature = "unstable")]
889 pub premium_hint: Option<String>,
890 pub status: String,
893 pub version: String,
895}
896
897#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
899#[serde(rename_all = "camelCase")]
900#[non_exhaustive]
901pub struct Warnings {
902 pub incomplete_results: bool,
904}
905
906#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
908#[serde(rename_all = "camelCase")]
909#[non_exhaustive]
910pub struct CheckResponse {
911 pub language: LanguageResponse,
913 pub matches: Vec<Match>,
915 #[cfg(feature = "unstable")]
917 pub sentence_ranges: Option<Vec<[usize; 2]>>,
918 pub software: Software,
920 #[cfg(feature = "unstable")]
922 pub warnings: Option<Warnings>,
923}
924
925impl CheckResponse {
926 pub fn iter_matches(&self) -> std::slice::Iter<'_, Match> {
928 self.matches.iter()
929 }
930
931 pub fn iter_matches_mut(&mut self) -> std::slice::IterMut<'_, Match> {
933 self.matches.iter_mut()
934 }
935
936 #[cfg(feature = "annotate")]
938 #[must_use]
939 pub fn annotate(&self, text: &str, origin: Option<&str>, color: bool) -> String {
940 if self.matches.is_empty() {
941 return "No error were found in provided text".to_string();
942 }
943 let replacements: Vec<_> = self
944 .matches
945 .iter()
946 .map(|m| {
947 m.replacements.iter().fold(String::new(), |mut acc, r| {
948 if !acc.is_empty() {
949 acc.push_str(", ");
950 }
951 acc.push_str(&r.value);
952 acc
953 })
954 })
955 .collect();
956
957 let snippets = self.matches.iter().zip(replacements.iter()).map(|(m, r)| {
958 Snippet {
959 title: Some(Annotation {
960 label: Some(&m.message),
961 id: Some(&m.rule.id),
962 annotation_type: AnnotationType::Error,
963 }),
964 footer: vec![],
965 slices: vec![Slice {
966 source: &m.context.text,
967 line_start: 1 + text.chars().take(m.offset).filter(|c| *c == '\n').count(),
968 origin,
969 fold: true,
970 annotations: vec![
971 SourceAnnotation {
972 label: &m.rule.description,
973 annotation_type: AnnotationType::Error,
974 range: (m.context.offset, m.context.offset + m.context.length),
975 },
976 SourceAnnotation {
977 label: r,
978 annotation_type: AnnotationType::Help,
979 range: (m.context.offset, m.context.offset + m.context.length),
980 },
981 ],
982 }],
983 opt: FormatOptions {
984 color,
985 ..Default::default()
986 },
987 }
988 });
989
990 let mut annotation = String::new();
991
992 for snippet in snippets {
993 if !annotation.is_empty() {
994 annotation.push('\n');
995 }
996 annotation.push_str(&DisplayList::from(snippet).to_string());
997 }
998 annotation
999 }
1000}
1001
1002#[derive(Debug, Clone, PartialEq)]
1007pub struct CheckResponseWithContext {
1008 pub text: String,
1010 pub response: CheckResponse,
1012 pub text_length: usize,
1014}
1015
1016impl CheckResponseWithContext {
1017 #[must_use]
1019 pub fn new(text: String, response: CheckResponse) -> Self {
1020 let text_length = text.chars().count();
1021 Self {
1022 text,
1023 response,
1024 text_length,
1025 }
1026 }
1027
1028 pub fn iter_matches(&self) -> std::slice::Iter<'_, Match> {
1030 self.response.iter_matches()
1031 }
1032
1033 pub fn iter_matches_mut(&mut self) -> std::slice::IterMut<'_, Match> {
1035 self.response.iter_matches_mut()
1036 }
1037
1038 #[must_use]
1041 pub fn iter_match_positions(&self) -> MatchPositions<'_, std::slice::Iter<'_, Match>> {
1042 self.into()
1043 }
1044
1045 #[must_use]
1050 pub fn append(mut self, mut other: Self) -> Self {
1051 let offset = self.text_length;
1052 for m in other.iter_matches_mut() {
1053 m.offset += offset;
1054 }
1055
1056 #[cfg(feature = "unstable")]
1057 if let Some(ref mut sr_other) = other.response.sentence_ranges {
1058 match self.response.sentence_ranges {
1059 Some(ref mut sr_self) => {
1060 sr_self.append(sr_other);
1061 },
1062 None => {
1063 std::mem::swap(
1064 &mut self.response.sentence_ranges,
1065 &mut other.response.sentence_ranges,
1066 );
1067 },
1068 }
1069 }
1070
1071 self.response.matches.append(&mut other.response.matches);
1072 self.text.push_str(other.text.as_str());
1073 self.text_length += other.text_length;
1074 self
1075 }
1076}
1077
1078impl From<CheckResponseWithContext> for CheckResponse {
1079 #[allow(clippy::needless_borrow)]
1080 fn from(mut resp: CheckResponseWithContext) -> Self {
1081 let iter: MatchPositions<'_, std::slice::IterMut<'_, Match>> = (&mut resp).into();
1082
1083 for (line_number, line_offset, m) in iter {
1084 m.more_context = Some(MoreContext {
1085 line_number,
1086 line_offset,
1087 });
1088 }
1089 resp.response
1090 }
1091}
1092
1093#[derive(Clone, Debug)]
1095pub struct MatchPositions<'source, T> {
1096 text_chars: std::str::Chars<'source>,
1097 matches: T,
1098 line_number: usize,
1099 line_offset: usize,
1100 offset: usize,
1101}
1102
1103impl<'source> From<&'source CheckResponseWithContext>
1104 for MatchPositions<'source, std::slice::Iter<'source, Match>>
1105{
1106 fn from(response: &'source CheckResponseWithContext) -> Self {
1107 MatchPositions {
1108 text_chars: response.text.chars(),
1109 matches: response.iter_matches(),
1110 line_number: 1,
1111 line_offset: 0,
1112 offset: 0,
1113 }
1114 }
1115}
1116
1117impl<'source> From<&'source mut CheckResponseWithContext>
1118 for MatchPositions<'source, std::slice::IterMut<'source, Match>>
1119{
1120 fn from(response: &'source mut CheckResponseWithContext) -> Self {
1121 MatchPositions {
1122 text_chars: response.text.chars(),
1123 matches: response.response.iter_matches_mut(),
1124 line_number: 1,
1125 line_offset: 0,
1126 offset: 0,
1127 }
1128 }
1129}
1130
1131impl<'source, T> MatchPositions<'source, T> {
1132 pub fn set_line_number(mut self, line_number: usize) -> Self {
1136 self.line_number = line_number;
1137 self
1138 }
1139
1140 fn update_line_number_and_offset(&mut self, m: &Match) {
1141 let n = m.offset - self.offset;
1142 for _ in 0..n {
1143 match self.text_chars.next() {
1144 Some('\n') => {
1145 self.line_number += 1;
1146 self.line_offset = 0;
1147 },
1148 None => {
1149 panic!(
1150 "text is shorter than expected, are you sure this text was the one used \
1151 for the check request?"
1152 )
1153 },
1154 _ => self.line_offset += 1,
1155 }
1156 }
1157 self.offset = m.offset;
1158 }
1159}
1160
1161impl<'source> Iterator for MatchPositions<'source, std::slice::Iter<'source, Match>> {
1162 type Item = (usize, usize, &'source Match);
1163
1164 fn next(&mut self) -> Option<Self::Item> {
1165 if let Some(m) = self.matches.next() {
1166 self.update_line_number_and_offset(m);
1167 Some((self.line_number, self.line_offset, m))
1168 } else {
1169 None
1170 }
1171 }
1172}
1173
1174impl<'source> Iterator for MatchPositions<'source, std::slice::IterMut<'source, Match>> {
1175 type Item = (usize, usize, &'source mut Match);
1176
1177 fn next(&mut self) -> Option<Self::Item> {
1178 if let Some(m) = self.matches.next() {
1179 self.update_line_number_and_offset(m);
1180 Some((self.line_number, self.line_offset, m))
1181 } else {
1182 None
1183 }
1184 }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::*;
1190
1191 #[derive(Debug)]
1192 enum Token<'source> {
1193 Text(&'source str),
1194 Skip(&'source str),
1195 }
1196
1197 #[derive(Debug, Clone)]
1198 struct ParseTokenError;
1199
1200 impl<'source> From<&'source str> for Token<'source> {
1201 fn from(s: &'source str) -> Self {
1202 if s.chars().all(|c| c.is_ascii_alphabetic()) {
1203 Token::Text(s)
1204 } else {
1205 Token::Skip(s)
1206 }
1207 }
1208 }
1209
1210 impl<'source> From<Token<'source>> for DataAnnotation {
1211 fn from(token: Token<'source>) -> Self {
1212 match token {
1213 Token::Text(s) => DataAnnotation::new_text(s.to_string()),
1214 Token::Skip(s) => DataAnnotation::new_markup(s.to_string()),
1215 }
1216 }
1217 }
1218
1219 #[test]
1220 fn test_data_annotation() {
1221 let words: Vec<&str> = "My name is Q34XY".split(' ').collect();
1222 let data: Data = words.iter().map(|w| Token::from(*w)).collect();
1223
1224 let expected_data = Data {
1225 annotation: vec![
1226 DataAnnotation::new_text("My".to_string()),
1227 DataAnnotation::new_text("name".to_string()),
1228 DataAnnotation::new_text("is".to_string()),
1229 DataAnnotation::new_markup("Q34XY".to_string()),
1230 ],
1231 };
1232
1233 assert_eq!(data, expected_data);
1234 }
1235
1236 #[test]
1237 fn test_serialize_option_vec_string() {
1238 use serde::Serialize;
1239
1240 #[derive(Serialize)]
1241 struct Foo {
1242 #[serde(serialize_with = "serialize_option_vec_string")]
1243 values: Option<Vec<String>>,
1244 }
1245
1246 impl Foo {
1247 fn new<I, T>(values: I) -> Self
1248 where
1249 I: IntoIterator<Item = T>,
1250 T: ToString,
1251 {
1252 Self {
1253 values: Some(values.into_iter().map(|v| v.to_string()).collect()),
1254 }
1255 }
1256 fn none() -> Self {
1257 Self { values: None }
1258 }
1259 }
1260
1261 let got = serde_json::to_string(&Foo::new(vec!["en-US", "de-DE"])).unwrap();
1262 assert_eq!(got, r#"{"values":"en-US,de-DE"}"#);
1263
1264 let got = serde_json::to_string(&Foo::new(vec!["en-US"])).unwrap();
1265 assert_eq!(got, r#"{"values":"en-US"}"#);
1266
1267 let got = serde_json::to_string(&Foo::new(Vec::<String>::new())).unwrap();
1268 assert_eq!(got, r#"{"values":null}"#);
1269
1270 let got = serde_json::to_string(&Foo::none()).unwrap();
1271 assert_eq!(got, r#"{"values":null}"#);
1272 }
1273}