from flask import Flask, request, render_template_string, send_file import markdown import imgkit import os import traceback from io import BytesIO import re app = Flask(__name__) # Configure a temporary directory for file operations TEMP_DIR = os.path.join(os.getcwd(), "temp") os.makedirs(TEMP_DIR, exist_ok=True) # Define default values for styling options DEFAULT_STYLES = { "font_family": "'Arial', sans-serif", "font_size": "16", "text_color": "#333333", "background_color": "#ffffff", "code_bg_color": "#f4f4f4", "code_padding": "15", "custom_css": "" } def parse_markdown(text): """ Parses markdown text to separate code blocks from base text. Returns a list of dictionaries, each representing a part of the document. """ # Regex to find fenced code blocks and capture them, including the fences and language # This regex is non-greedy `([\s\S]*?)` to handle multiple blocks correctly. parts = re.split(r'(```[\s\S]*?```)', text) components = [] code_block_counter = 0 for i, part in enumerate(parts): if not part: continue # Check if the part is a fenced code block if part.startswith('```'): # Extract language and content from the block # The first line is ` ```language `, the last line is ` ``` ` lines = part.strip().split('\n') language = lines[3:].strip() content = '\n'.join(lines[1:-1]) components.append({ 'type': 'code', 'id': code_block_counter, 'language': language or 'plaintext', 'content': content }) code_block_counter += 1 else: # This is a standard text part components.append({ 'type': 'text', 'id': i, 'content': part }) return components @app.route("/", methods=["GET", "POST"]) def index(): """Main route to handle file uploads, component selection, and conversion.""" # Initialize variables preview_html = None download_available = False error_message = None components = [] # Set default values on GET request markdown_text = "" download_type = "png" styles = DEFAULT_STYLES.copy() include_fontawesome = False # --- FORM SUBMISSION LOGIC --- if request.method == "POST": # Get styling options from the form, preserving them across submissions 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"]), "code_padding": request.form.get("code_padding", DEFAULT_STYLES["code_padding"]), "custom_css": request.form.get("custom_css", DEFAULT_STYLES["custom_css"]) } include_fontawesome = "include_fontawesome" in request.form download_type = request.form.get("download_type", "png") final_markdown_to_render = "" # --- CASE 1: File is being uploaded for parsing --- uploaded_file = request.files.get("markdown_file") if uploaded_file and uploaded_file.filename != '': try: markdown_text = uploaded_file.read().decode("utf-8") components = parse_markdown(markdown_text) except Exception as e: error_message = f"Error reading or parsing file: {e}" # --- CASE 2: User is generating a preview/download from components --- elif 'generate_from_components' in request.form: # Reconstruct the markdown from the selected components final_markdown_parts = [] for i in range(len(request.form) // 2): # Approx number of components comp_type = request.form.get(f'comp_type_{i}') if not comp_type: continue # Check if the component was selected if f'include_comp_{i}' in request.form: content = request.form.get(f'comp_content_{i}', '') if comp_type == 'code': lang = request.form.get(f'comp_lang_{i}', '') final_markdown_parts.append(f"```{lang}\n{content}\n```") else: # text final_markdown_parts.append(content) final_markdown_to_render = "".join(final_markdown_parts) # Pass the reconstructed components back to the template to preserve the UI state components = [] for i in range(len(request.form) // 2): if request.form.get(f'comp_type_{i}'): components.append({ 'type': request.form.get(f'comp_type_{i}'), 'id': i, 'language': request.form.get(f'comp_lang_{i}'), 'content': request.form.get(f'comp_content_{i}'), 'is_selected': f'include_comp_{i}' in request.form }) # --- CASE 3: Simple text area input (fallback) --- else: final_markdown_to_render = request.form.get("markdown_text", "") markdown_text = final_markdown_to_render # --- HTML & PNG GENERATION LOGIC --- if final_markdown_to_render: try: html_content = markdown.markdown(final_markdown_to_render, extensions=['fenced_code', 'tables']) # ... (The HTML and CSS generation logic remains the same) ... fontawesome_link = '' if include_fontawesome else "" style_block = f"""""" full_html = f'
{fontawesome_link}{style_block}{html_content}' 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 during conversion: {e}" print(f"Error: {traceback.format_exc()}") # --- RENDER THE MAIN TEMPLATE --- return render_template_string("""{{ error_message }}
{% endif %} {% if preview_html %}