1use std::{ops::Range, rc::Rc};
2
3use crate::{ParagraphStyle, SpanStyle};
4
5#[derive(Clone)]
26pub enum LinkAnnotation {
27 Url(String),
31
32 Clickable {
36 tag: String,
38 handler: Rc<dyn Fn()>,
40 },
41}
42
43impl std::fmt::Debug for LinkAnnotation {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::Url(url) => f.debug_tuple("Url").field(url).finish(),
47 Self::Clickable { tag, .. } => f.debug_struct("Clickable").field("tag", tag).finish(),
48 }
49 }
50}
51
52impl PartialEq for LinkAnnotation {
53 fn eq(&self, other: &Self) -> bool {
54 match (self, other) {
55 (Self::Url(a), Self::Url(b)) => a == b,
56 (
57 Self::Clickable {
58 tag: ta,
59 handler: ha,
60 },
61 Self::Clickable {
62 tag: tb,
63 handler: hb,
64 },
65 ) => ta == tb && Rc::ptr_eq(ha, hb),
66 _ => false,
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq)]
76pub struct StringAnnotation {
77 pub tag: String,
80 pub annotation: String,
82}
83
84#[derive(Debug, Clone, PartialEq)]
88pub enum LinkKey {
89 Url(String),
91 Clickable(String),
93}
94
95#[derive(Debug, Clone, PartialEq)]
104pub struct RenderString {
105 text: String,
106 span_styles: Vec<RangeStyle<SpanStyle>>,
107 paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
108 string_annotations: Vec<RangeStyle<StringAnnotation>>,
109 links: Vec<RangeStyle<LinkKey>>,
110 hash: u64,
111}
112
113impl Default for RenderString {
114 fn default() -> Self {
115 Self::from_parts(
116 String::new(),
117 Vec::new(),
118 Vec::new(),
119 Vec::new(),
120 Vec::new(),
121 )
122 }
123}
124
125const _: () = {
126 fn assert_send<T: Send + Sync>() {}
127 #[expect(dead_code, reason = "a compile-time check that is never called")]
128 fn assert_render_string_is_send_sync() {
129 assert_send::<RenderString>();
130 }
131};
132
133impl RenderString {
134 fn from_parts(
135 text: String,
136 span_styles: Vec<RangeStyle<SpanStyle>>,
137 paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
138 string_annotations: Vec<RangeStyle<StringAnnotation>>,
139 links: Vec<RangeStyle<LinkKey>>,
140 ) -> Self {
141 let hash = render_hash_impl(&text, &span_styles, ¶graph_styles);
142 Self {
143 text,
144 span_styles,
145 paragraph_styles,
146 string_annotations,
147 links,
148 hash,
149 }
150 }
151
152 pub fn text(&self) -> &str {
154 &self.text
155 }
156
157 pub fn span_styles(&self) -> &[RangeStyle<SpanStyle>] {
159 &self.span_styles
160 }
161
162 pub fn paragraph_styles(&self) -> &[RangeStyle<ParagraphStyle>] {
164 &self.paragraph_styles
165 }
166
167 pub fn string_annotations(&self) -> &[RangeStyle<StringAnnotation>] {
169 &self.string_annotations
170 }
171
172 pub fn links(&self) -> &[RangeStyle<LinkKey>] {
175 &self.links
176 }
177
178 pub fn len(&self) -> usize {
180 self.text.len()
181 }
182
183 pub fn is_empty(&self) -> bool {
185 self.text.is_empty()
186 }
187
188 pub fn span_boundaries(&self) -> SpanBoundaries {
192 span_boundaries_impl(&self.text, &self.span_styles)
193 }
194
195 pub fn render_hash(&self) -> u64 {
199 self.hash
200 }
201
202 pub fn subsequence(&self, range: Range<usize>) -> Self {
206 let start = range.start.min(self.text.len());
207 let end = range.end.max(start).min(self.text.len());
208 if start == end {
209 return Self::default();
210 }
211
212 Self::from_parts(
213 self.text[start..end].to_string(),
214 clip_range_styles(&self.span_styles, start, end),
215 clip_range_styles(&self.paragraph_styles, start, end),
216 clip_range_styles(&self.string_annotations, start, end),
217 clip_range_styles(&self.links, start, end),
218 )
219 }
220}
221
222fn clip_range_styles<T: Clone>(
223 styles: &[RangeStyle<T>],
224 start: usize,
225 end: usize,
226) -> Vec<RangeStyle<T>> {
227 let mut clipped = Vec::new();
228 for style in styles {
229 let intersection_start = style.range.start.max(start);
230 let intersection_end = style.range.end.min(end);
231 if intersection_start < intersection_end {
232 clipped.push(RangeStyle {
233 item: style.item.clone(),
234 range: (intersection_start - start)..(intersection_end - start),
235 });
236 }
237 }
238 clipped
239}
240
241pub type SpanBoundaries = smallvec::SmallVec<[usize; 8]>;
244
245fn span_boundaries_impl(text: &str, span_styles: &[RangeStyle<SpanStyle>]) -> SpanBoundaries {
246 let mut boundaries: SpanBoundaries = smallvec::smallvec![0, text.len()];
247 for span in span_styles {
248 boundaries.push(span.range.start);
249 boundaries.push(span.range.end);
250 }
251 boundaries.sort_unstable();
252 boundaries.dedup();
253 boundaries.retain(|boundary| *boundary <= text.len() && text.is_char_boundary(*boundary));
254 boundaries
255}
256
257fn render_hash_impl(
258 text: &str,
259 span_styles: &[RangeStyle<SpanStyle>],
260 paragraph_styles: &[RangeStyle<ParagraphStyle>],
261) -> u64 {
262 use std::hash::{Hash, Hasher};
263
264 let mut hasher = cranpose_ui_graphics::FxHasher::default();
265 text.hash(&mut hasher);
266 span_styles.len().hash(&mut hasher);
267 for span in span_styles {
268 span.range.start.hash(&mut hasher);
269 span.range.end.hash(&mut hasher);
270 span.item.render_hash().hash(&mut hasher);
271 }
272 paragraph_styles.len().hash(&mut hasher);
273 for paragraph in paragraph_styles {
274 paragraph.range.start.hash(&mut hasher);
275 paragraph.range.end.hash(&mut hasher);
276 paragraph.item.render_hash().hash(&mut hasher);
277 }
278 hasher.finish()
279}
280
281#[derive(Debug, Clone, PartialEq, Default)]
285pub struct AnnotatedString {
286 pub text: String,
288 pub span_styles: Vec<RangeStyle<SpanStyle>>,
290 pub paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
292 pub string_annotations: Vec<RangeStyle<StringAnnotation>>,
295 pub link_annotations: Vec<RangeStyle<LinkAnnotation>>,
298}
299
300#[derive(Debug, Clone, PartialEq)]
302pub struct RangeStyle<T> {
303 pub item: T,
305 pub range: Range<usize>,
307}
308
309impl AnnotatedString {
310 pub fn new(text: String) -> Self {
312 Self {
313 text,
314 span_styles: vec![],
315 paragraph_styles: vec![],
316 string_annotations: vec![],
317 link_annotations: vec![],
318 }
319 }
320
321 pub fn builder() -> Builder {
323 Builder::new()
324 }
325
326 pub fn len(&self) -> usize {
328 self.text.len()
329 }
330
331 pub fn is_empty(&self) -> bool {
333 self.text.is_empty()
334 }
335
336 pub fn span_boundaries(&self) -> SpanBoundaries {
338 span_boundaries_impl(&self.text, &self.span_styles)
339 }
340
341 pub fn render_string(&self) -> RenderString {
346 RenderString::from_parts(
347 self.text.clone(),
348 self.span_styles.clone(),
349 self.paragraph_styles.clone(),
350 self.string_annotations.clone(),
351 self.link_annotations
352 .iter()
353 .map(|link| RangeStyle {
354 item: match &link.item {
355 LinkAnnotation::Url(url) => LinkKey::Url(url.clone()),
356 LinkAnnotation::Clickable { tag, .. } => LinkKey::Clickable(tag.clone()),
357 },
358 range: link.range.clone(),
359 })
360 .collect(),
361 )
362 }
363
364 pub fn span_measurement_hash(&self) -> u64 {
368 use std::hash::Hasher;
369 let mut hasher = cranpose_ui_graphics::FxHasher::default();
370 hasher.write_usize(self.span_styles.len());
371 for span in &self.span_styles {
372 hasher.write_usize(span.range.start);
373 hasher.write_usize(span.range.end);
374 crate::style::hash_span_measurement(&span.item, &mut hasher);
375 }
376 hasher.finish()
377 }
378
379 pub fn render_hash(&self) -> u64 {
381 render_hash_impl(&self.text, &self.span_styles, &self.paragraph_styles)
382 }
383
384 pub fn subsequence(&self, range: Range<usize>) -> Self {
387 if range.is_empty() {
388 return Self::new(String::new());
389 }
390
391 let start = range.start.min(self.text.len());
392 let end = range.end.max(start).min(self.text.len());
393
394 if start == end {
395 return Self::new(String::new());
396 }
397
398 let mut new_spans = Vec::new();
399 for span in &self.span_styles {
400 let intersection_start = span.range.start.max(start);
401 let intersection_end = span.range.end.min(end);
402 if intersection_start < intersection_end {
403 new_spans.push(RangeStyle {
404 item: span.item.clone(),
405 range: (intersection_start - start)..(intersection_end - start),
406 });
407 }
408 }
409
410 let mut new_paragraphs = Vec::new();
411 for span in &self.paragraph_styles {
412 let intersection_start = span.range.start.max(start);
413 let intersection_end = span.range.end.min(end);
414 if intersection_start < intersection_end {
415 new_paragraphs.push(RangeStyle {
416 item: span.item.clone(),
417 range: (intersection_start - start)..(intersection_end - start),
418 });
419 }
420 }
421
422 let mut new_string_annotations = Vec::new();
423 for ann in &self.string_annotations {
424 let intersection_start = ann.range.start.max(start);
425 let intersection_end = ann.range.end.min(end);
426 if intersection_start < intersection_end {
427 new_string_annotations.push(RangeStyle {
428 item: ann.item.clone(),
429 range: (intersection_start - start)..(intersection_end - start),
430 });
431 }
432 }
433
434 let mut new_link_annotations = Vec::new();
435 for ann in &self.link_annotations {
436 let intersection_start = ann.range.start.max(start);
437 let intersection_end = ann.range.end.min(end);
438 if intersection_start < intersection_end {
439 new_link_annotations.push(RangeStyle {
440 item: ann.item.clone(),
441 range: (intersection_start - start)..(intersection_end - start),
442 });
443 }
444 }
445
446 Self {
447 text: self.text[start..end].to_string(),
448 span_styles: new_spans,
449 paragraph_styles: new_paragraphs,
450 string_annotations: new_string_annotations,
451 link_annotations: new_link_annotations,
452 }
453 }
454
455 pub fn get_string_annotations(
459 &self,
460 tag: &str,
461 start: usize,
462 end: usize,
463 ) -> Vec<&RangeStyle<StringAnnotation>> {
464 self.string_annotations
465 .iter()
466 .filter(|ann| ann.item.tag == tag && ann.range.start < end && ann.range.end > start)
467 .collect()
468 }
469
470 pub fn get_link_annotations(
474 &self,
475 start: usize,
476 end: usize,
477 ) -> Vec<&RangeStyle<LinkAnnotation>> {
478 self.link_annotations
479 .iter()
480 .filter(|ann| ann.range.start < end && ann.range.end > start)
481 .collect()
482 }
483}
484
485impl From<String> for AnnotatedString {
486 fn from(text: String) -> Self {
487 Self::new(text)
488 }
489}
490
491impl From<&str> for AnnotatedString {
492 fn from(text: &str) -> Self {
493 Self::new(text.to_owned())
494 }
495}
496
497impl From<&String> for AnnotatedString {
498 fn from(text: &String) -> Self {
499 Self::new(text.clone())
500 }
501}
502
503impl From<&mut String> for AnnotatedString {
504 fn from(text: &mut String) -> Self {
505 Self::new(text.clone())
506 }
507}
508
509#[derive(Debug, Default, Clone)]
511pub struct Builder {
512 text: String,
513 span_styles: Vec<MutableRange<SpanStyle>>,
514 paragraph_styles: Vec<MutableRange<ParagraphStyle>>,
515 string_annotations: Vec<MutableRange<StringAnnotation>>,
516 link_annotations: Vec<MutableRange<LinkAnnotation>>,
517 style_stack: Vec<StyleStackRecord>,
518}
519
520#[derive(Debug, Clone)]
521struct MutableRange<T> {
522 item: T,
523 start: usize,
524 end: usize,
525}
526
527#[derive(Debug, Clone)]
528struct StyleStackRecord {
529 style_type: StyleType,
530 index: usize,
531}
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534enum StyleType {
535 Span,
536 Paragraph,
537 StringAnnotation,
538 LinkAnnotation,
539}
540
541fn clamp_subsequence_range(text: &str, range: Range<usize>) -> Range<usize> {
542 let start = range.start.min(text.len());
543 let end = range.end.max(start).min(text.len());
544 start..end
545}
546
547fn finished_ranges<T>(ranges: Vec<MutableRange<T>>) -> Vec<RangeStyle<T>> {
552 let mut finished: Vec<RangeStyle<T>> = ranges
553 .into_iter()
554 .map(|range| RangeStyle {
555 item: range.item,
556 range: range.start..range.end,
557 })
558 .collect();
559 finished.shrink_to_fit();
560 finished
561}
562
563fn append_clipped_ranges<T: Clone>(
564 target: &mut Vec<MutableRange<T>>,
565 source: &[RangeStyle<T>],
566 source_range: Range<usize>,
567 target_offset: usize,
568) {
569 for style in source {
570 let intersection_start = style.range.start.max(source_range.start);
571 let intersection_end = style.range.end.min(source_range.end);
572 if intersection_start < intersection_end {
573 target.push(MutableRange {
574 item: style.item.clone(),
575 start: (intersection_start - source_range.start) + target_offset,
576 end: (intersection_end - source_range.start) + target_offset,
577 });
578 }
579 }
580}
581
582impl Builder {
583 pub fn new() -> Self {
585 Self::default()
586 }
587
588 pub fn append(mut self, text: &str) -> Self {
590 self.text.push_str(text);
591 self
592 }
593
594 pub fn append_annotated(self, annotated: &AnnotatedString) -> Self {
596 self.append_annotated_subsequence(annotated, 0..annotated.text.len())
597 }
598
599 pub fn append_annotated_subsequence(
602 mut self,
603 annotated: &AnnotatedString,
604 range: Range<usize>,
605 ) -> Self {
606 let range = clamp_subsequence_range(annotated.text.as_str(), range);
607 if range.is_empty() {
608 return self;
609 }
610
611 debug_assert!(annotated.text.is_char_boundary(range.start));
612 debug_assert!(annotated.text.is_char_boundary(range.end));
613
614 let target_offset = self.text.len();
615 self.text.push_str(&annotated.text[range.clone()]);
616 append_clipped_ranges(
617 &mut self.span_styles,
618 &annotated.span_styles,
619 range.clone(),
620 target_offset,
621 );
622 append_clipped_ranges(
623 &mut self.paragraph_styles,
624 &annotated.paragraph_styles,
625 range.clone(),
626 target_offset,
627 );
628 append_clipped_ranges(
629 &mut self.string_annotations,
630 &annotated.string_annotations,
631 range.clone(),
632 target_offset,
633 );
634 append_clipped_ranges(
635 &mut self.link_annotations,
636 &annotated.link_annotations,
637 range,
638 target_offset,
639 );
640 self
641 }
642
643 pub fn push_style(mut self, style: SpanStyle) -> Self {
647 let index = self.span_styles.len();
648 self.span_styles.push(MutableRange {
649 item: style,
650 start: self.text.len(),
651 end: usize::MAX,
652 });
653 self.style_stack.push(StyleStackRecord {
654 style_type: StyleType::Span,
655 index,
656 });
657 self
658 }
659
660 pub fn push_paragraph_style(mut self, style: ParagraphStyle) -> Self {
662 let index = self.paragraph_styles.len();
663 self.paragraph_styles.push(MutableRange {
664 item: style,
665 start: self.text.len(),
666 end: usize::MAX,
667 });
668 self.style_stack.push(StyleStackRecord {
669 style_type: StyleType::Paragraph,
670 index,
671 });
672 self
673 }
674
675 pub fn push_string_annotation(mut self, tag: &str, annotation: &str) -> Self {
679 let index = self.string_annotations.len();
680 self.string_annotations.push(MutableRange {
681 item: StringAnnotation {
682 tag: tag.to_string(),
683 annotation: annotation.to_string(),
684 },
685 start: self.text.len(),
686 end: usize::MAX,
687 });
688 self.style_stack.push(StyleStackRecord {
689 style_type: StyleType::StringAnnotation,
690 index,
691 });
692 self
693 }
694
695 pub fn push_link(mut self, link: LinkAnnotation) -> Self {
700 let index = self.link_annotations.len();
701 self.link_annotations.push(MutableRange {
702 item: link,
703 start: self.text.len(),
704 end: usize::MAX,
705 });
706 self.style_stack.push(StyleStackRecord {
707 style_type: StyleType::LinkAnnotation,
708 index,
709 });
710 self
711 }
712
713 pub fn with_link(self, link: LinkAnnotation, block: impl FnOnce(Self) -> Self) -> Self {
728 let b = self.push_link(link);
729 let b = block(b);
730 b.pop()
731 }
732
733 pub fn pop(mut self) -> Self {
735 if let Some(record) = self.style_stack.pop() {
736 match record.style_type {
737 StyleType::Span => {
738 self.span_styles[record.index].end = self.text.len();
739 }
740 StyleType::Paragraph => {
741 self.paragraph_styles[record.index].end = self.text.len();
742 }
743 StyleType::StringAnnotation => {
744 self.string_annotations[record.index].end = self.text.len();
745 }
746 StyleType::LinkAnnotation => {
747 self.link_annotations[record.index].end = self.text.len();
748 }
749 }
750 }
751 self
752 }
753
754 pub fn to_annotated_string(mut self) -> AnnotatedString {
756 while let Some(record) = self.style_stack.pop() {
757 match record.style_type {
758 StyleType::Span => {
759 self.span_styles[record.index].end = self.text.len();
760 }
761 StyleType::Paragraph => {
762 self.paragraph_styles[record.index].end = self.text.len();
763 }
764 StyleType::StringAnnotation => {
765 self.string_annotations[record.index].end = self.text.len();
766 }
767 StyleType::LinkAnnotation => {
768 self.link_annotations[record.index].end = self.text.len();
769 }
770 }
771 }
772
773 AnnotatedString {
774 text: self.text,
775 span_styles: finished_ranges(self.span_styles),
776 paragraph_styles: finished_ranges(self.paragraph_styles),
777 string_annotations: finished_ranges(self.string_annotations),
778 link_annotations: finished_ranges(self.link_annotations),
779 }
780 }
781}
782
783#[cfg(test)]
784#[path = "tests/annotated_string_tests.rs"]
785mod tests;