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:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Markdown;
|
||||
@@ -7,40 +8,179 @@ final readonly class MarkdownConverter
|
||||
{
|
||||
public function toHtml(string $markdown): string
|
||||
{
|
||||
// Einfacher Markdown-zu-HTML Converter
|
||||
$html = $markdown;
|
||||
$lines = explode("\n", $markdown);
|
||||
$html = [];
|
||||
$inCodeBlock = false;
|
||||
$inList = false;
|
||||
$listType = null;
|
||||
$currentParagraph = [];
|
||||
|
||||
// Headers
|
||||
$html = preg_replace('/^# (.+)$/m', '<h1>$1</h1>', $html);
|
||||
$html = preg_replace('/^## (.+)$/m', '<h2>$1</h2>', $html);
|
||||
$html = preg_replace('/^### (.+)$/m', '<h3>$1</h3>', $html);
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
|
||||
// Bold und Italic
|
||||
$html = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $html);
|
||||
$html = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $html);
|
||||
// Code blocks
|
||||
if (str_starts_with($trimmed, '```')) {
|
||||
if ($inCodeBlock) {
|
||||
$html[] = '</code></pre>';
|
||||
$inCodeBlock = false;
|
||||
} else {
|
||||
$this->flushParagraph($currentParagraph, $html);
|
||||
$this->closeList($inList, $html);
|
||||
|
||||
// Code Blöcke
|
||||
$html = preg_replace('/```(\w+)?\n(.*?)\n```/s', '<pre><code class="language-$1">$2</code></pre>', $html);
|
||||
$html = preg_replace('/`(.+?)`/', '<code>$1</code>', $html);
|
||||
$language = substr($trimmed, 3);
|
||||
$langClass = $language ? " class=\"language-{$language}\"" : '';
|
||||
$html[] = "<pre><code{$langClass}>";
|
||||
$inCodeBlock = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($inCodeBlock) {
|
||||
$html[] = htmlspecialchars($line);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Empty line - close current paragraph/list
|
||||
if ($trimmed === '') {
|
||||
$this->flushParagraph($currentParagraph, $html);
|
||||
$this->closeList($inList, $html);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Headers
|
||||
if (preg_match('/^(#{1,6})\s+(.+)$/', $trimmed, $matches)) {
|
||||
$this->flushParagraph($currentParagraph, $html);
|
||||
$this->closeList($inList, $html);
|
||||
|
||||
$level = strlen($matches[1]);
|
||||
$text = $this->parseInlineElements($matches[2]);
|
||||
$html[] = "<h{$level}>{$text}</h{$level}>";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lists
|
||||
if (preg_match('/^[\-\*\+]\s+(.+)$/', $trimmed, $matches)) {
|
||||
$this->flushParagraph($currentParagraph, $html);
|
||||
$this->handleList($inList, $listType, 'ul', $html);
|
||||
|
||||
$text = $this->parseInlineElements($matches[1]);
|
||||
$html[] = "<li>{$text}</li>";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^\d+\.\s+(.+)$/', $trimmed, $matches)) {
|
||||
$this->flushParagraph($currentParagraph, $html);
|
||||
$this->handleList($inList, $listType, 'ol', $html);
|
||||
|
||||
$text = $this->parseInlineElements($matches[1]);
|
||||
$html[] = "<li>{$text}</li>";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquotes
|
||||
if (str_starts_with($trimmed, '> ')) {
|
||||
$this->flushParagraph($currentParagraph, $html);
|
||||
$this->closeList($inList, $html);
|
||||
|
||||
$text = $this->parseInlineElements(substr($trimmed, 2));
|
||||
$html[] = "<blockquote>{$text}</blockquote>";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rules
|
||||
if (preg_match('/^[\-\*_]{3,}$/', $trimmed)) {
|
||||
$this->flushParagraph($currentParagraph, $html);
|
||||
$this->closeList($inList, $html);
|
||||
$html[] = '<hr>';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular paragraph text
|
||||
$this->closeList($inList, $html);
|
||||
$currentParagraph[] = $this->parseInlineElements($trimmed);
|
||||
}
|
||||
|
||||
// Flush remaining content
|
||||
$this->flushParagraph($currentParagraph, $html);
|
||||
$this->closeList($inList, $html);
|
||||
|
||||
return implode("\n", $html);
|
||||
}
|
||||
|
||||
private function parseInlineElements(string $text): string
|
||||
{
|
||||
// Code spans (before other formatting)
|
||||
$text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
|
||||
|
||||
// Bold and italic
|
||||
$text = preg_replace('/\*\*\*(.+?)\*\*\*/', '<strong><em>$1</em></strong>', $text);
|
||||
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
|
||||
$text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
|
||||
|
||||
// Alternative bold/italic syntax
|
||||
$text = preg_replace('/___(.+?)___/', '<strong><em>$1</em></strong>', $text);
|
||||
$text = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $text);
|
||||
$text = preg_replace('/_(.+?)_/', '<em>$1</em>', $text);
|
||||
|
||||
// Links
|
||||
$html = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2">$1</a>', $html);
|
||||
$text = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $text);
|
||||
|
||||
// Listen
|
||||
$html = preg_replace('/^- (.+)$/m', '<li>$1</li>', $html);
|
||||
$html = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $html);
|
||||
// Images
|
||||
$text = preg_replace('/!\[([^\]]*)\]\(([^)]+)\)/', '<img src="$2" alt="$1">', $text);
|
||||
|
||||
// Blockquotes
|
||||
$html = preg_replace('/^> (.+)$/m', '<blockquote>$1</blockquote>', $html);
|
||||
return $text;
|
||||
}
|
||||
|
||||
// Paragraphen
|
||||
$html = preg_replace('/\n\n/', '</p><p>', $html);
|
||||
$html = '<p>' . $html . '</p>';
|
||||
private function flushParagraph(array &$currentParagraph, array &$html): void
|
||||
{
|
||||
if (! empty($currentParagraph)) {
|
||||
$text = implode(' ', $currentParagraph);
|
||||
$html[] = "<p>{$text}</p>";
|
||||
$currentParagraph = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Zeilenumbrüche
|
||||
$html = preg_replace('/\n/', '<br>', $html);
|
||||
private function handleList(bool &$inList, ?string &$listType, string $newType, array &$html): void
|
||||
{
|
||||
if (! $inList) {
|
||||
$html[] = "<{$newType}>";
|
||||
$inList = true;
|
||||
$listType = $newType;
|
||||
} elseif ($listType !== $newType) {
|
||||
$html[] = "</{$listType}>";
|
||||
$html[] = "<{$newType}>";
|
||||
$listType = $newType;
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
private function closeList(bool &$inList, array &$html): void
|
||||
{
|
||||
if ($inList) {
|
||||
$html[] = "</{$this->getListType($html)}>";
|
||||
$inList = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getListType(array $html): string
|
||||
{
|
||||
for ($i = count($html) - 1; $i >= 0; $i--) {
|
||||
if (str_starts_with($html[$i], '<ul>')) {
|
||||
return 'ul';
|
||||
}
|
||||
if (str_starts_with($html[$i], '<ol>')) {
|
||||
return 'ol';
|
||||
}
|
||||
}
|
||||
|
||||
return 'ul';
|
||||
}
|
||||
|
||||
public function stripMarkdown(string $markdown): string
|
||||
|
||||
84
src/Framework/Markdown/MarkdownRenderer.php
Normal file
84
src/Framework/Markdown/MarkdownRenderer.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Markdown;
|
||||
|
||||
/**
|
||||
* Complete Markdown renderer with styling and themes
|
||||
*/
|
||||
final readonly class MarkdownRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private MarkdownConverter $converter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function render(string $markdown, string $theme = 'default', array $options = []): string
|
||||
{
|
||||
$html = $this->converter->toHtml($markdown);
|
||||
|
||||
return $this->wrapWithLayout($html, $theme, $options);
|
||||
}
|
||||
|
||||
public function renderFile(string $filePath, string $theme = 'default', array $options = []): string
|
||||
{
|
||||
if (! file_exists($filePath)) {
|
||||
throw new \InvalidArgumentException("Markdown file not found: {$filePath}");
|
||||
}
|
||||
|
||||
$markdown = file_get_contents($filePath);
|
||||
|
||||
return $this->render($markdown, $theme, $options);
|
||||
}
|
||||
|
||||
public function renderToHtmlOnly(string $markdown): string
|
||||
{
|
||||
return $this->converter->toHtml($markdown);
|
||||
}
|
||||
|
||||
private function wrapWithLayout(string $content, string $theme, array $options): string
|
||||
{
|
||||
$markdownTheme = new MarkdownTheme($theme);
|
||||
$styles = $markdownTheme->getStyles();
|
||||
|
||||
$title = $options['title'] ?? 'Documentation';
|
||||
$viewport = $options['viewport'] ?? 'width=device-width, initial-scale=1.0';
|
||||
$extraStyles = $options['extraStyles'] ?? '';
|
||||
$extraScripts = $options['extraScripts'] ?? '';
|
||||
|
||||
$syntaxHighlighting = $options['syntaxHighlighting'] ?? true;
|
||||
$syntaxHighlightingScript = $syntaxHighlighting ? $this->getSyntaxHighlightingScript() : '';
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="{$viewport}">
|
||||
<title>{$title}</title>
|
||||
<style>{$styles}{$extraStyles}</style>
|
||||
{$syntaxHighlightingScript}
|
||||
</head>
|
||||
<body>
|
||||
<div class="markdown-content">
|
||||
{$content}
|
||||
</div>
|
||||
{$extraScripts}
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function getSyntaxHighlightingScript(): string
|
||||
{
|
||||
return <<<HTML
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-php.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
337
src/Framework/Markdown/MarkdownTheme.php
Normal file
337
src/Framework/Markdown/MarkdownTheme.php
Normal file
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Markdown;
|
||||
|
||||
/**
|
||||
* Theme system for Markdown rendering
|
||||
*/
|
||||
final readonly class MarkdownTheme
|
||||
{
|
||||
public function __construct(
|
||||
private string $name = 'default',
|
||||
private array $colors = [],
|
||||
private array $fonts = [],
|
||||
private array $spacing = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function getStyles(): string
|
||||
{
|
||||
return match ($this->name) {
|
||||
'github' => $this->getGitHubTheme(),
|
||||
'docs' => $this->getDocsTheme(),
|
||||
'api' => $this->getApiTheme(),
|
||||
default => $this->getDefaultTheme(),
|
||||
};
|
||||
}
|
||||
|
||||
private function getDefaultTheme(): string
|
||||
{
|
||||
return <<<CSS
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
h1 { font-size: 2em; border-bottom: 1px solid #eaecef; padding-bottom: 10px; }
|
||||
h2 { font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: 8px; }
|
||||
h3 { font-size: 1.25em; }
|
||||
h4 { font-size: 1em; }
|
||||
h5 { font-size: 0.875em; }
|
||||
h6 { font-size: 0.85em; color: #6a737d; }
|
||||
|
||||
p {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: 16px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f6f8fa;
|
||||
border-radius: 3px;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
padding: 0.2em 0.4em;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
display: inline;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #dfe2e5;
|
||||
margin: 0 0 16px 0;
|
||||
padding: 0 16px;
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #dfe2e5;
|
||||
padding: 6px 13px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0366d6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #e1e4e8;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
CSS;
|
||||
}
|
||||
|
||||
private function getGitHubTheme(): string
|
||||
{
|
||||
return $this->getDefaultTheme();
|
||||
}
|
||||
|
||||
private function getDocsTheme(): string
|
||||
{
|
||||
return <<<CSS
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
line-height: 1.7;
|
||||
color: #2d3748;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #1a202c;
|
||||
font-weight: 700;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
border-bottom: 3px solid #4299e1;
|
||||
padding-bottom: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.875em;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
h3 { font-size: 1.5em; color: #2d3748; }
|
||||
|
||||
code {
|
||||
background: #edf2f7;
|
||||
color: #e53e3e;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
overflow-x: auto;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #4299e1;
|
||||
background: #ebf8ff;
|
||||
margin: 24px 0;
|
||||
padding: 16px 24px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 16px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #4299e1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
CSS;
|
||||
}
|
||||
|
||||
private function getApiTheme(): string
|
||||
{
|
||||
return <<<CSS
|
||||
body {
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #2c3e50;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #ecf0f1;
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #34495e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2em;
|
||||
color: #2980b9;
|
||||
border-bottom: 3px solid #3498db;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.6em;
|
||||
color: #27ae60;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3em;
|
||||
color: #8e44ad;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f8f9fa;
|
||||
color: #e74c3c;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.method {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-right: 10px;
|
||||
font-size: 0.8em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.method.get { background: #27ae60; }
|
||||
.method.post { background: #f39c12; }
|
||||
.method.put { background: #e67e22; }
|
||||
.method.delete { background: #e74c3c; }
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #3498db;
|
||||
background: #ebf3fd;
|
||||
margin: 20px 0;
|
||||
padding: 15px 20px;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
CSS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user