use crate::places_new::autocomplete::response::StringRange;
#[derive(
// std
Clone,
Debug,
Eq,
PartialEq,
// serde
serde::Deserialize,
serde::Serialize,
// getset
getset::Getters,
getset::MutGetters,
getset::Setters,
// other
bon::Builder
)]
#[serde(rename_all = "camelCase")]
pub struct FormattableText {
#[getset(get = "pub", set = "pub", get_mut = "pub")]
pub text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[getset(set = "pub", get_mut = "pub")]
pub matches: Vec<StringRange>,
}
impl FormattableText {
#[must_use]
pub const fn new(text: String, matches: Vec<StringRange>) -> Self {
Self { text, matches }
}
#[must_use]
pub const fn new_unmatched(text: String) -> Self {
Self {
text,
matches: Vec::new(),
}
}
#[must_use]
pub fn matches(&self) -> &[StringRange] {
&self.matches
}
#[must_use]
pub fn has_matches(&self) -> bool {
!self.matches.is_empty()
}
#[must_use]
pub fn match_text(&self, range: StringRange) -> Option<&str> {
self.text.get(range.range())
}
#[must_use]
pub fn all_matches(&self) -> Vec<&str> {
self.matches
.iter()
.filter_map(|range| self.match_text(*range))
.collect()
}
#[must_use]
pub fn unmatched_portions(&self) -> Vec<&str> {
let mut portions = Vec::new();
let mut last_end = 0;
for range in &self.matches {
let r = range.range();
if r.start > last_end {
if let Some(unmatched) = self.text.get(last_end..r.start) {
portions.push(unmatched);
}
}
last_end = r.end;
}
if last_end < self.text.len() {
if let Some(unmatched) = self.text.get(last_end..) {
portions.push(unmatched);
}
}
portions
}
#[must_use]
pub fn to_html(&self, tag: &str) -> String {
let mut result = String::with_capacity(self.text.len() + self.matches.len() * 20);
let mut last_end = 0;
for range in &self.matches {
let r = range.range();
if r.start > last_end {
if let Some(unmatched) = self.text.get(last_end..r.start) {
result.push_str(unmatched);
}
}
if let Some(matched) = self.text.get(r.clone()) {
result.push('<');
result.push_str(tag);
result.push('>');
result.push_str(matched);
result.push_str("</");
result.push_str(tag);
result.push('>');
}
last_end = r.end;
}
if last_end < self.text.len() {
if let Some(remaining) = self.text.get(last_end..) {
result.push_str(remaining);
}
}
result
}
#[must_use]
pub fn to_html_with_class(&self, tag: &str, class: &str) -> String {
let mut result = String::with_capacity(self.text.len() + self.matches.len() * 30);
let mut last_end = 0;
for range in &self.matches {
let r = range.range();
if r.start > last_end {
if let Some(unmatched) = self.text.get(last_end..r.start) {
result.push_str(unmatched);
}
}
if let Some(matched) = self.text.get(r.clone()) {
result.push('<');
result.push_str(tag);
result.push_str(" class=\"");
result.push_str(class);
result.push_str("\">");
result.push_str(matched);
result.push_str("</");
result.push_str(tag);
result.push('>');
}
last_end = r.end;
}
if last_end < self.text.len() {
if let Some(remaining) = self.text.get(last_end..) {
result.push_str(remaining);
}
}
result
}
#[must_use]
pub fn format_with<F>(&self, mut formatter: F) -> String
where
F: FnMut(&str, bool) -> String,
{
let mut result = String::with_capacity(self.text.len() + self.matches.len() * 20);
let mut last_end = 0;
for range in &self.matches {
let r = range.range();
if r.start > last_end {
if let Some(unmatched) = self.text.get(last_end..r.start) {
result.push_str(&formatter(unmatched, false));
}
}
if let Some(matched) = self.text.get(r.clone()) {
result.push_str(&formatter(matched, true));
}
last_end = r.end;
}
if last_end < self.text.len() {
if let Some(remaining) = self.text.get(last_end..) {
result.push_str(&formatter(remaining, false));
}
}
result
}
}
impl std::fmt::Display for FormattableText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text)
}
}
impl From<String> for FormattableText {
fn from(text: String) -> Self {
Self::new_unmatched(text)
}
}
impl From<&str> for FormattableText {
fn from(text: &str) -> Self {
Self::new_unmatched(text.to_string())
}
}