import streamlit as st import markdown from weasyprint import HTML, CSS from datetime import datetime import base64 import os import re import zlib import requests from io import BytesIO from PIL import Image # Page config st.set_page_config(page_title="Markdown to PDF", layout="wide", page_icon="📄") # Font options FONTS = { "Open Sans": "Open+Sans:wght@400;600;700", "Montserrat": "Montserrat:wght@400;600;700", "DM Mono": "DM+Mono:wght@400;500", "Anonymous Pro": "Anonymous+Pro:wght@400;700", "Inconsolata": "Inconsolata:wght@400;700" } MONOSPACE_FONTS = ["DM Mono", "Anonymous Pro", "Inconsolata"] def get_css_template(font_name, spacing="normal", font_size=11): """Generate CSS template with selected font and spacing""" is_mono = font_name in MONOSPACE_FONTS code_font = font_name if is_mono else "DM Mono" google_fonts_url = f"https://fonts.googleapis.com/css2?family={FONTS[font_name]}" if not is_mono: google_fonts_url += f"&family={FONTS[code_font]}" # Custom line-height per font type if font_name in MONOSPACE_FONTS: line_height = 1.6 if spacing == "spacious" else 1.4 else: line_height = 1.8 if spacing == "spacious" else 1.6 margin_multiplier = 1.2 if spacing == "spacious" else 1.0 return f""" @import url('{google_fonts_url}'); @page {{ size: A4; margin: {2.5 * margin_multiplier}cm {2 * margin_multiplier}cm; @top-left {{ content: element(header-left); vertical-align: middle; }} @top-right {{ content: element(header-right); vertical-align: middle; text-align: right; }} @bottom-center {{ content: "Page " counter(page) " of " counter(pages); font-family: '{font_name}', sans-serif; font-size: 9pt; color: #666; }} @bottom-right {{ content: "{datetime.now().strftime('%B %d, %Y')}"; font-family: '{font_name}', sans-serif; font-size: 9pt; color: #666; }} }} /* Add extra space after header on pages 2+ */ @page :not(:first) {{ margin-top: {3.5 * margin_multiplier}cm; }} @page :first {{ margin-top: {2.5 * margin_multiplier}cm; }} .header-left {{ position: running(header-left); }} .header-right {{ position: running(header-right); }} .logo-container {{ max-width: 120px; max-height: 40px; display: inline-block; }} .logo-container img {{ max-width: 120px; max-height: 40px; width: auto; height: auto; display: block; }} .title-header {{ font-family: '{font_name}', sans-serif; font-size: 14pt; font-weight: 600; color: #2c3e50; margin: 0; padding: 0; }} body {{ font-family: '{font_name}', sans-serif; font-size: {font_size}pt; line-height: {line_height}; color: #333; margin: 0; padding: 0; }} h1, h2, h3, h4, h5, h6 {{ font-family: '{font_name}', sans-serif; color: #2c3e50; margin-top: {1.5 * margin_multiplier}em; margin-bottom: {0.5 * margin_multiplier}em; page-break-after: avoid; font-weight: 600; }} h1 {{ font-size: {font_size * 2}pt; border-bottom: 2px solid #e0e0e0; padding-bottom: 0.3em; }} h2 {{ font-size: {font_size * 1.6}pt; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.2em; }} h3 {{ font-size: {font_size * 1.3}pt; }} h4 {{ font-size: {font_size * 1.1}pt; }} h5 {{ font-size: {font_size}pt; }} h6 {{ font-size: {font_size * 0.9}pt; color: #666; }} p {{ margin: {0.8 * margin_multiplier}em 0; }} /* List item atomic page breaking */ ul, ol {{ margin: {1 * margin_multiplier}em 0; padding-left: 2em; }} li {{ margin: {0.5 * margin_multiplier}em 0; page-break-inside: avoid; break-inside: avoid; }} /* Prevent orphaned list items */ ul, ol {{ orphans: 3; widows: 3; }} blockquote {{ border-left: 4px solid #3498db; margin: {1.2 * margin_multiplier}em 0; padding: {0.5 * margin_multiplier}em 0 {0.5 * margin_multiplier}em 1em; background: #f8f9fa; font-style: italic; color: #555; page-break-inside: avoid; }} code {{ font-family: '{code_font}', monospace; font-size: {font_size * 0.9}pt; background: #f4f4f4; padding: 0.1em 0.3em; border-radius: 3px; color: #c7254e; }} pre {{ font-family: '{code_font}', monospace; font-size: {font_size * 0.85}pt; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px; padding: {1 * margin_multiplier}em; overflow-x: auto; line-height: 1.4; page-break-inside: avoid; margin: {1 * margin_multiplier}em 0; }} pre code {{ background: none; padding: 0; color: #333; }} table {{ border-collapse: collapse; width: 100%; margin: {1.2 * margin_multiplier}em 0; page-break-inside: avoid; }} th, td {{ border: 1px solid #ddd; padding: {0.6 * margin_multiplier}em {0.8 * margin_multiplier}em; text-align: left; }} th {{ background: #f5f5f5; font-weight: 600; color: #2c3e50; }} tr:nth-child(even) {{ background: #fafafa; }} img {{ max-width: 100%; height: auto; display: block; margin: {1.2 * margin_multiplier}em 0; page-break-inside: avoid; }} hr {{ border: none; border-top: 1px solid #ddd; margin: {2 * margin_multiplier}em 0; }} a {{ color: #3498db; text-decoration: none; }} a:hover {{ text-decoration: underline; }} /* Mermaid diagram container - fits within one page */ .mermaid-container {{ max-height: 600px; width: 100%; page-break-inside: avoid; break-inside: avoid; margin: {1.5 * margin_multiplier}em 0; text-align: center; background: #fafafa; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1em; box-sizing: border-box; }} .mermaid-container img {{ max-width: 100%; max-height: 550px; width: auto; height: auto; object-fit: contain; display: block; margin: 0 auto; }} .mermaid-error {{ padding: 1em; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; color: #856404; margin: 1em 0; }} /* Syntax highlighting for code blocks - Complete Pygments style */ .codehilite .hll {{ background-color: #ffffcc }} .codehilite .c {{ color: #008000; font-style: italic }} /* Comment */ .codehilite .err {{ border: 1px solid #FF0000 }} /* Error */ .codehilite .k {{ color: #0000ff; font-weight: bold }} /* Keyword */ .codehilite .o {{ color: #666666 }} /* Operator */ .codehilite .ch {{ color: #008000; font-style: italic }} /* Comment.Hashbang */ .codehilite .cm {{ color: #008000; font-style: italic }} /* Comment.Multiline */ .codehilite .cp {{ color: #0000ff }} /* Comment.Preproc */ .codehilite .cpf {{ color: #008000; font-style: italic }} /* Comment.PreprocFile */ .codehilite .c1 {{ color: #008000; font-style: italic }} /* Comment.Single */ .codehilite .cs {{ color: #008000; font-style: italic }} /* Comment.Special */ .codehilite .gd {{ color: #A00000 }} /* Generic.Deleted */ .codehilite .ge {{ font-style: italic }} /* Generic.Emph */ .codehilite .gr {{ color: #FF0000 }} /* Generic.Error */ .codehilite .gh {{ color: #000080; font-weight: bold }} /* Generic.Heading */ .codehilite .gi {{ color: #00A000 }} /* Generic.Inserted */ .codehilite .go {{ color: #888888 }} /* Generic.Output */ .codehilite .gp {{ color: #000080; font-weight: bold }} /* Generic.Prompt */ .codehilite .gs {{ font-weight: bold }} /* Generic.Strong */ .codehilite .gu {{ color: #800080; font-weight: bold }} /* Generic.Subheading */ .codehilite .gt {{ color: #0044DD }} /* Generic.Traceback */ .codehilite .kc {{ color: #0000ff; font-weight: bold }} /* Keyword.Constant */ .codehilite .kd {{ color: #0000ff; font-weight: bold }} /* Keyword.Declaration */ .codehilite .kn {{ color: #0000ff; font-weight: bold }} /* Keyword.Namespace */ .codehilite .kp {{ color: #0000ff }} /* Keyword.Pseudo */ .codehilite .kr {{ color: #0000ff; font-weight: bold }} /* Keyword.Reserved */ .codehilite .kt {{ color: #2b91af }} /* Keyword.Type */ .codehilite .m {{ color: #009999 }} /* Literal.Number */ .codehilite .s {{ color: #a31515 }} /* Literal.String */ .codehilite .na {{ color: #FF0000 }} /* Name.Attribute */ .codehilite .nb {{ color: #0086B3 }} /* Name.Builtin */ .codehilite .nc {{ color: #2b91af; font-weight: bold }} /* Name.Class */ .codehilite .no {{ color: #008080 }} /* Name.Constant */ .codehilite .nd {{ color: #AA22FF }} /* Name.Decorator */ .codehilite .ni {{ color: #999999; font-weight: bold }} /* Name.Entity */ .codehilite .ne {{ color: #D2413A; font-weight: bold }} /* Name.Exception */ .codehilite .nf {{ color: #000000; font-weight: bold }} /* Name.Function */ .codehilite .nl {{ color: #A0A000 }} /* Name.Label */ .codehilite .nn {{ color: #0000FF; font-weight: bold }} /* Name.Namespace */ .codehilite .nt {{ color: #0000ff }} /* Name.Tag */ .codehilite .nv {{ color: #008080 }} /* Name.Variable */ .codehilite .ow {{ color: #0000ff; font-weight: bold }} /* Operator.Word */ .codehilite .w {{ color: #bbbbbb }} /* Text.Whitespace */ .codehilite .mb {{ color: #009999 }} /* Literal.Number.Bin */ .codehilite .mf {{ color: #009999 }} /* Literal.Number.Float */ .codehilite .mh {{ color: #009999 }} /* Literal.Number.Hex */ .codehilite .mi {{ color: #009999 }} /* Literal.Number.Integer */ .codehilite .mo {{ color: #009999 }} /* Literal.Number.Oct */ .codehilite .sa {{ color: #a31515 }} /* Literal.String.Affix */ .codehilite .sb {{ color: #a31515 }} /* Literal.String.Backtick */ .codehilite .sc {{ color: #a31515 }} /* Literal.String.Char */ .codehilite .dl {{ color: #a31515 }} /* Literal.String.Delimiter */ .codehilite .sd {{ color: #a31515; font-style: italic }} /* Literal.String.Doc */ .codehilite .s2 {{ color: #a31515 }} /* Literal.String.Double */ .codehilite .se {{ color: #a31515; font-weight: bold }} /* Literal.String.Escape */ .codehilite .sh {{ color: #a31515 }} /* Literal.String.Heredoc */ .codehilite .si {{ color: #a31515 }} /* Literal.String.Interpol */ .codehilite .sx {{ color: #a31515 }} /* Literal.String.Other */ .codehilite .sr {{ color: #a31515 }} /* Literal.String.Regex */ .codehilite .s1 {{ color: #a31515 }} /* Literal.String.Single */ .codehilite .ss {{ color: #a31515 }} /* Literal.String.Symbol */ .codehilite .bp {{ color: #0086B3 }} /* Name.Builtin.Pseudo */ .codehilite .fm {{ color: #000000; font-weight: bold }} /* Name.Function.Magic */ .codehilite .vc {{ color: #008080 }} /* Name.Variable.Class */ .codehilite .vg {{ color: #008080 }} /* Name.Variable.Global */ .codehilite .vi {{ color: #008080 }} /* Name.Variable.Instance */ .codehilite .vm {{ color: #008080 }} /* Name.Variable.Magic */ .codehilite .il {{ color: #009999 }} /* Literal.Number.Integer.Long */ """ def generate_html(markdown_text, title, font_name, spacing="normal", font_size=11, logo_data=None): """Convert Markdown to styled HTML""" # Process Mermaid blocks first (renders diagrams and keeps code blocks) processed_text = process_mermaid_blocks(markdown_text) # Convert Markdown to HTML with extensions md = markdown.Markdown( extensions=[ 'extra', 'codehilite', 'tables', 'fenced_code', 'nl2br', 'sane_lists' ], extension_configs={ 'codehilite': { 'linenums': False, 'guess_lang': True, 'css_class': 'codehilite', 'use_pygments': True } } ) body_html = md.convert(processed_text) # Handle logo logo_html = "" if logo_data: logo_html = f'