/home/complianthowden/public_html/vendor/psy/psysh/src/Formatter/ManualFormatter.php
<?php

/*
 * This file is part of Psy Shell.
 *
 * (c) 2012-2025 Justin Hileman
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Psy\Formatter;

use Psy\Manual\ManualInterface;

/**
 * Formats structured manual data for display at runtime.
 *
 * Takes structured data from the v3 manual format and formats it for display,
 * adapting to terminal width and converting semantic tags to console styles.
 */
class ManualFormatter
{
    // Maximum width for text wrapping, even on very wide terminals
    private const MAX_WIDTH = 120;

    private ManualWrapper $wrapper;
    private int $width;
    private ?ManualInterface $manual;

    /**
     * @param int                  $width  Terminal width for text wrapping
     * @param ManualInterface|null $manual Optional manual for generating hyperlinks
     */
    public function __construct(int $width = 100, ?ManualInterface $manual = null)
    {
        $this->wrapper = new ManualWrapper();
        // Cap width at MAX_WIDTH for readability on ultra-wide terminals
        $this->width = \min($width, self::MAX_WIDTH);
        $this->manual = $manual;
    }

    /**
     * Format structured manual data for display.
     *
     * @param array $data Structured manual data
     */
    public function format(array $data): string
    {
        $output = [];

        // Format based on type
        switch ($data['type'] ?? '') {
            case 'function':
                $output[] = $this->formatFunction($data);
                break;
            case 'class':
                $output[] = $this->formatClass($data);
                break;
            case 'constant':
                $output[] = $this->formatConstant($data);
                break;
            default:
                // Generic fallback
                if (!empty($data['description'])) {
                    $output[] = $this->formatDescription($data['description']);
                }
        }

        return \implode("\n\n", \array_filter($output))."\n";
    }

    /**
     * Format a function entry.
     *
     * @param array $data Function data
     */
    private function formatFunction(array $data): string
    {
        $output = [];

        if (!empty($data['description'])) {
            $output[] = $this->formatDescription($data['description']);
        }

        if (!empty($data['params'])) {
            $output[] = $this->formatParameters($data['params']);
        }

        if (!empty($data['return'])) {
            $output[] = $this->formatReturn($data['return']);
        }

        if (!empty($data['seeAlso'])) {
            $output[] = $this->formatSeeAlso($data['seeAlso']);
        }

        return \implode("\n\n", \array_filter($output));
    }

    /**
     * Format a class entry.
     *
     * @param array $data Class data
     */
    private function formatClass(array $data): string
    {
        $output = [];

        // Description
        if (!empty($data['description'])) {
            $output[] = $this->formatDescription($data['description']);
        }

        // See also
        if (!empty($data['seeAlso'])) {
            $output[] = $this->formatSeeAlso($data['seeAlso']);
        }

        return \implode("\n\n", \array_filter($output));
    }

    /**
     * Format a constant entry.
     *
     * @param array $data Constant data
     */
    private function formatConstant(array $data): string
    {
        $output = [];

        if (isset($data['value'])) {
            $output[] = '<strong>Value:</strong> '.$this->thunkTags($data['value']);
        }

        if (!empty($data['description'])) {
            $output[] = $this->formatDescription($data['description']);
        }

        if (!empty($data['seeAlso'])) {
            $output[] = $this->formatSeeAlso($data['seeAlso']);
        }

        return \implode("\n\n", \array_filter($output));
    }

    /**
     * Format a description section.
     *
     * @param string $description Description text with semantic tags
     *
     * @return string Formatted description
     */
    private function formatDescription(string $description): string
    {
        $output = ['<comment>Description:</comment>'];

        $text = $this->thunkTags($description);
        $wrapped = $this->wrapper->wrap($text, $this->width - 2);

        $output = \array_merge($output, $this->indentWrappedLines($wrapped, '  '));

        return \implode("\n", $output);
    }

    /**
     * Format parameters section.
     *
     * @param array $params Parameter list
     */
    private function formatParameters(array $params): string
    {
        // Decide layout based on terminal width
        // Use table layout for wide terminals (80+), stacked for narrow
        if ($this->width >= 80) {
            return $this->formatParametersTable($params);
        } else {
            return $this->formatParametersStacked($params);
        }
    }

