broadfield-dev commited on
Commit
fb41e6b
·
verified ·
1 Parent(s): f6e05b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -53
app.py CHANGED
@@ -43,7 +43,7 @@ def parse_changelog(text):
43
  components = []
44
  parts = re.split(r'^(## \[\d+\.\d+\.\d+.*?\].*?)$', text, flags=re.MULTILINE)
45
  if parts[0].strip():
46
- components.append({'type': 'intro', 'filename': 'Header', 'content': parts[0].strip()})
47
  for i in range(1, len(parts), 2):
48
  components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
49
  return components
@@ -62,8 +62,6 @@ def build_full_html(markdown_text, styles, for_image=False):
62
  wrapper_id = "#output-wrapper"
63
  font_family = styles.get('font_family', "sans-serif")
64
 
65
- # If rendering for an image, we avoid external network calls to Google Fonts
66
- # as wkhtmltoimage often fails on SSL handshakes in Docker.
67
  google_font_link = ""
68
  if not for_image:
69
  google_font_name = font_family.split(',')[0].strip("'\"")
@@ -117,7 +115,6 @@ def parse_endpoint():
117
  def convert_endpoint():
118
  data = request.json
119
  try:
120
- # Generate two versions: one for UI (with external fonts) and one for Image (clean)
121
  preview_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), for_image=False)
122
  image_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), for_image=True)
123
 
@@ -126,7 +123,7 @@ def convert_endpoint():
126
  "encoding": "UTF-8",
127
  "width": 1024,
128
  "disable-smart-width": "",
129
- "load-error-handling": "ignore", # CRITICAL: Prevents crash on minor network errors
130
  "load-media-error-handling": "ignore"
131
  }
132
 
@@ -142,7 +139,6 @@ def convert_endpoint():
142
  'preview_png_base64': base64.b64encode(png_bytes).decode('utf-8')
143
  })
144
  except Exception as e:
145
- traceback.print_exc()
146
  return jsonify({'error': str(e)}), 500
147
 
148
  @app.route('/')
@@ -155,83 +151,76 @@ def index():
155
  <style>
156
  :root { --bg: #f4f7f6; --text: #333; --card: #fff; --border: #ddd; --primary: #5a32a3; }
157
  body.dark-mode { --bg: #1a1a1a; --text: #eee; --card: #252525; --border: #444; }
158
- 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; }
159
- 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); }
160
  legend { font-weight: bold; padding: 0 10px; color: var(--primary); }
161
  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; }
162
  .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; }
163
- .style-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
164
- .comp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
165
  .comp-card { background: var(--card); border: 1px solid var(--border); padding: 12px; border-radius: 8px; }
166
  .comp-card textarea { height: 60px; font-size: 11px; margin-top: 8px; opacity: 0.8; pointer-events: none; resize: none; }
