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 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
483 let mut result = text;
484
485 if is_monospace {
486 result = format!("<code>{}</code>", result);
487 }
488 if is_bold {
489 result = format!("<strong>{}</strong>", result);
490 }
491 if is_italic {
492 result = format!("<em>{}</em>", result);
493 }
494 if is_underline {
495 result = format!("<u>{}</u>", result);
496 }
497 if is_strikeout {
498 result = format!("<s>{}</s>", result);
499 }
500 if is_anchor && let Some(ref href) = elem.fmt_anchor_href {
501 result = format!("<a href=\"{}\">{}</a>", escape_html(href), result);
502 }
503
504 out.push_str(&result);
505 }
506}
507
508fn push_table_html(out: &mut String, table: &FragmentTable) {
510 out.push_str("<table>");
511 for row in 0..table.rows {
512 out.push_str("<tr>");
513 for col in 0..table.columns {
514 if let Some(cell) = table.cells.iter().find(|c| c.row == row && c.column == col) {
515 out.push_str("<td");
516 if cell.row_span > 1 {
517 out.push_str(&format!(" rowspan=\"{}\"", cell.row_span));
518 }
519 if cell.column_span > 1 {
520 out.push_str(&format!(" colspan=\"{}\"", cell.column_span));
521 }
522 out.push('>');
523 for (i, block) in cell.blocks.iter().enumerate() {
524 if i > 0 {
525 out.push_str("<br>");
526 }
527 push_inline_html(out, &block.elements);
528 }
529 out.push_str("</td>");
530 }
531 }
533 out.push_str("</tr>");
534 }
535 out.push_str("</table>");
536}
537
538fn escape_markdown(s: &str) -> String {
541 let mut out = String::with_capacity(s.len());
542 for c in s.chars() {
543 if matches!(
544 c,
545 '\\' | '`'
546 | '*'
547 | '_'
548 | '{'
549 | '}'
550 | '['
551 | ']'
552 | '('
553 | ')'
554 | '#'
555 | '+'
556 | '-'
557 | '.'
558 | '!'
559 | '|'
560 | '~'
561 | '<'
562 | '>'
563 ) {
564 out.push('\\');
565 }
566 out.push(c);
567 }
568 out
569}
570
571fn render_inline_markdown(elements: &[FragmentElement]) -> String {
572 let mut out = String::new();
573 for elem in elements {
574 let raw_text = match &elem.content {
575 InlineContent::Text(t) => t.clone(),
576 InlineContent::Image { name, alt, .. } => format!(""),
579 InlineContent::FootnoteRef { label } => format!("[^{label}]"),
580 InlineContent::Empty => String::new(),
581 };
582
583 let is_monospace = elem
584 .fmt_font_family
585 .as_deref()
586 .is_some_and(|f| f == "monospace");
587 let is_bold = elem.fmt_font_bold.unwrap_or(false);
588 let is_italic = elem.fmt_font_italic.unwrap_or(false);
589 let is_strikeout = elem.fmt_font_strikeout.unwrap_or(false);
590 let is_anchor = elem.fmt_is_anchor.unwrap_or(false);
591
592 if is_monospace {
593 out.push('`');
594 out.push_str(&raw_text);
595 out.push('`');
596 } else {
597 let mut text = escape_markdown(&raw_text);
598 if is_bold && is_italic {
599 text = format!("***{}***", text);
600 } else if is_bold {
601 text = format!("**{}**", text);
602 } else if is_italic {
603 text = format!("*{}*", text);
604 }
605 if is_strikeout {
606 text = format!("~~{}~~", text);
607 }
608 if is_anchor {
609 let href = elem.fmt_anchor_href.as_deref().unwrap_or("");
610 out.push_str(&format!("[{}]({})", text, href));
611 } else {
612 out.push_str(&text);
613 }
614 }
615 }
616 out
617}
618
619fn render_table_markdown(table: &FragmentTable) -> String {
621 let mut rows: Vec<Vec<String>> = vec![vec![String::new(); table.columns]; table.rows];
622
623 for cell in &table.cells {
624 let text: String = cell
625 .blocks
626 .iter()
627 .map(|b| render_inline_markdown(&b.elements))
628 .collect::<Vec<_>>()
629 .join(" ");
630 if cell.row < table.rows && cell.column < table.columns {
631 rows[cell.row][cell.column] = text;
632 }
633 }
634
635 let mut out = String::new();
636 for (i, row) in rows.iter().enumerate() {
637 out.push_str("| ");
638 out.push_str(&row.join(" | "));
639 out.push_str(" |");
640 if i == 0 {
641 out.push('\n');
643 out.push('|');
644 for _ in 0..table.columns {
645 out.push_str(" --- |");
646 }
647 }
648 if i + 1 < rows.len() {
649 out.push('\n');
650 }
651 }
652 out
653}
654
655fn spans_plain_text(spans: &[ParsedSpan]) -> String {
667 spans
668 .iter()
669 .map(|s| {
670 if s.image.is_some() || s.footnote_ref.is_some() {
675 "\u{FFFC}"
676 } else {
677 s.text.as_str()
678 }
679 })
680 .collect()
681}
682
683fn span_to_fragment_element(span: &ParsedSpan) -> FragmentElement {
684 let content = match (&span.footnote_ref, &span.image) {
693 (Some(label), _) => InlineContent::FootnoteRef {
694 label: label.clone(),
695 },
696 (None, Some(img)) => InlineContent::Image {
697 name: img.src.clone(),
698 alt: img.alt.clone(),
699 width: img.width,
700 height: img.height,
701 quality: 100,
704 },
705 (None, None) => InlineContent::Text(span.text.clone()),
706 };
707 let fmt_font_family = if span.code {
708 Some("monospace".into())
709 } else {
710 None
711 };
712 let fmt_font_bold = if span.bold { Some(true) } else { None };
713 let fmt_font_italic = if span.italic { Some(true) } else { None };
714 let fmt_font_underline = if span.underline { Some(true) } else { None };
715 let fmt_font_strikeout = if span.strikeout { Some(true) } else { None };
716 let (fmt_anchor_href, fmt_is_anchor) = if let Some(ref href) = span.link_href {
717 (Some(href.clone()), Some(true))
718 } else {
719 (None, None)
720 };
721
722 FragmentElement {
723 content,
724 fmt_font_family,
725 fmt_font_point_size: None,
726 fmt_font_weight: None,
727 fmt_font_bold,
728 fmt_font_italic,
729 fmt_font_underline,
730 fmt_font_overline: None,
731 fmt_font_strikeout,
732 fmt_letter_spacing: None,
733 fmt_word_spacing: None,
734 fmt_anchor_href,
735 fmt_anchor_names: vec![],
736 fmt_is_anchor,
737 fmt_tooltip: None,
738 fmt_underline_style: None,
739 fmt_vertical_alignment: None,
740 }
741}
742
743fn parsed_elements_to_fragment(parsed: Vec<ParsedElement>) -> DocumentFragment {
746 use frontend::common::parser_tools::fragment_schema::FragmentList;
747
748 let mut blocks: Vec<FragmentBlock> = Vec::new();
749 let mut tables: Vec<FragmentTable> = Vec::new();
750
751 for elem in parsed {
752 match elem {
753 ParsedElement::FootnoteDefinition { .. } => {}
758 ParsedElement::Block(pb) => {
759 let elements: Vec<FragmentElement> =
760 pb.spans.iter().map(span_to_fragment_element).collect();
761 let plain_text: String = spans_plain_text(&pb.spans);
762 let list = pb.list_style.map(|style| FragmentList {
763 style,
764 indent: pb.list_indent as i64,
765 prefix: String::new(),
766 suffix: String::new(),
767 });
768
769 blocks.push(FragmentBlock {
770 plain_text,
771 elements,
772 heading_level: pb.heading_level,
773 list,
774 alignment: None,
775 indent: None,
776 text_indent: None,
777 marker: None,
778 top_margin: None,
779 bottom_margin: None,
780 left_margin: None,
781 right_margin: None,
782 tab_positions: vec![],
783 line_height: pb.line_height,
784 non_breakable_lines: pb.non_breakable_lines,
785 page_break_before: pb.page_break_before,
786 direction: pb.direction,
787 background_color: pb.background_color,
788 is_code_block: None,
789 code_language: None,
790 hyphenate: None,
791 language: None,
792 });
793 }
794 ParsedElement::Table(pt) => {
795 let block_insert_index = blocks.len();
796 let num_columns = pt.rows.iter().map(|r| r.len()).max().unwrap_or(0);
797 let num_rows = pt.rows.len();
798
799 let mut frag_cells: Vec<FragmentTableCell> = Vec::new();
800 for (row_idx, row) in pt.rows.iter().enumerate() {
801 for (col_idx, cell) in row.iter().enumerate() {
802 let cell_elements: Vec<FragmentElement> =
803 cell.spans.iter().map(span_to_fragment_element).collect();
804 let cell_text: String = spans_plain_text(&cell.spans);
805
806 frag_cells.push(FragmentTableCell {
807 row: row_idx,
808 column: col_idx,
809 row_span: 1,
810 column_span: 1,
811 blocks: vec![FragmentBlock {
812 plain_text: cell_text,
813 elements: cell_elements,
814 heading_level: None,
815 list: None,
816 alignment: None,
817 indent: None,
818 text_indent: None,
819 marker: None,
820 top_margin: None,
821 bottom_margin: None,
822 left_margin: None,
823 right_margin: None,
824 tab_positions: vec![],
825 line_height: None,
826 non_breakable_lines: None,
827 page_break_before: None,
828 direction: None,
829 background_color: None,
830 is_code_block: None,
831 code_language: None,
832 hyphenate: None,
833 language: None,
834 }],
835 fmt_padding: None,
836 fmt_border: None,
837 fmt_vertical_alignment: None,
838 fmt_background_color: None,
839 });
840 }
841 }
842
843 tables.push(FragmentTable {
844 rows: num_rows,
845 columns: num_columns,
846 cells: frag_cells,
847 block_insert_index,
848 fmt_border: None,
849 fmt_cell_spacing: None,
850 fmt_cell_padding: None,
851 fmt_width: None,
852 fmt_alignment: None,
853 column_widths: vec![],
854 });
855 }
856 }
857 }
858
859 let data = serde_json::to_string(&FragmentData { blocks, tables })
860 .expect("fragment serialization should not fail");
861
862 let plain_text = parsed_plain_text_from_data(&data);
863
864 DocumentFragment { data, plain_text }
865}
866
867fn parsed_plain_text_from_data(data: &str) -> String {
869 let fragment_data: FragmentData = match serde_json::from_str(data) {
870 Ok(d) => d,
871 Err(_) => return String::new(),
872 };
873
874 fragment_data
875 .blocks
876 .iter()
877 .map(|b| b.plain_text.as_str())
878 .collect::<Vec<_>>()
879 .join("\n")
880}