broadfield-dev commited on
Commit
fa73f36
·
verified ·
1 Parent(s): 471be14

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +138 -81
app.py CHANGED
@@ -6,6 +6,12 @@ import traceback
6
  from io import BytesIO
7
  import re
8
 
 
 
 
 
 
 
9
  app = Flask(__name__)
10
 
11
  # Configure a temporary directory for file operations
@@ -21,47 +27,25 @@ def is_repo2markdown_format(text):
21
  def parse_repo2markdown(text):
22
  """Parses Repo2Markdown text, extracts files, and prepares them for the UI."""
23
  components = []
24
- # Regex to find sections starting with '### File:'
25
  pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
26
 
27
  first_match = pattern.search(text)
28
  if first_match:
29
  intro_text = text[:first_match.start()].strip()
30
  if intro_text:
31
- components.append({
32
- 'type': 'intro',
33
- 'filename': 'Introduction',
34
- 'content': intro_text,
35
- 'is_code_block': False,
36
- 'language': ''
37
- })
38
 
39
  for match in pattern.finditer(text):
40
  filename = match.group(1).strip()
41
  raw_content = match.group(2).strip()
42
-
43
- # Check if the content is a single fenced code block and extract its parts
44
  code_match = re.search(r'^```(\w*)\s*\n([\s\S]*?)\s*```$', raw_content, re.DOTALL)
45
 
46
  if code_match:
47
  language = code_match.group(1)
48
  inner_content = code_match.group(2).strip()
49
- components.append({
50
- 'type': 'file',
51
- 'filename': filename,
52
- 'content': inner_content,
53
- 'is_code_block': True,
54
- 'language': language
55
- })
56
  else:
57
- # It's plain text (e.g., binary file notification)
58
- components.append({
59
- 'type': 'file',
60
- 'filename': filename,
61
- 'content': raw_content,
62
- 'is_code_block': False,
63
- 'language': ''
64
- })
65
 
66
  return components
67
 
@@ -81,12 +65,10 @@ def parse_endpoint():
81
 
82
  if is_repo2markdown_format(text):
83
  try:
84
- components = parse_repo2markdown(text)
85
- return jsonify(components)
86
  except Exception as e:
87
  return jsonify({'error': f'Failed to parse Repo2Markdown: {str(e)}'}), 500
88
  else:
89
- # If not the special format, return it as a single "text" component
90
  return jsonify([{'type': 'text', 'filename': 'Full Text', 'content': text}])
91
 
92
  @app.route('/convert', methods=['POST'])
@@ -98,27 +80,69 @@ def convert_endpoint():
98
  include_fontawesome = data.get('include_fontawesome', False)
99
  download_type = data.get('download_type', 'png')
100
  is_download_request = data.get('download', False)
101
-
102
- if not markdown_text:
103
- return jsonify({'error': 'No markdown content to convert.'}), 400
104
-
105
  try:
