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, 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/// 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            parent_frame_id: None,
312            table_cell: None,
313        })
314    }
315}
316
317// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
318// Internal helpers (called while lock is held)
319// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
320
321/// Find the parent frame of a block by scanning all frames.
322fn 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
333/// Find table cell context for a block (snapshot-friendly, no live handles).
334/// Returns `None` if the block is not inside a table cell.
335fn 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    // Fast path: anchor frame with `table` field set
343    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    // Slow path: scan all tables for a cell referencing this frame
365    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
387/// Compute 0-indexed block number by scanning all blocks sorted by document_position.
388fn 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
395/// Build fragments for a block from its InlineElements.
396pub(crate) fn build_fragments(inner: &TextDocumentInner, block_id: u64) -> Vec<FragmentContent> {
397    let block_dto = match block_commands::get_block(&inner.ctx, &block_id)
398        .ok()
399        .flatten()
400    {
401        Some(b) => b,
402        None => return Vec::new(),
403    };
404
405    let element_ids = &block_dto.elements;
406    let elements: Vec<_> = element_ids
407        .iter()
408        .filter_map(|&id| {
409            inline_element_commands::get_inline_element(&inner.ctx, &{ id })
410                .ok()
411                .flatten()
412        })
413        .collect();
414
415    let mut fragments = Vec::with_capacity(elements.len());
416    let mut offset: usize = 0;
417
418    for el in &elements {
419        let format = TextFormat::from(el);
420        match &el.content {
421            InlineContent::Text(text) => {
422                let length = text.chars().count();
423                fragments.push(FragmentContent::Text {
424                    text: text.clone(),
425                    format,
426                    offset,
427                    length,
428                });
429                offset += length;
430            }
431            InlineContent::Image {
432                name,
433                width,
434                height,
435                quality,
436            } => {
437                fragments.push(FragmentContent::Image {
438                    name: name.clone(),
439                    width: *width as u32,
440                    height: *height as u32,
441                    quality: *quality as u32,
442                    format,
443                    offset,
444                });
445                offset += 1; // images take 1 character position
446            }
447            InlineContent::Empty => {
448                // Empty elements don't produce fragments
449            }
450        }
451    }
452
453    fragments
454}
455
456/// Compute 0-based index of a block within its list.
457fn compute_list_item_index(inner: &TextDocumentInner, list_id: EntityId, block_id: u64) -> usize {
458    let all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
459    let mut list_blocks: Vec<_> = all_blocks
460        .iter()
461        .filter(|b| b.list == Some(list_id))
462        .collect();
463    list_blocks.sort_by_key(|b| b.document_position);
464    list_blocks
465        .iter()
466        .position(|b| b.id == block_id)
467        .unwrap_or(0)
468}
469
470/// Format a list marker for the given item index.
471pub(crate) fn format_list_marker(
472    list_dto: &frontend::list::dtos::ListDto,
473    item_index: usize,
474) -> String {
475    let number = item_index + 1; // 1-based for display
476    let marker_body = match list_dto.style {
477        ListStyle::Disc => "\u{2022}".to_string(),   // •
478        ListStyle::Circle => "\u{25E6}".to_string(), // ◦
479        ListStyle::Square => "\u{25AA}".to_string(), // ▪
480        ListStyle::Decimal => format!("{number}"),
481        ListStyle::LowerAlpha => {
482            if number <= 26 {
483                ((b'a' + (number as u8 - 1)) as char).to_string()
484            } else {
485                format!("{number}")
486            }
487        }
488        ListStyle::UpperAlpha => {
489            if number <= 26 {
490                ((b'A' + (number as u8 - 1)) as char).to_string()
491            } else {
492                format!("{number}")
493            }
494        }
495        ListStyle::LowerRoman => to_roman_lower(number),
496        ListStyle::UpperRoman => to_roman_upper(number),
497    };
498    format!("{}{marker_body}{}", list_dto.prefix, list_dto.suffix)
499}
500
501fn to_roman_upper(mut n: usize) -> String {
502    const VALUES: &[(usize, &str)] = &[
503        (1000, "M"),
504        (900, "CM"),
505        (500, "D"),
506        (400, "CD"),
507        (100, "C"),
508        (90, "XC"),
509        (50, "L"),
510        (40, "XL"),
511        (10, "X"),
512        (9, "IX"),
513        (5, "V"),
514        (4, "IV"),
515        (1, "I"),
516    ];
517    let mut result = String::new();
518    for &(val, sym) in VALUES {
519        while n >= val {
520            result.push_str(sym);
521            n -= val;
522        }
523    }
524    result
525}
526
527fn to_roman_lower(n: usize) -> String {
528    to_roman_upper(n).to_lowercase()
529}
530
531/// Build a ListInfo for a block. Called while lock is held.
532fn build_list_info(
533    inner: &TextDocumentInner,
534    block_dto: &frontend::block::dtos::BlockDto,
535) -> Option<ListInfo> {
536    let list_id = block_dto.list?;
537    let list_dto = list_commands::get_list(&inner.ctx, &{ list_id })
538        .ok()
539        .flatten()?;
540
541    let item_index = compute_list_item_index(inner, list_id, block_dto.id);
542    let marker = format_list_marker(&list_dto, item_index);
543
544    Some(ListInfo {
545        list_id: list_id as usize,
546        style: list_dto.style.clone(),
547        indent: list_dto.indent as u8,
548        marker,
549        item_index,
550    })
551}
552
553/// Build a BlockSnapshot for a block. Called while lock is held.
554pub(crate) fn build_block_snapshot(
555    inner: &TextDocumentInner,
556    block_id: u64,
557) -> Option<BlockSnapshot> {
558    let block_dto = block_commands::get_block(&inner.ctx, &block_id)
559        .ok()
560        .flatten()?;
561
562    let fragments = build_fragments(inner, block_id);
563    let block_format = BlockFormat::from(&block_dto);
564    let list_info = build_list_info(inner, &block_dto);
565
566    let parent_frame_id = find_parent_frame(inner, block_id).map(|id| id as usize);
567    let table_cell = find_table_cell_context(inner, block_id);
568
569    Some(BlockSnapshot {
570        block_id: block_id as usize,
571        position: to_usize(block_dto.document_position),
572        length: to_usize(block_dto.text_length),
573        text: block_dto.plain_text,
574        fragments,
575        block_format,
576        list_info,
577        parent_frame_id,
578        table_cell,
579    })
580}
581
582/// Build BlockSnapshots for all blocks in a frame, sorted by document_position.
583pub(crate) fn build_blocks_snapshot_for_frame(
584    inner: &TextDocumentInner,
585    frame_id: u64,
586) -> Vec<BlockSnapshot> {
587    let frame_dto = match frame_commands::get_frame(&inner.ctx, &(frame_id as EntityId))
588        .ok()
589        .flatten()
590    {
591        Some(f) => f,
592        None => return Vec::new(),
593    };
594
595    let mut block_dtos: Vec<_> = frame_dto
596        .blocks
597        .iter()
598        .filter_map(|&id| {
599            block_commands::get_block(&inner.ctx, &{ id })
600                .ok()
601                .flatten()
602        })
603        .collect();
604    block_dtos.sort_by_key(|b| b.document_position);
605
606    block_dtos
607        .iter()
608        .filter_map(|b| build_block_snapshot(inner, b.id))
609        .collect()
610}