From cb7eb183e26a716adeea48d0f1fb155247748a35 Mon Sep 17 00:00:00 2001 From: Samuel Akopyan Date: Mon, 24 Nov 2025 20:21:43 +0200 Subject: [PATCH 1/4] Improved documentation in dockblocks --- src/PrettyPrint.php | 364 ++++++++++++++++++++++++++------------------ 1 file changed, 218 insertions(+), 146 deletions(-) diff --git a/src/PrettyPrint.php b/src/PrettyPrint.php index 105512a..d70e0e9 100644 --- a/src/PrettyPrint.php +++ b/src/PrettyPrint.php @@ -33,13 +33,173 @@ * $pp('Line without newline', ['end' => '']); */ class PrettyPrint { + // ---- Callable main entry ---- + /** + * Invoke the pretty printer. + * + * Supported arguments patterns: + * - Scalars/strings: will be printed space-separated. + * - Multiple 1D arrays: treated as rows and aligned in columns. + * - Label + 2D array: prints label on first line and aligned matrix below. + * - Label + 3D array: prints label on first line and tensor([...]) below. + * - Single 2D array: prints tensor([[...]], ...) like PyTorch. + * - Single 3D array: prints tensor([...]) with head/tail summarization. + * + * Options (pass as trailing array): + * - 'end' => string // line terminator, default "\n" + * - 'headB' => int, 'tailB' => int // number of head/tail 2D blocks for 3D tensors + * - 'headRows' => int, 'tailRows' => int // rows per 2D slice to show (with ellipsis if truncated) + * - 'headCols' => int, 'tailCols' => int // columns per 2D slice to show (with ellipsis if truncated) + * + * Call examples: + * (new PrettyPrint())('Metrics:', ['end' => "\n\n"]); + * (new PrettyPrint())([1,2,3], [4,5,6]); + * (new PrettyPrint())($matrix2d, ['headRows' => 4, 'tailRows' => 0]); + * (new PrettyPrint())($tensor3d, ['headB' => 4, 'tailB' => 2]); + */ + public function __invoke(...$args) { + $end = PHP_EOL; + if (isset($args['end'])) { + $end = (string)$args['end']; + unset($args['end']); + } else if (!empty($args)) { + $last = end($args); + if (is_array($last) && array_key_exists('end', $last)) { + $end = (string)($last['end'] ?? ''); + array_pop($args); + } + reset($args); + } + + // Extract optional tensor formatting options from trailing options array + $fmt = []; + // 1) Support PHP named arguments for formatting keys + $fmtKeys = ['headB','tailB','headRows','tailRows','headCols','tailCols']; + foreach ($fmtKeys as $k) { + if (array_key_exists($k, $args)) { + $fmt[$k] = $args[$k]; + unset($args[$k]); + } + } + if (!empty($args)) { + $last = end($args); + if (is_array($last)) { + $hasFmt = false; + foreach ($fmtKeys as $k) { if (array_key_exists($k, $last)) { $hasFmt = true; break; } } + if ($hasFmt) { + // Merge trailing array options (takes precedence over named if both provided) + $fmt = array_merge($fmt, $last); + array_pop($args); + reset($args); + } + } + } + + $args = array_values($args); + + // Label + single 3D tensor + if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && $this->is3D($args[1])) { + $out = $this->format3DTorch( + $args[1], + (int)($fmt['headB'] ?? 3), + (int)($fmt['tailB'] ?? 3), + (int)($fmt['headRows'] ?? 2), + (int)($fmt['tailRows'] ?? 3), + (int)($fmt['headCols'] ?? 3), + (int)($fmt['tailCols'] ?? 3) + ); + echo $out . $end; + return; + } + + // Label + 2D matrix + if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && $this->is2D($args[1])) { + $label = is_bool($args[0]) ? ($args[0] ? 'True' : 'False') : (is_null($args[0]) ? 'None' : (string)$args[0]); + $out = $this->format2DAligned($args[1]); + echo ($label . "\n" . $out) . $end; + return; + } + + // Multiple 1D rows → align + $label = null; $rows = []; + if (count($args) > 1) { + $startIndex = 0; + if (!is_array($args[0])) { + $label = is_bool($args[0]) ? ($args[0] ? 'True' : 'False') : (is_null($args[0]) ? 'None' : (string)$args[0]); + $startIndex = 1; + } + $allRows = true; + for ($i = $startIndex; $i < count($args); $i++) { + if ($this->is1D($args[$i])) { $rows[] = $args[$i]; } else { $allRows = false; break; } + } + if ($allRows && count($rows) > 1) { + $out = $this->format2DAligned($rows); + echo (($label !== null) ? ($label . "\n" . $out) : $out) . $end; + return; + } + } + + // Default formatting + $parts = []; + foreach ($args as $arg) { + if (is_array($arg)) { + if ($this->is3D($arg)) { + $parts[] = $this->format3DTorch( + $arg, + (int)($fmt['headB'] ?? 3), + (int)($fmt['tailB'] ?? 3), + (int)($fmt['headRows'] ?? 2), + (int)($fmt['tailRows'] ?? 3), + (int)($fmt['headCols'] ?? 3), + (int)($fmt['tailCols'] ?? 3) + ); + } elseif ($this->is2D($arg)) { + $parts[] = $this->format2DTorch( + $arg, + (int)($fmt['headRows'] ?? 2), + (int)($fmt['tailRows'] ?? 3), + (int)($fmt['headCols'] ?? 3), + (int)($fmt['tailCols'] ?? 3) + ); + } else { + $parts[] = $this->formatForArray($arg); + } + } elseif (is_bool($arg)) { + $parts[] = $arg ? 'True' : 'False'; + } elseif (is_null($arg)) { + $parts[] = 'None'; + } elseif (is_int($arg) || is_float($arg)) { + $parts[] = $this->formatNumber($arg); + } else { + $parts[] = (string)$arg; + } + } + + echo implode(' ', $parts) . $end; + } + // ---- Private helpers ---- + /** + * Format a value as a number when possible. + * + * Integers are returned verbatim; floats are rendered with 4 decimal places; + * non-numeric values are cast to string. + * + * @param mixed $v + * @return string + */ private function formatNumber($v): string { if (is_int($v)) return (string)$v; if (is_float($v)) return number_format($v, 4, '.', ''); return (string)$v; } + /** + * Determine if the given value is a 1D array of numeric scalars. + * + * @param mixed $value + * @return bool True if $value is an array where every element is int or float. + */ private function is1D($value): bool { if (!is_array($value)) return false; foreach ($value as $cell) { @@ -48,6 +208,14 @@ private function is1D($value): bool { return true; } + /** + * Determine if the given value is a 2D numeric matrix. + * + * Accepts empty arrays as 2D. + * + * @param mixed $value + * @return bool True if $value is an array of arrays of int|float. + */ private function is2D($value): bool { if (!is_array($value)) return false; if (empty($value)) return true; @@ -60,6 +228,12 @@ private function is2D($value): bool { return true; } + /** + * Determine if the given value is a 3D tensor of numeric matrices. + * + * @param mixed $value + * @return bool True if $value is an array of 2D numeric arrays. + */ private function is3D($value): bool { if (!is_array($value)) return false; foreach ($value as $matrix) { @@ -68,6 +242,12 @@ private function is3D($value): bool { return true; } + /** + * Format a 2D numeric matrix with aligned columns. + * + * @param array $matrix 2D array of ints/floats. + * @return string + */ private function format2DAligned(array $matrix): string { $cols = 0; foreach ($matrix as $row) { @@ -108,6 +288,16 @@ private function format2DAligned(array $matrix): string { return "[" . implode(",\n ", $lines) . "]"; } + /** + * Format a 2D matrix showing head/tail rows and columns with ellipses in-between. + * + * @param array $matrix 2D array of ints/floats. + * @param int $headRows Number of head rows to display. + * @param int $tailRows Number of tail rows to display. + * @param int $headCols Number of head columns to display. + * @param int $tailCols Number of tail columns to display. + * @return string + */ private function format2DSummarized(array $matrix, int $headRows = 2, int $tailRows = 2, int $headCols = 3, int $tailCols = 3): string { $rows = count($matrix); $cols = 0; @@ -174,6 +364,12 @@ private function format2DSummarized(array $matrix, int $headRows = 2, int $tailR return '[' . trim(implode(",\n ", $lines)) . ']'; } + /** + * Generic array-aware formatter producing Python-like representations. + * + * @param mixed $value Scalar or array value to format. + * @return string + */ private function formatForArray($value): string { if (is_array($value)) { if ($this->is2D($value)) return $this->format2DAligned($value); @@ -186,6 +382,18 @@ private function formatForArray($value): string { return "'" . addslashes((string)$value) . "'"; } + /** + * Format a 3D numeric tensor in a PyTorch-like multiline representation. + * + * @param array $tensor3d 3D array of ints/floats. + * @param int $headB Number of head 2D slices to display. + * @param int $tailB Number of tail 2D slices to display. + * @param int $headRows Number of head rows per 2D slice. + * @param int $tailRows Number of tail rows per 2D slice. + * @param int $headCols Number of head columns per 2D slice. + * @param int $tailCols Number of tail columns per 2D slice. + * @return string + */ private function format3DTorch(array $tensor3d, int $headB = 3, int $tailB = 3, int $headRows = 2, int $tailRows = 3, int $headCols = 3, int $tailCols = 3): string { $B = count($tensor3d); $idxs = []; @@ -221,7 +429,16 @@ private function format3DTorch(array $tensor3d, int $headB = 3, int $tailB = 3, return "tensor([\n " . $joined . "\n])"; } - // 2D tensor pretty-print in PyTorch style using summarized 2D formatter + /** + * Format a 2D numeric matrix in a PyTorch-like representation with summarization. + * + * @param array $matrix 2D array of ints/floats. + * @param int $headRows Number of head rows to display. + * @param int $tailRows Number of tail rows to display. + * @param int $headCols Number of head columns to display. + * @param int $tailCols Number of tail columns to display. + * @return string + */ private function format2DTorch(array $matrix, int $headRows = 2, int $tailRows = 3, int $headCols = 3, int $tailCols = 3): string { $s = $this->format2DSummarized($matrix, $headRows, $tailRows, $headCols, $tailCols); // Replace the very first '[' with 'tensor([[' @@ -240,151 +457,6 @@ private function format2DTorch(array $matrix, int $headRows = 2, int $tailRows = } return $s; } - - // ---- Callable main entry ---- - /** - * Invoke the pretty printer. - * - * Supported arguments patterns: - * - Scalars/strings: will be printed space-separated. - * - Multiple 1D arrays: treated as rows and aligned in columns. - * - Label + 2D array: prints label on first line and aligned matrix below. - * - Label + 3D array: prints label on first line and tensor([...]) below. - * - Single 2D array: prints tensor([[...]], ...) like PyTorch. - * - Single 3D array: prints tensor([...]) with head/tail summarization. - * - * Options (pass as trailing array): - * - 'end' => string // line terminator, default "\n" - * - 'headB' => int, 'tailB' => int // number of head/tail 2D blocks for 3D tensors - * - 'headRows' => int, 'tailRows' => int // rows per 2D slice to show (with ellipsis if truncated) - * - 'headCols' => int, 'tailCols' => int // columns per 2D slice to show (with ellipsis if truncated) - * - * Call examples: - * (new PrettyPrint())('Metrics:', ['end' => "\n\n"]); - * (new PrettyPrint())([1,2,3], [4,5,6]); - * (new PrettyPrint())($matrix2d, ['headRows' => 4, 'tailRows' => 0]); - * (new PrettyPrint())($tensor3d, ['headB' => 4, 'tailB' => 2]); - */ - public function __invoke(...$args) { - $end = PHP_EOL; - if (isset($args['end'])) { - $end = (string)$args['end']; - unset($args['end']); - } else if (!empty($args)) { - $last = end($args); - if (is_array($last) && array_key_exists('end', $last)) { - $end = (string)($last['end'] ?? ''); - array_pop($args); - } - reset($args); - } - - // Extract optional tensor formatting options from trailing options array - $fmt = []; - // 1) Support PHP named arguments for formatting keys - $fmtKeys = ['headB','tailB','headRows','tailRows','headCols','tailCols']; - foreach ($fmtKeys as $k) { - if (array_key_exists($k, $args)) { - $fmt[$k] = $args[$k]; - unset($args[$k]); - } - } - if (!empty($args)) { - $last = end($args); - if (is_array($last)) { - $hasFmt = false; - foreach ($fmtKeys as $k) { if (array_key_exists($k, $last)) { $hasFmt = true; break; } } - if ($hasFmt) { - // Merge trailing array options (takes precedence over named if both provided) - $fmt = array_merge($fmt, $last); - array_pop($args); - reset($args); - } - } - } - - $args = array_values($args); - - // Label + single 3D tensor - if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && $this->is3D($args[1])) { - $out = $this->format3DTorch( - $args[1], - (int)($fmt['headB'] ?? 3), - (int)($fmt['tailB'] ?? 3), - (int)($fmt['headRows'] ?? 2), - (int)($fmt['tailRows'] ?? 3), - (int)($fmt['headCols'] ?? 3), - (int)($fmt['tailCols'] ?? 3) - ); - echo $out . $end; - return; - } - - // Label + 2D matrix - if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && $this->is2D($args[1])) { - $label = is_bool($args[0]) ? ($args[0] ? 'True' : 'False') : (is_null($args[0]) ? 'None' : (string)$args[0]); - $out = $this->format2DAligned($args[1]); - echo ($label . "\n" . $out) . $end; - return; - } - - // Multiple 1D rows → align - $label = null; $rows = []; - if (count($args) > 1) { - $startIndex = 0; - if (!is_array($args[0])) { - $label = is_bool($args[0]) ? ($args[0] ? 'True' : 'False') : (is_null($args[0]) ? 'None' : (string)$args[0]); - $startIndex = 1; - } - $allRows = true; - for ($i = $startIndex; $i < count($args); $i++) { - if ($this->is1D($args[$i])) { $rows[] = $args[$i]; } else { $allRows = false; break; } - } - if ($allRows && count($rows) > 1) { - $out = $this->format2DAligned($rows); - echo (($label !== null) ? ($label . "\n" . $out) : $out) . $end; - return; - } - } - - // Default formatting - $parts = []; - foreach ($args as $arg) { - if (is_array($arg)) { - if ($this->is3D($arg)) { - $parts[] = $this->format3DTorch( - $arg, - (int)($fmt['headB'] ?? 3), - (int)($fmt['tailB'] ?? 3), - (int)($fmt['headRows'] ?? 2), - (int)($fmt['tailRows'] ?? 3), - (int)($fmt['headCols'] ?? 3), - (int)($fmt['tailCols'] ?? 3) - ); - } elseif ($this->is2D($arg)) { - $parts[] = $this->format2DTorch( - $arg, - (int)($fmt['headRows'] ?? 2), - (int)($fmt['tailRows'] ?? 3), - (int)($fmt['headCols'] ?? 3), - (int)($fmt['tailCols'] ?? 3) - ); - } else { - $parts[] = $this->formatForArray($arg); - } - } elseif (is_bool($arg)) { - $parts[] = $arg ? 'True' : 'False'; - } elseif (is_null($arg)) { - $parts[] = 'None'; - } elseif (is_int($arg) || is_float($arg)) { - $parts[] = $this->formatNumber($arg); - } else { - $parts[] = (string)$arg; - } - } - - echo implode(' ', $parts) . $end; - } } From f4063adf0b2625731bef47639691f93a553ce6a3 Mon Sep 17 00:00:00 2001 From: Samuel Akopyan Date: Mon, 24 Nov 2025 20:32:39 +0200 Subject: [PATCH 2/4] Add start option to PrettyPrint for prefix control --- CHANGELOG.md | 1 + README.md | 8 ++++- src/PrettyPrint.php | 64 +++++++++++++++++++++++++++------------ tests/PrettyPrintTest.php | 22 ++++++++++++++ 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5c20d..4a192eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 0.3.0 - Improved documentation + - Add "start" option to PrettyPrint for prefix control 0.2.1 / 2025-11-22 - Added Github Actions diff --git a/README.md b/README.md index 1ee854e..c625bf7 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ pprint($tensor3d, headB: 1, tailB: 1, headRows: 1, tailRows: 1, headCols: 1, tai // ]) ``` -New line control +Postfix and prefix control ```php // No newline at the end (like Python's end="") pprint('Same line', end: ''); @@ -90,6 +90,11 @@ pprint('Add line'); pprint('Add line', end: "\n"); // Added addedional 2 newlines at the end after printing pprint('Add 2 lines', end: "\n\n"); + +// Add a prefix at the start of the printed string +pprint('Tabbed', start: "\t"); +// Combine with end to avoid newline +pprint('Prompted', start: '>>> ', end: ''); ``` Print and then exit the script @@ -120,6 +125,7 @@ $pp('Metrics:', [[0.91, 0.02], [0.03, 0.88]]); ### Options reference +- **start**: string. Prefix printed before the content. Example: `pprint('Hello', ['start' => "\t"])`. - **end**: string. Line terminator, default to new line. Example: `pprint('no newline', ['end' => '']);` - **headB / tailB**: ints. Number of head/tail 2D blocks shown for 3D tensors. - **headRows / tailRows**: ints. Rows shown per 2D slice with ellipsis between. diff --git a/src/PrettyPrint.php b/src/PrettyPrint.php index d70e0e9..b7af36a 100644 --- a/src/PrettyPrint.php +++ b/src/PrettyPrint.php @@ -59,34 +59,47 @@ class PrettyPrint { */ public function __invoke(...$args) { $end = PHP_EOL; + $start = ''; + + // Named args for simple options if (isset($args['end'])) { $end = (string)$args['end']; unset($args['end']); - } else if (!empty($args)) { - $last = end($args); - if (is_array($last) && array_key_exists('end', $last)) { - $end = (string)($last['end'] ?? ''); - array_pop($args); - } - reset($args); + } + if (isset($args['start'])) { + $start = (string)$args['start']; + unset($args['start']); } // Extract optional tensor formatting options from trailing options array $fmt = []; // 1) Support PHP named arguments for formatting keys - $fmtKeys = ['headB','tailB','headRows','tailRows','headCols','tailCols']; + $fmtKeys = ['headB', 'tailB', 'headRows', 'tailRows', 'headCols', 'tailCols']; foreach ($fmtKeys as $k) { if (array_key_exists($k, $args)) { $fmt[$k] = $args[$k]; unset($args[$k]); } } + // 2) Trailing array options: may contain start/end and/or formatting keys if (!empty($args)) { $last = end($args); if (is_array($last)) { - $hasFmt = false; - foreach ($fmtKeys as $k) { if (array_key_exists($k, $last)) { $hasFmt = true; break; } } - if ($hasFmt) { + $hasOptions = false; + $optionKeys = array_merge(['end', 'start'], $fmtKeys); + foreach ($optionKeys as $k) { + if (array_key_exists($k, $last)) { + $hasOptions = true; + break; + } + } + if ($hasOptions) { + if (array_key_exists('end', $last)) { + $end = (string)$last['end']; + } + if (array_key_exists('start', $last)) { + $start = (string)$last['start']; + } // Merge trailing array options (takes precedence over named if both provided) $fmt = array_merge($fmt, $last); array_pop($args); @@ -108,7 +121,7 @@ public function __invoke(...$args) { (int)($fmt['headCols'] ?? 3), (int)($fmt['tailCols'] ?? 3) ); - echo $out . $end; + echo $start . $out . $end; return; } @@ -116,12 +129,13 @@ public function __invoke(...$args) { if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && $this->is2D($args[1])) { $label = is_bool($args[0]) ? ($args[0] ? 'True' : 'False') : (is_null($args[0]) ? 'None' : (string)$args[0]); $out = $this->format2DAligned($args[1]); - echo ($label . "\n" . $out) . $end; + echo $start . ($label . "\n" . $out) . $end; return; } // Multiple 1D rows → align - $label = null; $rows = []; + $label = null; + $rows = []; if (count($args) > 1) { $startIndex = 0; if (!is_array($args[0])) { @@ -130,11 +144,16 @@ public function __invoke(...$args) { } $allRows = true; for ($i = $startIndex; $i < count($args); $i++) { - if ($this->is1D($args[$i])) { $rows[] = $args[$i]; } else { $allRows = false; break; } + if ($this->is1D($args[$i])) { + $rows[] = $args[$i]; + } else { + $allRows = false; + break; + } } if ($allRows && count($rows) > 1) { $out = $this->format2DAligned($rows); - echo (($label !== null) ? ($label . "\n" . $out) : $out) . $end; + echo $start . ((($label !== null) ? ($label . "\n" . $out) : $out)) . $end; return; } } @@ -175,10 +194,11 @@ public function __invoke(...$args) { } } - echo implode(' ', $parts) . $end; + echo $start . implode(' ', $parts) . $end; } // ---- Private helpers ---- + /** * Format a value as a number when possible. * @@ -301,7 +321,11 @@ private function format2DAligned(array $matrix): string { private function format2DSummarized(array $matrix, int $headRows = 2, int $tailRows = 2, int $headCols = 3, int $tailCols = 3): string { $rows = count($matrix); $cols = 0; - foreach ($matrix as $row) { if (is_array($row)) { $cols = max($cols, count($row)); } } + foreach ($matrix as $row) { + if (is_array($row)) { + $cols = max($cols, count($row)); + } + } $rowIdxs = []; $useRowEllipsis = false; @@ -339,7 +363,9 @@ private function format2DSummarized(array $matrix, int $headRows = 2, int $tailR } $formatted[] = $frow; } - foreach ($colPositions as $i => $pos) { if ($pos === '...') $widths[$i] = max($widths[$i], 3); } + foreach ($colPositions as $i => $pos) { + if ($pos === '...') $widths[$i] = max($widths[$i], 3); + } // Build lines from pre-formatted rows $buildRow = function (array $frow) use ($widths) { diff --git a/tests/PrettyPrintTest.php b/tests/PrettyPrintTest.php index 08dc88a..98ce5c3 100644 --- a/tests/PrettyPrintTest.php +++ b/tests/PrettyPrintTest.php @@ -145,6 +145,28 @@ public function endOptionNamedArgument(): void self::assertSame('Named', $out); } + #[Test] + #[TestDox('respects start option passed as trailing array')] + public function startOptionTrailingArray(): void + { + $pp = new PrettyPrint(); + ob_start(); + $pp('Hello', ['start' => "\t", 'end' => '']); + $out = ob_get_clean(); + self::assertSame("\tHello", $out); + } + + #[Test] + #[TestDox('respects named start option (PHP named args)')] + public function startOptionNamedArgument(): void + { + $pp = new PrettyPrint(); + ob_start(); + $pp('World', start: '>>> ' , end: ''); + $out = ob_get_clean(); + self::assertSame('>>> World', $out); + } + #[Test] #[TestDox('prints label followed by formatted 3D tensor')] public function labelPlus3DTensor(): void From e8730ba2db710b10fe7dce92a403ce7fec5df1a1 Mon Sep 17 00:00:00 2001 From: Samuel Akopyan Date: Mon, 24 Nov 2025 20:38:10 +0200 Subject: [PATCH 3/4] Add custom label support for tensor formatting in PrettyPrint --- CHANGELOG.md | 1 + README.md | 11 +++++++++++ src/PrettyPrint.php | 23 ++++++++++++++--------- tests/PrettyPrintTest.php | 26 ++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a192eb..ba34dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 0.3.0 - Improved documentation - Add "start" option to PrettyPrint for prefix control + - Add custom "label" support for tensor formatting in PrettyPrint 0.2.1 / 2025-11-22 - Added Github Actions diff --git a/README.md b/README.md index c625bf7..f823246 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,16 @@ pprint($matrix); // ]) ``` +Custom label instead of "tensor" +```php +pprint($matrix, label: 'arr'); +// arr([ +// [ 1, 2, 3, 4, 5], +// [ 6, 7, 8, 9, 10], +// [11, 12, 13, 14, 15] +// ]) +``` + 2D tensor-style formatting with summarization ```php $matrix = [ @@ -127,6 +137,7 @@ $pp('Metrics:', [[0.91, 0.02], [0.03, 0.88]]); - **start**: string. Prefix printed before the content. Example: `pprint('Hello', ['start' => "\t"])`. - **end**: string. Line terminator, default to new line. Example: `pprint('no newline', ['end' => '']);` +- **label**: string. Prefix label for 2D/3D formatted arrays, default `tensor`. Example: `pprint($m, ['label' => 'arr'])`. - **headB / tailB**: ints. Number of head/tail 2D blocks shown for 3D tensors. - **headRows / tailRows**: ints. Rows shown per 2D slice with ellipsis between. - **headCols / tailCols**: ints. Columns shown per 2D slice with ellipsis between. diff --git a/src/PrettyPrint.php b/src/PrettyPrint.php index b7af36a..db96445 100644 --- a/src/PrettyPrint.php +++ b/src/PrettyPrint.php @@ -74,7 +74,7 @@ public function __invoke(...$args) { // Extract optional tensor formatting options from trailing options array $fmt = []; // 1) Support PHP named arguments for formatting keys - $fmtKeys = ['headB', 'tailB', 'headRows', 'tailRows', 'headCols', 'tailCols']; + $fmtKeys = ['headB', 'tailB', 'headRows', 'tailRows', 'headCols', 'tailCols', 'label']; foreach ($fmtKeys as $k) { if (array_key_exists($k, $args)) { $fmt[$k] = $args[$k]; @@ -119,7 +119,8 @@ public function __invoke(...$args) { (int)($fmt['headRows'] ?? 2), (int)($fmt['tailRows'] ?? 3), (int)($fmt['headCols'] ?? 3), - (int)($fmt['tailCols'] ?? 3) + (int)($fmt['tailCols'] ?? 3), + (string)($fmt['label'] ?? 'tensor') ); echo $start . $out . $end; return; @@ -170,7 +171,8 @@ public function __invoke(...$args) { (int)($fmt['headRows'] ?? 2), (int)($fmt['tailRows'] ?? 3), (int)($fmt['headCols'] ?? 3), - (int)($fmt['tailCols'] ?? 3) + (int)($fmt['tailCols'] ?? 3), + (string)($fmt['label'] ?? 'tensor') ); } elseif ($this->is2D($arg)) { $parts[] = $this->format2DTorch( @@ -178,7 +180,8 @@ public function __invoke(...$args) { (int)($fmt['headRows'] ?? 2), (int)($fmt['tailRows'] ?? 3), (int)($fmt['headCols'] ?? 3), - (int)($fmt['tailCols'] ?? 3) + (int)($fmt['tailCols'] ?? 3), + (string)($fmt['label'] ?? 'tensor') ); } else { $parts[] = $this->formatForArray($arg); @@ -418,9 +421,10 @@ private function formatForArray($value): string { * @param int $tailRows Number of tail rows per 2D slice. * @param int $headCols Number of head columns per 2D slice. * @param int $tailCols Number of tail columns per 2D slice. + * @param string $label Prefix label used instead of "tensor". * @return string */ - private function format3DTorch(array $tensor3d, int $headB = 3, int $tailB = 3, int $headRows = 2, int $tailRows = 3, int $headCols = 3, int $tailCols = 3): string { + private function format3DTorch(array $tensor3d, int $headB = 3, int $tailB = 3, int $headRows = 2, int $tailRows = 3, int $headCols = 3, int $tailCols = 3, string $label = 'tensor'): string { $B = count($tensor3d); $idxs = []; $useBEllipsis = false; @@ -452,7 +456,7 @@ private function format3DTorch(array $tensor3d, int $headB = 3, int $tailB = 3, } $joined = implode(",\n\n ", $blocks); - return "tensor([\n " . $joined . "\n])"; + return $label . "([\n " . $joined . "\n])"; } /** @@ -463,15 +467,16 @@ private function format3DTorch(array $tensor3d, int $headB = 3, int $tailB = 3, * @param int $tailRows Number of tail rows to display. * @param int $headCols Number of head columns to display. * @param int $tailCols Number of tail columns to display. + * @param string $label Prefix label used instead of "tensor". * @return string */ - private function format2DTorch(array $matrix, int $headRows = 2, int $tailRows = 3, int $headCols = 3, int $tailCols = 3): string { + private function format2DTorch(array $matrix, int $headRows = 2, int $tailRows = 3, int $headCols = 3, int $tailCols = 3, string $label = 'tensor'): string { $s = $this->format2DSummarized($matrix, $headRows, $tailRows, $headCols, $tailCols); // Replace the very first '[' with 'tensor([[' if (strlen($s) > 0 && $s[0] === '[') { - $s = "tensor([\n " . substr($s, 1); + $s = $label . "([\n " . substr($s, 1); } else { - return 'tensor(' . $s . ')'; + return $label . '(' . $s . ')'; } // Indent subsequent lines by one extra space to align under the double braket $s = str_replace("\n ", "\n ", $s); diff --git a/tests/PrettyPrintTest.php b/tests/PrettyPrintTest.php index 98ce5c3..feda146 100644 --- a/tests/PrettyPrintTest.php +++ b/tests/PrettyPrintTest.php @@ -167,6 +167,32 @@ public function startOptionNamedArgument(): void self::assertSame('>>> World', $out); } + #[Test] + #[TestDox('allows custom label for 2D tensor formatting')] + public function customLabel2D(): void + { + $pp = new PrettyPrint(); + $m = [[1,2],[3,4]]; + ob_start(); + $pp($m, label: 'arr'); + $out = ob_get_clean(); + self::assertTrue(str_starts_with($out, 'arr([')); + self::assertTrue(str_ends_with($out, "])\n")); + } + + #[Test] + #[TestDox('allows custom label for 3D tensor formatting')] + public function customLabel3D(): void + { + $pp = new PrettyPrint(); + $t = [[[1,2],[3,4]], [[5,6],[7,8]]]; + ob_start(); + $pp($t, ['label' => 'ndarray']); + $out = ob_get_clean(); + self::assertTrue(str_starts_with($out, 'ndarray([')); + self::assertTrue(str_ends_with($out, "])\n")); + } + #[Test] #[TestDox('prints label followed by formatted 3D tensor')] public function labelPlus3DTensor(): void From dd7babaac74074630ac9d2500b3be7d321a11f78 Mon Sep 17 00:00:00 2001 From: Samuel Akopyan Date: Mon, 24 Nov 2025 20:45:09 +0200 Subject: [PATCH 4/4] Add web environment `
` wrapping support and update
 README.md

---
 CHANGELOG.md        | 7 ++++---
 README.md           | 4 +++-
 src/PrettyPrint.php | 7 +++++++
 3 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba34dbe..1f2b820 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,8 @@
-0.3.0
+0.3.0 / 2025-11-24
     - Improved documentation
-    - Add "start" option to PrettyPrint for prefix control
-    - Add custom "label" support for tensor formatting in PrettyPrint
+    - Added "start" option to PrettyPrint for prefix control
+    - Added custom "label" support for tensor formatting in PrettyPrint
+    - Added web environment `
` wrapping support
 
 0.2.1 / 2025-11-22
     - Added Github Actions
diff --git a/README.md b/README.md
index f823246..70ec386 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,11 @@ composer require apphp/pretty-print
 
 ## Usage
 
+Note: When used in web (non-CLI) environments, output is automatically wrapped in `
` to preserve spacing. In CLI, no wrapping is applied.
+
 ### Global helper functions
 
-// Print scalars/strings
+Print scalars/strings
 ```php
 pprint('Hello', 123, 4.56);            
 // Hello 123 4.5600
diff --git a/src/PrettyPrint.php b/src/PrettyPrint.php
index db96445..483a98d 100644
--- a/src/PrettyPrint.php
+++ b/src/PrettyPrint.php
@@ -108,6 +108,13 @@ public function __invoke(...$args) {
             }
         }
 
+        // Auto-wrap with 
 for web (non-CLI) usage
+        $isCli = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
+        if (!$isCli) {
+            $start = '
' . $start;
+            $end = $end . '
'; + } + $args = array_values($args); // Label + single 3D tensor