1use text_document::{
7 BlockSnapshot, CellSnapshot, FlowElementSnapshot, FlowSnapshot, FragmentContent, FrameSnapshot,
8 TableSnapshot,
9};
10
11use crate::layout::block::{BlockLayoutParams, FragmentParams, PaintSpan};
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#[derive(Clone, Copy)]
26pub struct BridgeOptions {
27 pub code_block_background: [f32; 4],
32 pub code_block_foreground: Option<[f32; 4]>,
38 pub echo_char: Option<char>,
48}
49
50impl Default for BridgeOptions {
51 fn default() -> Self {
52 Self {
53 code_block_background: [0.95, 0.95, 0.95, 1.0],
54 code_block_foreground: None,
55 echo_char: None,
56 }
57 }
58}
59
60pub fn convert_flow(flow: &FlowSnapshot) -> FlowElements {
64 convert_flow_with(flow, &BridgeOptions::default())
65}
66
67pub fn convert_flow_with(flow: &FlowSnapshot, opts: &BridgeOptions) -> FlowElements {
70 let mut blocks = Vec::new();
71 let mut tables = Vec::new();
72 let mut frames = Vec::new();
73
74 for (i, element) in flow.elements.iter().enumerate() {
75 match element {
76 FlowElementSnapshot::Block(block) => {
77 blocks.push((i, convert_block_with(block, opts)));
78 }
79 FlowElementSnapshot::Table(table) => {
80 tables.push((i, convert_table_with(table, opts)));
81 }
82 FlowElementSnapshot::Frame(frame) => {
83 frames.push((i, convert_frame_with(frame, opts)));
84 }
85 }
86 }
87
88 FlowElements {
89 blocks,
90 tables,
91 frames,
92 }
93}
94
95pub struct FlowElements {
97 pub blocks: Vec<(usize, BlockLayoutParams)>,
99 pub tables: Vec<(usize, TableLayoutParams)>,
100 pub frames: Vec<(usize, FrameLayoutParams)>,
101}
102
103pub fn convert_block(block: &BlockSnapshot) -> BlockLayoutParams {
104 convert_block_with(block, &BridgeOptions::default())
105}
106
107pub fn convert_block_with(block: &BlockSnapshot, opts: &BridgeOptions) -> BlockLayoutParams {
110 let alignment = block
111 .block_format
112 .alignment
113 .as_ref()
114 .map(convert_alignment)
115 .unwrap_or_default();
116
117 let heading_scale = match block.block_format.heading_level {
118 Some(1) => 2.0,
119 Some(2) => 1.5,
120 Some(3) => 1.25,
121 Some(4) => 1.1,
122 _ => 1.0,
123 };
124
125 let char_to_byte: Vec<usize> = block
143 .text
144 .char_indices()
145 .map(|(b, _)| b)
146 .chain(std::iter::once(block.text.len()))
147 .collect();
148 let fragments: Vec<FragmentParams> = block
149 .fragments
150 .iter()
151 .map(|f| {
152 let char_offset = match f {
153 FragmentContent::Text { offset, .. } => *offset,
154 FragmentContent::Image { offset, .. } => *offset,
155 };
156 let byte_offset = char_to_byte
157 .get(char_offset)
158 .copied()
159 .unwrap_or(block.text.len());
160 convert_fragment(f, heading_scale, opts, byte_offset)
161 })
162 .collect();
163
164 let indent_level = block.block_format.indent.unwrap_or(0) as f32;
165
166 let (list_marker, list_indent) = if let Some(ref info) = block.list_info {
167 let list_indent_level = info.indent as f32;
168 (
169 info.marker.clone(),
170 DEFAULT_LIST_INDENT + list_indent_level * INDENT_PER_LEVEL,
171 )
172 } else {
173 (String::new(), indent_level * INDENT_PER_LEVEL)
174 };
175
176 let checkbox = match block.block_format.marker {
177 Some(text_document::MarkerType::Checked) => Some(true),
178 Some(text_document::MarkerType::Unchecked) => Some(false),
179 _ => None,
180 };
181
182 let mut params = BlockLayoutParams {
183 block_id: block.block_id,
184 position: block.position,
185 text: block.text.clone(),
186 fragments,
187 alignment,
188 top_margin: block.block_format.top_margin.unwrap_or(0) as f32,
189 bottom_margin: block.block_format.bottom_margin.unwrap_or(0) as f32,
190 left_margin: block.block_format.left_margin.unwrap_or(0) as f32,
191 right_margin: block.block_format.right_margin.unwrap_or(0) as f32,
192 text_indent: block.block_format.text_indent.unwrap_or(0) as f32,
193 list_marker,
194 list_indent,
195 tab_positions: block
196 .block_format
197 .tab_positions
198 .iter()
199 .map(|&t| t as f32)
200 .collect(),
201 line_height_multiplier: block.block_format.line_height,
202 non_breakable_lines: block.block_format.non_breakable_lines.unwrap_or(false)
203 || block.block_format.is_code_block == Some(true),
204 checkbox,
205 background_color: block
206 .block_format
207 .background_color
208 .as_ref()
209 .and_then(|s| parse_css_color(s))
210 .or_else(|| {
211 if block.block_format.is_code_block == Some(true) {
212 Some(opts.code_block_background)
213 } else {
214 None
215 }
216 }),
217 };
218
219 if let Some(echo) = opts.echo_char {
220 mask_block_params(&mut params, echo);
221 }
222
223 params
224}
225
226fn mask_block_params(params: &mut BlockLayoutParams, echo: char) {
235 if params.fragments.is_empty() {
236 params.text = echo.to_string().repeat(params.text.chars().count());
237 return;
238 }
239 let mut masked_block = String::new();
240 let mut byte_cursor = 0usize;
241 for frag in params.fragments.iter_mut() {
242 frag.offset = byte_cursor;
243 if frag.image_name.is_some() {
244 masked_block.push_str(&frag.text);
247 byte_cursor += frag.text.len();
248 continue;
249 }
250 let masked = echo.to_string().repeat(frag.text.chars().count());
251 byte_cursor += masked.len();
252 masked_block.push_str(&masked);
253 frag.text = masked;
254 }
255 params.text = masked_block;
256}
257
258fn convert_fragment(
259 frag: &FragmentContent,
260 heading_scale: f32,
261 opts: &BridgeOptions,
262 byte_offset: usize,
263) -> FragmentParams {
264 match frag {
265 FragmentContent::Text {
266 text,
267 format,
268 length,
269 ..
270 } => {
271 let is_monospace = format
276 .font_family
277 .as_deref()
278 .map(|f| f.eq_ignore_ascii_case("monospace"))
279 .unwrap_or(false);
280 let foreground_color =
281 format
282 .foreground_color
283 .as_ref()
284 .map(convert_color)
285 .or(if is_monospace {
286 opts.code_block_foreground
287 } else {
288 None
289 });
290 FragmentParams {
291 text: text.clone(),
292 offset: byte_offset,
293 length: *length,
294 font_family: format.font_family.clone(),
295 font_weight: format.font_weight,
296 font_bold: format.font_bold,
297 font_italic: format.font_italic,
298 font_point_size: if heading_scale != 1.0 {
299 Some((format.font_point_size.unwrap_or(16) as f32 * heading_scale) as u32)
301 } else {
302 format.font_point_size
303 },
304 underline_style: convert_underline_style(format),
305 overline: format.font_overline.unwrap_or(false),
306 strikeout: format.font_strikeout.unwrap_or(false),
307 is_link: format.is_anchor.unwrap_or(false),
308 letter_spacing: format.letter_spacing.unwrap_or(0) as f32,
309 word_spacing: format.word_spacing.unwrap_or(0) as f32,
310 foreground_color,
311 underline_color: format.underline_color.as_ref().map(convert_color),
312 background_color: format.background_color.as_ref().map(convert_color),
313 anchor_href: format.anchor_href.clone(),
314 tooltip: format.tooltip.clone(),
315 vertical_alignment: convert_vertical_alignment(format),
316 image_name: None,
317 image_width: 0.0,
318 image_height: 0.0,
319 }
320 }
321 FragmentContent::Image {
322 name,
323 width,
324 height,
325 quality: _,
326 format,
327 ..
328 } => FragmentParams {
329 text: "\u{FFFC}".to_string(),
330 offset: byte_offset,
331 length: 1,
332 font_family: None,
333 font_weight: None,
334 font_bold: None,
335 font_italic: None,
336 font_point_size: None,
337 underline_style: crate::types::UnderlineStyle::None,
338 overline: false,
339 strikeout: false,
340 is_link: format.is_anchor.unwrap_or(false),
341 letter_spacing: 0.0,
342 word_spacing: 0.0,
343 foreground_color: None,
344 underline_color: None,
345 background_color: None,
346 anchor_href: format.anchor_href.clone(),
347 tooltip: format.tooltip.clone(),
348 vertical_alignment: crate::types::VerticalAlignment::Normal,
349 image_name: Some(name.clone()),
350 image_width: *width as f32,
351 image_height: *height as f32,
352 },
353 }
354}
355
356fn convert_vertical_alignment(
357 format: &text_document::TextFormat,
358) -> crate::types::VerticalAlignment {
359 use crate::types::VerticalAlignment;
360 match format.vertical_alignment {
361 Some(text_document::CharVerticalAlignment::SuperScript) => VerticalAlignment::SuperScript,
362 Some(text_document::CharVerticalAlignment::SubScript) => VerticalAlignment::SubScript,
363 _ => VerticalAlignment::Normal,
364 }
365}
366
367fn convert_underline_style(format: &text_document::TextFormat) -> crate::types::UnderlineStyle {
368 use crate::types::UnderlineStyle;
369 match &format.underline_style {
370 Some(s) => convert_underline_style_value(s),
371 None => {
372 if format.font_underline.unwrap_or(false) {
373 UnderlineStyle::Single
374 } else {
375 UnderlineStyle::None
376 }
377 }
378 }
379}
380
381fn convert_underline_style_value(
383 s: &text_document::UnderlineStyle,
384) -> crate::types::UnderlineStyle {
385 use crate::types::UnderlineStyle;
386 match s {
387 text_document::UnderlineStyle::SingleUnderline => UnderlineStyle::Single,
388 text_document::UnderlineStyle::DashUnderline => UnderlineStyle::Dash,
389 text_document::UnderlineStyle::DotLine => UnderlineStyle::Dot,
390 text_document::UnderlineStyle::DashDotLine => UnderlineStyle::DashDot,
391 text_document::UnderlineStyle::DashDotDotLine => UnderlineStyle::DashDotDot,
392 text_document::UnderlineStyle::WaveUnderline => UnderlineStyle::Wave,
393 text_document::UnderlineStyle::SpellCheckUnderline => UnderlineStyle::SpellCheck,
394 text_document::UnderlineStyle::NoUnderline => UnderlineStyle::None,
395 }
396}
397
398pub fn convert_paint_spans(block: &BlockSnapshot) -> Vec<PaintSpan> {
404 block
405 .paint_highlights
406 .iter()
407 .map(|h| {
408 let underline_style = match &h.underline_style {
409 Some(s) => Some(convert_underline_style_value(s)),
410 None => match h.font_underline {
411 Some(true) => Some(crate::types::UnderlineStyle::Single),
412 Some(false) => Some(crate::types::UnderlineStyle::None),
413 None => None,
414 },
415 };
416 PaintSpan {
417 char_start: h.start,
418 char_end: h.start + h.length,
419 foreground_color: h.foreground_color.as_ref().map(convert_color),
420 underline_color: h.underline_color.as_ref().map(convert_color),
421 background_color: h.background_color.as_ref().map(convert_color),
422 underline_style,
423 overline: h.font_overline,
424 strikeout: h.font_strikeout,
425 }
426 })
427 .collect()
428}
429
430pub fn collect_paint_spans(
435 flow: &FlowSnapshot,
436) -> std::collections::HashMap<usize, Vec<PaintSpan>> {
437 let mut out = std::collections::HashMap::new();
438 for el in &flow.elements {
439 collect_paint_spans_element(el, &mut out);
440 }
441 out
442}
443
444fn collect_paint_spans_element(
445 el: &FlowElementSnapshot,
446 out: &mut std::collections::HashMap<usize, Vec<PaintSpan>>,
447) {
448 match el {
449 FlowElementSnapshot::Block(b) => {
450 if !b.paint_highlights.is_empty() {
451 out.insert(b.block_id, convert_paint_spans(b));
452 }
453 }
454 FlowElementSnapshot::Table(t) => {
455 for c in &t.cells {
456 for b in &c.blocks {
457 if !b.paint_highlights.is_empty() {
458 out.insert(b.block_id, convert_paint_spans(b));
459 }
460 }
461 }
462 }
463 FlowElementSnapshot::Frame(f) => {
464 for e in &f.elements {
465 collect_paint_spans_element(e, out);
466 }
467 }
468 }
469}
470
471fn convert_color(c: &text_document::Color) -> [f32; 4] {
472 [
473 c.red as f32 / 255.0,
474 c.green as f32 / 255.0,
475 c.blue as f32 / 255.0,
476 c.alpha as f32 / 255.0,
477 ]
478}
479
480fn parse_css_color(s: &str) -> Option<[f32; 4]> {
485 let s = s.trim();
486
487 match s.to_ascii_lowercase().as_str() {
489 "transparent" => return Some([0.0, 0.0, 0.0, 0.0]),
490 "black" => return Some([0.0, 0.0, 0.0, 1.0]),
491 "white" => return Some([1.0, 1.0, 1.0, 1.0]),
492 "red" => return Some([1.0, 0.0, 0.0, 1.0]),
493 "green" => return Some([0.0, 128.0 / 255.0, 0.0, 1.0]),
494 "blue" => return Some([0.0, 0.0, 1.0, 1.0]),
495 "yellow" => return Some([1.0, 1.0, 0.0, 1.0]),
496 "cyan" | "aqua" => return Some([0.0, 1.0, 1.0, 1.0]),
497 "magenta" | "fuchsia" => return Some([1.0, 0.0, 1.0, 1.0]),
498 "gray" | "grey" => return Some([128.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0, 1.0]),
499 _ => {}
500 }
501
502 if let Some(hex) = s.strip_prefix('#') {
504 let hex = hex.trim();
505 return match hex.len() {
506 3 => {
507 let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
509 let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
510 let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
511 Some([
512 (r * 17) as f32 / 255.0,
513 (g * 17) as f32 / 255.0,
514 (b * 17) as f32 / 255.0,
515 1.0,
516 ])
517 }
518 4 => {
519 let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
521 let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
522 let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
523 let a = u8::from_str_radix(&hex[3..4], 16).ok()?;
524 Some([
525 (r * 17) as f32 / 255.0,
526 (g * 17) as f32 / 255.0,
527 (b * 17) as f32 / 255.0,
528 (a * 17) as f32 / 255.0,
529 ])
530 }
531 6 => {
532 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
534 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
535 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
536 Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
537 }
538 8 => {
539 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
541 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
542 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
543 let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
544 Some([
545 r as f32 / 255.0,
546 g as f32 / 255.0,
547 b as f32 / 255.0,
548 a as f32 / 255.0,
549 ])
550 }
551 _ => None,
552 };
553 }
554
555 let inner = s
557 .strip_prefix("rgba(")
558 .and_then(|s| s.strip_suffix(')'))
559 .or_else(|| s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')))?;
560
561 let parts: Vec<&str> = inner.split(',').collect();
562 match parts.len() {
563 3 => {
564 let r: u8 = parts[0].trim().parse().ok()?;
565 let g: u8 = parts[1].trim().parse().ok()?;
566 let b: u8 = parts[2].trim().parse().ok()?;
567 Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
568 }
569 4 => {
570 let r: u8 = parts[0].trim().parse().ok()?;
571 let g: u8 = parts[1].trim().parse().ok()?;
572 let b: u8 = parts[2].trim().parse().ok()?;
573 let a: f32 = parts[3].trim().parse().ok()?;
574 Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a])
575 }
576 _ => None,
577 }
578}
579
580fn convert_alignment(a: &text_document::Alignment) -> Alignment {
581 match a {
582 text_document::Alignment::Left => Alignment::Left,
583 text_document::Alignment::Right => Alignment::Right,
584 text_document::Alignment::Center => Alignment::Center,
585 text_document::Alignment::Justify => Alignment::Justify,
586 }
587}
588
589pub fn convert_table(table: &TableSnapshot) -> TableLayoutParams {
590 convert_table_with(table, &BridgeOptions::default())
591}
592
593pub fn convert_table_with(table: &TableSnapshot, opts: &BridgeOptions) -> TableLayoutParams {
594 let column_widths: Vec<f32> = table.column_widths.iter().map(|&w| w as f32).collect();
595
596 let cells: Vec<CellLayoutParams> = table.cells.iter().map(|c| convert_cell(c, opts)).collect();
597
598 TableLayoutParams {
599 table_id: table.table_id,
600 rows: table.rows,
601 columns: table.columns,
602 column_widths,
603 border_width: table.format.border.unwrap_or(1) as f32,
604 cell_spacing: table.format.cell_spacing.unwrap_or(0) as f32,
605 cell_padding: table.format.cell_padding.unwrap_or(4) as f32,
606 cells,
607 }
608}
609
610fn convert_cell(cell: &CellSnapshot, opts: &BridgeOptions) -> CellLayoutParams {
611 let blocks: Vec<BlockLayoutParams> = cell
612 .blocks
613 .iter()
614 .map(|b| convert_block_with(b, opts))
615 .collect();
616
617 let background_color = cell
618 .format
619 .background_color
620 .as_ref()
621 .and_then(|s| parse_css_color(s));
622
623 CellLayoutParams {
624 row: cell.row,
625 column: cell.column,
626 blocks,
627 background_color,
628 }
629}
630
631pub fn convert_frame(frame: &FrameSnapshot) -> FrameLayoutParams {
632 convert_frame_with(frame, &BridgeOptions::default())
633}
634
635pub fn convert_frame_with(frame: &FrameSnapshot, opts: &BridgeOptions) -> FrameLayoutParams {
636 let mut blocks = Vec::new();
637 let mut tables = Vec::new();
638 let mut frames = Vec::new();
639
640 for (i, element) in frame.elements.iter().enumerate() {
641 match element {
642 FlowElementSnapshot::Block(block) => {
643 blocks.push(convert_block_with(block, opts));
644 }
645 FlowElementSnapshot::Table(table) => {
646 tables.push((i, convert_table_with(table, opts)));
647 }
648 FlowElementSnapshot::Frame(inner_frame) => {
649 frames.push((i, convert_frame_with(inner_frame, opts)));
650 }
651 }
652 }
653
654 let position = match &frame.format.position {
655 Some(text_document::FramePosition::InFlow) | None => FramePosition::Inline,
656 Some(text_document::FramePosition::FloatLeft) => FramePosition::FloatLeft,
657 Some(text_document::FramePosition::FloatRight) => FramePosition::FloatRight,
658 };
659
660 let is_blockquote = frame.format.is_blockquote == Some(true);
661
662 FrameLayoutParams {
663 frame_id: frame.frame_id,
664 position,
665 width: frame.format.width.map(|w| w as f32),
666 height: frame.format.height.map(|h| h as f32),
667 margin_top: frame
668 .format
669 .top_margin
670 .unwrap_or(if is_blockquote { 4 } else { 0 }) as f32,
671 margin_bottom: frame
672 .format
673 .bottom_margin
674 .unwrap_or(if is_blockquote { 4 } else { 0 }) as f32,
675 margin_left: frame
676 .format
677 .left_margin
678 .unwrap_or(if is_blockquote { 16 } else { 0 }) as f32,
679 margin_right: frame.format.right_margin.unwrap_or(0) as f32,
680 padding: frame
681 .format
682 .padding
683 .unwrap_or(if is_blockquote { 8 } else { 0 }) as f32,
684 border_width: frame
685 .format
686 .border
687 .unwrap_or(if is_blockquote { 3 } else { 0 }) as f32,
688 border_style: if is_blockquote {
689 crate::layout::frame::FrameBorderStyle::LeftOnly
690 } else {
691 crate::layout::frame::FrameBorderStyle::Full
692 },
693 blocks,
694 tables,
695 frames,
696 }
697}