broadfield-dev commited on
Commit
fb80aa3
·
verified ·
1 Parent(s): f9e6006

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +191 -75
app.py CHANGED
@@ -17,18 +17,16 @@ os.makedirs(TEMP_DIR, exist_ok=True)
17
 
18
  def parse_repo2markdown(text):
19
  components = []
20
- # Pattern to capture file sections
21
- pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
22
-
23
  # Extract File Structure specifically
24
  struct_match = re.search(r'## File Structure\n([\s\S]*?)(?=\n### File:|\Z)', text)
25
  if struct_match:
26
  components.append({'type': 'structure', 'filename': 'File Structure', 'content': struct_match.group(1).strip()})
27
 
 
 
28
  for match in pattern.finditer(text):
29
  filename = match.group(1).strip()
30
  content = match.group(2).strip()
31
- # Strip wrapping code fences if they encompass the whole file
32
  code_match = re.search(r'^```(?:\w*)\s*\n([\s\S]*?)\s*```$', content, re.DOTALL)
33
  final_content = code_match.group(1).strip() if code_match else content
34
  components.append({'type': 'file', 'filename': filename, 'content': final_content})
@@ -38,7 +36,7 @@ def parse_standard_readme(text):
38
  components = []
39
  parts = re.split(r'^(## .*?)$', text, flags=re.MULTILINE)
40
  if parts[0].strip():
41
- components.append({'type': 'intro', 'filename': 'Intro', 'content': parts[0].strip()})
42
  for i in range(1, len(parts), 2):
43
  components.append({'type': 'section', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
44
  return components
@@ -47,7 +45,7 @@ def parse_changelog(text):
47
  components = []
48
  parts = re.split(r'^(## \[\d+\.\d+\.\d+.*?\].*?)$', text, flags=re.MULTILINE)
49
  if parts[0].strip():
50
- components.append({'type': 'intro', 'filename': 'Header', 'content': parts[0].strip()})
51
  for i in range(1, len(parts), 2):
52
  components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
53
  return components
@@ -84,7 +82,10 @@ def build_full_html(markdown_text, styles, include_fontawesome):
84
  background-color: {styles.get('background_color', '#fff')};
85
  padding: {styles.get('page_padding', '40')}px;
86
  }}
 
 
87
  {wrapper_id} pre {{ padding: {styles.get('code_padding', '15')}px; border-radius: 8px; overflow-x: auto; }}
 
88
  {pygments_css}
89
  {styles.get('custom_css', '')}
