use std::collections::BTreeMap;
use crate::utils;
#[derive(Clone)]
pub struct StflRichText {
text: String,
style_tags: BTreeMap<usize, String>,
}
impl StflRichText {
pub fn from_plaintext(text: &str) -> Self {
let quoted_text = utils::quote_for_stfl(text);
Self::from_quoted("ed_text)
}
pub fn from_quoted(text: &str) -> Self {
let (text, style_tags) = Self::extract_style_tags(text);
Self { text, style_tags }
}
pub fn plaintext(&self) -> &str {
&self.text
}
pub fn quoted(&self) -> String {
self.insert_style_tags()
}
pub fn apply_style_tag(&mut self, tag: &str, start: usize, end: usize) {
self.merge_style_tag(tag, start, end);
}
fn extract_style_tags(text: &str) -> (String, BTreeMap<usize, String>) {
let mut result = text.to_owned();
let mut tags = BTreeMap::new();
let mut pos = 0;
while pos < result.len() {
let remainder = &result[pos..];
let tag_start = remainder.find(['<', '>']);
let Some(tag_start) = tag_start else {
break;
};
if remainder[tag_start..].starts_with('>') {
pos += tag_start + 1;
continue;
}
let remainder_in_tag = &remainder[tag_start + 1..];
let tag_end = remainder_in_tag.find(['<', '>']).map(|i| i + tag_start + 1);
let Some(tag_end) = tag_end else {
break;
};
if remainder[tag_end..].starts_with('<') {
pos += tag_start + 1;
continue;
}
if tag_end - tag_start == 1 {
result.remove(pos + tag_end);
pos += tag_start + 1;
continue;
}
tags.insert(
pos + tag_start,
remainder[tag_start..tag_end + 1].to_owned(),
);
result.replace_range(pos + tag_start..pos + tag_end + 1, "");
pos += tag_start;
}
(result, tags)
}
fn merge_style_tag(&mut self, tag: &str, start: usize, end: usize) {
if end <= start {
return;
}
let mut latest_tag = "</>";
for (&pos, tag) in self.style_tags.iter() {
if pos > end {
break;
}
latest_tag = tag;
}
let previous_tag = latest_tag.to_owned();
self.style_tags.insert(start, tag.into());
self.style_tags.insert(end, previous_tag);
self.style_tags.retain(|&k, _v| k <= start || k >= end);
}
fn insert_style_tags(&self) -> String {
let mut result = self.text.clone();
let mut tags = self.style_tags.clone();
let mut pos = 0;
while pos < result.len() {
let bracket = result[pos..].find('<').map(|i| i + pos);
let Some(bracket) = bracket else {
break;
};
pos = bracket + 1;
tags.insert(pos, '>'.into());
}
for (&pos, tag) in tags.iter().rev() {
result.insert_str(pos, tag);
}
result
}
}
#[cfg(test)]
mod tests {
use super::StflRichText;
proptest::proptest! {
#[test]
fn t_from_plaintext_does_not_crash_on_any_utf8_inputs(ref input in "\\PC*") {
let _ = StflRichText::from_plaintext(input);
}
#[test]
fn t_from_quoted_does_not_crash_on_any_utf8_inputs(ref input in "\\PC*") {
let _ = StflRichText::from_quoted(input);
}
}
}