Spaces:
Sleeping
Sleeping
File size: 21,572 Bytes
028aa43 0cf906b 028aa43 0cf906b 028aa43 c458111 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
from flask import Flask, request, render_template_string, send_file, jsonify
import markdown
import imgkit
import os
import traceback
from io import BytesIO
import re
import base64
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
from pygments.styles import get_all_styles
app = Flask(__name__)
# Corrected line to use the universally writable /tmp directory
TEMP_DIR = "/tmp/markdown_temp"
os.makedirs(TEMP_DIR, exist_ok=True)
def parse_repo2markdown(text):
components = []
pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
first_match = pattern.search(text)
if first_match:
intro_text = text[:first_match.start()].strip()
if intro_text:
components.append({'type': 'intro', 'filename': 'Introduction', 'content': intro_text, 'is_code_block': False, 'language': ''})
for match in pattern.finditer(text):
filename = match.group(1).strip()
raw_content = match.group(2).strip()
code_match = re.search(r'^```(\w*)\s*\n([\s\S]*?)\s*```$', raw_content, re.DOTALL)
if code_match:
components.append({'type': 'file', 'filename': filename, 'content': code_match.group(2).strip(), 'is_code_block': True, 'language': code_match.group(1)})
else:
components.append({'type': 'file', 'filename': filename, 'content': raw_content, 'is_code_block': False, 'language': ''})
return components
def parse_standard_readme(text):
components = []
parts = re.split(r'^(## .*?)$', text, flags=re.MULTILINE)
intro_content = parts[0].strip()
if intro_content:
components.append({'type': 'intro', 'filename': 'Introduction', 'content': intro_content})
for i in range(1, len(parts), 2):
components.append({'type': 'section', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
return components
def parse_changelog(text):
components = []
parts = re.split(r'^(## \[\d+\.\d+\.\d+.*?\].*?)$', text, flags=re.MULTILINE)
intro_content = parts[0].strip()
if intro_content:
components.append({'type': 'intro', 'filename': 'Changelog Header', 'content': intro_content})
for i in range(1, len(parts), 2):
components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
return components
@app.route('/parse', methods=['POST'])
def parse_endpoint():
text = request.form.get('markdown_text', '')
if 'markdown_file' in request.files and request.files['markdown_file'].filename != '':
text = request.files['markdown_file'].read().decode('utf-8')
if not text: return jsonify({'error': 'No text or file provided.'}), 400
try:
if "## File Structure" in text and "### File:" in text:
format_name, components = "Repo2Markdown", parse_repo2markdown(text)
elif re.search(r'^## \[\d+\.\d+\.\d+.*?\].*?$', text, flags=re.MULTILINE):
format_name, components = "Changelog", parse_changelog(text)
elif text.strip().startswith("#") and re.search(r'^## ', text, flags=re.MULTILINE):
format_name, components = "Standard README", parse_standard_readme(text)
else:
format_name, components = "Unknown", [{'type': 'text', 'filename': 'Full Text', 'content': text}]
return jsonify({'format': format_name, 'components': components})
except Exception as e:
return jsonify({'error': f'Failed to parse: {e}'}), 500
def build_full_html(markdown_text, styles, include_fontawesome):
wrapper_id = "#output-wrapper"
font_family = styles.get('font_family', "'Arial', sans-serif")
google_font_name = font_family.split(',')[0].strip("'\"")
google_font_link = ""
if " " in google_font_name and google_font_name not in ["Times New Roman", "Courier New"]:
google_font_link = f'<link href="https://fonts.googleapis.com/css2?family={google_font_name.replace(" ", "+")}:wght@400;700&display=swap" rel="stylesheet">'
highlight_theme = styles.get('highlight_theme', 'default')
pygments_css = ""
if highlight_theme != 'none':
formatter = HtmlFormatter(style=highlight_theme, cssclass="codehilite")
pygments_css = formatter.get_style_defs(f' {wrapper_id}')
scoped_css = f"""
{wrapper_id} {{
font-family: {font_family}; font-size: {styles.get('font_size', '16')}px;
color: {styles.get('text_color', '#333')}; background-color: {styles.get('background_color', '#fff')};
}}
{wrapper_id} table {{ border-collapse: collapse; width: 100%; }}
{wrapper_id} th, {wrapper_id} td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
{wrapper_id} th {{ background-color: #f2f2f2; }}
{wrapper_id} img {{ max-width: 100%; height: auto; }}
{wrapper_id} pre {{ padding: {styles.get('code_padding', '15')}px; border-radius: 5px; white-space: pre-wrap; word-wrap: break-word; }}
{wrapper_id} h1, {wrapper_id} h2, {wrapper_id} h3 {{ border-bottom: 1px solid #eee; padding-bottom: 5px; margin-top: 1.5em; }}
{wrapper_id} :not(pre) > code {{ font-family: 'Courier New', monospace; background-color: #eef; padding: .2em .4em; border-radius: 3px; }}
{pygments_css} {styles.get('custom_css', '')}
"""
md_extensions = ['fenced_code', 'tables', 'codehilite']
html_content = markdown.markdown(markdown_text, extensions=md_extensions, extension_configs={'codehilite': {'css_class': 'codehilite'}})
final_html_body = f'<div id="output-wrapper">{html_content}</div>'
fontawesome_link = '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">' if include_fontawesome else ""
full_html = f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8">{google_font_link}{fontawesome_link}<style>
#ouput-wrapper {{ background-color: {styles.get('background_color', '#fff')}; padding: 25px; display: inline-block;}}
{scoped_css}
</style></head><body>{final_html_body}</body></html>"""
return full_html
@app.route('/convert', methods=['POST'])
def convert_endpoint():
data = request.json
try:
full_html = build_full_html(
markdown_text=data.get('markdown_text', ''),
styles=data.get('styles', {}),
include_fontawesome=data.get('include_fontawesome', False)
)
options = {"quiet": "", 'encoding': "UTF-8", "--no-cache": ""}
if data.get('download', False):
download_type = data.get('download_type', 'png')
if download_type == 'html':
return send_file(BytesIO(full_html.encode("utf-8")), as_attachment=True, download_name="output.html", mimetype="text/html")
else:
png_bytes = imgkit.from_string(full_html, False, options=options)
return send_file(BytesIO(png_bytes), as_attachment=True, download_name="output.png", mimetype="image/png")
else:
png_bytes = imgkit.from_string(full_html, False, options=options)
png_base64 = base64.b64encode(png_bytes).decode('utf-8')
return jsonify({'preview_html': full_html, 'preview_png_base64': png_base64})
except Exception as e:
traceback.print_exc()
return jsonify({'error': f'Failed to convert content: {str(e)}'}), 500
@app.route('/')
def index():
highlight_styles = sorted(list(get_all_styles()))
return render_template_string("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Intelligent Markdown Converter</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f9f9f9; }
h1, h2 { text-align: center; color: #333; }
form { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
textarea { width: 100%; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; padding: 10px; font-family: monospace; }
fieldset { border: 1px solid #ddd; padding: 15px; border-radius: 5px; margin-top: 20px; }
legend { font-weight: bold; color: #555; padding: 0 10px; }
select, input[type="number"], input[type="color"] { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;}
button { padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; }
.action-btn { background-color: #007BFF; color: white; font-size: 16px; padding: 12px 20px;}
.generate-btn { background-color: #5a32a3; color: white; font-size: 16px; padding: 12px 20px; }
.download-btn { background-color: #28a745; color: white; display: none; }
.main-actions { display: flex; flex-wrap: wrap; gap: 15px; align-items: center; margin-top: 20px; }
.preview-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 15px; }
.preview-container { border: 1px solid #ddd; padding: 10px; margin-top: 20px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); min-height: 100px; }
.preview-container img { max-width: 100%; }
.error { color: #D8000C; background-color: #FFD2D2; padding: 10px; border-radius: 5px; margin-top: 15px; display: none; }
.info { color: #00529B; background-color: #BDE5F8; padding: 10px; border-radius: 5px; margin: 10px 0; display: none;}
.style-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 15px; align-items: end; }
.component-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
.component-container { border: 1px solid #e0e0e0; border-radius: 5px; background: #fafafa; }
.component-header { background: #f1f1f1; padding: 8px 12px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 10px; }
.component-content textarea { height: 150px; }
.selection-controls { margin: 15px 0; display: flex; gap: 10px; }
</style>
</head>
<body>
<h1>Intelligent Markdown Converter</h1>
<form id="main-form" onsubmit="return false;">
<fieldset><legend>1. Load Content</legend><div id="info-box" class="info"></div><textarea id="markdown-text-input" name="markdown_text" rows="8"></textarea><div style="margin-top: 10px; display: flex; align-items: center; gap: 10px;"><label for="markdown-file-input">Or upload a file:</label><input type="file" id="markdown-file-input" name="markdown_file" accept=".md,.txt,text/markdown"></div><div style="margin-top: 15px;"><button type="button" id="load-btn" class="action-btn">Load & Analyze</button></div></fieldset>
<fieldset id="components-fieldset" style="display:none;"><legend>2. Select Components</legend><div class="selection-controls"><button type="button" onclick="toggleAllComponents(true)">Select All</button><button type="button" onclick="toggleAllComponents(false)">Deselect All</button></div><div id="components-container" class="component-grid"></div></fieldset>
<fieldset><legend>3. Configure Styles</legend><div class="style-grid"><div><label>Font Family:</label><select id="font_family"><optgroup label="Sans-Serif"><option value="'Arial', sans-serif">Arial</option><option value="'Roboto', sans-serif">Roboto</option></optgroup><optgroup label="Serif"><option value="'Times New Roman', serif">Times New Roman</option><option value="'Georgia', serif">Georgia</option></optgroup></select></div><div><label>Font Size (px):</label><input type="number" id="font_size" value="16"></div><div><label>Highlight Theme:</label><select id="highlight_theme"><option value="none">None</option>{% for style in highlight_styles %}<option value="{{ style }}" {% if style == 'default' %}selected{% endif %}>{{ style }}</option>{% endfor %}</select></div><div><label>Text Color:</label><input type="color" id="text_color" value="#333333"></div><div><label>Background Color:</label><input type="color" id="background_color" value="#ffffff"></div><div><label>Code Padding (px):</label><input type="number" id="code_padding" value="15"></div></div><div><input type="checkbox" id="include_fontawesome"><label for="include_fontawesome">Include Font Awesome</label></div><div><label for="custom_css">Custom CSS:</label><textarea id="custom_css" rows="3"></textarea></div></fieldset>
<div class="main-actions"><button type="button" id="generate-btn" class="generate-btn">Generate Preview</button></div>
</form>
<div id="error-box" class="error"></div>
<div id="preview-section" style="display:none;">
<h2>Preview</h2>
<div class="preview-header">
<h3>HTML Output</h3>
<button type="button" id="download-html-btn" class="download-btn">Download HTML</button>
</div>
<div id="html-preview-container" class="preview-container"></div>
<div class="preview-header" style="margin-top: 30px;">
<h3>PNG Output</h3>
<button type="button" id="download-png-btn" class="download-btn">Download PNG</button>
</div>
<div id="png-preview-container" class="preview-container"></div>
</div>
<script>
const loadBtn = document.getElementById('load-btn'), generateBtn = document.getElementById('generate-btn'),
downloadHtmlBtn = document.getElementById('download-html-btn'), downloadPngBtn = document.getElementById('download-png-btn'),
markdownTextInput = document.getElementById('markdown-text-input'), markdownFileInput = document.getElementById('markdown-file-input'),
componentsFieldset = document.getElementById('components-fieldset'), componentsContainer = document.getElementById('components-container'),
previewSection = document.getElementById('preview-section'), htmlPreviewContainer = document.getElementById('html-preview-container'),
pngPreviewContainer = document.getElementById('png-preview-container'), errorBox = document.getElementById('error-box'),
infoBox = document.getElementById('info-box');
function toggleAllComponents(checked) { componentsContainer.querySelectorAll('.component-checkbox').forEach(cb => cb.checked = checked); }
function displayError(message) { errorBox.textContent = message; errorBox.style.display = 'block'; previewSection.style.display = 'none'; }
function buildPayload() {
let finalMarkdown = "";
if (componentsFieldset.style.display === 'block') {
const parts = [];
componentsContainer.querySelectorAll('.component-container').forEach(div => {
if (div.querySelector('.component-checkbox').checked) { parts.push(div.dataset.reconstructed || div.dataset.content); }
});
finalMarkdown = parts.join('\\n\\n---\\n\\n');
} else { finalMarkdown = markdownTextInput.value; }
return {
markdown_text: finalMarkdown,
styles: {
font_family: document.getElementById('font_family').value, font_size: document.getElementById('font_size').value,
text_color: document.getElementById('text_color').value, background_color: document.getElementById('background_color').value,
code_padding: document.getElementById('code_padding').value, highlight_theme: document.getElementById('highlight_theme').value,
custom_css: document.getElementById('custom_css').value
},
include_fontawesome: document.getElementById('include_fontawesome').checked,
};
}
generateBtn.addEventListener('click', async () => {
generateBtn.textContent = 'Generating...'; generateBtn.disabled = true; errorBox.style.display = 'none';
const payload = buildPayload();
payload.download = false;
try {
const response = await fetch('/convert', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
const result = await response.json();
if (!response.ok) throw new Error(result.error || `Server error ${response.status}`);
previewSection.style.display = 'block';
htmlPreviewContainer.innerHTML = result.preview_html;
pngPreviewContainer.innerHTML = `<img src="data:image/png;base64,${result.preview_png_base64}" alt="PNG Preview">`;
downloadHtmlBtn.style.display = 'inline-block'; downloadPngBtn.style.display = 'inline-block';
} catch (err) { displayError('Error generating preview: ' + err.message); }
finally { generateBtn.textContent = 'Generate Preview'; generateBtn.disabled = false; }
});
async function handleDownload(fileType) {
const button = fileType === 'html' ? downloadHtmlBtn : downloadPngBtn;
button.textContent = 'Preparing...'; const payload = buildPayload();
payload.download = true; payload.download_type = fileType;
try {
const response = await fetch('/convert', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!response.ok) throw new Error(`Download failed: ${response.statusText}`);
const blob = await response.blob(); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = 'output.' + fileType;
document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); a.remove();
} catch (err) { displayError('Error preparing download: ' + err.message); }
finally { button.textContent = `Download ${fileType.toUpperCase()}`; }
}
downloadHtmlBtn.addEventListener('click', () => handleDownload('html'));
downloadPngBtn.addEventListener('click', () => handleDownload('png'));
loadBtn.addEventListener('click', async () => {
loadBtn.textContent = 'Loading...'; loadBtn.disabled = true; errorBox.style.display = 'none'; infoBox.style.display = 'none'; componentsFieldset.style.display = 'none'; componentsContainer.innerHTML = '';
const formData = new FormData();
if (markdownFileInput.files.length > 0) { formData.append('markdown_file', markdownFileInput.files[0]); } else { formData.append('markdown_text', markdownTextInput.value); }
try {
const response = await fetch('/parse', { method: 'POST', body: formData });
const result = await response.json();
if (!response.ok) throw new Error(result.error || `Server error`);
infoBox.innerHTML = `Detected Format: <strong>${result.format}</strong>`; infoBox.style.display = 'block';
if (result.format !== 'Unknown') {
componentsFieldset.style.display = 'block';
result.components.forEach((comp, index) => {
const div = document.createElement('div'); div.className = 'component-container'; div.dataset.type = comp.type; div.dataset.filename = comp.filename; div.dataset.content = comp.content;
let reconstructedContent = comp.content;
if (comp.is_code_block) { div.dataset.isCodeBlock = 'true'; div.dataset.language = comp.language || ''; reconstructedContent = "```" + (comp.language || '') + "\\n" + comp.content + "\\n```"; }
if (comp.type === 'section') div.dataset.reconstructed = `## ${comp.filename}\\n${comp.content}`; if (comp.type === 'version') div.dataset.reconstructed = `## ${comp.filename}\\n${comp.content}`;
div.innerHTML = `<div class="component-header"><input type="checkbox" id="comp-check-${index}" class="component-checkbox" checked><label for="comp-check-${index}">${comp.filename}</label></div><div class="component-content"><textarea readonly>${comp.content}</textarea></div>`;
componentsContainer.appendChild(div);
});
}
if(markdownFileInput.files.length > 0) { markdownTextInput.value = await markdownFileInput.files[0].text(); }
} catch (err) { displayError('Error parsing content: ' + err.message); } finally { loadBtn.textContent = 'Load & Analyze'; loadBtn.disabled = false; }
});
</script>
</body>
</html>
""", highlight_styles=highlight_styles)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860) |