Skip to main content

text_document/
convert.rs

1//! Conversion helpers between public API types and backend DTOs.
2//!
3//! The backend uses `i64` for all positions/sizes. The public API uses `usize`.
4//! All Option mapping between public format structs and backend DTOs lives here.
5
6use crate::{
7    BlockFormat, BlockInfo, DocumentStats, FindMatch, FindOptions, FrameFormat, ListFormat,
8    TextFormat,
9};
10
11// ── Position conversion ─────────────────────────────────────────
12
13pub fn to_i64(v: usize) -> i64 {
14    debug_assert!(v <= i64::MAX as usize, "position overflow: {v}");
15    v as i64
16}
17
18pub fn to_usize(v: i64) -> usize {
19    assert!(v >= 0, "negative position: {v}");
20    v as usize
21}
22
23// ── DocumentStats ───────────────────────────────────────────────
24
25impl From<&frontend::document_inspection::DocumentStatsDto> for DocumentStats {
26    fn from(dto: &frontend::document_inspection::DocumentStatsDto) -> Self {
27        Self {
28            character_count: to_usize(dto.character_count),
29            word_count: to_usize(dto.word_count),
30            block_count: to_usize(dto.block_count),
31            frame_count: to_usize(dto.frame_count),
32            image_count: to_usize(dto.image_count),
33            list_count: to_usize(dto.list_count),
34            table_count: to_usize(dto.table_count),
35        }
36    }
37}
38
39// ── BlockInfo ───────────────────────────────────────────────────
40
41impl From<&frontend::document_inspection::BlockInfoDto> for BlockInfo {
42    fn from(dto: &frontend::document_inspection::BlockInfoDto) -> Self {
43        Self {
44            block_id: to_usize(dto.block_id),
45            block_number: to_usize(dto.block_number),
46            start: to_usize(dto.block_start),
47            length: to_usize(dto.block_length),
48        }
49    }
50}
51
52// ── FindMatch / FindOptions ─────────────────────────────────────
53
54impl FindOptions {
55    pub(crate) fn to_find_text_dto(
56        &self,
57        query: &str,
58        start_position: usize,
59    ) -> frontend::document_search::FindTextDto {
60        frontend::document_search::FindTextDto {
61            query: query.into(),
62            case_sensitive: self.case_sensitive,
63            whole_word: self.whole_word,
64            use_regex: self.use_regex,
65            search_backward: self.search_backward,
66            start_position: to_i64(start_position),
67        }
68    }
69
70    pub(crate) fn to_find_all_dto(&self, query: &str) -> frontend::document_search::FindAllDto {
71        frontend::document_search::FindAllDto {
72            query: query.into(),
73            case_sensitive: self.case_sensitive,
74            whole_word: self.whole_word,
75            use_regex: self.use_regex,
76        }
77    }
78
79    pub(crate) fn to_replace_dto(
80        &self,
81        query: &str,
82        replacement: &str,
83        replace_all: bool,
84    ) -> frontend::document_search::ReplaceTextDto {
85        frontend::document_search::ReplaceTextDto {
86            query: query.into(),
87            replacement: replacement.into(),
88            case_sensitive: self.case_sensitive,
89            whole_word: self.whole_word,
90            use_regex: self.use_regex,
91            replace_all,
92        }
93    }
94}
95
96pub fn find_result_to_match(dto: &frontend::document_search::FindResultDto) -> Option<FindMatch> {
97    if dto.found {
98        Some(FindMatch {
99            position: to_usize(dto.position),
100            length: to_usize(dto.length),
101        })
102    } else {
103        None
104    }
105}
106
107pub fn find_all_to_matches(dto: &frontend::document_search::FindAllResultDto) -> Vec<FindMatch> {
108    dto.positions
109        .iter()
110        .zip(dto.lengths.iter())
111        .map(|(&pos, &len)| FindMatch {
112            position: to_usize(pos),
113            length: to_usize(len),
114        })
115        .collect()
116}
117
118// ── Domain ↔ DTO enum conversions ───────────────────────────────
119//
120// The DTO layer has its own enum types, separate from domain enums
121// in `common::entities`. This keeps the API boundary stable even
122// when domain internals change.
123
124// Formatting DTOs have their own enum types (separate from entity DTO enums).
125// These conversion functions bridge the two at the public API boundary.
126use frontend::document_formatting::dtos as fmt_dto;
127
128fn underline_style_to_dto(v: &crate::UnderlineStyle) -> fmt_dto::UnderlineStyle {
129    match v {
130        crate::UnderlineStyle::NoUnderline => fmt_dto::UnderlineStyle::NoUnderline,
131        crate::UnderlineStyle::SingleUnderline => fmt_dto::UnderlineStyle::SingleUnderline,
132        crate::UnderlineStyle::DashUnderline => fmt_dto::UnderlineStyle::DashUnderline,
133        crate::UnderlineStyle::DotLine => fmt_dto::UnderlineStyle::DotLine,
134        crate::UnderlineStyle::DashDotLine => fmt_dto::UnderlineStyle::DashDotLine,
135        crate::UnderlineStyle::DashDotDotLine => fmt_dto::UnderlineStyle::DashDotDotLine,
136        crate::UnderlineStyle::WaveUnderline => fmt_dto::UnderlineStyle::WaveUnderline,
137        crate::UnderlineStyle::SpellCheckUnderline => fmt_dto::UnderlineStyle::SpellCheckUnderline,
138    }
139}
140
141fn vertical_alignment_to_dto(v: &crate::CharVerticalAlignment) -> fmt_dto::CharVerticalAlignment {
142    match v {
143        crate::CharVerticalAlignment::Normal => fmt_dto::CharVerticalAlignment::Normal,
144        crate::CharVerticalAlignment::SuperScript => fmt_dto::CharVerticalAlignment::SuperScript,
145        crate::CharVerticalAlignment::SubScript => fmt_dto::CharVerticalAlignment::SubScript,
146        crate::CharVerticalAlignment::Middle => fmt_dto::CharVerticalAlignment::Middle,
147        crate::CharVerticalAlignment::Bottom => fmt_dto::CharVerticalAlignment::Bottom,
148        crate::CharVerticalAlignment::Top => fmt_dto::CharVerticalAlignment::Top,
149        crate::CharVerticalAlignment::Baseline => fmt_dto::CharVerticalAlignment::Baseline,
150    }
151}
152
153fn alignment_to_dto(v: &crate::Alignment) -> fmt_dto::Alignment {
154    match v {
155        crate::Alignment::Left => fmt_dto::Alignment::Left,
156        crate::Alignment::Right => fmt_dto::Alignment::Right,
157        crate::Alignment::Center => fmt_dto::Alignment::Center,
158        crate::Alignment::Justify => fmt_dto::Alignment::Justify,
159    }
160}
161
162fn marker_to_dto(v: &crate::MarkerType) -> fmt_dto::MarkerType {
163    match v {
164        crate::MarkerType::NoMarker => fmt_dto::MarkerType::NoMarker,
165        crate::MarkerType::Unchecked => fmt_dto::MarkerType::Unchecked,
166        crate::MarkerType::Checked => fmt_dto::MarkerType::Checked,
167    }
168}
169
170// ── TextFormat → SetTextFormatDto ───────────────────────────────
171//
172// Backend DTOs now use `Option` fields: `None` means "don't change
173// this property" and `Some(value)` means "set to value".
174
175impl TextFormat {
176    pub(crate) fn to_set_dto(
177        &self,
178        position: usize,
179        anchor: usize,
180    ) -> frontend::document_formatting::SetTextFormatDto {
181        frontend::document_formatting::SetTextFormatDto {
182            position: to_i64(position),
183            anchor: to_i64(anchor),
184            font_family: self.font_family.clone(),
185            font_point_size: self.font_point_size.map(|v| v as i64),
186            font_weight: self.font_weight.map(|v| v as i64),
187            font_bold: self.font_bold,
188            font_italic: self.font_italic,
189            font_underline: self.font_underline,
190            font_overline: self.font_overline,
191            font_strikeout: self.font_strikeout,
192            letter_spacing: self.letter_spacing.map(|v| v as i64),
193            word_spacing: self.word_spacing.map(|v| v as i64),
194            underline_style: self.underline_style.as_ref().map(underline_style_to_dto),
195            vertical_alignment: self
196                .vertical_alignment
197                .as_ref()
198                .map(vertical_alignment_to_dto),
199        }
200    }
201
202    pub(crate) fn to_merge_dto(
203        &self,
204        position: usize,
205        anchor: usize,
206    ) -> frontend::document_formatting::MergeTextFormatDto {
207        frontend::document_formatting::MergeTextFormatDto {
208            position: to_i64(position),
209            anchor: to_i64(anchor),
210            font_family: self.font_family.clone(),
211            font_bold: self.font_bold,
212            font_italic: self.font_italic,
213            font_underline: self.font_underline,
214            font_strikeout: self.font_strikeout,
215        }
216    }
217}
218
219// ── CharacterFormat (Phase 1 format_runs) → TextFormat ─────────
220
221impl From<&frontend::common::format_runs::CharacterFormat> for TextFormat {
222    fn from(fmt: &frontend::common::format_runs::CharacterFormat) -> Self {
223        Self {
224            font_family: fmt.font_family.clone(),
225            font_point_size: fmt.font_point_size.map(|v| v as u32),
226            font_weight: fmt.font_weight.map(|v| v as u32),
227            font_bold: fmt.font_bold,
228            font_italic: fmt.font_italic,
229            font_underline: fmt.font_underline,
230            font_overline: fmt.font_overline,
231            font_strikeout: fmt.font_strikeout,
232            letter_spacing: fmt.letter_spacing.map(|v| v as i32),
233            word_spacing: fmt.word_spacing.map(|v| v as i32),
234            underline_style: fmt.underline_style.clone(),
235            vertical_alignment: fmt.vertical_alignment.clone(),
236            anchor_href: fmt.anchor_href.clone(),
237            anchor_names: fmt.anchor_names.clone(),
238            is_anchor: fmt.is_anchor,
239            tooltip: fmt.tooltip.clone(),
240            foreground_color: None,
241            background_color: None,
242            underline_color: None,
243        }
244    }
245}
246
247// ── BlockFormat ─────────────────────────────────────────────────
248
249impl BlockFormat {
250    pub(crate) fn to_set_dto(
251        &self,
252        position: usize,
253        anchor: usize,
254    ) -> frontend::document_formatting::SetBlockFormatDto {
255        frontend::document_formatting::SetBlockFormatDto {
256            position: to_i64(position),
257            anchor: to_i64(anchor),
258            alignment: self.alignment.as_ref().map(alignment_to_dto),
259            heading_level: self.heading_level.map(|v| v as i64),
260            indent: self.indent.map(|v| v as i64),
261            marker: self.marker.as_ref().map(marker_to_dto),
262            line_height: self.line_height.map(|v| (v * 1000.0) as i64),
263            non_breakable_lines: self.non_breakable_lines,
264            direction: self.direction.clone(),
265            background_color: self.background_color.clone(),
266            is_code_block: self.is_code_block,
267            code_language: self.code_language.clone(),
268            top_margin: self.top_margin.map(|v| v as i64),
269            bottom_margin: self.bottom_margin.map(|v| v as i64),
270            left_margin: self.left_margin.map(|v| v as i64),
271            right_margin: self.right_margin.map(|v| v as i64),
272            text_indent: self.text_indent.map(|v| v as i64),
273        }
274    }
275}
276
277impl From<&frontend::block::dtos::BlockDto> for BlockFormat {
278    fn from(b: &frontend::block::dtos::BlockDto) -> Self {
279        Self {
280            alignment: b.fmt_alignment.clone(),
281            top_margin: b.fmt_top_margin.map(|v| v as i32),
282            bottom_margin: b.fmt_bottom_margin.map(|v| v as i32),
283            left_margin: b.fmt_left_margin.map(|v| v as i32),
284            right_margin: b.fmt_right_margin.map(|v| v as i32),
285            heading_level: b.fmt_heading_level.map(|v| v as u8),
286            indent: b.fmt_indent.map(|v| v as u8),
287            text_indent: b.fmt_text_indent.map(|v| v as i32),
288            marker: b.fmt_marker.clone(),
289            tab_positions: b.fmt_tab_positions.iter().map(|&v| v as i32).collect(),
290            line_height: b.fmt_line_height.map(|v| v as f32 / 1000.0),
291            non_breakable_lines: b.fmt_non_breakable_lines,
292            direction: b.fmt_direction.clone(),
293            background_color: b.fmt_background_color.clone(),
294            is_code_block: b.fmt_is_code_block,
295            code_language: b.fmt_code_language.clone(),
296        }
297    }
298}
299
300// ── FrameFormat ─────────────────────────────────────────────────
301
302impl FrameFormat {
303    pub(crate) fn to_set_dto(
304        &self,
305        position: usize,
306        anchor: usize,
307        frame_id: usize,
308    ) -> frontend::document_formatting::SetFrameFormatDto {
309        frontend::document_formatting::SetFrameFormatDto {
310            position: to_i64(position),
311            anchor: to_i64(anchor),
312            frame_id: to_i64(frame_id),
313            height: self.height.map(|v| v as i64),
314            width: self.width.map(|v| v as i64),
315            top_margin: self.top_margin.map(|v| v as i64),
316            bottom_margin: self.bottom_margin.map(|v| v as i64),
317            left_margin: self.left_margin.map(|v| v as i64),
318            right_margin: self.right_margin.map(|v| v as i64),
319            padding: self.padding.map(|v| v as i64),
320            border: self.border.map(|v| v as i64),
321            is_blockquote: self.is_blockquote,
322        }
323    }
324}
325
326// ── ListFormat ─────────────────────────────────────────────────
327
328impl ListFormat {
329    pub(crate) fn to_set_dto(
330        &self,
331        list_id: usize,
332    ) -> frontend::document_formatting::SetListFormatDto {
333        frontend::document_formatting::SetListFormatDto {
334            list_id: to_i64(list_id),
335            style: self.style.clone(),
336            indent: self.indent.map(|v| v as i64),
337            prefix: self.prefix.clone(),
338            suffix: self.suffix.clone(),
339        }
340    }
341}
342
343// ── TableFormat ────────────────────────────────────────────────
344
345impl crate::flow::TableFormat {
346    pub(crate) fn to_set_dto(
347        &self,
348        table_id: usize,
349    ) -> frontend::document_formatting::SetTableFormatDto {
350        frontend::document_formatting::SetTableFormatDto {
351            table_id: to_i64(table_id),
352            border: self.border.map(|v| v as i64),
353            cell_spacing: self.cell_spacing.map(|v| v as i64),
354            cell_padding: self.cell_padding.map(|v| v as i64),
355            width: self.width.map(|v| v as i64),
356            alignment: self.alignment.as_ref().map(alignment_to_dto),
357        }
358    }
359}
360
361// ── CellFormat ─────────────────────────────────────────────────
362
363fn cell_vertical_alignment_to_dto(
364    v: &crate::flow::CellVerticalAlignment,
365) -> fmt_dto::CellVerticalAlignment {
366    match v {
367        crate::flow::CellVerticalAlignment::Top => fmt_dto::CellVerticalAlignment::Top,
368        crate::flow::CellVerticalAlignment::Middle => fmt_dto::CellVerticalAlignment::Middle,
369        crate::flow::CellVerticalAlignment::Bottom => fmt_dto::CellVerticalAlignment::Bottom,
370    }
371}
372
373impl crate::flow::CellFormat {
374    pub(crate) fn to_set_dto(
375        &self,
376        cell_id: usize,
377    ) -> frontend::document_formatting::SetTableCellFormatDto {
378        frontend::document_formatting::SetTableCellFormatDto {
379            cell_id: to_i64(cell_id),
380            padding: self.padding.map(|v| v as i64),
381            border: self.border.map(|v| v as i64),
382            vertical_alignment: self
383                .vertical_alignment
384                .as_ref()
385                .map(cell_vertical_alignment_to_dto),
386            background_color: self.background_color.clone(),
387        }
388    }
389}