--- /dev/null
+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<W> {
+ 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<W> HtmlRenderer<W>
+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, "<table")?;
+ if let Some(notes) = &pivot_table.notes {
+ write!(&mut self.writer, r#" title="{}""#, Escape::new(notes))?;
+ }
+ writeln!(&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, "<thead>")?;
+ for cell in layers.cells() {
+ writeln!(&mut self.writer, "<tr>")?;
+ 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, "</tr>")?;
+ }
+ writeln!(&mut self.writer, "</thead>")?;
+ }
+
+ writeln!(&mut self.writer, "<tbody>")?;
+ for y in 0..output.body.n.y() {
+ writeln!(&mut self.writer, "<tr>")?;
+ 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, "</tr>")?;
+ }
+ writeln!(&mut self.writer, "</tbody>")?;
+
+ if output.caption.is_some() || output.footnotes.is_some() {
+ writeln!(&mut self.writer, "<tfoot>")?;
+ if let Some(caption) = &output.caption {}
+ writeln!(&mut self.writer, "</tfoot>")?;
+ }
+ }
+ 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("<br>")
+ )?;
+
+ if suffixes.has_subscripts() {
+ write!(&mut self.writer, "<sub>")?;
+ 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("<br>")
+ )?;
+ }
+ write!(&mut self.writer, "</sub>")?;
+ }
+
+ if suffixes.has_footnotes() {
+ write!(&mut self.writer, "<sup>")?;
+ 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("<br>")
+ )?;
+ }
+ write!(&mut self.writer, "</sup>")?;
+ }
+
+ write!(&mut self.writer, "</{tag}>")
+ }
+
+ 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<W>(mut writer: W) -> std::io::Result<()>
+where
+ W: Write,
+{
+ write!(
+ &mut writer,
+ r#"<!doctype html>
+<html>
+<head>
+<title>PSPP Output</title>
+<meta name="generator" content="PSPP {}"
+<meta http-equiv="content-type" content="text/html; charset=utf8">
+{}
+"#,
+ Escape::new(env!("CARGO_PKG_VERSION")),
+ HEADER_CSS,
+ )?;
+ Ok(())
+}
+
+const HEADER_CSS: &str = r#"<style>
+body {
+ background: white;
+ color: black;
+ padding: 0em 12em 0em 3em;
+ margin: 0
+}
+body>p {
+ margin: 0pt 0pt 0pt 0em
+}
+body>p + p {
+ text-indent: 1.5em;
+}
+h1 {
+ font-size: 150%;
+ margin-left: -1.33em
+}
+h2 {
+ font-size: 125%;
+ font-weight: bold;
+ margin-left: -.8em
+}
+h3 {
+ font-size: 100%;
+ font-weight: bold;
+ margin-left: -.5em }
+h4 {
+ font-size: 100%;
+ margin-left: 0em
+}
+h1, h2, h3, h4, h5, h6 {
+ font-family: sans-serif;
+ color: blue
+}
+html {
+ margin: 0
+}
+code {
+ font-family: sans-serif
+}
+table {
+ border-collapse: collapse;
+ margin-bottom: 1em
+}
+caption {
+ text-align: left
+}
+th { font-weight: normal }
+a:link {
+ color: #1f00ff;
+}
+a:visited {
+ color: #9900dd;
+}
+a:active {
+ color: red;
+}
+</style>
+</head>
+<body>
+"#;
+
+impl<W> Driver for HtmlRenderer<W>
+where
+ W: Write,
+{
+ fn name(&self) -> Cow<'static, str> {
+ Cow::from("html")
+ }
+
+ fn write(&mut self, item: &Arc<Item>) {
+ 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(())
+ }
+}
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,
}
}
- 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())
}
}
}
}
+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")]
self.subscripts.iter().map(String::as_str)
}
+ pub fn has_subscripts(&self) -> bool {
+ !self.subscripts.is_empty()
+ }
+
pub fn footnotes(&self) -> impl Iterator<Item = DisplayMarker<'_>> {
self.footnotes
.iter()
.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: &[],
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.
///
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<Footnote>],
- 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