1use std::collections::HashMap;
2use std::{fmt, mem};
3
4use serde::{Deserialize, Serialize};
5#[cfg(feature = "jsbindings")]
6use wasm_bindgen::prelude::wasm_bindgen;
7
8use crate::document::RtfDocument;
9use crate::header::{CharacterSet, Color, ColorRef, ColorTable, Font, FontFamily, FontRef, FontTable, RtfHeader, StyleSheet};
10use crate::paragraph::{Alignment, Paragraph, SpaceBetweenLine};
11use crate::tokens::{ControlWord, Property, Token};
12
13macro_rules! header_control_word {
15 ($cw:ident) => {
16 &Token::ControlSymbol((ControlWord::$cw, _))
17 };
18 ($cw:ident, $prop:ident) => {
19 &Token::ControlSymbol((ControlWord::$cw, Property::$prop))
20 };
21}
22
23#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
24#[cfg_attr(feature = "jsbindings", wasm_bindgen(getter_with_clone))]
25pub struct StyleBlock {
26 pub painter: Painter,
27 pub paragraph: Paragraph,
28 pub text: String,
29}
30
31#[derive(Debug, Clone, PartialEq, Hash, Deserialize, Serialize)]
32#[cfg_attr(feature = "jsbindings", wasm_bindgen)]
33pub struct Painter {
34 pub color_ref: ColorRef,
35 pub font_ref: FontRef,
36 pub font_size: u16,
37 pub bold: bool,
38 pub italic: bool,
39 pub underline: bool,
40 pub superscript: bool,
41 pub subscript: bool,
42 pub smallcaps: bool,
43 pub strike: bool,
44}
45
46impl Default for Painter {
47 fn default() -> Self {
48 Self {
49 color_ref: Default::default(),
50 font_ref: Default::default(),
51 font_size: 12,
52 bold: Default::default(),
53 italic: Default::default(),
54 underline: Default::default(),
55 superscript: Default::default(),
56 subscript: Default::default(),
57 smallcaps: Default::default(),
58 strike: Default::default(),
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
64pub enum ParserError {
65 InvalidToken(String),
66 IgnorableDestinationParsingError,
67 MalformedPainterStack,
68 InvalidFontIdentifier(Property),
69 InvalidColorIdentifier(Property),
70 NoMoreToken,
71 ValueCastError(String),
72 UnicodeParsingError(i32),
73 ParseEmptyToken,
74}
75
76impl std::error::Error for ParserError {}
77
78impl fmt::Display for ParserError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 let _ = write!(f, "[RTF Parser] : ");
81 return match self {
82 ParserError::InvalidToken(msg) => write!(f, "{}", msg),
83 ParserError::IgnorableDestinationParsingError => write!(f, "No ignorable destination should be left"),
84 ParserError::MalformedPainterStack => write!(f, "Malformed painter stack : Unbalanced number of brackets"),
85 ParserError::InvalidFontIdentifier(property) => write!(f, "Invalid font identifier : {:?}", property),
86 ParserError::InvalidColorIdentifier(property) => write!(f, "Invalid color identifier : {:?}", property),
87 ParserError::NoMoreToken => write!(f, "No more token to parse"),
88 ParserError::ValueCastError(_type) => write!(f, "Unable to cast i32 to {_type}"),
89 ParserError::UnicodeParsingError(value) => write!(f, "Unable to parse {value} value to unicode"),
90 ParserError::ParseEmptyToken => write!(f, "Try to parse an empty token, this should never happen. If so, please open an issue in the github repository"),
91 };
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Hash)]
97struct ParserState {
98 pub painter: Painter,
99 pub paragraph: Paragraph,
100 pub unicode_ignore_count: i32,
101}
102
103impl Default for ParserState {
104 fn default() -> Self {
105 Self {
106 painter: Default::default(),
107 paragraph: Default::default(),
108 unicode_ignore_count: 1,
109 }
110 }
111}
112
113pub struct Parser<'a> {
114 tokens: Vec<Token<'a>>,
115 parsed_item: Vec<bool>,
116 cursor: usize,
117}
118
119impl<'a> Parser<'a> {
120 pub fn new(tokens: Vec<Token<'a>>) -> Self {
121 return Self {
122 parsed_item: vec![false; tokens.len()],
123 tokens,
124 cursor: 0,
125 };
126 }
127
128 pub fn get_tokens(&self) -> Vec<&Token> {
129 return self.tokens.iter().filter(|t| *t != &Token::Empty).collect();
131 }
132
133 fn check_document_validity(&self) -> Result<(), ParserError> {
134 if let Some(token) = self.tokens.first() {
136 if token != &Token::OpeningBracket {
137 return Err(ParserError::InvalidToken(format!("Invalid first token : {:?} not a '{{'", token)));
138 }
139 } else {
140 return Err(ParserError::NoMoreToken);
141 }
142 if let Some(token) = self.tokens.last() {
143 if token != &Token::ClosingBracket {
144 return Err(ParserError::InvalidToken(format!("Invalid last token : {:?} not a '}}'", token)));
145 }
146 } else {
147 return Err(ParserError::NoMoreToken);
148 }
149 return Ok(());
150 }
151
152 pub fn parse(&mut self) -> Result<RtfDocument, ParserError> {
153 self.check_document_validity()?;
154 let mut document = RtfDocument::default(); document.header = self.parse_header()?;
157 let mut state_stack: Vec<ParserState> = vec![ParserState::default()];
159 let len = self.tokens.len();
161 let mut i = 0;
162 let mut pending_high_surrogate: Option<u16> = None;
163
164 while i < len {
165 if self.parsed_item[i] {
166 i += 1;
168 continue;
169 }
170 let token = &self.tokens[i];
171
172 if pending_high_surrogate.is_some() && !matches!(token, Token::ControlSymbol((ControlWord::Unicode | ControlWord::UnicodeIgnoreCount, _))) {
173 pending_high_surrogate = None;
174 Self::add_text_to_document("\u{FFFD}", &state_stack, &mut document)?;
175 }
176
177 match token {
178 Token::OpeningBracket => {
179 if let Some(last_state) = state_stack.last() {
180 state_stack.push(last_state.clone()); } else {
182 state_stack.push(ParserState::default());
183 }
184 }
185 Token::ClosingBracket => {
186 let state = state_stack.pop();
187 if state.is_none() {
188 return Err(ParserError::MalformedPainterStack);
189 }
190 }
191 Token::ControlSymbol((control_word, property)) => {
192 let Some(current_state) = state_stack.last_mut() else {
193 return Err(ParserError::MalformedPainterStack);
194 };
195 let current_painter = &mut current_state.painter;
196 let paragraph = &mut current_state.paragraph;
197 #[rustfmt::skip] match control_word {
199 ControlWord::ColorNumber => current_painter.color_ref = property.get_value_as::<ColorRef>()?,
200 ControlWord::FontNumber => current_painter.font_ref = property.get_value_as::<FontRef>()?,
201 ControlWord::FontSize => current_painter.font_size = property.get_value_as::<u16>()?,
202 ControlWord::Bold => current_painter.bold = property.as_bool(),
203 ControlWord::Italic => current_painter.italic = property.as_bool(),
204 ControlWord::Underline => current_painter.underline = property.as_bool(),
205 ControlWord::UnderlineNone => current_painter.underline = false,
206 ControlWord::Superscript => current_painter.superscript = property.as_bool(),
207 ControlWord::Subscript => current_painter.subscript = property.as_bool(),
208 ControlWord::Smallcaps => current_painter.smallcaps = property.as_bool(),
209 ControlWord::Strikethrough => current_painter.strike = property.as_bool(),
210 ControlWord::Pard => *paragraph = Paragraph::default(), ControlWord::Plain => *current_painter = Painter::default(), ControlWord::ParDefTab => paragraph.tab_width = property.get_value(),
214 ControlWord::LeftAligned
215 | ControlWord::RightAligned
216 | ControlWord::Center
217 | ControlWord::Justify => paragraph.alignment = Alignment::from(control_word),
218 ControlWord::SpaceBefore => paragraph.spacing.before = property.get_value(),
219 ControlWord::SpaceAfter => paragraph.spacing.after = property.get_value(),
220 ControlWord::SpaceBetweenLine => paragraph.spacing.between_line = SpaceBetweenLine::from(property.get_value()),
221 ControlWord::SpaceLineMul => paragraph.spacing.line_multiplier = property.get_value(),
222 ControlWord::UnicodeIgnoreCount => current_state.unicode_ignore_count = property.get_value(),
223 ControlWord::Unicode => {
224 let mut unicodes = Vec::new();
225 if let Ok(unicode) = property.get_unicode_value() {
226 unicodes.push(unicode);
227 }
228 while i + 1 < len {
230 if let Token::ControlSymbol((ControlWord::Unicode, property)) = &self.tokens[i + 1] {
232 if let Ok(unicode) = property.get_unicode_value() {
233 unicodes.push(unicode);
234 }
235 i += 1;
236 } else {
237 break;
238 }
239 }
240 if unicodes.len() > 0 {
241 let mut ignore_mask = vec![true; unicodes.len()];
243 let mut ignore_counter = 0;
244 for i in 1..unicodes.len() {
245 if unicodes[i] <= 255 && ignore_counter < current_state.unicode_ignore_count {
246 ignore_counter += 1;
247 ignore_mask[i] = false;
248 } else {
249 ignore_counter = 0;
250 }
251 }
252 let mut ignore_mask_iter = ignore_mask.iter();
253 unicodes.retain(|_| *ignore_mask_iter.next().unwrap());
254 if let Some(high) = pending_high_surrogate.take() {
255 let pairs = unicodes.first().is_some_and(|u| (0xDC00..=0xDFFF).contains(u));
256 unicodes.insert(0, if pairs { high } else { 0xFFFD });
257 }
258 if unicodes.last().is_some_and(|u| (0xD800..=0xDBFF).contains(u)) {
259 pending_high_surrogate = unicodes.pop();
260 }
261 if !unicodes.is_empty() {
262 let str = String::from_utf16_lossy(unicodes.as_slice());
263 Self::add_text_to_document(&str, &state_stack, &mut document)?;
264 }
265 }
266 }
267 ControlWord::Emdash => Self::add_text_to_document("\u{2014}", &state_stack, &mut document)?,
269 ControlWord::Endash => Self::add_text_to_document("\u{2013}", &state_stack, &mut document)?,
270 ControlWord::Bullet => Self::add_text_to_document("\u{2022}", &state_stack, &mut document)?,
271 ControlWord::LeftSingleQuote => Self::add_text_to_document("\u{2018}", &state_stack, &mut document)?,
272 ControlWord::RightSingleQuote => Self::add_text_to_document("\u{2019}", &state_stack, &mut document)?,
273 ControlWord::LeftDoubleQuote => Self::add_text_to_document("\u{201C}", &state_stack, &mut document)?,
274 ControlWord::RightDoubleQuote => Self::add_text_to_document("\u{201D}", &state_stack, &mut document)?,
275 ControlWord::Tab => Self::add_text_to_document("\t", &state_stack, &mut document)?,
276 ControlWord::Line => Self::add_text_to_document("\n", &state_stack, &mut document)?,
277 _ => {}
279 };
280 }
281 Token::PlainText(text) => Self::add_text_to_document(*text, &state_stack, &mut document)?,
282 Token::CRLF => Self::add_text_to_document("\n", &state_stack, &mut document)?,
283 Token::IgnorableDestination => {
284 return Err(ParserError::IgnorableDestinationParsingError);
285 }
286 Token::Empty => return Err(ParserError::ParseEmptyToken),
287 };
288 i += 1;
289 }
290 if pending_high_surrogate.is_some() && !state_stack.is_empty() {
291 Self::add_text_to_document("\u{FFFD}", &state_stack, &mut document)?;
292 }
293 return Ok(document);
294 }
295
296 fn add_text_to_document(text: &str, state_stack: &Vec<ParserState>, document: &mut RtfDocument) -> Result<(), ParserError> {
297 let Some(current_state) = state_stack.last() else {
298 return Err(ParserError::MalformedPainterStack);
299 };
300 let current_painter = ¤t_state.painter;
301 let paragraph = ¤t_state.paragraph;
302 let last_style_group = document.body.last_mut();
303 if let Some(group) = last_style_group {
305 if group.painter.eq(current_painter) && group.paragraph.eq(¶graph) {
306 group.text.push_str(text);
307 return Ok(());
308 }
309 }
310 document.body.push(StyleBlock {
312 painter: current_painter.clone(),
313 paragraph: paragraph.clone(),
314 text: String::from(text),
315 });
316 return Ok(());
317 }
318
319 fn get_token_at(&'a self, index: usize) -> Option<&'a Token<'a>> {
320 return self.tokens.get(index);
321 }
322
323 fn get_next_token(&'a self) -> Option<&'a Token<'a>> {
325 return self.get_token_at(self.cursor);
326 }
327
328 #[inline]
329 fn consume_token_at(&mut self, index: usize) -> Option<Token<'a>> {
330 if self.tokens.is_empty() || index >= self.tokens.len() {
331 return None;
332 }
333 self.cursor += 1;
335 self.parsed_item[index] = true;
336 return Some(mem::replace(&mut self.tokens[index], Token::Empty));
337 }
338
339 fn consume_next_token(&mut self) -> Option<Token<'a>> {
340 return self.consume_token_at(self.cursor);
341 }
342
343 fn _consume_tokens_until(&mut self, reference_token: &Token<'a>) -> Vec<Token<'a>> {
345 let mut ret = vec![];
346 let token_type_id = mem::discriminant(reference_token);
347 while let Some(token) = self.consume_next_token() {
348 let type_id = mem::discriminant(&token);
349 ret.push(token);
350 if type_id == token_type_id {
351 break;
352 }
353 }
354 return ret;
355 }
356
357 fn consume_tokens_until_matching_bracket(&mut self) -> Vec<Token<'a>> {
359 let mut ret = vec![];
360 let mut count = 0;
361 while let Some(token) = self.consume_next_token() {
362 match token {
363 Token::OpeningBracket => count += 1,
364 Token::ClosingBracket => count -= 1,
365 _ => {}
366 }
367 ret.push(token);
368 if count < 0 {
369 break;
370 }
371 }
372 return ret;
373 }
374
375 fn consume_group(&mut self) -> Vec<Token<'a>> {
377 self.consume_token_at(self.cursor); return self.consume_tokens_until_matching_bracket();
380 }
381
382 fn parse_header(&mut self) -> Result<RtfHeader, ParserError> {
384 self.cursor = 0; let mut header = RtfHeader::default();
386 while let (Some(token), Some(mut next_token)) = (self.get_token_at(self.cursor), self.get_token_at(self.cursor + 1)) {
387 let mut i = 0;
390 while *next_token == Token::CRLF {
391 if let Some(next_token_not_crlf) = self.get_token_at(self.cursor + 1 + i) {
392 next_token = next_token_not_crlf;
393 i += 1;
394 } else {
395 break;
396 }
397 }
398 match (token, next_token) {
399 (Token::OpeningBracket, Token::IgnorableDestination) => {
400 let ignore_group_tokens = self.consume_group();
401 Self::parse_ignore_groups(&ignore_group_tokens);
402 }
403 (Token::OpeningBracket, header_control_word!(FontTable, None)) => {
404 let font_table_tokens = self.consume_group();
405 header.font_table = Self::parse_font_table(&font_table_tokens)?;
406 }
407 (Token::OpeningBracket, header_control_word!(ColorTable, None)) => {
408 let color_table_tokens = self.consume_group();
409 header.color_table = Self::parse_color_table(&color_table_tokens)?;
410 }
411 (Token::OpeningBracket, header_control_word!(StyleSheet, None)) => {
412 let stylesheet_tokens = self.consume_group();
413 header.stylesheet = Self::parse_stylesheet(&stylesheet_tokens)?;
414 }
415 (token, _) => {
417 if let Some(charset) = CharacterSet::from(token) {
418 header.character_set = charset;
419 }
420 self.cursor += 1;
421 }
422 }
423 }
424 return Ok(header);
425 }
426
427 fn parse_font_table(font_tables_tokens: &Vec<Token<'a>>) -> Result<FontTable, ParserError> {
428 let Some(font_table_first_token) = font_tables_tokens.get(0) else {
429 return Err(ParserError::NoMoreToken);
430 };
431 if font_table_first_token != header_control_word!(FontTable, None) {
432 return Err(ParserError::InvalidToken(format!("{:?} is not a FontTable token", font_table_first_token)));
433 }
434 let mut table = HashMap::new();
435 let mut current_key = 0;
436 let mut current_font = Font::default();
437 for token in font_tables_tokens.iter() {
438 match token {
439 Token::ControlSymbol((control_word, property)) => match control_word {
440 ControlWord::FontNumber => {
441 table.insert(current_key, current_font.clone());
443 if let Property::Value(key) = property {
444 current_key = *key as FontRef;
445 } else {
446 return Err(ParserError::InvalidFontIdentifier(*property));
447 }
448 }
449 ControlWord::Unknown(name) => {
450 if let Some(font_family) = FontFamily::from(name) {
451 current_font.font_family = font_family;
452 }
453 }
454 _ => {}
455 },
456 Token::PlainText(name) => {
457 current_font.name = name.trim_end_matches(';').to_string();
458 }
459 Token::ClosingBracket => {
460 table.insert(current_key, current_font.clone());
461 } _ => {}
463 }
464 }
465 return Ok(table);
466 }
467
468 fn parse_color_table(color_table_tokens: &Vec<Token<'a>>) -> Result<ColorTable, ParserError> {
469 let Some(color_table_first_token) = color_table_tokens.get(0) else {
470 return Err(ParserError::NoMoreToken);
471 };
472 if color_table_first_token != header_control_word!(ColorTable, None) {
473 return Err(ParserError::InvalidToken(format!("ParserError: {:?} is not a ColorTable token", color_table_first_token)));
474 }
475 let mut table = HashMap::new();
476 let mut current_key = 1;
477 let mut current_color = Color::default();
478 for token in color_table_tokens.iter() {
479 match token {
480 Token::ControlSymbol((control_word, property)) => match control_word {
481 ControlWord::ColorRed => current_color.red = property.get_value_as::<u8>()?,
482 ControlWord::ColorGreen => current_color.green = property.get_value_as::<u8>()?,
483 ControlWord::ColorBlue => {
484 current_color.blue = property.get_value_as::<u8>()?;
485 table.insert(current_key, current_color.clone());
486 current_key += 1;
487 }
488 _ => {}
489 },
490 _ => {}
491 }
492 }
493 return Ok(table);
494 }
495
496 fn parse_stylesheet(_stylesheet_tokens: &Vec<Token<'a>>) -> Result<StyleSheet, ParserError> {
497 return Ok(StyleSheet::from([]));
499 }
500
501 fn parse_ignore_groups(_tokens: &Vec<Token<'a>>) {
502 }
504}
505
506#[cfg(test)]
507pub mod tests {
508 use super::*;
509 use crate::header::CharacterSet::*;
510 use crate::header::FontFamily::*;
511 use crate::header::RtfHeader;
512 use crate::include_test_file;
513 use crate::lexer::Lexer;
514
515 #[test]
516 fn parser_header() {
517 let tokens = Lexer::scan(r#"{ \rtf1\ansi{\fonttbl\f0\fswiss Helvetica;}\f0\pard Voici du texte en {\b gras}.\par }"#).unwrap();
518 let doc = Parser::new(tokens).parse().unwrap();
519 assert_eq!(
520 doc.header,
521 RtfHeader {
522 character_set: Ansi,
523 font_table: FontTable::from([(
524 0,
525 Font {
526 name: "Helvetica".into(),
527 character_set: 0,
528 font_family: Swiss
529 }
530 )]),
531 ..RtfHeader::default()
532 }
533 );
534 assert_eq!(
535 doc.body,
536 [
537 StyleBlock {
538 painter: Painter::default(),
539 paragraph: Default::default(),
540 text: "Voici du texte en ".into(),
541 },
542 StyleBlock {
543 painter: Painter { bold: true, ..Painter::default() },
544 paragraph: Default::default(),
545 text: "gras".into(),
546 },
547 StyleBlock {
548 painter: Painter::default(),
549 paragraph: Default::default(),
550 text: ".".into(),
551 },
552 ]
553 );
554 }
555
556 #[test]
557 fn parse_multiline_document() {
558 let document = r"{\rtf1\ansi\deff0 {\fonttbl {\f0 Courier;}{\f1 ProFontWindows;}}
559 {\colortbl;\red0\green0\blue0;\red255\green0\blue0;\red255\green255\blue0;}
560 This line is font 0 which is courier\line
561 \f1
562 This line is font 1\line
563 \f0
564 This line is font 0 again\line
565 This line has a \cf2 red \cf1 word\line
566 \highlight3 while this line has a \cf2 red \cf1 word and is highlighted in yellow\highlight0\line
567 Finally, back to the default color.\line
568 }";
569 let tokens = Lexer::scan(document).unwrap();
570 let _doc = Parser::new(tokens).parse().unwrap();
571 }
572
573 #[test]
574 fn parse_entire_file_header() {
575 let file_content = include_test_file!("test-file.rtf");
576 let tokens = Lexer::scan(file_content).unwrap();
577 let doc = Parser::new(tokens).parse().unwrap();
578 assert_eq!(
579 doc.header,
580 RtfHeader {
581 character_set: Ansi,
582 font_table: FontTable::from([
583 (
584 0,
585 Font {
586 name: "Helvetica".into(),
587 character_set: 0,
588 font_family: Swiss,
589 }
590 ),
591 (
592 1,
593 Font {
594 name: "Helvetica-Bold".into(),
595 character_set: 0,
596 font_family: Swiss,
597 }
598 )
599 ]),
600 color_table: ColorTable::from([(1, Color { red: 255, green: 255, blue: 255 }),]),
601 ..RtfHeader::default()
602 }
603 );
604 }
605
606 #[test]
607 fn parse_ignore_group() {
608 let rtf = r"{\*\expandedcolortbl;;}";
609 let tokens = Lexer::scan(rtf).unwrap();
610 let mut parser = Parser::new(tokens);
611 let document = parser.parse().unwrap();
612 assert_eq!(parser.get_tokens(), Vec::<&Token>::new()); assert_eq!(document.header, RtfHeader::default());
614 }
615
616 #[test]
617 fn parse_ignore_group_with_crlf() {
618 let rtf = r"{\
619 \
620 \*\expandedcolortbl;;}";
621 let tokens = Lexer::scan(rtf).unwrap();
622 let mut parser = Parser::new(tokens);
623 let document = parser.parse().unwrap();
624 assert_eq!(parser.get_tokens(), Vec::<&Token>::new()); assert_eq!(document.header, RtfHeader::default());
626 }
627
628 #[test]
629 #[ignore] fn parse_whitespaces() {
631 let file_content = include_test_file!("list-item.rtf");
632 let tokens = Lexer::scan(file_content).unwrap();
633 let mut parser = Parser::new(tokens);
634 let document = parser.parse().unwrap();
635 assert_eq!(
636 document.body,
637 vec![StyleBlock {
638 painter: Painter { font_size: 24, ..Painter::default() },
639 paragraph: Default::default(),
640 text: "\nEmpty start\n\nList test : \n - item 1\n - item 2\n - item 3\n - item 4".into(),
641 },]
642 );
643 }
644
645 #[test]
646 fn parse_image_data() {
647 let rtf_content = include_test_file!("file-with-image.rtf");
649 let tokens = Lexer::scan(rtf_content).unwrap();
650 let _document = Parser::new(tokens).parse();
651 }
652
653 #[test]
654 fn parse_header_and_body() {
655 let rtf = r#"{\rtf1\ansi\ansicpg1252\cocoartf2639
656\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\froman\fcharset0 Times-Bold;\f1\froman\fcharset0 Times-Roman;\f2\froman\fcharset0 Times-Italic;
657\f3\fswiss\fcharset0 Helvetica;}
658{\colortbl;\red255\green255\blue255;\red0\green0\blue10;\red0\green0\blue1;\red191\green191\blue191;
659}
660\f0\b\fs21 \cf2 Lorem ipsum
661\fs56 \
662\pard\pardeftab709\sl288\slmult1\sa225\qj\partightenfactor0
663
664\f1\b0\fs21 \cf0 \
665\pard\pardeftab709\fi-432\ri-1\sb240\sa120\partightenfactor0
666\ls1\ilvl0
667\f0\b\fs36\cf2\plain Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ac faucibus odio. \
668\pard\pardeftab709\sl288\slmult1\sa225\qj\partightenfactor0
669}"#;
670 let tokens = Lexer::scan(rtf).unwrap();
671 let document = Parser::new(tokens).parse().unwrap();
672 assert_eq!(document.body[0].text, "Lorem ipsum");
673 assert_eq!(document.body[1].text, "\n");
674 assert_eq!(document.body[2].text, "\n");
675 assert_eq!(document.body[3].text, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ac faucibus odio. \n");
676 }
677
678 #[test]
679 fn parse_paragraph_aligment() {
680 let rtf = r#"{\rtf1\ansi\deff0 {\fonttbl {\f0 Times;}}
681 \fs34
682 {\pard \qc \fs60 Annalium Romae\par}
683 {\pard \qj
684 Urbem Romam a principio reges habuere; libertatem et
685 \par}
686 {\pard \ql
687 Non Cinnae, non Sullae longa dominatio; et Pompei Crassique potentia
688 \par}"#;
689 let tokens = Lexer::scan(rtf).unwrap();
690 let document = Parser::new(tokens).parse().unwrap();
691 assert_eq!(document.body[0].paragraph.alignment, Alignment::Center);
692 assert_eq!(document.body[1].paragraph.alignment, Alignment::Justify);
693 assert_eq!(document.body[2].paragraph.alignment, Alignment::LeftAligned);
694 }
695
696 #[test]
697 fn should_parse_escaped_char() {
698 let rtf = r"{\rtf1\ansi\deff0 {\fonttbl {\f0 Times;}}je suis une b\'eate}";
699 let tokens = Lexer::scan(rtf).unwrap();
700 let document = Parser::new(tokens).parse().unwrap();
701 assert_eq!(document.body[0].text, "je suis une bête");
702 }
703
704 #[test]
705 fn parse_plain_directive() {
706 let rtf = r"{\rtf1{\fonttbl {\f0 Times;}}\f0\b\fs36\u\cf2\plain Plain text}";
707 let tokens = Lexer::scan(rtf).unwrap();
708 let document = Parser::new(tokens).parse().unwrap();
709 assert_eq!(document.body[0].painter, Painter::default());
710 }
711
712 #[test]
713 fn parse_color_table() {
714 let rtf = r#"{\rtf1\ansi\ansicpg936\cocoartf2761
716 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 PingFangSC-Regular;}
717 {\colortbl;\red255\green255\blue255;\red251\green2\blue7;\red114\green44\blue253;}
718 {\*\expandedcolortbl;;\cssrgb\c100000\c14913\c0;\cssrgb\c52799\c30710\c99498;}
719 \f0\fs24 \cf2 A
720 \f1 \cf3 B}"#;
721 let tokens = Lexer::scan(rtf).unwrap();
722 let document = Parser::new(tokens).parse().unwrap();
723 assert_eq!(document.header.color_table.get(&document.body[0].painter.color_ref).unwrap(), &Color { red: 251, green: 2, blue: 7 });
724 }
725
726 #[test]
727 fn parse_underline() {
728 let rtf = r#"{\rtf1\ansi\ansicpg936\cocoartf2761
731 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
732 {\colortbl;\red255\green255\blue255;}
733 {\*\expandedcolortbl;;}
734 \paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0
735 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
736
737 \f0\fs24 \cf0 \ul \ulc0 a\ulnone A}"#;
738 let tokens = Lexer::scan(rtf).unwrap();
739 let document = Parser::new(tokens).parse().unwrap();
740 assert_eq!(&document.body[0].painter.underline, &true);
741 assert_eq!(&document.body[1].painter.underline, &false);
742 }
743
744 #[test]
745 fn parse_unicode() {
746 let rtf = r#"{\rtf1\ansi\ansicpg936\cocoartf2761
749 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
750 \f0\fs24 \cf0 \uc0\u21834 \u21834 }"#;
751 let tokens = Lexer::scan(rtf).unwrap();
752 let document = Parser::new(tokens).parse().unwrap();
753 assert_eq!(&document.body[0].text, "啊 啊");
754 }
755
756 #[test]
757 fn parse_two_characters_compound_unicode() {
758 let rtf = r#"{\rtf1\ansi
759 \f0 a\u55357 \u56447 1 \u21834}"#;
760 let tokens = Lexer::scan(rtf).unwrap();
761 let document = Parser::new(tokens).parse().unwrap();
762 assert_eq!(&document.body[0].text, "a👿1 啊");
763 }
764
765 #[test]
766 fn parse_unicode_with_fallback() {
767 let rtf = r#"{\rtf1\ansi
769 {\f0 \u-10179\'5f\u-9089\'5f}
770 {\f1 \uc2\u32767\'c2\'52}
771 {\f2 \uc2\u26789\'97\'73}
772 {\f3 b\'eate}
773 {\f4 \uc0 b\'ea\'eate}
774 }"#;
775 let tokens = Lexer::scan(rtf).unwrap();
776 let document = Parser::new(tokens).parse().unwrap();
777 assert_eq!(&document.body[0].text, "👿");
778 assert_eq!(&document.body[1].text, "翿");
779 assert_eq!(&document.body[2].text, "梥");
780 assert_eq!(&document.body[3].text, "bête");
781 assert_eq!(&document.body[4].text, "bêête");
782 }
783
784 #[test]
785 fn body_starts_with_a_group() {
786 let rtf = r"{\rtf1\ansi\deff0{\fonttbl {\f0\fnil\fcharset0 Calibri;}{\f1\fnil\fcharset2 Symbol;}}{\colortbl ;}{\pard \u21435 \sb70\par}}";
787 let tokens = Lexer::scan(rtf).unwrap();
788 let _document = Parser::new(tokens).parse().unwrap();
789 }
790
791 #[test]
792 fn rtf_different_semantic() {
793 let rtf1 = r"{\rtf1 \b bold \i Bold Italic \i0 Bold again}";
794 let rtf2 = r"{\rtf1 \b bold {\i Bold Italic }Bold again}";
795 let rtf3 = r"{\rtf1 \b bold \i Bold Italic \plain\b Bold again}";
796 let doc1 = RtfDocument::try_from(rtf1).unwrap();
797 let doc2 = RtfDocument::try_from(rtf2).unwrap();
798 let doc3 = RtfDocument::try_from(rtf3).unwrap();
799 assert_eq!(doc1.body, doc2.body);
800 assert_eq!(doc3.body, doc2.body);
801 }
802
803 #[test]
804 fn parse_emdash() {
805 let rtf = r"{\rtf1\ansi hello\emdash world}";
806 let doc = RtfDocument::try_from(rtf).unwrap();
807 let text: String = doc.body.iter().map(|b| b.text.as_str()).collect();
808 assert!(text.contains("\u{2014}"), "Em-dash not found in: {}", text);
809 assert!(text.contains("hello\u{2014}world"), "Expected 'hello—world', got: {}", text);
810 }
811
812 #[test]
813 fn parse_endash() {
814 let rtf = r"{\rtf1\ansi 2020\endash 2025}";
815 let doc = RtfDocument::try_from(rtf).unwrap();
816 let text: String = doc.body.iter().map(|b| b.text.as_str()).collect();
817 assert!(text.contains("\u{2013}"), "En-dash not found in: {}", text);
818 }
819
820 #[test]
821 fn parse_smart_quotes() {
822 let rtf = r"{\rtf1\ansi \ldblquote Hello\rdblquote and \lquote hi\rquote}";
823 let doc = RtfDocument::try_from(rtf).unwrap();
824 let text: String = doc.body.iter().map(|b| b.text.as_str()).collect();
825 assert!(text.contains("\u{201C}"), "Left double quote not found");
826 assert!(text.contains("\u{201D}"), "Right double quote not found");
827 assert!(text.contains("\u{2018}"), "Left single quote not found");
828 assert!(text.contains("\u{2019}"), "Right single quote not found");
829 }
830
831 #[test]
832 fn parse_bullet() {
833 let rtf = r"{\rtf1\ansi \bullet Item one}";
834 let doc = RtfDocument::try_from(rtf).unwrap();
835 let text: String = doc.body.iter().map(|b| b.text.as_str()).collect();
836 assert!(text.contains("\u{2022}"), "Bullet not found in: {}", text);
837 }
838
839 #[test]
840 fn parse_tab_and_line() {
841 let rtf = r"{\rtf1\ansi col1\tab col2\line next}";
842 let doc = RtfDocument::try_from(rtf).unwrap();
843 let text: String = doc.body.iter().map(|b| b.text.as_str()).collect();
844 assert!(text.contains("\t"), "Tab not found in: {}", text);
845 assert!(text.contains("\n"), "Line break not found in: {}", text);
846 }
847
848 #[test]
849 fn parse_special_chars_in_scrivener_style() {
850 let rtf = r"{\rtf1\ansi\ansicpg1252\deff0
852{\fonttbl{\f0\fnil\fcharset0 TimesNewRomanPSMT;}}
853\f0\fs24 The transformation in reverse\emdash confident expert to tired father.\par
854He said, \ldblquote Hello.\rdblquote\par}";
855 let doc = RtfDocument::try_from(rtf).unwrap();
856 let text: String = doc.body.iter().map(|b| b.text.as_str()).collect();
857 assert!(text.contains("reverse\u{2014}confident"),
858 "Em-dash not properly placed in: {}", text);
859 assert!(text.contains("\u{201C}Hello.\u{201D}"),
860 "Smart quotes not properly placed in: {}", text);
861 }
862}