at main 122 lines 3.4 kB view raw
1//! Paragraph-level rendering for incremental updates. 2//! 3//! Paragraphs are discovered during markdown rendering by tracking 4//! Tag::Paragraph events. This allows updating only changed paragraphs in the DOM. 5 6use smol_str::{SmolStr, format_smolstr}; 7 8use crate::offset_map::OffsetMapping; 9use crate::syntax::SyntaxSpanInfo; 10use std::collections::hash_map::DefaultHasher; 11use std::hash::{Hash, Hasher}; 12use std::ops::Range; 13 14/// A rendered paragraph with its source range and offset mappings. 15#[derive(Debug, Clone, PartialEq)] 16pub struct ParagraphRender { 17 /// Stable content-based ID for DOM diffing (format: `p-{index}`) 18 pub id: SmolStr, 19 20 /// Source byte range in the text buffer 21 pub byte_range: Range<usize>, 22 23 /// Source char range in the text buffer 24 pub char_range: Range<usize>, 25 26 /// Rendered HTML content (without wrapper div) 27 pub html: String, 28 29 /// Offset mappings for this paragraph 30 pub offset_map: Vec<OffsetMapping>, 31 32 /// Syntax spans for conditional visibility 33 pub syntax_spans: Vec<SyntaxSpanInfo>, 34 35 /// Hash of source text for quick change detection 36 pub source_hash: u64, 37} 38 39impl ParagraphRender { 40 /// Check if this paragraph contains a given byte offset. 41 pub fn contains_byte(&self, offset: usize) -> bool { 42 self.byte_range.contains(&offset) 43 } 44 45 /// Check if this paragraph contains a given char offset. 46 pub fn contains_char(&self, offset: usize) -> bool { 47 self.char_range.contains(&offset) 48 } 49 50 /// Get the length in chars. 51 pub fn char_len(&self) -> usize { 52 self.char_range.len() 53 } 54 55 /// Get the length in bytes. 56 pub fn byte_len(&self) -> usize { 57 self.byte_range.len() 58 } 59} 60 61/// Simple hash function for source text comparison. 62/// 63/// Used to quickly detect if paragraph content has changed. 64pub fn hash_source(text: &str) -> u64 { 65 let mut hasher = DefaultHasher::new(); 66 text.hash(&mut hasher); 67 hasher.finish() 68} 69 70/// Generate a paragraph ID from monotonic counter. 71/// 72/// IDs are stable across content changes - only position/cursor determines identity. 73pub fn make_paragraph_id(index: usize) -> SmolStr { 74 format_smolstr!("p-{}", index) 75} 76 77#[cfg(test)] 78mod tests { 79 use smol_str::ToSmolStr; 80 81 use super::*; 82 83 #[test] 84 fn test_hash_source() { 85 let h1 = hash_source("hello world"); 86 let h2 = hash_source("hello world"); 87 let h3 = hash_source("hello world!"); 88 89 assert_eq!(h1, h2); 90 assert_ne!(h1, h3); 91 } 92 93 #[test] 94 fn test_make_paragraph_id() { 95 assert_eq!(make_paragraph_id(0), "p-0"); 96 assert_eq!(make_paragraph_id(42), "p-42"); 97 } 98 99 #[test] 100 fn test_paragraph_contains() { 101 let para = ParagraphRender { 102 id: "p-0".to_smolstr(), 103 byte_range: 10..50, 104 char_range: 10..50, 105 html: String::new(), 106 offset_map: vec![], 107 syntax_spans: vec![], 108 source_hash: 0, 109 }; 110 111 assert!(!para.contains_byte(9)); 112 assert!(para.contains_byte(10)); 113 assert!(para.contains_byte(25)); 114 assert!(para.contains_byte(49)); 115 assert!(!para.contains_byte(50)); 116 117 assert!(!para.contains_char(9)); 118 assert!(para.contains_char(10)); 119 assert!(para.contains_char(25)); 120 assert!(!para.contains_char(50)); 121 } 122}