106
- html_content = markdown.markdown(markdown_text, extensions=['fenced_code', 'tables'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
 
 
 
108
  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 ""
109
- style_block = f"""<style>
110
- body {{ font-family: {styles.get('font_family', 'Arial, sans-serif')}; font-size: {styles.get('font_size', '16')}px; color: {styles.get('text_color', '#333')}; background-color: {styles.get('background_color', '#fff')}; padding: 25px; display: inline-block; }}
111
- h3 {{ border-bottom: 1px solid #ccc; padding-bottom: 5px; margin-top: 2em; }}
112
- table {{ border-collapse: collapse; width: 100%; }}
113
- th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
114
- th {{ background-color: #f2f2f2; }}
115
- img {{ max-width: 100%; height: auto; }}
116
- pre {{ background: {styles.get('code_bg_color', '#f4f4f4')}; padding: {styles.get('code_padding', '15')}px; border-radius: 5px; white-space: pre-wrap; word-wrap: break-word; }}
117
- code {{ background: {styles.get('code_bg_color', '#f4f4f4')}; padding: 0.2em 0.4em; margin: 0; font-size: 85%; border-radius: 3px; }}
118
- pre > code {{ padding: 0; margin: 0; font-size: inherit; background: transparent; border-radius: 0; }}
119
- {styles.get('custom_css', '')}
120
- </style>"""
121
- full_html = f'<!DOCTYPE html><html><head><meta charset="UTF-8">{fontawesome_link}{style_block}</head><body>{html_content}</body></html>'
 
 
 
 
122
 
123
  if is_download_request:
124
  if download_type == 'html':
@@ -128,18 +152,19 @@ def convert_endpoint():
128
  imgkit.from_string(full_html, png_path, options={"quiet": "", 'encoding': "UTF-8"})
129
  return send_file(png_path, as_attachment=True, download_name="output.png", mimetype="image/png")
130
  else:
131
- # Return HTML for preview
132
  return jsonify({'preview_html': full_html})
133
 
134
  except Exception as e:
135
  traceback.print_exc()
136
  return jsonify({'error': f'Failed to convert content: {str(e)}'}), 500
137
 
 
138
  # --- MAIN PAGE (Front-end) ---
139
 
140
  @app.route('/')
141
  def index():
142
  """Serves the main HTML page with all the client-side JavaScript."""
 
143
  return render_template_string("""
144
  <!DOCTYPE html>
145
  <html lang="en">
@@ -154,19 +179,20 @@ def index():
154
  textarea { width: 100%; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; padding: 10px; font-family: monospace; }
155
  fieldset { border: 1px solid #ddd; padding: 15px; border-radius: 5px; margin-top: 20px; }
156
  legend { font-weight: bold; color: #555; padding: 0 10px; }
 
157
  button { padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; }
158
  .action-btn { background-color: #007BFF; color: white; font-size: 16px; padding: 12px 20px;}
159
  .action-btn:hover { background-color: #0056b3; }
160
  .generate-btn { background-color: #5a32a3; color: white; font-size: 16px; padding: 12px 20px; }
161
  .generate-btn:hover { background-color: #4a298a; }
162
- .download-btn { background-color: #28a745; color: white; display: none; } /* Hidden by default */
163
  .download-btn:hover { background-color: #218838; }
164
  .controls { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 20px; margin-top: 20px; }
165
  .main-actions { display: flex; flex-wrap: wrap; gap: 15px; align-items: center; }
166
- .preview-container { border: 1px solid #ddd; padding: 20px; margin-top: 20px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); min-height: 100px; }
167
  .error { color: #D8000C; background-color: #FFD2D2; padding: 10px; border-radius: 5px; margin-top: 15px; display: none; }
168
  .info { color: #00529B; background-color: #BDE5F8; padding: 10px; border-radius: 5px; margin: 10px 0; }
169
- /* Component Selection Styles */
170
  .component-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
171
  .component-container { border: 1px solid #e0e0e0; border-radius: 5px; background: #fafafa; }
172
  .component-header { background: #f1f1f1; padding: 8px 12px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 10px; }
@@ -179,7 +205,7 @@ def index():
179
  </head>
180
  <body>
181
  <h1>Advanced Markdown Converter & Composer</h1>
182
- <form id="main-form">
183
  <fieldset>
184
  <legend>1. Load Content</legend>
185
  <div class="info">Paste content or upload a file, then click "Load & Analyze".</div>
@@ -205,15 +231,60 @@ def index():
205
  <fieldset>
206
  <legend>3. Configure Styles</legend>
207
  <div class="style-grid">
208
- <div><label>Font Family:</label><select id="font_family"><option value="'Arial', sans-serif">Arial</option><option value="'Georgia', serif">Georgia</option><option value="'Times New Roman', serif">Times New Roman</option></select></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  <div><label>Font Size (px):</label><input type="number" id="font_size" value="16"></div>
 
 
 
 
 
 
 
 
 
210
  <div><label>Text Color:</label><input type="color" id="text_color" value="#333333"></div>
211
  <div><label>Background Color:</label><input type="color" id="background_color" value="#ffffff"></div>
212
- <div><label>Code BG Color:</label><input type="color" id="code_bg_color" value="#f4f4f4"></div>
213
  <div><label>Code Padding (px):</label><input type="number" id="code_padding" value="15"></div>
214
  </div>
215
  <div><input type="checkbox" id="include_fontawesome"><label for="include_fontawesome">Include Font Awesome</label></div>
216
- <div><label for="custom_css">Custom CSS:</label><textarea id="custom_css" rows="3"></textarea></div>
217
  </fieldset>
218
 
219
  <div class="controls">
@@ -231,7 +302,6 @@ def index():
231
  <div id="preview-container" class="preview-container"></div>
232
 
233
  <script>
234
- // --- DOM Elements ---
235
  const loadBtn = document.getElementById('load-btn');
236
  const generateBtn = document.getElementById('generate-btn');
237
  const downloadBtn = document.getElementById('download-btn');
@@ -242,7 +312,6 @@ def index():
242
  const previewContainer = document.getElementById('preview-container');
243
  const errorBox = document.getElementById('error-box');
244
 
245
- // --- Client-Side Functions ---
246
  function toggleAllComponents(checked) {
247
  componentsContainer.querySelectorAll('.component-checkbox').forEach(cb => cb.checked = checked);
248
  }
@@ -253,10 +322,12 @@ def index():
253
  previewContainer.innerHTML = '';
254
  }
255
 
256
- // --- Event Listeners ---
257
  loadBtn.addEventListener('click', async () => {
258
  loadBtn.textContent = 'Loading...';
259
  errorBox.style.display = 'none';
 
 
 
260
 
261
  const formData = new FormData();
262
  if (markdownFileInput.files.length > 0) {
@@ -269,14 +340,9 @@ def index():
269
  const response = await fetch('/parse', { method: 'POST', body: formData });
270
  const components = await response.json();
271
 
272
- if (components.error) {
273
- throw new Error(components.error);
274
- }
275
 
276
- // Clear previous components
277
- componentsContainer.innerHTML = '';
278
-
279
- if (components.length > 1 || components[0]?.type !== 'text') {
280
  componentsFieldset.style.display = 'block';
281
  components.forEach((comp, index) => {
282
  const div = document.createElement('div');
@@ -285,7 +351,7 @@ def index():
285
  div.dataset.type = comp.type;
286
  div.dataset.isCodeBlock = comp.is_code_block;
287
  div.dataset.language = comp.language;
288
- // Store content in a hidden div to avoid issues with textarea rendering
289
  const contentHolder = document.createElement('div');
290
  contentHolder.style.display = 'none';
291
  contentHolder.textContent = comp.content;
@@ -296,21 +362,13 @@ def index():
296
  <input type="checkbox" id="comp-check-${index}" class="component-checkbox" checked>
297
  <label for="comp-check-${index}">${comp.filename}</label>
298
  </div>
299
- <div class="component-content">
300
- <textarea readonly>${comp.content}</textarea>
301
- </div>`;
302
  componentsContainer.appendChild(div);
303
  });
304
- } else {
305
- // Not Repo2Markdown format, hide the component selector
306
- componentsFieldset.style.display = 'none';
307
  }
308
- // Populate the text area if a file was uploaded
309
  if(markdownFileInput.files.length > 0) {
310
  markdownTextInput.value = await markdownFileInput.files[0].text();
311
  }
312
-
313
-
314
  } catch (err) {
315
  displayError('Error parsing content: ' + err.message);
316
  } finally {
@@ -321,16 +379,16 @@ def index():
321
  async function handleGeneration(isDownload = false) {
322
  const buttonToUpdate = isDownload ? downloadBtn : generateBtn;
323
  buttonToUpdate.textContent = 'Generating...';
 
324
  errorBox.style.display = 'none';
325
 
326
  let finalMarkdown = "";
327
- // If components are visible, compose from them. Otherwise, use the text area.
328
  if (componentsFieldset.style.display === 'block') {
329
  const parts = [];
330
  const componentDivs = componentsContainer.querySelectorAll('.component-container');
331
  componentDivs.forEach(div => {
332
  if (div.querySelector('.component-checkbox').checked) {
333
- const content = div.querySelector('div').textContent; // Get content from hidden holder
334
  let partContent = content;
335
  if (div.dataset.isCodeBlock === 'true') {
336
  partContent = "```" + div.dataset.language + "\\n" + content + "\\n```";
@@ -355,8 +413,8 @@ def index():
355
  font_size: document.getElementById('font_size').value,
356
  text_color: document.getElementById('text_color').value,
357
  background_color: document.getElementById('background_color').value,
358
- code_bg_color: document.getElementById('code_bg_color').value,
359
  code_padding: document.getElementById('code_padding').value,
 
360
  custom_css: document.getElementById('custom_css').value
361
  },
362
  include_fontawesome: document.getElementById('include_fontawesome').checked,
@@ -387,24 +445,23 @@ def index():
387
  const result = await response.json();
388
  if (result.error) throw new Error(result.error);
389
  previewContainer.innerHTML = result.preview_html;
390
- downloadBtn.style.display = 'inline-block'; // Show download button after a successful preview
391
  }
392
-
393
  } catch (err) {
394
  displayError('Error generating output: ' + err.message);
395
  } finally {
396
  generateBtn.textContent = 'Generate Preview';
397
  downloadBtn.textContent = 'Download';
 
398
  }
399
  }
400
 
401
  generateBtn.addEventListener('click', () => handleGeneration(false));
402
  downloadBtn.addEventListener('click', () => handleGeneration(true));
403
-
404
  </script>
405
  </body>
406
  </html>
407
- """)
408
 
409
  if __name__ == "__main__":
410
  app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
 
6
  from io import BytesIO
7
  import re
8
 
9
+ # New imports for syntax highlighting
10
+ from pygments import highlight
11
+ from pygments.lexers import get_lexer_by_name
12
+ from pygments.formatters import HtmlFormatter
13
+ from pygments.styles import get_all_styles
14
+
15
  app = Flask(__name__)
16
 
17
  # Configure a temporary directory for file operations
 
27
  def parse_repo2markdown(text):
28
  """Parses Repo2Markdown text, extracts files, and prepares them for the UI."""
29
  components = []
 
30
  pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
31
 
32
  first_match = pattern.search(text)
33
  if first_match:
34
  intro_text = text[:first_match.start()].strip()
35
  if intro_text:
36
+ components.append({'type': 'intro', 'filename': 'Introduction', 'content': intro_text, 'is_code_block': False, 'language': ''})
 
 
 
 
 
 
37
 
38
  for match in pattern.finditer(text):
39
  filename = match.group(1).strip()
40
  raw_content = match.group(2).strip()
 
 
41
  code_match = re.search(r'^```(\w*)\s*\n([\s\S]*?)\s*```$', raw_content, re.DOTALL)
42
 
43
  if code_match:
44
  language = code_match.group(1)
45
  inner_content = code_match.group(2).strip()
46
+ components.append({'type': 'file', 'filename': filename, 'content': inner_content, 'is_code_block': True, 'language': language})
 
 
 
 
 
 
47
  else:
48
+ components.append({'type': 'file', 'filename': filename, 'content': raw_content, 'is_code_block': False, 'language': ''})
 
 
 
 
 
 
 
49
 
50
  return components
51
 
 
65
 
66
  if is_repo2markdown_format(text):
67
  try:
68
+ return jsonify(parse_repo2markdown(text))
 
69
  except Exception as e:
70
  return jsonify({'error': f'Failed to parse Repo2Markdown: {str(e)}'}), 500
71
  else:
 
72
  return jsonify([{'type': 'text', 'filename': 'Full Text', 'content': text}])
73
 
74
  @app.route('/convert', methods=['POST'])
 
80
  include_fontawesome = data.get('include_fontawesome', False)
81
  download_type = data.get('download_type', 'png')
82
  is_download_request = data.get('download', False)
83
+
 
 
 
84
  try:
85
+ # --- CSS & Font Generation (Scoped) ---
86
+ wrapper_id = "#output-wrapper"
87
+ font_family = styles.get('font_family', "'Arial', sans-serif")
88
+ google_font_name = font_family.split(',')[0].strip("'\"")
89
+ google_font_link = ""
90
+ if " " in google_font_name and google_font_name not in ["Times New Roman", "Courier New"]: # Simple check for Google Font
91
+ google_font_link = f'<link href="https://fonts.googleapis.com/css2?family={google_font_name.replace(" ", "+")}:wght@400;700&display=swap" rel="stylesheet">'
92
+
93
+ highlight_theme = styles.get('highlight_theme', 'default')
94
+ pygments_css = ""
95
+ if highlight_theme != 'none':
96
+ formatter = HtmlFormatter(style=highlight_theme, cssclass="codehilite")
97
+ # Get the CSS definitions for the chosen theme.
98
+ pygments_css = formatter.get_style_defs()
99
+
100
+ # All styles are now prefixed with the wrapper_id for isolation.
101
+ scoped_css = f"""
102
+ {wrapper_id} {{
103
+ font-family: {font_family};
104
+ font-size: {styles.get('font_size', '16')}px;
105
+ color: {styles.get('text_color', '#333')};
106
+ background-color: {styles.get('background_color', '#fff')};
107
+ }}
108
+ {wrapper_id} h3 {{ border-bottom: 1px solid #ccc; padding-bottom: 5px; margin-top: 2em; }}
109
+ {wrapper_id} table {{ border-collapse: collapse; width: 100%; }}
110
+ {wrapper_id} th, {wrapper_id} td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
111
+ {wrapper_id} th {{ background-color: #f2f2f2; }}
112
+ {wrapper_id} img {{ max-width: 100%; height: auto; }}
113
+ {wrapper_id} pre {{ padding: {styles.get('code_padding', '15')}px; border-radius: 5px; white-space: pre-wrap; word-wrap: break-word; }}
114
+ {wrapper_id} code {{ font-family: 'Courier New', monospace; padding: 0.2em 0.4em; margin: 0; font-size: 85%; }}
115
+ {wrapper_id} pre code {{ padding: 0; margin: 0; font-size: inherit; background: transparent; border-radius: 0; }}
116
+ {pygments_css}
117
+ {styles.get('custom_css', '')}
118
+ """
119
+
120
+ # --- HTML Conversion ---
121
+ # The 'codehilite' extension enables the CSS classes that Pygments uses.
122
+ md_extensions = ['fenced_code', 'tables', 'codehilite']
123
+ html_content = markdown.markdown(markdown_text, extensions=md_extensions, extension_configs={'codehilite': {'css_class': 'codehilite'}})
124
 
125
+ # Wrap final HTML content in the isolated div
126
+ final_html_body = f'<div id="output-wrapper">{html_content}</div>'
127
+
128
  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 ""
129
+
130
+ full_html = f"""<!DOCTYPE html>
131
+ <html>
132
+ <head>
133
+ <meta charset="UTF-8">
134
+ {google_font_link}
135
+ {fontawesome_link}
136
+ <style>
137
+ /* For PNG generation, give the body the background color */
138
+ body {{ background-color: {styles.get('background_color', '#fff')}; padding: 25px; display: inline-block;}}
139
+ {scoped_css}
140
+ </style>
141
+ </head>
142
+ <body>
143
+ {final_html_body}
144
+ </body>
145
+ </html>"""
146
 
147
  if is_download_request:
148
  if download_type == 'html':
 
152
  imgkit.from_string(full_html, png_path, options={"quiet": "", 'encoding': "UTF-8"})
153
  return send_file(png_path, as_attachment=True, download_name="output.png", mimetype="image/png")
154
  else:
 
155
  return jsonify({'preview_html': full_html})
156
 
157
  except Exception as e:
158
  traceback.print_exc()
159
  return jsonify({'error': f'Failed to convert content: {str(e)}'}), 500
160
 
161
+
162
  # --- MAIN PAGE (Front-end) ---
163
 
164
  @app.route('/')
165
  def index():
166
  """Serves the main HTML page with all the client-side JavaScript."""
167
+ highlight_styles = sorted(list(get_all_styles()))
168
  return render_template_string("""
169
  <!DOCTYPE html>
170
  <html lang="en">
 
179
  textarea { width: 100%; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; padding: 10px; font-family: monospace; }
180
  fieldset { border: 1px solid #ddd; padding: 15px; border-radius: 5px; margin-top: 20px; }
181
  legend { font-weight: bold; color: #555; padding: 0 10px; }
182
+ select, input[type="number"], input[type="color"] { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;}
183
  button { padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; }
184
  .action-btn { background-color: #007BFF; color: white; font-size: 16px; padding: 12px 20px;}
185
  .action-btn:hover { background-color: #0056b3; }
186
  .generate-btn { background-color: #5a32a3; color: white; font-size: 16px; padding: 12px 20px; }
187
  .generate-btn:hover { background-color: #4a298a; }
188
+ .download-btn { background-color: #28a745; color: white; display: none; }
189
  .download-btn:hover { background-color: #218838; }
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
+ .preview-container { border: 1px solid #ddd; padding: 10px; margin-top: 20px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); min-height: 100px; }
193
  .error { color: #D8000C; background-color: #FFD2D2; padding: 10px; border-radius: 5px; margin-top: 15px; display: none; }
194
  .info { color: #00529B; background-color: #BDE5F8; padding: 10px; border-radius: 5px; margin: 10px 0; }
195
+ .style-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 15px; align-items: end; }
196
  .component-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
197
  .component-container { border: 1px solid #e0e0e0; border-radius: 5px; background: #fafafa; }
198
  .component-header { background: #f1f1f1; padding: 8px 12px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 10px; }
 
205
  </head>
206
  <body>
207
  <h1>Advanced Markdown Converter & Composer</h1>
208
+ <form id="main-form" onsubmit="return false;">
209
  <fieldset>
210
  <legend>1. Load Content</legend>
211
  <div class="info">Paste content or upload a file, then click "Load & Analyze".</div>
 
231
  <fieldset>
232
  <legend>3. Configure Styles</legend>
233
  <div class="style-grid">
234
+ <div>
235
+ <label>Font Family:</label>
236
+ <select id="font_family">
237
+ <optgroup label="Sans-Serif">
238
+ <option value="'Arial', sans-serif">Arial</option>
239
+ <option value="'Helvetica', sans-serif">Helvetica</option>
240
+ <option value="'Verdana', sans-serif">Verdana</option>
241
+ <option value="'Roboto', sans-serif">Roboto</option>
242
+ <option value="'Open Sans', sans-serif">Open Sans</option>
243
+ <option value="'Lato', sans-serif">Lato</option>
244
+ <option value="'Montserrat', sans-serif">Montserrat</option>
245
+ </optgroup>
246
+ <optgroup label="Serif">
247
+ <option value="'Times New Roman', serif">Times New Roman</option>
248
+ <option value="'Georgia', serif">Georgia</option>
249
+ <option value="'Garamond', serif">Garamond</option>
250
+ <option value="'Playfair Display', serif">Playfair Display</option>
251
+ <option value="'Merriweather', serif">Merriweather</option>
252
+ </optgroup>
253
+ <optgroup label="Monospace">
254
+ <option value="'Courier New', monospace">Courier New</option>
255
+ <option value="'Lucida Console', monospace">Lucida Console</option>
256
+ <option value="'Roboto Mono', monospace">Roboto Mono</option>
257
+ <option value="'Source Code Pro', monospace">Source Code Pro</option>
258
+ <option value="'Inconsolata', monospace">Inconsolata</option>
259
+ </optgroup>
260
+ <optgroup label="Display">
261
+ <option value="'Oswald', sans-serif">Oswald</option>
262
+ <option value="'Raleway', sans-serif">Raleway</option>
263
+ <option value="'Lobster', cursive">Lobster</option>
264
+ </optgroup>
265
+ <optgroup label="Handwriting">
266
+ <option value="'Dancing Script', cursive">Dancing Script</option>
267
+ <option value="'Pacifico', cursive">Pacifico</option>
268
+ <option value="'Caveat', cursive">Caveat</option>
269
+ </optgroup>
270
+ </select>
271
+ </div>
272
  <div><label>Font Size (px):</label><input type="number" id="font_size" value="16"></div>
273
+ <div>
274
+ <label>Highlight Theme:</label>
275
+ <select id="highlight_theme">
276
+ <option value="none">None</option>
277
+ {% for style in highlight_styles %}
278
+ <option value="{{ style }}" {% if style == 'default' %}selected{% endif %}>{{ style }}</option>
279
+ {% endfor %}
280
+ </select>
281
+ </div>
282
  <div><label>Text Color:</label><input type="color" id="text_color" value="#333333"></div>
283
  <div><label>Background Color:</label><input type="color" id="background_color" value="#ffffff"></div>
 
284
  <div><label>Code Padding (px):</label><input type="number" id="code_padding" value="15"></div>
285
  </div>
286
  <div><input type="checkbox" id="include_fontawesome"><label for="include_fontawesome">Include Font Awesome</label></div>
287
+ <div><label for="custom_css">Custom CSS:</label><textarea id="custom_css" rows="3" placeholder="Styles will be scoped to the output. e.g. h1 { text-decoration: underline; }"></textarea></div>
288
  </fieldset>
289
 
290
  <div class="controls">
 
302
  <div id="preview-container" class="preview-container"></div>
303
 
304
  <script>
 
305
  const loadBtn = document.getElementById('load-btn');
306
  const generateBtn = document.getElementById('generate-btn');
307
  const downloadBtn = document.getElementById('download-btn');
 
312
  const previewContainer = document.getElementById('preview-container');
313
  const errorBox = document.getElementById('error-box');
314
 
 
315
  function toggleAllComponents(checked) {
316
  componentsContainer.querySelectorAll('.component-checkbox').forEach(cb => cb.checked = checked);
317
  }
 
322
  previewContainer.innerHTML = '';
323
  }
324
 
 
325
  loadBtn.addEventListener('click', async () => {
326
  loadBtn.textContent = 'Loading...';
327
  errorBox.style.display = 'none';
328
+ componentsFieldset.style.display = 'none';
329
+ componentsContainer.innerHTML = '';
330
+
331
 
332
  const formData = new FormData();
333
  if (markdownFileInput.files.length > 0) {
 
340
  const response = await fetch('/parse', { method: 'POST', body: formData });
341
  const components = await response.json();
342
 
343
+ if (components.error) throw new Error(components.error);
 
 
344
 
345
+ if (components.length > 1 || (components.length > 0 && components[0]?.type !== 'text')) {
 
 
 
346
  componentsFieldset.style.display = 'block';
347
  components.forEach((comp, index) => {
348
  const div = document.createElement('div');
 
351
  div.dataset.type = comp.type;
352
  div.dataset.isCodeBlock = comp.is_code_block;
353
  div.dataset.language = comp.language;
354
+
355
  const contentHolder = document.createElement('div');
356
  contentHolder.style.display = 'none';
357
  contentHolder.textContent = comp.content;
 
362
  <input type="checkbox" id="comp-check-${index}" class="component-checkbox" checked>
363
  <label for="comp-check-${index}">${comp.filename}</label>
364
  </div>
365
+ <div class="component-content"><textarea readonly>${comp.content}</textarea></div>`;
 
 
366
  componentsContainer.appendChild(div);
367
  });
 
 
 
368
  }
 
369
  if(markdownFileInput.files.length > 0) {
370
  markdownTextInput.value = await markdownFileInput.files[0].text();
371
  }
 
 
372
  } catch (err) {
373
  displayError('Error parsing content: ' + err.message);
374
  } finally {
 
379
  async function handleGeneration(isDownload = false) {
380
  const buttonToUpdate = isDownload ? downloadBtn : generateBtn;
381
  buttonToUpdate.textContent = 'Generating...';
382
+ buttonToUpdate.disabled = true;
383
  errorBox.style.display = 'none';
384
 
385
  let finalMarkdown = "";
 
386
  if (componentsFieldset.style.display === 'block') {
387
  const parts = [];
388
  const componentDivs = componentsContainer.querySelectorAll('.component-container');
389
  componentDivs.forEach(div => {
390
  if (div.querySelector('.component-checkbox').checked) {
391
+ const content = div.querySelector('div').textContent;
392
  let partContent = content;
393
  if (div.dataset.isCodeBlock === 'true') {
394
  partContent = "```" + div.dataset.language + "\\n" + content + "\\n```";
 
413
  font_size: document.getElementById('font_size').value,
414
  text_color: document.getElementById('text_color').value,
415
  background_color: document.getElementById('background_color').value,
 
416
  code_padding: document.getElementById('code_padding').value,
417
+ highlight_theme: document.getElementById('highlight_theme').value,
418
  custom_css: document.getElementById('custom_css').value
419
  },
420
  include_fontawesome: document.getElementById('include_fontawesome').checked,
 
445
  const result = await response.json();
446
  if (result.error) throw new Error(result.error);
447
  previewContainer.innerHTML = result.preview_html;
448
+ downloadBtn.style.display = 'inline-block';
449
  }
 
450
  } catch (err) {
451
  displayError('Error generating output: ' + err.message);
452
  } finally {
453
  generateBtn.textContent = 'Generate Preview';
454
  downloadBtn.textContent = 'Download';
455
+ buttonToUpdate.disabled = false;
456
  }
457
  }
458
 
459
  generateBtn.addEventListener('click', () => handleGeneration(false));
460
  downloadBtn.addEventListener('click', () => handleGeneration(true));
 
461
  </script>
462
  </body>
463
  </html>
464
+ """, highlight_styles=highlight_styles)
465
 
466
  if __name__ == "__main__":
467
  app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))