167
- .action-bar { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; }
168
- button { padding: 12px 24px; cursor: pointer; border: none; border-radius: 6px; font-weight: bold; transition: opacity 0.2s; }
169
- .btn-primary { background: var(--primary); color: #fff; flex: 1; min-width: 200px; }
170
  .btn-secondary { background: #333; color: #fff; border: 1px solid #555; }
171
- .btn-download { background: #28a745; color: white; font-size: 12px; padding: 5px 10px; }
172
  .preview-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 30px; }
173
- .preview-box { background: #fff; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; height: 600px; display: flex; flex-direction: column; }
174
- .preview-header { background: #eee; padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; color: #333; font-weight:bold; }
175
  .preview-content { flex: 1; overflow: auto; padding: 15px; }
 
176
  </style>
177
  </head>
178
  <body>
179
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 20px;">
180
- <h1>Intelligent Markdown Converter</h1>
181
- <button onclick="document.body.classList.toggle('dark-mode')" class="btn-secondary">🌓 Toggle Dark Mode</button>
182
  </div>
183
 
184
  <fieldset>
185
- <legend>1. Input Source</legend>
186
- <textarea id="md-input" rows="10" placeholder="Paste Markdown text here..."></textarea>
187
- <div class="action-bar"><button onclick="analyze()" class="btn-primary" id="load-btn">Analyze & Extract Components</button></div>
188
  </fieldset>
189
 
190
  <div id="comp-section" style="display:none;">
191
  <div id="detected-format" class="format-banner"></div>
192
  <fieldset>
193
- <legend>2. Component Selection</legend>
194
  <div class="comp-grid" id="comp-list"></div>
195
  </fieldset>
196
  </div>
197
 
198
  <fieldset>
199
- <legend>3. Visual Styling</legend>
200
  <div class="style-grid">
201
- <div><label>Font</label>
202
- <select id="f_family">
203
- <option value="Arial, sans-serif">Arial</option>
204
- <option value="'Courier New', monospace">Courier</option>
205
- <option value="'Times New Roman', serif">Times</option>
206
- </select>
207
- </div>
208
- <div><label>Font Size</label><input type="number" id="f_size" value="16"></div>
209
- <div><label>Line Height</label><input type="number" id="l_height" value="1.6" step="0.1"></div>
210
- <div><label>Text Color</label><input type="color" id="t_color" value="#333333"></div>
211
- <div><label>Background</label><input type="color" id="b_color" value="#ffffff"></div>
212
- <div><label>Syntax</label>
213
- <select id="h_theme">
214
- {% for s in styles %}<option value="{{s}}" {% if s == 'monokai' %}selected{% endif %}>{{s}}</option>{% endfor %}
215
- </select>
216
- </div>
217
- </div>
218
- <div style="margin-top:15px;">
219
- <label><b>Custom CSS Overrides</b></label>
220
- <textarea id="c_css" rows="3" placeholder="#output-wrapper h1 { color: red; }"></textarea>
221
  </div>
 
222
  </fieldset>
223
 
224
- <button onclick="process('preview')" class="btn-primary" id="gen-btn" style="width:100%; height:60px; font-size:20px;">GENERATE PREVIEW</button>
225
 
226
  <div id="preview-area" style="display:none;">
227
  <div class="preview-section">
228
  <div class="preview-box">
229
- <div class="preview-header">HTML Preview <button class="btn-download" onclick="process('download', 'html')">Download</button></div>
230
  <div id="html-prev" class="preview-content"></div>
231
  </div>
232
  <div class="preview-box">
233
- <div class="preview-header">PNG Preview <button class="btn-download" onclick="process('download', 'png')">Download</button></div>
234
- <div id="png-prev" class="preview-content" style="background:#888; display:flex; align-items:center; justify-content:center;"></div>
235
  </div>
236
  </div>
237
  </div>
@@ -241,13 +230,13 @@ def index():
241
  const fd = new FormData(); fd.append('markdown_text', document.getElementById('md-input').value);
242
  const res = await fetch('/parse', {method:'POST', body:fd});
243
  const data = await res.json();
244
- document.getElementById('detected-format').innerText = "FORMAT: " + data.format;
245
  const list = document.getElementById('comp-list'); list.innerHTML = '';
246
  data.components.forEach(c => {
247
  const safe = btoa(unescape(encodeURIComponent(c.content)));
248
  list.innerHTML += `<div class="comp-card">
249
  <label><input type="checkbox" checked class="c-check" data-name="${c.filename}" data-content="${safe}"> <b>${c.filename}</b></label>
250
- <textarea readonly>${c.content.substring(0,100)}</textarea>
251
  </div>`;
252
  });
253
  document.getElementById('comp-section').style.display = 'block';
@@ -281,17 +270,17 @@ def index():
281
  }
282
  };
283
 
284
- if(action === 'preview') btn.innerText = "Generating...";
285
  const res = await fetch('/convert', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)});
286
 
