From 552414235559f6806f100add48cd077e116d2afd Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Mon, 24 Feb 2025 19:41:20 -0800 Subject: [PATCH] progress on text_line tests --- rust/pspp/src/output/text.rs | 31 +--- rust/pspp/src/output/text_line.rs | 264 +++++++++++++++++++++++++++++- 2 files changed, 264 insertions(+), 31 deletions(-) diff --git a/rust/pspp/src/output/text.rs b/rust/pspp/src/output/text.rs index 50e4eaa9d6..9f788bd07d 100644 --- a/rust/pspp/src/output/text.rs +++ b/rust/pspp/src/output/text.rs @@ -10,13 +10,11 @@ use enum_map::{Enum, EnumMap}; use unicode_linebreak::{linebreaks, BreakOpportunity}; use unicode_width::UnicodeWidthStr; -use crate::output::pivot::DisplayValue; - use super::{ driver::Driver, pivot::{Axis2, BorderStyle, Coord2, PivotTable, Rect2, Stroke}, render::{Device, DrawCell, Pager, Params}, - table::{CellInner, Content}, + table::Content, text_line::TextLine, Details, Item, }; @@ -274,30 +272,11 @@ impl TextDriver { return Coord2::default(); } - /* - let mut breaks = linebreaks(text); - let bb_w = bb[Axis2::X].len(); - let bb_h = bb[Axis2::Y].len(); - let mut pos = 0; - for _ in 0..bb_h { - let mut w = 0; - loop { - let (index, opportunity) = breaks.next().unwrap(); - match opportunity { - BreakOpportunity::Mandatory => break index, - BreakOpportunity::Allowed => { - let segment_width = text[pos..index].width(); - if w > 0 && w + segment_width > bb_w { - break index; - } - pos = index; - w += segment_width; - } - } - } - todo!() + let mut breaks = new_line_breaks(text, bb[Axis2::X].len()); + let mut size = Coord2::new(0, 0); + for text in breaks { + } - */ todo!() } } diff --git a/rust/pspp/src/output/text_line.rs b/rust/pspp/src/output/text_line.rs index 79f1164c68..33d9419684 100644 --- a/rust/pspp/src/output/text_line.rs +++ b/rust/pspp/src/output/text_line.rs @@ -1,6 +1,7 @@ -use std::ops::Range; +use enum_iterator::Sequence; +use std::{borrow::Cow, fmt::Debug, ops::Range}; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; +use unicode_width::UnicodeWidthChar; /// A line of text, encoded in UTF-8, with support functions that properly /// handle double-width characters and backspaces. @@ -43,7 +44,8 @@ impl TextLine { } pub fn put(&mut self, x0: usize, s: &str) { - let w = s.width(); + let w = Widths::new(s).sum::(); + dbg!(w); let x1 = x0 + w; if w == 0 { // Nothing to do. @@ -62,7 +64,10 @@ impl TextLine { self.string.push_str(s); self.width = x1; } else { + dbg!(x0); + dbg!(x1); let span = self.find_span(x0, x1); + dbg!(&span); if span.columns.start < x0 || span.columns.end > x1 { let tail = self.string.split_off(span.offsets.end); self.string.truncate(span.offsets.start); @@ -79,7 +84,7 @@ impl TextLine { fn find_span(&self, x0: usize, x1: usize) -> Position { let p0 = self.find_pos(x0); let p1 = self.find_pos(x1); - if p0.columns.start >= x1 { + if true || p0.columns.start >= x1 { Position { columns: p0.columns.start..p1.columns.start, offsets: p0.offsets.start..p1.offsets.start, @@ -92,6 +97,7 @@ impl TextLine { } } + // Returns the [Position] that contains column `target_x`. fn find_pos(&self, target_x: usize) -> Position { let mut x = 0; let mut ofs = 0; @@ -113,9 +119,14 @@ impl TextLine { offsets: ofs..ofs, } } + + fn str(&self) -> &str { + &self.string + } } /// Position of one or more characters within a [TextLine]. +#[derive(Debug)] struct Position { /// 0-based display columns. columns: Range, @@ -124,6 +135,7 @@ struct Position { offsets: Range, } +/// Iterates through the column widths in a string. struct Widths<'a> { s: &'a str, base: &'a str, @@ -134,10 +146,13 @@ impl<'a> Widths<'a> { Self { s, base: s } } + /// Returns the amount of the string remaining to be visited. fn as_str(&self) -> &str { self.s } + // Returns the offset into the original string of the characters remaining + // to be visited. fn offset(&self) -> usize { self.base.len() - self.s.len() } @@ -149,7 +164,7 @@ impl<'a> Iterator for Widths<'a> { fn next(&mut self) -> Option { let mut iter = self.s.char_indices(); let (_, mut c) = iter.next()?; - if iter.as_str().starts_with('\x08') { + while iter.as_str().starts_with('\x08') { iter.next(); c = match iter.next() { Some((_, c)) => c, @@ -176,3 +191,242 @@ impl<'a> Iterator for Widths<'a> { Some(w) } } + +#[derive(Copy, Clone, PartialEq, Eq, Sequence)] +struct Emphasis { + pub bold: bool, + pub underline: bool, +} + +impl Emphasis { + const fn plain() -> Self { + Self { + bold: false, + underline: false, + } + } + fn is_plain(&self) -> bool { + *self == Self::plain() + } + fn apply<'a>(&self, s: &'a str) -> Cow<'a, str> { + if self.is_plain() { + Cow::from(s) + } else { + let mut output = String::with_capacity( + s.len() * (1 + self.bold as usize * 2 + self.underline as usize * 2), + ); + for c in s.chars() { + if self.bold { + output.push(c); + output.push('\x08'); + } + if self.underline { + output.push('_'); + output.push('\x08'); + } + output.push(c); + } + Cow::from(output) + } + } +} + +impl Debug for Emphasis { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self { + bold: false, + underline: false, + } => "plain", + Self { + bold: true, + underline: false, + } => "bold", + Self { + bold: false, + underline: true, + } => "underline", + Self { + bold: true, + underline: true, + } => "bold+underline", + } + ) + } +} + +#[cfg(test)] +mod test { + use super::{Emphasis, TextLine}; + use enum_iterator::all; + + #[test] + fn overwrite_rest_of_line() { + for lowercase in all::() { + for uppercase in all::() { + let mut line = TextLine::new(); + line.put(0, &lowercase.apply("abc")); + line.put(1, &uppercase.apply("BCD")); + assert_eq!( + line.str(), + &format!("{}{}", lowercase.apply("a"), uppercase.apply("BCD")), + "uppercase={uppercase:?} lowercase={lowercase:?}" + ); + } + } + } + + #[test] + fn overwrite_partial_line() { + for lowercase in all::() { + for uppercase in all::() { + let mut line = TextLine::new(); + // Produces `AbCDEf`. + line.put(0, &lowercase.apply("abcdef")); + line.put(0, &uppercase.apply("A")); + line.put(2, &uppercase.apply("CDE")); + assert_eq!( + line.str().replace('\x08', "#"), + format!( + "{}{}{}{}", + uppercase.apply("A"), + lowercase.apply("b"), + uppercase.apply("CDE"), + lowercase.apply("f") + ) + .replace('\x08', "#"), + "uppercase={uppercase:?} lowercase={lowercase:?}" + ); + } + } + } + + #[test] + fn overwrite_rest_with_double_width() { + for lowercase in all::() { + for hiragana in all::() { + let mut line = TextLine::new(); + // Produces `kaきくけ"`. + line.put(0, &lowercase.apply("kakiku")); + line.put(2, &hiragana.apply("きくけ")); + assert_eq!( + line.str(), + &format!("{}{}", lowercase.apply("ka"), hiragana.apply("きくけ")), + "lowercase={lowercase:?} hiragana={hiragana:?}" + ); + } + } + } + + #[test] + fn overwrite_partial_with_double_width() { + for lowercase in all::() { + for hiragana in all::() { + let mut line = TextLine::new(); + // Produces `かkiくけko". + line.put(0, &lowercase.apply("kakikukeko")); + line.put(0, &hiragana.apply("か")); + line.put(4, &hiragana.apply("くけ")); + assert_eq!( + line.str(), + &format!( + "{}{}{}{}", + hiragana.apply("か"), + lowercase.apply("ki"), + hiragana.apply("くけ"), + lowercase.apply("ko") + ), + "lowercase={lowercase:?} hiragana={hiragana:?}" + ); + } + } + } + + /// Overwrite rest of line, aligned double-width over double-width + #[test] + fn aligned_double_width_rest_of_line() { + for bottom in all::() { + for top in all::() { + let mut line = TextLine::new(); + // Produces `あきくけ`. + line.put(0, &bottom.apply("あいう")); + line.put(2, &top.apply("きくけ")); + assert_eq!( + line.str(), + &format!("{}{}", bottom.apply("あ"), top.apply("きくけ")), + "bottom={bottom:?} top={top:?}" + ); + } + } + } + + /// Overwrite rest of line, misaligned double-width over double-width + #[test] + fn misaligned_double_width_rest_of_line() { + for bottom in all::() { + for top in all::() { + let mut line = TextLine::new(); + // Produces `あきくけ`. + line.put(0, &bottom.apply("あいう")); + line.put(3, &top.apply("きくけ")); + assert_eq!( + line.str(), + &format!("{}?{}", bottom.apply("あ"), top.apply("きくけ")), + "bottom={bottom:?} top={top:?}" + ); + } + } + } + + /// Overwrite partial line, aligned double-width over double-width + #[test] + fn aligned_double_width_partial() { + for bottom in all::() { + for top in all::() { + let mut line = TextLine::new(); + // Produces `かいくけお`. + line.put(0, &bottom.apply("あいうえお")); + line.put(0, &top.apply("か")); + line.put(4, &top.apply("くけ")); + assert_eq!( + line.str(), + &format!( + "{}{}{}{}", + top.apply("か"), + bottom.apply("い"), + top.apply("くけ"), + bottom.apply("お") + ), + "bottom={bottom:?} top={top:?}" + ); + } + } + } + + /// Overwrite partial line, misaligned double-width over double-width + #[test] + fn misaligned_double_width_partial() { + for bottom in all::() { + for top in all::() { + let mut line = TextLine::new(); + // Produces `?か??くけ?さ`. + line.put(0, &bottom.apply("あいうえお")); + line.put(1, &top.apply("か")); + //line.put(5, &top.apply("くけ")); + assert_eq!( + line.str(), + &format!( + "?{}??{}{}", + top.apply("か"), + top.apply("くけ"), + bottom.apply("さ") + ), + "bottom={bottom:?} top={top:?}" + ); + } + } + } +} -- 2.30.2