use std::ops::Range;
#[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct Attrs(pub u8);
impl Attrs {
pub const NONE: Attrs = Attrs(0);
pub const BOLD: Attrs = Attrs(1 << 0);
pub const ITALIC: Attrs = Attrs(1 << 1);
pub const UNDERLINE: Attrs = Attrs(1 << 2);
pub const STRIKETHROUGH: Attrs = Attrs(1 << 3);
pub const CODE: Attrs = Attrs(1 << 4);
pub const REVERSE: Attrs = Attrs(1 << 5);
#[inline]
pub fn contains(self, other: Attrs) -> bool {
self.0 & other.0 == other.0
}
#[inline]
pub fn union(self, other: Attrs) -> Attrs {
Attrs(self.0 | other.0)
}
#[inline]
pub fn remove(self, other: Attrs) -> Attrs {
Attrs(self.0 & !other.0)
}
#[inline]
pub fn is_empty(self) -> bool {
self.0 == 0
}
}
impl std::fmt::Debug for Attrs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
return f.write_str("NONE");
}
let mut first = true;
for (bit, name) in [
(Attrs::BOLD, "BOLD"),
(Attrs::ITALIC, "ITALIC"),
(Attrs::UNDERLINE, "UNDERLINE"),
(Attrs::STRIKETHROUGH, "STRIKETHROUGH"),
(Attrs::CODE, "CODE"),
(Attrs::REVERSE, "REVERSE"),
] {
if self.contains(bit) {
if !first {
f.write_str("|")?;
}
f.write_str(name)?;
first = false;
}
}
Ok(())
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Hash)]
pub enum ColorRef {
#[default]
Default,
Palette(u8),
Rgb(u32),
}
pub type LinkId = u32;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Hash)]
pub struct Style {
pub attrs: Attrs,
pub fg: ColorRef,
pub bg: ColorRef,
pub link: Option<LinkId>,
}
impl Style {
pub fn with_attrs(mut self, a: Attrs) -> Style {
self.attrs = self.attrs.union(a);
self
}
pub fn with_fg(mut self, fg: ColorRef) -> Style {
self.fg = fg;
self
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Span {
pub range: Range<usize>,
pub style: Style,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Link {
pub href: String,
pub range: Range<usize>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct ParsedText {
pub text: String,
pub spans: Vec<Span>,
pub links: Vec<Link>,
}
impl ParsedText {
pub fn compact(&mut self) {
exact_string(&mut self.text);
exact_vec(&mut self.spans);
exact_vec(&mut self.links);
for l in &mut self.links {
exact_string(&mut l.href);
}
}
pub fn plain(text: impl Into<String>) -> ParsedText {
ParsedText {
text: text.into(),
spans: Vec::new(),
links: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn len(&self) -> usize {
self.text.len()
}
pub fn style_at(&self, at: usize) -> Style {
for s in &self.spans {
if s.range.contains(&at) {
return s.style;
}
if s.range.start > at {
break;
}
}
Style::default()
}
pub fn add_link(&mut self, range: Range<usize>, href: impl Into<String>) -> Option<LinkId> {
if range.start >= range.end
|| range.end > self.text.len()
|| !self.text.is_char_boundary(range.start)
|| !self.text.is_char_boundary(range.end)
{
return None;
}
let id = self.links.len() as LinkId;
self.links.push(Link {
href: href.into(),
range: range.clone(),
});
let mut out: Vec<Span> = Vec::with_capacity(self.spans.len() + 2);
let mut cursor = range.start;
for s in &self.spans {
if s.range.end <= range.start || s.range.start >= range.end {
out.push(s.clone());
continue;
}
if s.range.start < range.start {
out.push(Span {
range: s.range.start..range.start,
style: s.style,
});
}
let ms = s.range.start.max(range.start);
let me = s.range.end.min(range.end);
if ms > cursor {
out.push(Span {
range: cursor..ms,
style: link_style(Style::default(), id),
});
}
if ms < me {
out.push(Span {
range: ms..me,
style: link_style(s.style, id),
});
cursor = me;
}
if s.range.end > range.end {
out.push(Span {
range: range.end..s.range.end,
style: s.style,
});
}
}
if cursor < range.end {
out.push(Span {
range: cursor..range.end,
style: link_style(Style::default(), id),
});
}
out.sort_by_key(|s| s.range.start);
self.spans = out;
self.debug_assert_well_formed();
Some(id)
}
pub fn link_at(&self, at: usize) -> Option<&Link> {
let id = self.style_at(at).link?;
self.links.get(id as usize)
}
pub fn debug_assert_well_formed(&self) {
if !cfg!(debug_assertions) {
return;
}
let mut prev_end = 0usize;
for s in &self.spans {
assert!(s.range.start < s.range.end, "empty span {:?}", s.range);
assert!(
s.range.start >= prev_end,
"overlapping or unsorted spans: {:?} after end {}",
s.range,
prev_end
);
assert!(
s.range.end <= self.text.len(),
"span {:?} past text len {}",
s.range,
self.text.len()
);
assert!(
self.text.is_char_boundary(s.range.start)
&& self.text.is_char_boundary(s.range.end),
"span {:?} not on char boundaries",
s.range
);
prev_end = s.range.end;
}
}
}
#[derive(Default)]
pub(crate) struct SpanBuilder {
text: String,
spans: Vec<Span>,
links: Vec<Link>,
}
impl SpanBuilder {
pub(crate) fn new() -> SpanBuilder {
SpanBuilder::default()
}
pub(crate) fn len(&self) -> usize {
self.text.len()
}
pub(crate) fn push(&mut self, s: &str, style: Style) {
if s.is_empty() {
return;
}
let start = self.text.len();
self.text.push_str(s);
let end = self.text.len();
if style == Style::default() {
return;
}
if let Some(last) = self.spans.last_mut() {
if last.range.end == start && last.style == style {
last.range.end = end;
return;
}
}
self.spans.push(Span {
range: start..end,
style,
});
}
pub(crate) fn reserve_link(&mut self, href: String) -> LinkId {
self.links.push(Link { href, range: 0..0 });
(self.links.len() - 1) as LinkId
}
pub(crate) fn set_link_range(&mut self, id: LinkId, range: Range<usize>) {
if let Some(l) = self.links.get_mut(id as usize) {
l.range = range;
}
}
pub(crate) fn finish(self) -> ParsedText {
ParsedText {
text: self.text,
spans: self.spans,
links: self.links,
}
}
}
fn link_style(base: Style, id: LinkId) -> Style {
let mut s = base;
s.link = Some(id);
s.attrs = s.attrs.union(Attrs::UNDERLINE);
s
}
pub(crate) fn exact_string(s: &mut String) {
if s.capacity() > s.len() {
*s = s.as_str().to_owned();
}
}
pub(crate) fn exact_vec<T>(v: &mut Vec<T>) {
if v.capacity() > v.len() {
let mut exact = Vec::with_capacity(v.len());
exact.append(v);
*v = exact;
}
}