    /**
     * Format parameters as a table (for wide terminals).
     *
     * @param array $params Parameter list
     */
    private function formatParametersTable(array $params): string
    {
        $output = ['<comment>Param:</comment>'];

        // Calculate column widths (matching old format)
        $typeWidth = \max(\array_map(function ($param) {
            return \mb_strlen($param['type'] ?? 'mixed');
        }, $params));

        $nameWidth = \max(\array_map(function ($param) {
            return \mb_strlen($param['name']);
        }, $params));

        // Build columns with padding OUTSIDE style tags
        $indent = \str_repeat(' ', $typeWidth + $nameWidth + 6);
        $wrapWidth = $this->width - \mb_strlen($indent);

        foreach ($params as $param) {
            $type = $param['type'] ?? 'mixed';
            $name = $param['name'];
            $desc = $this->thunkTags($param['description'] ?? '');

            // Wrap in style tags first, THEN pad to avoid long color blocks
            $typeFormatted = '<info>'.$type.'</info>'.\str_repeat(' ', $typeWidth - \mb_strlen($type));
            $nameFormatted = '<strong>'.$name.'</strong>'.\str_repeat(' ', $nameWidth - \mb_strlen($name));

            // Wrap description with proper indentation
            if (!empty($desc)) {
                $wrapped = $this->wrapper->wrap($desc, $wrapWidth);
                $firstLine = '  '.$typeFormatted.'  '.$nameFormatted.'  ';
                $output = \array_merge($output, $this->indentWrappedLines($wrapped, $indent, $firstLine));
            } else {
                $output[] = '  '.$typeFormatted.'  '.$nameFormatted;
            }
        }

        return \implode("\n", $output);
    }

    /**
     * Format parameters stacked (for narrow terminals).
     *
     * @param array $params Parameter list
     */
    private function formatParametersStacked(array $params): string
    {
        $output = ['<comment>Param:</comment>'];

        // Calculate type width for alignment
        $typeWidth = \max(\array_map(function ($param) {
            return \mb_strlen($param['type'] ?? 'mixed');
        }, $params));

        foreach ($params as $param) {
            $type = \str_pad($param['type'] ?? 'mixed', $typeWidth);
            $name = $param['name'];

            $output[] = \sprintf('  <info>%s</info>  <strong>%s</strong>', $type, $name);

            if (!empty($param['description'])) {
                $desc = $this->thunkTags($param['description']);
                $indent = \str_repeat(' ', $typeWidth + 4);
                $wrapped = $this->wrapper->wrap($desc, $this->width - \mb_strlen($indent));
                $output = \array_merge($output, $this->indentWrappedLines($wrapped, $indent));
            }
        }

        return \implode("\n", $output);
    }

    /**
     * Format return value section.
     *
     * @param array $return Return value data
     */
    private function formatReturn(array $return): string
    {
        $output = ['<comment>Return:</comment>'];

        $type = $return['type'] ?? 'unknown';
        $desc = $return['description'] ?? '';

        $indent = \str_repeat(' ', \mb_strlen($type) + 4);
        $wrapWidth = $this->width - \mb_strlen($indent);

        if (!empty($desc)) {
            $desc = $this->thunkTags($desc);
            $wrapped = $this->wrapper->wrap($desc, $wrapWidth);
            $firstLine = \sprintf('  <info>%s</info>  ', $type);
            $output = \array_merge($output, $this->indentWrappedLines($wrapped, $indent, $firstLine));
        } else {
            $output[] = \sprintf('  <info>%s</info>', $type);
        }

        return \implode("\n", $output);
    }

    /**
     * Format see also section.
     *
     * @param array $seeAlso List of related functions/classes
     */
    private function formatSeeAlso(array $seeAlso): string
    {
        if (empty($seeAlso)) {
            return '';
        }

        $output = ['<comment>See Also:</comment>'];

        // Format items with hyperlinks if manual is available
        $items = \array_map(function ($item) {
            return $this->formatSeeAlsoItem($item);
        }, $seeAlso);

        // Don't wrap - console tags need to stay intact
        // Just join with commas and indent
        $output[] = '  '.\implode(', ', $items);

        return \implode("\n", $output);
    }

    /**
     * Format a single see also item with hyperlink if available.
     *
     * @param string $item Function or class name (may contain XML tags)
     */
    private function formatSeeAlsoItem(string $item): string
    {
        // Strip XML tags to get the actual function/class name
        $cleanItem = \strip_tags($item);

        // Check if this item exists in the manual
        $href = null;
        if ($this->manual !== null && $this->manual->get($cleanItem) !== null) {
            $href = LinkFormatter::getPhpNetUrl($cleanItem);
        }

        // Add parentheses to functions (like php.net and old manual format)
        // Items with <function> tags are functions, otherwise classes/constants
        $displayText = $cleanItem;
        if (\strpos($item, '<function>') !== false) {
            $displayText .= '()';
        }

        if ($href !== null) {
            return LinkFormatter::styleWithHref('info', $displayText, $href);
        }

        // No hyperlink; apply semantic tag formatting, then add parens if function
        $formatted = $this->thunkTags($item);
        if (\strpos($item, '<function>') !== false && \strpos($formatted, '()') === false) {
            $formatted .= '()';
        }

        return $formatted;
    }

