diff options
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/helper/table.rs')
| -rw-r--r-- | crates/shirabe-external-packages/src/symfony/console/helper/table.rs | 1443 |
1 files changed, 0 insertions, 1443 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/table.rs b/crates/shirabe-external-packages/src/symfony/console/helper/table.rs deleted file mode 100644 index 650dfffa..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/table.rs +++ /dev/null @@ -1,1443 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/Table.php - -use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; -use crate::symfony::console::exception::runtime_exception::RuntimeException; -use crate::symfony::console::formatter::output_formatter::OutputFormatter; -use crate::symfony::console::formatter::wrappable_output_formatter_interface::WrappableOutputFormatterInterface; -use crate::symfony::console::helper::helper::Helper; -use crate::symfony::console::helper::table_cell::{TableCell, TableCellOption}; -use crate::symfony::console::helper::table_cell_style::TableCellStyle; -use crate::symfony::console::helper::table_rows::TableRows; -use crate::symfony::console::helper::table_separator::TableSeparator; -use crate::symfony::console::helper::table_style::TableStyle; -use crate::symfony::console::output::console_section_output::ConsoleSectionOutput; -use crate::symfony::console::output::output_interface::OutputInterface; -use indexmap::IndexMap; -use shirabe_pcre::preg::Preg; -use shirabe_php_shim::{PhpMixed, php_regex}; - -/// A single cell within a table row. -/// -/// PHP types a cell as `TableCell|string|int|null` (scalars are stringified). Because -/// `TableSeparator extends TableCell`, a separator can in principle appear as a cell, so it has its -/// own variant; `is_table_cell` reports `true` for it, mirroring `instanceof TableCell`. -#[derive(Debug, Clone)] -pub enum Cell { - Null, - Value(String), - Cell(TableCell), - Separator(TableSeparator), -} - -impl Cell { - /// PHP `$cell instanceof TableCell` (true for `TableSeparator`, which extends `TableCell`). - fn is_table_cell(&self) -> bool { - matches!(self, Cell::Cell(_) | Cell::Separator(_)) - } - - /// PHP `$cell instanceof TableSeparator`. - fn is_table_separator(&self) -> bool { - matches!(self, Cell::Separator(_)) - } - - fn colspan(&self) -> i64 { - match self { - Cell::Cell(c) => c.get_colspan(), - Cell::Separator(s) => s.get_colspan(), - _ => 1, - } - } - - fn rowspan(&self) -> i64 { - match self { - Cell::Cell(c) => c.get_rowspan(), - Cell::Separator(s) => s.get_rowspan(), - _ => 1, - } - } - - fn style(&self) -> Option<std::rc::Rc<TableCellStyle>> { - match self { - Cell::Cell(c) => c.get_style(), - Cell::Separator(s) => s.get_style(), - _ => None, - } - } - - /// PHP `(string) $cell`. - fn to_php_string(&self) -> String { - match self { - Cell::Null | Cell::Separator(_) => String::new(), - Cell::Value(s) => s.clone(), - Cell::Cell(c) => c.to_string(), - } - } - - /// PHP truthiness / `!empty($cell)` for a cell value: only "" and "0" (and null) are falsy; - /// any object is truthy. - fn is_truthy(&self) -> bool { - match self { - Cell::Null => false, - Cell::Value(s) => !s.is_empty() && s != "0", - Cell::Cell(_) | Cell::Separator(_) => true, - } - } - - fn is_null(&self) -> bool { - matches!(self, Cell::Null) - } -} - -impl From<String> for Cell { - fn from(value: String) -> Self { - Cell::Value(value) - } -} - -impl From<&str> for Cell { - fn from(value: &str) -> Self { - Cell::Value(value.to_string()) - } -} - -impl From<TableCell> for Cell { - fn from(value: TableCell) -> Self { - Cell::Cell(value) - } -} - -/// Bridges PHP-typed cell values built elsewhere in the codebase (where rows are still assembled as -/// `PhpMixed`) onto the typed cell. A cell is a scalar or null; collections stringify as PHP would. -impl From<PhpMixed> for Cell { - fn from(value: PhpMixed) -> Self { - match value { - PhpMixed::Null => Cell::Null, - other => Cell::Value(shirabe_php_shim::to_string(&other)), - } - } -} - -/// A table row. -/// -/// PHP types a row as `array<int, Cell>|TableSeparator`. The header/body boundary that PHP creates -/// as a fresh `TableSeparator` and recognizes by object identity (`$divider === $row`) is modeled -/// here by its own [`Row::HeaderDivider`] variant rather than by reference identity: it counts as a -/// separator for layout (`is_table_separator`), but unlike a user-supplied [`Row::Separator`] it -/// renders no separator line — it only flips the header/first-row state during rendering. -#[derive(Debug, Clone)] -pub enum Row { - HeaderDivider, - Separator(TableSeparator), - Cells(Vec<Cell>), -} - -impl Row { - /// PHP `$row instanceof TableSeparator` (also true for the internal header/body divider). - fn is_table_separator(&self) -> bool { - matches!(self, Row::HeaderDivider | Row::Separator(_)) - } - - /// PHP `$divider === $row`: identifies the internal header/body boundary marker. - fn is_divider(&self) -> bool { - matches!(self, Row::HeaderDivider) - } - - /// The row's cells, or an empty list for a separator (PHP `foreach` over a separator object, - /// which exposes no public properties, yields nothing). - fn cells(&self) -> Vec<Cell> { - match self { - Row::Cells(cells) => cells.clone(), - _ => Vec::new(), - } - } - - /// PHP `!$row`: an empty cell array is falsy; a separator is an object and thus truthy. - fn is_truthy(&self) -> bool { - match self { - Row::Cells(cells) => !cells.is_empty(), - _ => true, - } - } - - /// PHP `isset($row[$i])`: `false` for a missing or null cell. - fn get_cell_isset(&self, index: i64) -> Option<Cell> { - match self { - Row::Cells(cells) => match cells.get(index as usize) { - Some(Cell::Null) | None => None, - Some(cell) => Some(cell.clone()), - }, - _ => None, - } - } -} - -impl From<Vec<Cell>> for Row { - fn from(cells: Vec<Cell>) -> Self { - Row::Cells(cells) - } -} - -/// Bridges PHP-typed rows built elsewhere (still assembled as `PhpMixed`) onto the typed row. -impl From<PhpMixed> for Row { - fn from(value: PhpMixed) -> Self { - match value { - PhpMixed::List(items) => Row::Cells(items.into_iter().map(Cell::from).collect()), - PhpMixed::Array(map) => Row::Cells(map.into_values().map(Cell::from).collect()), - PhpMixed::Null => Row::Cells(Vec::new()), - other => Row::Cells(vec![Cell::from(other)]), - } - } -} - -/// A table or column style argument. PHP types it as `string|TableStyle`: either the name of a -/// registered style or a `TableStyle` instance. -#[derive(Debug, Clone)] -pub enum StyleName { - Name(String), - Style(TableStyle), -} - -impl From<&str> for StyleName { - fn from(name: &str) -> Self { - StyleName::Name(name.to_string()) - } -} - -impl From<String> for StyleName { - fn from(name: String) -> Self { - StyleName::Name(name) - } -} - -impl From<TableStyle> for StyleName { - fn from(style: TableStyle) -> Self { - StyleName::Style(style) - } -} - -/// PHP `$row[$index] = $value`: assigns at the integer key, growing the (dense) row with null -/// cells to reach `index` when necessary. -fn set_cell(row: &mut Vec<Cell>, index: i64, value: Cell) { - let index = index as usize; - while row.len() <= index { - row.push(Cell::Null); - } - row[index] = value; -} - -/// Provides helpers to display a table. -#[derive(Debug)] -pub struct Table { - header_title: Option<String>, - footer_title: Option<String>, - - /// Table headers. - headers: Vec<Row>, - - /// Table rows. - rows: Vec<Row>, - horizontal: bool, - - /// Column widths cache. - effective_column_widths: IndexMap<i64, i64>, - - /// Number of columns cache. - number_of_columns: Option<i64>, - - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - - style: TableStyle, - - column_styles: IndexMap<i64, TableStyle>, - - /// User set column widths. - column_widths: IndexMap<i64, i64>, - column_max_widths: IndexMap<i64, i64>, - - rendered: bool, -} - -const SEPARATOR_TOP: i64 = 0; -const SEPARATOR_TOP_BOTTOM: i64 = 1; -const SEPARATOR_MID: i64 = 2; -const SEPARATOR_BOTTOM: i64 = 3; -const BORDER_OUTSIDE: i64 = 0; -const BORDER_INSIDE: i64 = 1; - -/// Global style definitions, lazily initialized. -/// -/// In PHP this is `private static $styles`. Here it is a process-global cache. -fn styles() -> &'static std::sync::Mutex<Option<IndexMap<String, TableStyle>>> { - static STYLES: std::sync::Mutex<Option<IndexMap<String, TableStyle>>> = - std::sync::Mutex::new(None); - &STYLES -} - -impl Table { - pub fn new(output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) -> Self { - let mut styles_guard = styles().lock().unwrap(); - if styles_guard.is_none() { - *styles_guard = Some(Self::init_styles()); - } - drop(styles_guard); - - let mut this = Self { - header_title: None, - footer_title: None, - headers: Vec::new(), - rows: Vec::new(), - horizontal: false, - effective_column_widths: IndexMap::new(), - number_of_columns: None, - output, - style: TableStyle::default(), - column_styles: IndexMap::new(), - column_widths: IndexMap::new(), - column_max_widths: IndexMap::new(), - rendered: false, - }; - - this.set_style(StyleName::from("default")); - - this - } - - /// Sets a style definition. - pub fn set_style_definition(name: String, style: TableStyle) { - let mut styles_guard = styles().lock().unwrap(); - if styles_guard.is_none() { - *styles_guard = Some(Self::init_styles()); - } - - styles_guard.as_mut().unwrap().insert(name, style); - } - - /// Gets a style definition by name. - pub fn get_style_definition( - name: String, - ) -> anyhow::Result<Result<TableStyle, InvalidArgumentException>> { - let mut styles_guard = styles().lock().unwrap(); - if styles_guard.is_none() { - *styles_guard = Some(Self::init_styles()); - } - - if let Some(style) = styles_guard.as_ref().unwrap().get(&name) { - return Ok(Ok(style.clone())); - } - - Ok(Err(InvalidArgumentException::new(format!( - "Style \"{}\" is not defined.", - name - )))) - } - - /// Sets table style. - /// - /// `$name` is the style name or a TableStyle instance. - pub fn set_style( - &mut self, - name: StyleName, - ) -> anyhow::Result<Result<&mut Self, InvalidArgumentException>> { - match self.resolve_style(name)? { - Ok(style) => { - self.style = style; - Ok(Ok(self)) - } - Err(e) => Ok(Err(e)), - } - } - - /// Gets the current table style. - pub fn get_style(&self) -> &TableStyle { - &self.style - } - - /// Sets table column style. - /// - /// `$name` is the style name or a TableStyle instance. - pub fn set_column_style( - &mut self, - column_index: i64, - name: StyleName, - ) -> anyhow::Result<Result<&mut Self, InvalidArgumentException>> { - match self.resolve_style(name)? { - Ok(style) => { - self.column_styles.insert(column_index, style); - Ok(Ok(self)) - } - Err(e) => Ok(Err(e)), - } - } - - /// Gets the current style for a column. - /// - /// If style was not set, it returns the global table style. - pub fn get_column_style(&self, column_index: i64) -> &TableStyle { - self.column_styles - .get(&column_index) - .unwrap_or_else(|| self.get_style()) - } - - /// Sets the minimum width of a column. - pub fn set_column_width(&mut self, column_index: i64, width: i64) -> &mut Self { - self.column_widths.insert(column_index, width); - - self - } - - /// Sets the minimum width of all columns. - pub fn set_column_widths(&mut self, widths: Vec<i64>) -> &mut Self { - self.column_widths = IndexMap::new(); - for (index, width) in widths.into_iter().enumerate() { - self.set_column_width(index as i64, width); - } - - self - } - - /// Sets the maximum width of a column. - /// - /// Any cell within this column which contents exceeds the specified width will be wrapped into - /// multiple lines, while formatted strings are preserved. - pub fn set_column_max_width(&mut self, column_index: i64, width: i64) -> &mut Self { - if !Self::formatter_is_wrappable(&self.output) { - // PHP throws \LogicException here. This represents a programming error: the caller must - // supply a WrappableOutputFormatterInterface before setting a maximum column width. - panic!( - "Setting a maximum column width is only supported when using a \"{}\" formatter, got \"{}\".", - "Symfony\\Component\\Console\\Formatter\\WrappableOutputFormatterInterface", - shirabe_php_shim::get_debug_type(&PhpMixed::from(())) - ); - } - - self.column_max_widths.insert(column_index, width); - - self - } - - pub fn set_headers(&mut self, headers: Vec<Cell>) -> &mut Self { - // PHP wraps a flat list of cells into a single header row. (Multi-row headers, which PHP - // also accepts, are not used in this codebase and are not modeled by the typed API.) - self.headers = if headers.is_empty() { - Vec::new() - } else { - vec![Row::Cells(headers)] - }; - - self - } - - pub fn set_rows(&mut self, rows: Vec<Row>) -> &mut Self { - self.rows = Vec::new(); - - self.add_rows(rows) - } - - pub fn add_rows(&mut self, rows: Vec<Row>) -> &mut Self { - for row in rows { - self.add_row(row); - } - - self - } - - pub fn add_row(&mut self, row: Row) -> &mut Self { - // PHP `array_values($row)` reindexing is the identity on a positional cell vector, and the - // "row must be an array or a TableSeparator" check is now guaranteed by the type. - self.rows.push(row); - - self - } - - /// Adds a row to the table, and re-renders the table. - pub fn append_row(&mut self, row: Row) -> anyhow::Result<Result<&mut Self, RuntimeException>> { - if !Self::output_is_console_section(&self.output) { - return Ok(Err(RuntimeException::new(format!( - "Output should be an instance of \"{}\" when calling \"{}\".", - "Symfony\\Component\\Console\\Output\\ConsoleSectionOutput", - "Symfony\\Component\\Console\\Helper\\Table::appendRow", - )))); - } - - if self.rendered { - // TODO(phase-c): downcast output to ConsoleSectionOutput to call clear(). - let _ = ConsoleSectionOutput::clear; - let row_count = self.calculate_row_count(); - let _ = row_count; - todo!() - } - - self.add_row(row); - self.render(); - - Ok(Ok(self)) - } - - pub fn set_row(&mut self, column: i64, row: Vec<Cell>) -> &mut Self { - // PHP indexes $this->rows by arbitrary key; sparse assignment over a positional Vec is not - // modeled and has no callers. - let _ = (column, row); - // TODO(phase-c): sparse `$this->rows[$column] = $row` over a positional row vector. - todo!() - } - - pub fn set_header_title(&mut self, title: Option<String>) -> &mut Self { - self.header_title = title; - - self - } - - pub fn set_footer_title(&mut self, title: Option<String>) -> &mut Self { - self.footer_title = title; - - self - } - - pub fn set_horizontal(&mut self, horizontal: bool) -> &mut Self { - self.horizontal = horizontal; - - self - } - - /// Renders table to output. - pub fn render(&mut self) { - let rows: Vec<Row> = if self.horizontal { - let mut horizontal_rows: IndexMap<i64, Vec<Cell>> = IndexMap::new(); - let header0 = self.headers.first().map(|h| h.cells()).unwrap_or_default(); - for (i, header) in header0.into_iter().enumerate() { - let i = i as i64; - horizontal_rows.insert(i, vec![header]); - for row in &self.rows { - if row.is_table_separator() { - continue; - } - if let Some(cell) = row.get_cell_isset(i) { - let entry = horizontal_rows.get_mut(&i).unwrap(); - entry.push(cell); - } else { - let first = horizontal_rows.get(&i).unwrap().first().cloned(); - let is_title_noop = match first { - Some(ref c) if c.is_table_cell() => c.colspan() >= 2, - _ => false, - }; - if is_title_noop { - // Noop, there is a "title" - } else { - let entry = horizontal_rows.get_mut(&i).unwrap(); - entry.push(Cell::Null); - } - } - } - } - horizontal_rows.into_values().map(Row::Cells).collect() - } else { - let mut merged = self.headers.clone(); - merged.push(Row::HeaderDivider); - merged.extend(self.rows.clone()); - merged - }; - - self.calculate_number_of_columns(&rows); - - let row_groups = self.build_table_rows(rows); - self.calculate_columns_width(&row_groups); - - let mut is_header = !self.horizontal; - let mut is_first_row = self.horizontal; - let mut has_title = - self.header_title.is_some() && !self.header_title.as_deref().unwrap_or("").is_empty(); - - for row_group in &row_groups { - let mut is_header_separator_rendered = false; - - for row in row_group { - if row.is_divider() { - is_header = false; - is_first_row = true; - - continue; - } - - if row.is_table_separator() { - self.render_row_separator(SEPARATOR_MID, None, None); - - continue; - } - - if !row.is_truthy() { - continue; - } - - if is_header && !is_header_separator_rendered { - self.render_row_separator( - if is_header { - SEPARATOR_TOP - } else { - SEPARATOR_TOP_BOTTOM - }, - if has_title { - self.header_title.clone() - } else { - None - }, - if has_title { - Some(self.style.get_header_title_format()) - } else { - None - }, - ); - has_title = false; - is_header_separator_rendered = true; - } - - if is_first_row { - self.render_row_separator( - if is_header { - SEPARATOR_TOP - } else { - SEPARATOR_TOP_BOTTOM - }, - if has_title { - self.header_title.clone() - } else { - None - }, - if has_title { - Some(self.style.get_header_title_format()) - } else { - None - }, - ); - is_first_row = false; - has_title = false; - } - - if self.horizontal { - self.render_row( - row.cells(), - self.style.get_cell_row_format(), - Some(self.style.get_cell_header_format()), - ); - } else { - self.render_row( - row.cells(), - if is_header { - self.style.get_cell_header_format() - } else { - self.style.get_cell_row_format() - }, - None, - ); - } - } - } - self.render_row_separator( - SEPARATOR_BOTTOM, - self.footer_title.clone(), - Some(self.style.get_footer_title_format()), - ); - - self.cleanup(); - self.rendered = true; - } - - /// Renders horizontal header separator. - fn render_row_separator( - &self, - r#type: i64, - title: Option<String>, - title_format: Option<String>, - ) { - let count = match self.number_of_columns { - Some(0) | None => return, - Some(c) => c, - }; - - let borders = self.style.get_border_chars(); - if borders[0].is_empty() - && borders[2].is_empty() - && self.style.get_crossing_char().is_empty() - { - return; - } - - let crossings = self.style.get_crossing_chars(); - let (horizontal, left_char, mid_char, right_char) = if SEPARATOR_MID == r#type { - ( - borders[2].clone(), - crossings[8].clone(), - crossings[0].clone(), - crossings[4].clone(), - ) - } else if SEPARATOR_TOP == r#type { - ( - borders[0].clone(), - crossings[1].clone(), - crossings[2].clone(), - crossings[3].clone(), - ) - } else if SEPARATOR_TOP_BOTTOM == r#type { - ( - borders[0].clone(), - crossings[9].clone(), - crossings[10].clone(), - crossings[11].clone(), - ) - } else { - ( - borders[0].clone(), - crossings[7].clone(), - crossings[6].clone(), - crossings[5].clone(), - ) - }; - - let mut markup = left_char; - let mut column = 0; - while column < count { - markup.push_str(&shirabe_php_shim::str_repeat( - &horizontal, - self.effective_column_widths[&column] as usize, - )); - markup.push_str(if column == count - 1 { - &right_char - } else { - &mid_char - }); - column += 1; - } - - if let Some(title) = title { - let title_format = title_format.unwrap(); - let formatted_title = - shirabe_php_shim::sprintf(&title_format, &[PhpMixed::from(title.clone())]); - let mut formatted_title = formatted_title; - let mut title_length = Helper::width(&self.remove_decoration(&formatted_title)); - let markup_length = Helper::width(&markup); - let limit = markup_length - 4; - if title_length > limit { - title_length = limit; - let format_length = Helper::width(&self.remove_decoration( - &shirabe_php_shim::sprintf(&title_format, &[PhpMixed::from("")]), - )); - formatted_title = shirabe_php_shim::sprintf( - &title_format, - &[PhpMixed::from(format!( - "{}...", - Helper::substr(&title, 0, Some(limit - format_length - 3)) - ))], - ); - } - - let title_start = (markup_length - title_length) / 2; - if shirabe_php_shim::mb_detect_encoding(&markup, None, true).is_none() { - markup = shirabe_php_shim::substr_replace( - &markup, - &formatted_title, - title_start as usize, - title_length as usize, - ); - } else { - markup = format!( - "{}{}{}", - shirabe_php_shim::mb_substr(&markup, 0, Some(title_start), None), - formatted_title, - shirabe_php_shim::mb_substr(&markup, title_start + title_length, None, None), - ); - } - } - - self.output.borrow().writeln( - &[shirabe_php_shim::sprintf( - &self.style.get_border_format(), - &[PhpMixed::from(markup)], - )], - crate::symfony::console::output::output_interface::OUTPUT_NORMAL, - ); - } - - /// Renders vertical column separator. - fn render_column_separator(&self, r#type: i64) -> String { - let borders = self.style.get_border_chars(); - - shirabe_php_shim::sprintf( - &self.style.get_border_format(), - &[PhpMixed::from(if BORDER_OUTSIDE == r#type { - borders[1].clone() - } else { - borders[3].clone() - })], - ) - } - - /// Renders table row. - fn render_row(&self, row: Vec<Cell>, cell_format: String, first_cell_format: Option<String>) { - let mut row_content = self.render_column_separator(BORDER_OUTSIDE); - let columns = self.get_row_columns(&row); - let last = columns.len() as i64 - 1; - for (i, column) in columns.into_iter().enumerate() { - let i = i as i64; - if first_cell_format.is_some() && 0 == i { - row_content.push_str(&self.render_cell( - &row, - column, - first_cell_format.clone().unwrap(), - )); - } else { - row_content.push_str(&self.render_cell(&row, column, cell_format.clone())); - } - row_content.push_str(&self.render_column_separator(if last == i { - BORDER_OUTSIDE - } else { - BORDER_INSIDE - })); - } - self.output.borrow().writeln( - &[row_content], - crate::symfony::console::output::output_interface::OUTPUT_NORMAL, - ); - } - - /// Renders table cell with padding. - fn render_cell(&self, row: &[Cell], column: i64, cell_format: String) -> String { - let cell = row - .get(column as usize) - .cloned() - .unwrap_or(Cell::Value(String::new())); - let mut width = self.effective_column_widths[&column]; - if cell.is_table_cell() && cell.colspan() > 1 { - // add the width of the following columns(numbers of colspan). - for next_column in (column + 1)..=(column + cell.colspan() - 1) { - width += - self.get_column_separator_width() + self.effective_column_widths[&next_column]; - } - } - - // str_pad won't work properly with multi-byte strings, we need to fix the padding - let cell_str = cell.to_php_string(); - if let Some(encoding) = shirabe_php_shim::mb_detect_encoding(&cell_str, None, true) { - width += shirabe_php_shim::strlen(&cell_str) - - shirabe_php_shim::mb_strwidth(&cell_str, Some(&encoding)); - } - - let style = self.get_column_style(column); - - if cell.is_table_separator() { - return shirabe_php_shim::sprintf( - &style.get_border_format(), - &[PhpMixed::from(shirabe_php_shim::str_repeat( - &style.get_border_chars()[2], - width as usize, - ))], - ); - } - - width += Helper::length(&cell_str) - Helper::length(&self.remove_decoration(&cell_str)); - let mut content = shirabe_php_shim::sprintf( - &style.get_cell_row_content_format(), - &[PhpMixed::from(cell_str.clone())], - ); - - let mut cell_format = cell_format; - let mut pad_type = style.get_pad_type(); - if cell.is_table_cell() && cell.style().is_some() { - let is_not_styled_by_tag = !Preg::is_match( - php_regex!("/^<(\\w+|(\\w+=[\\w,]+;?)*)>.+<\\/(\\w+|(\\w+=\\w+;?)*)?>$/"), - &cell_str, - ); - if is_not_styled_by_tag { - let cell_style = cell.style().unwrap(); - match cell_style.get_cell_format() { - Some(fmt) => cell_format = fmt, - None => { - let tag = shirabe_php_shim::http_build_query_mixed( - &cell_style.get_tag_options(), - "", - ";", - ); - cell_format = format!("<{}>%s</>", tag); - } - } - - if shirabe_php_shim::strstr(&content, "</>").is_some() { - content = shirabe_php_shim::str_replace("</>", "", &content); - width -= 3; - } - if shirabe_php_shim::strstr(&content, "<fg=default;bg=default>").is_some() { - content = - shirabe_php_shim::str_replace("<fg=default;bg=default>", "", &content); - width -= shirabe_php_shim::strlen("<fg=default;bg=default>"); - } - } - - pad_type = cell.style().unwrap().get_pad_by_align(); - } - - shirabe_php_shim::sprintf( - &cell_format, - &[PhpMixed::from(shirabe_php_shim::str_pad( - &content, - width as usize, - &style.get_padding_char(), - pad_type, - ))], - ) - } - - /// Calculate number of columns for this table. - fn calculate_number_of_columns(&mut self, rows: &[Row]) { - let mut columns = vec![0i64]; - for row in rows { - if row.is_table_separator() { - continue; - } - - columns.push(self.get_number_of_columns(&row.cells())); - } - - self.number_of_columns = Some(*columns.iter().max().unwrap()); - } - - fn build_table_rows(&mut self, rows: Vec<Row>) -> TableRows { - let mut rows = rows; - let mut unmerged_rows: IndexMap<i64, IndexMap<i64, Vec<Cell>>> = IndexMap::new(); - let mut row_key = 0i64; - while row_key < rows.len() as i64 { - rows = self.fill_next_rows(rows, row_key); - - // Remove any new line breaks and replace it with a new line - let current = rows[row_key as usize].cells(); - for (column, cell) in current.iter().enumerate() { - let column = column as i64; - let mut cell = cell.clone(); - let colspan = if cell.is_table_cell() { - cell.colspan() - } else { - 1 - }; - - if self.column_max_widths.contains_key(&column) - && Helper::width(&self.remove_decoration(&cell.to_php_string())) - > self.column_max_widths[&column] - { - let wrapped = self.format_and_wrap( - &cell.to_php_string(), - self.column_max_widths[&column] * colspan, - ); - cell = Cell::Value(wrapped); - } - let cell_str = cell.to_php_string(); - if shirabe_php_shim::strstr(&cell_str, "\n").is_none() { - continue; - } - let eol = if shirabe_php_shim::str_contains(&cell_str, "\r\n") { - "\r\n" - } else { - "\n" - }; - let escaped = shirabe_php_shim::implode( - eol, - &shirabe_php_shim::explode(eol, &cell_str) - .iter() - .map(|line| OutputFormatter::escape_trailing_backslash(line)) - .collect::<Vec<_>>(), - ); - cell = if cell.is_table_cell() { - Cell::Cell(TableCell::new2( - &escaped, - Self::table_cell_options_colspan(cell.colspan()), - )) - } else { - Cell::Value(escaped.clone()) - }; - let lines = shirabe_php_shim::explode( - eol, - &shirabe_php_shim::str_replace( - eol, - &format!("<fg=default;bg=default></>{}", eol), - &cell.to_php_string(), - ), - ); - for (line_key, line) in lines.into_iter().enumerate() { - let line_key = line_key as i64; - let mut line = Cell::Value(line); - if colspan > 1 { - line = Cell::Cell(TableCell::new2( - &line.to_php_string(), - Self::table_cell_options_colspan(colspan), - )); - } - if 0 == line_key { - let mut r = rows[row_key as usize].cells(); - set_cell(&mut r, column, line); - rows[row_key as usize] = Row::Cells(r); - } else { - if !unmerged_rows.contains_key(&row_key) - || !unmerged_rows[&row_key].contains_key(&line_key) - { - let copied = self.copy_row(&rows, row_key); - unmerged_rows - .entry(row_key) - .or_default() - .insert(line_key, copied); - } - let target = unmerged_rows - .get_mut(&row_key) - .unwrap() - .get_mut(&line_key) - .unwrap(); - set_cell(target, column, line); - } - } - } - row_key += 1; - } - - // PHP returns a TableRows wrapping a generator that lazily yields row groups. - // The generator borrows $this to call fillCells(). Here the row groups are - // precomputed eagerly to preserve behavior, then handed to TableRows. - let mut row_groups: Vec<Vec<Row>> = Vec::new(); - for (row_key, row) in rows.into_iter().enumerate() { - let row_key = row_key as i64; - let mut row_group: Vec<Row> = vec![if row.is_table_separator() { - row - } else { - Row::Cells(self.fill_cells(row.cells())) - }]; - - if let Some(extra) = unmerged_rows.get(&row_key) { - for r in extra.values() { - row_group.push(Row::Cells(self.fill_cells(r.clone()))); - } - } - row_groups.push(row_group); - } - - TableRows::from_row_groups(row_groups) - } - - fn calculate_row_count(&mut self) -> i64 { - let mut merged = self.headers.clone(); - merged.push(Row::Separator(TableSeparator::new())); - merged.extend(self.rows.clone()); - let mut number_of_rows = self.build_table_rows(merged).into_row_groups().len() as i64; - - if !self.headers.is_empty() { - number_of_rows += 1; // Add row for header separator - } - - if !self.rows.is_empty() { - number_of_rows += 1; // Add row for footer separator - } - - number_of_rows - } - - /// fill rows that contains rowspan > 1. - fn fill_next_rows(&self, rows: Vec<Row>, line: i64) -> Vec<Row> { - let mut rows = rows; - let mut unmerged_rows: IndexMap<i64, IndexMap<i64, Cell>> = IndexMap::new(); - let current = rows[line as usize].cells(); - for (column, cell) in current.iter().enumerate() { - let column = column as i64; - let cell = cell.clone(); - // PHP validates here that a cell is null, a TableCell, a scalar, or a __toString object. - // The `Cell` type makes every variant valid, so the InvalidArgumentException is dead. - if cell.is_table_cell() && cell.rowspan() > 1 { - let mut nb_lines = cell.rowspan() - 1; - let cell_str = cell.to_php_string(); - let mut lines = vec![cell.clone()]; - if shirabe_php_shim::strstr(&cell_str, "\n").is_some() { - let eol = if shirabe_php_shim::str_contains(&cell_str, "\r\n") { - "\r\n" - } else { - "\n" - }; - let exploded = shirabe_php_shim::explode( - eol, - &shirabe_php_shim::str_replace( - eol, - &format!("<fg=default;bg=default>{}</>", eol), - &cell_str, - ), - ); - lines = exploded.into_iter().map(Cell::Value).collect(); - nb_lines = if (lines.len() as i64) > nb_lines { - shirabe_php_shim::substr_count(&cell_str, eol) - } else { - nb_lines - }; - - let mut r = rows[line as usize].cells(); - set_cell( - &mut r, - column, - Cell::Cell(TableCell::new2( - &lines[0].to_php_string(), - Self::table_cell_options_colspan_style(cell.colspan(), cell.style()), - )), - ); - rows[line as usize] = Row::Cells(r); - lines.remove(0); - } - - // create a two dimensional array (rowspan x colspan) - for k in (line + 1)..=(line + nb_lines) { - unmerged_rows.entry(k).or_default(); - } - for unmerged_row_key in unmerged_rows.keys().cloned().collect::<Vec<_>>() { - let idx = unmerged_row_key - line; - let value = lines - .get(idx as usize) - .cloned() - .unwrap_or(Cell::Value(String::new())); - unmerged_rows.get_mut(&unmerged_row_key).unwrap().insert( - column, - Cell::Cell(TableCell::new2( - &value.to_php_string(), - Self::table_cell_options_colspan_style(cell.colspan(), cell.style()), - )), - ); - if nb_lines == unmerged_row_key - line { - break; - } - } - } - } - - for (unmerged_row_key, unmerged_row) in unmerged_rows.clone() { - // we need to know if $unmergedRow will be merged or inserted into $rows - let fits = (unmerged_row_key as usize) < rows.len() - && matches!(rows[unmerged_row_key as usize], Row::Cells(_)) - && (self.get_number_of_columns(&rows[unmerged_row_key as usize].cells()) - + self.get_number_of_columns( - &unmerged_rows[&unmerged_row_key] - .values() - .cloned() - .collect::<Vec<_>>(), - ) - <= self.number_of_columns.unwrap()); - if fits { - let mut target = rows[unmerged_row_key as usize].cells(); - for (cell_key, cell) in unmerged_row { - // insert cell into row at cellKey position - shirabe_php_shim::array_splice(&mut target, cell_key, Some(0), vec![cell]); - } - rows[unmerged_row_key as usize] = Row::Cells(target); - } else { - let mut row = self.copy_row(&rows, unmerged_row_key - 1); - for (column, cell) in &unmerged_row { - if cell.is_truthy() { - set_cell(&mut row, *column, unmerged_row[column].clone()); - } - } - shirabe_php_shim::array_splice( - &mut rows, - unmerged_row_key, - Some(0), - vec![Row::Cells(row)], - ); - } - } - - rows - } - - /// fill cells for a row that contains colspan > 1. - fn fill_cells(&self, row: Vec<Cell>) -> Vec<Cell> { - let mut new_row: Vec<Cell> = Vec::new(); - - for (column, cell) in row.iter().enumerate() { - let column = column as i64; - new_row.push(cell.clone()); - if cell.is_table_cell() && cell.colspan() > 1 { - for _position in (column + 1)..=(column + cell.colspan() - 1) { - // insert empty value at column position - new_row.push(Cell::Value(String::new())); - } - } - } - - if new_row.is_empty() { row } else { new_row } - } - - fn copy_row(&self, rows: &[Row], line: i64) -> Vec<Cell> { - let mut row = rows[line as usize].cells(); - for cell in &mut row { - let cell_value = cell.clone(); - *cell = Cell::Value(String::new()); - if cell_value.is_table_cell() { - *cell = Cell::Cell(TableCell::new2( - "", - Self::table_cell_options_colspan(cell_value.colspan()), - )); - } - } - - row - } - - /// Gets number of columns by row. - fn get_number_of_columns(&self, row: &[Cell]) -> i64 { - let mut columns = row.len() as i64; - for column in row { - columns += if column.is_table_cell() { - column.colspan() - 1 - } else { - 0 - }; - } - - columns - } - - /// Gets list of columns for the given row. - fn get_row_columns(&self, row: &[Cell]) -> Vec<i64> { - let mut columns: Vec<i64> = (0..self.number_of_columns.unwrap()).collect(); - for (cell_key, cell) in row.iter().enumerate() { - let cell_key = cell_key as i64; - if cell.is_table_cell() && cell.colspan() > 1 { - // exclude grouped columns. - let excluded: Vec<i64> = - ((cell_key + 1)..=(cell_key + cell.colspan() - 1)).collect(); - columns.retain(|c| !excluded.contains(c)); - } - } - - columns - } - - /// Calculates columns widths. - fn calculate_columns_width(&mut self, groups: &TableRows) { - let mut column = 0; - while column < self.number_of_columns.unwrap() { - let mut lengths: Vec<i64> = Vec::new(); - for group in groups { - for row in group { - if row.is_table_separator() { - continue; - } - - let mut row_arr = row.cells(); - for i in 0..row_arr.len() { - let cell = row_arr[i].clone(); - if cell.is_table_cell() { - let text_content = self.remove_decoration(&cell.to_php_string()); - let text_length = Helper::width(&text_content); - if text_length > 0 { - let content_columns = shirabe_php_shim::mb_str_split( - &text_content, - (text_length as f64 / cell.colspan() as f64).ceil() as i64, - ); - for (position, content) in content_columns.into_iter().enumerate() { - set_cell( - &mut row_arr, - i as i64 + position as i64, - Cell::Value(content), - ); - } - } - } - } - - lengths.push(self.get_cell_width(&row_arr, column)); - } - } - - self.effective_column_widths.insert( - column, - *lengths.iter().max().unwrap() - + Helper::width(&self.style.get_cell_row_content_format()) - - 2, - ); - column += 1; - } - } - - fn get_column_separator_width(&self) -> i64 { - Helper::width(&shirabe_php_shim::sprintf( - &self.style.get_border_format(), - &[PhpMixed::from(self.style.get_border_chars()[3].clone())], - )) - } - - fn get_cell_width(&self, row: &[Cell], column: i64) -> i64 { - let mut cell_width = 0; - - if let Some(cell) = row.get(column as usize) { - cell_width = Helper::width(&self.remove_decoration(&cell.to_php_string())); - } - - let column_width = *self.column_widths.get(&column).unwrap_or(&0); - cell_width = cell_width.max(column_width); - - if let Some(max) = self.column_max_widths.get(&column) { - (*max).min(cell_width) - } else { - cell_width - } - } - - /// Called after rendering to cleanup cache data. - fn cleanup(&mut self) { - self.effective_column_widths = IndexMap::new(); - self.number_of_columns = None; - } - - fn init_styles() -> IndexMap<String, TableStyle> { - let mut borderless = TableStyle::default(); - borderless - .set_horizontal_border_chars("=".to_string(), None) - .set_vertical_border_chars(" ".to_string(), None) - .set_default_crossing_char(" ".to_string()); - - let mut compact = TableStyle::default(); - compact - .set_horizontal_border_chars("".to_string(), None) - .set_vertical_border_chars("".to_string(), None) - .set_default_crossing_char("".to_string()) - .set_cell_row_content_format("%s ".to_string()); - - let mut style_guide = TableStyle::default(); - style_guide - .set_horizontal_border_chars("-".to_string(), None) - .set_vertical_border_chars(" ".to_string(), None) - .set_default_crossing_char(" ".to_string()) - .set_cell_header_format("%s".to_string()); - - let mut r#box = TableStyle::default(); - r#box - .set_horizontal_border_chars("─".to_string(), None) - .set_vertical_border_chars("│".to_string(), None) - .set_crossing_chars( - "┼".to_string(), - "┌".to_string(), - "┬".to_string(), - "┐".to_string(), - "┤".to_string(), - "┘".to_string(), - "┴".to_string(), - "└".to_string(), - "├".to_string(), - None, - None, - None, - ); - - let mut box_double = TableStyle::default(); - box_double - .set_horizontal_border_chars("═".to_string(), Some("─".to_string())) - .set_vertical_border_chars("║".to_string(), Some("│".to_string())) - .set_crossing_chars( - "┼".to_string(), - "╔".to_string(), - "╤".to_string(), - "╗".to_string(), - "╢".to_string(), - "╝".to_string(), - "╧".to_string(), - "╚".to_string(), - "╟".to_string(), - Some("╠".to_string()), - Some("╪".to_string()), - Some("╣".to_string()), - ); - - let mut result: IndexMap<String, TableStyle> = IndexMap::new(); - result.insert("default".to_string(), TableStyle::default()); - result.insert("borderless".to_string(), borderless); - result.insert("compact".to_string(), compact); - result.insert("symfony-style-guide".to_string(), style_guide); - result.insert("box".to_string(), r#box); - result.insert("box-double".to_string(), box_double); - - result - } - - fn resolve_style( - &self, - name: StyleName, - ) -> anyhow::Result<Result<TableStyle, InvalidArgumentException>> { - let name = match name { - StyleName::Style(style) => return Ok(Ok(style)), - StyleName::Name(name) => name, - }; - - let styles_guard = styles().lock().unwrap(); - if let Some(style) = styles_guard.as_ref().and_then(|s| s.get(&name)) { - return Ok(Ok(style.clone())); - } - - Ok(Err(InvalidArgumentException::new(format!( - "Style \"{}\" is not defined.", - name - )))) - } - - fn formatter_is_wrappable( - _output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> bool { - // PHP: $this->output->getFormatter() instanceof WrappableOutputFormatterInterface. - // The sole OutputFormatterInterface implementor in this port is OutputFormatter, which - // implements WrappableOutputFormatterInterface, so the instanceof check always holds. - true - } - - /// `setColumnMaxWidth` guarantees the formatter is a `WrappableOutputFormatterInterface`, and - /// `OutputFormatter` is the sole implementor in this port, so `instanceof` reduces to this - /// downcast. - fn format_and_wrap(&self, string: &str, width: i64) -> String { - let formatter = self.output.borrow().get_formatter(); - let mut formatter = formatter.borrow_mut(); - let formatter = formatter - .as_any_mut() - .downcast_mut::<OutputFormatter>() - .expect("formatter must be a WrappableOutputFormatterInterface"); - formatter - .format_and_wrap(Some(string), width) - .unwrap() - .unwrap_or_default() - } - - /// PHP `Helper::removeDecoration($this->output->getFormatter(), $string)`. - fn remove_decoration(&self, string: &str) -> String { - let formatter = self.output.borrow().get_formatter(); - let mut formatter = formatter.borrow_mut(); - Helper::remove_decoration(&mut *formatter, string) - } - - fn output_is_console_section( - output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> bool { - // PHP: $this->output instanceof ConsoleSectionOutput - let borrowed = output.borrow(); - (*borrowed) - .as_any() - .downcast_ref::<ConsoleSectionOutput>() - .is_some() - } - - fn table_cell_options_colspan(colspan: i64) -> IndexMap<String, TableCellOption> { - // PHP: ['colspan' => $colspan] - let mut options = IndexMap::new(); - options.insert("colspan".to_string(), TableCellOption::Int(colspan)); - options - } - - fn table_cell_options_colspan_style( - colspan: i64, - style: Option<std::rc::Rc<TableCellStyle>>, - ) -> IndexMap<String, TableCellOption> { - // PHP: ['colspan' => $colspan, 'style' => $style] - let mut options = IndexMap::new(); - options.insert("colspan".to_string(), TableCellOption::Int(colspan)); - options.insert( - "style".to_string(), - match style { - Some(style) => TableCellOption::Style(style), - None => TableCellOption::Null, - }, - ); - options - } -} |
