From: Ben Pfaff Date: Thu, 1 May 2025 00:41:39 +0000 (-0700) Subject: html driver compiles X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3c4427e4ae87a9805d2c6945b39577df15542450;p=pspp html driver compiles --- diff --git a/rust/pspp/src/output/cairo/fsm.rs b/rust/pspp/src/output/cairo/fsm.rs index 7355f6e9a2..e35b7c95b1 100644 --- a/rust/pspp/src/output/cairo/fsm.rs +++ b/rust/pspp/src/output/cairo/fsm.rs @@ -12,7 +12,8 @@ use smallvec::{smallvec, SmallVec}; use crate::output::cairo::{horz_align_to_pango, px_to_xr, xr_to_pt}; use crate::output::pivot::{Axis2, BorderStyle, Coord2, FontStyle, HorzAlign, Rect2, Stroke}; -use crate::output::render::{Device, DrawCell, Extreme, Pager, Params}; +use crate::output::render::{Device, Extreme, Pager, Params}; +use crate::output::table::DrawCell; use crate::output::{pivot::Color, table::Content}; use crate::output::{Details, Item}; diff --git a/rust/pspp/src/output/html.rs b/rust/pspp/src/output/html.rs new file mode 100644 index 0000000000..e1fda2a654 --- /dev/null +++ b/rust/pspp/src/output/html.rs @@ -0,0 +1,431 @@ +use std::{ + borrow::Cow, + fmt::{Display, Write as _}, + io::Write, + sync::Arc, +}; + +use smallstr::SmallString; + +use crate::output::{ + driver::Driver, + pivot::{Axis2, BorderStyle, Color, Coord2, HorzAlign, PivotTable, Rect2, Stroke, VertAlign}, + table::{DrawCell, Table}, + Details, Item, +}; + +pub struct HtmlRenderer { + writer: W, + fg: Color, + bg: Color, +} + +impl Stroke { + fn as_css(&self) -> Option<&'static str> { + match self { + Stroke::None => None, + Stroke::Solid => Some("solid"), + Stroke::Dashed => Some("dashed"), + Stroke::Thick => Some("thick solid"), + Stroke::Thin => Some("thin solid"), + Stroke::Double => Some("double"), + } + } +} + +impl HtmlRenderer +where + W: Write, +{ + pub fn new(mut writer: W) -> Self { + let _ = put_header(&mut writer); + Self { + fg: Color::BLACK, + bg: Color::WHITE, + writer, + } + } + + fn render(&mut self, pivot_table: &PivotTable) -> std::io::Result<()> { + for (index, layer_indexes) in pivot_table.layers(true).enumerate() { + let output = pivot_table.output(&layer_indexes, false); + write!(&mut self.writer, "")?; + + if let Some(title) = output.title { + let cell = title.get(Coord2::new(0, 0)); + self.put_cell( + DrawCell::new(cell.inner(), &title), + Rect2::new(0..1, 0..1), + false, + "caption", + None, + )?; + } + + if let Some(layers) = output.layers { + writeln!(&mut self.writer, "")?; + for cell in layers.cells() { + writeln!(&mut self.writer, "")?; + self.put_cell( + DrawCell::new(cell.inner(), &layers), + Rect2::new(0..output.body.n[Axis2::X], 0..1), + false, + "td", + None, + )?; + writeln!(&mut self.writer, "")?; + } + writeln!(&mut self.writer, "")?; + } + + writeln!(&mut self.writer, "")?; + for y in 0..output.body.n.y() { + writeln!(&mut self.writer, "")?; + for x in output.body.iter_x(y) { + let cell = output.body.get(Coord2::new(x, y)); + if cell.is_top_left() { + let is_header = x < output.body.h[Axis2::X] || y < output.body.h[Axis2::Y]; + let tag = if is_header { "th" } else { "td" }; + let alternate_row = y + .checked_sub(output.body.h[Axis2::Y]) + .is_some_and(|y| y % 2 == 1); + self.put_cell( + DrawCell::new(cell.inner(), &output.body), + cell.rect(), + alternate_row, + tag, + Some(&output.body), + ); + } + } + writeln!(&mut self.writer, "")?; + } + writeln!(&mut self.writer, "")?; + + if output.caption.is_some() || output.footnotes.is_some() { + writeln!(&mut self.writer, "")?; + if let Some(caption) = &output.caption {} + writeln!(&mut self.writer, "")?; + } + } + Ok(()) + } + + fn put_cell( + &mut self, + cell: DrawCell<'_>, + rect: Rect2, + alternate_row: bool, + tag: &str, + table: Option<&Table>, + ) -> std::io::Result<()> { + write!(&mut self.writer, "<{tag}")?; + let (body, suffixes) = cell.display().split_suffixes(); + + let mut style = String::new(); + let horz_align = match cell.horz_align(&body) { + HorzAlign::Right | HorzAlign::Decimal { .. } => Some("right"), + HorzAlign::Center => Some("center"), + HorzAlign::Left => None, + }; + if let Some(horz_align) = horz_align { + write!(&mut style, "text-align: {horz_align}; ").unwrap(); + } + + if cell.rotate { + write!(&mut style, "writing-mode: sideways-lr; ").unwrap(); + } + + let vert_align = match cell.style.cell_style.vert_align { + VertAlign::Top => None, + VertAlign::Middle => Some("middle"), + VertAlign::Bottom => Some("bottom"), + }; + if let Some(vert_align) = vert_align { + write!(&mut style, "vertical-align: {vert_align}; ").unwrap(); + } + let bg = cell.style.font_style.bg[alternate_row as usize]; + if bg != Color::WHITE { + write!(&mut style, "background: {}; ", bg.display_css()).unwrap(); + } + + let fg = cell.style.font_style.fg[alternate_row as usize]; + if fg != Color::BLACK { + write!(&mut style, "color: {}; ", fg.display_css()).unwrap(); + } + + if !cell.style.font_style.font.is_empty() { + write!( + &mut style, + r#"font-family: "{}""#, + Escape::new(&cell.style.font_style.font) + ); + } + + if cell.style.font_style.bold { + write!(&mut style, "font-weight: bold; ").unwrap(); + } + if cell.style.font_style.italic { + write!(&mut style, "font-style: italic; ").unwrap(); + } + if cell.style.font_style.underline { + write!(&mut style, "text-decoration: underline; ").unwrap(); + } + if cell.style.font_style.size != 0 { + write!(&mut style, "font-size: {}pt; ", cell.style.font_style.size).unwrap(); + } + + if let Some(table) = table { + Self::put_border(&mut style, table.get_rule(Axis2::Y, rect.top_left()), "top"); + Self::put_border( + &mut style, + table.get_rule(Axis2::X, rect.top_left()), + "left", + ); + if rect[Axis2::X].end == table.n[Axis2::X] { + Self::put_border( + &mut style, + table.get_rule( + Axis2::X, + Coord2::new(rect[Axis2::X].end, rect[Axis2::Y].start), + ), + "right", + ); + } + if rect[Axis2::Y].end == table.n[Axis2::Y] { + Self::put_border( + &mut style, + table.get_rule( + Axis2::Y, + Coord2::new(rect[Axis2::X].start, rect[Axis2::Y].end), + ), + "bottom", + ); + } + } + + if !style.is_empty() { + write!( + &mut self.writer, + r#" style="{}""#, + style.trim_end_matches("; ") + )?; + } + + let col_span = rect[Axis2::X].len(); + if col_span > 1 { + write!(&mut self.writer, r#" colspan="{col_span}"#); + } + + let row_span = rect[Axis2::Y].len(); + if row_span > 1 { + write!(&mut self.writer, r#" rowspan="{row_span}"#); + } + + write!(&mut self.writer, ">")?; + + let mut text = SmallString::<[u8; 64]>::new(); + write!(&mut text, "{body}").unwrap(); + write!( + &mut self.writer, + "{}", + Escape::new(&text).with_newline("
") + )?; + + if suffixes.has_subscripts() { + write!(&mut self.writer, "")?; + for (index, subscript) in suffixes.subscripts().enumerate() { + if index > 0 { + write!(&mut self.writer, ",")?; + } + write!( + &mut self.writer, + "{}", + Escape::new(subscript) + .with_space(" ") + .with_newline("
") + )?; + } + write!(&mut self.writer, "
")?; + } + + if suffixes.has_footnotes() { + write!(&mut self.writer, "")?; + for (index, footnote) in suffixes.footnotes().enumerate() { + if index > 0 { + write!(&mut self.writer, ",")?; + } + let mut marker = SmallString::<[u8; 8]>::new(); + write!(&mut marker, "{footnote}").unwrap(); + write!( + &mut self.writer, + "{}", + Escape::new(&marker) + .with_space(" ") + .with_newline("
") + )?; + } + write!(&mut self.writer, "
")?; + } + + write!(&mut self.writer, "") + } + + fn put_border(dst: &mut String, style: BorderStyle, border_name: &str) { + if let Some(css_style) = style.stroke.as_css() { + write!(dst, "border-{border_name}: {css_style}").unwrap(); + if style.color != Color::BLACK { + write!(dst, " {}", style.color.display_css()).unwrap(); + } + write!(dst, "; ").unwrap(); + } + } +} + +fn put_header(mut writer: W) -> std::io::Result<()> +where + W: Write, +{ + write!( + &mut writer, + r#" + + +PSPP Output + +{} +"#, + Escape::new(env!("CARGO_PKG_VERSION")), + HEADER_CSS, + )?; + Ok(()) +} + +const HEADER_CSS: &str = r#" + + +"#; + +impl Driver for HtmlRenderer +where + W: Write, +{ + fn name(&self) -> Cow<'static, str> { + Cow::from("html") + } + + fn write(&mut self, item: &Arc) { + match &item.details { + Details::Chart => todo!(), + Details::Image => todo!(), + Details::Group(_) => todo!(), + Details::Message(_diagnostic) => todo!(), + Details::PageBreak => (), + Details::Table(pivot_table) => { + self.render(pivot_table).unwrap(); // XXX + } + Details::Text(_text) => todo!(), + } + } +} + +struct Escape<'a> { + string: &'a str, + space: &'static str, + newline: &'static str, +} + +impl<'a> Escape<'a> { + fn new(string: &'a str) -> Self { + Self { + string, + space: " ", + newline: "\n", + } + } + fn with_space(self, space: &'static str) -> Self { + Self { space, ..self } + } + fn with_newline(self, newline: &'static str) -> Self { + Self { newline, ..self } + } +} + +impl Display for Escape<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for c in self.string.chars() { + match c { + '\n' => f.write_str(self.newline)?, + ' ' => f.write_str(self.space)?, + '&' => f.write_str("&")?, + '<' => f.write_str("<")?, + '>' => f.write_str(">")?, + '"' => f.write_str(""")?, + _ => f.write_char(c)?, + } + } + Ok(()) + } +} diff --git a/rust/pspp/src/output/mod.rs b/rust/pspp/src/output/mod.rs index 7f23f8aeb4..7f23a50cb9 100644 --- a/rust/pspp/src/output/mod.rs +++ b/rust/pspp/src/output/mod.rs @@ -10,6 +10,7 @@ use self::pivot::Value; pub mod cairo; pub mod csv; pub mod driver; +pub mod html; pub mod page; pub mod pivot; pub mod render; diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index c00f2cb574..352bc9081c 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -872,7 +872,7 @@ impl Color { pub const BLUE: Color = Color::new(0, 0, 255); pub const TRANSPARENT: Color = Color::new(0, 0, 0).with_alpha(0); - const fn new(r: u8, g: u8, b: u8) -> Self { + pub const fn new(r: u8, g: u8, b: u8) -> Self { Self { alpha: 255, r, @@ -881,18 +881,18 @@ impl Color { } } - const fn with_alpha(self, alpha: u8) -> Self { + pub const fn with_alpha(self, alpha: u8) -> Self { Self { alpha, ..self } } + + pub fn display_css(&self) -> DisplayCss { + DisplayCss(*self) + } } impl Debug for Color { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Color { alpha, r, g, b } = *self; - match alpha { - 255 => write!(f, "#{r:02x}{g:02x}{b:02x}"), - _ => write!(f, "rgb({r}, {g}, {b}, {:.2})", alpha as f64 / 255.0), - } + write!(f, "{}", self.display_css()) } } @@ -951,6 +951,18 @@ impl<'de> Deserialize<'de> for Color { } } +pub struct DisplayCss(Color); + +impl Display for DisplayCss { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Color { alpha, r, g, b } = self.0; + match alpha { + 255 => write!(f, "#{r:02x}{g:02x}{b:02x}"), + _ => write!(f, "rgb({r}, {g}, {b}, {:.2})", alpha as f64 / 255.0), + } + } +} + #[derive(Copy, Clone, Debug, Deserialize)] pub struct BorderStyle { #[serde(rename = "@borderStyleType")] @@ -1695,6 +1707,10 @@ impl<'a> DisplayValue<'a> { self.subscripts.iter().map(String::as_str) } + pub fn has_subscripts(&self) -> bool { + !self.subscripts.is_empty() + } + pub fn footnotes(&self) -> impl Iterator> { self.footnotes .iter() @@ -1702,6 +1718,10 @@ impl<'a> DisplayValue<'a> { .map(|f| f.display_marker(self.options)) } + pub fn has_footnotes(&self) -> bool { + !self.footnotes().next().is_some() + } + pub fn without_suffixes(self) -> Self { Self { subscripts: &[], diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index 7d9e80cb62..6e72ddb74d 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -9,13 +9,11 @@ use itertools::interleave; use num::Integer; use smallvec::SmallVec; -use crate::output::pivot::{DisplayValue, HorzAlign, VertAlign}; +use crate::output::pivot::VertAlign; +use crate::output::table::DrawCell; -use super::pivot::{ - AreaStyle, Axis2, BorderStyle, Coord2, Footnote, Look, PivotTable, Rect2, Stroke, ValueInner, - ValueOptions, -}; -use super::table::{CellInner, Content, Table}; +use super::pivot::{Axis2, BorderStyle, Coord2, Look, PivotTable, Rect2, Stroke}; +use super::table::{Content, Table}; /// Parameters for rendering a table_item to a device. /// @@ -153,53 +151,6 @@ pub trait Device { fn scale(&mut self, factor: f64); } -pub struct DrawCell<'a> { - pub rotate: bool, - pub inner: &'a ValueInner, - pub style: &'a AreaStyle, - pub subscripts: &'a [String], - pub footnotes: &'a [Arc], - pub value_options: &'a ValueOptions, -} - -impl<'a> DrawCell<'a> { - fn new(inner: &'a CellInner, table: &'a Table) -> Self { - let default_area_style = &table.areas[inner.area]; - let (style, subscripts, footnotes) = if let Some(styling) = &inner.value.styling { - ( - styling.style.as_ref().unwrap_or(default_area_style), - styling.subscripts.as_slice(), - styling.footnotes.as_slice(), - ) - } else { - (default_area_style, [].as_slice(), [].as_slice()) - }; - Self { - rotate: inner.rotate, - inner: &inner.value.inner, - style, - subscripts, - footnotes, - value_options: &table.value_options, - } - } - - pub fn display(&self) -> DisplayValue<'a> { - self.inner - .display(self.value_options) - .with_font_style(&self.style.font_style) - .with_subscripts(self.subscripts) - .with_footnotes(self.footnotes) - } - - pub fn horz_align(&self, display: &DisplayValue) -> HorzAlign { - self.style - .cell_style - .horz_align - .unwrap_or_else(|| HorzAlign::for_mixed(display.var_type())) - } -} - /// A layout for rendering a specific table on a specific device. /// /// May represent the layout of an entire table presented to [Pager::new], or a diff --git a/rust/pspp/src/output/table.rs b/rust/pspp/src/output/table.rs index 797e72fad3..02441b4ab1 100644 --- a/rust/pspp/src/output/table.rs +++ b/rust/pspp/src/output/table.rs @@ -15,7 +15,7 @@ use std::{ops::Range, sync::Arc}; use enum_map::{enum_map, EnumMap}; use ndarray::{Array, Array2}; -use crate::output::pivot::Coord2; +use crate::output::pivot::{Coord2, DisplayValue, Footnote, HorzAlign, ValueInner}; use super::pivot::{ Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, Rect2, Value, ValueOptions, @@ -361,3 +361,50 @@ impl<'a> Iterator for Cells<'a> { Some(this) } } + +pub struct DrawCell<'a> { + pub rotate: bool, + pub inner: &'a ValueInner, + pub style: &'a AreaStyle, + pub subscripts: &'a [String], + pub footnotes: &'a [Arc], + pub value_options: &'a ValueOptions, +} + +impl<'a> DrawCell<'a> { + pub fn new(inner: &'a CellInner, table: &'a Table) -> Self { + let default_area_style = &table.areas[inner.area]; + let (style, subscripts, footnotes) = if let Some(styling) = &inner.value.styling { + ( + styling.style.as_ref().unwrap_or(default_area_style), + styling.subscripts.as_slice(), + styling.footnotes.as_slice(), + ) + } else { + (default_area_style, [].as_slice(), [].as_slice()) + }; + Self { + rotate: inner.rotate, + inner: &inner.value.inner, + style, + subscripts, + footnotes, + value_options: &table.value_options, + } + } + + pub fn display(&self) -> DisplayValue<'a> { + self.inner + .display(self.value_options) + .with_font_style(&self.style.font_style) + .with_subscripts(self.subscripts) + .with_footnotes(self.footnotes) + } + + pub fn horz_align(&self, display: &DisplayValue) -> HorzAlign { + self.style + .cell_style + .horz_align + .unwrap_or_else(|| HorzAlign::for_mixed(display.var_type())) + } +} diff --git a/rust/pspp/src/output/text.rs b/rust/pspp/src/output/text.rs index 3a2df088b3..098214c675 100644 --- a/rust/pspp/src/output/text.rs +++ b/rust/pspp/src/output/text.rs @@ -11,12 +11,12 @@ use enum_map::{enum_map, Enum, EnumMap}; use unicode_linebreak::{linebreaks, BreakOpportunity}; use unicode_width::UnicodeWidthStr; -use crate::output::{render::Extreme, text_line::Emphasis}; +use crate::output::{render::Extreme, table::DrawCell, text_line::Emphasis}; use super::{ driver::Driver, pivot::{Axis2, BorderStyle, Coord2, HorzAlign, PivotTable, Rect2, Stroke}, - render::{Device, DrawCell, Pager, Params}, + render::{Device, Pager, Params}, table::Content, text_line::{clip_text, TextLine}, Details, Item, @@ -319,11 +319,8 @@ impl TextRenderer { } let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice())); - dbg!(); while pager.has_next(self) { - dbg!(); pager.draw_next(self, usize::MAX); - dbg!(); output.append(&mut self.lines); } }