broadfield-dev commited on
Commit
22c14b2
·
verified ·
1 Parent(s): 81502e6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -66
app.py CHANGED
@@ -9,7 +9,7 @@ import base64
9
  from pygments import highlight
10
  from pygments.lexers import get_lexer_by_name
11
  from pygments.formatters import HtmlFormatter
12
- from pygments.styles import get_all_styles
13
 
14
  app = Flask(__name__)
15
  TEMP_DIR = "/tmp/markdown_temp"
@@ -69,28 +69,37 @@ def parse_agent_action(text):
69
 
70
  def build_full_html(markdown_text, styles, for_image=False):
71
  wrapper_id = "#output-wrapper"
72
- # Default to sans-serif if missing
73
  font_family = styles.get('font_family', "sans-serif")
74
 
75
- # Only include Google Fonts link for the Web Preview.
76
- # Wkhtmltopdf (Image gen) often fails on HTTPS font links in Docker.
77
  google_font_link = ""
78
  if not for_image and "sans-serif" not in font_family and "monospace" not in font_family:
79
  clean_font_name = font_family.split(',')[0].strip("'\"")
80
  google_font_link = f'<link href="https://fonts.googleapis.com/css2?family={clean_font_name.replace(" ", "+")}:wght@400;700&display=swap" rel="stylesheet">'
81
 
82
- # Syntax Highlighting
83
  highlight_theme = styles.get('highlight_theme', 'monokai')
 
 
 
 
 
 
 
 
 
84
  pygments_css = ""
85
  if highlight_theme != 'none':
86
  try:
 
87
  formatter = HtmlFormatter(style=highlight_theme, cssclass="codehilite")
88
- pygments_css = formatter.get_style_defs(f' {wrapper_id}')
89
  except Exception:
90
- pygments_css = "" # Fallback if theme fails
91
 
92
  scoped_css = f"""
93
  body {{ background-color: {styles.get('background_color', '#ffffff')}; margin: 0; padding: 0; }}
 
94
  {wrapper_id} {{
95
  font-family: {font_family};
96
  font-size: {styles.get('font_size', '16')}px;
@@ -99,17 +108,57 @@ def build_full_html(markdown_text, styles, for_image=False):
99
  background-color: {styles.get('background_color', '#fff')};
100
  padding: {styles.get('page_padding', '40')}px;
101
  }}
 
 
102
  {wrapper_id} table {{ border-collapse: collapse; width: 100%; margin-bottom: 1em; }}
103
  {wrapper_id} th, {wrapper_id} td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
104
- {wrapper_id} pre {{ padding: {styles.get('code_padding', '15')}px; border-radius: 8px; overflow-x: auto; background: #222; color: #fff; }}
105
- {wrapper_id} code {{ font-family: 'Fira Code', monospace; }}
106
- {wrapper_id} h1, {wrapper_id} h2, {wrapper_id} h3 {{ border-bottom: 1px solid #eee; padding-bottom: 5px; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  {pygments_css}
 
 
108
  {styles.get('custom_css', '')}
109
  """
110
 
111
- # Convert Markdown to HTML
112
- html_content = markdown.markdown(markdown_text, extensions=['fenced_code', 'tables', 'codehilite', 'nl2br'])
 
 
 
113
 
