Skip to main content

common/parser_tools/
fragment_schema.rs

1use serde::{Deserialize, Serialize};
2
3use crate::entities::*;
4use crate::format_runs::{InlineContent, InlineSegment};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct FragmentData {
8    pub blocks: Vec<FragmentBlock>,
9    /// Table fragments extracted from cell selections. Empty for text-only fragments.
10    #[serde(default, skip_serializing_if = "Vec::is_empty")]
11    pub tables: Vec<FragmentTable>,
12}
13
14/// A table (or rectangular sub-region) captured from a cell selection.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct FragmentTable {
17    pub rows: usize,
18    pub columns: usize,
19    pub cells: Vec<FragmentTableCell>,
20    /// Index into the parent `FragmentData::blocks` at which this table
21    /// should be inserted.  Blocks `[0..index)` come before the table,
22    /// blocks `[index..]` come after.  Default `0` for backward compat.
23    #[serde(default)]
24    pub block_insert_index: usize,
25    // ── Table-level formatting ────────────────────────────────────
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub fmt_border: Option<i64>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub fmt_cell_spacing: Option<i64>,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub fmt_cell_padding: Option<i64>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub fmt_width: Option<i64>,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub fmt_alignment: Option<Alignment>,
36    #[serde(default, skip_serializing_if = "Vec::is_empty")]
37    pub column_widths: Vec<i64>,
38}
39
40/// One cell within a [`FragmentTable`].
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct FragmentTableCell {
43    pub row: usize,
44    pub column: usize,
45    pub row_span: usize,
46    pub column_span: usize,
47    pub blocks: Vec<FragmentBlock>,
48    // ── Cell-level formatting ─────────────────────────────────────
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub fmt_padding: Option<i64>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub fmt_border: Option<i64>,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub fmt_vertical_alignment: Option<CellVerticalAlignment>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub fmt_background_color: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct FragmentBlock {
61    pub plain_text: String,
62    pub elements: Vec<FragmentElement>,
63    pub heading_level: Option<i64>,
64    pub list: Option<FragmentList>,
65    pub alignment: Option<Alignment>,
66    pub indent: Option<i64>,
67    pub text_indent: Option<i64>,
68    pub marker: Option<MarkerType>,
69    pub top_margin: Option<i64>,
70    pub bottom_margin: Option<i64>,
71    pub left_margin: Option<i64>,
72    pub right_margin: Option<i64>,
73    pub tab_positions: Vec<i64>,
74    pub line_height: Option<i64>,
75    pub non_breakable_lines: Option<bool>,
76    /// Start this block on a new page. `#[serde(default)]` because a fragment
77    /// copied by a build that predates the field carries no such key.
78    #[serde(default)]
79    pub page_break_before: Option<bool>,
80    pub direction: Option<TextDirection>,
81    pub background_color: Option<String>,
82    pub is_code_block: Option<bool>,
83    pub code_language: Option<String>,
84    #[serde(default)]
85    pub hyphenate: Option<bool>,
86    #[serde(default)]
87    pub language: Option<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct FragmentElement {
92    pub content: InlineContent,
93    pub fmt_font_family: Option<String>,
94    pub fmt_font_point_size: Option<i64>,
95    pub fmt_font_weight: Option<i64>,
96    pub fmt_font_bold: Option<bool>,
97    pub fmt_font_italic: Option<bool>,
98    pub fmt_font_underline: Option<bool>,
99    pub fmt_font_overline: Option<bool>,
100    pub fmt_font_strikeout: Option<bool>,
101    pub fmt_letter_spacing: Option<i64>,
102    pub fmt_word_spacing: Option<i64>,
103    pub fmt_anchor_href: Option<String>,
104    pub fmt_anchor_names: Vec<String>,
105    pub fmt_is_anchor: Option<bool>,
106    pub fmt_tooltip: Option<String>,
107    pub fmt_underline_style: Option<UnderlineStyle>,
108    pub fmt_vertical_alignment: Option<CharVerticalAlignment>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct FragmentList {
113    pub style: ListStyle,
114    pub indent: i64,
115    pub prefix: String,
116    pub suffix: String,
117}
118
119impl FragmentElement {
120    pub fn from_segment(seg: &InlineSegment) -> Self {
121        FragmentElement {
122            content: seg.content.clone(),
123            fmt_font_family: seg.fmt_font_family.clone(),
124            fmt_font_point_size: seg.fmt_font_point_size,
125            fmt_font_weight: seg.fmt_font_weight,
126            fmt_font_bold: seg.fmt_font_bold,
127            fmt_font_italic: seg.fmt_font_italic,
128            fmt_font_underline: seg.fmt_font_underline,
129            fmt_font_overline: seg.fmt_font_overline,
130            fmt_font_strikeout: seg.fmt_font_strikeout,
131            fmt_letter_spacing: seg.fmt_letter_spacing,
132            fmt_word_spacing: seg.fmt_word_spacing,
133            fmt_anchor_href: seg.fmt_anchor_href.clone(),
134            fmt_anchor_names: seg.fmt_anchor_names.clone(),
135            fmt_is_anchor: seg.fmt_is_anchor,
136            fmt_tooltip: seg.fmt_tooltip.clone(),
137            fmt_underline_style: seg.fmt_underline_style.clone(),
138            fmt_vertical_alignment: seg.fmt_vertical_alignment.clone(),
139        }
140    }
141}
142
143impl FragmentBlock {
144    /// Returns `true` when this block carries no block-level formatting,
145    /// meaning its content is purely inline.
146    pub fn is_inline_only(&self) -> bool {
147        self.heading_level.is_none()
148            && self.list.is_none()
149            && self.alignment.is_none()
150            && self.indent.unwrap_or(0) == 0
151            && self.text_indent.unwrap_or(0) == 0
152            && self.marker.is_none()
153            && self.top_margin.is_none()
154            && self.bottom_margin.is_none()
155            && self.left_margin.is_none()
156            && self.right_margin.is_none()
157            && self.line_height.is_none()
158            && self.non_breakable_lines.is_none()
159            && self.direction.is_none()
160            && self.background_color.is_none()
161            && self.is_code_block.is_none()
162            && self.code_language.is_none()
163            && self.hyphenate.is_none()
164            && self.language.is_none()
165    }
166}
167
168impl FragmentList {
169    pub fn from_entity(list: &List) -> Self {
170        FragmentList {
171            style: list.style.clone(),
172            indent: list.indent,
173            prefix: list.prefix.clone(),
174            suffix: list.suffix.clone(),
175        }
176    }
177
178    pub fn to_entity(&self) -> List {
179        List {
180            id: 0,
181            created_at: chrono::Utc::now(),
182            updated_at: chrono::Utc::now(),
183            style: self.style.clone(),
184            indent: self.indent,
185            prefix: self.prefix.clone(),
186            suffix: self.suffix.clone(),
187        }
188    }
189}