From: Ben Pfaff Date: Thu, 30 Jan 2025 21:38:53 +0000 (-0800) Subject: work on ascii driver X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6a07a93968d609cca38c23115b74e3425b84307b;p=pspp work on ascii driver --- diff --git a/rust/pspp/src/output/csv.rs b/rust/pspp/src/output/csv.rs index 73149f8dd0..4dd38f2a15 100644 --- a/rust/pspp/src/output/csv.rs +++ b/rust/pspp/src/output/csv.rs @@ -8,12 +8,7 @@ use std::{ use crate::output::pivot::Coord2; -use super::{ - driver::Driver, - pivot::{PivotTable, ValueOptions}, - table::Table, - Details, Item, TextType, -}; +use super::{driver::Driver, pivot::PivotTable, table::Table, Details, Item, TextType}; struct CsvDriver { file: File, @@ -166,12 +161,7 @@ impl Driver for CsvDriver { TextType::Syntax | TextType::PageTitle => (), TextType::Title | TextType::Log => { self.start_item(); - for line in text - .content - .display(ValueOptions::default()) - .to_string() - .lines() - { + for line in text.content.display(()).to_string().lines() { writeln!(&self.file, "{}", CsvField::new(line, self.options)).unwrap(); } } diff --git a/rust/pspp/src/output/mod.rs b/rust/pspp/src/output/mod.rs index edf93bfd82..6feac2adcb 100644 --- a/rust/pspp/src/output/mod.rs +++ b/rust/pspp/src/output/mod.rs @@ -13,6 +13,8 @@ pub mod page; pub mod pivot; pub mod render; pub mod table; +pub mod text; +pub mod text_line; /// A single output item. pub struct Item { diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index 6f78a28136..b4937f707e 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -31,44 +31,44 @@ use super::table::{CellInner, Content, Table}; pub struct Params { /// Page size to try to fit the rendering into. Some tables will, of /// course, overflow this size. - size: Coord2, + pub size: Coord2, /// Nominal size of a character in the most common font: /// `font_size[Axis2::X]` is the em width. /// `font_size[Axis2::Y]` is the line leading. - font_size: EnumMap, + pub font_size: EnumMap, /// Width of different kinds of lines. - line_widths: EnumMap, + pub line_widths: EnumMap, /// 1/96" of an inch (1px) in the rendering unit. Currently used only for /// column width ranges, as in `width_ranges` in /// [crate::output::pivot::Look]. Set to `None` to disable this feature. - px_size: Option, + pub px_size: Option, /// Minimum cell width or height before allowing the cell to be broken /// across two pages. (Joined cells may always be broken at join /// points.) - min_break: EnumMap, + pub min_break: EnumMap, /// True if the driver supports cell margins. (If false, the rendering /// engine will insert a small space betweeen adjacent cells that don't have /// an intervening rule.) - supports_margins: bool, + pub supports_margins: bool, /// True if the local language has a right-to-left direction, otherwise /// false. - rtl: bool, + pub rtl: bool, /// True if the table is being rendered for printing (as opposed to /// on-screen display). - printing: bool, + pub printing: bool, /// Whether [RenderOps::adjust_break] is implemented. - can_adjust_break: bool, + pub can_adjust_break: bool, /// Whether [RenderOps::scale] is implemented. - can_scale: bool, + pub can_scale: bool, } pub trait Device { @@ -97,7 +97,7 @@ pub trait Device { fn adjust_break(&self, cell: &Content, size: Coord2) -> usize; } -trait Draw { +pub trait Draw { /// Draws a generalized intersection of lines in `bb`. /// /// `styles` is interpreted this way: @@ -138,7 +138,7 @@ trait Draw { fn scale(&mut self, factor: f64); } -struct DrawCell<'a> { +pub struct DrawCell<'a> { pub rotate: bool, pub inner: &'a ValueInner, pub style: &'a AreaStyle, @@ -1425,7 +1425,7 @@ impl Pager { } /// True if there's content left to rnder. - fn has_next(&mut self) -> bool { + pub fn has_next(&mut self) -> bool { while self .y_break .as_mut() @@ -1455,7 +1455,7 @@ impl Pager { /// Returns the amount of vertical space actually used by the rendered /// chunk, which will be 0 if `space` is too small to render anything or if /// no content remains (use [Self::has_next] to distinguish these cases). - fn draw_next(&mut self, mut space: usize, draw: &mut dyn Draw) -> usize { + pub fn draw_next(&mut self, mut space: usize, draw: &mut dyn Draw) -> usize { use Axis2::*; if self.scale != 1.0 { diff --git a/rust/pspp/src/output/text.rs b/rust/pspp/src/output/text.rs new file mode 100644 index 0000000000..256dc8bdb3 --- /dev/null +++ b/rust/pspp/src/output/text.rs @@ -0,0 +1,324 @@ +use std::{ + borrow::Cow, + fs::File, + io::{BufWriter, Write}, + sync::{Arc, LazyLock}, +}; + +use enum_map::{Enum, EnumMap}; + +use super::{ + driver::Driver, + pivot::{Axis2, BorderStyle, Coord2, PivotTable, Rect2, Stroke}, + render::{Device, Draw, DrawCell, Pager, Params}, + table::{CellInner, Content}, + text_line::TextLine, + Details, Item, +}; + +pub struct TextDriver { + file: BufWriter, + + /// Enable bold and underline in output? + emphasis: bool, + + /// Page width. + width: usize, + + /// Minimum cell size to break across pages. + min_hbreak: usize, + + box_chars: &'static BoxChars, + + params: Params, + n_objects: usize, + lines: Vec, +} + +#[derive(Copy, Clone, PartialEq, Eq, Enum)] +enum Line { + None, + Dashed, + Single, + Double, +} + +impl From for Line { + fn from(stroke: Stroke) -> Self { + match stroke { + Stroke::None => Self::None, + Stroke::Solid | Stroke::Thick | Stroke::Thin => Self::Single, + Stroke::Dashed => Self::Dashed, + Stroke::Double => Self::Double, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Enum)] +struct Lines { + r: Line, + b: Line, + l: Line, + t: Line, +} + +#[derive(Default)] +struct BoxChars(EnumMap); + +impl BoxChars { + fn put(&mut self, r: Line, b: Line, l: Line, chars: [char; 4]) { + use Line::*; + for (t, c) in [None, Dashed, Single, Double] + .into_iter() + .zip(chars.into_iter()) + { + self.0[Lines { r, b, l, t }] = c; + } + } + fn get(&self, lines: Lines) -> char { + self.0[lines] + } +} + +static ASCII_BOX: LazyLock = LazyLock::new(|| { + let mut ascii_box = BoxChars::default(); + let n = Line::None; + let d = Line::Dashed; + use Line::{Double as D, Single as S}; + ascii_box.put(n, n, n, [' ', '|', '|', '#']); + ascii_box.put(n, n, d, ['-', '+', '+', '#']); + ascii_box.put(n, n, S, ['-', '+', '+', '#']); + ascii_box.put(n, n, D, ['=', '#', '#', '#']); + ascii_box.put(n, d, n, ['|', '|', '|', '#']); + ascii_box.put(n, d, d, ['+', '+', '+', '#']); + ascii_box.put(n, d, S, ['+', '+', '+', '#']); + ascii_box.put(n, d, D, ['#', '#', '#', '#']); + ascii_box.put(n, S, n, ['|', '|', '|', '#']); + ascii_box.put(n, S, d, ['+', '+', '+', '#']); + ascii_box.put(n, S, S, ['+', '+', '+', '#']); + ascii_box.put(n, S, D, ['#', '#', '#', '#']); + ascii_box.put(n, D, n, ['#', '#', '#', '#']); + ascii_box.put(n, D, d, ['#', '#', '#', '#']); + ascii_box.put(n, D, S, ['#', '#', '#', '#']); + ascii_box.put(n, D, D, ['#', '#', '#', '#']); + ascii_box.put(d, n, n, ['-', '+', '+', '#']); + ascii_box.put(d, n, d, ['-', '+', '+', '#']); + ascii_box.put(d, n, S, ['-', '+', '+', '#']); + ascii_box.put(d, n, D, ['#', '#', '#', '#']); + ascii_box.put(d, d, n, ['+', '+', '+', '#']); + ascii_box.put(d, d, d, ['+', '+', '+', '#']); + ascii_box.put(d, d, S, ['+', '+', '+', '#']); + ascii_box.put(d, d, D, ['#', '#', '#', '#']); + ascii_box.put(d, S, n, ['+', '+', '+', '#']); + ascii_box.put(d, S, d, ['+', '+', '+', '#']); + ascii_box.put(d, S, S, ['+', '+', '+', '#']); + ascii_box.put(d, S, D, ['#', '#', '#', '#']); + ascii_box.put(d, D, n, ['#', '#', '#', '#']); + ascii_box.put(d, D, d, ['#', '#', '#', '#']); + ascii_box.put(d, D, S, ['#', '#', '#', '#']); + ascii_box.put(d, D, D, ['#', '#', '#', '#']); + ascii_box.put(S, n, n, ['-', '+', '+', '#']); + ascii_box.put(S, n, d, ['-', '+', '+', '#']); + ascii_box.put(S, n, S, ['-', '+', '+', '#']); + ascii_box.put(S, n, D, ['#', '#', '#', '#']); + ascii_box.put(S, d, n, ['+', '+', '+', '#']); + ascii_box.put(S, d, d, ['+', '+', '+', '#']); + ascii_box.put(S, d, S, ['+', '+', '+', '#']); + ascii_box.put(S, d, D, ['#', '#', '#', '#']); + ascii_box.put(S, S, n, ['+', '+', '+', '#']); + ascii_box.put(S, S, d, ['+', '+', '+', '#']); + ascii_box.put(S, S, S, ['+', '+', '+', '#']); + ascii_box.put(S, S, D, ['#', '#', '#', '#']); + ascii_box.put(S, D, n, ['#', '#', '#', '#']); + ascii_box.put(S, D, d, ['#', '#', '#', '#']); + ascii_box.put(S, D, S, ['#', '#', '#', '#']); + ascii_box.put(S, D, D, ['#', '#', '#', '#']); + ascii_box.put(D, n, n, ['=', '#', '#', '#']); + ascii_box.put(D, n, d, ['#', '#', '#', '#']); + ascii_box.put(D, n, S, ['#', '#', '#', '#']); + ascii_box.put(D, n, D, ['=', '#', '#', '#']); + ascii_box.put(D, d, n, ['#', '#', '#', '#']); + ascii_box.put(D, d, d, ['#', '#', '#', '#']); + ascii_box.put(D, d, S, ['#', '#', '#', '#']); + ascii_box.put(D, d, D, ['#', '#', '#', '#']); + ascii_box.put(D, S, n, ['#', '#', '#', '#']); + ascii_box.put(D, S, d, ['#', '#', '#', '#']); + ascii_box.put(D, S, S, ['#', '#', '#', '#']); + ascii_box.put(D, S, D, ['#', '#', '#', '#']); + ascii_box.put(D, D, n, ['#', '#', '#', '#']); + ascii_box.put(D, D, d, ['#', '#', '#', '#']); + ascii_box.put(D, D, S, ['#', '#', '#', '#']); + ascii_box.put(D, D, D, ['#', '#', '#', '#']); + ascii_box +}); + +static UNICODE_BOX: LazyLock = LazyLock::new(|| { + let mut unicode_box = BoxChars::default(); + let n = Line::None; + let d = Line::Dashed; + use Line::{Double as D, Single as S}; + unicode_box.put(n, n, n, [' ', '╵', '╵', '║']); + unicode_box.put(n, n, d, ['╴', '╯', '╯', '╜']); + unicode_box.put(n, n, S, ['╴', '╯', '╯', '╜']); + unicode_box.put(n, n, D, ['═', '╛', '╛', '╝']); + unicode_box.put(n, S, n, ['╷', '│', '│', '║']); + unicode_box.put(n, S, d, ['╮', '┤', '┤', '╢']); + unicode_box.put(n, S, S, ['╮', '┤', '┤', '╢']); + unicode_box.put(n, S, D, ['╕', '╡', '╡', '╣']); + unicode_box.put(n, d, n, ['╷', '┊', '│', '║']); + unicode_box.put(n, d, d, ['╮', '┤', '┤', '╢']); + unicode_box.put(n, d, S, ['╮', '┤', '┤', '╢']); + unicode_box.put(n, d, D, ['╕', '╡', '╡', '╣']); + unicode_box.put(n, D, n, ['║', '║', '║', '║']); + unicode_box.put(n, D, d, ['╖', '╢', '╢', '╢']); + unicode_box.put(n, D, S, ['╖', '╢', '╢', '╢']); + unicode_box.put(n, D, D, ['╗', '╣', '╣', '╣']); + unicode_box.put(d, n, n, ['╶', '╰', '╰', '╙']); + unicode_box.put(d, n, d, ['╌', '┴', '┴', '╨']); + unicode_box.put(d, n, S, ['─', '┴', '┴', '╨']); + unicode_box.put(d, n, D, ['═', '╧', '╧', '╩']); + unicode_box.put(d, d, n, ['╭', '├', '├', '╟']); + unicode_box.put(d, d, d, ['┬', '+', '┼', '╪']); + unicode_box.put(d, d, S, ['┬', '┼', '┼', '╪']); + unicode_box.put(d, d, D, ['╤', '╪', '╪', '╬']); + unicode_box.put(d, S, n, ['╭', '├', '├', '╟']); + unicode_box.put(d, S, d, ['┬', '┼', '┼', '╪']); + unicode_box.put(d, S, S, ['┬', '┼', '┼', '╪']); + unicode_box.put(d, S, D, ['╤', '╪', '╪', '╬']); + unicode_box.put(d, D, n, ['╓', '╟', '╟', '╟']); + unicode_box.put(d, D, d, ['╥', '╫', '╫', '╫']); + unicode_box.put(d, D, S, ['╥', '╫', '╫', '╫']); + unicode_box.put(d, D, D, ['╦', '╬', '╬', '╬']); + unicode_box.put(S, n, n, ['╶', '╰', '╰', '╙']); + unicode_box.put(S, n, d, ['─', '┴', '┴', '╨']); + unicode_box.put(S, n, S, ['─', '┴', '┴', '╨']); + unicode_box.put(S, n, D, ['═', '╧', '╧', '╩']); + unicode_box.put(S, d, n, ['╭', '├', '├', '╟']); + unicode_box.put(S, d, d, ['┬', '┼', '┼', '╪']); + unicode_box.put(S, d, S, ['┬', '┼', '┼', '╪']); + unicode_box.put(S, d, D, ['╤', '╪', '╪', '╬']); + unicode_box.put(S, S, n, ['╭', '├', '├', '╟']); + unicode_box.put(S, S, d, ['┬', '┼', '┼', '╪']); + unicode_box.put(S, S, S, ['┬', '┼', '┼', '╪']); + unicode_box.put(S, S, D, ['╤', '╪', '╪', '╬']); + unicode_box.put(S, D, n, ['╓', '╟', '╟', '╟']); + unicode_box.put(S, D, d, ['╥', '╫', '╫', '╫']); + unicode_box.put(S, D, S, ['╥', '╫', '╫', '╫']); + unicode_box.put(S, D, D, ['╦', '╬', '╬', '╬']); + unicode_box.put(D, n, n, ['═', '╘', '╘', '╚']); + unicode_box.put(D, n, d, ['═', '╧', '╧', '╩']); + unicode_box.put(D, n, S, ['═', '╧', '╧', '╩']); + unicode_box.put(D, n, D, ['═', '╧', '╧', '╩']); + unicode_box.put(D, d, n, ['╒', '╞', '╞', '╠']); + unicode_box.put(D, d, d, ['╤', '╪', '╪', '╬']); + unicode_box.put(D, d, S, ['╤', '╪', '╪', '╬']); + unicode_box.put(D, d, D, ['╤', '╪', '╪', '╬']); + unicode_box.put(D, S, n, ['╒', '╞', '╞', '╠']); + unicode_box.put(D, S, d, ['╤', '╪', '╪', '╬']); + unicode_box.put(D, S, S, ['╤', '╪', '╪', '╬']); + unicode_box.put(D, S, D, ['╤', '╪', '╪', '╬']); + unicode_box.put(D, D, n, ['╔', '╠', '╠', '╠']); + unicode_box.put(D, D, d, ['╠', '╬', '╬', '╬']); + unicode_box.put(D, D, S, ['╠', '╬', '╬', '╬']); + unicode_box.put(D, D, D, ['╦', '╬', '╬', '╬']); + unicode_box +}); + +impl TextDriver { + fn new(file: File) -> TextDriver { + let width = 80; + Self { + file: BufWriter::new(file), + emphasis: false, + width, + min_hbreak: 20, + box_chars: &*ASCII_BOX, + n_objects: 0, + params: Params { + size: Coord2::new(width, usize::MAX), + font_size: EnumMap::from_fn(|_| 1), + line_widths: EnumMap::from_fn(|stroke| if stroke == Stroke::None { 0 } else { 1 }), + px_size: None, + min_break: EnumMap::default(), + supports_margins: false, + rtl: false, + printing: true, + can_adjust_break: false, + can_scale: false, + }, + lines: Vec::new(), + } + } + + fn output_table(&mut self, table: &PivotTable) { + for layer_indexes in table.layers(true) { + let pager = Pager::new(todo!(), table, Some(layer_indexes.as_slice())); + while pager.has_next() { + if self.n_objects > 0 { + writeln!(&mut self.file).unwrap(); + } + self.n_objects += 1; + + pager.draw_next(usize::MAX, self); + } + } + } +} + +impl Driver for TextDriver { + fn name(&self) -> Cow<'static, str> { + Cow::from("text") + } + + fn write(&mut self, item: &Arc) { + match &item.details { + Details::Chart => todo!(), + Details::Image => todo!(), + Details::Group(vec) => todo!(), + Details::Message(diagnostic) => todo!(), + Details::PageBreak => todo!(), + Details::Table(pivot_table) => self.output_table(pivot_table), + Details::Text(text) => todo!(), + } + } +} + +impl Draw for TextDriver { + fn draw_line(&mut self, bb: Rect2, styles: EnumMap) { + todo!() + } + + fn draw_cell( + &mut self, + draw_cell: &DrawCell, + alternate_row: bool, + bb: &Rect2, + valign_offset: usize, + spill: EnumMap, + clip: &Rect2, + ) { + todo!() + } + + fn scale(&mut self, factor: f64) { + unreachable!() + } +} + +impl Device for TextDriver { + fn params(&self) -> &Params { + todo!() + } + + fn measure_cell_width(&self, cell: &CellInner) -> [usize; 2] { + todo!() + } + + fn measure_cell_height(&self, cell: &CellInner, width: usize) -> usize { + todo!() + } + + fn adjust_break(&self, cell: &Content, size: Coord2) -> usize { + todo!() + } +} diff --git a/rust/pspp/src/output/text_line.rs b/rust/pspp/src/output/text_line.rs new file mode 100644 index 0000000000..79f1164c68 --- /dev/null +++ b/rust/pspp/src/output/text_line.rs @@ -0,0 +1,178 @@ +use std::ops::Range; + +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; + +/// A line of text, encoded in UTF-8, with support functions that properly +/// handle double-width characters and backspaces. +/// +/// Designed to make appending text fast, and access and modification of other +/// column positions possible. +#[derive(Default)] +pub struct TextLine { + /// Content. + string: String, + + /// Display width, in character positions. + width: usize, +} + +impl TextLine { + pub fn new() -> Self { + Self::default() + } + + pub fn clear(&mut self) { + self.string.clear(); + self.width = 0; + } + + /// Changes the width of this line to `x` columns. If `x` is longer than + /// the current width, extends the line with spaces. If `x` is shorter than + /// the current width, removes trailing characters. + pub fn resize(&mut self, x: usize) { + if x > self.width { + self.string.extend((self.width..x).map(|_| ' ')); + } else if x < self.width { + let pos = self.find_pos(x); + self.string.truncate(pos.offsets.start); + if x > pos.offsets.start { + self.string.extend((pos.offsets.start..x).map(|_| '?')); + } + } + self.width = x; + } + + pub fn put(&mut self, x0: usize, s: &str) { + let w = s.width(); + let x1 = x0 + w; + if w == 0 { + // Nothing to do. + } else if x0 >= self.width { + // The common case: adding new characters at the end of a line. + self.string.extend((self.width..x0).map(|_| ' ')); + self.string.push_str(s); + self.width = x1; + } else if x1 >= self.width { + let p0 = self.find_pos(x0); + + // If a double-width character occupies both `x0 - 1` and `x0`, then + // replace its first character width by `?`. + self.string.truncate(p0.offsets.start); + self.string.extend((p0.columns.start..x0).map(|_| '?')); + self.string.push_str(s); + self.width = x1; + } else { + let span = self.find_span(x0, x1); + if span.columns.start < x0 || span.columns.end > x1 { + let tail = self.string.split_off(span.offsets.end); + self.string.truncate(span.offsets.start); + self.string.extend((span.offsets.start..x0).map(|_| '?')); + self.string.push_str(s); + self.string.extend((x1..span.offsets.end).map(|_| '?')); + self.string.push_str(&tail); + } else { + self.string.replace_range(span.offsets, s); + } + } + } + + 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 { + Position { + columns: p0.columns.start..p1.columns.start, + offsets: p0.offsets.start..p1.offsets.start, + } + } else { + Position { + columns: p0.columns.start..p1.columns.end, + offsets: p0.offsets.start..p1.offsets.end, + } + } + } + + fn find_pos(&self, target_x: usize) -> Position { + let mut x = 0; + let mut ofs = 0; + let mut widths = Widths::new(&self.string); + while let Some(w) = widths.next() { + if x + w > target_x { + return Position { + columns: x..x + w, + offsets: ofs..widths.offset(), + }; + } + ofs = widths.offset(); + x += w; + } + + // This can happen if there are non-printable characters in a line. + Position { + columns: x..x, + offsets: ofs..ofs, + } + } +} + +/// Position of one or more characters within a [TextLine]. +struct Position { + /// 0-based display columns. + columns: Range, + + /// Byte offests. + offsets: Range, +} + +struct Widths<'a> { + s: &'a str, + base: &'a str, +} + +impl<'a> Widths<'a> { + fn new(s: &'a str) -> Self { + Self { s, base: s } + } + + fn as_str(&self) -> &str { + self.s + } + + fn offset(&self) -> usize { + self.base.len() - self.s.len() + } +} + +impl<'a> Iterator for Widths<'a> { + type Item = usize; + + fn next(&mut self) -> Option { + let mut iter = self.s.char_indices(); + let (_, mut c) = iter.next()?; + if iter.as_str().starts_with('\x08') { + iter.next(); + c = match iter.next() { + Some((_, c)) => c, + _ => { + self.s = iter.as_str(); + return Some(0); + } + }; + } + + let w = c.width().unwrap_or_default(); + if w == 0 { + self.s = iter.as_str(); + return Some(0); + } + + while let Some((index, c)) = iter.next() { + if c.width().is_some_and(|width| width > 0) { + self.s = &self.s[index..]; + return Some(w); + } + } + self.s = iter.as_str(); + Some(w) + } +}