| | from flask import Flask, request, render_template_string, send_file |
| | import markdown |
| | import imgkit |
| | import os |
| | import traceback |
| | from io import BytesIO |
| |
|
| | app = Flask(__name__) |
| |
|
| | |
| | |
| | TEMP_DIR = os.path.join(os.getcwd(), "temp") |
| |
|
| | |
| | try: |
| | os.makedirs(TEMP_DIR, exist_ok=True) |
| | except Exception as e: |
| | |
| | print(f"Error creating temp directory: {e}") |
| |
|
| | |
| | DEFAULT_STYLES = { |
| | "font_family": "'Arial', sans-serif", |
| | "font_size": "16", |
| | "text_color": "#333333", |
| | "background_color": "#ffffff", |
| | "code_bg_color": "#f4f4f4", |
| | "custom_css": "" |
| | } |
| |
|
| | @app.route("/", methods=["GET", "POST"]) |
| | def index(): |
| | """ |
| | Main route to handle form submission, Markdown processing, and rendering. |
| | """ |
| | preview_html = None |
| | download_available = False |
| | error_message = None |
| |
|
| | |
| | if request.method == "POST": |
| | markdown_text = request.form.get("markdown_text", "") |
| | download_type = request.form.get("download_type", "png") |
| |
|
| | |
| | styles = { |
| | "font_family": request.form.get("font_family", DEFAULT_STYLES["font_family"]), |
| | "font_size": request.form.get("font_size", DEFAULT_STYLES["font_size"]), |
| | "text_color": request.form.get("text_color", DEFAULT_STYLES["text_color"]), |
| | "background_color": request.form.get("background_color", DEFAULT_STYLES["background_color"]), |
| | "code_bg_color": request.form.get("code_bg_color", DEFAULT_STYLES["code_bg_color"]), |
| | "custom_css": request.form.get("custom_css", DEFAULT_STYLES["custom_css"]) |
| | } |
| | include_fontawesome = "include_fontawesome" in request.form |
| |
|
| | else: |
| | markdown_text = "" |
| | download_type = "png" |
| | styles = DEFAULT_STYLES.copy() |
| | include_fontawesome = False |
| |
|
| |
|
| | if request.method == "POST" and markdown_text: |
| | try: |
| | |
| | |
| | html_content = markdown.markdown(markdown_text, extensions=['fenced_code', 'tables']) |
| |
|
| | |
| | fontawesome_link = "" |
| | if include_fontawesome: |
| | fontawesome_link = '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">' |
| |
|
| | |
| | style_block = f""" |
| | <style> |
| | body {{ |
| | font-family: {styles['font_family']}; |
| | font-size: {styles['font_size']}px; |
| | color: {styles['text_color']}; |
| | background-color: {styles['background_color']}; |
| | padding: 25px; |
| | display: inline-block; /* Helps imgkit to crop correctly */ |
| | }} |
| | pre, code {{ |
| | background: {styles['code_bg_color']}; |
| | padding: 10px; |
| | border-radius: 5px; |
| | }} |
| | table {{ |
| | border-collapse: collapse; |
| | width: 100%; |
| | }} |
| | th, td {{ |
| | border: 1px solid #ddd; |
| | padding: 8px; |
| | text-align: left; |
| | }} |
| | th {{ |
| | background-color: #f2f2f2; |
| | }} |
| | img {{ |
| | max-width: 100%; |
| | height: auto; |
| | }} |
| | /* User-defined custom CSS */ |
| | {styles['custom_css']} |
| | </style> |
| | """ |
| |
|
| | |
| | full_html = f""" |
| | <!DOCTYPE html> |
| | <html> |
| | <head> |
| | <meta charset="UTF-8"> |
| | {fontawesome_link} |
| | {style_block} |
| | </head> |
| | <body> |
| | {html_content} |
| | </body> |
| | </html> |
| | """ |
| |
|
| | |
| | preview_html = full_html |
| | download_available = True |
| |
|
| | |
| | if "download" in request.form: |
| | 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_path = os.path.join(TEMP_DIR, "output.png") |
| | imgkit.from_string(full_html, png_path, options={"quiet": "", 'encoding': "UTF-8"}) |
| |
|
| | return send_file( |
| | png_path, |
| | as_attachment=True, |
| | download_name="output.png", |
| | mimetype="image/png" |
| | ) |
| |
|
| | except Exception as e: |
| | |
| | error_message = f"An error occurred: {str(e)}" |
| | print(f"Error: {traceback.format_exc()}") |
| |
|
| | |
| | return render_template_string(""" |
| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <title>Advanced Markdown to PNG/HTML 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 { text-align: center; color: #333; } |
| | form { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } |
| | textarea { width: 100%; height: 300px; margin-bottom: 10px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; padding: 10px; } |
| | .controls { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 20px; } |
| | .main-actions, .style-options { display: flex; flex-wrap: wrap; gap: 15px; align-items: center; } |
| | fieldset { border: 1px solid #ddd; padding: 15px; border-radius: 5px; margin-top: 20px; } |
| | legend { font-weight: bold; color: #555; } |
| | .style-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; } |
| | .style-grid > div { display: flex; flex-direction: column; } |
| | label { margin-bottom: 5px; color: #666; } |
| | select, input[type="number"], input[type="color"], input[type="text"] { padding: 8px; border: 1px solid #ccc; border-radius: 4px; } |
| | button { padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } |
| | .generate-btn { background-color: #007BFF; color: white; } |
| | .generate-btn:hover { background-color: #0056b3; } |
| | .download-btn { background-color: #28a745; color: white; } |
| | .download-btn:hover { background-color: #218838; } |
| | .preview { border: 1px solid #ddd; padding: 20px; margin-top: 20px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } |
| | .error { color: #D8000C; background-color: #FFD2D2; padding: 10px; border-radius: 5px; margin-top: 15px; } |
| | .info { color: #00529B; background-color: #BDE5F8; padding: 10px; border-radius: 5px; margin: 10px 0; } |
| | </style> |
| | </head> |
| | <body> |
| | <h1>Advanced Markdown to PNG/HTML Converter</h1> |
| | <form method="post"> |
| | <textarea name="markdown_text" placeholder="Paste your Markdown here...">{{ styles.get('markdown_text', '') }}</textarea> |
| | <div class="info"> |
| | <b>Tip:</b> To include images, use full public URLs (e.g., `https://.../image.png`). |
| | To use icons, check "Include Font Awesome" below and use tags like `<i class="fa-solid fa-star"></i>`. |
| | </div> |
| | |
| | <fieldset> |
| | <legend>Styling Options</legend> |
| | <div class="style-grid"> |
| | <div> |
| | <label for="font_family">Font Family:</label> |
| | <select id="font_family" name="font_family"> |
| | <option value="'Arial', sans-serif" {% if styles.font_family == "'Arial', sans-serif" %}selected{% endif %}>Arial</option> |
| | <option value="'Georgia', serif" {% if styles.font_family == "'Georgia', serif" %}selected{% endif %}>Georgia</option> |
| | <option value="'Times New Roman', serif" {% if styles.font_family == "'Times New Roman', serif" %}selected{% endif %}>Times New Roman</option> |
| | <option value="'Verdana', sans-serif" {% if styles.font_family == "'Verdana', sans-serif" %}selected{% endif %}>Verdana</option> |
| | <option value="'Courier New', monospace" {% if styles.font_family == "'Courier New', monospace" %}selected{% endif %}>Courier New</option> |
| | </select> |
| | </div> |
| | <div> |
| | <label for="font_size">Font Size (px):</label> |
| | <input type="number" id="font_size" name="font_size" value="{{ styles.font_size }}"> |
| | </div> |
| | <div> |
| | <label for="text_color">Text Color:</label> |
| | <input type="color" id="text_color" name="text_color" value="{{ styles.text_color }}"> |
| | </div> |
| | <div> |
| | <label for="background_color">Background Color:</label> |
| | <input type="color" id="background_color" name="background_color" value="{{ styles.background_color }}"> |
| | </div> |
| | <div> |
| | <label for="code_bg_color">Code BG Color:</label> |
| | <input type="color" id="code_bg_color" name="code_bg_color" value="{{ styles.code_bg_color }}"> |
| | </div> |
| | </div> |
| | <div> |
| | <input type="checkbox" id="include_fontawesome" name="include_fontawesome" {% if include_fontawesome %}checked{% endif %}> |
| | <label for="include_fontawesome">Include Font Awesome (for icons)</label> |
| | </div> |
| | <div> |
| | <label for="custom_css">Custom CSS:</label> |
| | <textarea id="custom_css" name="custom_css" rows="4" placeholder="e.g., h1 { color: blue; }">{{ styles.custom_css }}</textarea> |
| | </div> |
| | </fieldset> |
| | |
| | <div class="controls"> |
| | <div class="main-actions"> |
| | <button type="submit" class="generate-btn">Generate Preview</button> |
| | <div> |
| | <label for="download_type">Output format:</label> |
| | <select id="download_type" name="download_type"> |
| | <option value="png" {% if download_type == 'png' %}selected{% endif %}>PNG</option> |
| | <option value="html" {% if download_type == 'html' %}selected{% endif %}>HTML</option> |
| | </select> |
| | </div> |
| | {% if download_available %} |
| | <button type="submit" name="download" value="true" class="download-btn">Download {{ download_type.upper() }}</button> |
| | {% endif %} |
| | </div> |
| | </div> |
| | </form> |
| | |
| | {% if error_message %} |
| | <p class="error">{{ error_message }}</p> |
| | {% endif %} |
| | |
| | {% if preview_html %} |
| | <h2>Preview</h2> |
| | <div class="preview"> |
| | {{ preview_html | safe }} |
| | </div> |
| | {% endif %} |
| | </body> |
| | </html> |
| | """, |
| | styles=styles, |
| | markdown_text=markdown_text, |
| | download_type=download_type, |
| | include_fontawesome=include_fontawesome, |
| | download_available=download_available, |
| | preview_html=preview_html, |
| | error_message=error_message |
| | ) |
| |
|
| | if __name__ == "__main__": |
| | |
| | |
| | app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860))) |