use crate::schema::{Paint, TypeRole};
use std::rc::Rc;
pub const SPAN_LEN: usize = 36;
pub const SPAN_START_AT: usize = 0;
pub const SPAN_LENGTH_AT: usize = 4;
pub const SPAN_ROLE_AT: usize = 8;
pub const SPAN_FLAGS_AT: usize = 10;
pub const SPAN_COLOR_AT: usize = 12;
pub const SPAN_BACKGROUND_AT: usize = 20;
pub const SPAN_HANDLER_AT: usize = 28;
pub const FLAG_BOLD: u16 = 1;
pub const FLAG_ITALIC: u16 = 2;
pub const FLAG_UNDERLINE: u16 = 4;
pub const FLAG_STRIKETHROUGH: u16 = 8;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TextSpan {
pub start: u32,
pub length: u32,
pub type_role: Option<TypeRole>,
pub color: Option<Paint>,
pub background: Option<Paint>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub on_click: Option<u64>,
}
impl TextSpan {
pub const fn new(start: u32, length: u32) -> Self {
Self {
start,
length,
type_role: None,
color: None,
background: None,
bold: false,
italic: false,
underline: false,
strikethrough: false,
on_click: None,
}
}
pub const fn bold(mut self) -> Self {
self.bold = true;
self
}
pub const fn italic(mut self) -> Self {
self.italic = true;
self
}
pub const fn underline(mut self) -> Self {
self.underline = true;
self
}
pub const fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
pub const fn with_role(mut self, role: TypeRole) -> Self {
self.type_role = Some(role);
self
}
pub const fn with_color(mut self, color: Paint) -> Self {
self.color = Some(color);
self
}
pub const fn with_background(mut self, background: Paint) -> Self {
self.background = Some(background);
self
}
fn flags(&self) -> u16 {
let mut flags = 0;
if self.bold {
flags |= FLAG_BOLD;
}
if self.italic {
flags |= FLAG_ITALIC;
}
if self.underline {
flags |= FLAG_UNDERLINE;
}
if self.strikethrough {
flags |= FLAG_STRIKETHROUGH;
}
flags
}
fn write_into(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.start.to_le_bytes());
out.extend_from_slice(&self.length.to_le_bytes());
out.extend_from_slice(&self.type_role.map_or(0, u16::from).to_le_bytes());
out.extend_from_slice(&self.flags().to_le_bytes());
let paint = self.color.map_or(0, Paint::to_bits);
out.extend_from_slice(&(paint as u32).to_le_bytes());
out.extend_from_slice(&((paint >> 32) as u32).to_le_bytes());
let background = self.background.map_or(0, Paint::to_bits);
out.extend_from_slice(&(background as u32).to_le_bytes());
out.extend_from_slice(&((background >> 32) as u32).to_le_bytes());
let handler = self.on_click.unwrap_or(0);
out.extend_from_slice(&(handler as u32).to_le_bytes());
out.extend_from_slice(&((handler >> 32) as u32).to_le_bytes());
}
}
type Marked = (&'static [u8], fn(u32, u32) -> TextSpan);
#[derive(Clone, Default, PartialEq)]
pub struct TextSpans {
bytes: Option<Rc<[u8]>>,
}
impl std::fmt::Debug for TextSpans {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TextSpans")
.field("spans", &(self.as_bytes().len() / SPAN_LEN))
.finish()
}
}
impl TextSpans {
pub fn new(spans: impl IntoIterator<Item = TextSpan>) -> Self {
let mut bytes = Vec::new();
for span in spans {
span.write_into(&mut bytes);
}
Self::from_bytes(bytes)
}
pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
let bytes = bytes.into();
Self {
bytes: (!bytes.is_empty()).then_some(bytes),
}
}
pub fn as_bytes(&self) -> &[u8] {
self.bytes.as_deref().unwrap_or(&[])
}
pub fn is_empty(&self) -> bool {
self.bytes.is_none()
}
pub fn spans(&self) -> impl Iterator<Item = TextSpan> + '_ {
self.as_bytes().chunks_exact(SPAN_LEN).map(|record| {
let word = |at: usize| {
u32::from_le_bytes([record[at], record[at + 1], record[at + 2], record[at + 3]])
};
let half = |at: usize| u16::from_le_bytes([record[at], record[at + 1]]);
let flags = half(SPAN_FLAGS_AT);
let paint = u64::from(word(SPAN_COLOR_AT)) | (u64::from(word(SPAN_COLOR_AT + 4)) << 32);
let background = u64::from(word(SPAN_BACKGROUND_AT))
| (u64::from(word(SPAN_BACKGROUND_AT + 4)) << 32);
let handler =
u64::from(word(SPAN_HANDLER_AT)) | (u64::from(word(SPAN_HANDLER_AT + 4)) << 32);
TextSpan {
start: word(SPAN_START_AT),
length: word(SPAN_LENGTH_AT),
type_role: TypeRole::try_from(half(SPAN_ROLE_AT)).ok(),
color: Paint::from_bits(paint),
background: Paint::from_bits(background),
bold: flags & FLAG_BOLD != 0,
italic: flags & FLAG_ITALIC != 0,
underline: flags & FLAG_UNDERLINE != 0,
strikethrough: flags & FLAG_STRIKETHROUGH != 0,
on_click: (handler != 0).then_some(handler),
}
})
}
pub fn from_markdown(source: &str) -> (String, Self) {
let mut text = String::with_capacity(source.len());
let mut spans = Vec::new();
let bytes = source.as_bytes();
let mut at = 0;
while at < bytes.len() {
let (marker, span_of): Marked = if bytes[at..].starts_with(b"**") {
(b"**", |start, length| TextSpan::new(start, length).bold())
} else if bytes[at] == b'*' {
(b"*", |start, length| TextSpan::new(start, length).italic())
} else if bytes[at] == b'`' {
(b"`", |start, length| {
TextSpan::new(start, length).with_role(TypeRole::Mono)
})
} else {
text.push(source[at..].chars().next().expect("a char boundary"));
at += source[at..].chars().next().map_or(1, char::len_utf8);
continue;
};
let body = at + marker.len();
let Some(end) = find(&bytes[body..], marker).map(|offset| body + offset) else {
text.push_str(&source[at..body]);
at = body;
continue;
};
let start = text.len() as u32;
text.push_str(&source[body..end]);
spans.push(span_of(start, text.len() as u32 - start));
at = end + marker.len();
}
(text, Self::new(spans))
}
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}