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