1use crate::{CharVerticalAlignment, 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 page_break_before: None,
71 direction: None,
72 background_color: None,
73 is_code_block: None,
74 code_language: None,
75 hyphenate: None,
76 language: None,
77 })
78 .collect();
79
80 let data = serde_json::to_string(&FragmentData {
81 blocks,
82 tables: vec![],
83 })
84 .expect("fragment serialization should not fail");
85
86 Self {
87 data,
88 plain_text: text.to_string(),
89 }
90 }
91
92 pub fn from_html(html: &str) -> Self {
94 let parsed = frontend::common::parser_tools::content_parser::parse_html_elements(html);
95 parsed_elements_to_fragment(parsed)
96 }
97
98 pub fn from_markdown(markdown: &str) -> Self {
100 let parsed = frontend::common::parser_tools::content_parser::parse_markdown(markdown);
101 parsed_elements_to_fragment(parsed)
102 }
103
104 pub fn from_djot(djot: &str) -> Self {
108 let parsed = frontend::common::parser_tools::content_parser::parse_djot(
109 djot,
110 &frontend::common::parser_tools::DjotImportOptions::default(),
111 );
112 parsed_elements_to_fragment(parsed)
113 }
114
115 pub fn from_document(doc: &crate::TextDocument) -> crate::Result<Self> {
117 let inner = doc.inner.lock();
118 let dto = frontend::document_inspection::ExtractFragmentDto {
122 position: 0,
123 anchor: i64::MAX,
124 };
125 let result =
126 frontend::commands::document_inspection_commands::extract_fragment(&inner.ctx, &dto)?;
127 Ok(Self::from_raw(result.fragment_data, result.plain_text))
128 }
129
130 pub(crate) fn from_raw(data: String, plain_text: String) -> Self {
132 Self { data, plain_text }
133 }
134
135 pub fn to_plain_text(&self) -> &str {
137 &self.plain_text
138 }
139
140 pub fn to_html(&self) -> String {
142 if self.data.is_empty() {
143 return String::from("<html><head><meta charset=\"utf-8\"></head><body></body></html>");
144 }
145
146 let fragment_data: FragmentData = match serde_json::from_str(&self.data) {
147 Ok(d) => d,
148 Err(_) => {
149 return String::from(
150 "<html><head><meta charset=\"utf-8\"></head><body></body></html>",
151 );
152 }
153 };
154
155 let mut body = String::new();
156 let blocks = &fragment_data.blocks;
157
158 if blocks.len() == 1 && blocks[0].is_inline_only() && fragment_data.tables.is_empty() {
160 push_inline_html(&mut body, &blocks[0].elements);
161 return format!(
162 "<html><head><meta charset=\"utf-8\"></head><body>{}</body></html>",
163 body
164 );
165 }
166
167 let mut sorted_tables: Vec<&FragmentTable> = fragment_data.tables.iter().collect();
169 sorted_tables.sort_by_key(|t| t.block_insert_index);
170 let mut table_cursor = 0;
171
172 let mut i = 0;
173
174 while i < blocks.len() {
175 while table_cursor < sorted_tables.len()
177 && sorted_tables[table_cursor].block_insert_index <= i
178 {
179 push_table_html(&mut body, sorted_tables[table_cursor]);
180 table_cursor += 1;
181 }
182
183 let block = &blocks[i];
184
185 if let Some(ref list) = block.list {
186 let is_ordered = is_ordered_list_style(&list.style);
187 let list_tag = if is_ordered { "ol" } else { "ul" };
188 body.push('<');
189 body.push_str(list_tag);
190 body.push('>');
191
192 while i < blocks.len() {
193 let b = &blocks[i];
194 match &b.list {
195 Some(l) if is_ordered_list_style(&l.style) == is_ordered => {
196 body.push_str("<li>");
197 push_inline_html(&mut body, &b.elements);
198 body.push_str("</li>");
199 i += 1;
200 }
201 _ => break,
202 }
203 }
204
205 body.push_str("</");
206 body.push_str(list_tag);
207 body.push('>');
208 } else if let Some(level) = block.heading_level {
209 let n = level.clamp(1, 6);
210 body.push_str(&format!("<h{}>", n));
211 push_inline_html(&mut body, &block.elements);
212 body.push_str(&format!("</h{}>", n));
213 i += 1;
214 } else {
215 let style = block_style_attr(block);
217 if style.is_empty() {
218 body.push_str("<p>");
219 } else {
220 body.push_str(&format!("<p style=\"{}\">", style));
221 }
222 push_inline_html(&mut body, &block.elements);
223 body.push_str("</p>");
224 i += 1;
225 }
226 }
227
228 while table_cursor < sorted_tables.len() {
230 push_table_html(&mut body, sorted_tables[table_cursor]);
231 table_cursor += 1;
232 }
233
234 format!(
235 "<html><head><meta charset=\"utf-8\"></head><body>{}</body></html>",
236 body
237 )
238 }
239
240 pub fn to_markdown(&self) -> String {
242 if self.data.is_empty() {
243 return String::new();
244 }
245
246 let fragment_data: FragmentData = match serde_json::from_str(&self.data) {
247 Ok(d) => d,
248 Err(_) => return String::new(),
249 };
250
251 let mut parts: Vec<(String, bool)> = Vec::new();
253 let mut prev_was_list = false;
254 let mut list_counter: u32 = 0;
255
256 let mut sorted_tables: Vec<&FragmentTable> = fragment_data.tables.iter().collect();
258 sorted_tables.sort_by_key(|t| t.block_insert_index);
259 let mut table_cursor = 0;
260
261 for (blk_idx, block) in fragment_data.blocks.iter().enumerate() {
262 while table_cursor < sorted_tables.len()
264 && sorted_tables[table_cursor].block_insert_index <= blk_idx
265 {
266 parts.push((render_table_markdown(sorted_tables[table_cursor]), false));
267 prev_was_list = false;
268 list_counter = 0;
269 table_cursor += 1;
270 }
271
272 let inline_text = render_inline_markdown(&block.elements);
273 let is_list = block.list.is_some();
274
275 let indent_prefix = match block.indent {
276 Some(n) if n > 0 => " ".repeat(n as usize),
277 _ => String::new(),
278 };
279
280 if let Some(level) = block.heading_level {
281 let n = level.clamp(1, 6) as usize;
282 let prefix = "#".repeat(n);
283 parts.push((format!("{} {}", prefix, inline_text), false));
284 prev_was_list = false;
285 list_counter = 0;
286 } else if let Some(ref list) = block.list {
287 let is_ordered = is_ordered_list_style(&list.style);
288 if !prev_was_list {
289 list_counter = 0;
290 }
291 if is_ordered {
292 list_counter += 1;
293 parts.push((
294 format!("{}{}. {}", indent_prefix, list_counter, inline_text),
295 true,
296 ));
297 } else {
298 parts.push((format!("{}- {}", indent_prefix, inline_text), true));
299 }
300 prev_was_list = true;
301 } else {
302 if indent_prefix.is_empty() {
303 parts.push((inline_text, false));
304 } else {
305 parts.push((format!("{}{}", indent_prefix, inline_text), false));
306 }
307 prev_was_list = false;
308 list_counter = 0;
309 }
310
311 if !is_list {
312 prev_was_list = false;
313 }
314 }
315
316 while table_cursor < sorted_tables.len() {
318 parts.push((render_table_markdown(sorted_tables[table_cursor]), false));
319 table_cursor += 1;
320 }
321
322 let mut result = String::new();
324 for (idx, (text, is_list)) in parts.iter().enumerate() {
325 if idx > 0 {
326 let (_, prev_is_list) = &parts[idx - 1];
327 if *prev_is_list && *is_list {
328 result.push('\n');
329 } else {
330 result.push_str("\n\n");
331 }
332 }
333 result.push_str(text);
334 }
335
336 result
337 }
338
339 pub fn is_empty(&self) -> bool {
341 self.plain_text.is_empty()
342 }
343
344 pub(crate) fn raw_data(&self) -> &str {
346 &self.data
347 }
348}
349
350impl Default for DocumentFragment {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356fn is_ordered_list_style(style: &ListStyle) -> bool {
361 matches!(
362 style,
363 ListStyle::Decimal
364 | ListStyle::LowerAlpha
365 | ListStyle::UpperAlpha
366 | ListStyle::LowerRoman
367 | ListStyle::UpperRoman
368 )
369}
370
371fn escape_html(s: &str) -> String {
374 let mut out = String::with_capacity(s.len());
375 for c in s.chars() {
376 match c {
377 '&' => out.push_str("&"),
378 '<' => out.push_str("<"),
379 '>' => out.push_str(">"),
380 '"' => out.push_str("""),
381 '\'' => out.push_str("'"),
382 '\r' => out.push_str(" "),
387 _ => out.push(c),
388 }
389 }
390 out
391}
392
393fn block_style_attr(block: &FragmentBlock) -> String {
395 use crate::Alignment;
396
397 let mut parts = Vec::new();
398 if let Some(ref alignment) = block.alignment {
399 let value = match alignment {
400 Alignment::Left => "left",
401 Alignment::Right => "right",
402 Alignment::Center => "center",
403 Alignment::Justify => "justify",
404 };
405 parts.push(format!("text-align: {}", value));
406 }
407 if let Some(n) = block.indent
408 && n > 0
409 {
410 parts.push(format!("margin-left: {}em", n));
411 }
412 if let Some(px) = block.text_indent
413 && px != 0
414 {
415 parts.push(format!("text-indent: {}px", px));
416 }
417 if let Some(px) = block.top_margin {
418 parts.push(format!("margin-top: {}px", px));
419 }
420 if let Some(px) = block.bottom_margin {
421 parts.push(format!("margin-bottom: {}px", px));
422 }
423 if let Some(px) = block.left_margin {
424 parts.push(format!("margin-left: {}px", px));
425 }
426 if let Some(px) = block.right_margin {
427 parts.push(format!("margin-right: {}px", px));
428 }
429 parts.join("; ")
430}
431
432fn push_inline_html(out: &mut String, elements: &[FragmentElement]) {
433 for elem in elements {
434 let text = match &elem.content {
435 InlineContent::Text(t) => escape_html(t),
436 InlineContent::FootnoteRef { label } => {
439 let id = escape_html(label);
440 out.push_str(&format!(
441 "<a epub:type=\"noteref\" role=\"doc-noteref\" href=\"#fn-{id}\"><sup>{id}</sup></a>"
442 ));
443 continue;
444 }
445 InlineContent::Image {
446 name,
447 alt,
448 width,
449 height,
450 ..
451 } => {
452 let mut tag = format!(
457 "<img src=\"{}\" alt=\"{}\"",
458 escape_html(name),
459 escape_html(alt)
460 );
461 if *width > 0 {
462 tag.push_str(&format!(" width=\"{width}\""));
463 }
464 if *height > 0 {
465 tag.push_str(&format!(" height=\"{height}\""));
466 }
467 tag.push('>');
468 tag
469 }
470 InlineContent::Empty => String::new(),
471 };
472
473 let is_monospace = elem
474 .fmt_font_family
475 .as_deref()
476 .is_some_and(|f| f == "monospace");
477 let is_bold = elem.fmt_font_bold.unwrap_or(false);
478 let is_italic = elem.fmt_font_italic.unwrap_or(false);
479 let is_underline = elem.fmt_font_underline.unwrap_or(false);
480 let is_strikeout = elem.fmt_font_strikeout.unwrap_or(false);
481 let is_anchor = elem.fmt_is_anchor.unwrap_or(false);
482 let vertical = elem.fmt_vertical_alignment.as_ref();
487
488 let mut result = text;
489
490 if is_monospace {
491 result = format!("<code>{}</code>", result);
492 }
493 if is_bold {
494 result = format!("<strong>{}</strong>", result);
495 }
496 if is_italic {
497 result = format!("<em>{}</em>", result);
498 }
499 if is_underline {
500 result = format!("<u>{}</u>", result);
501 }
502 if is_strikeout {
503 result = format!("<s>{}</s>", result);
504 }
505 match vertical {
506 Some(CharVerticalAlignment::SuperScript) => result = format!("<sup>{result}</sup>"),
507 Some(CharVerticalAlignment::SubScript) => result = format!("<sub>{result}</sub>"),
508 _ => {}
509 }
510 if is_anchor && let Some(ref href) = elem.fmt_anchor_href {
511 result = format!("<a href=\"{}\">{}</a>", escape_html(href), result);
512 }
513
514 out.push_str(&result);
515 }
516}
517
518fn push_table_html(out: &mut String, table: &FragmentTable) {
520 out.push_str("<table>");
521 for row in 0..table.rows {
522 out.push_str("<tr>");
523 for col in 0..table.columns {
524 if let Some(cell) = table.cells.iter().find(|c| c.row == row && c.column == col) {
525 out.push_str("<td");
526 if cell.row_span > 1 {
527 out.push_str(&format!(" rowspan=\"{}\"", cell.row_span));
528 }
529 if cell.column_span > 1 {
530 out.push_str(&format!(" colspan=\"{}\"", cell.column_span));
531 }
532 out.push('>');
533 for (i, block) in cell.blocks.iter().enumerate() {
534 if i > 0 {
535 out.push_str("<br>");
536 }
537 push_inline_html(out, &block.elements);
538 }
539 out.push_str("</td>");
540 }
541 }
543 out.push_str("</tr>");
544 }
545 out.push_str("</table>");
546}
547
548fn escape_markdown(s: &str) -> String {
551 let mut out = String::with_capacity(s.len());
552 for c in s.chars() {
553 if matches!(
554 c,
555 '\\' | '`'
556 | '*'
557 | '_'
558 | '{'
559 | '}'
560 | '['
561 | ']'
562 | '('
563 | ')'
564 | '#'
565 | '+'
566 | '-'
567 | '.'
568 | '!'
569 | '|'
570 | '~'
571 | '<'
572 | '>'
573 ) {
574 out.push('\\');
575 }
576 out.push(c);
577 }
578 out
579}
580
581fn render_inline_markdown(elements: &[FragmentElement]) -> String {
582 let mut out = String::new();
583 for elem in elements {
584 let raw_text = match &elem.content {
585 InlineContent::Text(t) => t.clone(),
586 InlineContent::Image { name, alt, .. } => format!(""),
589 InlineContent::FootnoteRef { label } => format!("[^{label}]"),
590 InlineContent::Empty => String::new(),
591 };
592
593 let is_monospace = elem
594 .fmt_font_family
595 .as_deref()
596 .is_some_and(|f| f == "monospace");
597 let is_bold = elem.fmt_font_bold.unwrap_or(false);
598 let is_italic = elem.fmt_font_italic.unwrap_or(false);
599 let is_strikeout = elem.fmt_font_strikeout.unwrap_or(false);
600 let is_anchor = elem.fmt_is_anchor.unwrap_or(false);
601
602 if is_monospace {
603 out.push('`');
604 out.push_str(&raw_text);
605 out.push('`');
606 } else {
607 let mut text = escape_markdown(&raw_text);
608 if is_bold && is_italic {
609 text = format!("***{}***", text);
610 } else if is_bold {
611 text = format!("**{}**", text);
612 } else if is_italic {
613 text = format!("*{}*", text);
614 }
615 if is_strikeout {
616 text = format!("~~{}~~", text);
617 }
618 if is_anchor {
619 let href = elem.fmt_anchor_href.as_deref().unwrap_or("");
620 out.push_str(&format!("[{}]({})", text, href));
621 } else {
622 out.push_str(&text);
623 }
624 }
625 }
626 out
627}
628
629fn render_table_markdown(table: &FragmentTable) -> String {
631 let mut rows: Vec<Vec<String>> = vec![vec![String::new(); table.columns]; table.rows];
632
633 for cell in &table.cells {
634 let text: String = cell
635 .blocks
636 .iter()
637 .map(|b| render_inline_markdown(&b.elements))
638 .collect::<Vec<_>>()
639 .join(" ");
640 if cell.row < table.rows && cell.column < table.columns {
641 rows[cell.row][cell.column] = text;
642 }
643 }
644
645 let mut out = String::new();
646 for (i, row) in rows.iter().enumerate() {
647 out.push_str("| ");
648 out.push_str(&row.join(" | "));
649 out.push_str(" |");
650 if i == 0 {
651 out.push('\n');
653 out.push('|');
654 for _ in 0..table.columns {
655 out.push_str(" --- |");
656 }
657 }
658 if i + 1 < rows.len() {
659 out.push('\n');
660 }
661 }
662 out
663}
664
665fn spans_plain_text(spans: &[ParsedSpan]) -> String {
677 spans
678 .iter()
679 .map(|s| {
680 if s.image.is_some() || s.footnote_ref.is_some() {
685 "\u{FFFC}"
686 } else {
687 s.text.as_str()
688 }
689 })
690 .collect()
691}
692
693fn span_to_fragment_element(span: &ParsedSpan) -> FragmentElement {
694 let content = match (&span.footnote_ref, &span.image) {
703 (Some(label), _) => InlineContent::FootnoteRef {
704 label: label.clone(),
705 },
706 (None, Some(img)) => InlineContent::Image {
707 name: img.src.clone(),
708 alt: img.alt.clone(),
709 width: img.width,
710 height: img.height,
711 quality: 100,
714 },
715 (None, None) => InlineContent::Text(span.text.clone()),
716 };
717 let fmt_font_family = if span.code {
718 Some("monospace".into())
719 } else {
720 None
721 };
722 let fmt_font_bold = if span.bold { Some(true) } else { None };
723 let fmt_font_italic = if span.italic { Some(true) } else { None };
724 let fmt_font_underline = if span.underline { Some(true) } else { None };
725 let fmt_font_strikeout = if span.strikeout { Some(true) } else { None };
726 let (fmt_anchor_href, fmt_is_anchor) = if let Some(ref href) = span.link_href {
727 (Some(href.clone()), Some(true))
728 } else {
729 (None, None)
730 };
731 let fmt_vertical_alignment = if span.superscript {
737 Some(CharVerticalAlignment::SuperScript)
738 } else if span.subscript {
739 Some(CharVerticalAlignment::SubScript)
740 } else {
741 None
742 };
743
744 FragmentElement {
745 content,
746 fmt_font_family,
747 fmt_font_point_size: None,
748 fmt_font_weight: None,
749 fmt_font_bold,
750 fmt_font_italic,
751 fmt_font_underline,
752 fmt_font_overline: None,
753 fmt_font_strikeout,
754 fmt_letter_spacing: None,
755 fmt_word_spacing: None,
756 fmt_anchor_href,
757 fmt_anchor_names: vec![],
758 fmt_is_anchor,
759 fmt_tooltip: None,
760 fmt_underline_style: None,
761 fmt_vertical_alignment,
762 }
763}
764
765fn parsed_elements_to_fragment(parsed: Vec<ParsedElement>) -> DocumentFragment {
768 use frontend::common::parser_tools::fragment_schema::FragmentList;
769
770 let mut blocks: Vec<FragmentBlock> = Vec::new();
771 let mut tables: Vec<FragmentTable> = Vec::new();
772
773 for elem in parsed {
774 match elem {
775 ParsedElement::FootnoteDefinition { .. } => {}
780 ParsedElement::Block(pb) => {
781 let elements: Vec<FragmentElement> =
782 pb.spans.iter().map(span_to_fragment_element).collect();
783 let plain_text: String = spans_plain_text(&pb.spans);
784 let list = pb.list_style.map(|style| FragmentList {
785 style,
786 indent: pb.list_indent as i64,
787 prefix: String::new(),
788 suffix: String::new(),
789 });
790
791 blocks.push(FragmentBlock {
792 plain_text,
793 elements,
794 heading_level: pb.heading_level,
795 list,
796 alignment: None,
797 indent: None,
798 text_indent: None,
799 marker: None,
800 top_margin: None,
801 bottom_margin: None,
802 left_margin: None,
803 right_margin: None,
804 tab_positions: vec![],
805 line_height: pb.line_height,
806 non_breakable_lines: pb.non_breakable_lines,
807 page_break_before: pb.page_break_before,
808 direction: pb.direction,
809 background_color: pb.background_color,
810 is_code_block: None,
811 code_language: None,
812 hyphenate: None,
813 language: None,
814 });
815 }
816 ParsedElement::Table(pt) => {
817 let block_insert_index = blocks.len();
818 let num_columns = pt.rows.iter().map(|r| r.len()).max().unwrap_or(0);
819 let num_rows = pt.rows.len();
820
821 let mut frag_cells: Vec<FragmentTableCell> = Vec::new();
822 for (row_idx, row) in pt.rows.iter().enumerate() {
823 for (col_idx, cell) in row.iter().enumerate() {
824 let cell_elements: Vec<FragmentElement> =
825 cell.spans.iter().map(span_to_fragment_element).collect();
826 let cell_text: String = spans_plain_text(&cell.spans);
827
828 frag_cells.push(FragmentTableCell {
829 row: row_idx,
830 column: col_idx,
831 row_span: 1,
832 column_span: 1,
833 blocks: vec![FragmentBlock {
834 plain_text: cell_text,
835 elements: cell_elements,
836 heading_level: None,
837 list: None,
838 alignment: None,
839 indent: None,
840 text_indent: None,
841 marker: None,
842 top_margin: None,
843 bottom_margin: None,
844 left_margin: None,
845 right_margin: None,
846 tab_positions: vec![],
847 line_height: None,
848 non_breakable_lines: None,
849 page_break_before: None,
850 direction: None,
851 background_color: None,
852 is_code_block: None,
853 code_language: None,
854 hyphenate: None,
855 language: None,
856 }],
857 fmt_padding: None,
858 fmt_border: None,
859 fmt_vertical_alignment: None,
860 fmt_background_color: None,
861 });
862 }
863 }
864
865 tables.push(FragmentTable {
866 rows: num_rows,
867 columns: num_columns,
868 cells: frag_cells,
869 block_insert_index,
870 fmt_border: None,
871 fmt_cell_spacing: None,
872 fmt_cell_padding: None,
873 fmt_width: None,
874 fmt_alignment: None,
875 column_widths: vec![],
876 });
877 }
878 }
879 }
880
881 let data = serde_json::to_string(&FragmentData { blocks, tables })
882 .expect("fragment serialization should not fail");
883
884 let plain_text = parsed_plain_text_from_data(&data);
885
886 DocumentFragment { data, plain_text }
887}
888
889fn parsed_plain_text_from_data(data: &str) -> String {
891 let fragment_data: FragmentData = match serde_json::from_str(data) {
892 Ok(d) => d,
893 Err(_) => return String::new(),
894 };
895
896 fragment_data
897 .blocks
898 .iter()
899 .map(|b| b.plain_text.as_str())
900 .collect::<Vec<_>>()
901 .join("\n")
902}