broadfield-dev commited on
Commit
16bb5ee
·
verified ·
1 Parent(s): 12df627

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +217 -220
app.py CHANGED
@@ -4,46 +4,83 @@ import imgkit
4
  import os
5
  import traceback
6
  from io import BytesIO
 
7
 
8
  app = Flask(__name__)
9
 
10
- # Use a directory within the app's working directory to avoid permission issues.
11
- # This makes it more compatible with various deployment environments.
12
  TEMP_DIR = os.path.join(os.getcwd(), "temp")
 
13
 
14
- # Create the temporary directory if it doesn't exist.
15
- try:
16
- os.makedirs(TEMP_DIR, exist_ok=True)
17
- except Exception as e:
18
- # Log an error if the directory cannot be created.
19
- print(f"Error creating temp directory: {e}")
20
-
21
- # Define default values for styling options for a cleaner initial state.
22
  DEFAULT_STYLES = {
23
  "font_family": "'Arial', sans-serif",
24
  "font_size": "16",
25
  "text_color": "#333333",
26
  "background_color": "#ffffff",
27
  "code_bg_color": "#f4f4f4",
28
- "code_padding": "15", # Default padding for code blocks
29
  "custom_css": ""
30
  }
31
 
32
- @app.route("/", methods=["GET", "POST"])
33
- def index():
34
  """
35
- Main route to handle form submission, Markdown processing, and rendering.
 
36
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  preview_html = None
38
  download_available = False
39
  error_message = None
 
40
 
41
- # On POST, process the form data. On GET, use defaults.
 
 
 
 
 
 
42
  if request.method == "POST":
43
- markdown_text = request.form.get("markdown_text", "")
44
- download_type = request.form.get("download_type", "png")
45
-
46
- # Get styling options from the form, falling back to defaults.
47
  styles = {
48
  "font_family": request.form.get("font_family", DEFAULT_STYLES["font_family"]),
49
  "font_size": request.form.get("font_size", DEFAULT_STYLES["font_size"]),
@@ -54,146 +91,102 @@ def index():
54
  "custom_css": request.form.get("custom_css", DEFAULT_STYLES["custom_css"])
55
  }
56
  include_fontawesome = "include_fontawesome" in request.form
 
 
57
 
58
- else: # GET request
59
- markdown_text = ""
60
- download_type = "png"
61
- styles = DEFAULT_STYLES.copy()
62
- include_fontawesome = False
63
-
64
-
65
- if request.method == "POST" and markdown_text:
66
- try:
67
- # Convert Markdown to HTML using python-markdown library.
68
- # Extensions for tables and fenced code blocks are enabled.
69
- html_content = markdown.markdown(markdown_text, extensions=['fenced_code', 'tables'])
70
-
71
- # Optional: Include Font Awesome CSS if the user checked the box.
72
- fontawesome_link = ""
73
- if include_fontawesome:
74
- fontawesome_link = '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">'
75
 
76
- # Dynamically generate the CSS style block from user options.
77
- style_block = f"""
78
- <style>
79
- body {{
80
- font-family: {styles['font_family']};
81
- font-size: {styles['font_size']}px;
82
- color: {styles['text_color']};
83
- background-color: {styles['background_color']};
84
- padding: 25px;
85
- display: inline-block; /* Helps imgkit to crop correctly */
86
- }}
87
- table {{
88
- border-collapse: collapse;
89
- width: 100%;
90
- }}
91
- th, td {{
92
- border: 1px solid #ddd;
93
- padding: 8px;
94
- text-align: left;
95
- }}
96
- th {{
97
- background-color: #f2f2f2;
98
- }}
99
- img {{
100
- max-width: 100%;
101
- height: auto;
102
- }}
103
 
104
- /* --- Enhanced Code Block Styling --- */
 
 
 
 
 
 
 
 
 
 
105
 
