chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\SyntaxHighlighter;
use SplFileObject;
final readonly class FileHighlighter
{
public function __invoke(string $filename, int $startLine, int $range):string
{
$file = new SplFileObject($filename);
// Zu Zeile 9 springen (Index basiert auf 0, daher Zeile 10 = 9)
$file->seek($startLine);
$lines = [];
for ($i = 0; $i < $range && !$file->eof(); $i++) {
$lines[] = $file->current();
$file->next();
}
$code = implode('', $lines);
$highlighter = new SyntaxHighlighter();
#echo "<style>$css</style>";
return $highlighter->highlightFileSegment($code, $startLine);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Framework\SyntaxHighlighter\Formatters;
use App\Framework\SyntaxHighlighter\Token;
use App\Framework\SyntaxHighlighter\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);
}
$lines = $this->groupTokensByLines($tokens);
$output = [];
$lineNumberWidth = strlen((string) (count($lines) + $startLine - 1));
foreach ($lines as $lineNumber => $lineTokens) {
$actualLineNumber = $lineNumber + $startLine - 1;
$lineContent = $this->formatLine($lineTokens);
if ($showLineNumbers) {
$lineNumberStr = str_pad($actualLineNumber, $lineNumberWidth, ' ', STR_PAD_LEFT);
$output[] = "\033[90m{$lineNumberStr}\033[0m │ {$lineContent}";
} else {
$output[] = $lineContent;
}
}
return implode("\n", $output);
}
/*private function isColorSupported(): bool {
return DIRECTORY_SEPARATOR === '/' &&
function_exists('posix_isatty') &&
posix_isatty(STDOUT);
}*/
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;
}
}
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 {
if ($token->type === TokenType::WHITESPACE) {
return $token->value;
}
$color = $token->type->getConsoleColor();
return $color . $token->value . self::RESET;
}
private function formatPlainText(array $tokens, bool $showLineNumbers, int $startLine): string {
$content = '';
$lineNumber = $startLine;
$atLineStart = 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;
}
}
return $content;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Framework\SyntaxHighlighter\Formatters;
interface FormatterInterface {
public function format(array $tokens, array $options = []): string;
}

View File

@@ -0,0 +1,326 @@
<?php
namespace App\Framework\SyntaxHighlighter\Formatters;
use App\Framework\SyntaxHighlighter\Token;
use App\Framework\SyntaxHighlighter\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',
];
private static bool $cssOutputted = false;
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
));
}
$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;
}
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;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}
.syntax-highlighter:hover .copy-btn {
opacity: 1;
}
.syntax-highlighter .copy-btn:hover {
background: rgba(0, 0, 0, 0.9);
}
CSS;
}
public static function resetCssOutput(): void {
self::$cssOutputted = false;
}
public function getCss(array $options = []): string {
return $this->generateCss($options);
}
private function groupTokensByLines(array $tokens): array {
$lines = [];
$currentLine = 1;
foreach ($tokens as $token) {
if (!isset($lines[$currentLine])) {
$lines[$currentLine] = [];
}
// Spezielle Behandlung für reine Newline-Token
if ($token->value === "\n") {
$currentLine++;
continue;
}
if (str_contains($token->value, "\n")) {
$parts = explode("\n", $token->value);
// 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;
}
}
return $lines;
}
private function formatLine(array $tokens, bool $useCssClasses): string {
$content = '';
foreach ($tokens as $token) {
$content .= $this->formatToken($token, $useCssClasses);
}
return $content;
}
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
);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Framework\SyntaxHighlighter;
use App\Framework\SyntaxHighlighter\Formatters\FormatterInterface;
use App\Framework\SyntaxHighlighter\Formatters\HtmlFormatter;
use App\Framework\SyntaxHighlighter\Formatters\ConsoleFormatter;
final readonly class SyntaxHighlighter {
public Tokenizer $tokenizer;
private array $formatters;
public function __construct(
public TokenMapper $tokenMapper = new TokenMapper(),
?Tokenizer $tokenizer = null
) {
$this->tokenizer = $tokenizer ?? new Tokenizer($this->tokenMapper);
$this->registerDefaultFormatters();
}
private function registerDefaultFormatters(): void {
$formatters['html'] = new HtmlFormatter();
#$this->formatters['console'] = new ConsoleFormatter();
$this->formatters = $formatters;
}
public function highlight(string $code, string $format = 'html', array $options = []): string {
if (!isset($this->formatters[$format])) {
throw new \InvalidArgumentException("Unbekannter 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}");
}
return $this->formatters[$name];
}
public function tokenize(string $code): array {
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 {
$options['includeCss'] = true;
return $this->highlight($code, 'html', $options);
}
public function highlightWithoutCss(string $code, array $options = []): string {
$options['includeCss'] = false;
return $this->highlight($code, 'html', $options);
}
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();
}
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);
}
}

View File

@@ -0,0 +1,13 @@
<?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
) {}
}

View File

@@ -0,0 +1,132 @@
<?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;
}
}

View File

@@ -0,0 +1,36 @@
<?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
};
}
}

View File

@@ -0,0 +1,48 @@
<?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;
}
}