chore: complete update
This commit is contained in:
326
src/Framework/SyntaxHighlighter/Formatters/HtmlFormatter.php
Normal file
326
src/Framework/SyntaxHighlighter/Formatters/HtmlFormatter.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user