atproto blogging
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}