1use crate::{notebooks::Label, timestamps::Timestamp};
2#[cfg(feature = "fp-bindgen")]
3use fp_bindgen::prelude::Serializable;
4use serde::{Deserialize, Serialize};
5use time::OffsetDateTime;
6use typed_builder::TypedBuilder;
7
8pub type Formatting = Vec<AnnotationWithOffset>;
14
15#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
19#[cfg_attr(
20 feature = "fp-bindgen",
21 derive(Serializable),
22 fp(rust_module = "fiberplane_models::formatting")
23)]
24#[non_exhaustive]
25#[serde(rename_all = "camelCase")]
26pub struct AnnotationWithOffset {
27 pub offset: u32,
28 #[serde(flatten)]
29 pub annotation: Annotation,
30}
31
32impl AnnotationWithOffset {
33 pub fn new(offset: u32, annotation: Annotation) -> Self {
34 Self { offset, annotation }
35 }
36
37 pub fn translate(&self, delta: i64) -> Self {
39 Self {
40 offset: (self.offset as i64 + delta) as u32,
41 annotation: self.annotation.clone(),
42 }
43 }
44}
45
46#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
50#[cfg_attr(
51 feature = "fp-bindgen",
52 derive(Serializable),
53 fp(rust_module = "fiberplane_models::formatting")
54)]
55#[non_exhaustive]
56#[serde(rename_all = "snake_case", tag = "type")]
57pub enum Annotation {
58 StartBold,
59 EndBold,
60 StartCode,
61 EndCode,
62 StartHighlight,
63 EndHighlight,
64 StartItalics,
65 EndItalics,
66 #[serde(rename_all = "camelCase")]
67 StartLink {
68 url: String,
69 },
70 EndLink,
71 Mention(Mention),
72 Timestamp {
73 #[serde(with = "time::serde::rfc3339")]
74 timestamp: OffsetDateTime,
75 },
76 StartStrikethrough,
77 EndStrikethrough,
78 StartUnderline,
79 EndUnderline,
80 Label(Label),
81}
82
83impl Annotation {
84 pub fn toggle_opposite(&self) -> Option<Annotation> {
90 match self {
91 Annotation::StartBold => Some(Annotation::EndBold),
92 Annotation::EndBold => Some(Annotation::StartBold),
93 Annotation::StartCode => Some(Annotation::EndCode),
94 Annotation::EndCode => Some(Annotation::StartCode),
95 Annotation::StartHighlight => Some(Annotation::EndHighlight),
96 Annotation::EndHighlight => Some(Annotation::StartHighlight),
97 Annotation::StartItalics => Some(Annotation::EndItalics),
98 Annotation::EndItalics => Some(Annotation::StartItalics),
99 Annotation::StartLink { .. } => Some(Annotation::EndLink),
100 Annotation::EndLink => None,
101 Annotation::Mention(_) => None,
102 Annotation::Timestamp { .. } => None,
103 Annotation::StartStrikethrough => Some(Annotation::EndStrikethrough),
104 Annotation::EndStrikethrough => Some(Annotation::StartStrikethrough),
105 Annotation::StartUnderline => Some(Annotation::EndUnderline),
106 Annotation::EndUnderline => Some(Annotation::StartUnderline),
107 Annotation::Label(_) => None,
108 }
109 }
110}
111
112#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, TypedBuilder)]
115#[cfg_attr(
116 feature = "fp-bindgen",
117 derive(Serializable),
118 fp(rust_module = "fiberplane_models::formatting")
119)]
120#[non_exhaustive]
121#[serde(rename_all = "camelCase")]
122pub struct ActiveFormatting {
123 pub bold: bool,
124 pub code: bool,
125 pub highlight: bool,
126 pub italics: bool,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub link: Option<String>,
129 pub strikethrough: bool,
130 pub underline: bool,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub label: Option<Label>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub timestamp: Option<Timestamp>,
135}
136
137impl ActiveFormatting {
138 pub fn with_bold(&self, bold: bool) -> Self {
139 Self {
140 bold,
141 ..self.clone()
142 }
143 }
144
145 pub fn with_code(&self, code: bool) -> Self {
146 Self {
147 code,
148 ..self.clone()
149 }
150 }
151
152 pub fn with_highlight(&self, highlight: bool) -> Self {
153 Self {
154 highlight,
155 ..self.clone()
156 }
157 }
158
159 pub fn with_italics(&self, italics: bool) -> Self {
160 Self {
161 italics,
162 ..self.clone()
163 }
164 }
165
166 pub fn with_link(&self, link: impl Into<Option<String>>) -> Self {
167 Self {
168 link: link.into(),
169 ..self.clone()
170 }
171 }
172
173 pub fn with_strikethrough(&self, strikethrough: bool) -> Self {
174 Self {
175 strikethrough,
176 ..self.clone()
177 }
178 }
179
180 pub fn with_underline(&self, underline: bool) -> Self {
181 Self {
182 underline,
183 ..self.clone()
184 }
185 }
186
187 pub fn with_label(&self, label: impl Into<Option<Label>>) -> Self {
188 Self {
189 label: label.into(),
190 ..self.clone()
191 }
192 }
193
194 pub fn with_timestamp(&self, timestamp: impl Into<Option<Timestamp>>) -> Self {
195 Self {
196 timestamp: timestamp.into(),
197 ..self.clone()
198 }
199 }
200
201 pub fn annotations_for_toggled_formatting(&self, reference: &Self) -> Vec<Annotation> {
204 let mut annotations = Vec::new();
205 if self.bold != reference.bold {
206 annotations.push(if self.bold {
207 Annotation::StartBold
208 } else {
209 Annotation::EndBold
210 });
211 }
212 if self.code != reference.code {
213 annotations.push(if self.code {
214 Annotation::StartCode
215 } else {
216 Annotation::EndCode
217 });
218 }
219 if self.highlight != reference.highlight {
220 annotations.push(if self.highlight {
221 Annotation::StartHighlight
222 } else {
223 Annotation::EndHighlight
224 });
225 }
226 if self.italics != reference.italics {
227 annotations.push(if self.italics {
228 Annotation::StartItalics
229 } else {
230 Annotation::EndItalics
231 });
232 }
233 if self.link != reference.link {
234 annotations.push(if let Some(url) = self.link.as_ref() {
235 Annotation::StartLink { url: url.clone() }
236 } else {
237 Annotation::EndLink
238 });
239 }
240 if self.strikethrough != reference.strikethrough {
241 annotations.push(if self.strikethrough {
242 Annotation::StartStrikethrough
243 } else {
244 Annotation::EndStrikethrough
245 });
246 }
247 if self.underline != reference.underline {
248 annotations.push(if self.underline {
249 Annotation::StartUnderline
250 } else {
251 Annotation::EndUnderline
252 });
253 }
254 if self.label != reference.label {
255 if let Some(label) = self.label.as_ref() {
256 annotations.push(Annotation::Label(label.clone()))
257 }
258 }
259 if self.timestamp != reference.timestamp {
260 if let Some(timestamp) = self.timestamp {
261 annotations.push(Annotation::Timestamp {
262 timestamp: *timestamp,
263 })
264 }
265 }
266 annotations
267 }
268
269 pub fn contains(&self, annotation: &Annotation) -> bool {
271 match annotation {
272 Annotation::StartBold => self.bold,
273 Annotation::EndBold => !self.bold,
274 Annotation::StartCode => self.code,
275 Annotation::EndCode => !self.code,
276 Annotation::StartHighlight => self.highlight,
277 Annotation::EndHighlight => !self.highlight,
278 Annotation::StartItalics => self.italics,
279 Annotation::EndItalics => !self.italics,
280 Annotation::StartLink { .. } => self.link.is_some(),
281 Annotation::EndLink => self.link.is_none(),
282 Annotation::Mention(_) => false,
283 Annotation::Timestamp { .. } => self.timestamp.is_some(),
284 Annotation::StartStrikethrough => self.strikethrough,
285 Annotation::EndStrikethrough => !self.strikethrough,
286 Annotation::StartUnderline => self.underline,
287 Annotation::EndUnderline => !self.underline,
288 Annotation::Label(_) => self.label.is_some(),
289 }
290 }
291}
292
293#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, TypedBuilder)]
303#[cfg_attr(
304 feature = "fp-bindgen",
305 derive(Serializable),
306 fp(rust_module = "fiberplane_models::formatting")
307)]
308#[non_exhaustive]
309#[serde(rename_all = "camelCase")]
310pub struct Mention {
311 #[builder(setter(into))]
312 pub name: String,
313 #[builder(setter(into))]
314 pub user_id: String,
315}
316
317pub fn first_annotation_index_for_offset(range: &[AnnotationWithOffset], offset: u32) -> usize {
324 let mut index = annotation_insertion_index(range, offset);
325 while index > 0 && range[index - 1].offset == offset {
327 index -= 1;
328 }
329
330 index
331}
332
333#[test]
334fn test_first_annotation_index_for_offset() {
335 let formatting = vec![
336 AnnotationWithOffset::new(30, Annotation::StartBold),
337 AnnotationWithOffset::new(30, Annotation::StartItalics),
338 AnnotationWithOffset::new(94, Annotation::EndBold),
339 AnnotationWithOffset::new(94, Annotation::EndItalics),
340 ];
341
342 assert_eq!(first_annotation_index_for_offset(&formatting, 10), 0);
343 assert_eq!(first_annotation_index_for_offset(&formatting, 30), 0);
344 assert_eq!(first_annotation_index_for_offset(&formatting, 31), 2);
345 assert_eq!(first_annotation_index_for_offset(&formatting, 94), 2);
346 assert_eq!(first_annotation_index_for_offset(&formatting, 95), 4);
347}
348
349pub fn first_annotation_index_beyond_offset(range: &[AnnotationWithOffset], offset: u32) -> usize {
355 let mut index = annotation_insertion_index(range, offset);
356 while index < range.len() && range[index].offset == offset {
358 index += 1;
359 }
360
361 index
362}
363
364#[test]
365fn test_first_annotation_index_beyond_offset() {
366 let formatting = vec![
367 AnnotationWithOffset::new(30, Annotation::StartBold),
368 AnnotationWithOffset::new(30, Annotation::StartItalics),
369 AnnotationWithOffset::new(94, Annotation::EndBold),
370 AnnotationWithOffset::new(94, Annotation::EndItalics),
371 ];
372
373 assert_eq!(first_annotation_index_beyond_offset(&formatting, 10), 0);
374 assert_eq!(first_annotation_index_beyond_offset(&formatting, 30), 2);
375 assert_eq!(first_annotation_index_beyond_offset(&formatting, 31), 2);
376 assert_eq!(first_annotation_index_beyond_offset(&formatting, 94), 4);
377 assert_eq!(first_annotation_index_beyond_offset(&formatting, 95), 4);
378}
379
380pub fn annotation_insertion_index(range: &[AnnotationWithOffset], offset: u32) -> usize {
383 match range.binary_search_by(|annotation| annotation.offset.cmp(&offset)) {
384 Ok(index) => index,
385 Err(insertion_index) => insertion_index,
386 }
387}
388
389#[must_use]
392pub fn translate(range: &[AnnotationWithOffset], delta: i64) -> Formatting {
393 range
394 .iter()
395 .map(|annotation| annotation.translate(delta))
396 .collect()
397}