114
  return f"""
115
  <!DOCTYPE html>
@@ -158,14 +207,14 @@ def convert_endpoint():
158
  # Build HTML for Image Generation (Cleaner, no external fonts)
159
  image_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), for_image=True)
160
 
161
- # Wkhtmltopdf Options - SAFEST MODE
162
  options = {
163
  "quiet": "",
164
  "encoding": "UTF-8",
165
  "width": 1024,
166
  "disable-smart-width": "",
167
- "enable-local-file-access": "", # Crucial for some docker envs
168
- "disable-javascript": "", # Prevents JS hangs
169
  "load-error-handling": "ignore",
170
  "load-media-error-handling": "ignore"
171
  }
@@ -194,11 +243,10 @@ def convert_endpoint():
194
  b64_img = base64.b64encode(png_bytes).decode('utf-8')
195
  except Exception as img_error:
196
  print(f"Image generation failed: {img_error}")
197
- # Return HTML at least, but warn about image
198
  return jsonify({
199
  'preview_html': preview_html,
200
  'preview_png_base64': None,
201
- 'warning': 'Image generation failed on server, but HTML is valid.'
202
  })
203
 
204
  return jsonify({
@@ -265,8 +313,8 @@ def index():
265
  <legend>3. Styles</legend>
266
  <div class="style-grid">
267
  <div><label>Font</label><select id="f_family">
268
- <option value="sans-serif">Sans-Serif (Standard)</option>
269
- <option value="'Inter', sans-serif">Inter (Web Only)</option>
270
  <option value="monospace">Monospace</option>
271
  <option value="serif">Serif</option>
272
  </select></div>
@@ -278,7 +326,7 @@ def index():
278
  {% for s in styles %}<option value="{{s}}" {% if s == 'monokai' %}selected{% endif %}>{{s}}</option>{% endfor %}
279
  </select></div>
280
  </div>
281
- <textarea id="c_css" rows="2" style="margin-top:15px;" placeholder="Custom CSS (e.g. #output-wrapper h1 { color: red; })"></textarea>
282
  </fieldset>
283
 
284
  <button onclick="process('preview')" class="btn-primary" id="gen-btn" style="width:100%; height:50px; font-size:18px;">GENERATE PREVIEW</button>
@@ -297,17 +345,16 @@ def index():
297
  PNG Preview
298
  <button class="btn-download" onclick="process('download', 'png')">Download PNG</button>
299
  </div>
300
- <div id="png-prev" class="preview-content" style="background:#f0f0f0; display:flex; align-items:center; justify-content:center; text-align:center; color:#666;"></div>
301
  </div>
302
  </div>
303
  </div>
304
 
305
  <script>
306
- // --- 1. ANALYSIS FUNCTION ---
307
  async function analyze() {
308
  const btn = document.getElementById('load-btn');
309
  const text = document.getElementById('md-input').value;
310
- if(!text) return alert("Please enter markdown text first.");
311
 
312
  btn.innerText = "Analyzing...";
313
  const fd = new FormData();
@@ -317,17 +364,13 @@ def index():
317
  const res = await fetch('/parse', {method:'POST', body:fd});
318
  const data = await res.json();
319
 
320
- if(data.error) {
321
- alert("Error parsing: " + data.error);
322
- return;
323
- }
324
 
325
  document.getElementById('detected-format').innerText = "Detected: " + data.format;
326
  const list = document.getElementById('comp-list');
327
  list.innerHTML = '';
328
 
329
  data.components.forEach(c => {
330
- // Safe encoding of content for HTML attribute
331
  const safe = btoa(unescape(encodeURIComponent(c.content)));
332
  list.innerHTML += `
333
  <div class="comp-card">
@@ -339,18 +382,12 @@ def index():
339
  </div>`;
340
  });
341
  document.getElementById('comp-section').style.display = 'block';
342
- } catch(e) {
343
- alert("Network/Server Error: " + e);
344
- } finally {
345
- btn.innerText = "Analyze Content";
346
- }
347
  }
348
 
349
- // --- 2. GENERATE / DOWNLOAD FUNCTION ---
350
  async function process(action, type = null) {
351
  const btn = document.getElementById('gen-btn');
352
-
353
- // Reconstruct Markdown from checked components
354
  let md = "";
355
  const checks = document.querySelectorAll('.c-check');
356
  let hasSelection = false;
@@ -359,15 +396,13 @@ def index():
359
  if(c.checked) {
360
  hasSelection = true;
361
  const content = decodeURIComponent(escape(atob(c.dataset.content)));
362
- // Add headers for file types, skip for intros
363
- if(!c.dataset.name.includes("Intro") && !c.dataset.name.includes("Structure") && !c.dataset.name.includes("Header")) {
364
  md += "### File: " + c.dataset.name + "\\n";
365
  }
366
  md += content + "\\n\\n";
367
  }
368
  });
369
 
370
- // Fallback to raw input if no components selected
371
  if(!hasSelection) md = document.getElementById('md-input').value;
372
 
373
  const payload = {
@@ -382,8 +417,7 @@ def index():
382
  background_color: document.getElementById('b_color').value,
383
  highlight_theme: document.getElementById('h_theme').value,
384
  custom_css: document.getElementById('c_css').value,
385
- page_padding: 40,
386
- code_padding: 15
387
  }
388
  };
389
 
@@ -397,47 +431,30 @@ def index():
397
  });
398
 
399
  if(action === 'download') {
400
- if(!res.ok) {
401
- const errData = await res.json();
402
- throw new Error(errData.error || "Download failed");
403
- }
404
  const blob = await res.blob();
405
- const url = window.URL.createObjectURL(blob);
406
  const a = document.createElement('a');
407
- a.href = url;
408
  a.download = `export.${type}`;
409
  a.click();
410
  } else {
411
  const data = await res.json();
412
-
413
- // CRITICAL: Check for server-side errors before trying to read data
414
- if(data.error) {
415
- throw new Error(data.error);
416
- }
417
 
418
  document.getElementById('preview-area').style.display = 'block';
 
419
 
420
- // Set HTML
421
- document.getElementById('html-prev').innerHTML = data.preview_html || "No HTML generated.";
422
-
423
- // Set PNG
424
  const pngContainer = document.getElementById('png-prev');
425
  if (data.preview_png_base64) {
426
- pngContainer.innerHTML = `<img src="data:image/png;base64,${data.preview_png_base64}" alt="Preview">`;
427
- } else if (data.warning) {
428
- pngContainer.innerHTML = `<p style="color:orange">⚠️ ${data.warning}</p>`;
429
  } else {
430
- pngContainer.innerHTML = "<p>No image generated.</p>";
431
  }
432
 
433
- // Scroll to preview
434
  document.getElementById('preview-area').scrollIntoView({behavior: 'smooth'});
435
  }
436
- } catch(e) {
437
- alert("Error: " + e.message);
438
- } finally {
439
- if(action === 'preview') btn.innerText = "GENERATE PREVIEW";
440
- }
441
  }
442
  </script>
443
  </body></html>
 
9
  from pygments import highlight
10
  from pygments.lexers import get_lexer_by_name
11
  from pygments.formatters import HtmlFormatter
12
+ from pygments.styles import get_all_styles, get_style_by_name
13
 
14
  app = Flask(__name__)
15
  TEMP_DIR = "/tmp/markdown_temp"
 
69
 
70
  def build_full_html(markdown_text, styles, for_image=False):
71
  wrapper_id = "#output-wrapper"
 
72
  font_family = styles.get('font_family', "sans-serif")
73
 
74
+ # --- Fonts ---
 
75
  google_font_link = ""
76
  if not for_image and "sans-serif" not in font_family and "monospace" not in font_family:
77
  clean_font_name = font_family.split(',')[0].strip("'\"")
78
  google_font_link = f'<link href="https://fonts.googleapis.com/css2?family={clean_font_name.replace(" ", "+")}:wght@400;700&display=swap" rel="stylesheet">'
79
 
80
+ # --- Syntax Highlighting & Code Box Styling ---
81
  highlight_theme = styles.get('highlight_theme', 'monokai')
82
+
83
+ # We explicitly determine the background color of the theme to style the "Box"
84
+ try:
85
+ style_obj = get_style_by_name(highlight_theme)
86
+ bg_color = style_obj.background_color
87
+ except:
88
+ bg_color = "#272822" if highlight_theme == 'monokai' else "#f6f8fa"
89
+
90
+ # Generate the CSS for syntax highlighting
91
  pygments_css = ""
92
  if highlight_theme != 'none':
93
  try:
94
+ # We use 'codehilite' class which Markdown adds by default
95
  formatter = HtmlFormatter(style=highlight_theme, cssclass="codehilite")
96
+ pygments_css = formatter.get_style_defs('.codehilite')
97
  except Exception:
98
+ pygments_css = ""
99
 
100
  scoped_css = f"""
