from flask import Flask, render_template_string, request, send_file import markdown from io import BytesIO import re from xhtml2pdf import pisa app = Flask(__name__) def extract_first_header(md_text): """Extract the first header from markdown text for use as filename""" # Match headers (# Header, ## Header, etc.) header_pattern = r'^#{1,6}\s+(.+?)$' match = re.search(header_pattern, md_text, re.MULTILINE) if match: header_text = match.group(1).strip() # Remove any markdown formatting from header (bold, italic, links, etc.) header_text = re.sub(r'\*\*(.+?)\*\*', r'\1', header_text) # Bold header_text = re.sub(r'\*(.+?)\*', r'\1', header_text) # Italic header_text = re.sub(r'__(.+?)__', r'\1', header_text) # Bold header_text = re.sub(r'_(.+?)_', r'\1', header_text) # Italic header_text = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', header_text) # Links header_text = re.sub(r'`(.+?)`', r'\1', header_text) # Inline code # Clean up for filename (remove invalid characters) filename = re.sub(r'[<>:"/\\|?*]', '', header_text) filename = filename.strip() # Limit filename length if len(filename) > 100: filename = filename[:100].rsplit(' ', 1)[0] return filename if filename else 'document' return 'document' def generate_css(settings): """Generate CSS based on user settings""" base_size = settings['base_font_size'] h1_size = base_size * 2 h2_size = base_size * 1.4 h3_size = base_size * 1.2 page_size = settings['page_size'] wrap_behavior = 'pre-wrap' if settings['enable_wrap'] else 'pre' return f''' @page {{ size: {page_size}; margin: {settings['page_margin']}cm; }} * {{ margin: 0; padding: 0; box-sizing: border-box; }} body {{ font-family: 'DejaVu Sans', Georgia, serif; line-height: 1.5; color: #1a1a1a; font-size: {settings['base_font_size']}pt; -pdf-encoding: utf-8; }} h1 {{ color: #2c3e50; font-size: {h1_size}pt; margin-bottom: 15px; margin-top: 0; padding-bottom: 8px; border-bottom: 2px solid #3498db; page-break-after: avoid; }} h2 {{ color: #34495e; font-size: {h2_size}pt; margin-top: 20px; margin-bottom: 10px; padding-top: 5px; border-top: 1px solid #ecf0f1; page-break-after: avoid; }} h3 {{ color: #555; font-size: {h3_size}pt; margin-top: 15px; margin-bottom: 8px; page-break-after: avoid; }} h4 {{ color: #666; font-size: {base_size}pt; margin-top: 12px; margin-bottom: 6px; }} p {{ margin-bottom: {settings['paragraph_spacing']}px; text-align: justify; }} strong {{ color: #2c3e50; font-weight: 600; }} code {{ font-family: 'DejaVu Sans Mono', 'DejaVu Sans', 'Consolas', 'Monaco', 'Courier New', monospace; font-size: {settings['base_font_size']}pt; color: #000000; }} pre {{ background-color: {settings['code_bg_color']} !important; padding: {settings['code_padding_vertical']}px {settings['code_padding_horizontal']}px !important; margin: {settings['code_margin_top']}px 0 {settings['code_margin_bottom']}px 0 !important; border-radius: 3px; overflow: visible; word-wrap: break-word; }} pre code {{ display: none; }} .code-line {{ font-family: 'DejaVu Sans Mono', 'DejaVu Sans', 'Consolas', 'Monaco', 'Courier New', monospace; font-size: {settings['code_font_size']}pt; line-height: 1.4; margin: 0; padding: 0; border: none; background: transparent !important; display: block; white-space: pre-wrap; word-break: break-word; }} .code-block {{ background-color: {settings['code_bg_color']} !important; overflow: visible; white-space: normal; word-wrap: break-word; }} .sql-keyword {{ color: {settings['keyword_color']}; font-weight: bold; }} .sql-comment {{ color: {settings['comment_color']}; font-style: italic; }} .sql-string {{ color: {settings['string_color']}; }} .sql-number {{ color: {settings['number_color']}; }} .sql-function {{ color: {settings['function_color']}; font-weight: bold; }} .py-keyword {{ color: {settings['keyword_color']}; font-weight: bold; }} .py-string {{ color: {settings['string_color']}; }} .py-number {{ color: {settings['number_color']}; }} .py-comment {{ color: {settings['comment_color']}; font-style: italic; }} .py-function {{ color: {settings['function_color']}; }} .py-builtin {{ color: {settings['keyword_color']}; }} .py-decorator {{ color: #808080; }} table {{ width: 100%; border-collapse: collapse; margin: 10px 0; font-size: {settings['base_font_size'] - 1}pt; page-break-inside: avoid; }} table th {{ background-color: #3498db; color: white; padding: 5px 8px; text-align: left; font-weight: 600; border: 1px solid #2980b9; line-height: 1.2; }} table td {{ border: 1px solid #ddd; padding: 4px 8px; vertical-align: top; line-height: 1.2; }} table tr:nth-child(even) {{ background-color: #f9f9f9; }} ul, ol {{ margin-left: 25px; margin-bottom: 10px; }} li {{ margin-bottom: 4px; line-height: 1.4; }} blockquote {{ border-left: 3px solid #3498db; padding: 8px 12px; margin: 10px 0; color: #555; font-style: italic; background-color: #f8f9fa; }} hr {{ border: none; border-top: 1px solid #ecf0f1; margin: 15px 0; }} .warning {{ background-color: #fff3cd; border-left: 3px solid #ffc107; padding: 8px 12px; margin: 10px 0; border-radius: 3px; }} .success {{ background-color: #d4edda; border-left: 3px solid #28a745; padding: 8px 12px; margin: 10px 0; border-radius: 3px; }} .error {{ background-color: #f8d7da; border-left: 3px solid #dc3545; padding: 8px 12px; margin: 10px 0; border-radius: 3px; }} .info {{ background-color: #d1ecf1; border-left: 3px solid #0c5460; padding: 8px 12px; margin: 10px 0; border-radius: 3px; }} ''' HTML_TEMPLATE = '''
{html_lines}'
result = re.sub(r'```([a-zA-Z0-9]*)\n(.*?)\n```', replace_code_block, md_text, flags=re.DOTALL)
result = re.sub(r'```([a-zA-Z0-9]*)\n(.*?)```', replace_code_block, result, flags=re.DOTALL)
result = re.sub(r'```\s*([a-zA-Z0-9]*)\s*\n(.*?)```', replace_code_block, result, flags=re.DOTALL)
return result
def process_markdown(md_text, enable_wrap=True):
"""Convert markdown to HTML with syntax highlighting"""
md_with_highlighted_code = process_code_blocks(md_text, enable_wrap)
html = markdown.markdown(md_with_highlighted_code, extensions=['tables', 'fenced_code'])
html = re.sub(r'style="[^"]*"', '', html)
html = re.sub(r'⚠️[^<]*', r'
✓[^<]*', r'
✗[^<]*', r'
💡[^<]*', r'
PDF filename will be based on your first header