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 = s
326 .strip_prefix("rgba(")
327 .and_then(|s| s.strip_suffix(')'))
328 .or_else(|| s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')))?;
329
330 let parts: Vec<&str> = inner.split(',').collect();
331 match parts.len() {
332 3 => {
333 let r: u8 = parts[0].trim().parse().ok()?;
334 let g: u8 = parts[1].trim().parse().ok()?;
335 let b: u8 = parts[2].trim().parse().ok()?;
336 Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
337 }
338 4 => {
339 let r: u8 = parts[0].trim().parse().ok()?;
340 let g: u8 = parts[1].trim().parse().ok()?;
341 let b: u8 = parts[2].trim().parse().ok()?;
342 let a: f32 = parts[3].trim().parse().ok()?;
343 Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a])
344 }
345 _ => None,
346 }
347}
348
349fn convert_alignment(a: &text_document::Alignment) -> Alignment {
350 match a {
351 text_document::Alignment::Left => Alignment::Left,
352 text_document::Alignment::Right => Alignment::Right,
353 text_document::Alignment::Center => Alignment::Center,
354 text_document::Alignment::Justify => Alignment::Justify,
355 }
356}
357
358pub fn convert_table(table: &TableSnapshot) -> TableLayoutParams {
359 let column_widths: Vec<f32> = table.column_widths.iter().map(|&w| w as f32).collect();
360
361 let cells: Vec<CellLayoutParams> = table.cells.iter().map(convert_cell).collect();
362
363 TableLayoutParams {
364 table_id: table.table_id,
365 rows: table.rows,
366 columns: table.columns,
367 column_widths,
368 border_width: table.format.border.unwrap_or(1) as f32,
369 cell_spacing: table.format.cell_spacing.unwrap_or(0) as f32,
370 cell_padding: table.format.cell_padding.unwrap_or(4) as f32,
371 cells,
372 }
373}
374
375fn convert_cell(cell: &CellSnapshot) -> CellLayoutParams {
376 let blocks: Vec<BlockLayoutParams> = cell.blocks.iter().map(convert_block).collect();
377
378 let background_color = cell
379 .format
380 .background_color
381 .as_ref()
382 .and_then(|s| parse_css_color(s));
383
384 CellLayoutParams {
385 row: cell.row,
386 column: cell.column,
387 blocks,
388 background_color,
389 }
390}
391
392pub fn convert_frame(frame: &FrameSnapshot) -> FrameLayoutParams {
393 let mut blocks = Vec::new();
394 let mut tables = Vec::new();
395 let mut frames = Vec::new();
396
397 for (i, element) in frame.elements.iter().enumerate() {
398 match element {
399 FlowElementSnapshot::Block(block) => {
400 blocks.push(convert_block(block));
401 }
402 FlowElementSnapshot::Table(table) => {
403 tables.push((i, convert_table(table)));
404 }
405 FlowElementSnapshot::Frame(inner_frame) => {
406 frames.push((i, convert_frame(inner_frame)));
407 }
408 }
409 }
410
411 let position = match &frame.format.position {
412 Some(text_document::FramePosition::InFlow) | None => FramePosition::Inline,
413 Some(text_document::FramePosition::FloatLeft) => FramePosition::FloatLeft,
414 Some(text_document::FramePosition::FloatRight) => FramePosition::FloatRight,
415 };
416
417 let is_blockquote = frame.format.is_blockquote == Some(true);
418
419 FrameLayoutParams {
420 frame_id: frame.frame_id,
421 position,
422 width: frame.format.width.map(|w| w as f32),
423 height: frame.format.height.map(|h| h as f32),
424 margin_top: frame
425 .format
426 .top_margin
427 .unwrap_or(if is_blockquote { 4 } else { 0 }) as f32,
428 margin_bottom: frame
429 .format
430 .bottom_margin
431 .unwrap_or(if is_blockquote { 4 } else { 0 }) as f32,
432 margin_left: frame
433 .format
434 .left_margin
435 .unwrap_or(if is_blockquote { 16 } else { 0 }) as f32,
436 margin_right: frame.format.right_margin.unwrap_or(0) as f32,
437 padding: frame
438 .format
439 .padding
440 .unwrap_or(if is_blockquote { 8 } else { 0 }) as f32,
441 border_width: frame
442 .format
443 .border
444 .unwrap_or(if is_blockquote { 3 } else { 0 }) as f32,
445 border_style: if is_blockquote {
446 crate::layout::frame::FrameBorderStyle::LeftOnly
447 } else {
448 crate::layout::frame::FrameBorderStyle::Full
449 },
450 blocks,
451 tables,
452 frames,
453 }
454}