Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Console\Helper; Chris@0: Chris@17: use Symfony\Component\Console\Exception\InvalidArgumentException; Chris@0: use Symfony\Component\Console\Output\OutputInterface; Chris@0: Chris@0: /** Chris@0: * Provides helpers to display a table. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: * @author Саша Стаменковић Chris@0: * @author Abdellatif Ait boudad Chris@0: * @author Max Grigorian Chris@0: */ Chris@0: class Table Chris@0: { Chris@0: /** Chris@0: * Table headers. Chris@0: */ Chris@17: private $headers = []; Chris@0: Chris@0: /** Chris@0: * Table rows. Chris@0: */ Chris@17: private $rows = []; Chris@0: Chris@0: /** Chris@0: * Column widths cache. Chris@0: */ Chris@17: private $effectiveColumnWidths = []; Chris@0: Chris@0: /** Chris@0: * Number of columns cache. Chris@0: * Chris@14: * @var int Chris@0: */ Chris@0: private $numberOfColumns; Chris@0: Chris@0: /** Chris@0: * @var OutputInterface Chris@0: */ Chris@0: private $output; Chris@0: Chris@0: /** Chris@0: * @var TableStyle Chris@0: */ Chris@0: private $style; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@17: private $columnStyles = []; Chris@0: Chris@0: /** Chris@0: * User set column widths. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@17: private $columnWidths = []; Chris@0: Chris@0: private static $styles; Chris@0: Chris@0: public function __construct(OutputInterface $output) Chris@0: { Chris@0: $this->output = $output; Chris@0: Chris@0: if (!self::$styles) { Chris@0: self::$styles = self::initStyles(); Chris@0: } Chris@0: Chris@0: $this->setStyle('default'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets a style definition. Chris@0: * Chris@0: * @param string $name The style name Chris@0: * @param TableStyle $style A TableStyle instance Chris@0: */ Chris@0: public static function setStyleDefinition($name, TableStyle $style) Chris@0: { Chris@0: if (!self::$styles) { Chris@0: self::$styles = self::initStyles(); Chris@0: } Chris@0: Chris@0: self::$styles[$name] = $style; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets a style definition by name. Chris@0: * Chris@0: * @param string $name The style name Chris@0: * Chris@0: * @return TableStyle Chris@0: */ Chris@0: public static function getStyleDefinition($name) Chris@0: { Chris@0: if (!self::$styles) { Chris@0: self::$styles = self::initStyles(); Chris@0: } Chris@0: Chris@0: if (isset(self::$styles[$name])) { Chris@0: return self::$styles[$name]; Chris@0: } Chris@0: Chris@0: throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets table style. Chris@0: * Chris@0: * @param TableStyle|string $name The style name or a TableStyle instance Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function setStyle($name) Chris@0: { Chris@0: $this->style = $this->resolveStyle($name); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the current table style. Chris@0: * Chris@0: * @return TableStyle Chris@0: */ Chris@0: public function getStyle() Chris@0: { Chris@0: return $this->style; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets table column style. Chris@0: * Chris@0: * @param int $columnIndex Column index Chris@0: * @param TableStyle|string $name The style name or a TableStyle instance Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function setColumnStyle($columnIndex, $name) Chris@0: { Chris@12: $columnIndex = (int) $columnIndex; Chris@0: Chris@0: $this->columnStyles[$columnIndex] = $this->resolveStyle($name); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the current style for a column. Chris@0: * Chris@0: * If style was not set, it returns the global table style. Chris@0: * Chris@0: * @param int $columnIndex Column index Chris@0: * Chris@0: * @return TableStyle Chris@0: */ Chris@0: public function getColumnStyle($columnIndex) Chris@0: { Chris@0: if (isset($this->columnStyles[$columnIndex])) { Chris@0: return $this->columnStyles[$columnIndex]; Chris@0: } Chris@0: Chris@0: return $this->getStyle(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the minimum width of a column. Chris@0: * Chris@0: * @param int $columnIndex Column index Chris@0: * @param int $width Minimum column width in characters Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function setColumnWidth($columnIndex, $width) Chris@0: { Chris@12: $this->columnWidths[(int) $columnIndex] = (int) $width; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the minimum width of all columns. Chris@0: * Chris@0: * @param array $widths Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function setColumnWidths(array $widths) Chris@0: { Chris@17: $this->columnWidths = []; Chris@0: foreach ($widths as $index => $width) { Chris@0: $this->setColumnWidth($index, $width); Chris@0: } Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: public function setHeaders(array $headers) Chris@0: { Chris@0: $headers = array_values($headers); Chris@17: if (!empty($headers) && !\is_array($headers[0])) { Chris@17: $headers = [$headers]; Chris@0: } Chris@0: Chris@0: $this->headers = $headers; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: public function setRows(array $rows) Chris@0: { Chris@17: $this->rows = []; Chris@0: Chris@0: return $this->addRows($rows); Chris@0: } Chris@0: Chris@0: public function addRows(array $rows) Chris@0: { Chris@0: foreach ($rows as $row) { Chris@0: $this->addRow($row); Chris@0: } Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: public function addRow($row) Chris@0: { Chris@0: if ($row instanceof TableSeparator) { Chris@0: $this->rows[] = $row; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@17: if (!\is_array($row)) { Chris@0: throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.'); Chris@0: } Chris@0: Chris@0: $this->rows[] = array_values($row); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: public function setRow($column, array $row) Chris@0: { Chris@0: $this->rows[$column] = $row; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Renders table to output. Chris@0: * Chris@0: * Example: Chris@17: * Chris@17: * +---------------+-----------------------+------------------+ Chris@17: * | ISBN | Title | Author | Chris@17: * +---------------+-----------------------+------------------+ Chris@17: * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | Chris@17: * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | Chris@17: * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | Chris@17: * +---------------+-----------------------+------------------+ Chris@0: */ Chris@0: public function render() Chris@0: { Chris@0: $this->calculateNumberOfColumns(); Chris@0: $rows = $this->buildTableRows($this->rows); Chris@0: $headers = $this->buildTableRows($this->headers); Chris@0: Chris@0: $this->calculateColumnsWidth(array_merge($headers, $rows)); Chris@0: Chris@0: $this->renderRowSeparator(); Chris@0: if (!empty($headers)) { Chris@0: foreach ($headers as $header) { Chris@0: $this->renderRow($header, $this->style->getCellHeaderFormat()); Chris@0: $this->renderRowSeparator(); Chris@0: } Chris@0: } Chris@0: foreach ($rows as $row) { Chris@0: if ($row instanceof TableSeparator) { Chris@0: $this->renderRowSeparator(); Chris@0: } else { Chris@0: $this->renderRow($row, $this->style->getCellRowFormat()); Chris@0: } Chris@0: } Chris@0: if (!empty($rows)) { Chris@0: $this->renderRowSeparator(); Chris@0: } Chris@0: Chris@0: $this->cleanup(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Renders horizontal header separator. Chris@0: * Chris@17: * Example: Chris@17: * Chris@17: * +-----+-----------+-------+ Chris@0: */ Chris@0: private function renderRowSeparator() Chris@0: { Chris@0: if (0 === $count = $this->numberOfColumns) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if (!$this->style->getHorizontalBorderChar() && !$this->style->getCrossingChar()) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $markup = $this->style->getCrossingChar(); Chris@0: for ($column = 0; $column < $count; ++$column) { Chris@0: $markup .= str_repeat($this->style->getHorizontalBorderChar(), $this->effectiveColumnWidths[$column]).$this->style->getCrossingChar(); Chris@0: } Chris@0: Chris@0: $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Renders vertical column separator. Chris@0: */ Chris@0: private function renderColumnSeparator() Chris@0: { Chris@0: return sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Renders table row. Chris@0: * Chris@17: * Example: Chris@17: * Chris@17: * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | Chris@0: * Chris@0: * @param array $row Chris@0: * @param string $cellFormat Chris@0: */ Chris@0: private function renderRow(array $row, $cellFormat) Chris@0: { Chris@0: if (empty($row)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $rowContent = $this->renderColumnSeparator(); Chris@0: foreach ($this->getRowColumns($row) as $column) { Chris@0: $rowContent .= $this->renderCell($row, $column, $cellFormat); Chris@0: $rowContent .= $this->renderColumnSeparator(); Chris@0: } Chris@0: $this->output->writeln($rowContent); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Renders table cell with padding. Chris@0: * Chris@0: * @param array $row Chris@0: * @param int $column Chris@0: * @param string $cellFormat Chris@0: */ Chris@0: private function renderCell(array $row, $column, $cellFormat) Chris@0: { Chris@0: $cell = isset($row[$column]) ? $row[$column] : ''; Chris@0: $width = $this->effectiveColumnWidths[$column]; Chris@0: if ($cell instanceof TableCell && $cell->getColspan() > 1) { Chris@0: // add the width of the following columns(numbers of colspan). Chris@0: foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { Chris@0: $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn]; Chris@0: } Chris@0: } Chris@0: Chris@0: // str_pad won't work properly with multi-byte strings, we need to fix the padding Chris@0: if (false !== $encoding = mb_detect_encoding($cell, null, true)) { Chris@17: $width += \strlen($cell) - mb_strwidth($cell, $encoding); Chris@0: } Chris@0: Chris@0: $style = $this->getColumnStyle($column); Chris@0: Chris@0: if ($cell instanceof TableSeparator) { Chris@0: return sprintf($style->getBorderFormat(), str_repeat($style->getHorizontalBorderChar(), $width)); Chris@0: } Chris@0: Chris@0: $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); Chris@0: $content = sprintf($style->getCellRowContentFormat(), $cell); Chris@0: Chris@0: return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType())); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Calculate number of columns for this table. Chris@0: */ Chris@0: private function calculateNumberOfColumns() Chris@0: { Chris@0: if (null !== $this->numberOfColumns) { Chris@0: return; Chris@0: } Chris@0: Chris@17: $columns = [0]; Chris@0: foreach (array_merge($this->headers, $this->rows) as $row) { Chris@0: if ($row instanceof TableSeparator) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $columns[] = $this->getNumberOfColumns($row); Chris@0: } Chris@0: Chris@0: $this->numberOfColumns = max($columns); Chris@0: } Chris@0: Chris@0: private function buildTableRows($rows) Chris@0: { Chris@17: $unmergedRows = []; Chris@17: for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) { Chris@0: $rows = $this->fillNextRows($rows, $rowKey); Chris@0: Chris@0: // Remove any new line breaks and replace it with a new line Chris@0: foreach ($rows[$rowKey] as $column => $cell) { Chris@0: if (!strstr($cell, "\n")) { Chris@0: continue; Chris@0: } Chris@0: $lines = explode("\n", str_replace("\n", "\n", $cell)); Chris@0: foreach ($lines as $lineKey => $line) { Chris@0: if ($cell instanceof TableCell) { Chris@17: $line = new TableCell($line, ['colspan' => $cell->getColspan()]); Chris@0: } Chris@0: if (0 === $lineKey) { Chris@0: $rows[$rowKey][$column] = $line; Chris@0: } else { Chris@0: $unmergedRows[$rowKey][$lineKey][$column] = $line; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@17: $tableRows = []; Chris@0: foreach ($rows as $rowKey => $row) { Chris@0: $tableRows[] = $this->fillCells($row); Chris@0: if (isset($unmergedRows[$rowKey])) { Chris@0: $tableRows = array_merge($tableRows, $unmergedRows[$rowKey]); Chris@0: } Chris@0: } Chris@0: Chris@0: return $tableRows; Chris@0: } Chris@0: Chris@0: /** Chris@0: * fill rows that contains rowspan > 1. Chris@0: * Chris@0: * @param array $rows Chris@0: * @param int $line Chris@0: * Chris@0: * @return array Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@14: private function fillNextRows(array $rows, $line) Chris@0: { Chris@17: $unmergedRows = []; Chris@0: foreach ($rows[$line] as $column => $cell) { Chris@17: if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) { Chris@17: throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing __toString, %s given.', \gettype($cell))); Chris@14: } Chris@0: if ($cell instanceof TableCell && $cell->getRowspan() > 1) { Chris@0: $nbLines = $cell->getRowspan() - 1; Chris@17: $lines = [$cell]; Chris@0: if (strstr($cell, "\n")) { Chris@0: $lines = explode("\n", str_replace("\n", "\n", $cell)); Chris@17: $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; Chris@0: Chris@17: $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan()]); Chris@0: unset($lines[0]); Chris@0: } Chris@0: Chris@0: // create a two dimensional array (rowspan x colspan) Chris@17: $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows); Chris@0: foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { Chris@0: $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : ''; Chris@17: $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan()]); Chris@0: if ($nbLines === $unmergedRowKey - $line) { Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { Chris@0: // we need to know if $unmergedRow will be merged or inserted into $rows Chris@17: if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) { Chris@0: foreach ($unmergedRow as $cellKey => $cell) { Chris@0: // insert cell into row at cellKey position Chris@17: array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]); Chris@0: } Chris@0: } else { Chris@0: $row = $this->copyRow($rows, $unmergedRowKey - 1); Chris@0: foreach ($unmergedRow as $column => $cell) { Chris@0: if (!empty($cell)) { Chris@0: $row[$column] = $unmergedRow[$column]; Chris@0: } Chris@0: } Chris@17: array_splice($rows, $unmergedRowKey, 0, [$row]); Chris@0: } Chris@0: } Chris@0: Chris@0: return $rows; Chris@0: } Chris@0: Chris@0: /** Chris@0: * fill cells for a row that contains colspan > 1. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function fillCells($row) Chris@0: { Chris@17: $newRow = []; Chris@0: foreach ($row as $column => $cell) { Chris@0: $newRow[] = $cell; Chris@0: if ($cell instanceof TableCell && $cell->getColspan() > 1) { Chris@0: foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { Chris@0: // insert empty value at column position Chris@0: $newRow[] = ''; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return $newRow ?: $row; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param array $rows Chris@0: * @param int $line Chris@0: * Chris@0: * @return array Chris@0: */ Chris@14: private function copyRow(array $rows, $line) Chris@0: { Chris@0: $row = $rows[$line]; Chris@0: foreach ($row as $cellKey => $cellValue) { Chris@0: $row[$cellKey] = ''; Chris@0: if ($cellValue instanceof TableCell) { Chris@17: $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]); Chris@0: } Chris@0: } Chris@0: Chris@0: return $row; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets number of columns by row. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: private function getNumberOfColumns(array $row) Chris@0: { Chris@17: $columns = \count($row); Chris@0: foreach ($row as $column) { Chris@0: $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0; Chris@0: } Chris@0: Chris@0: return $columns; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets list of columns for the given row. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@14: private function getRowColumns(array $row) Chris@0: { Chris@0: $columns = range(0, $this->numberOfColumns - 1); Chris@0: foreach ($row as $cellKey => $cell) { Chris@0: if ($cell instanceof TableCell && $cell->getColspan() > 1) { Chris@0: // exclude grouped columns. Chris@0: $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1)); Chris@0: } Chris@0: } Chris@0: Chris@0: return $columns; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Calculates columns widths. Chris@0: */ Chris@14: private function calculateColumnsWidth(array $rows) Chris@0: { Chris@0: for ($column = 0; $column < $this->numberOfColumns; ++$column) { Chris@17: $lengths = []; Chris@0: foreach ($rows as $row) { Chris@0: if ($row instanceof TableSeparator) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: foreach ($row as $i => $cell) { Chris@0: if ($cell instanceof TableCell) { Chris@0: $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); Chris@0: $textLength = Helper::strlen($textContent); Chris@0: if ($textLength > 0) { Chris@0: $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); Chris@0: foreach ($contentColumns as $position => $content) { Chris@0: $row[$i + $position] = $content; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $lengths[] = $this->getCellWidth($row, $column); Chris@0: } Chris@0: Chris@17: $this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets column width. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: private function getColumnSeparatorWidth() Chris@0: { Chris@17: return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar())); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets cell width. Chris@0: * Chris@0: * @param array $row Chris@0: * @param int $column Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: private function getCellWidth(array $row, $column) Chris@0: { Chris@0: $cellWidth = 0; Chris@0: Chris@0: if (isset($row[$column])) { Chris@0: $cell = $row[$column]; Chris@0: $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); Chris@0: } Chris@0: Chris@0: $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0; Chris@0: Chris@0: return max($cellWidth, $columnWidth); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Called after rendering to cleanup cache data. Chris@0: */ Chris@0: private function cleanup() Chris@0: { Chris@17: $this->effectiveColumnWidths = []; Chris@0: $this->numberOfColumns = null; Chris@0: } Chris@0: Chris@0: private static function initStyles() Chris@0: { Chris@0: $borderless = new TableStyle(); Chris@0: $borderless Chris@0: ->setHorizontalBorderChar('=') Chris@0: ->setVerticalBorderChar(' ') Chris@0: ->setCrossingChar(' ') Chris@0: ; Chris@0: Chris@0: $compact = new TableStyle(); Chris@0: $compact Chris@0: ->setHorizontalBorderChar('') Chris@0: ->setVerticalBorderChar(' ') Chris@0: ->setCrossingChar('') Chris@0: ->setCellRowContentFormat('%s') Chris@0: ; Chris@0: Chris@0: $styleGuide = new TableStyle(); Chris@0: $styleGuide Chris@0: ->setHorizontalBorderChar('-') Chris@0: ->setVerticalBorderChar(' ') Chris@0: ->setCrossingChar(' ') Chris@0: ->setCellHeaderFormat('%s') Chris@0: ; Chris@0: Chris@17: return [ Chris@0: 'default' => new TableStyle(), Chris@0: 'borderless' => $borderless, Chris@0: 'compact' => $compact, Chris@0: 'symfony-style-guide' => $styleGuide, Chris@17: ]; Chris@0: } Chris@0: Chris@0: private function resolveStyle($name) Chris@0: { Chris@0: if ($name instanceof TableStyle) { Chris@0: return $name; Chris@0: } Chris@0: Chris@0: if (isset(self::$styles[$name])) { Chris@0: return self::$styles[$name]; Chris@0: } Chris@0: Chris@0: throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); Chris@0: } Chris@0: }