90
  """
@@ -96,16 +97,12 @@ def build_full_html(markdown_text, styles, include_fontawesome):
96
  def parse_endpoint():
97
  text = request.form.get('markdown_text', '')
98
  try:
99
- # Detect Repo2Markdown (Checking for '## File Structure' as primary key)
100
  if "## File Structure" in text and "### File:" in text:
101
  format_name, components = "Repo2Markdown", parse_repo2markdown(text)
102
- # Detect Agent Action (Strict anchor check for action keyword)
103
  elif re.search(r'^### HF_ACTION:', text, flags=re.MULTILINE):
104
  format_name, components = "Agent Action", parse_agent_action(text)
105
- # Detect Changelog
106
  elif re.search(r'^## \[\d+\.\d+\.\d+.*?\].*?$', text, flags=re.MULTILINE):
107
  format_name, components = "Changelog", parse_changelog(text)
108
- # Detect Standard README
109
  elif re.search(r'^# ', text, flags=re.MULTILINE) and re.search(r'^## ', text, flags=re.MULTILINE):
110
  format_name, components = "Standard README", parse_standard_readme(text)
111
  else:
@@ -118,11 +115,22 @@ def parse_endpoint():
118
  def convert_endpoint():
119
  data = request.json
120
  try:
121
- full_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), data.get('include_fontawesome', False))
122
  options = {"quiet": "", "encoding": "UTF-8", "width": 1024, "disable-smart-width": ""}
 
 
 
 
 
 
 
123
  png_bytes = imgkit.from_string(full_html, False, options=options)
124
- return jsonify({'preview_html': full_html, 'preview_png_base64': base64.b64encode(png_bytes).decode('utf-8')})
 
 
 
125
  except Exception as e:
 
126
  return jsonify({'error': str(e)}), 500
127
 
128
  @app.route('/')
@@ -131,93 +139,201 @@ def index():
131
  <!DOCTYPE html>
132
  <html lang="en">
133
  <head>
134
- <meta charset="UTF-8"><title>Intelligent Markdown</title>
135
  <style>
136
- :root { --bg: #f4f7f6; --text: #333; --card: #fff; --border: #ddd; }
137
  body.dark-mode { --bg: #1a1a1a; --text: #eee; --card: #252525; --border: #444; }
138
- body { font-family: 'Inter', sans-serif; max-width: 1100px; margin: 0 auto; padding: 20px; background: var(--bg); color: var(--text); }
139
- fieldset { border: 1px solid var(--border); background: var(--card); padding: 15px; margin-bottom: 20px; border-radius: 8px; }
140
- textarea { width: 100%; border-radius: 4px; padding: 10px; border: 1px solid var(--border); background: var(--card); color: var(--text); font-family: monospace; box-sizing: border-box; }
141
- .format-banner { background: #5a32a3; color: white; padding: 5px 15px; border-radius: 20px; font-size: 12px; display: inline-block; margin-bottom: 10px; }
142
- .style-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; }
143
- .comp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
144
- .comp-card { background: var(--card); border: 1px solid var(--border); padding: 10px; border-radius: 4px; }
145
- .comp-card textarea { height: 50px; font-size: 10px; margin-top: 5px; opacity: 0.7; pointer-events: none; }
146
- button { padding: 10px 20px; cursor: pointer; border: none; border-radius: 4px; font-weight: bold; }
147
- .btn-primary { background: #5a32a3; color: #fff; }
148
- .btn-dark { background: #333; color: #fff; border: 1px solid #555; }
149
- .preview-container { background: #fff; border: 1px solid #ddd; margin-top: 20px; padding: 15px; color: #333; }
 
 
 
 
 
 
 
 
 
150
  </style>
151
  </head>
152
  <body>
153
- <div style="display:flex; justify-content:space-between; align-items:center;">
154
- <h1>Markdown Tool</h1>
155
- <button onclick="document.body.classList.toggle('dark-mode')" class="btn-dark">🌓 Toggle Dark</button>
156
  </div>
 
157
  <fieldset>
158
- <legend>1. Input Markdown</legend>
159
- <textarea id="md-input" rows="8"></textarea>
160
- <div style="margin-top:10px;"><button onclick="load()" class="btn-primary" id="load-btn">Analyze & Parse</button></div>
161
  </fieldset>
 
162
  <div id="comp-section" style="display:none;">
163
  <div id="detected-format" class="format-banner"></div>
164
  <fieldset>
165
- <legend>2. Components</legend>
166
- <div id="comp-list" class="comp-grid"></div>
167
  </fieldset>
168
  </div>
 
169
  <fieldset>
170
- <legend>3. Export Settings</legend>
171
  <div class="style-grid">
172
- <div><label>Font</label><select id="f_family">
173
- <option value="'Inter', sans-serif">Inter</option>
174
- <option value="'Fira Code', monospace">Fira Code</option>
175
- </select></div>
176
- <div><label>Text Color</label><input type="color" id="t_color" value="#333333"></div>
177
- <div><label>BG Color</label><input type="color" id="b_color" value="#ffffff"></div>
178
- <div><label>Pygments Theme</label><select id="h_theme">
179
- {% for s in styles %}<option value="{{s}}">{{s}}</option>{% endfor %}
180
- </select></div>
 
 
 
 
 
 
 
 
 
 
 
 
181
  </div>
182
  </fieldset>
183
- <button onclick="generate()" class="btn-primary" style="width:100%; height:50px; font-size:18px;">GENERATE AS IMAGE</button>
184
- <div id="preview-area" style="display:none; margin-top:30px;">
185
- <h3>Generated Output</h3>
186
- <div id="png-prev"></div>
187
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  <script>
189
- async function load() {
190
- const fd = new FormData(); fd.append('markdown_text', document.getElementById('md-input').value);
191
- const res = await fetch('/parse', {method:'POST', body:fd});
192
- const data = await res.json();
193
- document.getElementById('detected-format').innerText = "DETECTED: " + data.format;
194
- const list = document.getElementById('comp-list'); list.innerHTML = '';
195
- data.components.forEach(c => {
196
- const safe = btoa(unescape(encodeURIComponent(c.content)));
197
- list.innerHTML += `<div class="comp-card">
198
- <input type="checkbox" checked class="c-check" data-name="${c.filename}" data-content="${safe}">
199
- <b>${c.filename}</b><br><textarea>${c.content.substring(0,80)}</textarea>
200
- </div>`;
201
- });
202
- document.getElementById('comp-section').style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
- async function generate() {
205
- let text = "";
206
- document.querySelectorAll('.c-check').forEach(c => {
207
- if(c.checked) text += "### " + c.dataset.name + "\\n" + decodeURIComponent(escape(atob(c.dataset.content))) + "\\n\\n";
208
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  const payload = {
210
- markdown_text: text || document.getElementById('md-input').value,
 
 
211
  styles: {
212
- font_family: document.getElementById('f_family').value, text_color: document.getElementById('t_color').value,
213
- background_color: document.getElementById('b_color').value, highlight_theme: document.getElementById('h_theme').value,
214
- font_size: 16, line_height: 1.6, page_padding: 40, code_padding: 15
 
 
 
 
 
 
215
  }
216
  };
217
- const res = await fetch('/convert', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)});
218
- const data = await res.json();
219
- document.getElementById('preview-area').style.display = 'block';
220
- document.getElementById('png-prev').innerHTML = `<img src="data:image/png;base64,${data.preview_png_base64}" style="width:100%; border:1px solid #ccc;">`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  }
222
  </script>
223
  </body></html>
 
17
 
18
  def parse_repo2markdown(text):
19
  components = []
 
 
 
20
  # Extract File Structure specifically
21
  struct_match = re.search(r'## File Structure\n([\s\S]*?)(?=\n### File:|\Z)', text)
22
  if struct_match:
23
  components.append({'type': 'structure', 'filename': 'File Structure', 'content': struct_match.group(1).strip()})
24
 
25
+ # Pattern to capture file sections
26
+ pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
27
  for match in pattern.finditer(text):
28
  filename = match.group(1).strip()
29
  content = match.group(2).strip()
 
30
  code_match = re.search(r'^```(?:\w*)\s*\n([\s\S]*?)\s*```$', content, re.DOTALL)
31
  final_content = code_match.group(1).strip() if code_match else content
32
  components.append({'type': 'file', 'filename': filename, 'content': final_content})
 
36
  components = []
37
  parts = re.split(r'^(## .*?)$', text, flags=re.MULTILINE)
38
  if parts[0].strip():
39
+ components.append({'type': 'intro', 'filename': 'Header/Intro', 'content': parts[0].strip()})
40
  for i in range(1, len(parts), 2):
41
  components.append({'type': 'section', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
42
  return components
 
45
  components = []
46
  parts = re.split(r'^(## \[\d+\.\d+\.\d+.*?\].*?)$', text, flags=re.MULTILINE)
47
  if parts[0].strip():
48
+ components.append({'type': 'intro', 'filename': 'Changelog Header', 'content': parts[0].strip()})
49
  for i in range(1, len(parts), 2):
50
  components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
51
  return components
 
82
  background-color: {styles.get('background_color', '#fff')};
83
  padding: {styles.get('page_padding', '40')}px;
84
  }}
85
+ {wrapper_id} table {{ border-collapse: collapse; width: 100%; margin-bottom: 1em; }}
86
+ {wrapper_id} th, {wrapper_id} td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
87
  {wrapper_id} pre {{ padding: {styles.get('code_padding', '15')}px; border-radius: 8px; overflow-x: auto; }}
88
+ {wrapper_id} h1, {wrapper_id} h2, {wrapper_id} h3 {{ border-bottom: 1px solid #eee; padding-bottom: 5px; }}
89
  {pygments_css}
90
  {styles.get('custom_css', '')}
91
  """
 
