aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
diff options
context:
space:
mode:
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.rs1333
1 files changed, 1321 insertions, 12 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
index 4ff5a5e..6f52458 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
@@ -1,47 +1,1356 @@
-use crate::symfony::console::output::OutputInterface;
+use crate::composer::pcre::preg::Preg;
+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_php_shim::PhpMixed;
+use std::cell::RefCell;
+use std::rc::Rc;
+/// Provides helpers to display a table.
#[derive(Debug)]
-pub struct Table;
+pub struct Table {
+ header_title: Option<String>,
+ footer_title: Option<String>,
+
+ /// Table headers.
+ headers: Vec<PhpMixed>,
+
+ /// Table rows.
+ rows: Vec<PhpMixed>,
+ horizontal: bool,
+
+ /// Column widths cache.
+ effective_column_widths: IndexMap<i64, i64>,
+
+ /// Number of columns cache.
+ number_of_columns: Option<i64>,
+
+ output: Rc<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 {
+ pub fn new(output: Rc<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(PhpMixed::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) {
+ // TODO(phase-b): TableStyle is not Clone; sharing semantics need resolving.
+ todo!()
+ }
+
+ Ok(Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "Style \"%s\" is not defined.",
+ &[PhpMixed::from(name)],
+ ),
+ code: 0,
+ },
+ )))
+ }
+
+ /// Sets table style.
+ ///
+ /// `$name` is the style name or a TableStyle instance.
+ pub fn set_style(
+ &mut self,
+ name: PhpMixed,
+ ) -> 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: PhpMixed,
+ ) -> 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<PhpMixed>) -> &mut Self {
+ // PHP: $headers = array_values($headers). A row is already modeled as a positional Vec,
+ // so reindexing is the identity here.
+ let mut headers = headers;
+ if !headers.is_empty() && !shirabe_php_shim::is_array(&headers[0]) {
+ headers = vec![Self::from_row_vec(headers)];
+ }
+
+ self.headers = headers;
+
+ self
+ }
+
+ pub fn set_rows(&mut self, rows: Vec<PhpMixed>) -> &mut Self {
+ self.rows = Vec::new();
+
+ self.add_rows(rows)
+ }
+
+ pub fn add_rows(&mut self, rows: Vec<PhpMixed>) -> &mut Self {
+ for row in rows {
+ self.add_row(row);
+ }
+
+ self
+ }
+
+ pub fn add_row(
+ &mut self,
+ row: PhpMixed,
+ ) -> anyhow::Result<Result<&mut Self, InvalidArgumentException>> {
+ if shirabe_php_shim::instance_of::<TableSeparator>(&row) {
+ self.rows.push(row);
+
+ return Ok(Ok(self));
+ }
+
+ if !shirabe_php_shim::is_array(&row) {
+ return Ok(Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: "A row must be an array or a TableSeparator instance.".to_string(),
+ code: 0,
+ },
+ )));
+ }
+
+ // PHP: $this->rows[] = array_values($row). The row is modeled as a positional Vec.
+ self.rows.push(Self::from_row_vec(Self::to_row_vec(row)));
+
+ Ok(Ok(self))
+ }
+
+ /// Adds a row to the table, and re-renders the table.
+ pub fn append_row(
+ &mut self,
+ row: PhpMixed,
+ ) -> anyhow::Result<Result<&mut Self, RuntimeException>> {
+ if !Self::output_is_console_section(&self.output) {
+ return Ok(Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "Output should be an instance of \"%s\" when calling \"%s\".",
+ &[
+ PhpMixed::from("Symfony\\Component\\Console\\Output\\ConsoleSectionOutput"),
+ PhpMixed::from("Symfony\\Component\\Console\\Helper\\Table::appendRow"),
+ ],
+ ),
+ code: 0,
+ })));
+ }
+
+ if self.rendered {
+ // TODO(phase-b): 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)?.ok();
+ self.render();
+
+ Ok(Ok(self))
+ }
+
+ pub fn set_row(&mut self, column: i64, row: Vec<PhpMixed>) -> &mut Self {
+ // PHP indexes $this->rows by arbitrary key; here we follow the integer-keyed case.
+ let _ = (column, row);
todo!()
}
- pub fn set_headers(&mut self, _headers: Vec<PhpMixed>) -> &mut Self {
+ 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 divider = TableSeparator::new();
+ let rows: Vec<PhpMixed>;
+ if self.horizontal {
+ let mut horizontal_rows: IndexMap<i64, Vec<PhpMixed>> = IndexMap::new();
+ let header0 = self
+ .headers
+ .first()
+ .map(|h| Self::to_row_vec(h.clone()))
+ .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 shirabe_php_shim::instance_of::<TableSeparator>(row) {
+ continue;
+ }
+ if let Some(cell) = Self::row_get(row, 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 shirabe_php_shim::instance_of::<TableCell>(c) => {
+ Self::cell_colspan(c) >= 2
+ }
+ _ => false,
+ };
+ if is_title_noop {
+ // Noop, there is a "title"
+ } else {
+ let entry = horizontal_rows.get_mut(&i).unwrap();
+ entry.push(PhpMixed::from(()));
+ }
+ }
+ }
+ }
+ rows = horizontal_rows
+ .into_values()
+ .map(Self::from_row_vec)
+ .collect();
+ } else {
+ let mut merged = self.headers.clone();
+ merged.push(Self::table_separator_to_mixed(divider.clone()));
+ merged.extend(self.rows.clone());
+ rows = 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 Self::is_divider(row, &divider) {
+ is_header = false;
+ is_first_row = true;
+
+ continue;
+ }
+
+ if shirabe_php_shim::instance_of::<TableSeparator>(row) {
+ self.render_row_separator(SEPARATOR_MID, None, None);
+
+ continue;
+ }
+
+ if !shirabe_php_shim::to_bool(row) {
+ 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(
+ Self::to_row_vec(row.clone()),
+ self.style.get_cell_row_format(),
+ Some(self.style.get_cell_header_format()),
+ );
+ } else {
+ self.render_row(
+ Self::to_row_vec(row.clone()),
+ 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 = shirabe_php_shim::intdiv(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<PhpMixed>,
+ 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: &[PhpMixed], column: i64, cell_format: String) -> String {
+ let cell = Self::row_get_index(row, column).unwrap_or_else(|| PhpMixed::from(""));
+ let mut width = self.effective_column_widths[&column];
+ if shirabe_php_shim::instance_of::<TableCell>(&cell) && Self::cell_colspan(&cell) > 1 {
+ // add the width of the following columns(numbers of colspan).
+ for next_column in (column + 1)..=(column + Self::cell_colspan(&cell) - 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 = shirabe_php_shim::to_string(&cell);
+ 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).clone();
+
+ if shirabe_php_shim::instance_of::<TableSeparator>(&cell) {
+ 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 shirabe_php_shim::instance_of::<TableCell>(&cell)
+ && Self::cell_style_is_table_cell_style(&cell)
+ {
+ let is_not_styled_by_tag = !Preg::is_match(
+ "/^<(\\w+|(\\w+=[\\w,]+;?)*)>.+<\\/(\\w+|(\\w+=\\w+;?)*)?>$/",
+ &cell_str,
+ )
+ .unwrap();
+ if is_not_styled_by_tag {
+ let cell_style = Self::cell_get_style(&cell).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 = Self::cell_get_style(&cell).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: &[PhpMixed]) {
+ let mut columns = vec![0i64];
+ for row in rows {
+ if shirabe_php_shim::instance_of::<TableSeparator>(row) {
+ continue;
+ }
+
+ columns.push(self.get_number_of_columns(&Self::to_row_vec(row.clone())));
+ }
+
+ self.number_of_columns = Some(*columns.iter().max().unwrap());
+ }
+
+ fn build_table_rows(&mut self, rows: Vec<PhpMixed>) -> TableRows {
+ let mut rows = rows;
+ let mut unmerged_rows: IndexMap<i64, IndexMap<i64, Vec<PhpMixed>>> = 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 = Self::to_row_vec(rows[row_key as usize].clone());
+ for (column, cell) in current.iter().enumerate() {
+ let column = column as i64;
+ let mut cell = cell.clone();
+ let colspan = if shirabe_php_shim::instance_of::<TableCell>(&cell) {
+ Self::cell_colspan(&cell)
+ } else {
+ 1
+ };
+
+ if self.column_max_widths.contains_key(&column)
+ && Helper::width(&self.remove_decoration(&shirabe_php_shim::to_string(&cell)))
+ > self.column_max_widths[&column]
+ {
+ // TODO(phase-b): formatAndWrap requires a WrappableOutputFormatterInterface;
+ // downcasting dyn OutputFormatterInterface to it needs concrete knowledge.
+ let _ = colspan;
+ let wrapped: Option<String> = todo!();
+ cell = PhpMixed::from(wrapped.unwrap_or_default());
+ }
+ let cell_str = shirabe_php_shim::to_string(&cell);
+ 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 shirabe_php_shim::instance_of::<TableCell>(&cell) {
+ Self::table_cell_to_mixed(TableCell::new2(
+ &escaped,
+ Self::table_cell_options_colspan(Self::cell_colspan(&cell)),
+ ))
+ } else {
+ PhpMixed::from(escaped.clone())
+ };
+ let lines = shirabe_php_shim::explode(
+ eol,
+ &shirabe_php_shim::str_replace(
+ eol,
+ &format!("<fg=default;bg=default></>{}", eol),
+ &shirabe_php_shim::to_string(&cell),
+ ),
+ );
+ for (line_key, line) in lines.into_iter().enumerate() {
+ let line_key = line_key as i64;
+ let mut line = PhpMixed::from(line);
+ if colspan > 1 {
+ line = Self::table_cell_to_mixed(TableCell::new2(
+ &shirabe_php_shim::to_string(&line),
+ Self::table_cell_options_colspan(colspan),
+ ));
+ }
+ if 0 == line_key {
+ let mut r = Self::to_row_vec(rows[row_key as usize].clone());
+ Self::array_set(&mut r, column, line);
+ rows[row_key as usize] = Self::from_row_vec(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();
+ Self::vec_set(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(). In Phase A we precompute the
+ // row groups eagerly to preserve behavior, then hand them to TableRows.
+ let mut row_groups: Vec<Vec<PhpMixed>> = Vec::new();
+ for (row_key, row) in rows.into_iter().enumerate() {
+ let row_key = row_key as i64;
+ let mut row_group: Vec<PhpMixed> =
+ vec![if shirabe_php_shim::instance_of::<TableSeparator>(&row) {
+ row
+ } else {
+ Self::from_row_vec(self.fill_cells(Self::to_row_vec(row)))
+ }];
+
+ if let Some(extra) = unmerged_rows.get(&row_key) {
+ for r in extra.values() {
+ let r = Self::from_row_vec(r.clone());
+ row_group.push(if shirabe_php_shim::instance_of::<TableSeparator>(&r) {
+ r
+ } else {
+ Self::from_row_vec(self.fill_cells(Self::to_row_vec(r)))
+ });
+ }
+ }
+ 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(Self::table_separator_to_mixed(TableSeparator::new()));
+ merged.extend(self.rows.clone());
+ let mut number_of_rows =
+ shirabe_php_shim::iterator_to_array(self.build_table_rows(merged)).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<PhpMixed>, line: i64) -> Vec<PhpMixed> {
+ let mut rows = rows;
+ let mut unmerged_rows: IndexMap<i64, IndexMap<i64, PhpMixed>> = IndexMap::new();
+ let current = Self::to_row_vec(rows[line as usize].clone());
+ for (column, cell) in current.iter().enumerate() {
+ let column = column as i64;
+ let cell = cell.clone();
+ if !shirabe_php_shim::is_null(&cell)
+ && !shirabe_php_shim::instance_of::<TableCell>(&cell)
+ && !shirabe_php_shim::is_scalar(&cell)
+ && !(shirabe_php_shim::is_object(&cell)
+ && shirabe_php_shim::method_exists(&cell, "__toString"))
+ {
+ // PHP throws InvalidArgumentException; the @throws contract makes this a
+ // recoverable error. In Phase A we keep the panic placeholder as fill_next_rows
+ // does not yet return a Result in the call chain.
+ // TODO(phase-b): thread InvalidArgumentException through fill_next_rows.
+ panic!(
+ "A cell must be a TableCell, a scalar or an object implementing \"__toString()\", \"{}\" given.",
+ shirabe_php_shim::get_debug_type(&cell)
+ );
+ }
+ if shirabe_php_shim::instance_of::<TableCell>(&cell) && Self::cell_rowspan(&cell) > 1 {
+ let mut nb_lines = Self::cell_rowspan(&cell) - 1;
+ let cell_str = shirabe_php_shim::to_string(&cell);
+ 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(PhpMixed::from).collect();
+ nb_lines = if (lines.len() as i64) > nb_lines {
+ shirabe_php_shim::substr_count(&cell_str, eol)
+ } else {
+ nb_lines
+ };
+
+ let mut r = Self::to_row_vec(rows[line as usize].clone());
+ Self::array_set(
+ &mut r,
+ column,
+ Self::table_cell_to_mixed(TableCell::new2(
+ &shirabe_php_shim::to_string(&lines[0]),
+ Self::table_cell_options_colspan_style(
+ Self::cell_colspan(&cell),
+ Self::cell_get_style(&cell),
+ ),
+ )),
+ );
+ rows[line as usize] = Self::from_row_vec(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_else(|| PhpMixed::from(""));
+ unmerged_rows.get_mut(&unmerged_row_key).unwrap().insert(
+ column,
+ Self::table_cell_to_mixed(TableCell::new2(
+ &shirabe_php_shim::to_string(&value),
+ Self::table_cell_options_colspan_style(
+ Self::cell_colspan(&cell),
+ Self::cell_get_style(&cell),
+ ),
+ )),
+ );
+ 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()
+ && shirabe_php_shim::is_array(&rows[unmerged_row_key as usize])
+ && (self.get_number_of_columns(&Self::to_row_vec(
+ rows[unmerged_row_key as usize].clone(),
+ )) + self.get_number_of_columns(
+ &unmerged_rows[&unmerged_row_key]
+ .values()
+ .cloned()
+ .collect::<Vec<_>>(),
+ ) <= self.number_of_columns.unwrap());
+ if fits {
+ let mut target = Self::to_row_vec(rows[unmerged_row_key as usize].clone());
+ 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] = Self::from_row_vec(target);
+ } else {
+ let mut row = self.copy_row(&rows, unmerged_row_key - 1);
+ for (column, cell) in &unmerged_row {
+ if shirabe_php_shim::to_bool(cell) {
+ Self::vec_set(&mut row, *column, unmerged_row[column].clone());
+ }
+ }
+ shirabe_php_shim::array_splice_mixed(
+ &mut rows,
+ unmerged_row_key,
+ 0,
+ vec![Self::from_row_vec(row)],
+ );
+ }
+ }
+
+ rows
+ }
+
+ /// fill cells for a row that contains colspan > 1.
+ fn fill_cells(&self, row: Vec<PhpMixed>) -> Vec<PhpMixed> {
+ let mut new_row: Vec<PhpMixed> = Vec::new();
+
+ for (column, cell) in row.iter().enumerate() {
+ let column = column as i64;
+ new_row.push(cell.clone());
+ if shirabe_php_shim::instance_of::<TableCell>(cell) && Self::cell_colspan(cell) > 1 {
+ for _position in (column + 1)..=(column + Self::cell_colspan(cell) - 1) {
+ // insert empty value at column position
+ new_row.push(PhpMixed::from(""));
+ }
+ }
+ }
+
+ if new_row.is_empty() { row } else { new_row }
+ }
+
+ fn copy_row(&self, rows: &[PhpMixed], line: i64) -> Vec<PhpMixed> {
+ let mut row = Self::to_row_vec(rows[line as usize].clone());
+ for cell_key in 0..row.len() {
+ let cell_value = row[cell_key].clone();
+ row[cell_key] = PhpMixed::from("");
+ if shirabe_php_shim::instance_of::<TableCell>(&cell_value) {
+ row[cell_key] = Self::table_cell_to_mixed(TableCell::new2(
+ "",
+ Self::table_cell_options_colspan(Self::cell_colspan(&cell_value)),
+ ));
+ }
+ }
+
+ row
+ }
+
+ /// Gets number of columns by row.
+ fn get_number_of_columns(&self, row: &[PhpMixed]) -> i64 {
+ let mut columns = row.len() as i64;
+ for column in row {
+ columns += if shirabe_php_shim::instance_of::<TableCell>(column) {
+ Self::cell_colspan(column) - 1
+ } else {
+ 0
+ };
+ }
+
+ columns
+ }
+
+ /// Gets list of columns for the given row.
+ fn get_row_columns(&self, row: &[PhpMixed]) -> 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 shirabe_php_shim::instance_of::<TableCell>(cell) && Self::cell_colspan(cell) > 1 {
+ // exclude grouped columns.
+ let excluded: Vec<i64> =
+ ((cell_key + 1)..=(cell_key + Self::cell_colspan(cell) - 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 shirabe_php_shim::instance_of::<TableSeparator>(row) {
+ continue;
+ }
+
+ let mut row_arr = Self::to_row_vec(row.clone());
+ for i in 0..row_arr.len() {
+ let cell = row_arr[i].clone();
+ if shirabe_php_shim::instance_of::<TableCell>(&cell) {
+ let text_content =
+ self.remove_decoration(&shirabe_php_shim::to_string(&cell));
+ let text_length = Helper::width(&text_content);
+ if text_length > 0 {
+ let content_columns = shirabe_php_shim::mb_str_split(
+ &text_content,
+ shirabe_php_shim::ceil(
+ text_length as f64 / Self::cell_colspan(&cell) as f64,
+ ) as i64,
+ );
+ for (position, content) in content_columns.into_iter().enumerate() {
+ Self::vec_set(
+ &mut row_arr,
+ i as i64 + position as i64,
+ PhpMixed::from(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: &[PhpMixed], column: i64) -> i64 {
+ let mut cell_width = 0;
+
+ if let Some(cell) = Self::row_get_index(row, column) {
+ cell_width =
+ Helper::width(&self.remove_decoration(&shirabe_php_shim::to_string(&cell)));
+ }
+
+ 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: PhpMixed,
+ ) -> anyhow::Result<Result<TableStyle, InvalidArgumentException>> {
+ if shirabe_php_shim::instance_of::<TableStyle>(&name) {
+ // TODO(phase-b): extract the owned TableStyle out of PhpMixed.
+ todo!()
+ }
+
+ let name_str = shirabe_php_shim::to_string(&name);
+ let styles_guard = styles().lock().unwrap();
+ if let Some(_style) = styles_guard.as_ref().and_then(|s| s.get(&name_str)) {
+ // TODO(phase-b): TableStyle is not Clone; sharing semantics need resolving.
+ todo!()
+ }
+
+ Ok(Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "Style \"%s\" is not defined.",
+ &[PhpMixed::from(name_str)],
+ ),
+ code: 0,
+ },
+ )))
+ }
+
+ // --- Phase A helpers for `mixed`/array semantics over PhpMixed -------------------------------
+ //
+ // These bridge PHP's dynamic typing (a cell is a TableCell, a scalar, null, or an array;
+ // a row is an array or a TableSeparator) onto PhpMixed. Their bodies are deferred to Phase B
+ // where the concrete PhpMixed representation is settled.
+
+ fn formatter_is_wrappable(_output: &Rc<RefCell<dyn OutputInterface>>) -> bool {
+ // PHP: $this->output->getFormatter() instanceof WrappableOutputFormatterInterface
+ // TODO(phase-b): trait-to-trait instanceof check requires concrete formatter knowledge.
+ let _ = std::any::type_name::<dyn WrappableOutputFormatterInterface>();
+ todo!()
+ }
+
+ /// 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: &Rc<RefCell<dyn OutputInterface>>) -> bool {
+ // PHP: $this->output instanceof ConsoleSectionOutput
todo!()
}
- pub fn set_rows(&mut self, _rows: Vec<PhpMixed>) -> &mut Self {
+ fn is_divider(_row: &PhpMixed, _divider: &TableSeparator) -> bool {
+ // PHP: $divider === $row (object identity)
todo!()
}
- pub fn add_row(&mut self, _row: PhpMixed) -> &mut Self {
+ fn cell_colspan(_cell: &PhpMixed) -> i64 {
+ // PHP: $cell->getColspan()
todo!()
}
- pub fn render(&mut self) {
+ fn cell_rowspan(_cell: &PhpMixed) -> i64 {
+ // PHP: $cell->getRowspan()
+ todo!()
+ }
+
+ fn cell_get_style(_cell: &PhpMixed) -> Option<TableCellStyle> {
+ // PHP: $cell->getStyle()
+ todo!()
+ }
+
+ fn cell_style_is_table_cell_style(_cell: &PhpMixed) -> bool {
+ // PHP: $cell->getStyle() instanceof TableCellStyle
+ todo!()
+ }
+
+ fn table_cell_options_colspan(_colspan: i64) -> IndexMap<String, TableCellOption> {
+ // PHP: ['colspan' => $colspan]
+ todo!()
+ }
+
+ fn table_cell_options_colspan_style(
+ _colspan: i64,
+ _style: Option<TableCellStyle>,
+ ) -> IndexMap<String, TableCellOption> {
+ // PHP: ['colspan' => $colspan, 'style' => $style]
+ todo!()
+ }
+
+ fn row_get(_row: &PhpMixed, _index: i64) -> Option<PhpMixed> {
+ // PHP: isset($row[$i]) ? $row[$i] : null, where $row is an array-typed PhpMixed
+ todo!()
+ }
+
+ fn row_get_index(_row: &[PhpMixed], _index: i64) -> Option<PhpMixed> {
+ // PHP: $row[$column] ?? null, honoring sparse integer keys
+ todo!()
+ }
+
+ fn array_set(_row: &mut [PhpMixed], _index: i64, _value: PhpMixed) {
+ // PHP: $row[$index] = $value (sparse assignment)
todo!()
}
- pub fn set_style(&mut self, _style: &str) -> &mut Self {
+ fn vec_set(_row: &mut Vec<PhpMixed>, _index: i64, _value: PhpMixed) {
+ // PHP: $row[$index] = $value (sparse assignment)
todo!()
}
- pub fn set_column_width(&mut self, _column_index: usize, _width: i64) -> &mut Self {
+ /// PHP rows/cells are integer-keyed arrays. We model a row as a positional
+ /// `Vec<PhpMixed>`. A cell that is a TableCell/TableSeparator cannot be carried by
+ /// PhpMixed (php-shim limitation), so such conversions are deferred.
+ fn to_row_vec(_row: PhpMixed) -> Vec<PhpMixed> {
+ // PHP: an array-typed value seen as a list of cells.
todo!()
}
- pub fn set_column_widths(&mut self, _widths: Vec<i64>) -> &mut Self {
+ fn from_row_vec(_row: Vec<PhpMixed>) -> PhpMixed {
+ // PHP: a list of cells seen as an array-typed value.
todo!()
}
- pub fn set_column_max_width(&mut self, _column_index: usize, _width: i64) -> &mut Self {
+ /// PhpMixed cannot hold console value objects (TableCell/TableSeparator). The cell
+ /// representation is deferred to a later phase.
+ fn table_cell_to_mixed(_cell: TableCell) -> PhpMixed {
todo!()
}
- pub fn set_horizontal(&mut self, _horizontal: bool) -> &mut Self {
+ fn table_separator_to_mixed(_separator: TableSeparator) -> PhpMixed {
todo!()
}
}