1mod export;
26pub mod graphics;
27pub mod regions;
28
29pub use export::{HtmlOptions, SvgOptions};
30pub use graphics::{Graphic, GraphicsPlan, Placement, PlacementSource, plan as plan_graphics};
31pub use regions::{Rect, Region, RegionRecorder, Span, render_frame, role_name};
32
33use std::collections::HashMap;
34use std::hash::{BuildHasherDefault, Hasher};
35use std::ops::Range;
36
37use rich::cells::{cell_len, split_graphemes};
38use rich::protocol::RegionId;
39use rich::{ColorSystem, Console, Segment, Style};
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub struct StyleId(u32);
44
45impl StyleId {
46 pub const NONE: StyleId = StyleId(0);
48
49 pub const GONE: StyleId = StyleId(u32::MAX);
52
53 pub fn index(self) -> usize {
54 self.0 as usize
55 }
56
57 pub(crate) fn from_index(index: usize) -> StyleId {
58 StyleId(index as u32)
59 }
60}
61
62pub type Rgb = [u8; 3];
64
65#[derive(Clone, Debug)]
77pub struct StyleTable {
78 styles: Vec<Option<Style>>,
79 keys: Vec<u64>,
81 pairs: Vec<u64>,
84 chain: Vec<u32>,
87 index: HashMap<u64, u32, FxBuild>,
89 rgb: HashMap<u64, StyleId, FxBuild>,
91 last: StyleId,
93 kept: usize,
95}
96
97impl Default for StyleTable {
98 fn default() -> Self {
99 StyleTable {
100 styles: vec![None],
101 keys: vec![0],
102 pairs: vec![0],
103 chain: vec![0],
104 index: HashMap::default(),
105 rgb: HashMap::default(),
106 last: StyleId::NONE,
107 kept: 1,
108 }
109 }
110}
111
112impl StyleTable {
113 pub fn new() -> Self {
114 StyleTable::default()
115 }
116
117 pub fn intern(&mut self, style: Option<&Style>) -> StyleId {
119 let Some(style) = style else {
120 return StyleId::NONE;
121 };
122 if self.styles[self.last.index()].as_ref() == Some(style) {
123 return self.last;
124 }
125 let key = style_key(style);
126 let id = match self.find(key, style) {
127 Some(id) => id,
128 None => self.push(key, style.clone()),
129 };
130 self.last = id;
131 id
132 }
133
134 pub fn intern_rgb(&mut self, fg: Option<Rgb>, bg: Option<Rgb>) -> StyleId {
151 let pair = pair_key(fg, bg);
152 if pair == 0 {
153 return StyleId::NONE;
154 }
155 if self.pairs[self.last.index()] == pair {
156 return self.last;
157 }
158 let id = match self.rgb.get(&pair) {
159 Some(&id) => id,
160 None => {
161 let style = Style::from_color(
162 fg.map(|[r, g, b]| rich::Color::from_rgb(r, g, b)),
163 bg.map(|[r, g, b]| rich::Color::from_rgb(r, g, b)),
164 );
165 let key = style_key(&style);
166 let id = match self.find(key, &style) {
167 Some(id) => id,
168 None => self.push(key, style),
169 };
170 self.pairs[id.index()] = pair;
171 self.rgb.insert(pair, id);
172 id
173 }
174 };
175 self.last = id;
176 id
177 }
178
179 pub fn rgb(&self, id: StyleId) -> Option<(Option<Rgb>, Option<Rgb>)> {
182 match self.pairs.get(id.index()).copied().unwrap_or(0) {
183 0 => None,
184 pair => Some((unpack(pair >> 32), unpack(pair))),
185 }
186 }
187
188 fn find(&self, key: u64, style: &Style) -> Option<StyleId> {
189 let mut at = self.index.get(&key).copied().unwrap_or(0);
190 while at != 0 {
191 if self.styles[at as usize].as_ref() == Some(style) {
192 return Some(StyleId(at));
193 }
194 at = self.chain[at as usize];
195 }
196 None
197 }
198
199 fn push(&mut self, key: u64, style: Style) -> StyleId {
200 let id = self.styles.len() as u32;
201 let pair = truecolor_pair(&style).unwrap_or(0);
204 self.styles.push(Some(style));
205 self.keys.push(key);
206 self.pairs.push(pair);
207 if pair != 0 {
208 self.rgb.entry(pair).or_insert(StyleId(id));
209 }
210 let next = self.index.insert(key, id).unwrap_or(0);
212 self.chain.push(next);
213 StyleId(id)
214 }
215
216 pub fn get(&self, id: StyleId) -> Option<&Style> {
218 self.styles.get(id.index()).and_then(Option::as_ref)
219 }
220
221 pub fn len(&self) -> usize {
223 self.styles.len()
224 }
225
226 pub fn is_empty(&self) -> bool {
228 false
229 }
230
231 pub fn piled_up(&self) -> bool {
235 self.styles.len() > PILE.max(2 * self.kept)
236 }
237
238 pub fn retain(&mut self, keep: &[bool]) -> StyleRemap {
260 let mut map = vec![StyleRemap::GONE; self.styles.len()];
261 map[0] = 0;
262 let mut next = 1usize;
263 for (old, new) in map.iter_mut().enumerate().skip(1) {
264 if !keep.get(old).copied().unwrap_or(false) {
265 continue;
266 }
267 *new = next as u32;
268 if old != next {
269 self.styles.swap(old, next);
270 self.keys[next] = self.keys[old];
271 self.pairs[next] = self.pairs[old];
272 }
273 next += 1;
274 }
275 self.styles.truncate(next);
276 self.keys.truncate(next);
277 self.pairs.truncate(next);
278 self.chain.clear();
279 self.chain.resize(next, 0);
280 self.index.clear();
281 self.rgb.clear();
282 for id in 1..next as u32 {
283 let key = self.keys[id as usize];
284 self.chain[id as usize] = self.index.insert(key, id).unwrap_or(0);
285 if self.pairs[id as usize] != 0 {
286 self.rgb.insert(self.pairs[id as usize], StyleId(id));
287 }
288 }
289 self.last = StyleId::NONE;
290 self.kept = next;
291 StyleRemap { map }
292 }
293}
294
295const PILE: usize = 1024;
297
298#[derive(Clone, Debug, Default, PartialEq, Eq)]
301pub struct StyleRemap {
302 map: Vec<u32>,
303}
304
305impl StyleRemap {
306 const GONE: u32 = u32::MAX;
307
308 pub fn get(&self, old: StyleId) -> Option<StyleId> {
311 match self.map.get(old.index()).copied() {
312 None | Some(StyleRemap::GONE) => None,
313 Some(new) => Some(StyleId(new)),
314 }
315 }
316
317 pub fn get_or_gone(&self, old: StyleId) -> StyleId {
321 self.get(old).unwrap_or(StyleId::GONE)
322 }
323}
324
325fn style_key(style: &Style) -> u64 {
328 let mut hasher = FxHasher::default();
329 hash_color(style.color(), &mut hasher);
330 hash_color(style.bgcolor(), &mut hasher);
331 let mut attrs = 0u32;
332 for index in 0..13 {
333 attrs |= match style.attr(index) {
334 None => 0,
335 Some(false) => 1,
336 Some(true) => 2,
337 } << (2 * index);
338 }
339 hasher.write_u32(attrs);
340 if let Some(link) = style.link() {
341 hasher.write(link.as_bytes());
342 }
343 hasher.finish()
344}
345
346fn hash_color(color: Option<&rich::Color>, hasher: &mut FxHasher) {
347 match color {
348 None => hasher.write_u32(0),
349 Some(color) => {
350 let triplet = color
351 .triplet
352 .map_or(0, |t| u32::from_be_bytes([0, t.red, t.green, t.blue]));
353 hasher.write_u64(
354 (color.kind as u64 + 1) << 40
355 | u64::from(color.number.map_or(0, |n| n as u32 + 1)) << 24
356 | u64::from(triplet),
357 );
358 }
359 }
360}
361
362fn truecolor_pair(style: &Style) -> Option<u64> {
368 let rgb = |color: Option<&rich::Color>| match color {
369 None => Some(None),
370 Some(c) if c.kind == rich::color::ColorType::Truecolor => {
371 c.triplet.map(|t| Some([t.red, t.green, t.blue]))
372 }
373 Some(_) => None,
374 };
375 let (fg, bg) = (rgb(style.color())?, rgb(style.bgcolor())?);
376 if fg.is_none() && bg.is_none() {
377 return None;
378 }
379 let plain = Style::from_color(style.color().cloned(), style.bgcolor().cloned());
380 (plain == *style).then(|| pair_key(fg, bg))
381}
382
383fn simple_text(text: &str) -> bool {
386 text.chars()
387 .all(|c| matches!(crate::surface::char_width(c), 1 | 2) && c != '\u{fe0f}')
388}
389
390fn text_width(text: &str) -> usize {
393 if text.contains(['\u{200d}', '\u{fe0f}']) {
394 return cell_len(text);
395 }
396 text.chars().map(crate::surface::char_width).sum()
397}
398
399fn pair_key(fg: Option<Rgb>, bg: Option<Rgb>) -> u64 {
400 let pack = |c: Option<Rgb>| {
401 c.map_or(0, |[r, g, b]| {
402 1 << 24 | u64::from(u32::from_be_bytes([0, r, g, b]))
403 })
404 };
405 pack(fg) << 32 | pack(bg)
406}
407
408fn unpack(half: u64) -> Option<Rgb> {
409 let half = half as u32;
410 (half & 1 << 24 != 0).then(|| {
411 let [_, r, g, b] = half.to_be_bytes();
412 [r, g, b]
413 })
414}
415
416#[derive(Clone, Copy, Default)]
420pub(crate) struct FxHasher {
421 hash: u64,
422}
423
424impl FxHasher {
425 const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
426
427 #[inline]
428 fn add(&mut self, word: u64) {
429 self.hash = (self.hash.rotate_left(5) ^ word).wrapping_mul(FxHasher::SEED);
430 }
431}
432
433impl Hasher for FxHasher {
434 #[inline]
435 fn write(&mut self, bytes: &[u8]) {
436 let (words, tail) = bytes.as_chunks::<8>();
437 for word in words {
438 self.add(u64::from_le_bytes(*word));
439 }
440 let mut rest = [0u8; 8];
441 rest[..tail.len()].copy_from_slice(tail);
442 self.add(u64::from_le_bytes(rest) ^ (tail.len() as u64) << 56);
443 }
444
445 #[inline]
446 fn write_u8(&mut self, n: u8) {
447 self.add(n.into());
448 }
449
450 #[inline]
451 fn write_u32(&mut self, n: u32) {
452 self.add(n.into());
453 }
454
455 #[inline]
456 fn write_u64(&mut self, n: u64) {
457 self.add(n);
458 }
459
460 #[inline]
461 fn write_usize(&mut self, n: usize) {
462 self.add(n as u64);
463 }
464
465 #[inline]
466 fn finish(&self) -> u64 {
467 self.hash
468 }
469}
470
471pub(crate) type FxBuild = BuildHasherDefault<FxHasher>;
473
474const NEWLINE: u8 = 1;
476const JOIN: u8 = 2;
478
479#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub struct Run {
483 text: u32,
484 len: u32,
485 cells: u32,
486 style: StyleId,
487 flags: u8,
488}
489
490impl Run {
491 pub fn cells(&self) -> usize {
493 self.cells as usize
494 }
495
496 pub fn style(&self) -> StyleId {
497 self.style
498 }
499
500 fn range(&self) -> Range<usize> {
501 self.text as usize..(self.text + self.len) as usize
502 }
503}
504
505#[derive(Clone, Copy, Debug, PartialEq, Eq)]
508pub struct Cell<'a> {
509 pub text: &'a str,
510 pub width: u8,
511 pub style: StyleId,
512}
513
514impl Cell<'_> {
515 pub fn is_continuation(&self) -> bool {
516 self.width == 0 && self.text.is_empty()
517 }
518}
519
520#[derive(Clone, Debug, PartialEq, Eq)]
522pub struct Change {
523 pub row: usize,
524 pub columns: Range<usize>,
525}
526
527#[derive(Clone, Debug, Default)]
529#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
530pub struct Frame {
531 rows: Vec<Range<u32>>,
533 runs: Vec<Run>,
534 text: String,
535 styles: StyleTable,
536 trailing_newline: bool,
538 tags: Vec<RegionId>,
540 tag_index: HashMap<RegionId, u32>,
542 run_tags: Vec<u32>,
545 regions: Vec<Region>,
547 region_ids: HashMap<RegionId, usize>,
548 placements: Vec<Placement>,
550}
551
552impl Frame {
553 pub fn from_segments(segments: &[Segment]) -> Frame {
556 let mut frame = Frame {
557 runs: Vec::with_capacity(segments.len()),
558 text: String::with_capacity(segments.iter().map(|s| s.text.len()).sum()),
559 ..Frame::default()
560 };
561 let mut row_start = 0u32;
562 for segment in segments.iter().filter(|segment| !segment.control) {
563 let (style, tag) = match segment.style.as_ref() {
567 Some(style) if style.meta_ref().is_some() => match regions::split_region(style) {
568 Some((id, bare)) => (frame.styles.intern(Some(&bare)), frame.tag(id)),
569 None => (frame.styles.intern(Some(style)), 0),
570 },
571 other => (frame.styles.intern(other), 0),
572 };
573 let mut flags = 0;
574 let mut rest = segment.text.as_str();
575 loop {
576 match rest.find('\n') {
577 Some(at) => {
578 frame.push(&rest[..at], style, flags | NEWLINE, tag);
579 let end = frame.runs.len() as u32;
580 frame.rows.push(row_start..end);
581 row_start = end;
582 rest = &rest[at + 1..];
583 flags = JOIN;
584 }
585 None => {
586 if !rest.is_empty() {
587 frame.push(rest, style, flags, tag);
588 }
589 break;
590 }
591 }
592 }
593 }
594 let end = frame.runs.len() as u32;
595 if end > row_start {
596 frame.rows.push(row_start..end);
597 } else {
598 frame.trailing_newline = !frame.rows.is_empty();
599 }
600 frame
601 }
602
603 fn tag(&mut self, id: RegionId) -> u32 {
605 let next = self.tags.len() as u32;
606 let at = *self.tag_index.entry(id).or_insert(next);
607 if at == next {
608 self.tags.push(id);
609 }
610 at + 1
611 }
612
613 fn push(&mut self, text: &str, style: StyleId, flags: u8, tag: u32) {
614 if tag != 0 && self.run_tags.len() < self.runs.len() {
615 self.run_tags.resize(self.runs.len(), 0);
616 }
617 if tag != 0 || !self.run_tags.is_empty() {
618 self.run_tags.push(tag);
619 }
620 let offset = self.text.len() as u32;
621 self.text.push_str(text);
622 self.runs.push(Run {
623 text: offset,
624 len: text.len() as u32,
625 cells: text_width(text) as u32,
626 style,
627 flags,
628 });
629 }
630
631 pub fn merged(&self) -> Frame {
636 let mut frame = Frame {
637 runs: Vec::with_capacity(self.runs.len()),
638 text: String::with_capacity(self.text.len()),
639 styles: self.styles.clone(),
640 trailing_newline: self.trailing_newline,
641 tags: self.tags.clone(),
642 tag_index: self.tag_index.clone(),
643 regions: self.regions.clone(),
644 region_ids: self.region_ids.clone(),
645 placements: self.placements.clone(),
646 ..Frame::default()
647 };
648 for row in 0..self.height() {
649 let start = frame.runs.len() as u32;
650 for index in self.row_range(row) {
651 let run = &self.runs[index];
652 if run.len == 0 {
653 continue;
654 }
655 let tag = self.run_tags.get(index).copied().unwrap_or(0);
656 let text = &self.text[run.range()];
657 let last_tag = frame.run_tags.last().copied().unwrap_or(0);
658 match frame.runs[start as usize..].last_mut() {
659 Some(last) if last.style == run.style && last_tag == tag => {
660 last.len += run.len;
661 last.cells += run.cells;
662 frame.text.push_str(text);
663 }
664 _ => frame.push(text, run.style, 0, tag),
665 }
666 }
667 frame.rows.push(start..frame.runs.len() as u32);
668 }
669 frame
670 }
671
672 pub fn height(&self) -> usize {
674 self.rows.len()
675 }
676
677 pub fn row(&self, index: usize) -> &[Run] {
679 &self.runs[self.row_range(index)]
680 }
681
682 fn row_range(&self, index: usize) -> Range<usize> {
684 let range = &self.rows[index];
685 range.start as usize..range.end as usize
686 }
687
688 fn run_tag(&self, index: usize) -> Option<RegionId> {
690 match self.run_tags.get(index).copied().unwrap_or(0) {
691 0 => None,
692 tag => self.tags.get(tag as usize - 1).copied(),
693 }
694 }
695
696 pub fn row_width(&self, index: usize) -> usize {
698 self.row(index).iter().map(Run::cells).sum()
699 }
700
701 pub fn width(&self) -> usize {
703 (0..self.height())
704 .map(|row| self.row_width(row))
705 .max()
706 .unwrap_or(0)
707 }
708
709 pub fn run_text(&self, run: &Run) -> &str {
711 &self.text[run.range()]
712 }
713
714 pub fn styles(&self) -> &StyleTable {
715 &self.styles
716 }
717
718 pub fn ends_with_newline(&self) -> bool {
720 self.trailing_newline
721 }
722
723 pub fn run_count(&self) -> usize {
725 self.runs.len()
726 }
727
728 pub fn plain(&self) -> String {
730 let mut out = String::with_capacity(self.text.len() + self.height());
731 for row in 0..self.height() {
732 if row > 0 {
733 out.push('\n');
734 }
735 for run in self.row(row) {
736 out.push_str(self.run_text(run));
737 }
738 }
739 if self.trailing_newline {
740 out.push('\n');
741 }
742 out
743 }
744
745 pub fn to_ansi(&self, console: &Console) -> String {
749 self.encode(console.color_system(), console.no_color())
750 }
751
752 pub fn encode(&self, system: Option<ColorSystem>, no_color: bool) -> String {
755 let styles = self.styles_for(system, no_color);
756 let mut out = String::with_capacity(self.text.len() * 2);
757 let mut piece = String::new();
760 let mut style = StyleId::NONE;
761 for row in 0..self.height() {
762 let runs = self.row(row);
763 for run in runs {
764 if run.flags & JOIN == 0 {
765 flush(&mut out, &piece, styles[style.index()].as_ref(), system);
766 piece.clear();
767 style = run.style;
768 }
769 piece.push_str(&self.text[run.range()]);
770 if run.flags & NEWLINE != 0 {
771 piece.push('\n');
772 }
773 }
774 let breaks = row + 1 < self.height() || self.trailing_newline;
776 if breaks && runs.last().is_none_or(|run| run.flags & NEWLINE == 0) {
777 flush(&mut out, &piece, styles[style.index()].as_ref(), system);
778 piece.clear();
779 out.push('\n');
780 }
781 }
782 flush(&mut out, &piece, styles[style.index()].as_ref(), system);
783 out
784 }
785
786 pub fn to_ansi_merged(&self, console: &Console) -> String {
789 self.merged()
790 .encode(console.color_system(), console.no_color())
791 }
792
793 pub fn encode_span(
798 &self,
799 row: usize,
800 columns: Range<usize>,
801 system: Option<ColorSystem>,
802 no_color: bool,
803 ) -> String {
804 let styles = self.styles_for(system, no_color);
805 let mut out = String::new();
806 let mut piece = String::new();
807 let mut style = None;
808 for cell in self
809 .cells(row)
810 .into_iter()
811 .skip(columns.start)
812 .take(columns.len())
813 {
814 if style != Some(cell.style) {
815 if let Some(id) = style {
816 flush(
817 &mut out,
818 &piece,
819 styles[StyleId::index(id)].as_ref(),
820 system,
821 );
822 }
823 piece.clear();
824 style = Some(cell.style);
825 }
826 if cell.is_continuation() {
827 if piece.is_empty() {
828 piece.push(' ');
829 }
830 continue;
831 }
832 piece.push_str(cell.text);
833 }
834 if let Some(id) = style {
835 flush(&mut out, &piece, styles[id.index()].as_ref(), system);
836 }
837 out
838 }
839
840 fn styles_for(&self, system: Option<ColorSystem>, no_color: bool) -> Vec<Option<Style>> {
841 if no_color && system.is_some() {
842 self.styles
843 .styles
844 .iter()
845 .map(|style| style.as_ref().map(Style::without_color))
846 .collect()
847 } else {
848 self.styles.styles.clone()
849 }
850 }
851
852 pub fn cells(&self, index: usize) -> Vec<Cell<'_>> {
858 let row = self.row(index);
859 let mut cells: Vec<(Range<usize>, u8, StyleId)> =
862 Vec::with_capacity(row.iter().map(Run::cells).sum());
863 let mut leading: Option<usize> = None;
864 for run in row {
865 let base = run.text as usize;
866 let text = self.run_text(run);
867 if leading.is_none() && simple_text(text) {
868 let mut at = base;
870 for c in text.chars() {
871 let (len, width) = (c.len_utf8(), crate::surface::char_width(c));
872 cells.push((at..at + len, width as u8, run.style));
873 at += len;
874 for _ in 1..width {
875 cells.push((at..at, 0, run.style));
876 }
877 }
878 continue;
879 }
880 let (graphemes, _) = split_graphemes(text);
881 for (start, end, width) in graphemes {
882 let range = base + start..base + end;
883 if width == 0 {
884 match cells.iter_mut().rev().find(|cell| cell.1 > 0) {
885 Some(lead) if lead.0.end == range.start => lead.0.end = range.end,
886 Some(_) => {}
887 None => {
888 leading.get_or_insert(range.start);
889 }
890 }
891 continue;
892 }
893 let start = leading.take().unwrap_or(range.start);
894 cells.push((start..range.end, width as u8, run.style));
895 for _ in 1..width {
896 cells.push((range.end..range.end, 0, run.style));
897 }
898 }
899 }
900 cells
901 .into_iter()
902 .map(|(range, width, style)| Cell {
903 text: &self.text[range],
904 width,
905 style,
906 })
907 .collect()
908 }
909
910 pub fn diff(&self, previous: &Frame) -> Vec<Change> {
914 let mut changes = Vec::new();
915 for row in 0..self.height().max(previous.height()) {
916 let new = if row < self.height() {
917 self.cells(row)
918 } else {
919 Vec::new()
920 };
921 let old = if row < previous.height() {
922 previous.cells(row)
923 } else {
924 Vec::new()
925 };
926 let width = new.len().max(old.len());
927 let mut open = None;
928 for column in 0..width {
929 let same = match (new.get(column), old.get(column)) {
930 (Some(a), Some(b)) => {
931 a.text == b.text
932 && a.width == b.width
933 && self.styles.get(a.style) == previous.styles.get(b.style)
934 }
935 _ => false,
936 };
937 match (same, open) {
938 (false, None) => open = Some(column),
939 (true, Some(start)) => {
940 changes.push(Change {
941 row,
942 columns: start..column,
943 });
944 open = None;
945 }
946 _ => {}
947 }
948 }
949 if let Some(start) = open {
950 changes.push(Change {
951 row,
952 columns: start..width,
953 });
954 }
955 }
956 changes
957 }
958}
959
960fn flush(out: &mut String, text: &str, style: Option<&Style>, system: Option<ColorSystem>) {
961 match (style, system) {
962 (Some(style), Some(system)) => out.push_str(&style.render(text, Some(system))),
963 _ => out.push_str(text),
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970
971 fn seg(text: &str, style: Option<&str>) -> Segment {
972 Segment::new(text, style.map(|s| Style::parse(s).unwrap()))
973 }
974
975 fn console(system: Option<ColorSystem>, no_color: bool) -> Console {
976 Console::builder()
977 .width(40)
978 .force_terminal(true)
979 .color_system(system)
980 .no_color(no_color)
981 .build()
982 }
983
984 #[test]
985 fn exact_encoding_keeps_line_breaks_inside_a_segment() {
986 let segments = vec![
987 seg("a\nb", Some("bold")),
988 seg("c", None),
989 seg("\n", None),
990 seg("d\n", Some("red link https://x.test")),
991 ];
992 let console = console(Some(ColorSystem::Truecolor), false);
993 let frame = Frame::from_segments(&segments);
994 assert_eq!(frame.height(), 3);
995 assert!(frame.ends_with_newline());
996 assert_eq!(frame.plain(), "a\nbc\nd\n");
997 assert_eq!(
998 frame.to_ansi(&console),
999 console.segments_to_string(&segments)
1000 );
1001 }
1002
1003 #[test]
1004 fn control_segments_are_dropped() {
1005 let segments = vec![seg("x", None), Segment::control("\x1b[2K"), seg("y", None)];
1006 let frame = Frame::from_segments(&segments);
1007 assert_eq!(frame.plain(), "xy");
1008 assert_eq!(frame.run_count(), 2);
1009 }
1010
1011 #[test]
1012 fn styles_are_interned_once() {
1013 let segments = vec![
1014 seg("a", Some("bold")),
1015 seg("b", Some("italic")),
1016 seg("c", Some("bold")),
1017 seg("d", None),
1018 ];
1019 let frame = Frame::from_segments(&segments);
1020 assert_eq!(frame.styles().len(), 3);
1021 let row = frame.row(0);
1022 assert_eq!(row[0].style(), row[2].style());
1023 assert_eq!(row[3].style(), StyleId::NONE);
1024 }
1025
1026 #[test]
1027 fn no_color_keeps_attributes() {
1028 let segments = vec![seg("hot", Some("bold red on blue"))];
1029 let console = console(Some(ColorSystem::Standard), true);
1030 let frame = Frame::from_segments(&segments);
1031 assert_eq!(
1032 frame.to_ansi(&console),
1033 console.segments_to_string(&segments)
1034 );
1035 assert_eq!(frame.to_ansi(&console), "\x1b[1mhot\x1b[0m");
1036 }
1037
1038 #[test]
1039 fn merging_joins_runs_of_one_style() {
1040 let segments = vec![
1041 seg("ab", Some("bold")),
1042 seg("cd", Some("bold")),
1043 seg("", Some("red")),
1044 seg("\n", None),
1045 seg("e", None),
1046 ];
1047 let console = console(Some(ColorSystem::Truecolor), false);
1048 let frame = Frame::from_segments(&segments);
1049 let merged = frame.merged();
1050 assert_eq!(merged.row(0).len(), 1);
1051 assert_eq!(frame.to_ansi_merged(&console), "\x1b[1mabcd\x1b[0m\ne");
1052 assert_eq!(merged.plain(), frame.plain());
1053 }
1054
1055 #[test]
1056 fn cells_follow_core_widths() {
1057 let frame = Frame::from_segments(&[seg("a漢b", None)]);
1058 let cells = frame.cells(0);
1059 let widths: Vec<u8> = cells.iter().map(|cell| cell.width).collect();
1060 assert_eq!(widths, [1, 2, 0, 1]);
1061 assert!(cells[2].is_continuation());
1062 assert_eq!(frame.width(), 4);
1063 }
1064
1065 #[test]
1066 fn zero_width_graphemes_take_no_column() {
1067 let frame =
1069 Frame::from_segments(&[seg("a", None), seg("\u{301}", Some("bold")), seg("x", None)]);
1070 let cells = frame.cells(0);
1071 assert_eq!(cells.len(), 2);
1072 assert_eq!(cells[0].text, "a\u{301}");
1073 assert_eq!(cells[1].text, "x");
1074 let changed =
1075 Frame::from_segments(&[seg("a", None), seg("\u{301}", Some("bold")), seg("y", None)]);
1076 assert_eq!(
1077 changed.diff(&frame),
1078 [Change {
1079 row: 0,
1080 columns: 1..2
1081 }]
1082 );
1083 let wide = Frame::from_segments(&[seg("漢", None), seg("\u{301}", None), seg("z", None)]);
1085 let cells = wide.cells(0);
1086 assert_eq!(cells.len(), 3);
1087 assert_eq!(cells[0].text, "漢\u{301}");
1088 assert!(cells[1].is_continuation());
1089 let leading = Frame::from_segments(&[seg("\u{301}", None), seg("b", None)]);
1091 let cells = leading.cells(0);
1092 assert_eq!(cells.len(), 1);
1093 assert_eq!(cells[0].text, "\u{301}b");
1094 }
1095
1096 #[test]
1097 fn diff_reports_changed_cells() {
1098 let before = Frame::from_segments(&[seg("hello\n", None), seg("world", None)]);
1099 let after = Frame::from_segments(&[
1100 seg("hel", None),
1101 seg("l", Some("bold")),
1102 seg("o\n", None),
1103 seg("world!", None),
1104 seg("\nnew", None),
1105 ]);
1106 assert_eq!(
1107 after.diff(&before),
1108 [
1109 Change {
1110 row: 0,
1111 columns: 3..4
1112 },
1113 Change {
1114 row: 1,
1115 columns: 5..6
1116 },
1117 Change {
1118 row: 2,
1119 columns: 0..3
1120 },
1121 ]
1122 );
1123 assert!(before.diff(&before).is_empty());
1124 let split = Frame::from_segments(&[seg("hel", None), seg("lo\nworld", None)]);
1126 assert!(split.diff(&before).is_empty());
1127 }
1128
1129 #[test]
1130 fn encode_span_repaints_one_change() {
1131 let frame = Frame::from_segments(&[seg("ab", None), seg("cd", Some("bold"))]);
1132 let span = frame.encode_span(0, 1..3, Some(ColorSystem::Standard), false);
1133 assert_eq!(span, "b\x1b[1mc\x1b[0m");
1134 let wide = Frame::from_segments(&[seg("漢x", None)]);
1135 assert_eq!(wide.encode_span(0, 1..3, None, false), " x");
1136 }
1137
1138 #[test]
1139 fn styles_that_hash_alike_stay_apart() {
1140 let plain = Style::parse("bold").unwrap();
1142 let meta: rich::style::Meta = [("k", rich::style::MetaValue::Int(1))]
1143 .into_iter()
1144 .collect();
1145 let tagged = plain.clone().with_meta(meta);
1146 let mut table = StyleTable::new();
1147 let a = table.intern(Some(&plain));
1148 let b = table.intern(Some(&tagged));
1149 assert_ne!(a, b);
1150 assert_eq!(table.intern(Some(&plain)), a);
1151 assert_eq!(table.intern(Some(&tagged)), b);
1152 let remap = table.retain(&[true, false, true]);
1153 let b = remap.get(b).unwrap();
1154 assert_eq!(table.get(b), Some(&tagged));
1155 assert_eq!(table.intern(Some(&tagged)), b);
1156 assert_eq!(table.intern(Some(&plain)), StyleId::from_index(2));
1157 }
1158
1159 #[test]
1160 fn colour_pairs_and_styles_share_ids() {
1161 let mut table = StyleTable::new();
1162 let style = Style::from_color(None, Some(rich::Color::from_rgb(1, 2, 3)));
1163 let id = table.intern(Some(&style));
1164 assert_eq!(table.rgb(id), Some((None, Some([1, 2, 3]))));
1166 assert_eq!(table.intern_rgb(None, Some([1, 2, 3])), id);
1167 assert_eq!(table.rgb(id), Some((None, Some([1, 2, 3]))));
1168 assert_eq!(table.intern_rgb(None, None), StyleId::NONE);
1169 let other = table.intern_rgb(Some([0, 0, 0]), Some([1, 2, 3]));
1170 assert_ne!(other, id);
1171 let remap = table.retain(&[true, false, true]);
1173 assert_eq!(remap.get(id), None);
1174 let other = remap.get(other).unwrap();
1175 assert_eq!(table.intern_rgb(Some([0, 0, 0]), Some([1, 2, 3])), other);
1176 assert!(!table.piled_up());
1177 }
1178}