97
  def parse_endpoint():
98
  text = request.form.get('markdown_text', '')
99
  try:
 
100
  if "## File Structure" in text and "### File:" in text:
101
  format_name, components = "Repo2Markdown", parse_repo2markdown(text)
 
102
  elif re.search(r'^### HF_ACTION:', text, flags=re.MULTILINE):
103
  format_name, components = "Agent Action", parse_agent_action(text)
 
104
  elif re.search(r'^## \[\d+\.\d+\.\d+.*?\].*?$', text, flags=re.MULTILINE):
105
  format_name, components = "Changelog", parse_changelog(text)
 
106
  elif re.search(r'^# ', text, flags=re.MULTILINE) and re.search(r'^## ', text, flags=re.MULTILINE):
107
  format_name, components = "Standard README", parse_standard_readme(text)
108
  else:
 
115
  def convert_endpoint():
116
  data = request.json
117
  try:
118
+ full_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), data.get('include_fontawesome', True))
119
  options = {"quiet": "", "encoding": "UTF-8", "width": 1024, "disable-smart-width": ""}
120
+
121
+ if data.get('download', False):
122
+ if data.get('download_type') == 'html':
123
+ return send_file(BytesIO(full_html.encode("utf-8")), as_attachment=True, download_name="output.html", mimetype="text/html")
124
+ png_bytes = imgkit.from_string(full_html, False, options=options)
125
+ return send_file(BytesIO(png_bytes), as_attachment=True, download_name="output.png", mimetype="image/png")
126
+
127
  png_bytes = imgkit.from_string(full_html, False, options=options)
