1use crate::{notebooks::Label, timestamps::Timestamp};
2#[cfg(feature = "fp-bindgen")]
3use fp_bindgen::prelude::Serializable;
4use serde::{Deserialize, Serialize};
5use typed_builder::TypedBuilder;
6
7#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
9#[cfg_attr(
10 feature = "fp-bindgen",
11 derive(Serializable),
12 fp(rust_module = "fiberplane_models::formatting")
13)]
14pub struct RichText {
15 pub text: String,
16 pub formatting: Formatting,
17}
18
19impl RichText {
20 pub fn new(text: impl Into<String>, formatting: impl Into<Formatting>) -> Self {
22 Self {
23 text: text.into(),
24 formatting: formatting.into(),
25 }
26 }
27
28 pub fn new_plain(text: impl Into<String>) -> Self {
30 Self {
31 text: text.into(),
32 formatting: Formatting::default(),
33 }
34 }
35}
36
37pub type Formatting = Vec<AnnotationWithOffset>;
43
44#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
49#[cfg_attr(
50 feature = "fp-bindgen",
51 derive(Serializable),
52 fp(rust_module = "fiberplane_models::formatting")
53)]
54#[non_exhaustive]
55#[serde(rename_all = "camelCase")]
56pub struct AnnotationWithOffset {
57 pub offset: u32,
58 #[serde(flatten)]
59 pub annotation: Annotation,
60}
61
62impl AnnotationWithOffset {
63 pub fn new(offset: u32, annotation: Annotation) -> Self {
64 Self { offset, annotation }
65 }
66
67 pub fn translate(&self, delta: i64) -> Self {
69 Self {
70 offset: (self.offset as i64 + delta) as u32,
71 annotation: self.annotation.clone(),
72 }
73 }
74}
75
76#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
80#[cfg_attr(
81 feature = "fp-bindgen",
82 derive(Serializable),
83 fp(rust_module = "fiberplane_models::formatting")
84)]
85#[non_exhaustive]
86#[serde(rename_all = "snake_case", tag = "type")]
87pub enum Annotation {
88 StartBold,
89 EndBold,
90 StartCode,
91 EndCode,
92 StartHighlight,
93 EndHighlight,
94 StartItalics,
95 EndItalics,
96 Label(Label),
97 StartLink { url: String },
98 EndLink,
99 Mention(Mention),
100 StartStrikethrough,
101 EndStrikethrough,
102 Timestamp { timestamp: Timestamp },
103 StartUnderline,
104 EndUnderline,
105}
106
107impl Annotation {
108 pub fn is_paired_annotation(&self) -> bool {
110 !matches!(
111 self,
112 Self::Label(_) | Self::Mention(_) | Self::Timestamp { .. }
113 )
114 }
115
116 pub fn toggle_opposite(&self) -> Option<Annotation> {
122 match self {
123 Annotation::StartBold => Some(Annotation::EndBold),
124 Annotation::EndBold => Some(Annotation::StartBold),
125 Annotation::StartCode => Some(Annotation::EndCode),
126 Annotation::EndCode => Some(Annotation::StartCode),
127 Annotation::StartHighlight => Some(Annotation::EndHighlight),
128 Annotation::EndHighlight => Some(Annotation::StartHighlight),
129 Annotation::StartItalics => Some(Annotation::EndItalics),
130 Annotation::EndItalics => Some(Annotation::StartItalics),
131 Annotation::StartLink { .. } => Some(Annotation::EndLink),
132 Annotation::EndLink => None,
133 Annotation::Mention(_) => None,
134 Annotation::Timestamp { .. } => None,
135 Annotation::StartStrikethrough => Some(Annotation::EndStrikethrough),
136 Annotation::EndStrikethrough => Some(Annotation::StartStrikethrough),
137 Annotation::StartUnderline => Some(Annotation::EndUnderline),
138 Annotation::EndUnderline => Some(Annotation::StartUnderline),
139 Annotation::Label(_) => None,
140 }
141 }
142}
143
144#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
147#[cfg_attr(
148 feature = "fp-bindgen",
149 derive(Serializable),
150 fp(rust_module = "fiberplane_models::formatting")
151)]
152#[non_exhaustive]
153#[serde(rename_all = "camelCase")]
154pub struct ActiveFormatting {
155 pub bold: bool,
156 pub code: bool,
157 pub highlight: bool,
158 pub italics: bool,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub link: Option<String>,
161 pub strikethrough: bool,
162 pub underline: bool,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub label: Option<Label>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub mention: Option<Mention>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub timestamp: Option<Timestamp>,
169}
170
171impl ActiveFormatting {
172 pub fn with_bold(&self, bold: bool) -> Self {
173 Self {
174 bold,
175 ..self.clone()
176 }
177 }
178
179 pub fn with_code(&self, code: bool) -> Self {
180 Self {
181 code,
182 ..self.clone()
183 }
184 }
185
186 pub fn with_highlight(&self, highlight: bool) -> Self {
187 Self {
188 highlight,
189 ..self.clone()
190 }
191 }
192
193 pub fn with_italics(&self, italics: bool) -> Self {
194 Self {
195 italics,
196 ..self.clone()
197 }
198 }
199
200 pub fn with_link(&self, link: impl Into<Option<String>>) -> Self {
201 Self {
202 link: link.into(),
203 ..self.clone()
204 }
205 }
206
207 pub fn with_strikethrough(&self, strikethrough: bool) -> Self {
208 Self {
209 strikethrough,
210 ..self.clone()
211 }
212 }
213
214 pub fn with_underline(&self, underline: bool) -> Self {
215 Self {
216 underline,
217 ..self.clone()
218 }
219 }
220
221 pub fn with_label(&self, label: impl Into<Option<Label>>) -> Self {
222 Self {
223 label: label.into(),
224 ..self.clone()
225 }
226 }
227
228 pub fn with_timestamp(&self, timestamp: impl Into<Option<Timestamp>>) -> Self {
229 Self {
230 timestamp: timestamp.into(),
231 ..self.clone()
232 }
233 }
234
235 pub fn annotations_for_toggled_formatting(&self, reference: &Self) -> Vec<Annotation> {
238 let mut annotations = Vec::new();
239 if self.bold != reference.bold {
240 annotations.push(if self.bold {
241 Annotation::StartBold
242 } else {
243 Annotation::EndBold
244 });
245 }
246 if self.code != reference.code {
247 annotations.push(if self.code {
248 Annotation::StartCode
249 } else {
250 Annotation::EndCode
251 });
252 }
253 if self.highlight != reference.highlight {
254 annotations.push(if self.highlight {
255 Annotation::StartHighlight
256 } else {
257 Annotation::EndHighlight
258 });
259 }
260 if self.italics != reference.italics {
261 annotations.push(if self.italics {
262 Annotation::StartItalics
263 } else {
264 Annotation::EndItalics
265 });
266 }
267 if self.link != reference.link {
268 annotations.push(if let Some(url) = self.link.as_ref() {
269 Annotation::StartLink { url: url.clone() }
270 } else {
271 Annotation::EndLink
272 });
273 }
274 if self.strikethrough != reference.strikethrough {
275 annotations.push(if self.strikethrough {
276 Annotation::StartStrikethrough
277 } else {
278 Annotation::EndStrikethrough
279 });
280 }
281 if self.underline != reference.underline {
282 annotations.push(if self.underline {
283 Annotation::StartUnderline
284 } else {
285 Annotation::EndUnderline
286 });
287 }
288 if self.label != reference.label {
289 if let Some(label) = self.label.as_ref() {
290 annotations.push(Annotation::Label(label.clone()))
291 }
292 }
293 if self.mention != reference.mention {
294 if let Some(mention) = self.mention.as_ref() {
295 annotations.push(Annotation::Mention(mention.clone()))
296 }
297 }
298 if self.timestamp != reference.timestamp {
299 if let Some(timestamp) = self.timestamp {
300 annotations.push(Annotation::Timestamp { timestamp })
301 }
302 }
303 annotations
304 }
305
306 pub fn contains(&self, annotation: &Annotation) -> bool {
308 match annotation {
309 Annotation::StartBold => self.bold,
310 Annotation::EndBold => !self.bold,
311 Annotation::StartCode => self.code,
312 Annotation::EndCode => !self.code,
313 Annotation::StartHighlight => self.highlight,
314 Annotation::EndHighlight => !self.highlight,
315 Annotation::StartItalics => self.italics,
316 Annotation::EndItalics => !self.italics,
317 Annotation::StartLink { .. } => self.link.is_some(),
318 Annotation::EndLink => self.link.is_none(),
319 Annotation::Mention(_) => self.mention.is_some(),
320 Annotation::Timestamp { .. } => self.timestamp.is_some(),
321 Annotation::StartStrikethrough => self.strikethrough,
322 Annotation::EndStrikethrough => !self.strikethrough,
323 Annotation::StartUnderline => self.underline,
324 Annotation::EndUnderline => !self.underline,
325 Annotation::Label(_) => self.label.is_some(),
326 }
327 }
328}
329
330#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, TypedBuilder)]
340#[cfg_attr(
341 feature = "fp-bindgen",
342 derive(Serializable),
343 fp(rust_module = "fiberplane_models::formatting")
344)]
345#[non_exhaustive]
346#[serde(rename_all = "camelCase")]
347pub struct Mention {
348 #[builder(setter(into))]
349 pub name: String,
350
351 #[builder(setter(into))]
352 pub user_id: String,
353}
354
355pub fn first_annotation_index_for_offset(range: &[AnnotationWithOffset], offset: u32) -> usize {
362 let mut index = annotation_insertion_index(range, offset);
363 while index > 0 && range[index - 1].offset == offset {
365 index -= 1;
366 }
367
368 index
369}
370
371#[test]
372fn test_first_annotation_index_for_offset() {
373 let formatting = vec![
374 AnnotationWithOffset::new(30, Annotation::StartBold),
375 AnnotationWithOffset::new(30, Annotation::StartItalics),
376 AnnotationWithOffset::new(94, Annotation::EndBold),
377 AnnotationWithOffset::new(94, Annotation::EndItalics),
378 ];
379
380 assert_eq!(first_annotation_index_for_offset(&formatting, 10), 0);
381 assert_eq!(first_annotation_index_for_offset(&formatting, 30), 0);
382 assert_eq!(first_annotation_index_for_offset(&formatting, 31), 2);
383 assert_eq!(first_annotation_index_for_offset(&formatting, 94), 2);
384 assert_eq!(first_annotation_index_for_offset(&formatting, 95), 4);
385}
386
387pub fn first_annotation_index_beyond_offset(range: &[AnnotationWithOffset], offset: u32) -> usize {
393 let mut index = annotation_insertion_index(range, offset);
394 while index < range.len() && range[index].offset == offset {
396 index += 1;
397 }
398
399 index
400}
401
402#[test]
403fn test_first_annotation_index_beyond_offset() {
404 let formatting = vec![
405 AnnotationWithOffset::new(30, Annotation::StartBold),
406 AnnotationWithOffset::new(30, Annotation::StartItalics),
407 AnnotationWithOffset::new(94, Annotation::EndBold),
408 AnnotationWithOffset::new(94, Annotation::EndItalics),
409 ];
410
411 assert_eq!(first_annotation_index_beyond_offset(&formatting, 10), 0);
412 assert_eq!(first_annotation_index_beyond_offset(&formatting, 30), 2);
413 assert_eq!(first_annotation_index_beyond_offset(&formatting, 31), 2);
414 assert_eq!(first_annotation_index_beyond_offset(&formatting, 94), 4);
415 assert_eq!(first_annotation_index_beyond_offset(&formatting, 95), 4);
416}
417
418pub fn annotation_insertion_index(range: &[AnnotationWithOffset], offset: u32) -> usize {
421 match range.binary_search_by(|annotation| annotation.offset.cmp(&offset)) {
422 Ok(index) => index,
423 Err(insertion_index) => insertion_index,
424 }
425}
426
427#[must_use]
430pub fn translate(range: &[AnnotationWithOffset], delta: i64) -> Formatting {
431 range
432 .iter()
433 .map(|annotation| annotation.translate(delta))
434 .collect()
435}