Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
162
src/Framework/SyntaxHighlighter/DocBlockParser.php
Normal file
162
src/Framework/SyntaxHighlighter/DocBlockParser.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter;
|
||||
|
||||
final readonly class DocBlockParser
|
||||
{
|
||||
public function parseDocBlock(string $docBlock): array
|
||||
{
|
||||
$tokens = [];
|
||||
|
||||
// Zeile für Zeile parsen, aber Originalformat beibehalten
|
||||
$lines = explode("\n", $docBlock);
|
||||
foreach ($lines as $lineIndex => $line) {
|
||||
if ($lineIndex === 0) {
|
||||
// Erste Zeile: /** plus Inhalt
|
||||
if (preg_match('/^(\/\*\*)(.*)$/', $line, $matches)) {
|
||||
$tokens[] = ['type' => 'doc-comment', 'value' => $matches[1]]; // /**
|
||||
if (! empty($matches[2])) { // Nicht trim() verwenden, um Leerzeichen zu erhalten
|
||||
$contentTokens = $this->parseLineContent($matches[2]);
|
||||
$tokens = array_merge($tokens, $contentTokens);
|
||||
}
|
||||
}
|
||||
} elseif ($lineIndex === count($lines) - 1 && str_contains($line, '*/')) {
|
||||
// Letzte Zeile: Inhalt plus */
|
||||
if (preg_match('/^(.*)(\*\/)$/', $line, $matches)) {
|
||||
$prefix = $matches[1];
|
||||
if (preg_match('/^(\s*\*)(.*)$/', $prefix, $prefixMatches)) {
|
||||
$tokens[] = ['type' => 'doc-comment', 'value' => $prefixMatches[1]]; // *
|
||||
if (! empty($prefixMatches[2])) { // Leerzeichen beibehalten
|
||||
$contentTokens = $this->parseLineContent($prefixMatches[2]);
|
||||
$tokens = array_merge($tokens, $contentTokens);
|
||||
}
|
||||
}
|
||||
$tokens[] = ['type' => 'doc-comment', 'value' => $matches[2]]; // */
|
||||
}
|
||||
} else {
|
||||
// Mittlere Zeilen: * plus Inhalt
|
||||
if (preg_match('/^(\s*\*)(.*)$/', $line, $matches)) {
|
||||
$tokens[] = ['type' => 'doc-comment', 'value' => $matches[1]]; // *
|
||||
if (! empty($matches[2])) { // Leerzeichen beibehalten
|
||||
$contentTokens = $this->parseLineContent($matches[2]);
|
||||
$tokens = array_merge($tokens, $contentTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Newline hinzufügen (außer bei letzter Zeile)
|
||||
if ($lineIndex < count($lines) - 1) {
|
||||
$tokens[] = ['type' => 'doc-comment', 'value' => "\n"];
|
||||
}
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
private function extractContent(string $docBlock): string
|
||||
{
|
||||
// /** und */ entfernen
|
||||
$content = preg_replace('/^\/\*\*/', '', $docBlock);
|
||||
$content = preg_replace('/\*\/$/', '', $content);
|
||||
|
||||
// Führende * entfernen
|
||||
$lines = explode("\n", $content);
|
||||
$cleanLines = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = ltrim($line);
|
||||
if (str_starts_with($line, '*')) {
|
||||
$line = ltrim(substr($line, 1));
|
||||
}
|
||||
$cleanLines[] = $line;
|
||||
}
|
||||
|
||||
return implode("\n", $cleanLines);
|
||||
}
|
||||
|
||||
private function parseLineContent(string $content): array
|
||||
{
|
||||
$tokens = [];
|
||||
|
||||
// @tags erkennen (@var, @param, @return, etc.)
|
||||
if (preg_match('/@(\w+)/', $content, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$tagStart = $matches[0][1];
|
||||
$tag = $matches[1][0];
|
||||
|
||||
// Text vor dem Tag
|
||||
if ($tagStart > 0) {
|
||||
$beforeTag = substr($content, 0, $tagStart);
|
||||
$tokens[] = ['type' => 'doc-text', 'value' => $beforeTag];
|
||||
}
|
||||
|
||||
// Der Tag selbst
|
||||
$tokens[] = ['type' => 'doc-tag', 'value' => '@' . $tag];
|
||||
|
||||
// Rest der Zeile nach dem Tag
|
||||
$afterTag = substr($content, $tagStart + strlen($matches[0][0]));
|
||||
if (! empty(trim($afterTag))) {
|
||||
$restTokens = $this->parseAfterTag($tag, trim($afterTag));
|
||||
$tokens = array_merge($tokens, $restTokens);
|
||||
}
|
||||
} else {
|
||||
// Normale Beschreibungszeile
|
||||
$tokens[] = ['type' => 'doc-text', 'value' => $content];
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
private function parseAfterTag(string $tag, string $content): array
|
||||
{
|
||||
$tokens = [];
|
||||
|
||||
switch ($tag) {
|
||||
case 'var':
|
||||
case 'param':
|
||||
case 'return':
|
||||
// Format: @var Type $variable description
|
||||
// oder: @param Type $variable description
|
||||
if (preg_match('/^(\S+)\s*(\$\w+)?\s*(.*)$/', $content, $matches)) {
|
||||
// Typ
|
||||
$tokens[] = ['type' => 'doc-type', 'value' => ' ' . $matches[1]];
|
||||
|
||||
// Variable (falls vorhanden)
|
||||
if (! empty($matches[2])) {
|
||||
$tokens[] = ['type' => 'doc-variable', 'value' => ' ' . $matches[2]];
|
||||
}
|
||||
|
||||
// Beschreibung (falls vorhanden)
|
||||
if (! empty(trim($matches[3]))) {
|
||||
$tokens[] = ['type' => 'doc-text', 'value' => ' ' . $matches[3]];
|
||||
}
|
||||
} else {
|
||||
$tokens[] = ['type' => 'doc-text', 'value' => ' ' . $content];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'throws':
|
||||
// @throws ExceptionClass description
|
||||
if (preg_match('/^(\S+)\s*(.*)$/', $content, $matches)) {
|
||||
$tokens[] = ['type' => 'doc-type', 'value' => ' ' . $matches[1]];
|
||||
if (! empty(trim($matches[2]))) {
|
||||
$tokens[] = ['type' => 'doc-text', 'value' => ' ' . $matches[2]];
|
||||
}
|
||||
} else {
|
||||
$tokens[] = ['type' => 'doc-text', 'value' => ' ' . $content];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Andere Tags - einfach als Text behandeln
|
||||
$tokens[] = ['type' => 'doc-text', 'value' => ' ' . $content];
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter;
|
||||
@@ -7,7 +8,7 @@ use SplFileObject;
|
||||
|
||||
final readonly class FileHighlighter
|
||||
{
|
||||
public function __invoke(string $filename, int $startLine, int $range):string
|
||||
public function __invoke(string $filename, int $startLine, int $range, ?int $errorLine = null): string
|
||||
{
|
||||
$file = new SplFileObject($filename);
|
||||
|
||||
@@ -15,7 +16,7 @@ final readonly class FileHighlighter
|
||||
$file->seek($startLine);
|
||||
|
||||
$lines = [];
|
||||
for ($i = 0; $i < $range && !$file->eof(); $i++) {
|
||||
for ($i = 0; $i < $range && ! $file->eof(); $i++) {
|
||||
$lines[] = $file->current();
|
||||
$file->next();
|
||||
}
|
||||
@@ -24,7 +25,40 @@ final readonly class FileHighlighter
|
||||
|
||||
$highlighter = new SyntaxHighlighter();
|
||||
|
||||
#echo "<style>$css</style>";
|
||||
return $highlighter->highlightFileSegment($code, $startLine);
|
||||
// PHPStorm-authentisches Darcula Theme
|
||||
$darkTheme = [
|
||||
'keyword' => '#cc7832', // Orange für Keywords (class, function, public, etc.)
|
||||
'variable' => '#9876aa', // Lila für Variablen ($var)
|
||||
'string' => '#6a8759', // Grün für Strings
|
||||
'identifier' => '#a9b7c6', // Helles Grau für normale Identifier
|
||||
'number' => '#6897bb', // Blau für Zahlen
|
||||
'comment' => '#808080', // Grau für normale Kommentare
|
||||
'doc-comment' => '#629755', // Grüner für DocBlocks
|
||||
'php-tag' => '#cc7832', // Orange für PHP-Tags
|
||||
'operator' => '#a9b7c6', // Helles Grau für Operatoren
|
||||
'constant' => '#9876aa', // Lila für Konstanten
|
||||
'method-call' => '#6897bb', // Blau für Funktionen/Methoden
|
||||
'property' => '#9876aa', // Lila für Properties
|
||||
'class-name' => '#a9b7c6', // Weiß/Grau für Klassennamen
|
||||
'background' => '#2b2b2b', // PHPStorm Darcula Hintergrund
|
||||
'border' => '#555555', // Border
|
||||
'line-number' => '#606366', // Zeilennummern
|
||||
];
|
||||
|
||||
// CSS-Output zurücksetzen für jeden neuen Aufruf
|
||||
$highlighter->resetCssOutput();
|
||||
|
||||
$formatter = $highlighter->getFormatter('html');
|
||||
$formatter->setTheme($darkTheme);
|
||||
|
||||
// Optionen für Syntax-Highlighting
|
||||
$options = [
|
||||
'filename' => basename($filename), // Dateiname für Window-Bar
|
||||
];
|
||||
if ($errorLine !== null) {
|
||||
$options['highlightLines'] = [$errorLine - 1];
|
||||
}
|
||||
|
||||
return $highlighter->highlightFileSegment($code, $startLine, $options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter\Formatters;
|
||||
|
||||
use App\Framework\SyntaxHighlighter\Token;
|
||||
use App\Framework\SyntaxHighlighter\TokenType;
|
||||
use App\Framework\Tokenizer\ValueObjects\Token;
|
||||
use App\Framework\Tokenizer\ValueObjects\TokenCollection;
|
||||
use App\Framework\Tokenizer\ValueObjects\TokenType;
|
||||
|
||||
class ConsoleFormatter implements FormatterInterface {
|
||||
private const string RESET = "\033[0m";
|
||||
private bool $useColors;
|
||||
|
||||
public function __construct(?bool $useColors = null) {
|
||||
$this->useColors = $useColors ?? $this->isColorSupported();
|
||||
}
|
||||
|
||||
public function format(array $tokens, array $options = []): string {
|
||||
$showLineNumbers = $options['lineNumbers'] ?? false;
|
||||
$startLine = $options['startLine'] ?? 1;
|
||||
$width = $options['width'] ?? 80;
|
||||
|
||||
if (!$this->useColors) {
|
||||
return $this->formatPlainText($tokens, $showLineNumbers, $startLine);
|
||||
/**
|
||||
* Console/Terminal formatter for the modern tokenizer
|
||||
*/
|
||||
final class ConsoleFormatter implements FormatterInterface
|
||||
{
|
||||
/**
|
||||
* Format tokens for terminal output
|
||||
*/
|
||||
public function format(TokenCollection|array $tokens, array $options = []): string
|
||||
{
|
||||
// Handle both old array format and new TokenCollection
|
||||
if (is_array($tokens)) {
|
||||
return $this->formatLegacy($tokens, $options);
|
||||
}
|
||||
|
||||
$lines = $this->groupTokensByLines($tokens);
|
||||
$output = [];
|
||||
$lineNumberWidth = strlen((string) (count($lines) + $startLine - 1));
|
||||
$lineNumbers = $options['lineNumbers'] ?? false;
|
||||
$lineOffset = $options['lineOffset'] ?? 0;
|
||||
$colorize = $options['colorize'] ?? true;
|
||||
$highlightLines = $options['highlightLines'] ?? [];
|
||||
|
||||
foreach ($lines as $lineNumber => $lineTokens) {
|
||||
$actualLineNumber = $lineNumber + $startLine - 1;
|
||||
$lineContent = $this->formatLine($lineTokens);
|
||||
$output = '';
|
||||
|
||||
if ($showLineNumbers) {
|
||||
$lineNumberStr = str_pad($actualLineNumber, $lineNumberWidth, ' ', STR_PAD_LEFT);
|
||||
$output[] = "\033[90m{$lineNumberStr}\033[0m │ {$lineContent}";
|
||||
} else {
|
||||
$output[] = $lineContent;
|
||||
// Group tokens by line
|
||||
$tokensByLine = $tokens->groupByLine();
|
||||
|
||||
foreach ($tokensByLine as $lineNum => $lineTokens) {
|
||||
$displayLineNum = $lineNum + $lineOffset;
|
||||
$isHighlighted = in_array($displayLineNum, $highlightLines, true);
|
||||
|
||||
// Add line number if requested
|
||||
if ($lineNumbers) {
|
||||
$lineNumStr = sprintf("%4d | ", $displayLineNum);
|
||||
$output .= $colorize ? "\033[90m{$lineNumStr}\033[0m" : $lineNumStr;
|
||||
}
|
||||
|
||||
// Highlight line background if needed
|
||||
if ($isHighlighted && $colorize) {
|
||||
$output .= "\033[43m"; // Yellow background
|
||||
}
|
||||
|
||||
// Add tokens
|
||||
foreach ($lineTokens as $token) {
|
||||
$output .= $this->formatSingle($token, $options);
|
||||
}
|
||||
|
||||
// Reset highlighting
|
||||
if ($isHighlighted && $colorize) {
|
||||
$output .= "\033[0m";
|
||||
}
|
||||
|
||||
// Ensure line break
|
||||
if (! str_ends_with($output, "\n")) {
|
||||
$output .= "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $output);
|
||||
return $output;
|
||||
}
|
||||
|
||||
/*private function isColorSupported(): bool {
|
||||
return DIRECTORY_SEPARATOR === '/' &&
|
||||
function_exists('posix_isatty') &&
|
||||
posix_isatty(STDOUT);
|
||||
}*/
|
||||
/**
|
||||
* Format a single token for terminal
|
||||
*/
|
||||
public function formatSingle(Token $token, array $options = []): string
|
||||
{
|
||||
$colorize = $options['colorize'] ?? true;
|
||||
|
||||
private function groupTokensByLines(array $tokens): array {
|
||||
$lines = [];
|
||||
$currentLine = 1;
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (!isset($lines[$currentLine])) {
|
||||
$lines[$currentLine] = [];
|
||||
}
|
||||
|
||||
if (str_contains($token->value, "\n")) {
|
||||
$parts = explode("\n", $token->value);
|
||||
for ($i = 0; $i < count($parts); $i++) {
|
||||
if ($i > 0) {
|
||||
$currentLine++;
|
||||
if (!isset($lines[$currentLine])) {
|
||||
$lines[$currentLine] = [];
|
||||
}
|
||||
}
|
||||
if ($parts[$i] !== '') {
|
||||
$lines[$currentLine][] = new Token(
|
||||
$token->type,
|
||||
$parts[$i],
|
||||
$currentLine,
|
||||
$token->column
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$lines[$currentLine][] = $token;
|
||||
}
|
||||
if (! $colorize) {
|
||||
return $token->value;
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function formatLine(array $tokens): string {
|
||||
$content = '';
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
$content .= $this->formatToken($token);
|
||||
}
|
||||
|
||||
return $content . self::RESET;
|
||||
}
|
||||
|
||||
private function formatToken(Token $token): string {
|
||||
// Skip coloring for whitespace
|
||||
if ($token->type === TokenType::WHITESPACE) {
|
||||
return $token->value;
|
||||
}
|
||||
|
||||
$color = $token->type->getConsoleColor();
|
||||
return $color . $token->value . self::RESET;
|
||||
$color = $token->type->getTerminalColor();
|
||||
|
||||
return $color . $token->value . "\033[0m";
|
||||
}
|
||||
|
||||
private function formatPlainText(array $tokens, bool $showLineNumbers, int $startLine): string {
|
||||
$content = '';
|
||||
$lineNumber = $startLine;
|
||||
$atLineStart = true;
|
||||
/**
|
||||
* Get formatter name
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'console';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if formatter supports option
|
||||
*/
|
||||
public function supportsOption(string $option): bool
|
||||
{
|
||||
return in_array($option, [
|
||||
'lineNumbers',
|
||||
'lineOffset',
|
||||
'colorize',
|
||||
'highlightLines',
|
||||
], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy format method for old Token format
|
||||
*/
|
||||
private function formatLegacy(array $tokens, array $options = []): string
|
||||
{
|
||||
$output = '';
|
||||
$colorize = $options['colorize'] ?? true;
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if ($atLineStart && $showLineNumbers) {
|
||||
$content .= sprintf('%3d: ', $lineNumber);
|
||||
$atLineStart = false;
|
||||
}
|
||||
|
||||
$content .= $token->value;
|
||||
|
||||
if (str_contains($token->value, "\n")) {
|
||||
$lineNumber += substr_count($token->value, "\n");
|
||||
$atLineStart = true;
|
||||
if (is_object($token) && property_exists($token, 'value')) {
|
||||
if ($colorize && property_exists($token, 'type')) {
|
||||
// Map old token type to color
|
||||
$color = $this->getLegacyColor($token->type);
|
||||
$output .= $color . $token->value . "\033[0m";
|
||||
} else {
|
||||
$output .= $token->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for legacy token type
|
||||
*/
|
||||
private function getLegacyColor($tokenType): string
|
||||
{
|
||||
if (! is_object($tokenType) || ! property_exists($tokenType, 'name')) {
|
||||
return "\033[0m";
|
||||
}
|
||||
|
||||
return match($tokenType->name) {
|
||||
'KEYWORD' => "\033[35m",
|
||||
'CLASS_NAME' => "\033[36m",
|
||||
'METHOD_CALL' => "\033[33m",
|
||||
'VARIABLE' => "\033[37m",
|
||||
'STRING' => "\033[32m",
|
||||
'NUMBER' => "\033[34m",
|
||||
'COMMENT', 'DOC_COMMENT' => "\033[90m",
|
||||
'OPERATOR' => "\033[31m",
|
||||
default => "\033[0m"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter\Formatters;
|
||||
|
||||
interface FormatterInterface {
|
||||
public function format(array $tokens, array $options = []): string;
|
||||
use App\Framework\Tokenizer\ValueObjects\Token;
|
||||
use App\Framework\Tokenizer\ValueObjects\TokenCollection;
|
||||
|
||||
/**
|
||||
* Interface for formatters that work with the new tokenizer
|
||||
*/
|
||||
interface FormatterInterface
|
||||
{
|
||||
/**
|
||||
* Format a collection of tokens
|
||||
*/
|
||||
public function format(TokenCollection $tokens, array $options = []): string;
|
||||
|
||||
/**
|
||||
* Format a single token
|
||||
*/
|
||||
public function formatSingle(Token $token, array $options = []): string;
|
||||
|
||||
/**
|
||||
* Get the name of this formatter
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Check if formatter supports a specific option
|
||||
*/
|
||||
public function supportsOption(string $option): bool;
|
||||
}
|
||||
|
||||
@@ -1,326 +1,386 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter\Formatters;
|
||||
|
||||
use App\Framework\SyntaxHighlighter\Token;
|
||||
use App\Framework\SyntaxHighlighter\TokenType;
|
||||
use App\Framework\Tokenizer\ValueObjects\Token;
|
||||
use App\Framework\Tokenizer\ValueObjects\TokenCollection;
|
||||
use App\Framework\Tokenizer\ValueObjects\TokenType;
|
||||
|
||||
class HtmlFormatter implements FormatterInterface {
|
||||
private array $theme = [
|
||||
'keyword' => '#0000FF',
|
||||
'variable' => '#800080',
|
||||
'string' => '#008000',
|
||||
'identifier' => '#000000',
|
||||
'number' => '#FF0000',
|
||||
'comment' => '#808080',
|
||||
'doc-comment' => '#008080',
|
||||
'php-tag' => '#000080',
|
||||
'operator' => '#000080',
|
||||
'background' => '#F8F8F8',
|
||||
'border' => '#DDDDDD',
|
||||
'line-number' => '#999999',
|
||||
];
|
||||
/**
|
||||
* HTML formatter for the modern tokenizer
|
||||
*/
|
||||
final class HtmlFormatter implements FormatterInterface
|
||||
{
|
||||
private static bool $cssOutput = false;
|
||||
|
||||
private static bool $cssOutputted = false;
|
||||
private array $customTheme = [];
|
||||
|
||||
/**
|
||||
* Format tokens as HTML
|
||||
*/
|
||||
public function format(TokenCollection|array $tokens, array $options = []): string
|
||||
{
|
||||
// Handle both old array format and new TokenCollection
|
||||
if (is_array($tokens)) {
|
||||
// Legacy support for old Token format
|
||||
return $this->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 .= '<pre class="syntax-highlight"><code>';
|
||||
}
|
||||
|
||||
// Group tokens by line for line number display
|
||||
$tokensByLine = $tokens->groupByLine();
|
||||
|
||||
foreach ($tokensByLine as $lineNum => $lineTokens) {
|
||||
$displayLineNum = $lineNum + $lineOffset;
|
||||
$isHighlighted = in_array($displayLineNum, $highlightLines, true);
|
||||
|
||||
// Add line wrapper
|
||||
$lineClass = $isHighlighted ? 'line highlighted' : 'line';
|
||||
$html .= sprintf('<div class="%s">', $lineClass);
|
||||
|
||||
// Add line number
|
||||
if ($lineNumbers) {
|
||||
$html .= sprintf('<span class="line-number">%d</span>', $displayLineNum);
|
||||
}
|
||||
|
||||
// Add tokens
|
||||
$html .= '<span class="code">';
|
||||
foreach ($lineTokens as $token) {
|
||||
$html .= $this->formatSingle($token, $options);
|
||||
}
|
||||
$html .= '</span></div>';
|
||||
}
|
||||
|
||||
// End wrapper
|
||||
if ($wrapInPre) {
|
||||
$html .= '</code></pre>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single token
|
||||
*/
|
||||
public function formatSingle(Token $token, array $options = []): string
|
||||
{
|
||||
// Skip whitespace tokens unless explicitly requested
|
||||
if ($token->type === TokenType::WHITESPACE && ! ($options['showWhitespace'] ?? false)) {
|
||||
return htmlspecialchars($token->value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
$cssClass = $token->type->getCssClass();
|
||||
$escapedValue = htmlspecialchars($token->value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
return sprintf('<span class="%s">%s</span>', $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;
|
||||
|
||||
public function setTheme(array $theme): self {
|
||||
$this->theme = array_merge($this->theme, $theme);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function format(array $tokens, array $options = []): string {
|
||||
$showLineNumbers = $options['lineNumbers'] ?? true;
|
||||
$startLine = $options['startLine'] ?? 1;
|
||||
$lineOffset = $options['lineOffset'] ?? 0; // Neuer Offset Parameter
|
||||
$highlightLines = $options['highlightLines'] ?? [];
|
||||
$cssClasses = $options['cssClasses'] ?? true;
|
||||
$includeCss = $options['includeCss'] ?? true;
|
||||
$language = $options['language'] ?? 'php';
|
||||
|
||||
$lines = $this->groupTokensByLines($tokens);
|
||||
$output = [];
|
||||
|
||||
foreach ($lines as $lineNumber => $lineTokens) {
|
||||
// Berücksichtigung von startLine und lineOffset
|
||||
$actualLineNumber = $lineNumber + $startLine - 1 + $lineOffset;
|
||||
$isHighlighted = in_array($actualLineNumber, $highlightLines);
|
||||
|
||||
$lineContent = $this->formatLine($lineTokens, $cssClasses);
|
||||
$output[] = rtrim($this->wrapLine(
|
||||
$lineContent,
|
||||
$actualLineNumber,
|
||||
$showLineNumbers,
|
||||
$isHighlighted
|
||||
));
|
||||
/**
|
||||
* 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('<span class="%s">%s</span>', $cssClass, $escapedValue);
|
||||
}
|
||||
}
|
||||
|
||||
$codeOutput = $this->wrapOutput(implode("\n", $output), $options);
|
||||
|
||||
// CSS automatisch einbinden (nur einmal pro Request)
|
||||
if ($includeCss && !self::$cssOutputted) {
|
||||
$css = $this->generateCss($options);
|
||||
self::$cssOutputted = true;
|
||||
return $css . "\n" . $codeOutput;
|
||||
}
|
||||
|
||||
return $codeOutput;
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function generateCss(array $options = []): string {
|
||||
$tabSize = $options['tabSize'] ?? 4;
|
||||
$fontSize = $options['fontSize'] ?? '14px';
|
||||
$lineHeight = $options['lineHeight'] ?? '1.5';
|
||||
|
||||
return sprintf(
|
||||
'<style>%s</style>',
|
||||
$this->getCssRules($tabSize, $fontSize, $lineHeight)
|
||||
);
|
||||
}
|
||||
|
||||
private function getCssRules(int $tabSize, string $fontSize, string $lineHeight): string {
|
||||
return <<<CSS
|
||||
.syntax-highlighter {
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 1em auto;
|
||||
position: relative;
|
||||
background-color: {$this->theme['background']};
|
||||
border: 1px solid {$this->theme['border']};
|
||||
max-width: fit-content;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.syntax-highlighter code {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: {$fontSize};
|
||||
line-height: {$lineHeight};
|
||||
padding: 16px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
white-space: pre;
|
||||
tab-size: {$tabSize};
|
||||
color: inherit;
|
||||
background: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.syntax-highlighter .line {
|
||||
display: inline-flex;
|
||||
min-height: {$lineHeight}em;
|
||||
width: 100%;
|
||||
|
||||
&:hover .line-number {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.syntax-highlighter .line.highlighted {
|
||||
background-color: rgba(255, 255, 0, 0.1);
|
||||
}
|
||||
|
||||
.syntax-highlighter .line-number {
|
||||
padding-right: 16px;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
border-right: 1px solid {$this->theme['border']};
|
||||
margin-right: 16px;
|
||||
user-select: none;
|
||||
color: {$this->theme['line-number']};
|
||||
}
|
||||
|
||||
.syntax-highlighter .code {
|
||||
flex: 1;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* Token Styles */
|
||||
.syntax-highlighter .token-keyword {
|
||||
color: {$this->theme['keyword']};
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.syntax-highlighter .token-variable {
|
||||
color: {$this->theme['variable']};
|
||||
}
|
||||
|
||||
.syntax-highlighter .token-string {
|
||||
color: {$this->theme['string']};
|
||||
}
|
||||
|
||||
.syntax-highlighter .token-comment {
|
||||
color: {$this->theme['comment']};
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.syntax-highlighter .token-doc-comment {
|
||||
color: {$this->theme['doc-comment']};
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.syntax-highlighter .token-number {
|
||||
color: {$this->theme['number']};
|
||||
}
|
||||
|
||||
.syntax-highlighter .token-php-tag {
|
||||
color: {$this->theme['php-tag']};
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.syntax-highlighter .token-operator {
|
||||
color: {$this->theme['operator']};
|
||||
}
|
||||
|
||||
.syntax-highlighter .token-identifier {
|
||||
color: {$this->theme['identifier']};
|
||||
}
|
||||
|
||||
/* Copy Button */
|
||||
.syntax-highlighter .copy-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
/**
|
||||
* Get default theme CSS
|
||||
*/
|
||||
private function getDefaultThemeCss(): string
|
||||
{
|
||||
return <<<'CSS'
|
||||
<style>
|
||||
.syntax-highlight {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.syntax-highlighter:hover .copy-btn {
|
||||
opacity: 1;
|
||||
.syntax-highlight .line {
|
||||
display: flex;
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
.syntax-highlighter .copy-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
.syntax-highlight .line.highlighted {
|
||||
background: rgba(255, 255, 100, 0.1);
|
||||
}
|
||||
|
||||
.syntax-highlight .line-number {
|
||||
color: #666;
|
||||
padding-right: 1em;
|
||||
user-select: none;
|
||||
min-width: 3em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.syntax-highlight .code {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Token colors */
|
||||
.token-keyword { color: #ff79c6; font-weight: bold; }
|
||||
.token-class-name { color: #8be9fd; }
|
||||
.token-interface-name { color: #8be9fd; }
|
||||
.token-trait-name { color: #8be9fd; }
|
||||
.token-enum-name { color: #8be9fd; }
|
||||
.token-namespace-name { color: #50fa7b; }
|
||||
.token-function-name { color: #f1fa8c; }
|
||||
.token-method-name { color: #f1fa8c; }
|
||||
.token-property-name { color: #bd93f9; }
|
||||
.token-constant-name { color: #ff5555; }
|
||||
.token-variable { color: #f8f8f2; }
|
||||
.token-parameter { color: #ffb86c; }
|
||||
.token-string-literal { color: #50fa7b; }
|
||||
.token-number-literal { color: #bd93f9; }
|
||||
.token-boolean-literal { color: #bd93f9; }
|
||||
.token-null-literal { color: #bd93f9; }
|
||||
.token-comment { color: #6272a4; font-style: italic; }
|
||||
.token-doc-comment { color: #6272a4; font-style: italic; }
|
||||
.token-doc-tag { color: #ff79c6; font-style: italic; }
|
||||
.token-doc-type { color: #8be9fd; font-style: italic; }
|
||||
.token-doc-variable { color: #ffb86c; font-style: italic; }
|
||||
.token-operator { color: #ff79c6; }
|
||||
.token-punctuation { color: #f8f8f2; }
|
||||
.token-attribute { color: #ff79c6; }
|
||||
.token-attribute-name { color: #f1fa8c; }
|
||||
.token-type-hint { color: #8be9fd; }
|
||||
.token-php-tag { color: #ff79c6; }
|
||||
.token-error { color: #ff5555; background: rgba(255, 85, 85, 0.1); }
|
||||
</style>
|
||||
CSS;
|
||||
}
|
||||
|
||||
public static function resetCssOutput(): void {
|
||||
self::$cssOutputted = false;
|
||||
/**
|
||||
* Get dark theme CSS
|
||||
*/
|
||||
private function getDarkThemeCss(): string
|
||||
{
|
||||
return $this->getDefaultThemeCss(); // Dark is default
|
||||
}
|
||||
|
||||
public function getCss(array $options = []): string {
|
||||
return $this->generateCss($options);
|
||||
/**
|
||||
* Get light theme CSS
|
||||
*/
|
||||
private function getLightThemeCss(): string
|
||||
{
|
||||
return <<<'CSS'
|
||||
<style>
|
||||
.syntax-highlight {
|
||||
background: #fafafa;
|
||||
color: #383a42;
|
||||
padding: 1em;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.syntax-highlight .line {
|
||||
display: flex;
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
.syntax-highlight .line.highlighted {
|
||||
background: rgba(255, 251, 0, 0.2);
|
||||
}
|
||||
|
||||
.syntax-highlight .line-number {
|
||||
color: #969896;
|
||||
padding-right: 1em;
|
||||
user-select: none;
|
||||
min-width: 3em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Light theme token colors */
|
||||
.token-keyword { color: #a626a4; font-weight: bold; }
|
||||
.token-class-name { color: #c18401; }
|
||||
.token-function-name { color: #4078f2; }
|
||||
.token-method-name { color: #4078f2; }
|
||||
.token-property-name { color: #986801; }
|
||||
.token-constant-name { color: #986801; }
|
||||
.token-variable { color: #383a42; }
|
||||
.token-parameter { color: #e45649; }
|
||||
.token-string-literal { color: #50a14f; }
|
||||
.token-number-literal { color: #986801; }
|
||||
.token-comment { color: #a0a1a7; font-style: italic; }
|
||||
.token-operator { color: #a626a4; }
|
||||
.token-attribute { color: #a626a4; }
|
||||
.token-type-hint { color: #c18401; }
|
||||
.token-error { color: #e45649; background: rgba(228, 86, 73, 0.1); }
|
||||
</style>
|
||||
CSS;
|
||||
}
|
||||
|
||||
private function groupTokensByLines(array $tokens): array {
|
||||
$lines = [];
|
||||
$currentLine = 1;
|
||||
/**
|
||||
* 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';
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (!isset($lines[$currentLine])) {
|
||||
$lines[$currentLine] = [];
|
||||
}
|
||||
// Build CSS with custom colors
|
||||
$css = "<style>\n";
|
||||
$css .= ".syntax-highlight {\n";
|
||||
$css .= " background: {$background};\n";
|
||||
$css .= " color: {$color};\n";
|
||||
$css .= " padding: 1em;\n";
|
||||
$css .= " border: 1px solid {$border};\n";
|
||||
$css .= " border-radius: 4px;\n";
|
||||
$css .= " overflow-x: auto;\n";
|
||||
$css .= " font-family: 'Fira Code', 'Consolas', monospace;\n";
|
||||
$css .= " font-size: 14px;\n";
|
||||
$css .= " line-height: 1.5;\n";
|
||||
$css .= "}\n\n";
|
||||
|
||||
// Spezielle Behandlung für reine Newline-Token
|
||||
if ($token->value === "\n") {
|
||||
$currentLine++;
|
||||
continue;
|
||||
}
|
||||
$css .= ".syntax-highlight .line { display: flex; min-height: 1.5em; }\n";
|
||||
$css .= ".syntax-highlight .line.highlighted { background: rgba(255, 255, 100, 0.1); }\n";
|
||||
$css .= ".syntax-highlight .line-number {\n";
|
||||
$css .= " color: {$lineNumber};\n";
|
||||
$css .= " padding-right: 1em;\n";
|
||||
$css .= " user-select: none;\n";
|
||||
$css .= " min-width: 3em;\n";
|
||||
$css .= " text-align: right;\n";
|
||||
$css .= "}\n";
|
||||
$css .= ".syntax-highlight .code { flex: 1; }\n\n";
|
||||
|
||||
if (str_contains($token->value, "\n")) {
|
||||
$parts = explode("\n", $token->value);
|
||||
// Map theme keys to CSS token classes
|
||||
$tokenMapping = [
|
||||
'keyword' => 'token-keyword',
|
||||
'variable' => 'token-variable',
|
||||
'string' => 'token-string-literal',
|
||||
'number' => 'token-number-literal',
|
||||
'comment' => 'token-comment',
|
||||
'doc-comment' => 'token-doc-comment',
|
||||
'php-tag' => 'token-php-tag',
|
||||
'operator' => 'token-operator',
|
||||
'constant' => 'token-constant-name',
|
||||
'method-call' => 'token-method-name',
|
||||
'property' => 'token-property-name',
|
||||
'class-name' => 'token-class-name',
|
||||
];
|
||||
|
||||
// Ersten Teil zur aktuellen Zeile hinzufügen
|
||||
if ($parts[0] !== '') {
|
||||
$lines[$currentLine][] = new Token(
|
||||
$token->type,
|
||||
$parts[0],
|
||||
$currentLine,
|
||||
$token->column
|
||||
);
|
||||
}
|
||||
|
||||
// Für jeden Newline eine neue Zeile erstellen
|
||||
for ($i = 1; $i < count($parts); $i++) {
|
||||
$currentLine++;
|
||||
if (!isset($lines[$currentLine])) {
|
||||
$lines[$currentLine] = [];
|
||||
}
|
||||
|
||||
// Nur nicht-leere Teile hinzufügen
|
||||
if ($parts[$i] !== '') {
|
||||
$lines[$currentLine][] = new Token(
|
||||
$token->type,
|
||||
$parts[$i],
|
||||
$currentLine,
|
||||
$token->column
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$lines[$currentLine][] = $token;
|
||||
// Generate token-specific styles
|
||||
foreach ($tokenMapping as $themeKey => $cssClass) {
|
||||
if (isset($theme[$themeKey])) {
|
||||
$css .= ".{$cssClass} { color: {$theme[$themeKey]}; }\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function formatLine(array $tokens, bool $useCssClasses): string {
|
||||
$content = '';
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
$content .= $this->formatToken($token, $useCssClasses);
|
||||
// Special cases with additional styling
|
||||
if (isset($theme['keyword'])) {
|
||||
$css .= ".token-keyword { color: {$theme['keyword']}; font-weight: bold; }\n";
|
||||
}
|
||||
if (isset($theme['comment'])) {
|
||||
$css .= ".token-comment { color: {$theme['comment']}; font-style: italic; }\n";
|
||||
}
|
||||
if (isset($theme['doc-comment'])) {
|
||||
$css .= ".token-doc-comment { color: {$theme['doc-comment']}; font-style: italic; }\n";
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
$css .= "</style>";
|
||||
|
||||
private function formatToken(Token $token, bool $useCssClasses): string {
|
||||
$escapedValue = htmlspecialchars($token->value);
|
||||
|
||||
if ($token->type === TokenType::WHITESPACE) {
|
||||
return $escapedValue;
|
||||
}
|
||||
|
||||
if ($useCssClasses) {
|
||||
return sprintf('<span class="%s">%s</span>',
|
||||
$token->type->getCssClass(), $escapedValue);
|
||||
} else {
|
||||
$color = $this->theme[$token->type->value] ?? $this->theme['identifier'];
|
||||
return sprintf('<span style="color: %s;">%s</span>',
|
||||
$color, $escapedValue);
|
||||
}
|
||||
}
|
||||
|
||||
private function wrapLine(string $content, int $lineNumber, bool $showLineNumbers, bool $isHighlighted): string {
|
||||
$lineClass = $isHighlighted ? 'line highlighted' : 'line';
|
||||
|
||||
if ($showLineNumbers) {
|
||||
$lineNumberSpan = sprintf(
|
||||
'<span class="line-number">%d</span>',
|
||||
$lineNumber
|
||||
);
|
||||
return sprintf('<div class="%s">%s<span class="code">%s</span></div>',
|
||||
$lineClass, $lineNumberSpan, $content);
|
||||
}
|
||||
|
||||
return sprintf('<div class="%s">%s</div>', $lineClass, $content);
|
||||
}
|
||||
|
||||
private function wrapOutput(string $content, array $options): string {
|
||||
$containerClass = $options['containerClass'] ?? 'syntax-highlighter';
|
||||
$language = $options['language'] ?? 'php';
|
||||
$copyButton = $options['copyButton'] ?? false;
|
||||
|
||||
$copyButtonHtml = $copyButton ?
|
||||
'<button class="copy-btn" onclick="navigator.clipboard.writeText(this.nextElementSibling.querySelector(\'code\').textContent)">Copy</button>' : '';
|
||||
|
||||
return sprintf(
|
||||
'<pre class="%s">%s<code class="language-%s">%s</code></pre>',
|
||||
$containerClass,
|
||||
$copyButtonHtml,
|
||||
$language,
|
||||
$content
|
||||
);
|
||||
return $css;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter;
|
||||
|
||||
use App\Framework\SyntaxHighlighter\Formatters\ConsoleFormatter;
|
||||
use App\Framework\SyntaxHighlighter\Formatters\FormatterInterface;
|
||||
use App\Framework\SyntaxHighlighter\Formatters\HtmlFormatter;
|
||||
use App\Framework\SyntaxHighlighter\Formatters\ConsoleFormatter;
|
||||
use App\Framework\Tokenizer\PhpTokenizer;
|
||||
use App\Framework\Tokenizer\ValueObjects\TokenCollection;
|
||||
|
||||
final readonly class SyntaxHighlighter {
|
||||
public Tokenizer $tokenizer;
|
||||
private array $formatters;
|
||||
/**
|
||||
* Modern syntax highlighter using the new tokenizer module
|
||||
*/
|
||||
final class SyntaxHighlighter
|
||||
{
|
||||
private readonly PhpTokenizer $tokenizer;
|
||||
|
||||
private array $formatters = [];
|
||||
|
||||
public function __construct(
|
||||
public TokenMapper $tokenMapper = new TokenMapper(),
|
||||
?Tokenizer $tokenizer = null
|
||||
?PhpTokenizer $tokenizer = null
|
||||
) {
|
||||
$this->tokenizer = $tokenizer ?? new Tokenizer($this->tokenMapper);
|
||||
$this->tokenizer = $tokenizer ?? new PhpTokenizer();
|
||||
$this->registerDefaultFormatters();
|
||||
}
|
||||
|
||||
private function registerDefaultFormatters(): void {
|
||||
$formatters['html'] = new HtmlFormatter();
|
||||
#$this->formatters['console'] = new ConsoleFormatter();
|
||||
|
||||
$this->formatters = $formatters;
|
||||
/**
|
||||
* Register default formatters
|
||||
*/
|
||||
private function registerDefaultFormatters(): void
|
||||
{
|
||||
$this->formatters['html'] = new HtmlFormatter();
|
||||
$this->formatters['console'] = new ConsoleFormatter();
|
||||
}
|
||||
|
||||
public function highlight(string $code, string $format = 'html', array $options = []): string {
|
||||
if (!isset($this->formatters[$format])) {
|
||||
throw new \InvalidArgumentException("Unbekannter Formatter: {$format}");
|
||||
/**
|
||||
* Highlight code with specified formatter
|
||||
*/
|
||||
public function highlight(string $code, string $format = 'html', array $options = []): string
|
||||
{
|
||||
if (! isset($this->formatters[$format])) {
|
||||
throw new \InvalidArgumentException("Unknown formatter: {$format}");
|
||||
}
|
||||
|
||||
$tokens = $this->tokenizer->tokenize($code);
|
||||
|
||||
return $this->formatters[$format]->format($tokens, $options);
|
||||
}
|
||||
|
||||
public function getFormatter(string $name): FormatterInterface {
|
||||
if (!isset($this->formatters[$name])) {
|
||||
throw new \InvalidArgumentException("Unbekannter Formatter: {$name}");
|
||||
/**
|
||||
* Get a specific formatter
|
||||
*/
|
||||
public function getFormatter(string $name): FormatterInterface
|
||||
{
|
||||
if (! isset($this->formatters[$name])) {
|
||||
throw new \InvalidArgumentException("Unknown formatter: {$name}");
|
||||
}
|
||||
|
||||
return $this->formatters[$name];
|
||||
}
|
||||
|
||||
public function tokenize(string $code): array {
|
||||
/**
|
||||
* Register a custom formatter
|
||||
*/
|
||||
public function registerFormatter(string $name, FormatterInterface $formatter): self
|
||||
{
|
||||
$this->formatters[$name] = $formatter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize code without formatting
|
||||
*/
|
||||
public function tokenize(string $code): TokenCollection
|
||||
{
|
||||
return $this->tokenizer->tokenize($code);
|
||||
}
|
||||
|
||||
public function addTokenMapping(int $phpToken, TokenType $tokenType): self {
|
||||
$this->tokenMapper->addMapping($phpToken, $tokenType);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCustomTokenMappings(array $mappings): self {
|
||||
$this->tokenMapper->mergeMappings($mappings);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function highlightWithCss(string $code, array $options = []): string {
|
||||
/**
|
||||
* Highlight with CSS included
|
||||
*/
|
||||
public function highlightWithCss(string $code, array $options = []): string
|
||||
{
|
||||
$options['includeCss'] = true;
|
||||
|
||||
return $this->highlight($code, 'html', $options);
|
||||
}
|
||||
|
||||
public function highlightWithoutCss(string $code, array $options = []): string {
|
||||
/**
|
||||
* Highlight without CSS
|
||||
*/
|
||||
public function highlightWithoutCss(string $code, array $options = []): string
|
||||
{
|
||||
$options['includeCss'] = false;
|
||||
|
||||
return $this->highlight($code, 'html', $options);
|
||||
}
|
||||
|
||||
public function getCss(array $options = []): string {
|
||||
/**
|
||||
* Get CSS for HTML formatter
|
||||
*/
|
||||
public function getCss(array $options = []): string
|
||||
{
|
||||
$formatter = $this->getFormatter('html');
|
||||
if (method_exists($formatter, 'getCss')) {
|
||||
return $formatter->getCss($options);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function resetCssOutput(): self {
|
||||
if (method_exists($this->getFormatter('html'), 'resetCssOutput')) {
|
||||
$this->getFormatter('html')::resetCssOutput();
|
||||
/**
|
||||
* Highlight with line offset
|
||||
*/
|
||||
public function highlightWithOffset(string $code, int $lineOffset, array $options = []): string
|
||||
{
|
||||
$options['lineOffset'] = $lineOffset;
|
||||
|
||||
return $this->highlight($code, 'html', $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight a file segment
|
||||
*/
|
||||
public function highlightFileSegment(string $code, int $startLineInFile, array $options = []): string
|
||||
{
|
||||
$options['lineOffset'] = $startLineInFile - 1;
|
||||
|
||||
return $this->highlight($code, 'html', $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream highlight for large files
|
||||
*/
|
||||
public function highlightStream(string $filePath, string $format = 'html', array $options = []): \Generator
|
||||
{
|
||||
if (! isset($this->formatters[$format])) {
|
||||
throw new \InvalidArgumentException("Unknown formatter: {$format}");
|
||||
}
|
||||
|
||||
$formatter = $this->formatters[$format];
|
||||
|
||||
foreach ($this->tokenizer->tokenizeStream($filePath) as $token) {
|
||||
yield $formatter->formatSingle($token, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset CSS output flag for HTML formatter (backward compatibility)
|
||||
*/
|
||||
public function resetCssOutput(): self
|
||||
{
|
||||
$formatter = $this->getFormatter('html');
|
||||
if (method_exists($formatter, 'resetCssOutput')) {
|
||||
$formatter::resetCssOutput();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function highlightWithOffset(string $code, int $lineOffset, array $options = []): string {
|
||||
$options['lineOffset'] = $lineOffset;
|
||||
return $this->highlight($code, 'html', $options);
|
||||
}
|
||||
|
||||
public function highlightFileSegment(string $code, int $startLineInFile, array $options = []): string {
|
||||
// startLineInFile ist die tatsächliche Zeilennummer in der Datei
|
||||
$options['lineOffset'] = $startLineInFile - 1;
|
||||
return $this->highlight($code, 'html', $options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter;
|
||||
|
||||
final readonly class Token {
|
||||
public function __construct(
|
||||
public TokenType $type,
|
||||
public string $value,
|
||||
public int $line = 1,
|
||||
public int $column = 1
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter;
|
||||
|
||||
final class TokenMapper {
|
||||
private array $mapping = [];
|
||||
|
||||
public function __construct() {
|
||||
$this->initializeDefaultMapping();
|
||||
}
|
||||
|
||||
private function initializeDefaultMapping(): void {
|
||||
$this->mapping = [
|
||||
// Schlüsselwörter
|
||||
T_ABSTRACT => TokenType::KEYWORD,
|
||||
T_ARRAY => TokenType::KEYWORD,
|
||||
T_AS => TokenType::KEYWORD,
|
||||
T_BREAK => TokenType::KEYWORD,
|
||||
T_CASE => TokenType::KEYWORD,
|
||||
T_CATCH => TokenType::KEYWORD,
|
||||
T_CLASS => TokenType::KEYWORD,
|
||||
T_CLONE => TokenType::KEYWORD,
|
||||
T_CONST => TokenType::KEYWORD,
|
||||
T_CONTINUE => TokenType::KEYWORD,
|
||||
T_DEFAULT => TokenType::KEYWORD,
|
||||
T_DO => TokenType::KEYWORD,
|
||||
T_ECHO => TokenType::KEYWORD,
|
||||
T_ELSE => TokenType::KEYWORD,
|
||||
T_ELSEIF => TokenType::KEYWORD,
|
||||
T_EMPTY => TokenType::KEYWORD,
|
||||
T_EXTENDS => TokenType::KEYWORD,
|
||||
T_FINAL => TokenType::KEYWORD,
|
||||
T_FINALLY => TokenType::KEYWORD,
|
||||
T_FOR => TokenType::KEYWORD,
|
||||
T_FOREACH => TokenType::KEYWORD,
|
||||
T_FUNCTION => TokenType::KEYWORD,
|
||||
T_IF => TokenType::KEYWORD,
|
||||
T_IMPLEMENTS => TokenType::KEYWORD,
|
||||
T_INCLUDE => TokenType::KEYWORD,
|
||||
T_INCLUDE_ONCE => TokenType::KEYWORD,
|
||||
T_INSTANCEOF => TokenType::KEYWORD,
|
||||
T_INTERFACE => TokenType::KEYWORD,
|
||||
T_ISSET => TokenType::KEYWORD,
|
||||
T_NAMESPACE => TokenType::KEYWORD,
|
||||
T_NEW => TokenType::KEYWORD,
|
||||
T_PRIVATE => TokenType::KEYWORD,
|
||||
T_PROTECTED => TokenType::KEYWORD,
|
||||
T_PUBLIC => TokenType::KEYWORD,
|
||||
T_REQUIRE => TokenType::KEYWORD,
|
||||
T_REQUIRE_ONCE => TokenType::KEYWORD,
|
||||
T_RETURN => TokenType::KEYWORD,
|
||||
T_STATIC => TokenType::KEYWORD,
|
||||
T_SWITCH => TokenType::KEYWORD,
|
||||
T_THROW => TokenType::KEYWORD,
|
||||
T_TRY => TokenType::KEYWORD,
|
||||
T_UNSET => TokenType::KEYWORD,
|
||||
T_USE => TokenType::KEYWORD,
|
||||
T_VAR => TokenType::KEYWORD,
|
||||
T_WHILE => TokenType::KEYWORD,
|
||||
T_YIELD => TokenType::KEYWORD,
|
||||
|
||||
// Variablen und Strings
|
||||
T_VARIABLE => TokenType::VARIABLE,
|
||||
T_CONSTANT_ENCAPSED_STRING => TokenType::STRING,
|
||||
T_ENCAPSED_AND_WHITESPACE => TokenType::STRING,
|
||||
T_STRING => TokenType::IDENTIFIER,
|
||||
|
||||
// Zahlen
|
||||
T_LNUMBER => TokenType::NUMBER,
|
||||
T_DNUMBER => TokenType::NUMBER,
|
||||
|
||||
// Kommentare
|
||||
T_COMMENT => TokenType::COMMENT,
|
||||
T_DOC_COMMENT => TokenType::DOC_COMMENT,
|
||||
|
||||
// PHP Tags
|
||||
T_OPEN_TAG => TokenType::PHP_TAG,
|
||||
T_OPEN_TAG_WITH_ECHO => TokenType::PHP_TAG,
|
||||
T_CLOSE_TAG => TokenType::PHP_TAG,
|
||||
|
||||
// Operatoren
|
||||
T_DOUBLE_ARROW => TokenType::OPERATOR,
|
||||
T_OBJECT_OPERATOR => TokenType::OPERATOR,
|
||||
T_PAAMAYIM_NEKUDOTAYIM => TokenType::OPERATOR,
|
||||
T_BOOLEAN_AND => TokenType::OPERATOR,
|
||||
T_BOOLEAN_OR => TokenType::OPERATOR,
|
||||
T_IS_EQUAL => TokenType::OPERATOR,
|
||||
T_IS_NOT_EQUAL => TokenType::OPERATOR,
|
||||
T_IS_IDENTICAL => TokenType::OPERATOR,
|
||||
T_IS_NOT_IDENTICAL => TokenType::OPERATOR,
|
||||
T_IS_SMALLER_OR_EQUAL => TokenType::OPERATOR,
|
||||
T_IS_GREATER_OR_EQUAL => TokenType::OPERATOR,
|
||||
T_SPACESHIP => TokenType::OPERATOR,
|
||||
T_COALESCE => TokenType::OPERATOR,
|
||||
|
||||
// Whitespace
|
||||
T_WHITESPACE => TokenType::WHITESPACE,
|
||||
];
|
||||
}
|
||||
|
||||
public function getTokenType(int $phpToken): TokenType {
|
||||
return $this->mapping[$phpToken] ?? TokenType::DEFAULT;
|
||||
}
|
||||
|
||||
public function addMapping(int $phpToken, TokenType $tokenType): self {
|
||||
$this->mapping[$phpToken] = $tokenType;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMapping(int $phpToken): self {
|
||||
unset($this->mapping[$phpToken]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasMapping(int $phpToken): bool {
|
||||
return isset($this->mapping[$phpToken]);
|
||||
}
|
||||
|
||||
public function getAllMappings(): array {
|
||||
return $this->mapping;
|
||||
}
|
||||
|
||||
public function setMappings(array $mappings): self {
|
||||
$this->mapping = $mappings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function mergeMappings(array $additionalMappings): self {
|
||||
$this->mapping = array_merge($this->mapping, $additionalMappings);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter;
|
||||
|
||||
enum TokenType: string {
|
||||
case KEYWORD = 'keyword';
|
||||
case VARIABLE = 'variable';
|
||||
case STRING = 'string';
|
||||
case IDENTIFIER = 'identifier';
|
||||
case NUMBER = 'number';
|
||||
case COMMENT = 'comment';
|
||||
case DOC_COMMENT = 'doc-comment';
|
||||
case PHP_TAG = 'php-tag';
|
||||
case OPERATOR = 'operator';
|
||||
case WHITESPACE = 'whitespace';
|
||||
case DEFAULT = 'default';
|
||||
|
||||
public function getCssClass(): string {
|
||||
return 'token-' . $this->value;
|
||||
}
|
||||
|
||||
public function getConsoleColor(): string {
|
||||
return match($this) {
|
||||
self::KEYWORD => "\033[34m", // Blau
|
||||
self::VARIABLE => "\033[35m", // Magenta
|
||||
self::STRING => "\033[32m", // Grün
|
||||
self::IDENTIFIER => "\033[37m", // Weiß
|
||||
self::NUMBER => "\033[31m", // Rot
|
||||
self::COMMENT => "\033[90m", // Grau
|
||||
self::DOC_COMMENT => "\033[36m", // Cyan
|
||||
self::PHP_TAG => "\033[94m", // Helles Blau
|
||||
self::OPERATOR => "\033[33m", // Gelb
|
||||
default => "\033[0m", // Reset
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\SyntaxHighlighter;
|
||||
|
||||
final readonly class Tokenizer {
|
||||
public function __construct(
|
||||
private TokenMapper $tokenMapper = new TokenMapper()
|
||||
) {}
|
||||
|
||||
public function tokenize(string $code): array {
|
||||
$phpTokens = token_get_all($code);
|
||||
$tokens = [];
|
||||
$line = 1;
|
||||
$column = 1;
|
||||
|
||||
foreach ($phpTokens as $phpToken) {
|
||||
if (is_array($phpToken)) {
|
||||
$tokenType = $this->tokenMapper->getTokenType($phpToken[0]);
|
||||
$value = $phpToken[1];
|
||||
$tokenLine = $phpToken[2] ?? $line;
|
||||
|
||||
$tokens[] = new Token($tokenType, $value, $tokenLine, $column);
|
||||
|
||||
// Zeilen- und Spaltenzählung aktualisieren
|
||||
$newlines = substr_count($value, "\n");
|
||||
if ($newlines > 0) {
|
||||
$line += $newlines;
|
||||
$column = strlen(substr($value, strrpos($value, "\n") + 1)) + 1;
|
||||
} else {
|
||||
$column += strlen($value);
|
||||
}
|
||||
} else {
|
||||
// Einzelne Zeichen
|
||||
$tokens[] = new Token(TokenType::DEFAULT, $phpToken, $line, $column);
|
||||
|
||||
if ($phpToken === "\n") {
|
||||
$line++;
|
||||
$column = 1;
|
||||
} else {
|
||||
$column++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user