mnoorchenar commited on
Commit
f9ab388
·
verified ·
1 Parent(s): 27cbf20

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1067 -911
app.py CHANGED
@@ -1,911 +1,1067 @@
1
- from flask import Flask, render_template_string, request, send_file
2
- import markdown
3
- from io import BytesIO
4
- import re
5
- from xhtml2pdf import pisa
6
- app = Flask(__name__)
7
- def generate_css(settings):
8
- """Generate CSS based on user settings"""
9
-
10
- # Calculate heading sizes based on base font
11
- base_size = settings['base_font_size']
12
- h1_size = base_size * 2
13
- h2_size = base_size * 1.4
14
- h3_size = base_size * 1.2
15
-
16
- # Code box border style
17
- code_border = f"border: 1px solid {settings['code_border_color']};" if settings['show_code_border'] else "border: none;"
18
- code_border_left = f"border-left: 3px solid {settings['code_accent_color']};" if settings['show_code_border'] else ""
19
-
20
- # PDF page size
21
- page_size = settings['page_size']
22
-
23
- return f'''
24
- @page {{
25
- size: {page_size};
26
- margin: {settings['page_margin']}cm;
27
- }}
28
-
29
- * {{
30
- margin: 0;
31
- padding: 0;
32
- box-sizing: border-box;
33
- }}
34
-
35
- body {{
36
- font-family: Georgia, serif;
37
- line-height: 1.5;
38
- color: #1a1a1a;
39
- font-size: {settings['base_font_size']}pt;
40
- }}
41
-
42
- h1 {{
43
- color: #2c3e50;
44
- font-size: {h1_size}pt;
45
- margin-bottom: 15px;
46
- margin-top: 0;
47
- padding-bottom: 8px;
48
- border-bottom: 2px solid #3498db;
49
- page-break-after: avoid;
50
- background-color: transparent;
51
- }}
52
-
53
- h2 {{
54
- color: #34495e;
55
- font-size: {h2_size}pt;
56
- margin-top: 20px;
57
- margin-bottom: 10px;
58
- padding-top: 5px;
59
- border-top: 1px solid #ecf0f1;
60
- page-break-after: avoid;
61
- background-color: transparent;
62
- }}
63
-
64
- h3 {{
65
- color: #555;
66
- font-size: {h3_size}pt;
67
- margin-top: 15px;
68
- margin-bottom: 8px;
69
- page-break-after: avoid;
70
- background-color: transparent;
71
- }}
72
-
73
- h4 {{
74
- color: #666;
75
- font-size: {base_size}pt;
76
- margin-top: 12px;
77
- margin-bottom: 6px;
78
- background-color: transparent;
79
- }}
80
-
81
- p {{
82
- margin-bottom: {settings['paragraph_spacing']}px;
83
- text-align: justify;
84
- background-color: transparent;
85
- }}
86
-
87
- strong {{
88
- color: #2c3e50;
89
- font-weight: 600;
90
- background-color: transparent;
91
- }}
92
-
93
- code {{
94
- background-color: transparent;
95
- padding: 0;
96
- border-radius: 0;
97
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
98
- font-size: {settings['base_font_size']}pt;
99
- color: #000000;
100
- }}
101
-
102
- /* Code blocks */
103
- pre {{
104
- background-color: {settings['code_bg_color']};
105
- {code_border}
106
- {code_border_left}
107
- padding: {settings['code_padding_vertical']}px {settings['code_padding_horizontal']}px;
108
- margin: {settings['code_margin_top']}px 0 {settings['code_margin_bottom']}px 0;
109
- overflow-x: auto;
110
- border-radius: 3px;
111
- page-break-inside: avoid;
112
- }}
113
-
114
- pre code {{
115
- background-color: transparent;
116
- padding: 0;
117
- color: #000000;
118
- font-size: {settings['code_font_size']}pt;
119
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
120
- display: block;
121
- line-height: 1.4;
122
- white-space: pre;
123
- }}
124
-
125
- /* SQL Syntax Highlighting */
126
- .sql-keyword {{
127
- color: {settings['keyword_color']};
128
- font-weight: bold;
129
- }}
130
-
131
- .sql-comment {{
132
- color: {settings['comment_color']};
133
- font-style: italic;
134
- }}
135
-
136
- .sql-string {{
137
- color: {settings['string_color']};
138
- }}
139
-
140
- .sql-number {{
141
- color: {settings['number_color']};
142
- }}
143
-
144
- .sql-function {{
145
- color: {settings['function_color']};
146
- font-weight: bold;
147
- }}
148
-
149
- /* Python Syntax Highlighting */
150
- .py-keyword {{
151
- color: {settings['keyword_color']};
152
- font-weight: bold;
153
- }}
154
-
155
- .py-string {{
156
- color: {settings['string_color']};
157
- }}
158
-
159
- .py-number {{
160
- color: {settings['number_color']};
161
- }}
162
-
163
- .py-comment {{
164
- color: {settings['comment_color']};
165
- font-style: italic;
166
- }}
167
-
168
- .py-function {{
169
- color: {settings['function_color']};
170
- }}
171
-
172
- .py-builtin {{
173
- color: {settings['keyword_color']};
174
- }}
175
-
176
- .py-decorator {{
177
- color: #808080;
178
- }}
179
-
180
- /* Tables */
181
- table {{
182
- width: 100%;
183
- border-collapse: collapse;
184
- margin: 10px 0;
185
- font-size: {settings['base_font_size'] - 1}pt;
186
- page-break-inside: avoid;
187
- }}
188
-
189
- table th {{
190
- background-color: #3498db;
191
- color: white;
192
- padding: 5px 8px;
193
- text-align: left;
194
- font-weight: 600;
195
- border: 1px solid #2980b9;
196
- line-height: 1.2;
197
- }}
198
-
199
- table td {{
200
- border: 1px solid #ddd;
201
- padding: 4px 8px;
202
- vertical-align: top;
203
- line-height: 1.2;
204
- }}
205
-
206
- table tr:nth-child(even) {{
207
- background-color: #f9f9f9;
208
- }}
209
-
210
- /* Lists */
211
- ul, ol {{
212
- margin-left: 25px;
213
- margin-bottom: 10px;
214
- }}
215
-
216
- li {{
217
- margin-bottom: 4px;
218
- line-height: 1.4;
219
- }}
220
-
221
- blockquote {{
222
- border-left: 3px solid #3498db;
223
- padding: 8px 12px;
224
- margin: 10px 0;
225
- color: #555;
226
- font-style: italic;
227
- background-color: #f8f9fa;
228
- }}
229
-
230
- hr {{
231
- border: none;
232
- border-top: 1px solid #ecf0f1;
233
- margin: 15px 0;
234
- }}
235
-
236
- /* Callout boxes */
237
- .warning {{
238
- background-color: #fff3cd;
239
- border-left: 3px solid #ffc107;
240
- padding: 8px 12px;
241
- margin: 10px 0;
242
- border-radius: 3px;
243
- }}
244
-
245
- .success {{
246
- background-color: #d4edda;
247
- border-left: 3px solid #28a745;
248
- padding: 8px 12px;
249
- margin: 10px 0;
250
- border-radius: 3px;
251
- }}
252
-
253
- .error {{
254
- background-color: #f8d7da;
255
- border-left: 3px solid #dc3545;
256
- padding: 8px 12px;
257
- margin: 10px 0;
258
- border-radius: 3px;
259
- }}
260
-
261
- .info {{
262
- background-color: #d1ecf1;
263
- border-left: 3px solid #0c5460;
264
- padding: 8px 12px;
265
- margin: 10px 0;
266
- border-radius: 3px;
267
- }}
268
- '''
269
- HTML_TEMPLATE = '''
270
- <!DOCTYPE html>
271
- <html lang="en">
272
- <head>
273
- <meta charset="UTF-8">
274
- <title>Document</title>
275
- <style>
276
- {{ css|safe }}
277
- </style>
278
- </head>
279
- <body>
280
- {{ content|safe }}
281
- </body>
282
- </html>
283
- '''
284
- def highlight_sql(code):
285
- """LaTeX-quality SQL syntax highlighting"""
286
-
287
- keywords = [
288
- 'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER',
289
- 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER',
290
- 'FULL', 'CROSS', 'ON', 'USING', 'AS', 'AND', 'OR', 'NOT', 'NULL', 'IS', 'IN',
291
- 'BETWEEN', 'LIKE', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET', 'DISTINCT',
292
- 'UNION', 'ALL', 'INTERSECT', 'EXCEPT', 'EXISTS', 'CASE', 'WHEN', 'THEN', 'ELSE',
293
- 'END', 'IF', 'WITH', 'RECURSIVE', 'ASC', 'DESC', 'INTO', 'VALUES', 'SET', 'DEFAULT',
294
- 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'UNIQUE', 'CHECK',
295
- 'AUTO_INCREMENT', 'SERIAL', 'AUTOINCREMENT', 'IDENTITY', 'RETURNS', 'BEGIN',
296
- 'COMMIT', 'ROLLBACK', 'TRANSACTION', 'GRANT', 'REVOKE', 'CASCADE', 'RESTRICT',
297
- 'INT', 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT', 'DECIMAL', 'NUMERIC', 'FLOAT',
298
- 'REAL', 'DOUBLE', 'VARCHAR', 'CHAR', 'TEXT', 'BLOB', 'DATE', 'TIME', 'DATETIME',
299
- 'TIMESTAMP', 'BOOLEAN', 'BOOL', 'ENUM', 'JSON', 'ARRAY'
300
- ]
301
-
302
- functions = [
303
- 'COUNT', 'SUM', 'AVG', 'MAX', 'MIN', 'CONCAT', 'UPPER', 'LOWER', 'LENGTH',
304
- 'SUBSTRING', 'TRIM', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'NOW', 'CURRENT_DATE',
305
- 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DATE', 'TIME', 'YEAR', 'MONTH', 'DAY',
306
- 'COALESCE', 'NULLIF', 'CAST', 'CONVERT'
307
- ]
308
-
309
- result = code
310
-
311
- # Protect comments
312
- comment_placeholder = {}
313
- comment_counter = 0
314
-
315
- for match in re.finditer(r'--[^\n]*', result):
316
- placeholder = f'___COMMENT_{comment_counter}___'
317
- comment_placeholder[placeholder] = f'<span class="sql-comment">{match.group(0)}</span>'
318
- result = result.replace(match.group(0), placeholder, 1)
319
- comment_counter += 1
320
-
321
- for match in re.finditer(r'/\*.*?\*/', result, re.DOTALL):
322
- placeholder = f'___COMMENT_{comment_counter}___'
323
- comment_placeholder[placeholder] = f'<span class="sql-comment">{match.group(0)}</span>'
324
- result = result.replace(match.group(0), placeholder, 1)
325
- comment_counter += 1
326
-
327
- # Protect strings
328
- string_placeholder = {}
329
- string_counter = 0
330
-
331
- for match in re.finditer(r"'(?:[^'\\]|\\.)*'", result):
332
- placeholder = f'___STRING_{string_counter}___'
333
- string_placeholder[placeholder] = f'<span class="sql-string">{match.group(0)}</span>'
334
- result = result.replace(match.group(0), placeholder, 1)
335
- string_counter += 1
336
-
337
- # Highlight numbers
338
- result = re.sub(r'\b(\d+\.?\d*)\b', r'<span class="sql-number">\1</span>', result)
339
-
340
- # Highlight functions
341
- for func in functions:
342
- result = re.sub(
343
- r'\b(' + func + r')\s*\(',
344
- r'<span class="sql-function">\1</span>(',
345
- result,
346
- flags=re.IGNORECASE
347
- )
348
-
349
- # Highlight keywords
350
- for keyword in keywords:
351
- result = re.sub(
352
- r'\b(' + keyword + r')\b',
353
- r'<span class="sql-keyword">\1</span>',
354
- result,
355
- flags=re.IGNORECASE
356
- )
357
-
358
- # Restore strings and comments
359
- for placeholder, original in string_placeholder.items():
360
- result = result.replace(placeholder, original)
361
-
362
- for placeholder, original in comment_placeholder.items():
363
- result = result.replace(placeholder, original)
364
-
365
- return result
366
- def highlight_python(code):
367
- """LaTeX-quality Python syntax highlighting"""
368
-
369
- keywords = [
370
- 'def', 'class', 'import', 'from', 'as', 'if', 'elif', 'else', 'for', 'while',
371
- 'return', 'try', 'except', 'finally', 'with', 'lambda', 'yield', 'async', 'await',
372
- 'pass', 'break', 'continue', 'and', 'or', 'not', 'in', 'is', 'None', 'True', 'False',
373
- 'raise', 'assert', 'del', 'global', 'nonlocal'
374
- ]
375
-
376
- builtins = [
377
- 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
378
- 'open', 'input', 'type', 'isinstance', 'enumerate', 'zip', 'map', 'filter', 'sum',
379
- 'max', 'min', 'sorted', 'reversed', 'all', 'any', 'abs', 'round', 'pow'
380
- ]
381
-
382
- result = code
383
-
384
- # Protect comments
385
- comment_placeholder = {}
386
- comment_counter = 0
387
-
388
- for match in re.finditer(r'#[^\n]*', result):
389
- placeholder = f'___COMMENT_{comment_counter}___'
390
- comment_placeholder[placeholder] = f'<span class="py-comment">{match.group(0)}</span>'
391
- result = result.replace(match.group(0), placeholder, 1)
392
- comment_counter += 1
393
-
394
- # Protect strings
395
- string_placeholder = {}
396
- string_counter = 0
397
-
398
- for match in re.finditer(r'"""(?:[^"\\]|\\.)*?"""|\'\'\'(?:[^\'\\]|\\.)*?\'\'\'', result, re.DOTALL):
399
- placeholder = f'___STRING_{string_counter}___'
400
- string_placeholder[placeholder] = f'<span class="py-string">{match.group(0)}</span>'
401
- result = result.replace(match.group(0), placeholder, 1)
402
- string_counter += 1
403
-
404
- for match in re.finditer(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'', result):
405
- placeholder = f'___STRING_{string_counter}___'
406
- string_placeholder[placeholder] = f'<span class="py-string">{match.group(0)}</span>'
407
- result = result.replace(match.group(0), placeholder, 1)
408
- string_counter += 1
409
-
410
- # Highlight decorators
411
- result = re.sub(r'(@\w+)', r'<span class="py-decorator">\1</span>', result)
412
-
413
- # Highlight numbers
414
- result = re.sub(r'\b(\d+\.?\d*)\b', r'<span class="py-number">\1</span>', result)
415
-
416
- # Highlight keywords
417
- for keyword in keywords:
418
- result = re.sub(r'\b(' + keyword + r')\b', r'<span class="py-keyword">\1</span>', result)
419
-
420
- # Highlight builtins
421
- for builtin in builtins:
422
- result = re.sub(r'\b(' + builtin + r')\b', r'<span class="py-builtin">\1</span>', result)
423
-
424
- # Restore strings and comments
425
- for placeholder, original in string_placeholder.items():
426
- result = result.replace(placeholder, original)
427
-
428
- for placeholder, original in comment_placeholder.items():
429
- result = result.replace(placeholder, original)
430
-
431
- return result
432
- def process_code_blocks(md_text):
433
- """Process all code blocks in markdown"""
434
-
435
- def replace_code_block(match):
436
- lang = match.group(1) if match.group(1) else ''
437
- code = match.group(2).strip('\n')
438
- lang_lower = lang.lower().strip()
439
-
440
- if lang_lower in ['sql', 'mysql', 'postgresql', 'postgres', 'sqlite', 'tsql', 'plsql']:
441
- highlighted = highlight_sql(code)
442
- elif lang_lower in ['python', 'py', 'python3']:
443
- highlighted = highlight_python(code)
444
- else:
445
- highlighted = code
446
-
447
- return f'<pre><code>{highlighted}</code></pre>'
448
-
449
- result = re.sub(r'```([a-zA-Z0-9]*)\n(.*?)\n```', replace_code_block, md_text, flags=re.DOTALL)
450
- result = re.sub(r'```([a-zA-Z0-9]*)\n(.*?)```', replace_code_block, result, flags=re.DOTALL)
451
- result = re.sub(r'```\s*([a-zA-Z0-9]*)\s*\n(.*?)```', replace_code_block, result, flags=re.DOTALL)
452
-
453
- return result
454
- def process_markdown(md_text):
455
- """Convert markdown to HTML with syntax highlighting"""
456
- md_with_highlighted_code = process_code_blocks(md_text)
457
- html = markdown.markdown(md_with_highlighted_code, extensions=['tables', 'fenced_code'])
458
- html = re.sub(r'style="[^"]*"', '', html)
459
-
460
- html = re.sub(r'<p><strong>⚠️[^<]*</strong>', r'<div class="warning"><strong>⚠️ Warning:</strong>', html)
461
- html = re.sub(r'<p><strong>✓[^<]*</strong>', r'<div class="success"><strong>✓ Best Practice:</strong>', html)
462
- html = re.sub(r'<p><strong>✗[^<]*</strong>', r'<div class="error"><strong>✗ Common Mistake:</strong>', html)
463
- html = re.sub(r'<p><strong>💡[^<]*</strong>', r'<div class="info"><strong>💡 Info:</strong>', html)
464
-
465
- return html
466
- @app.route('/')
467
- def index():
468
- return render_template_string('''
469
- <!DOCTYPE html>
470
- <html>
471
- <head>
472
- <title>Markdown to PDF Converter</title>
473
- <style>
474
- * { margin: 0; padding: 0; box-sizing: border-box; }
475
- body {
476
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
477
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
478
- height: 100vh;
479
- padding: 20px;
480
- overflow: hidden;
481
- margin: 0;
482
- }
483
- .container {
484
- max-width: 1400px;
485
- height: 100%;
486
- margin: 0 auto;
487
- background: white;
488
- border-radius: 20px;
489
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
490
- padding: 30px;
491
- display: flex;
492
- flex-direction: column;
493
- overflow: hidden;
494
- }
495
- h1 {
496
- color: #2c3e50;
497
- margin-bottom: 5px;
498
- font-size: 2em;
499
- }
500
- .subtitle {
501
- color: #7f8c8d;
502
- margin-bottom: 15px;
503
- font-size: 1em;
504
- }
505
- .main-grid {
506
- display: grid;
507
- grid-template-columns: 1fr 350px;
508
- gap: 25px;
509
- flex: 1;
510
- overflow: hidden;
511
- min-height: 0;
512
- }
513
- .textarea-wrapper {
514
- display: flex;
515
- flex-direction: column;
516
- overflow: hidden;
517
- min-height: 0;
518
- }
519
- textarea {
520
- width: 100%;
521
- flex: 1;
522
- padding: 15px;
523
- border: 2px solid #e0e0e0;
524
- border-radius: 10px;
525
- font-family: 'Consolas', monospace;
526
- font-size: 13px;
527
- resize: none;
528
- line-height: 1.5;
529
- overflow-y: auto;
530
- min-height: 0;
531
- }
532
- textarea:focus {
533
- outline: none;
534
- border-color: #667eea;
535
- }
536
- .settings-panel {
537
- background: #f8f9fa;
538
- padding: 20px;
539
- border-radius: 10px;
540
- border-left: 4px solid #667eea;
541
- overflow-y: auto;
542
- overflow-x: hidden;
543
- display: flex;
544
- flex-direction: column;
545
- min-height: 0;
546
- }
547
- .settings-panel h3 {
548
- color: #2c3e50;
549
- margin-bottom: 15px;
550
- font-size: 1.1em;
551
- position: sticky;
552
- top: 0;
553
- background: #f8f9fa;
554
- padding: 5px 0;
555
- z-index: 10;
556
- }
557
- .section {
558
- margin-bottom: 20px;
559
- padding-bottom: 15px;
560
- border-bottom: 2px solid #dee2e6;
561
- }
562
- .section:last-child {
563
- border-bottom: none;
564
- margin-bottom: 0;
565
- }
566
- .section h4 {
567
- color: #495057;
568
- font-size: 0.95em;
569
- margin-bottom: 12px;
570
- text-transform: uppercase;
571
- letter-spacing: 0.5px;
572
- }
573
- .field {
574
- margin-bottom: 12px;
575
- }
576
- .field label {
577
- display: block;
578
- color: #666;
579
- font-size: 0.9em;
580
- margin-bottom: 5px;
581
- font-weight: 500;
582
- }
583
- .field input[type="number"] {
584
- width: 100%;
585
- padding: 8px;
586
- border: 1px solid #dee2e6;
587
- border-radius: 5px;
588
- font-size: 0.9em;
589
- }
590
- .field input[type="color"] {
591
- width: 100%;
592
- height: 40px;
593
- border: 1px solid #dee2e6;
594
- border-radius: 5px;
595
- cursor: pointer;
596
- }
597
- .field input:focus {
598
- outline: none;
599
- border-color: #667eea;
600
- }
601
- .checkbox-field {
602
- display: flex;
603
- align-items: center;
604
- gap: 10px;
605
- margin-bottom: 12px;
606
- }
607
- .checkbox-field input[type="checkbox"] {
608
- width: 20px;
609
- height: 20px;
610
- cursor: pointer;
611
- }
612
- .checkbox-field label {
613
- color: #666;
614
- font-size: 0.9em;
615
- margin: 0;
616
- cursor: pointer;
617
- }
618
- .btn-generate {
619
- width: 100%;
620
- padding: 15px;
621
- margin-top: 15px;
622
- font-size: 16px;
623
- font-weight: 600;
624
- border: none;
625
- border-radius: 10px;
626
- cursor: pointer;
627
- text-transform: uppercase;
628
- letter-spacing: 1px;
629
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
630
- color: white;
631
- transition: all 0.3s;
632
- flex-shrink: 0;
633
- }
634
- .btn-generate:hover {
635
- transform: translateY(-2px);
636
- box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
637
- }
638
- .preset-grid {
639
- display: grid;
640
- grid-template-columns: 1fr 1fr;
641
- gap: 10px;
642
- margin-bottom: 20px;
643
- }
644
- .btn-preset {
645
- padding: 10px;
646
- font-size: 13px;
647
- font-weight: 600;
648
- border: 2px solid #28a745;
649
- border-radius: 8px;
650
- cursor: pointer;
651
- background: white;
652
- color: #28a745;
653
- transition: all 0.3s;
654
- }
655
- .btn-preset:hover {
656
- background: #28a745;
657
- color: white;
658
- }
659
- .btn-preset.compact {
660
- border-color: #17a2b8;
661
- color: #17a2b8;
662
- }
663
- .btn-preset.compact:hover {
664
- background: #17a2b8;
665
- }
666
- form {
667
- flex: 1;
668
- display: flex;
669
- flex-direction: column;
670
- overflow: hidden;
671
- min-height: 0;
672
- }
673
- </style>
674
- </head>
675
- <body>
676
- <div class="container">
677
- <h1>📄 Markdown to PDF Converter</h1>
678
- <p class="subtitle">Simplified settings with full color customization</p>
679
-
680
- <form method="POST" action="/generate">
681
- <div class="main-grid">
682
- <!-- Markdown Input -->
683
- <div class="textarea-wrapper">
684
- <textarea name="markdown" placeholder="Paste your markdown content here...
685
- # SQL Example
686
- ```sql
687
- -- Select products
688
- SELECT name, price
689
- FROM products
690
- WHERE price > 100;
691
- ```"></textarea>
692
- <button type="submit" class="btn-generate">Generate PDF</button>
693
- </div>
694
-
695
- <!-- Settings Panel -->
696
- <div class="settings-panel">
697
- <h3>⚙️ PDF Settings</h3>
698
-
699
- <!-- Presets -->
700
- <div class="preset-grid">
701
- <button type="button" class="btn-preset" onclick="applyDefault()">Default</button>
702
- <button type="button" class="btn-preset compact" onclick="applyCompact()">Compact</button>
703
- </div>
704
-
705
- <!-- Font Sizes -->
706
- <div class="section">
707
- <h4>📝 Font Sizes</h4>
708
- <div class="field">
709
- <label>Text Size (pt)</label>
710
- <input type="number" name="base_font_size" value="10" step="0.5" min="7" max="14">
711
- </div>
712
- <div class="field">
713
- <label>Code Size (pt)</label>
714
- <input type="number" name="code_font_size" value="7.5" step="0.5" min="6" max="12">
715
- </div>
716
- </div>
717
-
718
- <!-- Page Settings -->
719
- <div class="section">
720
- <h4>📄 Page Settings</h4>
721
- <div class="field">
722
- <label>Page Size</label>
723
- <select name="page_size" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 0.9em;">
724
- <option value="A4">A4 (21cm × 29.7cm)</option>
725
- <option value="Letter">Letter (21.6cm × 27.9cm)</option>
726
- <option value="Legal">Legal (21.6cm × 35.6cm)</option>
727
- <option value="A3">A3 (29.7cm × 42cm)</option>
728
- </select>
729
- </div>
730
- <div class="field">
731
- <label>Page Margin (cm)</label>
732
- <input type="number" name="page_margin" value="1.5" step="0.1" min="0.5" max="4">
733
- </div>
734
- </div>
735
-
736
- <!-- Spacing -->
737
- <div class="section">
738
- <h4>📐 Spacing</h4>
739
- <div class="field">
740
- <label>Paragraph Spacing (px)</label>
741
- <input type="number" name="paragraph_spacing" value="8" step="1" min="4" max="20">
742
- </div>
743
- <div class="field">
744
- <label>Code Padding - Top/Bottom (px)</label>
745
- <input type="number" name="code_padding_vertical" value="15" step="1" min="8" max="40">
746
- </div>
747
- <div class="field">
748
- <label>Code Padding - Left/Right (px)</label>
749
- <input type="number" name="code_padding_horizontal" value="12" step="1" min="8" max="40">
750
- </div>
751
- <div class="field">
752
- <label>Code Margin - Top (px)</label>
753
- <input type="number" name="code_margin_top" value="15" step="1" min="8" max="40">
754
- </div>
755
- <div class="field">
756
- <label>Code Margin - Bottom (px)</label>
757
- <input type="number" name="code_margin_bottom" value="15" step="1" min="8" max="40">
758
- </div>
759
- </div>
760
-
761
- <!-- Code Box Style -->
762
- <div class="section">
763
- <h4>📦 Code Box Style</h4>
764
- <div class="checkbox-field">
765
- <input type="checkbox" id="show_border" name="show_code_border" value="yes" checked>
766
- <label for="show_border">Show Border</label>
767
- </div>
768
- <div class="field">
769
- <label>Background Color</label>
770
- <input type="color" name="code_bg_color" value="#f5f5f5">
771
- </div>
772
- <div class="field">
773
- <label>Border Color</label>
774
- <input type="color" name="code_border_color" value="#dddddd">
775
- </div>
776
- <div class="field">
777
- <label>Accent Color (left bar)</label>
778
- <input type="color" name="code_accent_color" value="#3498db">
779
- </div>
780
- </div>
781
-
782
- <!-- Syntax Colors -->
783
- <div class="section">
784
- <h4>🎨 Syntax Highlighting</h4>
785
- <div class="field">
786
- <label>Keywords (SELECT, def, class)</label>
787
- <input type="color" name="keyword_color" value="#0000ff">
788
- </div>
789
- <div class="field">
790
- <label>Strings ('text', "text")</label>
791
- <input type="color" name="string_color" value="#ff8c00">
792
- </div>
793
- <div class="field">
794
- <label>Comments (-- , #)</label>
795
- <input type="color" name="comment_color" value="#006400">
796
- </div>
797
- <div class="field">
798
- <label>Numbers (123, 45.67)</label>
799
- <input type="color" name="number_color" value="#098658">
800
- </div>
801
- <div class="field">
802
- <label>Functions (COUNT, SUM)</label>
803
- <input type="color" name="function_color" value="#795e26">
804
- </div>
805
- </div>
806
- </div>
807
- </div>
808
- </form>
809
- </div>
810
-
811
- <script>
812
- function applyDefault() {
813
- document.querySelector('[name="base_font_size"]').value = 10;
814
- document.querySelector('[name="code_font_size"]').value = 7.5;
815
- document.querySelector('[name="page_size"]').value = "A4";
816
- document.querySelector('[name="page_margin"]').value = 1.5;
817
- document.querySelector('[name="paragraph_spacing"]').value = 8;
818
- document.querySelector('[name="code_padding_vertical"]').value = 15;
819
- document.querySelector('[name="code_padding_horizontal"]').value = 12;
820
- document.querySelector('[name="code_margin_top"]').value = 15;
821
- document.querySelector('[name="code_margin_bottom"]').value = 15;
822
- document.querySelector('[name="show_code_border"]').checked = true;
823
- document.querySelector('[name="code_bg_color"]').value = "#f5f5f5";
824
- document.querySelector('[name="code_border_color"]').value = "#dddddd";
825
- document.querySelector('[name="code_accent_color"]').value = "#3498db";
826
- document.querySelector('[name="keyword_color"]').value = "#0000ff";
827
- document.querySelector('[name="string_color"]').value = "#ff8c00";
828
- document.querySelector('[name="comment_color"]').value = "#006400";
829
- document.querySelector('[name="number_color"]').value = "#098658";
830
- document.querySelector('[name="function_color"]').value = "#795e26";
831
- }
832
-
833
- function applyCompact() {
834
- document.querySelector('[name="base_font_size"]').value = 9;
835
- document.querySelector('[name="code_font_size"]').value = 7;
836
- document.querySelector('[name="page_size"]').value = "A4";
837
- document.querySelector('[name="page_margin"]').value = 1.0;
838
- document.querySelector('[name="paragraph_spacing"]').value = 5;
839
- document.querySelector('[name="code_padding_vertical"]').value = 10;
840
- document.querySelector('[name="code_padding_horizontal"]').value = 10;
841
- document.querySelector('[name="code_margin_top"]').value = 10;
842
- document.querySelector('[name="code_margin_bottom"]').value = 10;
843
- document.querySelector('[name="show_code_border"]').checked = true;
844
- document.querySelector('[name="code_bg_color"]').value = "#f5f5f5";
845
- document.querySelector('[name="code_border_color"]').value = "#dddddd";
846
- document.querySelector('[name="code_accent_color"]').value = "#3498db";
847
- document.querySelector('[name="keyword_color"]').value = "#0000ff";
848
- document.querySelector('[name="string_color"]').value = "#ff8c00";
849
- document.querySelector('[name="comment_color"]').value = "#006400";
850
- document.querySelector('[name="number_color"]').value = "#098658";
851
- document.querySelector('[name="function_color"]').value = "#795e26";
852
- }
853
- </script>
854
- </body>
855
- </html>
856
- ''')
857
- @app.route('/generate', methods=['POST'])
858
- def generate_pdf():
859
- markdown_text = request.form.get('markdown', '')
860
-
861
- if not markdown_text:
862
- return "No markdown content provided", 400
863
-
864
- # Get settings from form
865
- settings = {
866
- 'base_font_size': float(request.form.get('base_font_size', 10)),
867
- 'code_font_size': float(request.form.get('code_font_size', 7.5)),
868
- 'page_size': request.form.get('page_size', 'A4'),
869
- 'page_margin': float(request.form.get('page_margin', 1.5)),
870
- 'paragraph_spacing': int(request.form.get('paragraph_spacing', 8)),
871
- 'code_padding_vertical': int(request.form.get('code_padding_vertical', 15)),
872
- 'code_padding_horizontal': int(request.form.get('code_padding_horizontal', 12)),
873
- 'code_margin_top': int(request.form.get('code_margin_top', 15)),
874
- 'code_margin_bottom': int(request.form.get('code_margin_bottom', 15)),
875
- 'show_code_border': request.form.get('show_code_border') == 'yes',
876
- 'code_bg_color': request.form.get('code_bg_color', '#f5f5f5'),
877
- 'code_border_color': request.form.get('code_border_color', '#dddddd'),
878
- 'code_accent_color': request.form.get('code_accent_color', '#3498db'),
879
- 'keyword_color': request.form.get('keyword_color', '#0000ff'),
880
- 'string_color': request.form.get('string_color', '#ff8c00'),
881
- 'comment_color': request.form.get('comment_color', '#006400'),
882
- 'number_color': request.form.get('number_color', '#098658'),
883
- 'function_color': request.form.get('function_color', '#795e26'),
884
- }
885
-
886
- # Generate CSS with user settings
887
- css = generate_css(settings)
888
-
889
- # Convert markdown to HTML
890
- content_html = process_markdown(markdown_text)
891
-
892
- # Render the full HTML
893
- full_html = render_template_string(HTML_TEMPLATE, css=css, content=content_html)
894
-
895
- # Generate PDF
896
- pdf_file = BytesIO()
897
- pisa_status = pisa.CreatePDF(full_html, dest=pdf_file)
898
-
899
- if pisa_status.err:
900
- return "Error generating PDF", 500
901
-
902
- pdf_file.seek(0)
903
-
904
- return send_file(
905
- pdf_file,
906
- mimetype='application/pdf',
907
- as_attachment=True,
908
- download_name='sql_guide.pdf'
909
- )
910
- if __name__ == '__main__':
911
- app.run(host='0.0.0.0', port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template_string, request, send_file
2
+ import markdown
3
+ from io import BytesIO
4
+ import re
5
+ from xhtml2pdf import pisa
6
+
7
+ app = Flask(__name__)
8
+
9
+
10
+ def extract_first_header(md_text):
11
+ """Extract the first header from markdown text for use as filename"""
12
+ # Match headers (# Header, ## Header, etc.)
13
+ header_pattern = r'^#{1,6}\s+(.+?)$'
14
+ match = re.search(header_pattern, md_text, re.MULTILINE)
15
+
16
+ if match:
17
+ header_text = match.group(1).strip()
18
+ # Remove any markdown formatting from header (bold, italic, links, etc.)
19
+ header_text = re.sub(r'\*\*(.+?)\*\*', r'\1', header_text) # Bold
20
+ header_text = re.sub(r'\*(.+?)\*', r'\1', header_text) # Italic
21
+ header_text = re.sub(r'__(.+?)__', r'\1', header_text) # Bold
22
+ header_text = re.sub(r'_(.+?)_', r'\1', header_text) # Italic
23
+ header_text = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', header_text) # Links
24
+ header_text = re.sub(r'`(.+?)`', r'\1', header_text) # Inline code
25
+
26
+ # Clean up for filename (remove invalid characters)
27
+ filename = re.sub(r'[<>:"/\\|?*]', '', header_text)
28
+ filename = filename.strip()
29
+
30
+ # Limit filename length
31
+ if len(filename) > 100:
32
+ filename = filename[:100].rsplit(' ', 1)[0]
33
+
34
+ return filename if filename else 'document'
35
+
36
+ return 'document'
37
+
38
+
39
+ def generate_css(settings):
40
+ """Generate CSS based on user settings"""
41
+ base_size = settings['base_font_size']
42
+ h1_size = base_size * 2
43
+ h2_size = base_size * 1.4
44
+ h3_size = base_size * 1.2
45
+ page_size = settings['page_size']
46
+ wrap_behavior = 'pre-wrap' if settings['enable_wrap'] else 'pre'
47
+
48
+ return f'''
49
+ @page {{
50
+ size: {page_size};
51
+ margin: {settings['page_margin']}cm;
52
+ }}
53
+
54
+ * {{
55
+ margin: 0;
56
+ padding: 0;
57
+ box-sizing: border-box;
58
+ }}
59
+
60
+ body {{
61
+ font-family: 'DejaVu Sans', Georgia, serif;
62
+ line-height: 1.5;
63
+ color: #1a1a1a;
64
+ font-size: {settings['base_font_size']}pt;
65
+ -pdf-encoding: utf-8;
66
+ }}
67
+
68
+ h1 {{
69
+ color: #2c3e50;
70
+ font-size: {h1_size}pt;
71
+ margin-bottom: 15px;
72
+ margin-top: 0;
73
+ padding-bottom: 8px;
74
+ border-bottom: 2px solid #3498db;
75
+ page-break-after: avoid;
76
+ }}
77
+
78
+ h2 {{
79
+ color: #34495e;
80
+ font-size: {h2_size}pt;
81
+ margin-top: 20px;
82
+ margin-bottom: 10px;
83
+ padding-top: 5px;
84
+ border-top: 1px solid #ecf0f1;
85
+ page-break-after: avoid;
86
+ }}
87
+
88
+ h3 {{
89
+ color: #555;
90
+ font-size: {h3_size}pt;
91
+ margin-top: 15px;
92
+ margin-bottom: 8px;
93
+ page-break-after: avoid;
94
+ }}
95
+
96
+ h4 {{
97
+ color: #666;
98
+ font-size: {base_size}pt;
99
+ margin-top: 12px;
100
+ margin-bottom: 6px;
101
+ }}
102
+
103
+ p {{
104
+ margin-bottom: {settings['paragraph_spacing']}px;
105
+ text-align: justify;
106
+ }}
107
+
108
+ strong {{
109
+ color: #2c3e50;
110
+ font-weight: 600;
111
+ }}
112
+
113
+ code {{
114
+ font-family: 'DejaVu Sans Mono', 'DejaVu Sans', 'Consolas', 'Monaco', 'Courier New', monospace;
115
+ font-size: {settings['base_font_size']}pt;
116
+ color: #000000;
117
+ }}
118
+
119
+ pre {{
120
+ background-color: {settings['code_bg_color']} !important;
121
+ padding: {settings['code_padding_vertical']}px {settings['code_padding_horizontal']}px !important;
122
+ margin: {settings['code_margin_top']}px 0 {settings['code_margin_bottom']}px 0 !important;
123
+ border-radius: 3px;
124
+ overflow: visible;
125
+ word-wrap: break-word;
126
+ }}
127
+
128
+ pre code {{
129
+ display: none;
130
+ }}
131
+
132
+ .code-line {{
133
+ font-family: 'DejaVu Sans Mono', 'DejaVu Sans', 'Consolas', 'Monaco', 'Courier New', monospace;
134
+ font-size: {settings['code_font_size']}pt;
135
+ line-height: 1.4;
136
+ margin: 0;
137
+ padding: 0;
138
+ border: none;
139
+ background: transparent !important;
140
+ display: block;
141
+ white-space: pre-wrap;
142
+ word-break: break-word;
143
+ }}
144
+
145
+ .code-block {{
146
+ background-color: {settings['code_bg_color']} !important;
147
+ overflow: visible;
148
+ white-space: normal;
149
+ word-wrap: break-word;
150
+ }}
151
+
152
+ .sql-keyword {{
153
+ color: {settings['keyword_color']};
154
+ font-weight: bold;
155
+ }}
156
+
157
+ .sql-comment {{
158
+ color: {settings['comment_color']};
159
+ font-style: italic;
160
+ }}
161
+
162
+ .sql-string {{
163
+ color: {settings['string_color']};
164
+ }}
165
+
166
+ .sql-number {{
167
+ color: {settings['number_color']};
168
+ }}
169
+
170
+ .sql-function {{
171
+ color: {settings['function_color']};
172
+ font-weight: bold;
173
+ }}
174
+
175
+ .py-keyword {{
176
+ color: {settings['keyword_color']};
177
+ font-weight: bold;
178
+ }}
179
+
180
+ .py-string {{
181
+ color: {settings['string_color']};
182
+ }}
183
+
184
+ .py-number {{
185
+ color: {settings['number_color']};
186
+ }}
187
+
188
+ .py-comment {{
189
+ color: {settings['comment_color']};
190
+ font-style: italic;
191
+ }}
192
+
193
+ .py-function {{
194
+ color: {settings['function_color']};
195
+ }}
196
+
197
+ .py-builtin {{
198
+ color: {settings['keyword_color']};
199
+ }}
200
+
201
+ .py-decorator {{
202
+ color: #808080;
203
+ }}
204
+
205
+ table {{
206
+ width: 100%;
207
+ border-collapse: collapse;
208
+ margin: 10px 0;
209
+ font-size: {settings['base_font_size'] - 1}pt;
210
+ page-break-inside: avoid;
211
+ }}
212
+
213
+ table th {{
214
+ background-color: #3498db;
215
+ color: white;
216
+ padding: 5px 8px;
217
+ text-align: left;
218
+ font-weight: 600;
219
+ border: 1px solid #2980b9;
220
+ line-height: 1.2;
221
+ }}
222
+
223
+ table td {{
224
+ border: 1px solid #ddd;
225
+ padding: 4px 8px;
226
+ vertical-align: top;
227
+ line-height: 1.2;
228
+ }}
229
+
230
+ table tr:nth-child(even) {{
231
+ background-color: #f9f9f9;
232
+ }}
233
+
234
+ ul, ol {{
235
+ margin-left: 25px;
236
+ margin-bottom: 10px;
237
+ }}
238
+
239
+ li {{
240
+ margin-bottom: 4px;
241
+ line-height: 1.4;
242
+ }}
243
+
244
+ blockquote {{
245
+ border-left: 3px solid #3498db;
246
+ padding: 8px 12px;
247
+ margin: 10px 0;
248
+ color: #555;
249
+ font-style: italic;
250
+ background-color: #f8f9fa;
251
+ }}
252
+
253
+ hr {{
254
+ border: none;
255
+ border-top: 1px solid #ecf0f1;
256
+ margin: 15px 0;
257
+ }}
258
+
259
+ .warning {{
260
+ background-color: #fff3cd;
261
+ border-left: 3px solid #ffc107;
262
+ padding: 8px 12px;
263
+ margin: 10px 0;
264
+ border-radius: 3px;
265
+ }}
266
+
267
+ .success {{
268
+ background-color: #d4edda;
269
+ border-left: 3px solid #28a745;
270
+ padding: 8px 12px;
271
+ margin: 10px 0;
272
+ border-radius: 3px;
273
+ }}
274
+
275
+ .error {{
276
+ background-color: #f8d7da;
277
+ border-left: 3px solid #dc3545;
278
+ padding: 8px 12px;
279
+ margin: 10px 0;
280
+ border-radius: 3px;
281
+ }}
282
+
283
+ .info {{
284
+ background-color: #d1ecf1;
285
+ border-left: 3px solid #0c5460;
286
+ padding: 8px 12px;
287
+ margin: 10px 0;
288
+ border-radius: 3px;
289
+ }}
290
+ '''
291
+
292
+
293
+ HTML_TEMPLATE = '''
294
+ <!DOCTYPE html>
295
+ <html lang="en">
296
+ <head>
297
+ <meta charset="UTF-8">
298
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
299
+ <title>Document</title>
300
+ <style>
301
+ {{ css|safe }}
302
+ </style>
303
+ </head>
304
+ <body>
305
+ {{ content|safe }}
306
+ </body>
307
+ </html>
308
+ '''
309
+
310
+
311
+ def highlight_sql(code):
312
+ """LaTeX-quality SQL syntax highlighting"""
313
+ keywords = [
314
+ 'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER',
315
+ 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER',
316
+ 'FULL', 'CROSS', 'ON', 'USING', 'AS', 'AND', 'OR', 'NOT', 'NULL', 'IS', 'IN',
317
+ 'BETWEEN', 'LIKE', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET', 'DISTINCT',
318
+ 'UNION', 'ALL', 'INTERSECT', 'EXCEPT', 'EXISTS', 'CASE', 'WHEN', 'THEN', 'ELSE',
319
+ 'END', 'IF', 'WITH', 'RECURSIVE', 'ASC', 'DESC', 'INTO', 'VALUES', 'SET', 'DEFAULT',
320
+ 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'UNIQUE', 'CHECK',
321
+ 'AUTO_INCREMENT', 'SERIAL', 'AUTOINCREMENT', 'IDENTITY', 'RETURNS', 'BEGIN',
322
+ 'COMMIT', 'ROLLBACK', 'TRANSACTION', 'GRANT', 'REVOKE', 'CASCADE', 'RESTRICT',
323
+ 'INT', 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT', 'DECIMAL', 'NUMERIC', 'FLOAT',
324
+ 'REAL', 'DOUBLE', 'VARCHAR', 'CHAR', 'TEXT', 'BLOB', 'DATE', 'TIME', 'DATETIME',
325
+ 'TIMESTAMP', 'BOOLEAN', 'BOOL', 'ENUM', 'JSON', 'ARRAY'
326
+ ]
327
+ functions = [
328
+ 'COUNT', 'SUM', 'AVG', 'MAX', 'MIN', 'CONCAT', 'UPPER', 'LOWER', 'LENGTH',
329
+ 'SUBSTRING', 'TRIM', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'NOW', 'CURRENT_DATE',
330
+ 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DATE', 'TIME', 'YEAR', 'MONTH', 'DAY',
331
+ 'COALESCE', 'NULLIF', 'CAST', 'CONVERT'
332
+ ]
333
+
334
+ result = code
335
+ comment_placeholder = {}
336
+ comment_counter = 0
337
+
338
+ matches = list(re.finditer(r'--[^\n]*', result))
339
+ for match in reversed(matches):
340
+ placeholder = f'___COMMENT_{comment_counter}___'
341
+ comment_text = match.group(0).rstrip('\n')
342
+ comment_placeholder[placeholder] = f'<span class="sql-comment">{comment_text}</span>'
343
+ result = result[:match.start()] + placeholder + result[match.end():]
344
+ comment_counter += 1
345
+
346
+ matches = list(re.finditer(r'/\*.*?\*/', result, re.DOTALL))
347
+ for match in reversed(matches):
348
+ placeholder = f'___COMMENT_{comment_counter}___'
349
+ comment_placeholder[placeholder] = f'<span class="sql-comment">{match.group(0)}</span>'
350
+ result = result[:match.start()] + placeholder + result[match.end():]
351
+ comment_counter += 1
352
+
353
+ string_placeholder = {}
354
+ string_counter = 0
355
+
356
+ matches = list(re.finditer(r"'(?:[^'\\]|\\.)*'", result))
357
+ for match in reversed(matches):
358
+ placeholder = f'___STRING_{string_counter}___'
359
+ string_placeholder[placeholder] = f'<span class="sql-string">{match.group(0)}</span>'
360
+ result = result[:match.start()] + placeholder + result[match.end():]
361
+ string_counter += 1
362
+
363
+ result = re.sub(r'\b(\d+\.?\d*)\b', r'<span class="sql-number">\1</span>', result)
364
+
365
+ for func in functions:
366
+ result = re.sub(
367
+ r'\b(' + func + r')\s*\(',
368
+ r'<span class="sql-function">\1</span>(',
369
+ result,
370
+ flags=re.IGNORECASE
371
+ )
372
+
373
+ for keyword in keywords:
374
+ result = re.sub(
375
+ r'\b(' + keyword + r')\b',
376
+ r'<span class="sql-keyword">\1</span>',
377
+ result,
378
+ flags=re.IGNORECASE
379
+ )
380
+
381
+ for placeholder, original in string_placeholder.items():
382
+ result = result.replace(placeholder, original)
383
+
384
+ for placeholder, original in comment_placeholder.items():
385
+ result = result.replace(placeholder, original)
386
+
387
+ return result
388
+
389
+
390
+ def highlight_python(code, is_pyspark=False):
391
+ """LaTeX-quality Python/PySpark syntax highlighting"""
392
+ keywords = [
393
+ 'def', 'class', 'import', 'from', 'as', 'if', 'elif', 'else', 'for', 'while',
394
+ 'return', 'try', 'except', 'finally', 'with', 'lambda', 'yield', 'async', 'await',
395
+ 'pass', 'break', 'continue', 'and', 'or', 'not', 'in', 'is', 'None', 'True', 'False',
396
+ 'raise', 'assert', 'del', 'global', 'nonlocal'
397
+ ]
398
+ builtins = [
399
+ 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
400
+ 'open', 'input', 'type', 'isinstance', 'enumerate', 'zip', 'map', 'filter', 'sum',
401
+ 'max', 'min', 'sorted', 'reversed', 'all', 'any', 'abs', 'round', 'pow'
402
+ ]
403
+
404
+ # PySpark specific keywords and functions
405
+ if is_pyspark:
406
+ builtins.extend([
407
+ 'SparkSession', 'SparkContext', 'SQLContext', 'HiveContext',
408
+ 'DataFrame', 'Column', 'Row', 'GroupedData',
409
+ 'select', 'filter', 'where', 'groupBy', 'orderBy', 'sortBy',
410
+ 'join', 'union', 'distinct', 'drop', 'dropDuplicates',
411
+ 'withColumn', 'withColumnRenamed', 'alias', 'cast',
412
+ 'agg', 'count', 'collect', 'show', 'printSchema', 'describe',
413
+ 'read', 'write', 'csv', 'json', 'parquet', 'orc', 'jdbc',
414
+ 'createDataFrame', 'createOrReplaceTempView', 'sql',
415
+ 'cache', 'persist', 'unpersist', 'checkpoint', 'repartition', 'coalesce',
416
+ 'broadcast', 'accumulator', 'parallelize',
417
+ 'map', 'flatMap', 'reduceByKey', 'groupByKey', 'sortByKey',
418
+ 'col', 'lit', 'when', 'otherwise', 'isnull', 'isnan',
419
+ 'concat', 'concat_ws', 'substring', 'trim', 'lower', 'upper',
420
+ 'split', 'explode', 'array', 'struct', 'to_date', 'to_timestamp',
421
+ 'datediff', 'date_add', 'date_sub', 'year', 'month', 'dayofmonth',
422
+ 'window', 'partitionBy', 'over', 'rowNumber', 'rank', 'dense_rank',
423
+ 'lag', 'lead', 'first', 'last', 'collect_list', 'collect_set',
424
+ 'approx_count_distinct', 'countDistinct', 'sumDistinct',
425
+ 'udf', 'pandas_udf', 'PandasUDFType'
426
+ ])
427
+
428
+ result = code
429
+ comment_placeholder = {}
430
+ comment_counter = 0
431
+
432
+ matches = list(re.finditer(r'#[^\n]*', result))
433
+ for match in reversed(matches):
434
+ placeholder = f'___COMMENT_{comment_counter}___'
435
+ comment_text = match.group(0).rstrip('\n')
436
+ comment_placeholder[placeholder] = f'<span class="py-comment">{comment_text}</span>'
437
+ result = result[:match.start()] + placeholder + result[match.end():]
438
+ comment_counter += 1
439
+
440
+ string_placeholder = {}
441
+ string_counter = 0
442
+
443
+ matches = list(re.finditer(r'"""(?:[^"\\]|\\.)*?"""|\'\'\'(?:[^\'\\]|\\.)*?\'\'\'', result, re.DOTALL))
444
+ for match in reversed(matches):
445
+ placeholder = f'___STRING_{string_counter}___'
446
+ string_placeholder[placeholder] = f'<span class="py-string">{match.group(0)}</span>'
447
+ result = result[:match.start()] + placeholder + result[match.end():]
448
+ string_counter += 1
449
+
450
+ matches = list(re.finditer(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'', result))
451
+ for match in reversed(matches):
452
+ placeholder = f'___STRING_{string_counter}___'
453
+ string_placeholder[placeholder] = f'<span class="py-string">{match.group(0)}</span>'
454
+ result = result[:match.start()] + placeholder + result[match.end():]
455
+ string_counter += 1
456
+
457
+ result = re.sub(r'(@\w+)', r'<span class="py-decorator">\1</span>', result)
458
+ result = re.sub(r'\b(\d+\.?\d*)\b', r'<span class="py-number">\1</span>', result)
459
+
460
+ for keyword in keywords:
461
+ result = re.sub(r'\b(' + keyword + r')\b', r'<span class="py-keyword">\1</span>', result)
462
+
463
+ for builtin in builtins:
464
+ result = re.sub(r'\b(' + builtin + r')\b', r'<span class="py-builtin">\1</span>', result)
465
+
466
+ for placeholder, original in string_placeholder.items():
467
+ result = result.replace(placeholder, original)
468
+
469
+ for placeholder, original in comment_placeholder.items():
470
+ result = result.replace(placeholder, original)
471
+
472
+ return result
473
+
474
+
475
+ def highlight_r(code):
476
+ """LaTeX-quality R syntax highlighting"""
477
+ keywords = [
478
+ 'if', 'else', 'for', 'while', 'repeat', 'in', 'next', 'break',
479
+ 'function', 'return', 'TRUE', 'FALSE', 'NULL', 'NA', 'NA_integer_',
480
+ 'NA_real_', 'NA_complex_', 'NA_character_', 'Inf', 'NaN',
481
+ 'library', 'require', 'source', 'setwd', 'getwd'
482
+ ]
483
+ builtins = [
484
+ 'print', 'cat', 'paste', 'paste0', 'sprintf', 'format',
485
+ 'c', 'list', 'vector', 'matrix', 'array', 'data.frame', 'tibble',
486
+ 'length', 'nrow', 'ncol', 'dim', 'names', 'colnames', 'rownames',
487
+ 'head', 'tail', 'str', 'summary', 'class', 'typeof', 'mode',
488
+ 'sum', 'mean', 'median', 'sd', 'var', 'min', 'max', 'range',
489
+ 'abs', 'sqrt', 'log', 'log10', 'log2', 'exp', 'round', 'floor', 'ceiling',
490
+ 'seq', 'rep', 'sort', 'order', 'rank', 'rev', 'unique', 'duplicated',
491
+ 'which', 'any', 'all', 'is.na', 'is.null', 'is.numeric', 'is.character',
492
+ 'as.numeric', 'as.character', 'as.factor', 'as.Date', 'as.POSIXct',
493
+ 'subset', 'merge', 'rbind', 'cbind', 'split', 'apply', 'lapply', 'sapply',
494
+ 'mapply', 'tapply', 'aggregate', 'transform', 'within',
495
+ 'read.csv', 'read.table', 'write.csv', 'write.table', 'readRDS', 'saveRDS',
496
+ 'grep', 'grepl', 'sub', 'gsub', 'regexpr', 'strsplit', 'nchar', 'substr',
497
+ 'tolower', 'toupper', 'trimws', 'chartr',
498
+ 'factor', 'levels', 'nlevels', 'droplevels', 'cut', 'table', 'prop.table',
499
+ 'plot', 'hist', 'boxplot', 'barplot', 'pie', 'lines', 'points', 'abline',
500
+ 'ggplot', 'aes', 'geom_point', 'geom_line', 'geom_bar', 'geom_histogram',
501
+ 'geom_boxplot', 'facet_wrap', 'facet_grid', 'theme', 'labs', 'ggtitle',
502
+ 'mutate', 'select', 'filter', 'arrange', 'group_by', 'summarise', 'summarize',
503
+ 'left_join', 'right_join', 'inner_join', 'full_join', 'anti_join', 'semi_join',
504
+ 'bind_rows', 'bind_cols', 'pivot_longer', 'pivot_wider', 'gather', 'spread',
505
+ 'rename', 'relocate', 'across', 'everything', 'starts_with', 'ends_with',
506
+ 'contains', 'matches', 'num_range', 'where', 'pull', 'distinct', 'count',
507
+ 'slice', 'slice_head', 'slice_tail', 'slice_min', 'slice_max', 'slice_sample',
508
+ 'lm', 'glm', 'aov', 'anova', 't.test', 'chisq.test', 'cor', 'cov',
509
+ 'predict', 'fitted', 'residuals', 'coef', 'confint',
510
+ 'tryCatch', 'stop', 'warning', 'message', 'stopifnot'
511
+ ]
512
+
513
+ result = code
514
+ comment_placeholder = {}
515
+ comment_counter = 0
516
+
517
+ # R comments start with #
518
+ matches = list(re.finditer(r'#[^\n]*', result))
519
+ for match in reversed(matches):
520
+ placeholder = f'___COMMENT_{comment_counter}___'
521
+ comment_text = match.group(0).rstrip('\n')
522
+ comment_placeholder[placeholder] = f'<span class="py-comment">{comment_text}</span>'
523
+ result = result[:match.start()] + placeholder + result[match.end():]
524
+ comment_counter += 1
525
+
526
+ string_placeholder = {}
527
+ string_counter = 0
528
+
529
+ # Double and single quoted strings
530
+ matches = list(re.finditer(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'', result))
531
+ for match in reversed(matches):
532
+ placeholder = f'___STRING_{string_counter}___'
533
+ string_placeholder[placeholder] = f'<span class="py-string">{match.group(0)}</span>'
534
+ result = result[:match.start()] + placeholder + result[match.end():]
535
+ string_counter += 1
536
+
537
+ # Highlight numbers (including scientific notation)
538
+ result = re.sub(r'\b(\d+\.?\d*(?:[eE][+-]?\d+)?[Li]?)\b', r'<span class="py-number">\1</span>', result)
539
+
540
+ # Highlight keywords
541
+ for keyword in keywords:
542
+ result = re.sub(r'\b(' + re.escape(keyword) + r')\b', r'<span class="py-keyword">\1</span>', result)
543
+
544
+ # Highlight builtins/functions
545
+ for builtin in builtins:
546
+ result = re.sub(r'\b(' + re.escape(builtin) + r')\b', r'<span class="py-builtin">\1</span>', result)
547
+
548
+ # Highlight assignment operators
549
+ result = re.sub(r'(&lt;-|&lt;&lt;-|-&gt;|-&gt;&gt;)', r'<span class="py-keyword">\1</span>', result)
550
+ result = re.sub(r'(<-|<<-|->|->>)', r'<span class="py-keyword">\1</span>', result)
551
+
552
+ # Highlight pipe operators
553
+ result = re.sub(r'(%&gt;%|%&lt;&gt;%|\|&gt;)', r'<span class="sql-function">\1</span>', result)
554
+ result = re.sub(r'(%>%|%<>%|\|>)', r'<span class="sql-function">\1</span>', result)
555
+
556
+ # Restore strings and comments
557
+ for placeholder, original in string_placeholder.items():
558
+ result = result.replace(placeholder, original)
559
+
560
+ for placeholder, original in comment_placeholder.items():
561
+ result = result.replace(placeholder, original)
562
+
563
+ return result
564
+
565
+
566
+ def process_code_blocks(md_text, enable_wrap=True):
567
+ """Process all code blocks in markdown"""
568
+
569
+ def replace_code_block(match):
570
+ lang = match.group(1) if match.group(1) else ''
571
+ code = match.group(2).strip('\n')
572
+ lang_lower = lang.lower().strip()
573
+
574
+ code = code.replace('├──', '|--')
575
+ code = code.replace('└──', '`--')
576
+ code = code.replace('├─', '|-')
577
+ code = code.replace('└─', '`-')
578
+ code = code.replace('│', '|')
579
+ code = code.replace('─', '-')
580
+ code = code.replace('├', '|')
581
+ code = code.replace('└', '`')
582
+
583
+ if lang_lower in ['sql', 'mysql', 'postgresql', 'postgres', 'sqlite', 'tsql', 'plsql']:
584
+ highlighted = highlight_sql(code)
585
+ elif lang_lower in ['python', 'py', 'python3']:
586
+ highlighted = highlight_python(code, is_pyspark=False)
587
+ elif lang_lower in ['pyspark', 'spark']:
588
+ highlighted = highlight_python(code, is_pyspark=True)
589
+ elif lang_lower in ['r', 'rlang', 'rscript']:
590
+ highlighted = highlight_r(code)
591
+ else:
592
+ highlighted = code
593
+
594
+ lines = highlighted.split('\n')
595
+ html_lines = ''.join(f'<div class="code-line">{line if line.strip() else " "}</div>' for line in lines)
596
+ return f'<pre class="code-block">{html_lines}</pre>'
597
+
598
+ result = re.sub(r'```([a-zA-Z0-9]*)\n(.*?)\n```', replace_code_block, md_text, flags=re.DOTALL)
599
+ result = re.sub(r'```([a-zA-Z0-9]*)\n(.*?)```', replace_code_block, result, flags=re.DOTALL)
600
+ result = re.sub(r'```\s*([a-zA-Z0-9]*)\s*\n(.*?)```', replace_code_block, result, flags=re.DOTALL)
601
+
602
+ return result
603
+
604
+
605
+ def process_markdown(md_text, enable_wrap=True):
606
+ """Convert markdown to HTML with syntax highlighting"""
607
+ md_with_highlighted_code = process_code_blocks(md_text, enable_wrap)
608
+ html = markdown.markdown(md_with_highlighted_code, extensions=['tables', 'fenced_code'])
609
+ html = re.sub(r'style="[^"]*"', '', html)
610
+
611
+ html = re.sub(r'<p><strong>⚠️[^<]*</strong>', r'<div class="warning"><strong>⚠️ Warning:</strong>', html)
612
+ html = re.sub(r'<p><strong>✓[^<]*</strong>', r'<div class="success"><strong>✓ Best Practice:</strong>', html)
613
+ html = re.sub(r'<p><strong>✗[^<]*</strong>', r'<div class="error"><strong>✗ Common Mistake:</strong>', html)
614
+ html = re.sub(r'<p><strong>💡[^<]*</strong>', r'<div class="info"><strong>💡 Info:</strong>', html)
615
+
616
+ return html
617
+
618
+
619
+ @app.route('/')
620
+ def index():
621
+ return render_template_string('''
622
+ <!DOCTYPE html>
623
+ <html>
624
+ <head>
625
+ <title>Markdown to PDF Converter</title>
626
+ <style>
627
+ * { margin: 0; padding: 0; box-sizing: border-box; }
628
+ body {
629
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
630
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
631
+ height: 100vh;
632
+ padding: 20px;
633
+ overflow: hidden;
634
+ margin: 0;
635
+ }
636
+ .container {
637
+ max-width: 1400px;
638
+ height: 100%;
639
+ margin: 0 auto;
640
+ background: white;
641
+ border-radius: 20px;
642
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
643
+ padding: 30px;
644
+ display: flex;
645
+ flex-direction: column;
646
+ overflow: hidden;
647
+ }
648
+ h1 {
649
+ color: #2c3e50;
650
+ margin-bottom: 5px;
651
+ font-size: 2em;
652
+ }
653
+ .subtitle {
654
+ color: #7f8c8d;
655
+ margin-bottom: 15px;
656
+ font-size: 1em;
657
+ }
658
+ .main-grid {
659
+ display: grid;
660
+ grid-template-columns: 1fr 350px;
661
+ gap: 25px;
662
+ flex: 1;
663
+ overflow: hidden;
664
+ min-height: 0;
665
+ }
666
+ .textarea-wrapper {
667
+ display: flex;
668
+ flex-direction: column;
669
+ overflow: hidden;
670
+ min-height: 0;
671
+ }
672
+ textarea {
673
+ width: 100%;
674
+ flex: 1;
675
+ padding: 15px;
676
+ border: 2px solid #e0e0e0;
677
+ border-radius: 10px;
678
+ font-family: 'Consolas', monospace;
679
+ font-size: 13px;
680
+ resize: none;
681
+ line-height: 1.5;
682
+ overflow-y: auto;
683
+ min-height: 0;
684
+ }
685
+ textarea:focus {
686
+ outline: none;
687
+ border-color: #667eea;
688
+ }
689
+ .settings-panel {
690
+ background: #f8f9fa;
691
+ padding: 20px;
692
+ border-radius: 10px;
693
+ border-left: 4px solid #667eea;
694
+ overflow-y: auto;
695
+ overflow-x: hidden;
696
+ display: flex;
697
+ flex-direction: column;
698
+ min-height: 0;
699
+ }
700
+ .settings-panel h3 {
701
+ color: #2c3e50;
702
+ margin-bottom: 15px;
703
+ font-size: 1.1em;
704
+ position: sticky;
705
+ top: 0;
706
+ background: #f8f9fa;
707
+ padding: 5px 0;
708
+ z-index: 10;
709
+ }
710
+ .section {
711
+ margin-bottom: 20px;
712
+ padding-bottom: 15px;
713
+ border-bottom: 2px solid #dee2e6;
714
+ }
715
+ .section:last-child {
716
+ border-bottom: none;
717
+ margin-bottom: 0;
718
+ }
719
+ .section h4 {
720
+ color: #495057;
721
+ font-size: 0.95em;
722
+ margin-bottom: 12px;
723
+ text-transform: uppercase;
724
+ letter-spacing: 0.5px;
725
+ }
726
+ .field {
727
+ margin-bottom: 12px;
728
+ }
729
+ .field label {
730
+ display: block;
731
+ color: #666;
732
+ font-size: 0.9em;
733
+ margin-bottom: 5px;
734
+ font-weight: 500;
735
+ }
736
+ .field input[type="number"] {
737
+ width: 100%;
738
+ padding: 8px;
739
+ border: 1px solid #dee2e6;
740
+ border-radius: 5px;
741
+ font-size: 0.9em;
742
+ }
743
+ .field input[type="color"] {
744
+ width: 100%;
745
+ height: 40px;
746
+ border: 1px solid #dee2e6;
747
+ border-radius: 5px;
748
+ cursor: pointer;
749
+ }
750
+ .field select {
751
+ width: 100%;
752
+ padding: 8px;
753
+ border: 1px solid #dee2e6;
754
+ border-radius: 5px;
755
+ font-size: 0.9em;
756
+ }
757
+ .field input:focus, .field select:focus {
758
+ outline: none;
759
+ border-color: #667eea;
760
+ }
761
+ .checkbox-field {
762
+ display: flex;
763
+ align-items: center;
764
+ gap: 10px;
765
+ margin-bottom: 12px;
766
+ }
767
+ .checkbox-field input[type="checkbox"] {
768
+ width: 20px;
769
+ height: 20px;
770
+ cursor: pointer;
771
+ }
772
+ .checkbox-field label {
773
+ color: #666;
774
+ font-size: 0.9em;
775
+ margin: 0;
776
+ cursor: pointer;
777
+ }
778
+ .btn-generate {
779
+ width: 100%;
780
+ padding: 15px;
781
+ margin-top: 15px;
782
+ font-size: 16px;
783
+ font-weight: 600;
784
+ border: none;
785
+ border-radius: 10px;
786
+ cursor: pointer;
787
+ text-transform: uppercase;
788
+ letter-spacing: 1px;
789
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
790
+ color: white;
791
+ transition: all 0.3s;
792
+ flex-shrink: 0;
793
+ }
794
+ .btn-generate:hover {
795
+ transform: translateY(-2px);
796
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
797
+ }
798
+ .preset-grid {
799
+ display: grid;
800
+ grid-template-columns: 1fr 1fr;
801
+ gap: 10px;
802
+ margin-bottom: 20px;
803
+ }
804
+ .btn-preset {
805
+ padding: 10px;
806
+ font-size: 13px;
807
+ font-weight: 600;
808
+ border: 2px solid #28a745;
809
+ border-radius: 8px;
810
+ cursor: pointer;
811
+ background: white;
812
+ color: #28a745;
813
+ transition: all 0.3s;
814
+ }
815
+ .btn-preset:hover {
816
+ background: #28a745;
817
+ color: white;
818
+ }
819
+ .btn-preset.compact {
820
+ border-color: #17a2b8;
821
+ color: #17a2b8;
822
+ }
823
+ .btn-preset.compact:hover {
824
+ background: #17a2b8;
825
+ }
826
+ form {
827
+ flex: 1;
828
+ display: flex;
829
+ flex-direction: column;
830
+ overflow: hidden;
831
+ min-height: 0;
832
+ }
833
+ .wrap-toggle {
834
+ background-color: #e7f3ff;
835
+ padding: 12px;
836
+ border-radius: 8px;
837
+ border-left: 3px solid #667eea;
838
+ margin-bottom: 15px;
839
+ }
840
+ </style>
841
+ </head>
842
+ <body>
843
+ <div class="container">
844
+ <h1>📄 Markdown to PDF Converter</h1>
845
+ <p class="subtitle">PDF filename will be based on your first header</p>
846
+
847
+ <form method="POST" action="/generate">
848
+ <div class="main-grid">
849
+ <div class="textarea-wrapper">
850
+ <textarea name="markdown" placeholder="# Your Document Title
851
+
852
+ Your content here...
853
+
854
+ ```sql
855
+ SELECT * FROM users;
856
+ ```"></textarea>
857
+ <button type="submit" class="btn-generate">Generate PDF</button>
858
+ </div>
859
+
860
+ <div class="settings-panel">
861
+ <h3>⚙️ PDF Settings</h3>
862
+
863
+ <div class="preset-grid">
864
+ <button type="button" class="btn-preset" onclick="applyDefault()">Default</button>
865
+ <button type="button" class="btn-preset compact" onclick="applyCompact()">Compact</button>
866
+ </div>
867
+
868
+ <div class="wrap-toggle">
869
+ <div class="checkbox-field">
870
+ <input type="checkbox" name="enable_wrap" id="enable_wrap" value="true" checked>
871
+ <label for="enable_wrap">✨ Enable code wrapping</label>
872
+ </div>
873
+ </div>
874
+
875
+ <div class="section">
876
+ <h4>📝 Font Sizes</h4>
877
+ <div class="field">
878
+ <label>Text Size (pt)</label>
879
+ <input type="number" name="base_font_size" value="12" step="0.5" min="7" max="14">
880
+ </div>
881
+ <div class="field">
882
+ <label>Code Size (pt)</label>
883
+ <input type="number" name="code_font_size" value="11" step="0.5" min="6" max="12">
884
+ </div>
885
+ </div>
886
+
887
+ <div class="section">
888
+ <h4>📄 Page Settings</h4>
889
+ <div class="field">
890
+ <label>Page Size</label>
891
+ <select name="page_size">
892
+ <option value="A4">A4 (21cm × 29.7cm)</option>
893
+ <option value="Letter">Letter (21.6cm × 27.9cm)</option>
894
+ <option value="Legal">Legal (21.6cm × 35.6cm)</option>
895
+ <option value="A3">A3 (29.7cm × 42cm)</option>
896
+ </select>
897
+ </div>
898
+ <div class="field">
899
+ <label>Page Margin (cm)</label>
900
+ <input type="number" name="page_margin" value="1.5" step="0.1" min="0.5" max="4">
901
+ </div>
902
+ </div>
903
+
904
+ <div class="section">
905
+ <h4>📐 Spacing</h4>
906
+ <div class="field">
907
+ <label>Paragraph Spacing (px)</label>
908
+ <input type="number" name="paragraph_spacing" value="8" step="1" min="4" max="20">
909
+ </div>
910
+ <div class="field">
911
+ <label>Code Padding - Top/Bottom (px)</label>
912
+ <input type="number" name="code_padding_vertical" value="15" step="1" min="8" max="40">
913
+ </div>
914
+ <div class="field">
915
+ <label>Code Padding - Left/Right (px)</label>
916
+ <input type="number" name="code_padding_horizontal" value="12" step="1" min="8" max="40">
917
+ </div>
918
+ <div class="field">
919
+ <label>Code Margin - Top (px)</label>
920
+ <input type="number" name="code_margin_top" value="15" step="1" min="8" max="40">
921
+ </div>
922
+ <div class="field">
923
+ <label>Code Margin - Bottom (px)</label>
924
+ <input type="number" name="code_margin_bottom" value="15" step="1" min="8" max="40">
925
+ </div>
926
+ </div>
927
+
928
+ <div class="section">
929
+ <h4>📦 Code Box Style</h4>
930
+ <div class="field">
931
+ <label>Background Color</label>
932
+ <input type="color" name="code_bg_color" value="#f5f5f5">
933
+ </div>
934
+ </div>
935
+
936
+ <div class="section">
937
+ <h4>🎨 Syntax Highlighting</h4>
938
+ <div class="field">
939
+ <label>Keywords</label>
940
+ <input type="color" name="keyword_color" value="#00BFFF">
941
+ </div>
942
+ <div class="field">
943
+ <label>Strings</label>
944
+ <input type="color" name="string_color" value="#ff8c00">
945
+ </div>
946
+ <div class="field">
947
+ <label>Comments</label>
948
+ <input type="color" name="comment_color" value="#006400">
949
+ </div>
950
+ <div class="field">
951
+ <label>Numbers</label>
952
+ <input type="color" name="number_color" value="#FF00FF">
953
+ </div>
954
+ <div class="field">
955
+ <label>Functions</label>
956
+ <input type="color" name="function_color" value="#795e26">
957
+ </div>
958
+ </div>
959
+ </div>
960
+ </div>
961
+ </form>
962
+ </div>
963
+
964
+ <script>
965
+ function applyDefault() {
966
+ document.querySelector('[name="base_font_size"]').value = 12;
967
+ document.querySelector('[name="code_font_size"]').value = 11;
968
+ document.querySelector('[name="page_size"]').value = "A4";
969
+ document.querySelector('[name="page_margin"]').value = 1.5;
970
+ document.querySelector('[name="paragraph_spacing"]').value = 8;
971
+ document.querySelector('[name="code_padding_vertical"]').value = 15;
972
+ document.querySelector('[name="code_padding_horizontal"]').value = 12;
973
+ document.querySelector('[name="code_margin_top"]').value = 15;
974
+ document.querySelector('[name="code_margin_bottom"]').value = 15;
975
+ document.querySelector('[name="code_bg_color"]').value = "#f5f5f5";
976
+ document.querySelector('[name="keyword_color"]').value = "#00BFFF";
977
+ document.querySelector('[name="string_color"]').value = "#ff8c00";
978
+ document.querySelector('[name="comment_color"]').value = "#006400";
979
+ document.querySelector('[name="number_color"]').value = "#FF00FF";
980
+ document.querySelector('[name="function_color"]').value = "#795e26";
981
+ document.querySelector('#enable_wrap').checked = true;
982
+ }
983
+
984
+ function applyCompact() {
985
+ document.querySelector('[name="base_font_size"]').value = 9;
986
+ document.querySelector('[name="code_font_size"]').value = 7;
987
+ document.querySelector('[name="page_size"]').value = "A4";
988
+ document.querySelector('[name="page_margin"]').value = 1.0;
989
+ document.querySelector('[name="paragraph_spacing"]').value = 5;
990
+ document.querySelector('[name="code_padding_vertical"]').value = 10;
991
+ document.querySelector('[name="code_padding_horizontal"]').value = 10;
992
+ document.querySelector('[name="code_margin_top"]').value = 10;
993
+ document.querySelector('[name="code_margin_bottom"]').value = 10;
994
+ document.querySelector('[name="code_bg_color"]').value = "#f5f5f5";
995
+ document.querySelector('[name="keyword_color"]').value = "#0000ff";
996
+ document.querySelector('[name="string_color"]').value = "#ff8c00";
997
+ document.querySelector('[name="comment_color"]').value = "#006400";
998
+ document.querySelector('[name="number_color"]').value = "#098658";
999
+ document.querySelector('[name="function_color"]').value = "#795e26";
1000
+ document.querySelector('#enable_wrap').checked = true;
1001
+ }
1002
+ </script>
1003
+ </body>
1004
+ </html>
1005
+ ''')
1006
+
1007
+
1008
+ @app.route('/generate', methods=['POST'])
1009
+ def generate_pdf():
1010
+ markdown_text = request.form.get('markdown', '')
1011
+
1012
+ if not markdown_text:
1013
+ return "No markdown content provided", 400
1014
+
1015
+ # Extract filename from first header
1016
+ pdf_filename = extract_first_header(markdown_text) + '.pdf'
1017
+
1018
+ settings = {
1019
+ 'base_font_size': float(request.form.get('base_font_size', 12)),
1020
+ 'code_font_size': float(request.form.get('code_font_size', 11)),
1021
+ 'page_size': request.form.get('page_size', 'A4'),
1022
+ 'page_margin': float(request.form.get('page_margin', 1.5)),
1023
+ 'paragraph_spacing': int(request.form.get('paragraph_spacing', 8)),
1024
+ 'code_padding_vertical': int(request.form.get('code_padding_vertical', 15)),
1025
+ 'code_padding_horizontal': int(request.form.get('code_padding_horizontal', 12)),
1026
+ 'code_margin_top': int(request.form.get('code_margin_top', 15)),
1027
+ 'code_margin_bottom': int(request.form.get('code_margin_bottom', 15)),
1028
+ 'code_bg_color': request.form.get('code_bg_color', '#f5f5f5'),
1029
+ 'keyword_color': request.form.get('keyword_color', '#00BFFF'),
1030
+ 'string_color': request.form.get('string_color', '#ff8c00'),
1031
+ 'comment_color': request.form.get('comment_color', '#006400'),
1032
+ 'number_color': request.form.get('number_color', '#FF00FF'),
1033
+ 'function_color': request.form.get('function_color', '#795e26'),
1034
+ 'enable_wrap': request.form.get('enable_wrap') == 'true',
1035
+ }
1036
+
1037
+ css = generate_css(settings)
1038
+ content_html = process_markdown(markdown_text, settings['enable_wrap'])
1039
+ full_html = render_template_string(HTML_TEMPLATE, css=css, content=content_html)
1040
+
1041
+ pdf_file = BytesIO()
1042
+ pisa_status = pisa.CreatePDF(
1043
+ full_html.encode('utf-8'),
1044
+ dest=pdf_file,
1045
+ encoding='utf-8',
1046
+ path=''
1047
+ )
1048
+
1049
+ if pisa_status.err:
1050
+ return "Error generating PDF", 500
1051
+
1052
+ pdf_file.seek(0)
1053
+
1054
+ return send_file(
1055
+ pdf_file,
1056
+ mimetype='application/pdf',
1057
+ as_attachment=True,
1058
+ download_name=pdf_filename
1059
+ )
1060
+
1061
+
1062
+ def main():
1063
+ app.run(debug=True, port=5001)
1064
+
1065
+
1066
+ if __name__ == '__main__':
1067
+ main()