287
  if(action === 'download') {
288
  const blob = await res.blob();
289
- const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `file.${type}`; a.click();
290
  } else {
291
  const data = await res.json();
292
  document.getElementById('preview-area').style.display = 'block';
293
  document.getElementById('html-prev').innerHTML = data.preview_html;
294
- document.getElementById('png-prev').innerHTML = `<img src="data:image/png;base64,${data.preview_png_base64}" style="max-width:100%;">`;
295
  btn.innerText = "GENERATE PREVIEW";
296
  }
297
  }
 
43
  components = []
44
  parts = re.split(r'^(## \[\d+\.\d+\.\d+.*?\].*?)$', text, flags=re.MULTILINE)
45
  if parts[0].strip():
46
+ components.append({'type': 'intro', 'filename': 'Changelog Header', 'content': parts[0].strip()})
47
  for i in range(1, len(parts), 2):
48
  components.append({'type': 'version', 'filename': parts[i].replace('##', '').strip(), 'content': parts[i+1].strip()})
49
  return components
 
62
  wrapper_id = "#output-wrapper"
63
  font_family = styles.get('font_family', "sans-serif")
64
 
 
 
65
  google_font_link = ""
66
  if not for_image:
67
  google_font_name = font_family.split(',')[0].strip("'\"")
 
115
  def convert_endpoint():
116
  data = request.json
117
  try:
 
118
  preview_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), for_image=False)
119
  image_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), for_image=True)
120
 
 
123
  "encoding": "UTF-8",
124
  "width": 1024,
125
  "disable-smart-width": "",
126
+ "load-error-handling": "ignore",
127
  "load-media-error-handling": "ignore"
128
  }
129
 
 
139
  'preview_png_base64': base64.b64encode(png_bytes).decode('utf-8')
140
  })
141
  except Exception as e:
 
142
  return jsonify({'error': str(e)}), 500
143
 
144
  @app.route('/')
 
151
  <style>
152
  :root { --bg: #f4f7f6; --text: #333; --card: #fff; --border: #ddd; --primary: #5a32a3; }
153
  body.dark-mode { --bg: #1a1a1a; --text: #eee; --card: #252525; --border: #444; }
154
+ body { font-family: 'Inter', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: var(--bg); color: var(--text); transition: background 0.3s; }
155
+ fieldset { border: 1px solid var(--border); background: var(--card); padding: 20px; margin-bottom: 25px; border-radius: 12px; }
156
  legend { font-weight: bold; padding: 0 10px; color: var(--primary); }
157
  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; }
158
  .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; }
159
+ .style-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; }
160
+ .comp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
161
  .comp-card { background: var(--card); border: 1px solid var(--border); padding: 12px; border-radius: 8px; }
162
  .comp-card textarea { height: 60px; font-size: 11px; margin-top: 8px; opacity: 0.8; pointer-events: none; resize: none; }
