formatLegacy($tokens, $options); } $includeCss = $options['includeCss'] ?? false; $lineNumbers = $options['lineNumbers'] ?? true; $lineOffset = $options['lineOffset'] ?? 0; $highlightLines = $options['highlightLines'] ?? []; $wrapInPre = $options['wrapInPre'] ?? true; $html = ''; // Include CSS if requested and not already output if ($includeCss && ! self::$cssOutput) { $html .= $this->getCss($options); self::$cssOutput = true; } // Start wrapper if ($wrapInPre) { $html .= '
';
        }

        // Group tokens by line for line number display
        $tokensByLine = $tokens->groupByLine();

        // Fill in missing lines (empty lines between code)
        if (!empty($tokensByLine)) {
            $minLine = min(array_keys($tokensByLine));
            $maxLine = max(array_keys($tokensByLine));

            for ($line = $minLine; $line <= $maxLine; $line++) {
                if (!isset($tokensByLine[$line])) {
                    $tokensByLine[$line] = []; // Empty line
                }
            }
            ksort($tokensByLine); // Sort by line number
        }

        foreach ($tokensByLine as $lineNum => $lineTokens) {
            $displayLineNum = $lineNum + $lineOffset;
            $isHighlighted = in_array($displayLineNum, $highlightLines, true);

            // Add line wrapper
            $lineClass = $isHighlighted ? 'line highlighted' : 'line';
            $html .= sprintf('
', $lineClass); // Add line number if ($lineNumbers) { $html .= sprintf('%d', $displayLineNum); } // Add tokens $html .= ''; foreach ($lineTokens as $token) { $html .= $this->formatSingle($token, $options); } $html .= '
'; } // End wrapper if ($wrapInPre) { $html .= '
'; } return $html; } /** * Format a single token */ public function formatSingle(Token $token, array $options = []): string { // Handle whitespace tokens if ($token->type === TokenType::WHITESPACE) { // Remove newlines from whitespace - we handle line breaks via
$whitespace = str_replace(["\r\n", "\n", "\r"], '', $token->value); // If empty after removing newlines, skip it if ($whitespace === '') { return ''; } // Wrap in span to ensure white-space: pre CSS is applied // Without the span, HTML collapses multiple spaces to one $escapedValue = htmlspecialchars($whitespace, ENT_QUOTES | ENT_HTML5, 'UTF-8'); return sprintf('%s', $escapedValue); } $cssClass = $token->type->getCssClass(); $escapedValue = htmlspecialchars($token->value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); return sprintf('%s', $cssClass, $escapedValue); } /** * Get CSS for syntax highlighting */ public function getCss(array $options = []): string { // Use custom theme if set via setTheme() if (! empty($this->customTheme)) { return $this->generateCustomThemeCss($this->customTheme); } $theme = $options['theme'] ?? 'default'; return match($theme) { 'dark' => $this->getDarkThemeCss(), 'light' => $this->getLightThemeCss(), default => $this->getDefaultThemeCss() }; } /** * Get formatter name */ public function getName(): string { return 'html'; } /** * Check if formatter supports option */ public function supportsOption(string $option): bool { return in_array($option, [ 'includeCss', 'lineNumbers', 'lineOffset', 'highlightLines', 'wrapInPre', 'showWhitespace', 'theme', ], true); } /** * Reset CSS output flag */ public static function resetCssOutput(): void { self::$cssOutput = false; } /** * Set custom theme colors (backward compatibility) */ public function setTheme(array $theme): self { $this->customTheme = $theme; return $this; } /** * Legacy format method for old Token format */ private function formatLegacy(array $tokens, array $options = []): string { $html = ''; foreach ($tokens as $token) { if (is_object($token) && property_exists($token, 'type') && property_exists($token, 'value')) { // Map old TokenType to CSS class $cssClass = 'token-' . str_replace('_', '-', strtolower($token->type->name)); $escapedValue = htmlspecialchars($token->value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $html .= sprintf('%s', $cssClass, $escapedValue); } } return $html; } /** * Get default theme CSS */ private function getDefaultThemeCss(): string { return <<<'CSS' CSS; } /** * Get dark theme CSS */ private function getDarkThemeCss(): string { return $this->getDefaultThemeCss(); // Dark is default } /** * Get light theme CSS */ private function getLightThemeCss(): string { return <<<'CSS' CSS; } /** * Generate custom theme CSS from theme array */ private function generateCustomThemeCss(array $theme): string { $background = $theme['background'] ?? '#2b2b2b'; $color = $theme['identifier'] ?? '#a9b7c6'; $border = $theme['border'] ?? '#555555'; $lineNumber = $theme['line-number'] ?? '#606366'; // Build CSS with custom colors $css = ""; return $css; } }