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;
}
}