Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,22 +1,22 @@
|
|
| 1 |
from flask import Flask, request, render_template_string, send_file, jsonify
|
| 2 |
import markdown
|
| 3 |
-
import imgkit
|
| 4 |
import os
|
| 5 |
import traceback
|
| 6 |
from io import BytesIO
|
| 7 |
import re
|
| 8 |
import base64
|
| 9 |
-
from pygments import highlight
|
| 10 |
from pygments.lexers import get_lexer_by_name
|
| 11 |
from pygments.formatters import HtmlFormatter
|
| 12 |
from pygments.styles import get_all_styles
|
|
|
|
|
|
|
| 13 |
|
| 14 |
app = Flask(__name__)
|
| 15 |
|
| 16 |
-
|
|
|
|
| 17 |
os.makedirs(TEMP_DIR, exist_ok=True)
|
| 18 |
|
| 19 |
-
# --- FORMAT PARSING AND DETECTION (Unchanged) ---
|
| 20 |
def parse_repo2markdown(text):
|
| 21 |
components = []
|
| 22 |
pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
|
|
@@ -55,7 +55,6 @@ def parse_changelog(text):
|
|
| 55 |
components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
|
| 56 |
return components
|
| 57 |
|
| 58 |
-
|
| 59 |
@app.route('/parse', methods=['POST'])
|
| 60 |
def parse_endpoint():
|
| 61 |
text = request.form.get('markdown_text', '')
|
|
@@ -76,7 +75,6 @@ def parse_endpoint():
|
|
| 76 |
except Exception as e:
|
| 77 |
return jsonify({'error': f'Failed to parse: {e}'}), 500
|
| 78 |
|
| 79 |
-
# --- HTML & PNG BUILDER (Unchanged but correct logic) ---
|
| 80 |
def build_full_html(markdown_text, styles, include_fontawesome):
|
| 81 |
wrapper_id = "#output-wrapper"
|
| 82 |
font_family = styles.get('font_family', "'Arial', sans-serif")
|
|
@@ -96,8 +94,6 @@ def build_full_html(markdown_text, styles, include_fontawesome):
|
|
| 96 |
font-family: {font_family}; font-size: {styles.get('font_size', '16')}px;
|
| 97 |
color: {styles.get('text_color', '#333')}; background-color: {styles.get('background_color', '#fff')};
|
| 98 |
}}
|
| 99 |
-
/* ... other scoped styles ... */
|
| 100 |
-
|
| 101 |
{wrapper_id} table {{ border-collapse: collapse; width: 100%; }}
|
| 102 |
{wrapper_id} th, {wrapper_id} td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
|
| 103 |
{wrapper_id} th {{ background-color: #f2f2f2; }}
|
|
@@ -116,13 +112,13 @@ def build_full_html(markdown_text, styles, include_fontawesome):
|
|
| 116 |
|
| 117 |
full_html = f"""<!DOCTYPE html>
|
| 118 |
<html><head><meta charset="UTF-8">{google_font_link}{fontawesome_link}<style>
|
| 119 |
-
|
|
|
|
| 120 |
{scoped_css}
|
| 121 |
</style></head><body>{final_html_body}</body></html>"""
|
| 122 |
|
| 123 |
return full_html
|
| 124 |
|
| 125 |
-
# --- API ENDPOINT for Conversion (CHANGED) ---
|
| 126 |
@app.route('/convert', methods=['POST'])
|
| 127 |
def convert_endpoint():
|
| 128 |
data = request.json
|
|
@@ -132,25 +128,34 @@ def convert_endpoint():
|
|
| 132 |
styles=data.get('styles', {}),
|
| 133 |
include_fontawesome=data.get('include_fontawesome', False)
|
| 134 |
)
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
if data.get('download', False):
|
| 139 |
download_type = data.get('download_type', 'png')
|
| 140 |
if download_type == 'html':
|
| 141 |
return send_file(BytesIO(full_html.encode("utf-8")), as_attachment=True, download_name="output.html", mimetype="text/html")
|
| 142 |
else:
|
| 143 |
-
png_bytes = imgkit.from_string(full_html, False, options=options)
|
| 144 |
return send_file(BytesIO(png_bytes), as_attachment=True, download_name="output.png", mimetype="image/png")
|
| 145 |
else:
|
| 146 |
-
png_bytes = imgkit.from_string(full_html, False, options=options)
|
| 147 |
png_base64 = base64.b64encode(png_bytes).decode('utf-8')
|
| 148 |
return jsonify({'preview_html': full_html, 'preview_png_base64': png_base64})
|
| 149 |
except Exception as e:
|
| 150 |
traceback.print_exc()
|
| 151 |
return jsonify({'error': f'Failed to convert content: {str(e)}'}), 500
|
| 152 |
|
| 153 |
-
# --- MAIN PAGE RENDERER (with corrected CSS) ---
|
| 154 |
@app.route('/')
|
| 155 |
def index():
|
| 156 |
highlight_styles = sorted(list(get_all_styles()))
|
|
@@ -182,22 +187,19 @@ def index():
|
|
| 182 |
.component-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
|
| 183 |
.component-container { border: 1px solid #e0e0e0; border-radius: 5px; background: #fafafa; }
|
| 184 |
.component-header { background: #f1f1f1; padding: 8px 12px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 10px; }
|
| 185 |
-
.component-content textarea { height: 150px; }
|
| 186 |
.selection-controls { margin: 15px 0; display: flex; gap: 10px; }
|
| 187 |
</style>
|
| 188 |
</head>
|
| 189 |
<body>
|
| 190 |
<h1>Intelligent Markdown Converter</h1>
|
| 191 |
<form id="main-form" onsubmit="return false;">
|
| 192 |
-
<!-- Input and Styling sections -->
|
| 193 |
<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>
|
| 194 |
<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>
|
| 195 |
<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>
|
| 196 |
<div class="main-actions"><button type="button" id="generate-btn" class="generate-btn">Generate Preview</button></div>
|
| 197 |
</form>
|
| 198 |
-
|
| 199 |
<div id="error-box" class="error"></div>
|
| 200 |
-
|
| 201 |
<div id="preview-section" style="display:none;">
|
| 202 |
<h2>Preview</h2>
|
| 203 |
<div class="preview-header">
|
|
@@ -212,8 +214,6 @@ def index():
|
|
| 212 |
<div id="png-preview-container" class="preview-container"></div>
|
| 213 |
</div>
|
| 214 |
<script>
|
| 215 |
-
// --- All JavaScript is unchanged from the previous correct version ---
|
| 216 |
-
// It correctly gathers style info without modifying the parent page.
|
| 217 |
const loadBtn = document.getElementById('load-btn'), generateBtn = document.getElementById('generate-btn'),
|
| 218 |
downloadHtmlBtn = document.getElementById('download-html-btn'), downloadPngBtn = document.getElementById('download-png-btn'),
|
| 219 |
markdownTextInput = document.getElementById('markdown-text-input'), markdownFileInput = document.getElementById('markdown-file-input'),
|
|
@@ -243,7 +243,6 @@ def index():
|
|
| 243 |
include_fontawesome: document.getElementById('include_fontawesome').checked,
|
| 244 |
};
|
| 245 |
}
|
| 246 |
-
loadBtn.addEventListener('click', async () => { /* Logic unchanged */ });
|
| 247 |
generateBtn.addEventListener('click', async () => {
|
| 248 |
generateBtn.textContent = 'Generating...'; generateBtn.disabled = true; errorBox.style.display = 'none';
|
| 249 |
const payload = buildPayload();
|
|
@@ -303,6 +302,4 @@ def index():
|
|
| 303 |
""", highlight_styles=highlight_styles)
|
| 304 |
|
| 305 |
if __name__ == "__main__":
|
| 306 |
-
|
| 307 |
-
# pip install Flask markdown imgkit pygments
|
| 308 |
-
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
|
|
|
|
| 1 |
from flask import Flask, request, render_template_string, send_file, jsonify
|
| 2 |
import markdown
|
|
|
|
| 3 |
import os
|
| 4 |
import traceback
|
| 5 |
from io import BytesIO
|
| 6 |
import re
|
| 7 |
import base64
|
|
|
|
| 8 |
from pygments.lexers import get_lexer_by_name
|
| 9 |
from pygments.formatters import HtmlFormatter
|
| 10 |
from pygments.styles import get_all_styles
|
| 11 |
+
# New import for the headless browser
|
| 12 |
+
from playwright.sync_api import sync_playwright
|
| 13 |
|
| 14 |
app = Flask(__name__)
|
| 15 |
|
| 16 |
+
# Corrected line to use the universally writable /tmp directory
|
| 17 |
+
TEMP_DIR = "/tmp/markdown_temp"
|
| 18 |
os.makedirs(TEMP_DIR, exist_ok=True)
|
| 19 |
|
|
|
|
| 20 |
def parse_repo2markdown(text):
|
| 21 |
components = []
|
| 22 |
pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
|
|
|
|
| 55 |
components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
|
| 56 |
return components
|
| 57 |
|
|
|
|
| 58 |
@app.route('/parse', methods=['POST'])
|
| 59 |
def parse_endpoint():
|
| 60 |
text = request.form.get('markdown_text', '')
|
|
|
|
| 75 |
except Exception as e:
|
| 76 |
return jsonify({'error': f'Failed to parse: {e}'}), 500
|
| 77 |
|
|
|
|
| 78 |
def build_full_html(markdown_text, styles, include_fontawesome):
|
| 79 |
wrapper_id = "#output-wrapper"
|
| 80 |
font_family = styles.get('font_family', "'Arial', sans-serif")
|
|
|
|
| 94 |
font-family: {font_family}; font-size: {styles.get('font_size', '16')}px;
|
| 95 |
color: {styles.get('text_color', '#333')}; background-color: {styles.get('background_color', '#fff')};
|
| 96 |
}}
|
|
|
|
|
|
|
| 97 |
{wrapper_id} table {{ border-collapse: collapse; width: 100%; }}
|
| 98 |
{wrapper_id} th, {wrapper_id} td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
|
| 99 |
{wrapper_id} th {{ background-color: #f2f2f2; }}
|
|
|
|
| 112 |
|
| 113 |
full_html = f"""<!DOCTYPE html>
|
| 114 |
<html><head><meta charset="UTF-8">{google_font_link}{fontawesome_link}<style>
|
| 115 |
+
body {{ background-color: transparent; margin: 0; padding: 0; }}
|
| 116 |
+
#output-wrapper {{ background-color: {styles.get('background_color', '#fff')}; padding: 25px; display: inline-block;}}
|
| 117 |
{scoped_css}
|
| 118 |
</style></head><body>{final_html_body}</body></html>"""
|
| 119 |
|
| 120 |
return full_html
|
| 121 |
|
|
|
|
| 122 |
@app.route('/convert', methods=['POST'])
|
| 123 |
def convert_endpoint():
|
| 124 |
data = request.json
|
|
|
|
| 128 |
styles=data.get('styles', {}),
|
| 129 |
include_fontawesome=data.get('include_fontawesome', False)
|
| 130 |
)
|
| 131 |
+
|
| 132 |
+
# --- REPLACEMENT FOR IMGKIT ---
|
| 133 |
+
with sync_playwright() as p:
|
| 134 |
+
browser = p.chromium.launch()
|
| 135 |
+
page = browser.new_page()
|
| 136 |
+
page.set_content(full_html)
|
| 137 |
+
|
| 138 |
+
# Locate the specific element we want to capture
|
| 139 |
+
element = page.locator("#output-wrapper")
|
| 140 |
+
|
| 141 |
+
# Take a screenshot of just that element
|
| 142 |
+
png_bytes = element.screenshot(type="png")
|
| 143 |
+
browser.close()
|
| 144 |
+
# --- END REPLACEMENT ---
|
| 145 |
+
|
| 146 |
if data.get('download', False):
|
| 147 |
download_type = data.get('download_type', 'png')
|
| 148 |
if download_type == 'html':
|
| 149 |
return send_file(BytesIO(full_html.encode("utf-8")), as_attachment=True, download_name="output.html", mimetype="text/html")
|
| 150 |
else:
|
|
|
|
| 151 |
return send_file(BytesIO(png_bytes), as_attachment=True, download_name="output.png", mimetype="image/png")
|
| 152 |
else:
|
|
|
|
| 153 |
png_base64 = base64.b64encode(png_bytes).decode('utf-8')
|
| 154 |
return jsonify({'preview_html': full_html, 'preview_png_base64': png_base64})
|
| 155 |
except Exception as e:
|
| 156 |
traceback.print_exc()
|
| 157 |
return jsonify({'error': f'Failed to convert content: {str(e)}'}), 500
|
| 158 |
|
|
|
|
| 159 |
@app.route('/')
|
| 160 |
def index():
|
| 161 |
highlight_styles = sorted(list(get_all_styles()))
|
|
|
|
| 187 |
.component-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
|
| 188 |
.component-container { border: 1px solid #e0e0e0; border-radius: 5px; background: #fafafa; }
|
| 189 |
.component-header { background: #f1f1f1; padding: 8px 12px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 10px; }
|
| 190 |
+
.component-content textarea { height: 150px; }
|
| 191 |
.selection-controls { margin: 15px 0; display: flex; gap: 10px; }
|
| 192 |
</style>
|
| 193 |
</head>
|
| 194 |
<body>
|
| 195 |
<h1>Intelligent Markdown Converter</h1>
|
| 196 |
<form id="main-form" onsubmit="return false;">
|
|
|
|
| 197 |
<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>
|
| 198 |
<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>
|
| 199 |
<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>
|
| 200 |
<div class="main-actions"><button type="button" id="generate-btn" class="generate-btn">Generate Preview</button></div>
|
| 201 |
</form>
|
|
|
|
| 202 |
<div id="error-box" class="error"></div>
|
|
|
|
| 203 |
<div id="preview-section" style="display:none;">
|
| 204 |
<h2>Preview</h2>
|
| 205 |
<div class="preview-header">
|
|
|
|
| 214 |
<div id="png-preview-container" class="preview-container"></div>
|
| 215 |
</div>
|
| 216 |
<script>
|
|
|
|
|
|
|
| 217 |
const loadBtn = document.getElementById('load-btn'), generateBtn = document.getElementById('generate-btn'),
|
| 218 |
downloadHtmlBtn = document.getElementById('download-html-btn'), downloadPngBtn = document.getElementById('download-png-btn'),
|
| 219 |
markdownTextInput = document.getElementById('markdown-text-input'), markdownFileInput = document.getElementById('markdown-file-input'),
|
|
|
|
| 243 |
include_fontawesome: document.getElementById('include_fontawesome').checked,
|
| 244 |
};
|
| 245 |
}
|
|
|
|
| 246 |
generateBtn.addEventListener('click', async () => {
|
| 247 |
generateBtn.textContent = 'Generating...'; generateBtn.disabled = true; errorBox.style.display = 'none';
|
| 248 |
const payload = buildPayload();
|
|
|
|
| 302 |
""", highlight_styles=highlight_styles)
|
| 303 |
|
| 304 |
if __name__ == "__main__":
|
| 305 |
+
app.run(host="0.0.0.0", port=7860)
|
|
|
|
|
|