101
  body {{ background-color: {styles.get('background_color', '#ffffff')}; margin: 0; padding: 0; }}
102
+
103
  {wrapper_id} {{
104
  font-family: {font_family};
105
  font-size: {styles.get('font_size', '16')}px;
 
108
  background-color: {styles.get('background_color', '#fff')};
109
  padding: {styles.get('page_padding', '40')}px;
110
  }}
111
+
112
+ /* Tables */
113
  {wrapper_id} table {{ border-collapse: collapse; width: 100%; margin-bottom: 1em; }}
114
  {wrapper_id} th, {wrapper_id} td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
115
+
116
+ /* Headers */
117
+ {wrapper_id} h1, {wrapper_id} h2, {wrapper_id} h3 {{ border-bottom: 1px solid #eee; padding-bottom: 5px; margin-top: 1.5em; }}
118
+
119
+ /* --- CODE BOX STYLING (CRITICAL) --- */
120
+
121
+ /* 1. The wrapper div generated by Pygments */
122
+ {wrapper_id} .codehilite {{
123
+ background: {bg_color};
124
+ padding: {styles.get('code_padding', '15')}px;
125
+ border-radius: 8px;
126
+ margin: 1em 0;
127
+ overflow-x: auto;
128
+ border: 1px solid rgba(0,0,0,0.1);
129
+ }}
130
+
131
+ /* 2. The pre tag inside the wrapper */
132
+ {wrapper_id} .codehilite pre {{
133
+ margin: 0;
134
+ padding: 0;
135
+ background: transparent; /* inherit from .codehilite */
136
+ color: inherit;
137
+ font-family: 'Fira Code', 'Consolas', monospace;
138
+ line-height: 1.4;
139
+ }}
140
+
141
+ /* 3. Fallback for plain code blocks not caught by Pygments */
142
+ {wrapper_id} pre:not([class]) {{
143
+ background: #f4f4f4;
144
+ padding: 15px;
145
+ border-radius: 8px;
146
+ overflow-x: auto;
147
+ font-family: monospace;
148
+ }}
149
+
150
+ /* Syntax Highlighting CSS */
151
  {pygments_css}
152
+
153
+ /* User Custom Overrides */
154
  {styles.get('custom_css', '')}
155
  """
156
 
157
+ # Convert Markdown to HTML with the 'codehilite' extension
158
+ html_content = markdown.markdown(
159
+ markdown_text,
160
+ extensions=['fenced_code', 'tables', 'codehilite', 'nl2br']
161
+ )
162
 
163
  return f"""
164
  <!DOCTYPE html>
 
207
  # Build HTML for Image Generation (Cleaner, no external fonts)
208
  image_html = build_full_html(data.get('markdown_text', ''), data.get('styles', {}), for_image=True)
209
 
210
+ # Wkhtmltopdf Options
211
  options = {
212
  "quiet": "",
213
  "encoding": "UTF-8",
214
  "width": 1024,
215
  "disable-smart-width": "",
216
+ "enable-local-file-access": "",
217
+ "disable-javascript": "",
218
  "load-error-handling": "ignore",
219
  "load-media-error-handling": "ignore"
220
  }
 
243
  b64_img = base64.b64encode(png_bytes).decode('utf-8')
244
  except Exception as img_error:
245
  print(f"Image generation failed: {img_error}")
 
246
  return jsonify({
247
  'preview_html': preview_html,
248
  'preview_png_base64': None,
249
+ 'warning': 'Image generation failed on server.'
250
  })
251
 
252
  return jsonify({
 
313
  <legend>3. Styles</legend>
314
  <div class="style-grid">
315
  <div><label>Font</label><select id="f_family">
316
+ <option value="sans-serif">Sans-Serif</option>
317
+ <option value="'Inter', sans-serif">Inter</option>
318
  <option value="monospace">Monospace</option>
319
  <option value="serif">Serif</option>
320
  </select></div>
 
326
  {% for s in styles %}<option value="{{s}}" {% if s == 'monokai' %}selected{% endif %}>{{s}}</option>{% endfor %}
327
  </select></div>
328
  </div>
329
+ <textarea id="c_css" rows="2" style="margin-top:15px;" placeholder="Custom CSS..."></textarea>
330
  </fieldset>
331
 
332
  <button onclick="process('preview')" class="btn-primary" id="gen-btn" style="width:100%; height:50px; font-size:18px;">GENERATE PREVIEW</button>
 
345
  PNG Preview
346
  <button class="btn-download" onclick="process('download', 'png')">Download PNG</button>
347
  </div>
348
+ <div id="png-prev" class="preview-content" style="background:#f0f0f0; display:flex; align-items:center; justify-content:center; text-align:center;"></div>
349
  </div>
350
  </div>
351
  </div>
352
 
353
  <script>
 
354
  async function analyze() {
355
  const btn = document.getElementById('load-btn');
356
  const text = document.getElementById('md-input').value;
357
+ if(!text) return alert("Please enter text.");
358
 
359
  btn.innerText = "Analyzing...";
360
  const fd = new FormData();
 
364
  const res = await fetch('/parse', {method:'POST', body:fd});
365
  const data = await res.json();
366
 
367
+ if(data.error) { alert(data.error); return; }
 
 
 
368
 
369
  document.getElementById('detected-format').innerText = "Detected: " + data.format;
370
  const list = document.getElementById('comp-list');
371
  list.innerHTML = '';
372
 
373
  data.components.forEach(c => {
 
374
  const safe = btoa(unescape(encodeURIComponent(c.content)));
375
  list.innerHTML += `
376
  <div class="comp-card">
 
382
  </div>`;
383
  });
384
  document.getElementById('comp-section').style.display = 'block';
385
+ } catch(e) { alert(e); }
386
+ finally { btn.innerText = "Analyze Content"; }
 
 
 
387
  }
388
 
 
389
  async function process(action, type = null) {
390
  const btn = document.getElementById('gen-btn');
 
 
391
  let md = "";
392
  const checks = document.querySelectorAll('.c-check');
393
  let hasSelection = false;
 
396
  if(c.checked) {
397
  hasSelection = true;
398
  const content = decodeURIComponent(escape(atob(c.dataset.content)));
399
+ if(!c.dataset.name.includes("Intro") && !c.dataset.name.includes("Structure")) {
 
400
  md += "### File: " + c.dataset.name + "\\n";
401
  }
402
  md += content + "\\n\\n";
403
  }
404
  });
405
 
 
406
  if(!hasSelection) md = document.getElementById('md-input').value;
407
 
408
  const payload = {
 
417
  background_color: document.getElementById('b_color').value,
418
  highlight_theme: document.getElementById('h_theme').value,
419
  custom_css: document.getElementById('c_css').value,
420
+ page_padding: 40, code_padding: 15
 
421
  }
422
  };
423
 
 
431
  });