106
- /* Style for the code block container */
107
- pre {{
108
- background: {styles['code_bg_color']};
109
- padding: {styles['code_padding']}px;
110
- border-radius: 5px;
111
- white-space: pre-wrap; /* Wrap long lines of code */
112
- word-wrap: break-word; /* Break long words if necessary */
113
- }}
114
 
115
- /* Style for inline `code` snippets */
116
- code {{
117
- background: {styles['code_bg_color']};
118
- padding: 0.2em 0.4em;
119
- margin: 0;
120
- font-size: 85%;
121
- border-radius: 3px;
122
- }}
123
 
124
- /* Reset styling for <code> inside <pre> to avoid double-styling */
125
- pre > code {{
126
- padding: 0;
127
- margin: 0;
128
- font-size: inherit;
129
- background: transparent;
130
- border-radius: 0;
131
- }}
132
 
133
- /* User-defined custom CSS */
134
- {styles['custom_css']}
135
- </style>
136
- """
137
-
138
- # Combine everything into a full HTML document.
139
- full_html = f"""
140
- <!DOCTYPE html>
141
- <html>
142
- <head>
143
- <meta charset="UTF-8">
144
- {fontawesome_link}
145
- {style_block}
146
- </head>
147
- <body>
148
- {html_content}
149
- </body>
150
- </html>
151
- """
152
-
153
- # Set flags and content for the frontend preview.
154
- preview_html = full_html
155
- download_available = True
156
 
157
- # If the user clicked the "Download" button.
158
- if "download" in request.form:
159
- if download_type == "html":
160
- # For HTML download, send the generated HTML directly.
161
- return send_file(
162
- BytesIO(full_html.encode("utf-8")),
163
- as_attachment=True,
164
- download_name="output.html",
165
- mimetype="text/html"
166
- )
167
- else: # For PNG download
168
- # Use imgkit to convert the HTML string to a PNG image.
169
- # The 'quiet' option suppresses console output from wkhtmltoimage.
170
- png_path = os.path.join(TEMP_DIR, "output.png")
171
- imgkit.from_string(full_html, png_path, options={"quiet": "", 'encoding': "UTF-8"})
172
 
173
- return send_file(
174
- png_path,
175
- as_attachment=True,
176
- download_name="output.png",
177
- mimetype="image/png"
178
- )
 
179
 
180
- except Exception as e:
181
- # In case of any error, display a detailed message to the user.
182
- error_message = f"An error occurred: {str(e)}"
183
- print(f"Error: {traceback.format_exc()}")
184
 
185
- # Render the main page template with all the necessary variables.
186
  return render_template_string("""
187
  <!DOCTYPE html>
188
  <html lang="en">
189
  <head>
190
  <meta charset="UTF-8">
191
- <title>Advanced Markdown to PNG/HTML Converter</title>
192
  <style>
193
  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f9f9f9; }
