Spaces:
Running
Running
File size: 22,343 Bytes
471be14 f6cda11 a9d4293 f6cda11 d5dc3fa f6cda11 16bb5ee f94ab6e 7ab3d84 fa73f36 f6cda11 d5dc3fa 16bb5ee d5dc3fa f94ab6e 7aa2446 16bb5ee 7aa2446 fa73f36 7aa2446 6fb9f70 5ecbc8d 6fb9f70 fa73f36 7aa2446 16bb5ee 5ecbc8d f94ab6e 5ecbc8d f94ab6e 5ecbc8d 6fb9f70 471be14 f94ab6e 471be14 a8c06d4 5ecbc8d f94ab6e a8c06d4 5ecbc8d a8c06d4 5ecbc8d a8c06d4 f94ab6e a8c06d4 5ecbc8d f94ab6e 5ecbc8d f94ab6e a8c06d4 f94ab6e a8c06d4 f94ab6e a8c06d4 f94ab6e 7ab3d84 a8c06d4 f94ab6e a8c06d4 9566c88 a8c06d4 471be14 efb00f7 471be14 a8c06d4 c5effb0 efb00f7 f94ab6e a8c06d4 471be14 f94ab6e c5effb0 a8c06d4 6fb9f70 c5effb0 a8c06d4 471be14 f94ab6e 471be14 fa73f36 f6cda11 727236c f6cda11 727236c 5ecbc8d f6cda11 727236c f94ab6e 16bb5ee 727236c 12df627 fa73f36 7aa2446 471be14 f94ab6e a8c06d4 fa73f36 a8c06d4 471be14 a8c06d4 fa73f36 7aa2446 f94ab6e 7aa2446 f6cda11 5ecbc8d fa73f36 f94ab6e a8c06d4 f6cda11 7aa2446 471be14 a8c06d4 471be14 f94ab6e a8c06d4 471be14 a8c06d4 f94ab6e 471be14 f94ab6e a8c06d4 471be14 a8c06d4 471be14 a8c06d4 f94ab6e a8c06d4 f94ab6e a8c06d4 471be14 f94ab6e a8c06d4 f94ab6e a8c06d4 f94ab6e a8c06d4 f94ab6e a8c06d4 f94ab6e 471be14 a8c06d4 f94ab6e a8c06d4 471be14 f6cda11 fa73f36 f6cda11 f94ab6e a9d4293 |
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 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
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__)
TEMP_DIR = os.path.join(os.getcwd(), "temp")
os.makedirs(TEMP_DIR, exist_ok=True)
# --- FORMAT PARSING AND DETECTION (Unchanged) ---
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
# --- HTML & PNG BUILDER (Unchanged but correct logic) ---
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')};
}}
/* ... other scoped styles ... */
{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
# --- API ENDPOINT for Conversion (CHANGED) ---
@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)
)
# Define options here to avoid repetition and add the required fix
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
# --- MAIN PAGE RENDERER (with corrected CSS) ---
@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; } /* <-- FIXED: Taller text area */
.selection-controls { margin: 15px 0; display: flex; gap: 10px; }
</style>
</head>
<body>
<h1>Intelligent Markdown Converter</h1>
<form id="main-form" onsubmit="return false;">
<!-- Input and Styling sections -->
<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>
// --- All JavaScript is unchanged from the previous correct version ---
// It correctly gathers style info without modifying the parent page.
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,
};
}
loadBtn.addEventListener('click', async () => { /* Logic unchanged */ });
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__":
# Ensure you have installed the required libraries:
# pip install Flask markdown imgkit pygments
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860))) |