1use 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
19pub 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
46pub struct FlowElements {
48 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 let list_indent_level = info.indent as f32;
80 (
81 info.marker.clone(),
82 DEFAULT_LIST_INDENT + list_indent_level * INDENT_PER_LEVEL,
83 )
84 } else {
85 (String::new(), indent_level * INDENT_PER_LEVEL)
86 };
87
88 let checkbox = match block.block_format.marker {
89 Some(text_document::MarkerType::Checked) => Some(true),
90 Some(text_document::MarkerType::Unchecked) => Some(false),
91 _ => None,
92 };
93
94 BlockLayoutParams {
95 block_id: block.block_id,
96 position: block.position,
97 text: block.text.clone(),
98 fragments,
99 alignment,
100 top_margin: block.block_format.top_margin.unwrap_or(0) as f32,
101 bottom_margin: block.block_format.bottom_margin.unwrap_or(0) as f32,
102 left_margin: block.block_format.left_margin.unwrap_or(0) as f32,
103 right_margin: block.block_format.right_margin.unwrap_or(0) as f32,
104 text_indent: block.block_format.text_indent.unwrap_or(0) as f32,
105 list_marker,
106 list_indent,
107 tab_positions: block
108 .block_format
109 .tab_positions
110 .iter()
111 .map(|&t| t as f32)
112 .collect(),
113 line_height_multiplier: block.block_format.line_height,
114 non_breakable_lines: block.block_format.non_breakable_lines.unwrap_or(false)
115 || block.block_format.is_code_block == Some(true),
116 checkbox,
117 background_color: block
118 .block_format
119 .background_color
120 .as_ref()
121 .and_then(|s| parse_css_color(s))
122 .or_else(|| {
123 if block.block_format.is_code_block == Some(true) {
124 Some([0.95, 0.95, 0.95, 1.0])
125 } else {
126 None
127 }
128 }),
129 }
130}
131
132fn convert_fragment(frag: &FragmentContent, heading_scale: f32) -> FragmentParams {
133 match frag {
134 FragmentContent::Text {
135 text,
136 format,
137 offset,
138 length,
139 } => FragmentParams {
140 text: text.clone(),
141 offset: *offset,
142 length: *length,
143 font_family: format.font_family.clone(),
144 font_weight: format.font_weight,
145 font_bold: format.font_bold,
146 font_italic: format.font_italic,
147 font_point_size: if heading_scale != 1.0 {
148 Some((format.font_point_size.unwrap_or(16) as f32 * heading_scale) as u32)
150 } else {
151 format.font_point_size
152 },
153 underline_style: convert_underline_style(format),
154 overline: format.font_overline.unwrap_or(false),
155 strikeout: format.font_strikeout.unwrap_or(false),
156 is_link: format.is_anchor.unwrap_or(false),
157 letter_spacing: format.letter_spacing.unwrap_or(0) as f32,
158 word_spacing: format.word_spacing.unwrap_or(0) as f32,
159 foreground_color: format.foreground_color.as_ref().map(convert_color),
160 underline_color: format.underline_color.as_ref().map(convert_color),
161 background_color: format.background_color.as_ref().map(convert_color),
162 anchor_href: format.anchor_href.clone(),
163 tooltip: format.tooltip.clone(),
164 vertical_alignment: convert_vertical_alignment(format),
165 image_name: None,
166 image_width: 0.0,
167 image_height: 0.0,
168 },
169 FragmentContent::Image {
170 name,
171 width,
172 height,
173 quality: _,
174 format,
175 offset,
176 } => FragmentParams {
177 text: "\u{FFFC}".to_string(),
178 offset: *offset,
179 length: 1,
180 font_family: None,
181 font_weight: None,
182 font_bold: None,
183 font_italic: None,
184 font_point_size: None,
185 underline_style: crate::types::UnderlineStyle::None,
186 overline: false,
187 strikeout: false,
188 is_link: format.is_anchor.unwrap_or(false),
189 letter_spacing: 0.0,
190 word_spacing: 0.0,
191 foreground_color: None,
192 underline_color: None,
193 background_color: None,
194 anchor_href: format.anchor_href.clone(),
195 tooltip: format.tooltip.clone(),
196 vertical_alignment: crate::types::VerticalAlignment::Normal,
197 image_name: Some(name.clone()),
198 image_width: *width as f32,
199 image_height: *height as f32,
200 },
201 }
202}
203
204fn convert_vertical_alignment(
205 format: &text_document::TextFormat,
206) -> crate::types::VerticalAlignment {
207 use crate::types::VerticalAlignment;
208 match format.vertical_alignment {
209 Some(text_document::CharVerticalAlignment::SuperScript) => VerticalAlignment::SuperScript,
210 Some(text_document::CharVerticalAlignment::SubScript) => VerticalAlignment::SubScript,
211 _ => VerticalAlignment::Normal,
212 }
213}
214
215fn convert_underline_style(format: &text_document::TextFormat) -> crate::types::UnderlineStyle {
216 use crate::types::UnderlineStyle;
217 match format.underline_style {
218 Some(text_document::UnderlineStyle::SingleUnderline) => UnderlineStyle::Single,
219 Some(text_document::UnderlineStyle::DashUnderline) => UnderlineStyle::Dash,
220 Some(text_document::UnderlineStyle::DotLine) => UnderlineStyle::Dot,
221 Some(text_document::UnderlineStyle::DashDotLine) => UnderlineStyle::DashDot,
222 Some(text_document::UnderlineStyle::DashDotDotLine) => UnderlineStyle::DashDotDot,
223 Some(text_document::UnderlineStyle::WaveUnderline) => UnderlineStyle::Wave,
224 Some(text_document::UnderlineStyle::SpellCheckUnderline) => UnderlineStyle::SpellCheck,
225 Some(text_document::UnderlineStyle::NoUnderline) => UnderlineStyle::None,
226 None => {
227 if format.font_underline.unwrap_or(false) {
228 UnderlineStyle::Single
229 } else {
230 UnderlineStyle::None
231 }
232 }
233 }
234}
235
236fn convert_color(c: &text_document::Color) -> [f32; 4] {
237 [
238 c.red as f32 / 255.0,
239 c.green as f32 / 255.0,
240 c.blue as f32 / 255.0,
241 c.alpha as f32 / 255.0,
242 ]
243}
244
245fn parse_css_color(s: &str) -> Option<[f32; 4]> {
250 let s = s.trim();
251
252 match s.to_ascii_lowercase().as_str() {
254 "transparent" => return Some([0.0, 0.0, 0.0, 0.0]),
255 "black" => return Some([0.0, 0.0, 0.0, 1.0]),
256 "white" => return Some([1.0, 1.0, 1.0, 1.0]),
257 "red" => return Some([1.0, 0.0, 0.0, 1.0]),
258 "green" => return Some([0.0, 128.0 / 255.0, 0.0, 1.0]),
259 "blue" => return Some([0.0, 0.0, 1.0, 1.0]),
260 "yellow" => return Some([1.0, 1.0, 0.0, 1.0]),
261 "cyan" | "aqua" => return Some([0.0, 1.0, 1.0, 1.0]),
262 "magenta" | "fuchsia" => return Some([1.0, 0.0, 1.0, 1.0]),
263 "gray" | "grey" => return Some([128.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0, 1.0]),
264 _ => {}
265 }
266
267 if let Some(hex) = s.strip_prefix('#') {
269 let hex = hex.trim();
270 return match hex.len() {
271 3 => {
272 let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
274 let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
275 let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
276 Some([
277 (r * 17) as f32 / 255.0,
278 (g * 17) as f32 / 255.0,
279 (b * 17) as f32 / 255.0,
280 1.0,
281 ])
282 }
283 4 => {
284 let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
286 let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
287 let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
288 let a = u8::from_str_radix(&hex[3..4], 16).ok()?;
289 Some([
290 (r * 17) as f32 / 255.0,
291 (g * 17) as f32 / 255.0,
292 (b * 17) as f32 / 255.0,
293 (a * 17) as f32 / 255.0,
294 ])
295 }
296 6 => {
297 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
299 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
300 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
301 Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
302 }
303 8 => {
304 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
306 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
307 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
308 let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
309 Some([
310 r as f32 / 255.0,
311 g as f32 / 255.0,
312 b as f32 / 255.0,
313 a as f32 / 255.0,
314 ])
315 }
316 _ => None,
317 };
318 }
319
320 let inner = if let Some(inner) = s.strip_prefix("rgba(").and_then(|s| s.strip_suffix(')')) {
322 inner
323 } else if let Some(inner) = s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
324 inner
325 } else {
326 return None;
327 };
328
329 let parts: Vec<&str> = inner.split(',').collect();
330 match parts.len() {
331 3 => {
332 let r: u8 = parts[0].trim().parse().ok()?;
333 let g: u8 = parts[1].trim().parse().ok()?;
334 let b: u8 = parts[2].trim().parse().ok()?;
335 Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
336 }
337 4 => {
338 let r: u8 = parts[0].trim().parse().ok()?;
339 let g: u8 = parts[1].trim().parse().ok()?;
340 let b: u8 = parts[2].trim().parse().ok()?;
341 let a: f32 = parts[3].trim().parse().ok()?;
342 Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a])
343 }
344 _ => None,
345 }
346}
347
348fn convert_alignment(a: &text_document::Alignment) -> Alignment {
349 match a {
350 text_document::Alignment::Left => Alignment::Left,
351 text_document::Alignment::Right => Alignment::Right,
352 text_document::Alignment::Center => Alignment::Center,
353 text_document::Alignment::Justify => Alignment::Justify,
354 }
355}
356
357pub fn convert_table(table: &TableSnapshot) -> TableLayoutParams {
358 let column_widths: Vec<f32> = table.column_widths.iter().map(|&w| w as f32).collect();
359
360 let cells: Vec<CellLayoutParams> = table.cells.iter().map(convert_cell).collect();
361
362 TableLayoutParams {
363 table_id: table.table_id,
364 rows: table.rows,
365 columns: table.columns,
366 column_widths,
367 border_width: table.format.border.unwrap_or(1) as f32,
368 cell_spacing: table.format.cell_spacing.unwrap_or(0) as f32,
369 cell_padding: table.format.cell_padding.unwrap_or(4) as f32,
370 cells,
371 }
372}
373
374fn convert_cell(cell: &CellSnapshot) -> CellLayoutParams {
375 let blocks: Vec<BlockLayoutParams> = cell.blocks.iter().map(convert_block).collect();
376
377 let background_color = cell
378 .format
379 .background_color
380 .as_ref()
381 .and_then(|s| parse_css_color(s));
382
383 CellLayoutParams {
384 row: cell.row,
385 column: cell.column,
386 blocks,
387 background_color,
388 }
389}
390
391pub fn convert_frame(frame: &FrameSnapshot) -> FrameLayoutParams {
392 let mut blocks = Vec::new();
393 let mut tables = Vec::new();
394 let mut frames = Vec::new();
395
396 for (i, element) in frame.elements.iter().enumerate() {
397 match element {
398 FlowElementSnapshot::Block(block) => {
399 blocks.push(convert_block(block));
400 }
401 FlowElementSnapshot::Table(table) => {
402 tables.push((i, convert_table(table)));
403 }
404 FlowElementSnapshot::Frame(inner_frame) => {
405 frames.push((i, convert_frame(inner_frame)));
406 }
407 }
408 }
409
410 let position = match &frame.format.position {
411 Some(text_document::FramePosition::InFlow) | None => FramePosition::Inline,
412 Some(text_document::FramePosition::FloatLeft) => FramePosition::FloatLeft,
413 Some(text_document::FramePosition::FloatRight) => FramePosition::FloatRight,
414 };
415
416 let is_blockquote = frame.format.is_blockquote == Some(true);
417
418 FrameLayoutParams {
419 frame_id: frame.frame_id,
420 position,
421 width: frame.format.width.map(|w| w as f32),
422 height: frame.format.height.map(|h| h as f32),
423 margin_top: frame
424 .format
425 .top_margin
426 .unwrap_or(if is_blockquote { 4 } else { 0 }) as f32,
427 margin_bottom: frame
428 .format
429 .bottom_margin
430 .unwrap_or(if is_blockquote { 4 } else { 0 }) as f32,
431 margin_left: frame
432 .format
433 .left_margin
434 .unwrap_or(if is_blockquote { 16 } else { 0 }) as f32,
435 margin_right: frame.format.right_margin.unwrap_or(0) as f32,
436 padding: frame
437 .format
438 .padding
439 .unwrap_or(if is_blockquote { 8 } else { 0 }) as f32,
440 border_width: frame
441 .format
442 .border
443 .unwrap_or(if is_blockquote { 3 } else { 0 }) as f32,
444 border_style: if is_blockquote {
445 crate::layout::frame::FrameBorderStyle::LeftOnly
446 } else {
447 crate::layout::frame::FrameBorderStyle::Full
448 },
449 blocks,
450 tables,
451 frames,
452 }
453}