163
+ .action-bar { display: flex; gap: 15px; margin-top: 20px; }
164
+ button { padding: 12px 24px; cursor: pointer; border: none; border-radius: 6px; font-weight: bold; }
165
+ .btn-primary { background: var(--primary); color: #fff; }
166
  .btn-secondary { background: #333; color: #fff; border: 1px solid #555; }
 
167
  .preview-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 30px; }
168
+ .preview-box { background: #fff; border: 1px solid #ddd; border-radius: 8px; height: 600px; display: flex; flex-direction: column; overflow: hidden; }
169
+ .preview-header { background: #eee; padding: 10px; display: flex; justify-content: space-between; align-items: center; color: #333; font-weight: bold; border-bottom: 1px solid #ddd; }
170
  .preview-content { flex: 1; overflow: auto; padding: 15px; }
171
+ img { max-width: 100%; height: auto; border: 1px solid #eee; }
172
  </style>
173
  </head>
174
  <body>
175
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
176
+ <h1>Markdown Tool</h1>
177
+ <button onclick="document.body.classList.toggle('dark-mode')" class="btn-secondary">🌓 Dark Mode</button>
178
  </div>
179
 
180
  <fieldset>
181
+ <legend>1. Input</legend>
182
+ <textarea id="md-input" rows="8"></textarea>
183
+ <div class="action-bar"><button onclick="analyze()" class="btn-primary" id="load-btn">Analyze Content</button></div>
184
  </fieldset>
185
 
186
  <div id="comp-section" style="display:none;">
187
  <div id="detected-format" class="format-banner"></div>
188
  <fieldset>
189
+ <legend>2. Components</legend>
190
  <div class="comp-grid" id="comp-list"></div>
191
  </fieldset>
192
  </div>
193
 
194
  <fieldset>
195
+ <legend>3. Styles</legend>
196
  <div class="style-grid">
197
+ <div><label>Font</label><select id="f_family">
198
+ <option value="sans-serif">Sans-Serif</option>
199
+ <option value="serif">Serif</option>
200
+ <option value="monospace">Monospace</option>
201
+ </select></div>
202
+ <div><label>Size</label><input type="number" id="f_size" value="16"></div>
203
+ <div><label>Height</label><input type="number" id="l_height" value="1.6" step="0.1"></div>
204
+ <div><label>Text</label><input type="color" id="t_color" value="#333333"></div>
205
+ <div><label>BG</label><input type="color" id="b_color" value="#ffffff"></div>
206
+ <div><label>Syntax</label><select id="h_theme">
207
+ {% for s in styles %}<option value="{{s}}" {% if s == 'monokai' %}selected{% endif %}>{{s}}</option>{% endfor %}
208
+ </select></div>
 
 
 
 
 
 
 
 
209
  </div>
210
+ <textarea id="c_css" rows="2" style="margin-top:15px;" placeholder="Custom CSS..."></textarea>
211
  </fieldset>
212
 
213
+ <button onclick="process('preview')" class="btn-primary" id="gen-btn" style="width:100%; height:50px;">GENERATE PREVIEW</button>
214
 
215
  <div id="preview-area" style="display:none;">
216
  <div class="preview-section">
217
  <div class="preview-box">
218
+ <div class="preview-header">HTML <button onclick="process('download', 'html')">Download</button></div>
219
  <div id="html-prev" class="preview-content"></div>
220
  </div>
221
  <div class="preview-box">
222
+ <div class="preview-header">PNG <button onclick="process('download', 'png')">Download</button></div>
223
+ <div id="png-prev" class="preview-content"></div>
224
  </div>
225
  </div>
226
  </div>
 
230
  const fd = new FormData(); fd.append('markdown_text', document.getElementById('md-input').value);
231
  const res = await fetch('/parse', {method:'POST', body:fd});
232
  const data = await res.json();
233
+ document.getElementById('detected-format').innerText = "Detected: " + data.format;
234
  const list = document.getElementById('comp-list'); list.innerHTML = '';
235
  data.components.forEach(c => {
236
  const safe = btoa(unescape(encodeURIComponent(c.content)));
237
  list.innerHTML += `<div class="comp-card">
238
  <label><input type="checkbox" checked class="c-check" data-name="${c.filename}" data-content="${safe}"> <b>${c.filename}</b></label>
239
+ <textarea readonly>${c.content.substring(0,80)}</textarea>
240
  </div>`;
241
  });
242
  document.getElementById('comp-section').style.display = 'block';
 
270
  }
271
  };
272
 
273
+ if(action === 'preview') btn.innerText = "Processing...";
274
  const res = await fetch('/convert', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)});
275
 
276
  if(action === 'download') {
277
  const blob = await res.blob();
278
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `export.${type}`; a.click();
279
  } else {
280
  const data = await res.json();
281
  document.getElementById('preview-area').style.display = 'block';
282
  document.getElementById('html-prev').innerHTML = data.preview_html;
283
+ document.getElementById('png-prev').innerHTML = `<img src="data:image/png;base64,${data.preview_png_base64}">`;
284
  btn.innerText = "GENERATE PREVIEW";
285
  }
286
  }