Skip to main content

text_typeset/
bridge.rs

1//! Bridge between text-document snapshot types and text-typeset layout params.
2//!
3//! Converts `FlowSnapshot`, `BlockSnapshot`, `TextFormat`, etc. into
4//! `BlockLayoutParams`, `FragmentParams`, `TableLayoutParams`, etc.
5
6use text_document::{
7    BlockSnapshot, CellSnapshot, FlowElementSnapshot, FlowSnapshot, FragmentContent, FrameSnapshot,
8    TableSnapshot,
9};
10
11use crate::layout::block::{BlockLayoutParams, FragmentParams};
12use crate::layout::frame::{FrameLayoutParams, FramePosition};
13use crate::layout::paragraph::Alignment;
14use crate::layout::table::{CellLayoutParams, TableLayoutParams};
15
16const DEFAULT_LIST_INDENT: f32 = 24.0;
17const INDENT_PER_LEVEL: f32 = 24.0;
18
19/// Convert a FlowSnapshot into layout params that can be fed to the Typesetter.
20pub fn convert_flow(flow: &FlowSnapshot) -> FlowElements {
21    let mut blocks = Vec::new();
22    let mut tables = Vec::new();
23    let mut frames = Vec::new();
24
25    for (i, element) in flow.elements.iter().enumerate() {
26        match element {
27            FlowElementSnapshot::Block(block) => {
28                blocks.push((i, convert_block(block)));
29            }
30            FlowElementSnapshot::Table(table) => {
31                tables.push((i, convert_table(table)));
32            }
33            FlowElementSnapshot::Frame(frame) => {
34                frames.push((i, convert_frame(frame)));
35            }
36        }
37    }
38
39    FlowElements {
40        blocks,
41        tables,
42        frames,
43    }
44}
45
46/// Converted flow elements, ordered by their position in the flow.
47pub struct FlowElements {
48    /// (flow_index, params)
49    pub blocks: Vec<(usize, BlockLayoutParams)>,
50    pub tables: Vec<(usize, TableLayoutParams)>,
51    pub frames: Vec<(usize, FrameLayoutParams)>,
52}
53
54pub fn convert_block(block: &BlockSnapshot) -> BlockLayoutParams {
55    let alignment = block
56        .block_format
57        .alignment
58        .as_ref()
59        .map(convert_alignment)
60        .unwrap_or_default();
61
62    let heading_scale = match block.block_format.heading_level {
63        Some(1) => 2.0,
64        Some(2) => 1.5,
65        Some(3) => 1.25,
66        Some(4) => 1.1,
67        _ => 1.0,
68    };
69
70    let fragments: Vec<FragmentParams> = block
71        .fragments
72        .iter()
73        .map(|f| convert_fragment(f, heading_scale))
74        .collect();
75
76    let indent_level = block.block_format.indent.unwrap_or(0) as f32;
77
78    let (list_marker, list_indent) = if let Some(ref info) = block.list_info {
79        (
80            info.marker.clone(),
81            DEFAULT_LIST_INDENT + indent_level * INDENT_PER_LEVEL,
82        )
83    } else {
84        (String::new(), indent_level * INDENT_PER_LEVEL)
85    };
86
87    let checkbox = match block.block_format.marker {
88        Some(text_document::MarkerType::Checked) => Some(true),
89        Some(text_document::MarkerType::Unchecked) => Some(false),
90        _ => None,
91    };
92
93    BlockLayoutParams {
94        block_id: block.block_id,
95        position: block.position,
96        text: block.text.clone(),
97        fragments,
98        alignment,
99        top_margin: block.block_format.top_margin.unwrap_or(0) as f32,
100        bottom_margin: block.block_format.bottom_margin.unwrap_or(0) as f32,
101        left_margin: block.block_format.left_margin.unwrap_or(0) as f32,
102        right_margin: block.block_format.right_margin.unwrap_or(0) as f32,
103        text_indent: block.block_format.text_indent.unwrap_or(0) as f32,
104        list_marker,
105        list_indent,
106        tab_positions: block
107            .block_format
108            .tab_positions
109            .iter()
110            .map(|&t| t as f32)
111            .collect(),
112        line_height_multiplier: block.block_format.line_height,
113        non_breakable_lines: block.block_format.non_breakable_lines.unwrap_or(false),
114        checkbox,
115        background_color: None, // TODO: parse CSS color string from block_format.background_color
116    }
117}
118
119fn convert_fragment(frag: &FragmentContent, heading_scale: f32) -> FragmentParams {
120    match frag {
121        FragmentContent::Text {
122            text,
123            format,
124            offset,
125            length,
126        } => FragmentParams {
127            text: text.clone(),
128            offset: *offset,
129            length: *length,
130            font_family: format.font_family.clone(),
131            font_weight: format.font_weight,
132            font_bold: format.font_bold,
133            font_italic: format.font_italic,
134            font_point_size: if heading_scale != 1.0 {
135                // Apply heading scale; use 16 as default if no explicit size
136                Some((format.font_point_size.unwrap_or(16) as f32 * heading_scale) as u32)
137            } else {
138                format.font_point_size
139            },
140            underline: format.font_underline.unwrap_or(false),
141            overline: format.font_overline.unwrap_or(false),
142            strikeout: format.font_strikeout.unwrap_or(false),
143            is_link: format.is_anchor.unwrap_or(false),
144            letter_spacing: format.letter_spacing.unwrap_or(0) as f32,
145            word_spacing: format.word_spacing.unwrap_or(0) as f32,
146        },
147        FragmentContent::Image {
148            name: _,
149            width: _,
150            height: _,
151            quality: _,
152            format,
153            offset,
154        } => {
155            // For now, images are rendered as empty placeholders.
156            // The adapter handles actual image loading.
157            FragmentParams {
158                text: String::new(),
159                offset: *offset,
160                length: 1,
161                font_family: None,
162                font_weight: None,
163                font_bold: None,
164                font_italic: None,
165                font_point_size: None,
166                underline: false,
167                overline: false,
168                strikeout: false,
169                is_link: format.is_anchor.unwrap_or(false),
170                letter_spacing: 0.0,
171                word_spacing: 0.0,
172            }
173        }
174    }
175}
176
177fn convert_alignment(a: &text_document::Alignment) -> Alignment {
178    match a {
179        text_document::Alignment::Left => Alignment::Left,
180        text_document::Alignment::Right => Alignment::Right,
181        text_document::Alignment::Center => Alignment::Center,
182        text_document::Alignment::Justify => Alignment::Justify,
183    }
184}
185
186pub fn convert_table(table: &TableSnapshot) -> TableLayoutParams {
187    let column_widths: Vec<f32> = table.column_widths.iter().map(|&w| w as f32).collect();
188
189    let cells: Vec<CellLayoutParams> = table.cells.iter().map(convert_cell).collect();
190
191    TableLayoutParams {
192        table_id: table.table_id,
193        rows: table.rows,
194        columns: table.columns,
195        column_widths,
196        border_width: table.format.border.unwrap_or(1) as f32,
197        cell_spacing: table.format.cell_spacing.unwrap_or(0) as f32,
198        cell_padding: table.format.cell_padding.unwrap_or(4) as f32,
199        cells,
200    }
201}
202
203fn convert_cell(cell: &CellSnapshot) -> CellLayoutParams {
204    let blocks: Vec<BlockLayoutParams> = cell.blocks.iter().map(convert_block).collect();
205
206    // Parse background color from CSS color string (simplified)
207    let background_color = cell
208        .format
209        .background_color
210        .as_ref()
211        .map(|_| [0.9, 0.9, 0.9, 1.0]); // placeholder: light gray
212
213    CellLayoutParams {
214        row: cell.row,
215        column: cell.column,
216        blocks,
217        background_color,
218    }
219}
220
221pub fn convert_frame(frame: &FrameSnapshot) -> FrameLayoutParams {
222    let mut blocks = Vec::new();
223    let mut tables = Vec::new();
224
225    for (i, element) in frame.elements.iter().enumerate() {
226        match element {
227            FlowElementSnapshot::Block(block) => {
228                blocks.push(convert_block(block));
229            }
230            FlowElementSnapshot::Table(table) => {
231                tables.push((i, convert_table(table)));
232            }
233            FlowElementSnapshot::Frame(_) => {
234                // Nested frames within frames -could recurse, but for now skip
235            }
236        }
237    }
238
239    let position = match &frame.format.position {
240        Some(text_document::FramePosition::InFlow) | None => FramePosition::Inline,
241        Some(text_document::FramePosition::FloatLeft) => FramePosition::FloatLeft,
242        Some(text_document::FramePosition::FloatRight) => FramePosition::FloatRight,
243    };
244
245    FrameLayoutParams {
246        frame_id: frame.frame_id,
247        position,
248        width: frame.format.width.map(|w| w as f32),
249        height: frame.format.height.map(|h| h as f32),
250        margin_top: frame.format.top_margin.unwrap_or(0) as f32,
251        margin_bottom: frame.format.bottom_margin.unwrap_or(0) as f32,
252        margin_left: frame.format.left_margin.unwrap_or(0) as f32,
253        margin_right: frame.format.right_margin.unwrap_or(0) as f32,
254        padding: frame.format.padding.unwrap_or(0) as f32,
255        border_width: frame.format.border.unwrap_or(0) as f32,
256        blocks,
257        tables,
258    }
259}