Skip to main content

text_document/
text_block.rs

1//! Read-only block (paragraph) handle.
2
3use 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, 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/// A lightweight, read-only handle to a single block (paragraph).
20///
21/// Holds a stable entity ID — the handle remains valid across edits
22/// that insert or remove other blocks. Each method acquires the
23/// document lock independently. For consistent reads across multiple
24/// fields, use [`snapshot()`](TextBlock::snapshot).
25#[derive(Clone)]
26pub struct TextBlock {
27    pub(crate) doc: Arc<Mutex<TextDocumentInner>>,
28    pub(crate) block_id: usize,
29}
30
31impl TextBlock {
32    // ── Content ──────────────────────────────────────────────
33
34    /// Block's plain text. O(1).
35    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    /// Character count. O(1).
45    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    /// `length() == 0`. O(1).
55    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    /// Block entity still exists in the database. O(1).
65    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    // ── Identity and Position ────────────────────────────────
74
75    /// Stable entity ID (stored in the handle). O(1).
76    pub fn id(&self) -> usize {
77        self.block_id
78    }
79
80    /// Character offset from `Block.document_position`. O(1).
81    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    /// Global 0-indexed block number. **O(n)**: requires scanning all blocks
91    /// sorted by `document_position`. Prefer [`id()`](TextBlock::id) for
92    /// identity and [`position()`](TextBlock::position) for ordering.
93    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    /// The next block in document order. **O(n)**.
99    /// Returns `None` if this is the last block.
100    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    /// The previous block in document order. **O(n)**.
113    /// Returns `None` if this is the first block.
114    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    // ── Structural Context ───────────────────────────────────
130
131    /// Parent frame. O(1).
132    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    /// If inside a table cell, returns table and cell coordinates.
142    ///
143    /// Finds the block's parent frame, then checks if any table cell
144    /// references that frame as its `cell_frame`. If so, identifies the
145    /// owning table.
146    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        // Check if this frame is referenced as a cell_frame by any table cell.
151        // First try the fast path: if the frame has a `table` field, use it.
152        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            // This frame is a table anchor frame (not a cell frame).
158            // Anchor frames don't contain blocks directly — cell frames do.
159            // So this path shouldn't match, but check cells just in case.
160            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        // Slow path: this frame has no `table` field (cell frames don't).
186        // Scan all tables to find if any cell references this frame.
187        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    // ── Formatting ──────────────────────────────────────────
215
216    /// Block format (alignment, margins, indent, heading level, marker, tabs). O(1).
217    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    /// Character format at a block-relative character offset. **O(k)**
227    /// where k = number of InlineElements.
228    ///
229    /// Returns the [`TextFormat`] of the fragment containing the given
230    /// offset. Returns `None` if the offset is out of range or the
231    /// block has no fragments.
232    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    // ── Fragments ───────────────────────────────────────────
262
263    /// All formatting runs in one call. O(k) where k = number of InlineElements.
264    pub fn fragments(&self) -> Vec<FragmentContent> {
265        let inner = self.doc.lock();
266        build_fragments(&inner, self.block_id as u64)
267    }
268
269    // ── List Membership ─────────────────────────────────────
270
271    /// List this block belongs to. O(1).
272    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    /// 0-based position within its list. **O(n)** where n = total blocks.
285    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    // ── Snapshot ─────────────────────────────────────────────
299
300    /// All layout-relevant data in one lock acquisition. O(k+n).
301    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        })
312    }
313}
314
315// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
316// Internal helpers (called while lock is held)
317// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
318
319/// Find the parent frame of a block by scanning all frames.
320fn find_parent_frame(inner: &TextDocumentInner, block_id: u64) -> Option<EntityId> {
321    let all_frames = frame_commands::get_all_frame(&inner.ctx).ok()?;
322    let block_entity_id = block_id as EntityId;
323    for frame in &all_frames {
324        if frame.blocks.contains(&block_entity_id) {
325            return Some(frame.id as EntityId);
326        }
327    }
328    None
329}
330
331/// Compute 0-indexed block number by scanning all blocks sorted by document_position.
332fn compute_block_number(inner: &TextDocumentInner, block_id: u64) -> usize {
333    let all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
334    let mut sorted: Vec<_> = all_blocks.iter().collect();
335    sorted.sort_by_key(|b| b.document_position);
336    sorted.iter().position(|b| b.id == block_id).unwrap_or(0)
337}
338
339/// Build fragments for a block from its InlineElements.
340pub(crate) fn build_fragments(inner: &TextDocumentInner, block_id: u64) -> Vec<FragmentContent> {
341    let block_dto = match block_commands::get_block(&inner.ctx, &block_id)
342        .ok()
343        .flatten()
344    {
345        Some(b) => b,
346        None => return Vec::new(),
347    };
348
349    let element_ids = &block_dto.elements;
350    let elements: Vec<_> = element_ids
351        .iter()
352        .filter_map(|&id| {
353            inline_element_commands::get_inline_element(&inner.ctx, &{ id })
354                .ok()
355                .flatten()
356        })
357        .collect();
358
359    let mut fragments = Vec::with_capacity(elements.len());
360    let mut offset: usize = 0;
361
362    for el in &elements {
363        let format = TextFormat::from(el);
364        match &el.content {
365            InlineContent::Text(text) => {
366                let length = text.chars().count();
367                fragments.push(FragmentContent::Text {
368                    text: text.clone(),
369                    format,
370                    offset,
371                    length,
372                });
373                offset += length;
374            }
375            InlineContent::Image {
376                name,
377                width,
378                height,
379                quality,
380            } => {
381                fragments.push(FragmentContent::Image {
382                    name: name.clone(),
383                    width: *width as u32,
384                    height: *height as u32,
385                    quality: *quality as u32,
386                    format,
387                    offset,
388                });
389                offset += 1; // images take 1 character position
390            }
391            InlineContent::Empty => {
392                // Empty elements don't produce fragments
393            }
394        }
395    }
396
397    fragments
398}
399
400/// Compute 0-based index of a block within its list.
401fn compute_list_item_index(inner: &TextDocumentInner, list_id: EntityId, block_id: u64) -> usize {
402    let all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
403    let mut list_blocks: Vec<_> = all_blocks
404        .iter()
405        .filter(|b| b.list == Some(list_id))
406        .collect();
407    list_blocks.sort_by_key(|b| b.document_position);
408    list_blocks
409        .iter()
410        .position(|b| b.id == block_id)
411        .unwrap_or(0)
412}
413
414/// Format a list marker for the given item index.
415pub(crate) fn format_list_marker(
416    list_dto: &frontend::list::dtos::ListDto,
417    item_index: usize,
418) -> String {
419    let number = item_index + 1; // 1-based for display
420    let marker_body = match list_dto.style {
421        ListStyle::Disc => "\u{2022}".to_string(),   // •
422        ListStyle::Circle => "\u{25E6}".to_string(), // ◦
423        ListStyle::Square => "\u{25AA}".to_string(), // ▪
424        ListStyle::Decimal => format!("{number}"),
425        ListStyle::LowerAlpha => {
426            if number <= 26 {
427                ((b'a' + (number as u8 - 1)) as char).to_string()
428            } else {
429                format!("{number}")
430            }
431        }
432        ListStyle::UpperAlpha => {
433            if number <= 26 {
434                ((b'A' + (number as u8 - 1)) as char).to_string()
435            } else {
436                format!("{number}")
437            }
438        }
439        ListStyle::LowerRoman => to_roman_lower(number),
440        ListStyle::UpperRoman => to_roman_upper(number),
441    };
442    format!("{}{marker_body}{}", list_dto.prefix, list_dto.suffix)
443}
444
445fn to_roman_upper(mut n: usize) -> String {
446    const VALUES: &[(usize, &str)] = &[
447        (1000, "M"),
448        (900, "CM"),
449        (500, "D"),
450        (400, "CD"),
451        (100, "C"),
452        (90, "XC"),
453        (50, "L"),
454        (40, "XL"),
455        (10, "X"),
456        (9, "IX"),
457        (5, "V"),
458        (4, "IV"),
459        (1, "I"),
460    ];
461    let mut result = String::new();
462    for &(val, sym) in VALUES {
463        while n >= val {
464            result.push_str(sym);
465            n -= val;
466        }
467    }
468    result
469}
470
471fn to_roman_lower(n: usize) -> String {
472    to_roman_upper(n).to_lowercase()
473}
474
475/// Build a ListInfo for a block. Called while lock is held.
476fn build_list_info(
477    inner: &TextDocumentInner,
478    block_dto: &frontend::block::dtos::BlockDto,
479) -> Option<ListInfo> {
480    let list_id = block_dto.list?;
481    let list_dto = list_commands::get_list(&inner.ctx, &{ list_id })
482        .ok()
483        .flatten()?;
484
485    let item_index = compute_list_item_index(inner, list_id, block_dto.id);
486    let marker = format_list_marker(&list_dto, item_index);
487
488    Some(ListInfo {
489        list_id: list_id as usize,
490        style: list_dto.style.clone(),
491        indent: list_dto.indent as u8,
492        marker,
493        item_index,
494    })
495}
496
497/// Build a BlockSnapshot for a block. Called while lock is held.
498pub(crate) fn build_block_snapshot(
499    inner: &TextDocumentInner,
500    block_id: u64,
501) -> Option<BlockSnapshot> {
502    let block_dto = block_commands::get_block(&inner.ctx, &block_id)
503        .ok()
504        .flatten()?;
505
506    let fragments = build_fragments(inner, block_id);
507    let block_format = BlockFormat::from(&block_dto);
508    let list_info = build_list_info(inner, &block_dto);
509
510    Some(BlockSnapshot {
511        block_id: block_id as usize,
512        position: to_usize(block_dto.document_position),
513        length: to_usize(block_dto.text_length),
514        text: block_dto.plain_text,
515        fragments,
516        block_format,
517        list_info,
518    })
519}
520
521/// Build BlockSnapshots for all blocks in a frame, sorted by document_position.
522pub(crate) fn build_blocks_snapshot_for_frame(
523    inner: &TextDocumentInner,
524    frame_id: u64,
525) -> Vec<BlockSnapshot> {
526    let frame_dto = match frame_commands::get_frame(&inner.ctx, &(frame_id as EntityId))
527        .ok()
528        .flatten()
529    {
530        Some(f) => f,
531        None => return Vec::new(),
532    };
533
534    let mut block_dtos: Vec<_> = frame_dto
535        .blocks
536        .iter()
537        .filter_map(|&id| {
538            block_commands::get_block(&inner.ctx, &{ id })
539                .ok()
540                .flatten()
541        })
542        .collect();
543    block_dtos.sort_by_key(|b| b.document_position);
544
545    block_dtos
546        .iter()
547        .filter_map(|b| build_block_snapshot(inner, b.id))
548        .collect()
549}