1use crate::{InlineContent, ListStyle};
4use frontend::common::parser_tools::content_parser::{ParsedElement, ParsedSpan};
5use frontend::common::parser_tools::fragment_schema::{
6 FragmentBlock, FragmentData, FragmentElement, FragmentTable, FragmentTableCell,
7};
8
9#[derive(Debug, Clone)]
15pub struct DocumentFragment {
16 data: String,
17 plain_text: String,
18}
19
20impl DocumentFragment {
21 pub fn new() -> Self {
23 Self {
24 data: String::new(),
25 plain_text: String::new(),
26 }
27 }
28
29 pub fn from_plain_text(text: &str) -> Self {
34 let blocks: Vec<FragmentBlock> = text
35 .split('\n')
36 .map(|line| FragmentBlock {
37 plain_text: line.to_string(),
38 elements: vec![FragmentElement {
39 content: InlineContent::Text(line.to_string()),
40 fmt_font_family: None,
41 fmt_font_point_size: None,
42 fmt_font_weight: None,
43 fmt_font_bold: None,
44 fmt_font_italic: None,
45 fmt_font_underline: None,
46 fmt_font_overline: None,
47 fmt_font_strikeout: None,
48 fmt_letter_spacing: None,
49 fmt_word_spacing: None,
50 fmt_anchor_href: None,
51 fmt_anchor_names: vec![],
52 fmt_is_anchor: None,
53 fmt_tooltip: None,
54 fmt_underline_style: None,
55 fmt_vertical_alignment: None,
56 }],
57 heading_level: None,
58 list: None,
59 alignment: None,
60 indent: None,
61 text_indent: None,
62 marker: None,
63 top_margin: None,
64 bottom_margin: None,
65 left_margin: None,
66 right_margin: None,
67 tab_positions: vec![],
68 line_height: None,
69 non_breakable_lines: None,
70 direction: None,
71 background_color: None,
72 is_code_block: None,
73 code_language: None,
74 hyphenate: None,
75 language: None,
76 })
77 .collect();
78
79 let data = serde_json::to_string(&FragmentData {
80 blocks,
81 tables: vec![],
82 })
83 .expect("fragment serialization should not fail");
84
85 Self {
86 data,
87 plain_text: text.to_string(),
88 }
89 }
90
91 pub fn from_html(html: &str) -> Self {
93 let parsed = frontend::common::parser_tools::content_parser::parse_html_elements(html);
94 parsed_elements_to_fragment(parsed)
95 }
96
97 pub fn from_markdown(markdown: &str) -> Self {
99 let parsed = frontend::common::parser_tools::content_parser::parse_markdown(markdown);
100 parsed_elements_to_fragment(parsed)
101 }
102
103 pub fn from_djot(djot: &str) -> Self {
107 let parsed = frontend::common::parser_tools::content_parser::parse_djot(
108 djot,
109 &frontend::common::parser_tools::DjotImportOptions::default(),
110 );
111 parsed_elements_to_fragment(parsed)
112 }
113
114 pub fn from_document(doc: &crate::TextDocument) -> crate::Result<Self> {
116 let inner = doc.inner.lock();
117 let dto = frontend::document_inspection::ExtractFragmentDto {
121 position: 0,
122 anchor: i64::MAX,
123 };
124 let result =
125 frontend::commands::document_inspection_commands::extract_fragment(&inner.ctx, &dto)?;
126 Ok(Self::from_raw(result.fragment_data, result.plain_text))
127 }
128
129 pub(crate) fn from_raw(data: String, plain_text: String) -> Self {
131 Self { data, plain_text }
132 }
133
134 pub fn to_plain_text(&self) -> &str {
136 &self.plain_text
137 }
138
139 pub fn to_html(&self) -> String {
141 if self.data.is_empty() {
142 return String::from("<html><head><meta charset=\"utf-8\"></head><body></body></html>");
143 }
144
145 let fragment_data: FragmentData = match serde_json::from_str(&self.data) {
146 Ok(d) => d,
147 Err(_) => {
148 return String::from(
149 "<html><head><meta charset=\"utf-8\"></head><body></body></html>",
150 );
151 }
152 };
153
154 let mut body = String::new();
155 let blocks = &fragment_data.blocks;
156
157 if blocks.len() == 1 && blocks[0].is_inline_only() && fragment_data.tables.is_empty() {
159 push_inline_html(&mut body, &blocks[0].elements);
160 return format!(
161 "<html><head><meta charset=\"utf-8\"></head><body>{}</body></html>",
162 body
163 );
164 }
165
166 let mut sorted_tables: Vec<&FragmentTable> = fragment_data.tables.iter().collect();
168 sorted_tables.sort_by_key(|t| t.block_insert_index);
169 let mut table_cursor = 0;
170
171 let mut i = 0;
172
173 while i < blocks.len() {
174 while table_cursor < sorted_tables.len()
176 && sorted_tables[table_cursor].block_insert_index <= i
177 {
178 push_table_html(&mut body, sorted_tables[table_cursor]);
179 table_cursor += 1;
180 }
181
182 let block = &blocks[i];
183
184 if let Some(ref list) = block.list {
185 let is_ordered = is_ordered_list_style(&list.style);
186 let list_tag = if is_ordered { "ol" } else { "ul" };
187 body.push('<');
188 body.push_str(list_tag);
189 body.push('>');
190
191 while i < blocks.len() {
192 let b = &blocks[i];
193 match &b.list {
194 Some(l) if is_ordered_list_style(&l.style) == is_ordered => {
195 body.push_str("<li>");
196 push_inline_html(&mut body, &b.elements);
197 body.push_str("</li>");
198 i += 1;
199 }
200 _ => break,
201 }
202 }
203
204 body.push_str("</");
205 body.push_str(list_tag);
206 body.push('>');
207 } else if let Some(level) = block.heading_level {
208 let n = level.clamp(1, 6);
209 body.push_str(&format!("<h{}>", n));
210 push_inline_html(&mut body, &block.elements);
211 body.push_str(&format!("</h{}>", n));
212 i += 1;
213 } else {
214 let style = block_style_attr(block);
216 if style.is_empty() {
217 body.push_str("<p>");
218 } else {
219 body.push_str(&format!("<p style=\"{}\">", style));
220 }
221 push_inline_html(&mut body, &block.elements);
222 body.push_str("</p>");
223 i += 1;
224 }
225 }
226
227 while table_cursor < sorted_tables.len() {
229 push_table_html(&mut body, sorted_tables[table_cursor]);
230 table_cursor += 1;
231 }
232
233 format!(
234 "<html><head><meta charset=\"utf-8\"></head><body>{}</body></html>",
235 body
236 )
237 }
238
239 pub fn to_markdown(&self) -> String {
241 if self.data.is_empty() {
242 return String::new();
243 }
244
245 let fragment_data: FragmentData = match serde_json::from_str(&self.data) {
246 Ok(d) => d,
247 Err(_) => return String::new(),
248 };
249
250 let mut parts: Vec<(String, bool)> = Vec::new();
252 let mut prev_was_list = false;
253 let mut list_counter: u32 = 0;
254
255 let mut sorted_tables: Vec<&FragmentTable> = fragment_data.tables.iter().collect();
257 sorted_tables.sort_by_key(|t| t.block_insert_index);
258 let mut table_cursor = 0;
259
260 for (blk_idx, block) in fragment_data.blocks.iter().enumerate() {
261 while table_cursor < sorted_tables.len()
263 && sorted_tables[table_cursor].block_insert_index <= blk_idx
264 {
265 parts.push((render_table_markdown(sorted_tables[table_cursor]), false));
266 prev_was_list = false;
267 list_counter = 0;
268 table_cursor += 1;
269 }
270
271 let inline_text = render_inline_markdown(&block.elements);
272 let is_list = block.list.is_some();
273
274 let indent_prefix = match block.indent {
275 Some(n) if n > 0 => " ".repeat(n as usize),
276 _ => String::new(),
277 };
278
279 if let Some(level) = block.heading_level {
280 let n = level.clamp(1, 6) as usize;
281 let prefix = "#".repeat(n);
282 parts.push((format!("{} {}", prefix, inline_text), false));
283 prev_was_list = false;
284 list_counter = 0;
285 } else if let Some(ref list) = block.list {
286 let is_ordered = is_ordered_list_style(&list.style);
287 if !prev_was_list {
288 list_counter = 0;
289 }
290 if is_ordered {
291 list_counter += 1;
292 parts.push((
293 format!("{}{}. {}", indent_prefix, list_counter, inline_text),
294 true,
295 ));
296 } else {
297 parts.push((format!("{}- {}", indent_prefix, inline_text), true));
298 }
299 prev_was_list = true;
300 } else {
301 if indent_prefix.is_empty() {
302 parts.push((inline_text, false));
303 } else {
304 parts.push((format!("{}{}", indent_prefix, inline_text), false));
305 }
306 prev_was_list = false;
307 list_counter = 0;
308 }
309
310 if !is_list {
311 prev_was_list = false;
312 }
313 }
314
315 while table_cursor < sorted_tables.len() {
317 parts.push((render_table_markdown(sorted_tables[table_cursor]), false));
318 table_cursor += 1;
319 }
320
321 let mut result = String::new();
323 for (idx, (text, is_list)) in parts.iter().enumerate() {
324 if idx > 0 {
325 let (_, prev_is_list) = &parts[idx - 1];
326 if *prev_is_list && *is_list {
327 result.push('\n');
328 } else {
329 result.push_str("\n\n");
330 }
331 }
332 result.push_str(text);
333 }
334
335 result
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.plain_text.is_empty()
341 }
342
343 pub(crate) fn raw_data(&self) -> &str {
345 &self.data
346 }
347}
348
349impl Default for DocumentFragment {
350 fn default() -> Self {
351 Self::new()
352 }
353}
354
355fn is_ordered_list_style(style: &ListStyle) -> bool {
360 matches!(
361 style,
362 ListStyle::Decimal
363 | ListStyle::LowerAlpha
364 | ListStyle::UpperAlpha
365 | ListStyle::LowerRoman
366 | ListStyle::UpperRoman
367 )
368}
369
370fn escape_html(s: &str) -> String {
373 let mut out = String::with_capacity(s.len());
374 for c in s.chars() {
375 match c {
376 '&' => out.push_str("&"),
377 '<' => out.push_str("<"),
378 '>' => out.push_str(">"),
379 '"' => out.push_str("""),
380 '\'' => out.push_str("'"),
381 '\r' => out.push_str(" "),
386 _ => out.push(c),
387 }
388 }
389 out
390}
391
392fn block_style_attr(block: &FragmentBlock) -> String {
394 use crate::Alignment;
395
396 let mut parts = Vec::new();
397 if let Some(ref alignment) = block.alignment {
398 let value = match alignment {
399 Alignment::Left => "left",
400 Alignment::Right => "right",
401 Alignment::Center => "center",
402 Alignment::Justify => "justify",
403 };
404 parts.push(format!("text-align: {}", value));
405 }
406 if let Some(n) = block.indent
407 && n > 0
408 {
409 parts.push(format!("margin-left: {}em", n));
410 }
411 if let Some(px) = block.text_indent
412 && px != 0
413 {
414 parts.push(format!("text-indent: {}px", px));
415 }
416 if let Some(px) = block.top_margin {
417 parts.push(format!("margin-top: {}px", px));
418 }
419 if let Some(px) = block.bottom_margin {
420 parts.push(format!("margin-bottom: {}px", px));
421 }
422 if let Some(px) = block.left_margin {
423 parts.push(format!("margin-left: {}px", px));
424 }
425 if let Some(px) = block.right_margin {
426 parts.push(format!("margin-right: {}px", px));
427 }
428 parts.join("; ")
429}
430
431fn push_inline_html(out: &mut String, elements: &[FragmentElement]) {
432 for elem in elements {
433 let text = match &elem.content {
434 InlineContent::Text(t) => escape_html(t),
435 InlineContent::Image {
436 name,
437 width,
438 height,
439 ..
440 } => {
441 format!(
442 "<img src=\"{}\" width=\"{}\" height=\"{}\">",
443 escape_html(name),
444 width,
445 height
446 )
447 }
448 InlineContent::Empty => String::new(),
449 };
450
451 let is_monospace = elem
452 .fmt_font_family
453 .as_deref()
454 .is_some_and(|f| f == "monospace");
455 let is_bold = elem.fmt_font_bold.unwrap_or(false);
456 let is_italic = elem.fmt_font_italic.unwrap_or(false);
457 let is_underline = elem.fmt_font_underline.unwrap_or(false);
458 let is_strikeout = elem.fmt_font_strikeout.unwrap_or(false);
459 let is_anchor = elem.fmt_is_anchor.unwrap_or(false);
460
461 let mut result = text;
462
463 if is_monospace {
464 result = format!("<code>{}</code>", result);
465 }
466 if is_bold {
467 result = format!("<strong>{}</strong>", result);
468 }
469 if is_italic {
470 result = format!("<em>{}</em>", result);
471 }
472 if is_underline {
473 result = format!("<u>{}</u>", result);
474 }
475 if is_strikeout {
476 result = format!("<s>{}</s>", result);
477 }
478 if is_anchor && let Some(ref href) = elem.fmt_anchor_href {
479 result = format!("<a href=\"{}\">{}</a>", escape_html(href), result);
480 }
481
482 out.push_str(&result);
483 }
484}
485
486fn push_table_html(out: &mut String, table: &FragmentTable) {
488 out.push_str("<table>");
489 for row in 0..table.rows {
490 out.push_str("<tr>");
491 for col in 0..table.columns {
492 if let Some(cell) = table.cells.iter().find(|c| c.row == row && c.column == col) {
493 out.push_str("<td");
494 if cell.row_span > 1 {
495 out.push_str(&format!(" rowspan=\"{}\"", cell.row_span));
496 }
497 if cell.column_span > 1 {
498 out.push_str(&format!(" colspan=\"{}\"", cell.column_span));
499 }
500 out.push('>');
501 for (i, block) in cell.blocks.iter().enumerate() {
502 if i > 0 {
503 out.push_str("<br>");
504 }
505 push_inline_html(out, &block.elements);
506 }
507 out.push_str("</td>");
508 }
509 }
511 out.push_str("</tr>");
512 }
513 out.push_str("</table>");
514}
515
516fn escape_markdown(s: &str) -> String {
519 let mut out = String::with_capacity(s.len());
520 for c in s.chars() {
521 if matches!(
522 c,
523 '\\' | '`'
524 | '*'
525 | '_'
526 | '{'
527 | '}'
528 | '['
529 | ']'
530 | '('
531 | ')'
532 | '#'
533 | '+'
534 | '-'
535 | '.'
536 | '!'
537 | '|'
538 | '~'
539 | '<'
540 | '>'
541 ) {
542 out.push('\\');
543 }
544 out.push(c);
545 }
546 out
547}
548
549fn render_inline_markdown(elements: &[FragmentElement]) -> String {
550 let mut out = String::new();
551 for elem in elements {
552 let raw_text = match &elem.content {
553 InlineContent::Text(t) => t.clone(),
554 InlineContent::Image { name, .. } => format!("", name, name),
555 InlineContent::Empty => String::new(),
556 };
557
558 let is_monospace = elem
559 .fmt_font_family
560 .as_deref()
561 .is_some_and(|f| f == "monospace");
562 let is_bold = elem.fmt_font_bold.unwrap_or(false);
563 let is_italic = elem.fmt_font_italic.unwrap_or(false);
564 let is_strikeout = elem.fmt_font_strikeout.unwrap_or(false);
565 let is_anchor = elem.fmt_is_anchor.unwrap_or(false);
566
567 if is_monospace {
568 out.push('`');
569 out.push_str(&raw_text);
570 out.push('`');
571 } else {
572 let mut text = escape_markdown(&raw_text);
573 if is_bold && is_italic {
574 text = format!("***{}***", text);
575 } else if is_bold {
576 text = format!("**{}**", text);
577 } else if is_italic {
578 text = format!("*{}*", text);
579 }
580 if is_strikeout {
581 text = format!("~~{}~~", text);
582 }
583 if is_anchor {
584 let href = elem.fmt_anchor_href.as_deref().unwrap_or("");
585 out.push_str(&format!("[{}]({})", text, href));
586 } else {
587 out.push_str(&text);
588 }
589 }
590 }
591 out
592}
593
594fn render_table_markdown(table: &FragmentTable) -> String {
596 let mut rows: Vec<Vec<String>> = vec![vec![String::new(); table.columns]; table.rows];
597
598 for cell in &table.cells {
599 let text: String = cell
600 .blocks
601 .iter()
602 .map(|b| render_inline_markdown(&b.elements))
603 .collect::<Vec<_>>()
604 .join(" ");
605 if cell.row < table.rows && cell.column < table.columns {
606 rows[cell.row][cell.column] = text;
607 }
608 }
609
610 let mut out = String::new();
611 for (i, row) in rows.iter().enumerate() {
612 out.push_str("| ");
613 out.push_str(&row.join(" | "));
614 out.push_str(" |");
615 if i == 0 {
616 out.push('\n');
618 out.push('|');
619 for _ in 0..table.columns {
620 out.push_str(" --- |");
621 }
622 }
623 if i + 1 < rows.len() {
624 out.push('\n');
625 }
626 }
627 out
628}
629
630fn span_to_fragment_element(span: &ParsedSpan) -> FragmentElement {
635 let content = InlineContent::Text(span.text.clone());
636 let fmt_font_family = if span.code {
637 Some("monospace".into())
638 } else {
639 None
640 };
641 let fmt_font_bold = if span.bold { Some(true) } else { None };
642 let fmt_font_italic = if span.italic { Some(true) } else { None };
643 let fmt_font_underline = if span.underline { Some(true) } else { None };
644 let fmt_font_strikeout = if span.strikeout { Some(true) } else { None };
645 let (fmt_anchor_href, fmt_is_anchor) = if let Some(ref href) = span.link_href {
646 (Some(href.clone()), Some(true))
647 } else {
648 (None, None)
649 };
650
651 FragmentElement {
652 content,
653 fmt_font_family,
654 fmt_font_point_size: None,
655 fmt_font_weight: None,
656 fmt_font_bold,
657 fmt_font_italic,
658 fmt_font_underline,
659 fmt_font_overline: None,
660 fmt_font_strikeout,
661 fmt_letter_spacing: None,
662 fmt_word_spacing: None,
663 fmt_anchor_href,
664 fmt_anchor_names: vec![],
665 fmt_is_anchor,
666 fmt_tooltip: None,
667 fmt_underline_style: None,
668 fmt_vertical_alignment: None,
669 }
670}
671
672fn parsed_elements_to_fragment(parsed: Vec<ParsedElement>) -> DocumentFragment {
675 use frontend::common::parser_tools::fragment_schema::FragmentList;
676
677 let mut blocks: Vec<FragmentBlock> = Vec::new();
678 let mut tables: Vec<FragmentTable> = Vec::new();
679
680 for elem in parsed {
681 match elem {
682 ParsedElement::Block(pb) => {
683 let elements: Vec<FragmentElement> =
684 pb.spans.iter().map(span_to_fragment_element).collect();
685 let plain_text: String = pb.spans.iter().map(|s| s.text.as_str()).collect();
686 let list = pb.list_style.map(|style| FragmentList {
687 style,
688 indent: pb.list_indent as i64,
689 prefix: String::new(),
690 suffix: String::new(),
691 });
692
693 blocks.push(FragmentBlock {
694 plain_text,
695 elements,
696 heading_level: pb.heading_level,
697 list,
698 alignment: None,
699 indent: None,
700 text_indent: None,
701 marker: None,
702 top_margin: None,
703 bottom_margin: None,
704 left_margin: None,
705 right_margin: None,
706 tab_positions: vec![],
707 line_height: pb.line_height,
708 non_breakable_lines: pb.non_breakable_lines,
709 direction: pb.direction,
710 background_color: pb.background_color,
711 is_code_block: None,
712 code_language: None,
713 hyphenate: None,
714 language: None,
715 });
716 }
717 ParsedElement::Table(pt) => {
718 let block_insert_index = blocks.len();
719 let num_columns = pt.rows.iter().map(|r| r.len()).max().unwrap_or(0);
720 let num_rows = pt.rows.len();
721
722 let mut frag_cells: Vec<FragmentTableCell> = Vec::new();
723 for (row_idx, row) in pt.rows.iter().enumerate() {
724 for (col_idx, cell) in row.iter().enumerate() {
725 let cell_elements: Vec<FragmentElement> =
726 cell.spans.iter().map(span_to_fragment_element).collect();
727 let cell_text: String =
728 cell.spans.iter().map(|s| s.text.as_str()).collect();
729
730 frag_cells.push(FragmentTableCell {
731 row: row_idx,
732 column: col_idx,
733 row_span: 1,
734 column_span: 1,
735 blocks: vec![FragmentBlock {
736 plain_text: cell_text,
737 elements: cell_elements,
738 heading_level: None,
739 list: None,
740 alignment: None,
741 indent: None,
742 text_indent: None,
743 marker: None,
744 top_margin: None,
745 bottom_margin: None,
746 left_margin: None,
747 right_margin: None,
748 tab_positions: vec![],
749 line_height: None,
750 non_breakable_lines: None,
751 direction: None,
752 background_color: None,
753 is_code_block: None,
754 code_language: None,
755 hyphenate: None,
756 language: None,
757 }],
758 fmt_padding: None,
759 fmt_border: None,
760 fmt_vertical_alignment: None,
761 fmt_background_color: None,
762 });
763 }
764 }
765
766 tables.push(FragmentTable {
767 rows: num_rows,
768 columns: num_columns,
769 cells: frag_cells,
770 block_insert_index,
771 fmt_border: None,
772 fmt_cell_spacing: None,
773 fmt_cell_padding: None,
774 fmt_width: None,
775 fmt_alignment: None,
776 column_widths: vec![],
777 });
778 }
779 }
780 }
781
782 let data = serde_json::to_string(&FragmentData { blocks, tables })
783 .expect("fragment serialization should not fail");
784
785 let plain_text = parsed_plain_text_from_data(&data);
786
787 DocumentFragment { data, plain_text }
788}
789
790fn parsed_plain_text_from_data(data: &str) -> String {
792 let fragment_data: FragmentData = match serde_json::from_str(data) {
793 Ok(d) => d,
794 Err(_) => return String::new(),
795 };
796
797 fragment_data
798 .blocks
799 .iter()
800 .map(|b| b.plain_text.as_str())
801 .collect::<Vec<_>>()
802 .join("\n")
803}