128
+ return jsonify({
129
+ 'preview_html': full_html,
130
+ 'preview_png_base64': base64.b64encode(png_bytes).decode('utf-8')
131
+ })
132
  except Exception as e:
133
+ traceback.print_exc()
134
  return jsonify({'error': str(e)}), 500
135
 
136
  @app.route('/')
 
139
  <!DOCTYPE html>
140
  <html lang="en">
141
  <head>
142
+ <meta charset="UTF-8"><title>Intelligent Markdown Converter</title>
143
  <style>
144
+ :root { --bg: #f4f7f6; --text: #333; --card: #fff; --border: #ddd; --primary: #5a32a3; }
145
  body.dark-mode { --bg: #1a1a1a; --text: #eee; --card: #252525; --border: #444; }
146
+ body { font-family: 'Inter', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: var(--bg); color: var(--text); line-height: 1.4; transition: background 0.3s; }
147
+ h1 { text-align: center; }
148
+ fieldset { border: 1px solid var(--border); background: var(--card); padding: 20px; margin-bottom: 25px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
149
+ legend { font-weight: bold; padding: 0 10px; color: var(--primary); }
150
+ textarea { width: 100%; border-radius: 6px; padding: 12px; border: 1px solid var(--border); background: var(--card); color: var(--text); font-family: 'Fira Code', monospace; box-sizing: border-box; }
151
+ .format-banner { background: var(--primary); color: white; padding: 6px 16px; border-radius: 20px; font-size: 13px; display: inline-block; margin-bottom: 15px; font-weight: bold; }
152
+ .style-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
153
+ .comp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
154
+ .comp-card { background: var(--card); border: 1px solid var(--border); padding: 12px; border-radius: 8px; position: relative; }
155
+ .comp-card textarea { height: 60px; font-size: 11px; margin-top: 8px; opacity: 0.8; pointer-events: none; resize: none; }
156
+ .controls { display: flex; flex-direction: column; gap: 8px; }
157
+ .action-bar { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; }
158
+ button { padding: 12px 24px; cursor: pointer; border: none; border-radius: 6px; font-weight: bold; transition: opacity 0.2s; }
159
+ .btn-primary { background: var(--primary); color: #fff; flex: 1; min-width: 200px; }
160
+ .btn-secondary { background: #333; color: #fff; border: 1px solid #555; }
161
+ .btn-download { background: #28a745; color: white; }
162
+ .preview-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 30px; }
163
+ .preview-box { background: #fff; border: 1px solid #ddd; padding: 0; border-radius: 8px; overflow: hidden; height: 600px; display: flex; flex-direction: column; }
164
+ .preview-header { background: #eee; padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; color: #333; border-bottom: 1px solid #ddd; }
165
+ .preview-content { flex: 1; overflow: auto; padding: 15px; }
166
+ @media (max-width: 900px) { .preview-section { grid-template-columns: 1fr; } }
167
  </style>
168
  </head>
169
  <body>
170
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 20px;">
171
+ <h1>Intelligent Markdown Converter</h1>
172
+ <button onclick="document.body.classList.toggle('dark-mode')" class="btn-secondary">🌓 Toggle Dark Mode</button>
173
  </div>
174
+
175
  <fieldset>
176
+ <legend>1. Input Source</legend>
177
+ <textarea id="md-input" rows="10" placeholder="Paste your Markdown, Repo2Markdown, or Agent Action text here..."></textarea>
178
+ <div class="action-bar"><button onclick="analyze()" class="btn-primary" id="load-btn">Analyze & Extract Components</button></div>
179
  </fieldset>
180
+
181
  <div id="comp-section" style="display:none;">
182
  <div id="detected-format" class="format-banner"></div>
183
  <fieldset>
184
+ <legend>2. Component Selection</legend>
185
+ <div class="comp-grid" id="comp-list"></div>
186
  </fieldset>
187
  </div>
188
+
189
  <fieldset>
190
+ <legend>3. Visual Styling</legend>
191
  <div class="style-grid">
192
+ <div class="controls"><label>Font Family</label>
193
+ <select id="f_family">
194
+ <option value="'Inter', sans-serif">Inter (Modern)</option>
195
+ <option value="'Fira Code', monospace">Fira Code (Monospace)</option>
196
+ <option value="'Playfair Display', serif">Playfair (Serif)</option>
197
+ <option value="'Roboto', sans-serif">Roboto</option>
198
+ </select>
199
+ </div>
200
+ <div class="controls"><label>Font Size (px)</label><input type="number" id="f_size" value="16"></div>
201
+ <div class="controls"><label>Line Height</label><input type="number" id="l_height" value="1.6" step="0.1"></div>
202
+ <div class="controls"><label>Text Color</label><input type="color" id="t_color" value="#333333"></div>
203
+ <div class="controls"><label>Background</label><input type="color" id="b_color" value="#ffffff"></div>
204
+ <div class="controls"><label>Syntax Theme</label>
205
+ <select id="h_theme">
206
+ {% for s in styles %}<option value="{{s}}" {% if s == 'monokai' %}selected{% endif %}>{{s}}</option>{% endfor %}
207
+ </select>
208
+ </div>
209
+ </div>
210
+ <div style="margin-top:20px;">
211
+ <label><b>Custom CSS Overrides</b></label>
212
+ <textarea id="c_css" rows="3" placeholder="#output-wrapper h1 { color: purple; }"></textarea>
213
  </div>
214
  </fieldset>
215
+
216
+ <div class="action-bar">
217
+ <button onclick="process('preview')" class="btn-primary" id="gen-btn">GENERATE PREVIEW</button>
 
218
  </div>
219
+
220
+ <div id="preview-area" style="display:none;">
221
+ <div class="preview-section">
222
+ <div class="preview-box">
223
+ <div class="preview-header">
224
+ <span>HTML Preview</span>
225
+ <button class="btn-download" onclick="process('download', 'html')">Download .html</button>
226
+ </div>
227
+ <div id="html-prev" class="preview-content"></div>
228
+ </div>
229
+ <div class="preview-box">
230
+ <div class="preview-header">
231
+ <span>PNG Preview</span>
232
+ <button class="btn-download" onclick="process('download', 'png')">Download .png</button>
233
+ </div>
234
+ <div id="png-prev" class="preview-content" style="background:#f0f0f0; text-align:center;"></div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
  <script>
240
+ async function analyze() {
241
+ const btn = document.getElementById('load-btn');
242
+ const text = document.getElementById('md-input').value;
243
+ if(!text) return alert("Please enter some text first.");
244
+
245
+ btn.innerText = "Analyzing...";
246
+ const fd = new FormData();
247
+ fd.append('markdown_text', text);
248
+
249
+ try {
250
+ const res = await fetch('/parse', {method:'POST', body:fd});
251
+ const data = await res.json();
252
+ if(data.error) throw new Error(data.error);
253
+
254
+ document.getElementById('detected-format').innerText = "FORMAT DETECTED: " + data.format;
255
+ const list = document.getElementById('comp-list');
256
+ list.innerHTML = '';
257
+
258
+ data.components.forEach((c, i) => {
259
+ const safe = btoa(unescape(encodeURIComponent(c.content)));
260
+ list.innerHTML += `
261
+ <div class="comp-card">
262
+ <label style="cursor:pointer; display:block;">
263
+ <input type="checkbox" checked class="c-check" data-name="${c.filename}" data-content="${safe}">
264
+ <strong>${c.filename}</strong>
265
+ </label>
266
+ <small>${c.type}</small>
267
+ <textarea readonly>${c.content.substring(0,120)}...</textarea>
268
+ </div>`;
269
+ });
270
+ document.getElementById('comp-section').style.display = 'block';
271
+ } catch(e) { alert(e.message); }
272
+ finally { btn.innerText = "Analyze & Extract Components"; }
273
  }
274
+
275
+ async function process(action, type = null) {
276
+ const btn = document.getElementById('gen-btn');
277
+ let finalMarkdown = "";
278
+ const checks = document.querySelectorAll('.c-check');
279
+
280
+ if(checks.length > 0) {
281
+ checks.forEach(c => {
282
+ if(c.checked) {
283
+ const content = decodeURIComponent(escape(atob(c.dataset.content)));
284
+ if(!c.dataset.name.includes("Intro") && !c.dataset.name.includes("Structure")) {
285
+ finalMarkdown += "### File: " + c.dataset.name + "\\n";
286
+ }
287
+ finalMarkdown += content + "\\n\\n";
288
+ }
289
+ });
290
+ } else {
291
+ finalMarkdown = document.getElementById('md-input').value;
292
+ }
293
+
294
  const payload = {
295
+ markdown_text: finalMarkdown,
296
+ download: action === 'download',
297
+ download_type: type,
298
  styles: {
299
+ font_family: document.getElementById('f_family').value,
300
+ font_size: document.getElementById('f_size').value,
301
+ line_height: document.getElementById('l_height').value,
302
+ text_color: document.getElementById('t_color').value,
303
+ background_color: document.getElementById('b_color').value,
304
+ highlight_theme: document.getElementById('h_theme').value,
305
+ custom_css: document.getElementById('c_css').value,
306
+ page_padding: 40,
307
+ code_padding: 15
308
  }
309
  };
310
+
311
+ if(action === 'preview') btn.innerText = "Processing...";
312
+
313
+ try {
314
+ const res = await fetch('/convert', {
315
+ method: 'POST',
316
+ headers: {'Content-Type':'application/json'},
317
+ body: JSON.stringify(payload)
318
+ });
319
+
320
+ if(action === 'download') {
321
+ const blob = await res.blob();
322
+ const url = window.URL.createObjectURL(blob);
323
+ const a = document.createElement('a');
324
+ a.href = url;
325
+ a.download = `output_${new Date().getTime()}.${type}`;
326
+ a.click();
327
+ } else {
328
+ const data = await res.json();
329
+ if(data.error) throw new Error(data.error);
330
+ document.getElementById('preview-area').style.display = 'block';
331
+ document.getElementById('html-prev').innerHTML = data.preview_html;
332
+ document.getElementById('png-prev').innerHTML = `<img src="data:image/png;base64,${data.preview_png_base64}" style="max-width:100%; box-shadow: 0 0 20px rgba(0,0,0,0.2);">`;
333
+ window.scrollTo({ top: document.getElementById('preview-area').offsetTop, behavior: 'smooth' });
334
+ }
335
+ } catch(e) { alert("Error: " + e.message); }
336
+ finally { if(action === 'preview') btn.innerText = "GENERATE PREVIEW"; }
337
  }
338
  </script>
339
  </body></html>