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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -123
app.py CHANGED
@@ -23,129 +23,129 @@ DEFAULT_STYLES = {
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"]),
87
- "text_color": request.form.get("text_color", DEFAULT_STYLES["text_color"]),
88
- "background_color": request.form.get("background_color", DEFAULT_STYLES["background_color"]),
89
- "code_bg_color": request.form.get("code_bg_color", DEFAULT_STYLES["code_bg_color"]),
90
- "code_padding": request.form.get("code_padding", DEFAULT_STYLES["code_padding"]),
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; }}
@@ -174,7 +174,7 @@ def index():
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>
@@ -195,8 +195,8 @@ def index():
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; }
@@ -204,14 +204,14 @@ def index():
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) {
@@ -223,8 +223,8 @@ def index():
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>
@@ -234,48 +234,46 @@ def index():
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>
@@ -288,13 +286,15 @@ def index():
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>
 
23
  "custom_css": ""
24
  }
25
 
26
+ def is_repo2markdown_format(text):
27
+ """Detects if the text is in the Repo2Markdown format."""
28
+ return "## File Structure" in text and text.count("### File:") > 1
29
+
30
+ def parse_repo2markdown(text):
31
  """
32
+ Parses Repo2Markdown text to extract individual files.
33
+ Returns a list of dictionaries, each representing a file.
34
  """
 
 
 
 
35
  components = []
36
+ # Regex to find sections starting with '### File:'
37
+ # It captures the filename and all content until the next '### File:' or the end of the string.
38
+ pattern = re.compile(r'### File: (.*?)\n([\s\S]*?)(?=\n### File:|\Z)', re.MULTILINE)
39
+
40
+ # Find the introductory text before the first file.
41
+ first_match = pattern.search(text)
42
+ if first_match:
43
+ intro_text = text[:first_match.start()].strip()
44
+ if intro_text:
 
 
 
 
 
 
 
 
 
 
 
 
45
  components.append({
46
+ 'type': 'intro',
47
+ 'filename': 'Introduction',
48
+ 'content': intro_text,
49
+ 'is_selected': True
50
  })
 
 
51
 
52
+ # Find all file sections
53
+ for match in pattern.finditer(text):
54
+ filename = match.group(1).strip()
55
+ content = match.group(2).strip()
56
+ components.append({
57
+ 'type': 'file',
58
+ 'filename': filename,
59
+ 'content': content,
60
+ 'is_selected': True # Default to selected
61
+ })
62
+
63
+ return components
64
 
65
  @app.route("/", methods=["GET", "POST"])
66
  def index():
67
+ """Main route to handle parsing, component selection, and conversion."""
68
  # Initialize variables
69
  preview_html = None
70
  download_available = False
71
  error_message = None
72
  components = []
73
+
74
  # Set default values on GET request
75
  markdown_text = ""
76
  download_type = "png"
77
  styles = DEFAULT_STYLES.copy()
78
  include_fontawesome = False
79
 
 
80
  if request.method == "POST":
81
+ # Preserve styling options across all POST requests
82
+ styles = {key: request.form.get(key, default) for key, default in DEFAULT_STYLES.items()}
 
 
 
 
 
 
 
 
83
  include_fontawesome = "include_fontawesome" in request.form
84
  download_type = request.form.get("download_type", "png")
85
+
86
  final_markdown_to_render = ""
 
 
 
 
 
 
 
 
 
87
 
88
+ # Determine the action based on the button/data submitted
89
+ action = "parse"
90
+ if 'generate_from_components' in request.form:
91
+ action = "generate"
92
+
93
+ # --- ACTION 1: PARSE uploaded text or file ---
94
+ if action == "parse":
95
+ markdown_text = request.form.get("markdown_text", "")
96
+ uploaded_file = request.files.get("markdown_file")
97
+ if uploaded_file and uploaded_file.filename != '':
98
+ try:
99
+ markdown_text = uploaded_file.read().decode("utf-8")
100
+ except Exception as e:
101
+ error_message = f"Error reading file: {e}"
102
+
103
+ if markdown_text and is_repo2markdown_format(markdown_text):
104
+ try:
105
+ components = parse_repo2markdown(markdown_text)
106
+ if not components:
107
+ error_message = "Repo2Markdown format detected, but no file components could be parsed."
108
+ except Exception as e:
109
+ error_message = f"Error parsing Repo2Markdown format: {e}"
110
+ else:
111
+ final_markdown_to_render = markdown_text
112
+
113
+ # --- ACTION 2: GENERATE from selected components ---
114
+ elif action == "generate":
115
  final_markdown_parts = []
116
+
117
+ # Re-read component data from hidden form fields to reconstruct the UI
118
+ comp_count = int(request.form.get('component_count', 0))
119
+ for i in range(comp_count):
120
  comp_type = request.form.get(f'comp_type_{i}')
121
  if not comp_type: continue
122
+
123
+ is_selected = f'include_comp_{i}' in request.form
124
+ filename = request.form.get(f'comp_filename_{i}')
125
+ content = request.form.get(f'comp_content_{i}')
126
 