    /**
     * Indent wrapped text lines.
     *
     * Takes wrapped text and adds indentation to each line.
     * The first line can have a different prefix than subsequent lines.
     *
     * @param string $wrapped     Wrapped text (may contain newlines)
     * @param string $indent      Indentation for continuation lines
     * @param string $firstIndent Optional different indentation for first line (defaults to $indent)
     *
     * @return array Lines with indentation applied
     */
    private function indentWrappedLines(string $wrapped, string $indent, ?string $firstIndent = null): array
    {
        $firstIndent = $firstIndent ?? $indent;
        $lines = \explode("\n", $wrapped);
        $output = [];

        foreach ($lines as $i => $line) {
            $output[] = ($i === 0 ? $firstIndent : $indent).$line;
        }

        return $output;
    }

    /**
     * Convert semantic XML tags to Symfony Console format tags.
     *
     * @param string $text Text with semantic tags
     *
     * @return string Text with console format tags
     */
    private function thunkTags(string $text): string
    {
        // First, escape any < and > that aren't part of our semantic tags
        // Protect our semantic tags by replacing them with placeholders
        $tagMap = [];
        $tagIndex = 0;

        // Protect semantic tags
        $semanticTags = ['parameter', 'function', 'constant', 'classname', 'type', 'literal', 'class'];
        foreach ($semanticTags as $tag) {
            $text = \preg_replace_callback(
                "/<{$tag}>|<\/{$tag}>/",
                function ($matches) use (&$tagMap, &$tagIndex) {
                    $placeholder = "\x00TAG{$tagIndex}\x00";
                    $tagMap[$placeholder] = $matches[0];
                    $tagIndex++;

                    return $placeholder;
                },
                $text
            );
        }

        // Now escape any remaining < and > (these are content, not tags)
        $text = \str_replace(['<', '>'], ['\\<', '\\>'], $text);

        // Restore protected tags
        $text = \str_replace(\array_keys($tagMap), \array_values($tagMap), $text);

        // Handle parameters: add $ prefix and make bold
        $text = \preg_replace_callback(
            '/<parameter>([^<]+)<\/parameter>/',
            function ($matches) {
                $name = $matches[1];
                // Add $ if not already present
                if ($name[0] !== '$') {
                    $name = '$'.$name;
                }

                return '<strong>'.$name.'</strong>';
            },
            $text
        );

        // Handle functions: add () suffix and make bold
        $text = \preg_replace_callback(
            '/<function>([^<]+)<\/function>/',
            function ($matches) {
                $name = $matches[1];
                // Add () if not already present
                if (\substr($name, -2) !== '()') {
                    $name .= '()';
                }

                return '<strong>'.$name.'</strong>';
            },
            $text
        );

        // Map other semantic tags to corresponding formats
        $replacements = [
            '<constant>'   => '<info>',
            '</constant>'  => '</info>',
            '<classname>'  => '<class>',
            '</classname>' => '</class>',
            '<class>'      => '<class>',
            '</class>'     => '</class>',
            '<type>'       => '<info>',
            '</type>'      => '</info>',
            '<literal>'    => '<return>',
            '</literal>'   => '</return>',
        ];

        $text = \str_replace(\array_keys($replacements), \array_values($replacements), $text);

        return $text;
    }
}
Customer Complaint Form | Howden Indonesia - Official Working Website

CUSTOMER COMPLAINT FORM

Please use this form to give us suggestions, compliments or complaints.
Click here to check complaint status.
Click here to show Term of Business Agreement
Howden


Notes: *.png, *.jpg, *.jpeg, *.pdf, *.doc, *.docx, *.xls, *.xlsx, *.ppt, *.pptx, *.eml are allowed, and size must be smaller than 5Mb.

Copyright © 2026 PT. Howden Insurance Brokers Indonesia. All rights reserved.
Authorised and regulated by Otoritas Jasa Keuangan (OJK).
Member of The Association of Indonesian Insurance & Reinsurance Brokers (APPARINDO).