1use std::sync::Arc;
4
5use parking_lot::Mutex;
6
7use frontend::commands::{block_commands, frame_commands, inline_element_commands, list_commands};
8use frontend::common::types::EntityId;
9use frontend::inline_element::dtos::InlineContent;
10
11use crate::convert::to_usize;
12use crate::flow::{BlockSnapshot, FragmentContent, ListInfo, TableCellContext, TableCellRef};
13use crate::inner::TextDocumentInner;
14use crate::text_frame::TextFrame;
15use crate::text_list::TextList;
16use crate::text_table::TextTable;
17use crate::{BlockFormat, ListStyle, TextFormat};
18
19#[derive(Clone)]
26pub struct TextBlock {
27 pub(crate) doc: Arc<Mutex<TextDocumentInner>>,
28 pub(crate) block_id: usize,
29}
30
31impl TextBlock {
32 pub fn text(&self) -> String {
36 let inner = self.doc.lock();
37 block_commands::get_block(&inner.ctx, &(self.block_id as u64))
38 .ok()
39 .flatten()
40 .map(|b| b.plain_text)
41 .unwrap_or_default()
42 }
43
44 pub fn length(&self) -> usize {
46 let inner = self.doc.lock();
47 block_commands::get_block(&inner.ctx, &(self.block_id as u64))
48 .ok()
49 .flatten()
50 .map(|b| to_usize(b.text_length))
51 .unwrap_or(0)
52 }
53
54 pub fn is_empty(&self) -> bool {
56 let inner = self.doc.lock();
57 block_commands::get_block(&inner.ctx, &(self.block_id as u64))
58 .ok()
59 .flatten()
60 .map(|b| b.text_length == 0)
61 .unwrap_or(true)
62 }
63
64 pub fn is_valid(&self) -> bool {
66 let inner = self.doc.lock();
67 block_commands::get_block(&inner.ctx, &(self.block_id as u64))
68 .ok()
69 .flatten()
70 .is_some()
71 }
72
73 pub fn id(&self) -> usize {
77 self.block_id
78 }
79
80 pub fn position(&self) -> usize {
82 let inner = self.doc.lock();
83 block_commands::get_block(&inner.ctx, &(self.block_id as u64))
84 .ok()
85 .flatten()
86 .map(|b| to_usize(b.document_position))
87 .unwrap_or(0)
88 }
89
90 pub fn block_number(&self) -> usize {
94 let inner = self.doc.lock();
95 compute_block_number(&inner, self.block_id as u64)
96 }
97
98 pub fn next(&self) -> Option<TextBlock> {
101 let inner = self.doc.lock();
102 let all_blocks = block_commands::get_all_block(&inner.ctx).ok()?;
103 let mut sorted: Vec<_> = all_blocks.into_iter().collect();
104 sorted.sort_by_key(|b| b.document_position);
105 let idx = sorted.iter().position(|b| b.id == self.block_id as u64)?;
106 sorted.get(idx + 1).map(|b| TextBlock {
107 doc: Arc::clone(&self.doc),
108 block_id: b.id as usize,
109 })
110 }
111
112 pub fn previous(&self) -> Option<TextBlock> {
115 let inner = self.doc.lock();
116 let all_blocks = block_commands::get_all_block(&inner.ctx).ok()?;
117 let mut sorted: Vec<_> = all_blocks.into_iter().collect();
118 sorted.sort_by_key(|b| b.document_position);
119 let idx = sorted.iter().position(|b| b.id == self.block_id as u64)?;
120 if idx == 0 {
121 return None;
122 }
123 sorted.get(idx - 1).map(|b| TextBlock {
124 doc: Arc::clone(&self.doc),
125 block_id: b.id as usize,
126 })
127 }
128
129 pub fn frame(&self) -> TextFrame {
133 let inner = self.doc.lock();
134 let frame_id = find_parent_frame(&inner, self.block_id as u64);
135 TextFrame {
136 doc: Arc::clone(&self.doc),
137 frame_id: frame_id.map(|id| id as usize).unwrap_or(0),
138 }
139 }
140
141 pub fn table_cell(&self) -> Option<TableCellRef> {
147 let inner = self.doc.lock();
148 let frame_id = find_parent_frame(&inner, self.block_id as u64)?;
149
150 let frame_dto = frame_commands::get_frame(&inner.ctx, &frame_id)
153 .ok()
154 .flatten()?;
155
156 if let Some(table_entity_id) = frame_dto.table {
157 let table_dto =
161 frontend::commands::table_commands::get_table(&inner.ctx, &{ table_entity_id })
162 .ok()
163 .flatten()?;
164 for &cell_id in &table_dto.cells {
165 if let Some(cell_dto) =
166 frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{
167 cell_id
168 })
169 .ok()
170 .flatten()
171 && cell_dto.cell_frame == Some(frame_id)
172 {
173 return Some(TableCellRef {
174 table: TextTable {
175 doc: Arc::clone(&self.doc),
176 table_id: table_entity_id as usize,
177 },
178 row: to_usize(cell_dto.row),
179 column: to_usize(cell_dto.column),
180 });
181 }
182 }
183 }
184
185 let all_tables =
188 frontend::commands::table_commands::get_all_table(&inner.ctx).unwrap_or_default();
189 for table_dto in &all_tables {
190 for &cell_id in &table_dto.cells {
191 if let Some(cell_dto) =
192 frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{
193 cell_id
194 })
195 .ok()
196 .flatten()
197 && cell_dto.cell_frame == Some(frame_id)
198 {
199 return Some(TableCellRef {
200 table: TextTable {
201 doc: Arc::clone(&self.doc),
202 table_id: table_dto.id as usize,
203 },
204 row: to_usize(cell_dto.row),
205 column: to_usize(cell_dto.column),
206 });
207 }
208 }
209 }
210
211 None
212 }
213
214 pub fn block_format(&self) -> BlockFormat {
218 let inner = self.doc.lock();
219 block_commands::get_block(&inner.ctx, &(self.block_id as u64))
220 .ok()
221 .flatten()
222 .map(|b| BlockFormat::from(&b))
223 .unwrap_or_default()
224 }
225
226 pub fn char_format_at(&self, offset: usize) -> Option<TextFormat> {
233 let inner = self.doc.lock();
234 let fragments = build_fragments(&inner, self.block_id as u64);
235 for frag in &fragments {
236 match frag {
237 FragmentContent::Text {
238 format,
239 offset: frag_offset,
240 length,
241 ..
242 } => {
243 if offset >= *frag_offset && offset < frag_offset + length {
244 return Some(format.clone());
245 }
246 }
247 FragmentContent::Image {
248 format,
249 offset: frag_offset,
250 ..
251 } => {
252 if offset == *frag_offset {
253 return Some(format.clone());
254 }
255 }
256 }
257 }
258 None
259 }
260
261 pub fn fragments(&self) -> Vec<FragmentContent> {
265 let inner = self.doc.lock();
266 build_fragments(&inner, self.block_id as u64)
267 }
268
269 pub fn list(&self) -> Option<TextList> {
273 let inner = self.doc.lock();
274 let block_dto = block_commands::get_block(&inner.ctx, &(self.block_id as u64))
275 .ok()
276 .flatten()?;
277 let list_id = block_dto.list?;
278 Some(TextList {
279 doc: Arc::clone(&self.doc),
280 list_id: list_id as usize,
281 })
282 }
283
284 pub fn list_item_index(&self) -> Option<usize> {
286 let inner = self.doc.lock();
287 let block_dto = block_commands::get_block(&inner.ctx, &(self.block_id as u64))
288 .ok()
289 .flatten()?;
290 let list_id = block_dto.list?;
291 Some(compute_list_item_index(
292 &inner,
293 list_id,
294 self.block_id as u64,
295 ))
296 }
297
298 pub fn snapshot(&self) -> BlockSnapshot {
302 let inner = self.doc.lock();
303 build_block_snapshot(&inner, self.block_id as u64).unwrap_or_else(|| BlockSnapshot {
304 block_id: self.block_id,
305 position: 0,
306 length: 0,
307 text: String::new(),
308 fragments: Vec::new(),
309 block_format: BlockFormat::default(),
310 list_info: None,
311 parent_frame_id: None,
312 table_cell: None,
313 })
314 }
315}
316
317fn find_parent_frame(inner: &TextDocumentInner, block_id: u64) -> Option<EntityId> {
323 let all_frames = frame_commands::get_all_frame(&inner.ctx).ok()?;
324 let block_entity_id = block_id as EntityId;
325 for frame in &all_frames {
326 if frame.blocks.contains(&block_entity_id) {
327 return Some(frame.id as EntityId);
328 }
329 }
330 None
331}
332
333fn find_table_cell_context(inner: &TextDocumentInner, block_id: u64) -> Option<TableCellContext> {
336 let frame_id = find_parent_frame(inner, block_id)?;
337
338 let frame_dto = frame_commands::get_frame(&inner.ctx, &frame_id)
339 .ok()
340 .flatten()?;
341
342 if let Some(table_entity_id) = frame_dto.table {
344 let table_dto =
345 frontend::commands::table_commands::get_table(&inner.ctx, &{ table_entity_id })
346 .ok()
347 .flatten()?;
348 for &cell_id in &table_dto.cells {
349 if let Some(cell_dto) =
350 frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
351 .ok()
352 .flatten()
353 && cell_dto.cell_frame == Some(frame_id)
354 {
355 return Some(TableCellContext {
356 table_id: table_entity_id as usize,
357 row: to_usize(cell_dto.row),
358 column: to_usize(cell_dto.column),
359 });
360 }
361 }
362 }
363
364 let all_tables =
366 frontend::commands::table_commands::get_all_table(&inner.ctx).unwrap_or_default();
367 for table_dto in &all_tables {
368 for &cell_id in &table_dto.cells {
369 if let Some(cell_dto) =
370 frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
371 .ok()
372 .flatten()
373 && cell_dto.cell_frame == Some(frame_id)
374 {
375 return Some(TableCellContext {
376 table_id: table_dto.id as usize,
377 row: to_usize(cell_dto.row),
378 column: to_usize(cell_dto.column),
379 });
380 }
381 }
382 }
383
384 None
385}
386
387fn compute_block_number(inner: &TextDocumentInner, block_id: u64) -> usize {
389 let all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
390 let mut sorted: Vec<_> = all_blocks.iter().collect();
391 sorted.sort_by_key(|b| b.document_position);
392 sorted.iter().position(|b| b.id == block_id).unwrap_or(0)
393}
394
395pub(crate) fn build_fragments(inner: &TextDocumentInner, block_id: u64) -> Vec<FragmentContent> {
398 let fragments = build_raw_fragments(inner, block_id);
399
400 if let Some(ref hl) = inner.highlight
401 && let Some(block_hl) = hl.blocks.get(&(block_id as usize))
402 && !block_hl.spans.is_empty()
403 {
404 return crate::highlight::merge_highlight_spans(fragments, &block_hl.spans);
405 }
406
407 fragments
408}
409
410fn build_raw_fragments(inner: &TextDocumentInner, block_id: u64) -> Vec<FragmentContent> {
412 let block_dto = match block_commands::get_block(&inner.ctx, &block_id)
413 .ok()
414 .flatten()
415 {
416 Some(b) => b,
417 None => return Vec::new(),
418 };
419
420 let element_ids = &block_dto.elements;
421 let elements: Vec<_> = element_ids
422 .iter()
423 .filter_map(|&id| {
424 inline_element_commands::get_inline_element(&inner.ctx, &{ id })
425 .ok()
426 .flatten()
427 })
428 .collect();
429
430 let mut fragments = Vec::with_capacity(elements.len());
431 let mut offset: usize = 0;
432
433 for el in &elements {
434 let format = TextFormat::from(el);
435 match &el.content {
436 InlineContent::Text(text) => {
437 let length = text.chars().count();
438 let word_starts = compute_word_starts(text);
439 fragments.push(FragmentContent::Text {
440 text: text.clone(),
441 format,
442 offset,
443 length,
444 element_id: el.id,
445 word_starts,
446 });
447 offset += length;
448 }
449 InlineContent::Image {
450 name,
451 width,
452 height,
453 quality,
454 } => {
455 fragments.push(FragmentContent::Image {
456 name: name.clone(),
457 width: *width as u32,
458 height: *height as u32,
459 quality: *quality as u32,
460 format,
461 offset,
462 element_id: el.id,
463 });
464 offset += 1; }
466 InlineContent::Empty => {
467 }
469 }
470 }
471
472 fragments
473}
474
475fn compute_word_starts(text: &str) -> Vec<u8> {
481 use unicode_segmentation::UnicodeSegmentation;
482 let mut result = Vec::new();
483 let mut byte_to_char: Vec<(usize, usize)> = Vec::new();
487 for (ci, (bi, _)) in text.char_indices().enumerate() {
488 byte_to_char.push((bi, ci));
489 }
490 for (byte_off, _word) in text.unicode_word_indices() {
491 let char_idx = byte_to_char
492 .iter()
493 .find(|(bi, _)| *bi == byte_off)
494 .map(|(_, ci)| *ci)
495 .unwrap_or(0);
496 if let Ok(idx) = u8::try_from(char_idx) {
503 result.push(idx);
504 } else {
505 break;
506 }
507 }
508 result
509}
510
511fn compute_list_item_index(inner: &TextDocumentInner, list_id: EntityId, block_id: u64) -> usize {
513 let all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
514 let mut list_blocks: Vec<_> = all_blocks
515 .iter()
516 .filter(|b| b.list == Some(list_id))
517 .collect();
518 list_blocks.sort_by_key(|b| b.document_position);
519 list_blocks
520 .iter()
521 .position(|b| b.id == block_id)
522 .unwrap_or(0)
523}
524
525pub(crate) fn format_list_marker(
527 list_dto: &frontend::list::dtos::ListDto,
528 item_index: usize,
529) -> String {
530 let number = item_index + 1; let marker_body = match list_dto.style {
532 ListStyle::Disc => "\u{2022}".to_string(), ListStyle::Circle => "\u{25E6}".to_string(), ListStyle::Square => "\u{25AA}".to_string(), ListStyle::Decimal => format!("{number}"),
536 ListStyle::LowerAlpha => {
537 if number <= 26 {
538 ((b'a' + (number as u8 - 1)) as char).to_string()
539 } else {
540 format!("{number}")
541 }
542 }
543 ListStyle::UpperAlpha => {
544 if number <= 26 {
545 ((b'A' + (number as u8 - 1)) as char).to_string()
546 } else {
547 format!("{number}")
548 }
549 }
550 ListStyle::LowerRoman => to_roman_lower(number),
551 ListStyle::UpperRoman => to_roman_upper(number),
552 };
553 format!("{}{marker_body}{}", list_dto.prefix, list_dto.suffix)
554}
555
556fn to_roman_upper(mut n: usize) -> String {
557 const VALUES: &[(usize, &str)] = &[
558 (1000, "M"),
559 (900, "CM"),
560 (500, "D"),
561 (400, "CD"),
562 (100, "C"),
563 (90, "XC"),
564 (50, "L"),
565 (40, "XL"),
566 (10, "X"),
567 (9, "IX"),
568 (5, "V"),
569 (4, "IV"),
570 (1, "I"),
571 ];
572 let mut result = String::new();
573 for &(val, sym) in VALUES {
574 while n >= val {
575 result.push_str(sym);
576 n -= val;
577 }
578 }
579 result
580}
581
582fn to_roman_lower(n: usize) -> String {
583 to_roman_upper(n).to_lowercase()
584}
585
586fn build_list_info(
588 inner: &TextDocumentInner,
589 block_dto: &frontend::block::dtos::BlockDto,
590) -> Option<ListInfo> {
591 let list_id = block_dto.list?;
592 let list_dto = list_commands::get_list(&inner.ctx, &{ list_id })
593 .ok()
594 .flatten()?;
595
596 let item_index = compute_list_item_index(inner, list_id, block_dto.id);
597 let marker = format_list_marker(&list_dto, item_index);
598
599 Some(ListInfo {
600 list_id: list_id as usize,
601 style: list_dto.style.clone(),
602 indent: list_dto.indent as u8,
603 marker,
604 item_index,
605 })
606}
607
608pub(crate) fn build_block_snapshot(
610 inner: &TextDocumentInner,
611 block_id: u64,
612) -> Option<BlockSnapshot> {
613 build_block_snapshot_with_position(inner, block_id, None)
614}
615
616pub(crate) fn build_block_snapshot_with_position(
620 inner: &TextDocumentInner,
621 block_id: u64,
622 computed_position: Option<usize>,
623) -> Option<BlockSnapshot> {
624 let block_dto = block_commands::get_block(&inner.ctx, &block_id)
625 .ok()
626 .flatten()?;
627
628 let fragments = build_fragments(inner, block_id);
629 let block_format = BlockFormat::from(&block_dto);
630 let list_info = build_list_info(inner, &block_dto);
631
632 let parent_frame_id = find_parent_frame(inner, block_id).map(|id| id as usize);
633 let table_cell = find_table_cell_context(inner, block_id);
634
635 let position = computed_position.unwrap_or_else(|| to_usize(block_dto.document_position));
636
637 Some(BlockSnapshot {
638 block_id: block_id as usize,
639 position,
640 length: to_usize(block_dto.text_length),
641 text: block_dto.plain_text,
642 fragments,
643 block_format,
644 list_info,
645 parent_frame_id,
646 table_cell,
647 })
648}
649
650pub(crate) fn build_blocks_snapshot_for_frame(
652 inner: &TextDocumentInner,
653 frame_id: u64,
654) -> Vec<BlockSnapshot> {
655 let frame_dto = match frame_commands::get_frame(&inner.ctx, &(frame_id as EntityId))
656 .ok()
657 .flatten()
658 {
659 Some(f) => f,
660 None => return Vec::new(),
661 };
662
663 let mut block_dtos: Vec<_> = frame_dto
664 .blocks
665 .iter()
666 .filter_map(|&id| {
667 block_commands::get_block(&inner.ctx, &{ id })
668 .ok()
669 .flatten()
670 })
671 .collect();
672 block_dtos.sort_by_key(|b| b.document_position);
673
674 block_dtos
675 .iter()
676 .filter_map(|b| build_block_snapshot(inner, b.id))
677 .collect()
678}
679
680pub(crate) fn build_blocks_snapshot_for_frame_with_positions(
686 inner: &TextDocumentInner,
687 frame_id: u64,
688 start_pos: usize,
689) -> (Vec<BlockSnapshot>, usize) {
690 let frame_dto = match frame_commands::get_frame(&inner.ctx, &(frame_id as EntityId))
691 .ok()
692 .flatten()
693 {
694 Some(f) => f,
695 None => return (Vec::new(), start_pos),
696 };
697
698 let mut block_dtos: Vec<_> = frame_dto
699 .blocks
700 .iter()
701 .filter_map(|&id| {
702 block_commands::get_block(&inner.ctx, &{ id })
703 .ok()
704 .flatten()
705 })
706 .collect();
707 block_dtos.sort_by_key(|b| b.document_position);
708
709 let mut running_pos = start_pos;
710 let mut snapshots = Vec::with_capacity(block_dtos.len());
711 for b in &block_dtos {
712 if let Some(snap) = build_block_snapshot_with_position(inner, b.id, Some(running_pos)) {
713 running_pos += snap.length + 1; snapshots.push(snap);
715 }
716 }
717 (snapshots, running_pos)
718}