194
- h1 { text-align: center; color: #333; }
195
- form { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
196
- textarea { width: 100%; height: 300px; margin-bottom: 10px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; padding: 10px; }
197
  .controls { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 20px; margin-top: 20px; }
198
  .main-actions { display: flex; flex-wrap: wrap; gap: 15px; align-items: center; }
199
  fieldset { border: 1px solid #ddd; padding: 15px; border-radius: 5px; margin-top: 20px; }
@@ -201,110 +194,114 @@ def index():
201
  .style-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; }
202
  .style-grid > div { display: flex; flex-direction: column; }
203
  label { margin-bottom: 5px; color: #666; font-size: 14px; }
204
- select, input[type="number"], input[type="color"], input[type="text"] { padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
205
  button { padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.2s; }
206
- .generate-btn { background-color: #007BFF; color: white; }
207
- .generate-btn:hover { background-color: #0056b3; }
208
  .download-btn { background-color: #28a745; color: white; }
209
  .download-btn:hover { background-color: #218838; }
210
  .preview { border: 1px solid #ddd; padding: 20px; margin-top: 20px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
211
  .error { color: #D8000C; background-color: #FFD2D2; padding: 10px; border-radius: 5px; margin-top: 15px; }
212
  .info { color: #00529B; background-color: #BDE5F8; padding: 10px; border-radius: 5px; margin: 10px 0; }
 
 
 
 
 
 
 
 
 
213
  </style>
 
 
 
 
 
 
214
  </head>
215
  <body>
216
- <h1>Advanced Markdown to PNG/HTML Converter</h1>
217
- <form method="post">
218
- <textarea name="markdown_text" placeholder="Paste your Markdown here...">{{ markdown_text }}</textarea>
219
- <div class="info">
220
- <b>Tip:</b> To include images, use full public URLs (e.g., `https://.../image.png`).
221
- To use icons, check "Include Font Awesome" below and use tags like `<i class="fa-solid fa-star"></i>`.
222
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
 
224
  <fieldset>
225
  <legend>Styling Options</legend>
226
  <div class="style-grid">
227
- <div>
228
- <label for="font_family">Font Family:</label>
229
- <select id="font_family" name="font_family">
230
- <option value="'Arial', sans-serif" {% if styles.font_family == "'Arial', sans-serif" %}selected{% endif %}>Arial</option>
231
- <option value="'Georgia', serif" {% if styles.font_family == "'Georgia', serif" %}selected{% endif %}>Georgia</option>
232
- <option value="'Times New Roman', serif" {% if styles.font_family == "'Times New Roman', serif" %}selected{% endif %}>Times New Roman</option>
233
- <option value="'Verdana', sans-serif" {% if styles.font_family == "'Verdana', sans-serif" %}selected{% endif %}>Verdana</option>
234
- <option value="'Courier New', monospace" {% if styles.font_family == "'Courier New', monospace" %}selected{% endif %}>Courier New</option>
235
- </select>
236
- </div>
237
- <div>
238
- <label for="font_size">Font Size (px):</label>
239
- <input type="number" id="font_size" name="font_size" value="{{ styles.font_size }}">
240
- </div>
241
- <div>
242
- <label for="text_color">Text Color:</label>
243
- <input type="color" id="text_color" name="text_color" value="{{ styles.text_color }}">
244
- </div>
245
- <div>
246
- <label for="background_color">Background Color:</label>
247
- <input type="color" id="background_color" name="background_color" value="{{ styles.background_color }}">
248
- </div>
249
- <div>
250
- <label for="code_bg_color">Code BG Color:</label>
251
- <input type="color" id="code_bg_color" name="code_bg_color" value="{{ styles.code_bg_color }}">
252
- </div>
253
- <div>
254
- <label for="code_padding">Code Padding (px):</label>
255
- <input type="number" id="code_padding" name="code_padding" value="{{ styles.code_padding }}">
256
- </div>
257
- </div>
258
- <div>
259
- <input type="checkbox" id="include_fontawesome" name="include_fontawesome" {% if include_fontawesome %}checked{% endif %}>
260
- <label for="include_fontawesome">Include Font Awesome (for icons)</label>
261
- </div>
262
- <div>
263
- <label for="custom_css">Custom CSS:</label>
264
- <textarea id="custom_css" name="custom_css" rows="4" placeholder="e.g., h1 { color: blue; }">{{ styles.custom_css }}</textarea>
265
  </div>
 
 
266
  </fieldset>
267
 
268
  <div class="controls">
269
  <div class="main-actions">
270
- <button type="submit" class="generate-btn">Generate Preview</button>
271
- <div>
272
- <label for="download_type">Output format:</label>
273
- <select id="download_type" name="download_type">
274
- <option value="png" {% if download_type == 'png' %}selected{% endif %}>PNG</option>
275
- <option value="html" {% if download_type == 'html' %}selected{% endif %}>HTML</option>
276
- </select>
277
- </div>
278
- {% if download_available %}
279
- <button type="submit" name="download" value="true" class="download-btn">Download {{ download_type.upper() }}</button>
280
- {% endif %}
281
  </div>
282
  </div>
283
  </form>
284
 
285
- {% if error_message %}
286
- <p class="error">{{ error_message }}</p>
287
- {% endif %}
288
-
289
- {% if preview_html %}
290
- <h2>Preview</h2>
291
- <div class="preview">
292
- {{ preview_html | safe }}
293
- </div>
294
- {% endif %}
295
  </body>
296
  </html>
297
  """,
298
- styles=styles,
299
- markdown_text=markdown_text,
300
- download_type=download_type,
301
- include_fontawesome=include_fontawesome,
302
- download_available=download_available,
303
- preview_html=preview_html,
304
- error_message=error_message
305
- )
306
 
307
  if __name__ == "__main__":
308
- # It's recommended to run Flask applications using a production-ready WSGI server.
309
- # The built-in development server is used here for convenience.
310
  app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
 
4
  import os
5
  import traceback
6
  from io import BytesIO
7
+ import re
8
 
9
  app = Flask(__name__)
10
 
11
+ # Configure a temporary directory for file operations
 
12
  TEMP_DIR = os.path.join(os.getcwd(), "temp")
13
+ os.makedirs(TEMP_DIR, exist_ok=True)
14
 
15
+ # Define default values for styling options
 
 
 
 
 
 
 
16
  DEFAULT_STYLES = {
17
  "font_family": "'Arial', sans-serif",
18
  "font_size": "16",
19
  "text_color": "#333333",
20
  "background_color": "#ffffff",
21
  "code_bg_color": "#f4f4f4",
22
+ "code_padding": "15",
23
  "custom_css": ""
24
  }
25
 
26
+ def parse_markdown(text):
 
27
  """
28
+ Parses markdown text to separate code blocks from base text.
29
+ Returns a list of dictionaries, each representing a part of the document.
30
  """
31
+ # Regex to find fenced code blocks and capture them, including the fences and language
32
+ # This regex is non-greedy `([\s\S]*?)` to handle multiple blocks correctly.
33
+ parts = re.split(r'(```[\s\S]*?```)', text)
34
+
35
+ components = []
36
+ code_block_counter = 0
37
+ for i, part in enumerate(parts):
38
+ if not part:
39
+ continue
40
+
41
+ # Check if the part is a fenced code block
42
+ if part.startswith('```'):
43
+ # Extract language and content from the block
44
+ # The first line is ` ```language `, the last line is ` ``` `
45
+ lines = part.strip().split('\n')
46
+ language = lines[3:].strip()
47
+ content = '\n'.join(lines[1:-1])
48
+ components.append({
49
+ 'type': 'code',
50
+ 'id': code_block_counter,
51
+ 'language': language or 'plaintext',
52
+ 'content': content
53
+ })
54
+ code_block_counter += 1
55
+ else:
56
+ # This is a standard text part
57
+ components.append({
58
+ 'type': 'text',
59
+ 'id': i,
60
+ 'content': part
61
+ })
62
+
63
+ return components
64
+
65
+
66
+ @app.route("/", methods=["GET", "POST"])
67
+ def index():
68
+ """Main route to handle file uploads, component selection, and conversion."""
69
+ # Initialize variables
70
  preview_html = None
71
  download_available = False
72
  error_message = None
73
+ components = []
74
 
75
+ # Set default values on GET request
76
+ markdown_text = ""
77
+ download_type = "png"
78
+ styles = DEFAULT_STYLES.copy()
79
+ include_fontawesome = False
80
+
81
+ # --- FORM SUBMISSION LOGIC ---
82
  if request.method == "POST":
83
+ # Get styling options from the form, preserving them across submissions
 
 
 
84
  styles = {
85
  "font_family": request.form.get("font_family", DEFAULT_STYLES["font_family"]),
86
  "font_size": request.form.get("font_size", DEFAULT_STYLES["font_size"]),
 
91
  "custom_css": request.form.get("custom_css", DEFAULT_STYLES["custom_css"])
92
  }
93
  include_fontawesome = "include_fontawesome" in request.form
94
+ download_type = request.form.get("download_type", "png")
95
+ final_markdown_to_render = ""
96
 
97
+ # --- CASE 1: File is being uploaded for parsing ---
98
+ uploaded_file = request.files.get("markdown_file")
99
+ if uploaded_file and uploaded_file.filename != '':
100
+ try:
101
+ markdown_text = uploaded_file.read().decode("utf-8")
102
+ components = parse_markdown(markdown_text)
103
+ except Exception as e:
104
+ error_message = f"Error reading or parsing file: {e}"
105
+
106
+ # --- CASE 2: User is generating a preview/download from components ---
107
+ elif 'generate_from_components' in request.form:
108
+ # Reconstruct the markdown from the selected components
109
+ final_markdown_parts = []
110
+ for i in range(len(request.form) // 2): # Approx number of components
111
+ comp_type = request.form.get(f'comp_type_{i}')
112
+ if not comp_type: continue
 
113
 
114
+ # Check if the component was selected
115
+ if f'include_comp_{i}' in request.form:
116
+ content = request.form.get(f'comp_content_{i}', '')
117
+ if comp_type == 'code':
118
+ lang = request.form.get(f'comp_lang_{i}', '')
119
+ final_markdown_parts.append(f"```{lang}\n{content}\n```")
120
+ else: # text
121
+ final_markdown_parts.append(content)
122
+
123
+ final_markdown_to_render = "".join(final_markdown_parts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
+ # Pass the reconstructed components back to the template to preserve the UI state
126
+ components = []
127
+ for i in range(len(request.form) // 2):
128
+ if request.form.get(f'comp_type_{i}'):
129
+ components.append({
130
+ 'type': request.form.get(f'comp_type_{i}'),
131
+ 'id': i,
132
+ 'language': request.form.get(f'comp_lang_{i}'),
133
+ 'content': request.form.get(f'comp_content_{i}'),
134
+ 'is_selected': f'include_comp_{i}' in request.form
135
+ })
136
 
137
+ # --- CASE 3: Simple text area input (fallback) ---
138
+ else:
139
+ final_markdown_to_render = request.form.get("markdown_text", "")
140
+ markdown_text = final_markdown_to_render
 
 
 
 
141
 
 
 
 
 
 
 
 
 
142
 
143
+ # --- HTML & PNG GENERATION LOGIC ---
144
+ if final_markdown_to_render:
145
+ try:
146
+ html_content = markdown.markdown(final_markdown_to_render, extensions=['fenced_code', 'tables'])
 
 
 
 
147
 
148
+ # ... (The HTML and CSS generation logic remains the same) ...
149
+ 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 ""
150
+ style_block = f"""<style>
151
+ 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; }}
152
+ table {{ border-collapse: collapse; width: 100%; }}
153
+ th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
154
+ th {{ background-color: #f2f2f2; }}
155
+ img {{ max-width: 100%; height: auto; }}
156
+ pre {{ background: {styles['code_bg_color']}; padding: {styles['code_padding']}px; border-radius: 5px; white-space: pre-wrap; word-wrap: break-word; }}
157
+ code {{ background: {styles['code_bg_color']}; padding: 0.2em 0.4em; margin: 0; font-size: 85%; border-radius: 3px; }}
158
+ pre > code {{ padding: 0; margin: 0; font-size: inherit; background: transparent; border-radius: 0; }}
159
+ {styles['custom_css']}
160
+ </style>"""
161
+ full_html = f'<!DOCTYPE html><html><head><meta charset="UTF-8">{fontawesome_link}{style_block}</head><body>{html_content}</body></html>'
 
 
 
 
 
 
 
 
 
162
 
163
+ preview_html = full_html
164
+ download_available = True
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
+ if "download" in request.form:
167
+ if download_type == "html":
168
+ return send_file(BytesIO(full_html.encode("utf-8")), as_attachment=True, download_name="output.html", mimetype="text/html")
169
+ else:
170
+ png_path = os.path.join(TEMP_DIR, "output.png")
171
+ imgkit.from_string(full_html, png_path, options={"quiet": "", 'encoding': "UTF-8"})
172
+ return send_file(png_path, as_attachment=True, download_name="output.png", mimetype="image/png")
173
 
174
+ except Exception as e:
175
+ error_message = f"An error occurred during conversion: {e}"
176
+ print(f"Error: {traceback.format_exc()}")
 
177
 
178
+ # --- RENDER THE MAIN TEMPLATE ---
179
  return render_template_string("""
180
  <!DOCTYPE html>
181
  <html lang="en">
182
  <head>
183
  <meta charset="UTF-8">
184
+ <title>Advanced Markdown Converter & Composer</title>
185
  <style>
186
  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f9f9f9; }
187
+ h1, h2 { text-align: center; color: #333; }
188
+ form { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
189
+ textarea { width: 100%; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; padding: 10px; font-family: monospace; }
190
  .controls { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 20px; margin-top: 20px; }
191
  .main-actions { display: flex; flex-wrap: wrap; gap: 15px; align-items: center; }
192
  fieldset { border: 1px solid #ddd; padding: 15px; border-radius: 5px; margin-top: 20px; }
 
194
  .style-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; }
195
  .style-grid > div { display: flex; flex-direction: column; }
196
  label { margin-bottom: 5px; color: #666; font-size: 14px; }
197
+ select, input[type="number"], input[type="color"], input[type="text"], input[type="file"] { padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
198
  button { padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.2s; }
199
+ .action-btn { background-color: #007BFF; color: white; }
200
+ .action-btn:hover { background-color: #0056b3; }
201
  .download-btn { background-color: #28a745; color: white; }
202
  .download-btn:hover { background-color: #218838; }
203
  .preview { border: 1px solid #ddd; padding: 20px; margin-top: 20px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
204
  .error { color: #D8000C; background-color: #FFD2D2; padding: 10px; border-radius: 5px; margin-top: 15px; }
205
  .info { color: #00529B; background-color: #BDE5F8; padding: 10px; border-radius: 5px; margin: 10px 0; }
206
+ /* Component Selection Styles */
207
+ .component-container { border: 1px solid #e0e0e0; border-radius: 5px; margin-bottom: 15px; background: #fafafa; }
208
+ .component-header { background: #f1f1f1; padding: 8px 12px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 10px; }
209
+ .component-header input[type="checkbox"] { width: 18px; height: 18px; }
210
+ .component-header label { margin-bottom: 0; font-weight: bold; }
211
+ .component-header .lang-tag { background: #007BFF; color: white; padding: 3px 8px; border-radius: 4px; font-size: 12px; }
212
+ .component-content { padding: 10px; }
213
+ .component-content textarea { height: 150px; }
214
+ .selection-controls { margin-bottom: 15px; }
215
  </style>
216
+ <script>
217
+ function toggleAllComponents(checked) {
218
+ const checkboxes = document.querySelectorAll('.component-checkbox');
219
+ checkboxes.forEach(cb => cb.checked = checked);
220
+ }
221
+ </script>
222
  </head>
223
  <body>
224
+ <h1>Advanced Markdown Converter & Composer</h1>
225
+
226
+ <!-- Uploader and Composer Form -->
227
+ <form method="post" enctype="multipart/form-data">
228
+ {% if not components %}
229
+ <fieldset>
230
+ <legend>Option 1: Paste Markdown</legend>
231
+ <textarea name="markdown_text" rows="10" placeholder="Paste your Markdown here...">{{ markdown_text }}</textarea>
232
+ </fieldset>
233
+ <h2 style="font-size: 1.2em;">OR</h2>
234
+ <fieldset>
235
+ <legend>Option 2: Upload a Markdown File</legend>
236
+ <input type="file" name="markdown_file" accept=".md,.txt,text/markdown">
237
+ <button type="submit" class="action-btn" style="margin-top:10px;">Upload and Analyze</button>
238
+ </fieldset>
239
+ {% endif %}
240
+
241
+ <!-- This section is shown only after a file is uploaded and parsed -->
242
+ {% if components %}
243
+ <input type="hidden" name="generate_from_components" value="true">
244
+ <fieldset>
245
+ <legend>Detected Components</legend>
246
+ <div class="info">Select the components to include in the final output.</div>
247
+ <div class="selection-controls">
248
+ <button type="button" onclick="toggleAllComponents(true)">Select All</button>
249
+ <button type="button" onclick="toggleAllComponents(false)">Deselect All</button>
250
+ </div>
251
+ {% for comp in components %}
252
+ <div class="component-container">
253
+ <div class="component-header">
254
+ <input type="checkbox" name="include_comp_{{ loop.index0 }}" id="include_comp_{{ loop.index0 }}" class="component-checkbox" {% if comp.is_selected is not defined or comp.is_selected %}checked{% endif %}>
255
+ {% if comp.type == 'code' %}
256
+ <label for="include_comp_{{ loop.index0 }}">Code Block <span class="lang-tag">{{ comp.language }}</span></label>
257
+ {% else %}
258
+ <label for="include_comp_{{ loop.index0 }}">Base Text</label>
259
+ {% endif %}
260
+ </div>
261
+ <div class="component-content">
262
+ <textarea readonly>{{ comp.content }}</textarea>
263
+ </div>
264
+ </div>
265
+ <!-- Hidden fields to preserve data on next post -->
266
+ <input type="hidden" name="comp_type_{{ loop.index0 }}" value="{{ comp.type }}">
267
+ <input type="hidden" name="comp_content_{{ loop.index0 }}" value="{{ comp.content }}">
268
+ <input type="hidden" name="comp_lang_{{ loop.index0 }}" value="{{ comp.language or '' }}">
269
+ {% endfor %}
270
+ </fieldset>
271
+ {% endif %}
272
 
273
+ <!-- Styling and Generation controls are always visible -->
274
  <fieldset>
275
  <legend>Styling Options</legend>
276
  <div class="style-grid">
277
+ <!-- Font, Size, Color inputs... -->
278
+ <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>
279
+ <div><label for="font_size">Font Size (px):</label><input type="number" id="font_size" name="font_size" value="{{ styles.font_size }}"></div>
280
+ <div><label for="text_color">Text Color:</label><input type="color" id="text_color" name="text_color" value="{{ styles.text_color }}"></div>
281
+ <div><label for="background_color">Background Color:</label><input type="color" id="background_color" name="background_color" value="{{ styles.background_color }}"></div>
282
+ <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>
283
+ <div><label for="code_padding">Code Padding (px):</label><input type="number" id="code_padding" name="code_padding" value="{{ styles.code_padding }}"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  </div>
285
+ <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>
286
+ <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>
287
  </fieldset>
288
 
289
  <div class="controls">
290
  <div class="main-actions">
291
+ <button type="submit" class="action-btn">Generate Preview</button>
292
+ <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>
293
+ {% if download_available %}<button type="submit" name="download" value="true" class="download-btn">Download {{ download_type.upper() }}</button>{% endif %}
 
 
 
 
 
 
 
 
294
  </div>
295
  </div>
296
  </form>
297
 
298
+ {% if error_message %}<p class="error">{{ error_message }}</p>{% endif %}
299
+ {% if preview_html %}<h2>Preview</h2><div class="preview">{{ preview_html | safe }}</div>{% endif %}
 
 
 
 
 
 
 
 
300
  </body>
301
  </html>
302
  """,
303
+ styles=styles, markdown_text=markdown_text, download_type=download_type, include_fontawesome=include_fontawesome,
304
+ download_available=download_available, preview_html=preview_html, error_message=error_message, components=components)
 
 
 
 
 
 
305
 
306
  if __name__ == "__main__":
 
 
307
  app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))