ssip_common/
lib.rs

1// ssip-client -- Speech Dispatcher client in Rust
2// Copyright (c) 2021 Laurent Pelecq
3//
4// Licensed under the Apache License, Version 2.0
5// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
6// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. All files in the project carrying such notice may not be copied,
8// modified, or distributed except according to those terms.
9
10use std::fmt;
11use std::io;
12use std::str::FromStr;
13use thiserror::Error as ThisError;
14
15use strum_macros::Display as StrumDisplay;
16
17/// Return code of SSIP commands
18pub type ReturnCode = u16;
19
20/// Message identifier
21pub type MessageId = u32;
22
23/// Client identifier
24pub type ClientId = u32;
25
26/// Message identifiers
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum MessageScope {
29    /// Last message from current client
30    Last,
31    /// Messages from all clients
32    All,
33    /// Specific message
34    Message(MessageId),
35}
36
37impl fmt::Display for MessageScope {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            MessageScope::Last => write!(f, "self"),
41            MessageScope::All => write!(f, "all"),
42            MessageScope::Message(id) => write!(f, "{}", id),
43        }
44    }
45}
46
47/// Client identifiers
48#[derive(Debug, Clone, Hash, Eq, PartialEq)]
49pub enum ClientScope {
50    /// Current client
51    Current,
52    /// All clients
53    All,
54    /// Specific client
55    Client(ClientId),
56}
57
58impl fmt::Display for ClientScope {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            ClientScope::Current => write!(f, "self"),
62            ClientScope::All => write!(f, "all"),
63            ClientScope::Client(id) => write!(f, "{}", id),
64        }
65    }
66}
67
68/// Priority
69#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
70pub enum Priority {
71    #[strum(serialize = "progress")]
72    Progress,
73    #[strum(serialize = "notification")]
74    Notification,
75    #[strum(serialize = "message")]
76    Message,
77    #[strum(serialize = "text")]
78    Text,
79    #[strum(serialize = "important")]
80    Important,
81}
82
83/// Punctuation mode.
84#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
85pub enum PunctuationMode {
86    #[strum(serialize = "none")]
87    None,
88    #[strum(serialize = "some")]
89    Some,
90    #[strum(serialize = "most")]
91    Most,
92    #[strum(serialize = "all")]
93    All,
94}
95
96/// Capital letters recognition mode.
97#[derive(StrumDisplay, Debug, Clone, Hash, Eq, PartialEq)]
98pub enum CapitalLettersRecognitionMode {
99    #[strum(serialize = "none")]
100    None,
101    #[strum(serialize = "spell")]
102    Spell,
103    #[strum(serialize = "icon")]
104    Icon,
105}
106
107/// Symbolic key names
108#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
109pub enum KeyName {
110    #[strum(serialize = "space")]
111    Space,
112    #[strum(serialize = "underscore")]
113    Underscore,
114    #[strum(serialize = "double-quote")]
115    DoubleQuote,
116    #[strum(serialize = "alt")]
117    Alt,
118    #[strum(serialize = "control")]
119    Control,
120    #[strum(serialize = "hyper")]
121    Hyper,
122    #[strum(serialize = "meta")]
123    Meta,
124    #[strum(serialize = "shift")]
125    Shift,
126    #[strum(serialize = "super")]
127    Super,
128    #[strum(serialize = "backspace")]
129    Backspace,
130    #[strum(serialize = "break")]
131    Break,
132    #[strum(serialize = "delete")]
133    Delete,
134    #[strum(serialize = "down")]
135    Down,
136    #[strum(serialize = "end")]
137    End,
138    #[strum(serialize = "enter")]
139    Enter,
140    #[strum(serialize = "escape")]
141    Escape,
142    #[strum(serialize = "f1")]
143    F1,
144    #[strum(serialize = "f2")]
145    F2,
146    #[strum(serialize = "f3")]
147    F3,
148    #[strum(serialize = "f4")]
149    F4,
150    #[strum(serialize = "f5")]
151    F5,
152    #[strum(serialize = "f6")]
153    F6,
154    #[strum(serialize = "f7")]
155    F7,
156    #[strum(serialize = "f8")]
157    F8,
158    #[strum(serialize = "f9")]
159    F9,
160    #[strum(serialize = "f10")]
161    F10,
162    #[strum(serialize = "f11")]
163    F11,
164    #[strum(serialize = "f12")]
165    F12,
166    #[strum(serialize = "f13")]
167    F13,
168    #[strum(serialize = "f14")]
169    F14,
170    #[strum(serialize = "f15")]
171    F15,
172    #[strum(serialize = "f16")]
173    F16,
174    #[strum(serialize = "f17")]
175    F17,
176    #[strum(serialize = "f18")]
177    F18,
178    #[strum(serialize = "f19")]
179    F19,
180    #[strum(serialize = "f20")]
181    F20,
182    #[strum(serialize = "f21")]
183    F21,
184    #[strum(serialize = "f22")]
185    F22,
186    #[strum(serialize = "f23")]
187    F23,
188    #[strum(serialize = "f24")]
189    F24,
190    #[strum(serialize = "home")]
191    Home,
192    #[strum(serialize = "insert")]
193    Insert,
194    #[strum(serialize = "kp-*")]
195    KpMultiply,
196    #[strum(serialize = "kp-+")]
197    KpPlus,
198    #[strum(serialize = "kp--")]
199    KpMinus,
200    #[strum(serialize = "kp-.")]
201    KpDot,
202    #[strum(serialize = "kp-/")]
203    KpDivide,
204    #[strum(serialize = "kp-0")]
205    Kp0,
206    #[strum(serialize = "kp-1")]
207    Kp1,
208    #[strum(serialize = "kp-2")]
209    Kp2,
210    #[strum(serialize = "kp-3")]
211    Kp3,
212    #[strum(serialize = "kp-4")]
213    Kp4,
214    #[strum(serialize = "kp-5")]
215    Kp5,
216    #[strum(serialize = "kp-6")]
217    Kp6,
218    #[strum(serialize = "kp-7")]
219    Kp7,
220    #[strum(serialize = "kp-8")]
221    Kp8,
222    #[strum(serialize = "kp-9")]
223    Kp9,
224    #[strum(serialize = "kp-enter")]
225    KpEnter,
226    #[strum(serialize = "left")]
227    Left,
228    #[strum(serialize = "menu")]
229    Menu,
230    #[strum(serialize = "next")]
231    Next,
232    #[strum(serialize = "num-lock")]
233    NumLock,
234    #[strum(serialize = "pause")]
235    Pause,
236    #[strum(serialize = "print")]
237    Print,
238    #[strum(serialize = "prior")]
239    Prior,
240    #[strum(serialize = "return")]
241    Return,
242    #[strum(serialize = "right")]
243    Right,
244    #[strum(serialize = "scroll-lock")]
245    ScrollLock,
246    #[strum(serialize = "tab")]
247    Tab,
248    #[strum(serialize = "up")]
249    Up,
250    #[strum(serialize = "window")]
251    Window,
252}
253
254/// Notification type
255#[derive(StrumDisplay, Debug, Clone, Hash, Eq, PartialEq)]
256pub enum NotificationType {
257    #[strum(serialize = "begin")]
258    Begin,
259    #[strum(serialize = "end")]
260    End,
261    #[strum(serialize = "cancel")]
262    Cancel,
263    #[strum(serialize = "pause")]
264    Pause,
265    #[strum(serialize = "resume")]
266    Resume,
267    #[strum(serialize = "index_mark")]
268    IndexMark,
269    #[strum(serialize = "all")]
270    All,
271}
272
273/// Notification event type (returned by server)
274#[derive(StrumDisplay, Debug, Clone)]
275pub enum EventType {
276    Begin,
277    End,
278    Cancel,
279    Pause,
280    Resume,
281    IndexMark(String),
282}
283
284/// Event identifier
285#[derive(Debug, Clone, Hash, Eq, PartialEq)]
286pub struct EventId {
287    // Message id
288    pub message: String,
289    // Client id
290    pub client: String,
291}
292
293impl EventId {
294    // New event identifier
295    pub fn new(message: &str, client: &str) -> Self {
296        Self {
297            message: message.to_string(),
298            client: client.to_string(),
299        }
300    }
301}
302
303/// Notification event
304#[derive(Debug, Clone)]
305pub struct Event {
306    pub ntype: EventType,
307    pub id: EventId,
308}
309
310impl Event {
311    pub fn new(ntype: EventType, message: &str, client: &str) -> Event {
312        Event {
313            ntype,
314            id: EventId::new(message, client),
315        }
316    }
317
318    pub fn begin(message: &str, client: &str) -> Event {
319        Event::new(EventType::Begin, message, client)
320    }
321
322    pub fn end(message: &str, client: &str) -> Event {
323        Event::new(EventType::End, message, client)
324    }
325
326    pub fn index_mark(mark: String, message: &str, client: &str) -> Event {
327        Event::new(EventType::IndexMark(mark), message, client)
328    }
329
330    pub fn cancel(message: &str, client: &str) -> Event {
331        Event::new(EventType::Cancel, message, client)
332    }
333
334    pub fn pause(message: &str, client: &str) -> Event {
335        Event::new(EventType::Pause, message, client)
336    }
337
338    pub fn resume(message: &str, client: &str) -> Event {
339        Event::new(EventType::Resume, message, client)
340    }
341}
342
343/// Synthesis voice
344#[derive(Debug, PartialEq, Eq, Clone, Hash)]
345pub struct SynthesisVoice {
346    pub name: String,
347    pub language: Option<String>,
348    pub dialect: Option<String>,
349}
350
351impl SynthesisVoice {
352    pub fn new(name: &str, language: Option<&str>, dialect: Option<&str>) -> SynthesisVoice {
353        SynthesisVoice {
354            name: name.to_string(),
355            language: language.map(|s| s.to_string()),
356            dialect: dialect.map(|s| s.to_string()),
357        }
358    }
359    /// Parse Option::None or string "none" into Option::None
360    fn parse_none(token: Option<&str>) -> Option<String> {
361        match token {
362            Some(s) => match s {
363                "none" => None,
364                s => Some(s.to_string()),
365            },
366            None => None,
367        }
368    }
369}
370
371impl FromStr for SynthesisVoice {
372    type Err = ClientError;
373
374    fn from_str(s: &str) -> Result<Self, Self::Err> {
375        let mut iter = s.split('\t');
376        match iter.next() {
377            Some(name) => Ok(SynthesisVoice {
378                name: name.to_string(),
379                language: SynthesisVoice::parse_none(iter.next()),
380                dialect: SynthesisVoice::parse_none(iter.next()),
381            }),
382            None => Err(ClientError::unexpected_eof("missing synthesis voice name")),
383        }
384    }
385}
386
387/// Command status line
388///
389/// Consists in a 3-digits code and a message. It can be a success or a failure.
390///
391/// Examples:
392/// - 216 OK OUTPUT MODULE SET
393/// - 409 ERR RATE TOO HIGH
394#[derive(Debug, PartialEq, Eq)]
395pub struct StatusLine {
396    pub code: ReturnCode,
397    pub message: String,
398}
399
400impl fmt::Display for StatusLine {
401    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
402        write!(f, "{} {}", self.code, self.message)
403    }
404}
405/// Client error, either I/O error or SSIP error.
406#[derive(ThisError, Debug)]
407pub enum ClientError {
408    #[error("I/O: {0}")]
409    Io(io::Error),
410    #[error("Not ready")]
411    NotReady,
412    #[error("SSIP: {0}")]
413    Ssip(StatusLine),
414    #[error("Too few lines")]
415    TooFewLines,
416    #[error("Too many lines")]
417    TooManyLines,
418    #[error("Unexpected status: {0}")]
419    UnexpectedStatus(ReturnCode),
420}
421
422impl ClientError {
423    /// Create I/O error
424    pub fn io_error(kind: io::ErrorKind, msg: &str) -> Self {
425        Self::Io(io::Error::new(kind, msg))
426    }
427
428    /// Invalid data I/O error
429    pub fn invalid_data(msg: &str) -> Self {
430        ClientError::io_error(io::ErrorKind::InvalidData, msg)
431    }
432
433    /// Unexpected EOF I/O error
434    pub fn unexpected_eof(msg: &str) -> Self {
435        ClientError::io_error(io::ErrorKind::UnexpectedEof, msg)
436    }
437}
438
439impl From<io::Error> for ClientError {
440    fn from(err: io::Error) -> Self {
441        if err.kind() == io::ErrorKind::WouldBlock {
442            ClientError::NotReady
443        } else {
444            ClientError::Io(err)
445        }
446    }
447}
448
449/// Client result.
450pub type ClientResult<T> = Result<T, ClientError>;
451
452/// Client result consisting in a single status line
453pub type ClientStatus = ClientResult<StatusLine>;
454
455/// Client name
456#[derive(Debug, Clone, PartialEq, Eq, Hash)]
457pub struct ClientName {
458    pub user: String,
459    pub application: String,
460    pub component: String,
461}
462
463impl ClientName {
464    pub fn new(user: &str, application: &str) -> Self {
465        ClientName::with_component(user, application, "main")
466    }
467
468    pub fn with_component(user: &str, application: &str, component: &str) -> Self {
469        ClientName {
470            user: user.to_string(),
471            application: application.to_string(),
472            component: component.to_string(),
473        }
474    }
475}
476
477/// Cursor motion in history
478#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
479pub enum CursorDirection {
480    #[strum(serialize = "backward")]
481    Backward,
482    #[strum(serialize = "forward")]
483    Forward,
484}
485
486/// Sort direction in history
487#[derive(StrumDisplay, Debug, Clone, Eq, PartialEq, Hash)]
488pub enum SortDirection {
489    #[strum(serialize = "asc")]
490    Ascending,
491    #[strum(serialize = "desc")]
492    Descending,
493}
494
495/// Property messages are ordered by in history
496#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
497pub enum SortKey {
498    #[strum(serialize = "client_name")]
499    ClientName,
500    #[strum(serialize = "priority")]
501    Priority,
502    #[strum(serialize = "message_type")]
503    MessageType,
504    #[strum(serialize = "time")]
505    Time,
506    #[strum(serialize = "user")]
507    User,
508}
509
510/// Sort ordering
511#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
512pub enum Ordering {
513    #[strum(serialize = "text")]
514    Text,
515    #[strum(serialize = "sound_icon")]
516    SoundIcon,
517    #[strum(serialize = "char")]
518    Char,
519    #[strum(serialize = "key")]
520    Key,
521}
522
523/// Position in history
524#[derive(Debug, Clone, Eq, PartialEq, Hash)]
525pub enum HistoryPosition {
526    First,
527    Last,
528    Pos(u16),
529}
530
531impl fmt::Display for HistoryPosition {
532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533        match self {
534            HistoryPosition::First => write!(f, "first"),
535            HistoryPosition::Last => write!(f, "last"),
536            HistoryPosition::Pos(n) => write!(f, "pos {}", n),
537        }
538    }
539}
540
541/// History client status
542#[derive(Debug, PartialEq, Eq, Clone, Hash)]
543pub struct HistoryClientStatus {
544    pub id: ClientId,
545    pub name: String,
546    pub connected: bool,
547}
548
549impl HistoryClientStatus {
550    pub fn new(id: ClientId, name: &str, connected: bool) -> Self {
551        Self {
552            id,
553            name: name.to_string(),
554            connected,
555        }
556    }
557}
558
559impl FromStr for HistoryClientStatus {
560    type Err = ClientError;
561
562    fn from_str(s: &str) -> Result<Self, Self::Err> {
563        let mut iter = s.splitn(3, ' ');
564        match iter.next() {
565            Some("") => Err(ClientError::unexpected_eof("expecting client id")),
566            Some(client_id) => match client_id.parse::<u32>() {
567                Ok(id) => match iter.next() {
568                    Some(name) => match iter.next() {
569                        Some(status) if status == "0" => {
570                            Ok(HistoryClientStatus::new(id, name, false))
571                        }
572                        Some(status) if status == "1" => {
573                            Ok(HistoryClientStatus::new(id, name, true))
574                        }
575                        Some(_) => Err(ClientError::invalid_data("invalid client status")),
576                        None => Err(ClientError::unexpected_eof("expecting client status")),
577                    },
578                    None => Err(ClientError::unexpected_eof("expecting client name")),
579                },
580                Err(_) => Err(ClientError::invalid_data("invalid client id")),
581            },
582            None => Err(ClientError::unexpected_eof("expecting client id")),
583        }
584    }
585}
586
587#[derive(Debug, Clone, Hash, PartialEq, Eq)]
588/// Request for SSIP server.
589pub enum Request {
590    SetName(ClientName),
591    // Speech related requests
592    Speak,
593    SendLine(String),
594    SendLines(Vec<String>),
595    SpeakChar(char),
596    SpeakKey(KeyName),
597    // Flow control
598    Stop(MessageScope),
599    Cancel(MessageScope),
600    Pause(MessageScope),
601    Resume(MessageScope),
602    // Setter and getter
603    SetPriority(Priority),
604    SetDebug(bool),
605    SetOutputModule(ClientScope, String),
606    GetOutputModule,
607    ListOutputModules,
608    SetLanguage(ClientScope, String),
609    GetLanguage,
610    SetSsmlMode(bool),
611    SetPunctuationMode(ClientScope, PunctuationMode),
612    SetSpelling(ClientScope, bool),
613    SetCapitalLettersRecognitionMode(ClientScope, CapitalLettersRecognitionMode),
614    SetVoiceType(ClientScope, String),
615    GetVoiceType,
616    ListVoiceTypes,
617    SetSynthesisVoice(ClientScope, String),
618    ListSynthesisVoices,
619    SetRate(ClientScope, i8),
620    GetRate,
621    SetPitch(ClientScope, i8),
622    GetPitch,
623    SetVolume(ClientScope, i8),
624    GetVolume,
625    SetPauseContext(ClientScope, u32),
626    SetNotification(NotificationType, bool),
627    // Blocks
628    Begin,
629    End,
630    // History
631    SetHistory(ClientScope, bool),
632    HistoryGetClients,
633    HistoryGetClientId,
634    HistoryGetClientMsgs(ClientScope, u32, u32),
635    HistoryGetLastMsgId,
636    HistoryGetMsg(MessageId),
637    HistoryCursorGet,
638    HistoryCursorSet(ClientScope, HistoryPosition),
639    HistoryCursorMove(CursorDirection),
640    HistorySpeak(MessageId),
641    HistorySort(SortDirection, SortKey),
642    HistorySetShortMsgLength(u32),
643    HistorySetMsgTypeOrdering(Vec<Ordering>),
644    HistorySearch(ClientScope, String),
645    // Misc.
646    Quit,
647}
648
649#[derive(Debug, Clone, Hash, PartialEq, Eq)]
650/// Response from SSIP server.
651pub enum Response {
652    LanguageSet,                                     // 201
653    PrioritySet,                                     // 202
654    RateSet,                                         // 203
655    PitchSet,                                        // 204
656    PunctuationSet,                                  // 205
657    CapLetRecognSet,                                 // 206
658    SpellingSet,                                     // 207
659    ClientNameSet,                                   // 208
660    VoiceSet,                                        // 209
661    Stopped,                                         // 210
662    Paused,                                          // 211
663    Resumed,                                         // 212
664    Canceled,                                        // 213
665    TableSet,                                        // 215
666    OutputModuleSet,                                 // 216
667    PauseContextSet,                                 // 217
668    VolumeSet,                                       // 218
669    SsmlModeSet,                                     // 219
670    NotificationSet,                                 // 220
671    PitchRangeSet,                                   // 263
672    DebugSet,                                        // 262
673    HistoryCurSetFirst,                              // 220
674    HistoryCurSetLast,                               // 221
675    HistoryCurSetPos,                                // 222
676    HistoryCurMoveFor,                               // 223
677    HistoryCurMoveBack,                              // 224
678    MessageQueued,                                   // 225,
679    SoundIconQueued,                                 // 226
680    MessageCanceled,                                 // 227
681    ReceivingData,                                   // 230
682    Bye,                                             // 231
683    HistoryClientListSent(Vec<HistoryClientStatus>), // 240
684    HistoryMsgsListSent(Vec<String>),                // 241
685    HistoryLastMsg(String),                          // 242
686    HistoryCurPosRet(String),                        // 243
687    TableListSent(Vec<String>),                      // 244
688    HistoryClientIdSent(ClientId),                   // 245
689    MessageTextSent,                                 // 246
690    HelpSent(Vec<String>),                           // 248
691    VoicesListSent(Vec<SynthesisVoice>),             // 249
692    OutputModulesListSent(Vec<String>),              // 250
693    Get(String),                                     // 251
694    InsideBlock,                                     // 260
695    OutsideBlock,                                    // 261
696    NotImplemented,                                  // 299
697    EventIndexMark(EventId, String),                 // 700
698    EventBegin(EventId),                             // 701
699    EventEnd(EventId),                               // 702
700    EventCanceled(EventId),                          // 703
701    EventPaused(EventId),                            // 704
702    EventResumed(EventId),                           // 705
703}
704
705#[cfg(test)]
706mod tests {
707
708    use std::io;
709    use std::str::FromStr;
710
711    use super::{ClientError, HistoryClientStatus, HistoryPosition, MessageScope, SynthesisVoice};
712
713    #[test]
714    fn parse_synthesis_voice() {
715        // Voice with dialect
716        let v1 =
717            SynthesisVoice::from_str("Portuguese (Portugal)+Kaukovalta\tpt\tKaukovalta").unwrap();
718        assert_eq!("Portuguese (Portugal)+Kaukovalta", v1.name);
719        assert_eq!("pt", v1.language.unwrap());
720        assert_eq!("Kaukovalta", v1.dialect.unwrap());
721
722        // Voice without dialect
723        let v2 = SynthesisVoice::from_str("Esperanto\teo\tnone").unwrap();
724        assert_eq!("Esperanto", v2.name);
725        assert_eq!("eo", v2.language.unwrap());
726        assert!(matches!(v2.dialect, None));
727    }
728
729    #[test]
730    fn format_message_scope() {
731        assert_eq!("self", format!("{}", MessageScope::Last).as_str());
732        assert_eq!("all", format!("{}", MessageScope::All).as_str());
733        assert_eq!("123", format!("{}", MessageScope::Message(123)).as_str());
734    }
735
736    #[test]
737    fn format_history_position() {
738        assert_eq!("first", format!("{}", HistoryPosition::First).as_str());
739        assert_eq!("last", format!("{}", HistoryPosition::Last).as_str());
740        assert_eq!("pos 15", format!("{}", HistoryPosition::Pos(15)).as_str());
741    }
742
743    #[test]
744    fn parse_history_client_status() {
745        assert_eq!(
746            HistoryClientStatus::new(10, "joe:speechd_client:main", false),
747            HistoryClientStatus::from_str("10 joe:speechd_client:main 0").unwrap()
748        );
749        assert_eq!(
750            HistoryClientStatus::new(11, "joe:speechd_client:main", true),
751            HistoryClientStatus::from_str("11 joe:speechd_client:main 1").unwrap()
752        );
753        for line in &[
754            "9 joe:speechd_client:main xxx",
755            "xxx joe:speechd_client:main 1",
756        ] {
757            match HistoryClientStatus::from_str(line) {
758                Ok(_) => panic!("parsing should have failed"),
759                Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::InvalidData => (),
760                Err(_) => panic!("expecting error 'invalid data' parsing \"{}\"", line),
761            }
762        }
763        for line in &["8 joe:speechd_client:main", "8", ""] {
764            match HistoryClientStatus::from_str(line) {
765                Ok(_) => panic!("parsing should have failed"),
766                Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::UnexpectedEof => (),
767                Err(_) => panic!("expecting error 'unexpected EOF' parsing \"{}\"", line),
768            }
769        }
770    }
771}