660 lines
20 KiB
Python
660 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PowerON Markdown to HTML Converter
|
|
Konvertiert Markdown-Dateien zu HTML mit PowerON-Styling
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import argparse
|
|
from pathlib import Path
|
|
import re
|
|
|
|
# Einfache Markdown-zu-HTML Konvertierung ohne externe Abhängigkeiten
|
|
def _convert_markdown_to_html(markdown_text):
|
|
"""Einfache Markdown-zu-HTML Konvertierung ohne externe Bibliotheken"""
|
|
|
|
# HTML-Escaping
|
|
def escape_html(text):
|
|
return (text.replace('&', '&')
|
|
.replace('<', '<')
|
|
.replace('>', '>')
|
|
.replace('"', '"')
|
|
.replace("'", '''))
|
|
|
|
# Code-Blöcke zuerst verarbeiten (vor anderen Formatierungen)
|
|
def process_code_blocks(text):
|
|
# Fenced code blocks (```)
|
|
def replace_code_block(match):
|
|
language = match.group(1) or ''
|
|
code = match.group(2)
|
|
return f'<pre><code class="language-{language}">{escape_html(code)}</code></pre>'
|
|
|
|
text = re.sub(r'```(\w+)?\n(.*?)\n```', replace_code_block, text, flags=re.DOTALL)
|
|
|
|
# Inline code (`)
|
|
text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
|
|
|
|
return text
|
|
|
|
# Headers
|
|
def process_headers(text):
|
|
text = re.sub(r'^###### (.*?)$', r'<h6>\1</h6>', text, flags=re.MULTILINE)
|
|
text = re.sub(r'^##### (.*?)$', r'<h5>\1</h5>', text, flags=re.MULTILINE)
|
|
text = re.sub(r'^#### (.*?)$', r'<h4>\1</h4>', text, flags=re.MULTILINE)
|
|
text = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', text, flags=re.MULTILINE)
|
|
text = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', text, flags=re.MULTILINE)
|
|
text = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', text, flags=re.MULTILINE)
|
|
return text
|
|
|
|
# Listen
|
|
def process_lists(text):
|
|
lines = text.split('\n')
|
|
in_ul = False
|
|
in_ol = False
|
|
result = []
|
|
|
|
for line in lines:
|
|
# Ungeordnete Listen
|
|
if re.match(r'^\s*[-*+]\s+', line):
|
|
if in_ol:
|
|
result.append('</ol>')
|
|
in_ol = False
|
|
if not in_ul:
|
|
result.append('<ul>')
|
|
in_ul = True
|
|
item = re.sub(r'^\s*[-*+]\s+', '', line)
|
|
result.append(f'<li>{item}</li>')
|
|
# Geordnete Listen
|
|
elif re.match(r'^\s*\d+\.\s+', line):
|
|
if in_ul:
|
|
result.append('</ul>')
|
|
in_ul = False
|
|
if not in_ol:
|
|
result.append('<ol>')
|
|
in_ol = True
|
|
item = re.sub(r'^\s*\d+\.\s+', '', line)
|
|
result.append(f'<li>{item}</li>')
|
|
else:
|
|
if in_ul:
|
|
result.append('</ul>')
|
|
in_ul = False
|
|
if in_ol:
|
|
result.append('</ol>')
|
|
in_ol = False
|
|
result.append(line)
|
|
|
|
if in_ul:
|
|
result.append('</ul>')
|
|
if in_ol:
|
|
result.append('</ol>')
|
|
|
|
return '\n'.join(result)
|
|
|
|
# Links und Bilder
|
|
def process_links_and_images(text):
|
|
# Bilder 
|
|
text = re.sub(r'!\[([^\]]*)\]\(([^)]+)\)', r'<img src="\2" alt="\1">', text)
|
|
# Links [text](url)
|
|
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2">\1</a>', text)
|
|
return text
|
|
|
|
# Bold und Italic
|
|
def process_emphasis(text):
|
|
# Bold **text**
|
|
text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text)
|
|
# Italic *text*
|
|
text = re.sub(r'\*(.*?)\*', r'<em>\1</em>', text)
|
|
return text
|
|
|
|
# Blockquotes
|
|
def process_blockquotes(text):
|
|
lines = text.split('\n')
|
|
in_quote = False
|
|
result = []
|
|
|
|
for line in lines:
|
|
if line.strip().startswith('>'):
|
|
if not in_quote:
|
|
result.append('<blockquote>')
|
|
in_quote = True
|
|
quote_text = line.strip()[1:].strip()
|
|
result.append(f'<p>{quote_text}</p>')
|
|
else:
|
|
if in_quote:
|
|
result.append('</blockquote>')
|
|
in_quote = False
|
|
result.append(line)
|
|
|
|
if in_quote:
|
|
result.append('</blockquote>')
|
|
|
|
return '\n'.join(result)
|
|
|
|
# Tabellen
|
|
def process_tables(text):
|
|
lines = text.split('\n')
|
|
result = []
|
|
i = 0
|
|
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
|
|
# Prüfe ob es eine Tabellenzeile ist (enthält |)
|
|
if '|' in line and not line.startswith('<'):
|
|
table_lines = []
|
|
j = i
|
|
|
|
# Sammle alle aufeinanderfolgenden Tabellenzeilen
|
|
while j < len(lines) and '|' in lines[j].strip() and not lines[j].strip().startswith('<'):
|
|
table_lines.append(lines[j].strip())
|
|
j += 1
|
|
|
|
if len(table_lines) >= 2: # Mindestens Header + Separator
|
|
# Erstelle HTML-Tabelle
|
|
table_html = ['<table>']
|
|
|
|
# Header-Zeile
|
|
header_cells = [cell.strip() for cell in table_lines[0].split('|')[1:-1]]
|
|
table_html.append('<thead><tr>')
|
|
for cell in header_cells:
|
|
table_html.append(f'<th>{cell}</th>')
|
|
table_html.append('</tr></thead>')
|
|
|
|
# Separator-Zeile überspringen
|
|
if len(table_lines) > 1 and '---' in table_lines[1]:
|
|
data_start = 2
|
|
else:
|
|
data_start = 1
|
|
|
|
# Daten-Zeilen
|
|
if len(table_lines) > data_start:
|
|
table_html.append('<tbody>')
|
|
for row in table_lines[data_start:]:
|
|
if '|' in row:
|
|
data_cells = [cell.strip() for cell in row.split('|')[1:-1]]
|
|
table_html.append('<tr>')
|
|
for cell in data_cells:
|
|
table_html.append(f'<td>{cell}</td>')
|
|
table_html.append('</tr>')
|
|
table_html.append('</tbody>')
|
|
|
|
table_html.append('</table>')
|
|
result.append('\n'.join(table_html))
|
|
|
|
i = j - 1 # -1 weil i am Ende des Loops erhöht wird
|
|
else:
|
|
result.append(f'<p>{line}</p>')
|
|
else:
|
|
result.append(f'<p>{line}</p>')
|
|
|
|
i += 1
|
|
|
|
return '\n'.join(result)
|
|
|
|
# Horizontale Linien
|
|
def process_hr(text):
|
|
text = re.sub(r'^---$', '<hr>', text, flags=re.MULTILINE)
|
|
return text
|
|
|
|
# Paragraphen - Jeder Zeilenumbruch wird zu einem <p> Tag
|
|
def process_paragraphs(text):
|
|
lines = text.split('\n')
|
|
result = []
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
if line:
|
|
# Nur wenn es nicht schon ein HTML-Tag ist
|
|
if not re.match(r'<[h1-6]|<ul|<ol|<blockquote|<pre|<hr|<li|<table|<thead|<tbody|<tr|<th|<td', line):
|
|
result.append(f'<p>{line}</p>')
|
|
else:
|
|
result.append(line)
|
|
else:
|
|
# Leere Zeilen werden zu leeren <p> Tags
|
|
result.append('<p></p>')
|
|
|
|
return '\n'.join(result)
|
|
|
|
# Verarbeitung in der richtigen Reihenfolge
|
|
text = markdown_text
|
|
|
|
# Code-Blöcke zuerst (vor anderen Formatierungen)
|
|
text = process_code_blocks(text)
|
|
|
|
# Headers
|
|
text = process_headers(text)
|
|
|
|
# Tabellen
|
|
text = process_tables(text)
|
|
|
|
# Blockquotes
|
|
text = process_blockquotes(text)
|
|
|
|
# Listen
|
|
text = process_lists(text)
|
|
|
|
# Links und Bilder
|
|
text = process_links_and_images(text)
|
|
|
|
# Emphasis
|
|
text = process_emphasis(text)
|
|
|
|
# Horizontale Linien
|
|
text = process_hr(text)
|
|
|
|
# Paragraphen
|
|
text = process_paragraphs(text)
|
|
|
|
return text
|
|
|
|
class PowerONHTMLConverter:
|
|
def __init__(self, css_file="poweron-styles.css"):
|
|
self.css_file = css_file
|
|
|
|
def create_html_template(self, title, content, css_path=None):
|
|
"""Erstellt HTML-Template mit PowerON-Styling"""
|
|
if css_path is None:
|
|
css_path = self.css_file
|
|
|
|
html_template = f"""<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="icon" href="/poweron-favicon.png" type="image/png">
|
|
<title>{title} | PowerON</title>
|
|
<meta name="description" content="PowerON - KI für Unternehmen | {title}">
|
|
<meta name="author" content="PowerON">
|
|
|
|
<!-- Open Graph Meta Tags -->
|
|
<meta property="og:title" content="{title} | PowerON">
|
|
<meta property="og:description" content="PowerON - KI für Unternehmen | {title}">
|
|
<meta property="og:type" content="article">
|
|
|
|
<!-- Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400&display=swap" rel="stylesheet">
|
|
|
|
<!-- PowerON Styles -->
|
|
<link rel="stylesheet" href="{css_path}">
|
|
|
|
<!-- Inline CSS als Fallback -->
|
|
<style>
|
|
/* PowerON Base Styles - Fallback */
|
|
body {{
|
|
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
line-height: 1.6;
|
|
color: #1a1a1a;
|
|
background-color: #ffffff;
|
|
margin: 0;
|
|
padding: 0;
|
|
}}
|
|
|
|
.header {{
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 1000;
|
|
padding: 1rem 0;
|
|
}}
|
|
|
|
.navbar {{
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 0 1rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}}
|
|
|
|
.logo {{
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #1a1a1a;
|
|
text-decoration: none;
|
|
}}
|
|
|
|
.footer {{
|
|
background: #1a1a1a;
|
|
color: white;
|
|
padding: 2rem 0;
|
|
text-align: center;
|
|
margin-top: 4rem;
|
|
}}
|
|
|
|
.container {{
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 0 1rem;
|
|
}}
|
|
</style>
|
|
|
|
<!-- Additional Styles for Markdown Content -->
|
|
<style>
|
|
/* Markdown Content Styling */
|
|
.markdown-content {{
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
line-height: 1.7;
|
|
}}
|
|
|
|
.markdown-content h1 {{
|
|
border-bottom: 3px solid #4B73FF;
|
|
padding-bottom: 0.5rem;
|
|
margin-bottom: 2rem;
|
|
}}
|
|
|
|
.markdown-content h2 {{
|
|
border-bottom: 2px solid #e5e5e5;
|
|
padding-bottom: 0.3rem;
|
|
margin-top: 2.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}}
|
|
|
|
.markdown-content h3 {{
|
|
color: #4B73FF;
|
|
margin-top: 2rem;
|
|
margin-bottom: 1rem;
|
|
}}
|
|
|
|
.markdown-content h4 {{
|
|
color: #6b7280;
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.75rem;
|
|
}}
|
|
|
|
.markdown-content blockquote {{
|
|
border-left: 4px solid #4B73FF;
|
|
background: #f8fafc;
|
|
padding: 1rem 1.5rem;
|
|
margin: 1.5rem 0;
|
|
border-radius: 0 8px 8px 0;
|
|
font-style: italic;
|
|
}}
|
|
|
|
.markdown-content code {{
|
|
background: #f1f5f9;
|
|
padding: 0.2rem 0.4rem;
|
|
border-radius: 4px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.9em;
|
|
color: #e11d48;
|
|
}}
|
|
|
|
.markdown-content pre {{
|
|
background: #1e293b;
|
|
color: #e2e8f0;
|
|
padding: 1.5rem;
|
|
border-radius: 8px;
|
|
overflow-x: auto;
|
|
margin: 1.5rem 0;
|
|
}}
|
|
|
|
.markdown-content pre code {{
|
|
background: none;
|
|
padding: 0;
|
|
color: inherit;
|
|
font-size: 0.9em;
|
|
}}
|
|
|
|
.markdown-content table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 1.5rem 0;
|
|
background: white;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|
}}
|
|
|
|
.markdown-content th {{
|
|
background: #4B73FF;
|
|
color: white;
|
|
padding: 1rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
.markdown-content td {{
|
|
padding: 1rem;
|
|
border-bottom: 1px solid #e5e5e5;
|
|
}}
|
|
|
|
.markdown-content tr:hover {{
|
|
background: #f8fafc;
|
|
}}
|
|
|
|
.markdown-content ul, .markdown-content ol {{
|
|
margin: 1rem 0;
|
|
padding-left: 2rem;
|
|
}}
|
|
|
|
.markdown-content li {{
|
|
margin: 0.5rem 0;
|
|
}}
|
|
|
|
.markdown-content img {{
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: 8px;
|
|
margin: 1.5rem 0;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}}
|
|
|
|
.markdown-content a {{
|
|
color: #4B73FF;
|
|
text-decoration: none;
|
|
border-bottom: 1px solid transparent;
|
|
transition: all 0.3s ease;
|
|
}}
|
|
|
|
.markdown-content a:hover {{
|
|
border-bottom-color: #4B73FF;
|
|
}}
|
|
|
|
.markdown-content hr {{
|
|
border: none;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, transparent, #4B73FF, transparent);
|
|
margin: 3rem 0;
|
|
}}
|
|
|
|
/* Table of Contents */
|
|
.toc {{
|
|
background: #f8fafc;
|
|
border: 1px solid #e5e5e5;
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
margin: 2rem 0;
|
|
}}
|
|
|
|
.toc h2 {{
|
|
margin-top: 0;
|
|
color: #1a1a1a;
|
|
border-bottom: 2px solid #4B73FF;
|
|
padding-bottom: 0.5rem;
|
|
}}
|
|
|
|
.toc ul {{
|
|
list-style: none;
|
|
padding-left: 0;
|
|
}}
|
|
|
|
.toc li {{
|
|
margin: 0.5rem 0;
|
|
}}
|
|
|
|
.toc a {{
|
|
color: #6b7280;
|
|
text-decoration: none;
|
|
transition: color 0.3s ease;
|
|
}}
|
|
|
|
.toc a:hover {{
|
|
color: #4B73FF;
|
|
}}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {{
|
|
.markdown-content {{
|
|
padding: 1rem;
|
|
}}
|
|
|
|
.markdown-content h1 {{
|
|
font-size: 2.5rem;
|
|
}}
|
|
|
|
.markdown-content h2 {{
|
|
font-size: 2rem;
|
|
}}
|
|
|
|
.markdown-content h3 {{
|
|
font-size: 1.5rem;
|
|
}}
|
|
}}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<div class="navbar">
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main style="margin-top: 80px;">
|
|
<div class="markdown-content">
|
|
{content}
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="footer">
|
|
<div class="container">
|
|
<p>© 2024 PowerON - KI für Unternehmen. Alle Rechte vorbehalten.</p>
|
|
</div>
|
|
</footer>
|
|
</body>
|
|
</html>"""
|
|
|
|
return html_template
|
|
|
|
def convert_markdown_file(self, input_file, output_file=None, css_path=None):
|
|
"""Konvertiert eine Markdown-Datei zu HTML"""
|
|
input_path = Path(input_file)
|
|
|
|
if not input_path.exists():
|
|
raise FileNotFoundError(f"Markdown-Datei nicht gefunden: {input_file}")
|
|
|
|
# Output-Datei bestimmen
|
|
if output_file is None:
|
|
output_file = input_path.with_suffix('.html')
|
|
else:
|
|
output_file = Path(output_file)
|
|
|
|
# Markdown lesen und konvertieren
|
|
with open(input_path, 'r', encoding='utf-8') as f:
|
|
markdown_content = f.read()
|
|
|
|
# Titel aus Markdown extrahieren (erste H1)
|
|
title_match = re.search(r'^#\s+(.+)$', markdown_content, re.MULTILINE)
|
|
title = title_match.group(1) if title_match else input_path.stem
|
|
|
|
# Markdown zu HTML konvertieren
|
|
html_content = _convert_markdown_to_html(markdown_content)
|
|
|
|
# HTML-Template erstellen
|
|
full_html = self.create_html_template(title, html_content, css_path)
|
|
|
|
# HTML-Datei schreiben
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
f.write(full_html)
|
|
|
|
print(f"Konvertiert: {input_file} -> {output_file}")
|
|
return output_file
|
|
|
|
def convert_directory(self, input_dir, output_dir=None, css_path=None):
|
|
"""Konvertiert alle Markdown-Dateien in einem Verzeichnis"""
|
|
input_path = Path(input_dir)
|
|
|
|
if not input_path.exists():
|
|
raise FileNotFoundError(f"Verzeichnis nicht gefunden: {input_dir}")
|
|
|
|
if output_dir is None:
|
|
output_dir = input_path / "html_output"
|
|
else:
|
|
output_dir = Path(output_dir)
|
|
|
|
output_dir.mkdir(exist_ok=True)
|
|
|
|
# Alle .md Dateien finden
|
|
md_files = list(input_path.glob("**/*.md"))
|
|
|
|
if not md_files:
|
|
print(f"⚠️ Keine Markdown-Dateien gefunden in: {input_dir}")
|
|
return
|
|
|
|
print(f"📁 Konvertiere {len(md_files)} Markdown-Dateien...")
|
|
|
|
for md_file in md_files:
|
|
# Relativen Pfad beibehalten
|
|
relative_path = md_file.relative_to(input_path)
|
|
output_file = output_dir / relative_path.with_suffix('.html')
|
|
|
|
# Verzeichnis erstellen falls nötig
|
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
self.convert_markdown_file(md_file, output_file, css_path)
|
|
except Exception as e:
|
|
print(f"❌ Fehler bei {md_file}: {e}")
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="PowerON Markdown zu HTML Konverter",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Beispiele:
|
|
python md_to_html_converter.py document.md
|
|
python md_to_html_converter.py document.md -o output.html
|
|
python md_to_html_converter.py -d ./docs -o ./html_output
|
|
python md_to_html_converter.py document.md -c ../styles/custom.css
|
|
"""
|
|
)
|
|
|
|
parser.add_argument('input', nargs='?', help='Markdown-Datei oder Verzeichnis')
|
|
parser.add_argument('-o', '--output', help='Output-Datei oder -verzeichnis')
|
|
parser.add_argument('-d', '--directory', help='Verzeichnis mit Markdown-Dateien konvertieren')
|
|
parser.add_argument('-c', '--css', default='poweron-styles.css', help='CSS-Datei (Standard: poweron-styles.css)')
|
|
parser.add_argument('--version', action='version', version='PowerON MD to HTML Converter 1.0')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.input and not args.directory:
|
|
parser.print_help()
|
|
return
|
|
|
|
try:
|
|
converter = PowerONHTMLConverter(args.css)
|
|
|
|
if args.directory:
|
|
converter.convert_directory(args.directory, args.output, args.css)
|
|
else:
|
|
converter.convert_markdown_file(args.input, args.output, args.css)
|
|
|
|
print("Konvertierung abgeschlossen!")
|
|
|
|
except Exception as e:
|
|
print(f"Fehler: {e}")
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|