1use crate::schema::{Paint, TypeRole};
15use std::rc::Rc;
16
17pub const SPAN_LEN: usize = 28;
20
21const FLAG_BOLD: u16 = 1;
22const FLAG_ITALIC: u16 = 2;
23const FLAG_UNDERLINE: u16 = 4;
24const FLAG_STRIKETHROUGH: u16 = 8;
25
26#[derive(Clone, Copy, Debug, Default, PartialEq)]
31pub struct TextSpan {
32 pub start: u32,
33 pub length: u32,
34 pub type_role: Option<TypeRole>,
35 pub color: Option<Paint>,
36 pub bold: bool,
37 pub italic: bool,
38 pub underline: bool,
39 pub strikethrough: bool,
40 pub on_click: Option<u64>,
42}
43
44impl TextSpan {
45 pub const fn new(start: u32, length: u32) -> Self {
47 Self {
48 start,
49 length,
50 type_role: None,
51 color: None,
52 bold: false,
53 italic: false,
54 underline: false,
55 strikethrough: false,
56 on_click: None,
57 }
58 }
59
60 pub const fn bold(mut self) -> Self {
61 self.bold = true;
62 self
63 }
64
65 pub const fn italic(mut self) -> Self {
66 self.italic = true;
67 self
68 }
69
70 pub const fn underline(mut self) -> Self {
71 self.underline = true;
72 self
73 }
74
75 pub const fn strikethrough(mut self) -> Self {
76 self.strikethrough = true;
77 self
78 }
79
80 pub const fn with_role(mut self, role: TypeRole) -> Self {
81 self.type_role = Some(role);
82 self
83 }
84
85 pub const fn with_color(mut self, color: Paint) -> Self {
86 self.color = Some(color);
87 self
88 }
89
90 fn flags(&self) -> u16 {
91 let mut flags = 0;
92 if self.bold {
93 flags |= FLAG_BOLD;
94 }
95 if self.italic {
96 flags |= FLAG_ITALIC;
97 }
98 if self.underline {
99 flags |= FLAG_UNDERLINE;
100 }
101 if self.strikethrough {
102 flags |= FLAG_STRIKETHROUGH;
103 }
104 flags
105 }
106
107 fn write_into(&self, out: &mut Vec<u8>) {
108 out.extend_from_slice(&self.start.to_le_bytes());
109 out.extend_from_slice(&self.length.to_le_bytes());
110 out.extend_from_slice(&self.type_role.map_or(0, u16::from).to_le_bytes());
111 out.extend_from_slice(&self.flags().to_le_bytes());
112 let paint = self.color.map_or(0, Paint::to_bits);
113 out.extend_from_slice(&(paint as u32).to_le_bytes());
114 out.extend_from_slice(&((paint >> 32) as u32).to_le_bytes());
115 let handler = self.on_click.unwrap_or(0);
116 out.extend_from_slice(&(handler as u32).to_le_bytes());
117 out.extend_from_slice(&((handler >> 32) as u32).to_le_bytes());
118 }
119}
120
121type Marked = (&'static [u8], fn(u32, u32) -> TextSpan);
124
125#[derive(Clone, Default, PartialEq)]
130pub struct TextSpans {
131 bytes: Option<Rc<[u8]>>,
132}
133
134impl std::fmt::Debug for TextSpans {
135 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 formatter
137 .debug_struct("TextSpans")
138 .field("spans", &(self.as_bytes().len() / SPAN_LEN))
139 .finish()
140 }
141}
142
143impl TextSpans {
144 pub fn new(spans: impl IntoIterator<Item = TextSpan>) -> Self {
145 let mut bytes = Vec::new();
146 for span in spans {
147 span.write_into(&mut bytes);
148 }
149 Self::from_bytes(bytes)
150 }
151
152 pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
154 let bytes = bytes.into();
155 Self {
156 bytes: (!bytes.is_empty()).then_some(bytes),
157 }
158 }
159
160 pub fn as_bytes(&self) -> &[u8] {
161 self.bytes.as_deref().unwrap_or(&[])
162 }
163
164 pub fn is_empty(&self) -> bool {
165 self.bytes.is_none()
166 }
167
168 pub fn spans(&self) -> impl Iterator<Item = TextSpan> + '_ {
170 self.as_bytes().chunks_exact(SPAN_LEN).map(|record| {
171 let word = |at: usize| {
172 u32::from_le_bytes([record[at], record[at + 1], record[at + 2], record[at + 3]])
173 };
174 let half = |at: usize| u16::from_le_bytes([record[at], record[at + 1]]);
175 let flags = half(10);
176 let paint = u64::from(word(12)) | (u64::from(word(16)) << 32);
177 let handler = u64::from(word(20)) | (u64::from(word(24)) << 32);
178 TextSpan {
179 start: word(0),
180 length: word(4),
181 type_role: TypeRole::try_from(half(8)).ok(),
182 color: Paint::from_bits(paint),
183 bold: flags & FLAG_BOLD != 0,
184 italic: flags & FLAG_ITALIC != 0,
185 underline: flags & FLAG_UNDERLINE != 0,
186 strikethrough: flags & FLAG_STRIKETHROUGH != 0,
187 on_click: (handler != 0).then_some(handler),
188 }
189 })
190 }
191
192 pub fn from_markdown(source: &str) -> (String, Self) {
201 let mut text = String::with_capacity(source.len());
202 let mut spans = Vec::new();
203 let bytes = source.as_bytes();
204 let mut at = 0;
205 while at < bytes.len() {
206 let (marker, span_of): Marked = if bytes[at..].starts_with(b"**") {
207 (b"**", |start, length| TextSpan::new(start, length).bold())
208 } else if bytes[at] == b'*' {
209 (b"*", |start, length| TextSpan::new(start, length).italic())
210 } else if bytes[at] == b'`' {
211 (b"`", |start, length| {
212 TextSpan::new(start, length).with_role(TypeRole::Mono)
213 })
214 } else {
215 text.push(source[at..].chars().next().expect("a char boundary"));
216 at += source[at..].chars().next().map_or(1, char::len_utf8);
217 continue;
218 };
219 let body = at + marker.len();
220 let Some(end) = find(&bytes[body..], marker).map(|offset| body + offset) else {
221 text.push_str(&source[at..body]);
223 at = body;
224 continue;
225 };
226 let start = text.len() as u32;
227 text.push_str(&source[body..end]);
228 spans.push(span_of(start, text.len() as u32 - start));
229 at = end + marker.len();
230 }
231 (text, Self::new(spans))
232 }
233}
234
235fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
236 haystack
237 .windows(needle.len())
238 .position(|window| window == needle)
239}
240
241impl dioxus_core::IntoAttributeValue for TextSpans {
242 fn into_value(self) -> dioxus_core::AttributeValue {
243 dioxus_core::AttributeValue::any_value(self)
244 }
245}