127
+ # Re-add to components list to maintain UI state
128
+ components.append({
129
+ 'type': comp_type,
130
+ 'filename': filename,
131
+ 'content': content,
132
+ 'is_selected': is_selected
133
+ })
134
+
135
+ # If selected, add it to the final document to be rendered
136
+ if is_selected:
137
+ if comp_type == 'intro':
138
  final_markdown_parts.append(content)
139
+ else: # file
140
+ final_markdown_parts.append(f"### File: {filename}\n{content}")
141
 
142
+ final_markdown_to_render = "\n\n---\n\n".join(final_markdown_parts)
143
+
144
+ # --- HTML & PNG Conversion Logic ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  if final_markdown_to_render:
146
  try:
147
  html_content = markdown.markdown(final_markdown_to_render, extensions=['fenced_code', 'tables'])
148
 
 
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; }}
 
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>
 
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: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; }
199
+ .action-btn { background-color: #007BFF; color: white; font-size: 16px; padding: 12px 20px;}
200
  .action-btn:hover { background-color: #0056b3; }
201
  .download-btn { background-color: #28a745; color: white; }
202
  .download-btn:hover { background-color: #218838; }
 
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-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }
208
+ .component-container { border: 1px solid #e0e0e0; border-radius: 5px; background: #fafafa; }
209
  .component-header { background: #f1f1f1; padding: 8px 12px; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 10px; }
210
  .component-header input[type="checkbox"] { width: 18px; height: 18px; }
211
  .component-header label { margin-bottom: 0; font-weight: bold; }
 
212
  .component-content { padding: 10px; }
213
  .component-content textarea { height: 150px; }
214
+ .selection-controls { margin: 15px 0; display: flex; gap: 10px; }
215
  </style>
216
  <script>
217
  function toggleAllComponents(checked) {
 
223
  <body>
224
  <h1>Advanced Markdown Converter & Composer</h1>
225
 
 
226
  <form method="post" enctype="multipart/form-data">
227
+ <!-- This section is shown if no components are parsed yet -->
228
  {% if not components %}
229
  <fieldset>
230
  <legend>Option 1: Paste Markdown</legend>
 
234
  <fieldset>
235
  <legend>Option 2: Upload a Markdown File</legend>
236
  <input type="file" name="markdown_file" accept=".md,.txt,text/markdown">
 
237
  </fieldset>
238
+ <div class="controls"><button type="submit" class="action-btn">Parse and Preview</button></div>
239
  {% endif %}
240
+
241
+ <!-- This section is shown only after a Repo2Markdown file is parsed -->
242
  {% if components %}
243
  <input type="hidden" name="generate_from_components" value="true">
244
+ <input type="hidden" name="component_count" value="{{ components|length }}">
245
  <fieldset>
246
+ <legend>Detected File Components</legend>
247
+ <div class="info">Repo2Markdown format detected. Select the files to include in the final document.</div>
248
  <div class="selection-controls">
249
  <button type="button" onclick="toggleAllComponents(true)">Select All</button>
250
  <button type="button" onclick="toggleAllComponents(false)">Deselect All</button>
251
  </div>
252
+ <div class="component-grid">
253
+ {% for comp in components %}
254
+ <div class="component-container">
255
+ <div class="component-header">
256
+ <input type="checkbox" name="include_comp_{{ loop.index0 }}" id="include_comp_{{ loop.index0 }}" class="component-checkbox" {% if comp.is_selected %}checked{% endif %}>
257
+ <label for="include_comp_{{ loop.index0 }}">{{ comp.filename }}</label>
258
+ </div>
259
+ <div class="component-content">
260
+ <textarea readonly>{{ comp.content }}</textarea>
261
+ </div>
262
  </div>
263
+ <!-- Hidden fields to preserve data on next post -->
264
+ <input type="hidden" name="comp_type_{{ loop.index0 }}" value="{{ comp.type }}">
265
+ <input type="hidden" name="comp_filename_{{ loop.index0 }}" value="{{ comp.filename }}">
266
+ <input type="hidden" name="comp_content_{{ loop.index0 }}" value="{{ comp.content }}">
267
+ {% endfor %}
268
+ </div>
 
 
 
269
  </fieldset>
270
  {% endif %}
271
 
272
+ <!-- Styling and Generation controls -->
273
  <fieldset>
274
  <legend>Styling Options</legend>
275
  <div class="style-grid">
276
+ <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>
 
277
  <div><label for="font_size">Font Size (px):</label><input type="number" id="font_size" name="font_size" value="{{ styles.font_size }}"></div>
278
  <div><label for="text_color">Text Color:</label><input type="color" id="text_color" name="text_color" value="{{ styles.text_color }}"></div>
279
  <div><label for="background_color">Background Color:</label><input type="color" id="background_color" name="background_color" value="{{ styles.background_color }}"></div>
 
286
 
287
  <div class="controls">
288
  <div class="main-actions">
289
+ <button type="submit" class="action-btn">
290
+ {% if components %}Re-Generate Preview{% else %}Generate Preview{% endif %}
291
+ </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>