432
 
433
  if(action === 'download') {
434
+ if(!res.ok) throw new Error("Download failed");
 
 
 
435
  const blob = await res.blob();
 
436
  const a = document.createElement('a');
437
+ a.href = URL.createObjectURL(blob);
438
  a.download = `export.${type}`;
439
  a.click();
440
  } else {
441
  const data = await res.json();
442
+ if(data.error) throw new Error(data.error);
 
 
 
 
443
 
444
  document.getElementById('preview-area').style.display = 'block';
445
+ document.getElementById('html-prev').innerHTML = data.preview_html;
446
 
 
 
 
 
447
  const pngContainer = document.getElementById('png-prev');
448
  if (data.preview_png_base64) {
449
+ pngContainer.innerHTML = `<img src="data:image/png;base64,${data.preview_png_base64}">`;
 
 
450
  } else {
451
+ pngContainer.innerHTML = `<p style="color:red">${data.warning || "Error generating image"}</p>`;
452
  }
453
 
 
454
  document.getElementById('preview-area').scrollIntoView({behavior: 'smooth'});
455
  }
456
+ } catch(e) { alert("Error: " + e.message); }
457
+ finally { if(action === 'preview') btn.innerText = "GENERATE PREVIEW"; }
 
 
 
458
  }
459
  </script>
460
  </body></html>