use crate::{notebooks::Label, timestamps::Timestamp};
#[cfg(feature = "fp-bindgen")]
use fp_bindgen::prelude::Serializable;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
pub type Formatting = Vec<AnnotationWithOffset>;
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::formatting")
)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct AnnotationWithOffset {
pub offset: u32,
#[serde(flatten)]
pub annotation: Annotation,
}
impl AnnotationWithOffset {
pub fn new(offset: u32, annotation: Annotation) -> Self {
Self { offset, annotation }
}
pub fn translate(&self, delta: i64) -> Self {
Self {
offset: (self.offset as i64 + delta) as u32,
annotation: self.annotation.clone(),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::formatting")
)]
#[non_exhaustive]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum Annotation {
StartBold,
EndBold,
StartCode,
EndCode,
StartHighlight,
EndHighlight,
StartItalics,
EndItalics,
StartLink { url: String },
EndLink,
Mention(Mention),
Timestamp { timestamp: Timestamp },
StartStrikethrough,
EndStrikethrough,
StartUnderline,
EndUnderline,
Label(Label),
}
impl Annotation {
pub fn toggle_opposite(&self) -> Option<Annotation> {
match self {
Annotation::StartBold => Some(Annotation::EndBold),
Annotation::EndBold => Some(Annotation::StartBold),
Annotation::StartCode => Some(Annotation::EndCode),
Annotation::EndCode => Some(Annotation::StartCode),
Annotation::StartHighlight => Some(Annotation::EndHighlight),
Annotation::EndHighlight => Some(Annotation::StartHighlight),
Annotation::StartItalics => Some(Annotation::EndItalics),
Annotation::EndItalics => Some(Annotation::StartItalics),
Annotation::StartLink { .. } => Some(Annotation::EndLink),
Annotation::EndLink => None,
Annotation::Mention(_) => None,
Annotation::Timestamp { .. } => None,
Annotation::StartStrikethrough => Some(Annotation::EndStrikethrough),
Annotation::EndStrikethrough => Some(Annotation::StartStrikethrough),
Annotation::StartUnderline => Some(Annotation::EndUnderline),
Annotation::EndUnderline => Some(Annotation::StartUnderline),
Annotation::Label(_) => None,
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::formatting")
)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct ActiveFormatting {
pub bold: bool,
pub code: bool,
pub highlight: bool,
pub italics: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub link: Option<String>,
pub strikethrough: bool,
pub underline: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<Label>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<Timestamp>,
}
impl ActiveFormatting {
pub fn with_bold(&self, bold: bool) -> Self {
Self {
bold,
..self.clone()
}
}
pub fn with_code(&self, code: bool) -> Self {
Self {
code,
..self.clone()
}
}
pub fn with_highlight(&self, highlight: bool) -> Self {
Self {
highlight,
..self.clone()
}
}
pub fn with_italics(&self, italics: bool) -> Self {
Self {
italics,
..self.clone()
}
}
pub fn with_link(&self, link: impl Into<Option<String>>) -> Self {
Self {
link: link.into(),
..self.clone()
}
}
pub fn with_strikethrough(&self, strikethrough: bool) -> Self {
Self {
strikethrough,
..self.clone()
}
}
pub fn with_underline(&self, underline: bool) -> Self {
Self {
underline,
..self.clone()
}
}
pub fn with_label(&self, label: impl Into<Option<Label>>) -> Self {
Self {
label: label.into(),
..self.clone()
}
}
pub fn with_timestamp(&self, timestamp: impl Into<Option<Timestamp>>) -> Self {
Self {
timestamp: timestamp.into(),
..self.clone()
}
}
pub fn annotations_for_toggled_formatting(&self, reference: &Self) -> Vec<Annotation> {
let mut annotations = Vec::new();
if self.bold != reference.bold {
annotations.push(if self.bold {
Annotation::StartBold
} else {
Annotation::EndBold
});
}
if self.code != reference.code {
annotations.push(if self.code {
Annotation::StartCode
} else {
Annotation::EndCode
});
}
if self.highlight != reference.highlight {
annotations.push(if self.highlight {
Annotation::StartHighlight
} else {
Annotation::EndHighlight
});
}
if self.italics != reference.italics {
annotations.push(if self.italics {
Annotation::StartItalics
} else {
Annotation::EndItalics
});
}
if self.link != reference.link {
annotations.push(if let Some(url) = self.link.as_ref() {
Annotation::StartLink { url: url.clone() }
} else {
Annotation::EndLink
});
}
if self.strikethrough != reference.strikethrough {
annotations.push(if self.strikethrough {
Annotation::StartStrikethrough
} else {
Annotation::EndStrikethrough
});
}
if self.underline != reference.underline {
annotations.push(if self.underline {
Annotation::StartUnderline
} else {
Annotation::EndUnderline
});
}
if self.label != reference.label {
if let Some(label) = self.label.as_ref() {
annotations.push(Annotation::Label(label.clone()))
}
}
if self.timestamp != reference.timestamp {
if let Some(timestamp) = self.timestamp {
annotations.push(Annotation::Timestamp { timestamp })
}
}
annotations
}
pub fn contains(&self, annotation: &Annotation) -> bool {
match annotation {
Annotation::StartBold => self.bold,
Annotation::EndBold => !self.bold,
Annotation::StartCode => self.code,
Annotation::EndCode => !self.code,
Annotation::StartHighlight => self.highlight,
Annotation::EndHighlight => !self.highlight,
Annotation::StartItalics => self.italics,
Annotation::EndItalics => !self.italics,
Annotation::StartLink { .. } => self.link.is_some(),
Annotation::EndLink => self.link.is_none(),
Annotation::Mention(_) => false,
Annotation::Timestamp { .. } => self.timestamp.is_some(),
Annotation::StartStrikethrough => self.strikethrough,
Annotation::EndStrikethrough => !self.strikethrough,
Annotation::StartUnderline => self.underline,
Annotation::EndUnderline => !self.underline,
Annotation::Label(_) => self.label.is_some(),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, TypedBuilder)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::formatting")
)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Mention {
#[builder(setter(into))]
pub name: String,
#[builder(setter(into))]
pub user_id: String,
}
pub fn first_annotation_index_for_offset(range: &[AnnotationWithOffset], offset: u32) -> usize {
let mut index = annotation_insertion_index(range, offset);
while index > 0 && range[index - 1].offset == offset {
index -= 1;
}
index
}
#[test]
fn test_first_annotation_index_for_offset() {
let formatting = vec![
AnnotationWithOffset::new(30, Annotation::StartBold),
AnnotationWithOffset::new(30, Annotation::StartItalics),
AnnotationWithOffset::new(94, Annotation::EndBold),
AnnotationWithOffset::new(94, Annotation::EndItalics),
];
assert_eq!(first_annotation_index_for_offset(&formatting, 10), 0);
assert_eq!(first_annotation_index_for_offset(&formatting, 30), 0);
assert_eq!(first_annotation_index_for_offset(&formatting, 31), 2);
assert_eq!(first_annotation_index_for_offset(&formatting, 94), 2);
assert_eq!(first_annotation_index_for_offset(&formatting, 95), 4);
}
pub fn first_annotation_index_beyond_offset(range: &[AnnotationWithOffset], offset: u32) -> usize {
let mut index = annotation_insertion_index(range, offset);
while index < range.len() && range[index].offset == offset {
index += 1;
}
index
}
#[test]
fn test_first_annotation_index_beyond_offset() {
let formatting = vec![
AnnotationWithOffset::new(30, Annotation::StartBold),
AnnotationWithOffset::new(30, Annotation::StartItalics),
AnnotationWithOffset::new(94, Annotation::EndBold),
AnnotationWithOffset::new(94, Annotation::EndItalics),
];
assert_eq!(first_annotation_index_beyond_offset(&formatting, 10), 0);
assert_eq!(first_annotation_index_beyond_offset(&formatting, 30), 2);
assert_eq!(first_annotation_index_beyond_offset(&formatting, 31), 2);
assert_eq!(first_annotation_index_beyond_offset(&formatting, 94), 4);
assert_eq!(first_annotation_index_beyond_offset(&formatting, 95), 4);
}
pub fn annotation_insertion_index(range: &[AnnotationWithOffset], offset: u32) -> usize {
match range.binary_search_by(|annotation| annotation.offset.cmp(&offset)) {
Ok(index) => index,
Err(insertion_index) => insertion_index,
}
}
#[must_use]
pub fn translate(range: &[AnnotationWithOffset], delta: i64) -> Formatting {
range
.iter()
.map(|annotation| annotation.translate(delta))
.collect()
}