#!/usr/bin/env python3 """Bouwt een Dify-handleiding (.html, .pdf, .docx) uit een markdown-bron. Gebruik vanuit een handleidingmap: python3 build.py (build.py in die map roept dit script aan met de naam van de .md) Vereist: python3-markdown, google-chrome, libreoffice (lokaal aanwezig). Het logo van AI Netwerk Limburg staat naast dit script en komt op de eerste pagina rechtsboven, in PDF, HTML en Word. """ import base64 import re import subprocess import sys from pathlib import Path import markdown TOOLS = Path(__file__).resolve().parent LOGO = TOOLS / "logo-ai-netwerk-limburg.png" CSS = """ @page { size: A4; margin: 2cm 2cm 2.2cm 2cm; } html { font-size: 11pt; } body { font-family: "Noto Sans", "DejaVu Sans", Arial, sans-serif; color: #1a1a1a; line-height: 1.45; max-width: 17cm; margin: 0 auto; } .kop { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5em; margin-bottom: 1em; } .kop-tekst { flex: 1 1 auto; } .kop img { flex: 0 0 auto; width: 3.4cm; height: auto; } h1 { font-size: 22pt; margin: 0 0 .3em; line-height: 1.2; } h2 { font-size: 15pt; margin: 1.6em 0 .5em; padding-top: .3em; border-top: 2px solid #1d4ed8; page-break-after: avoid; break-after: avoid; } h3 { font-size: 12pt; margin: 1.2em 0 .4em; page-break-after: avoid; break-after: avoid; } p { margin: .5em 0; } hr { border: 0; border-top: 1px solid #ddd; margin: 1.5em 0; } strong { font-weight: 600; } code { font-family: "DejaVu Sans Mono", Consolas, monospace; font-size: .92em; background: #f1f5f9; padding: 0 .25em; border-radius: 3px; } blockquote { margin: .8em 0; padding: .5em .9em; background: #fff7ed; border-left: 4px solid #f59e0b; page-break-inside: avoid; break-inside: avoid; } blockquote p { margin: .2em 0; } blockquote.kies { background: #ecfdf5; border-left-color: #10b981; } table { border-collapse: collapse; width: 100%; margin: .8em 0; font-size: .95em; page-break-inside: avoid; break-inside: avoid; } th, td { border: 1px solid #cbd5e1; padding: .35em .6em; text-align: left; vertical-align: top; } th { background: #eff6ff; } figure { margin: .9em 0 1.1em; page-break-inside: avoid; break-inside: avoid; } figure img { width: 100%; height: auto; border: 1px solid #cbd5e1; display: block; } figcaption { font-size: .88em; color: #555; margin-top: .3em; } ul, ol { margin: .4em 0 .4em 1.4em; padding: 0; } li { margin: .15em 0; } ol li { margin: .25em 0; } .step { page-break-inside: avoid; break-inside: avoid; } .typ { margin: .6em 0 .9em; border: 1px solid #94a3b8; border-radius: 4px; background: #f8fafc; page-break-inside: avoid; break-inside: avoid; } .typ-label { font-size: .75em; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #334155; background: #e2e8f0; padding: .15em .7em; } .schema { margin: .6em 0 .9em; padding: .5em .7em; border: 1px dashed #94a3b8; border-radius: 4px; background: #fff; overflow-x: auto; page-break-inside: avoid; break-inside: avoid; } .schema pre { margin: 0; white-space: pre; font-family: "DejaVu Sans Mono", Consolas, monospace; font-size: .8em; line-height: 1.3; } .typ pre { margin: 0; padding: .5em .7em; white-space: pre-wrap; word-wrap: break-word; font-family: "DejaVu Sans Mono", Consolas, monospace; font-size: .9em; } .voet { margin-top: 3em; padding-top: .6em; border-top: 1px solid #ddd; font-size: .85em; color: #555; display: flex; align-items: center; gap: .8em; } .voet img { width: 1.6cm; height: auto; } """ def read_source(src: Path) -> str: text = src.read_text(encoding="utf-8") # Het instructieblok voor Claude Code bovenin hoort niet in de uitgave. return re.sub(r"^\s*\s*", "", text, count=1, flags=re.S) def logo_src(embed: bool) -> str: if embed: return "data:image/png;base64," + base64.b64encode(LOGO.read_bytes()).decode() return LOGO.as_uri() COMPACT_CSS = """ html { font-size: 9.5pt; } h1 { font-size: 18pt; } h2 { margin-top: 1em; } p { margin: .35em 0; } table { margin: .5em 0; } th, td { padding: .25em .5em; } .voet { margin-top: .6em; } .voet img { width: 1.1cm; } .kop img { width: 2.8cm; } ul, ol { margin: .3em 0 .3em 1.3em; } """ def to_html(md_text: str, here: Path, embed_images: bool, compact: bool = False) -> str: body = markdown.markdown(md_text, extensions=["tables", "sane_lists", "smarty", "fenced_code"]) title = re.search(r"
(.*?)',
r'| \1 |
(.*?)',
r'\1
(.*?)",
r'| INTYPEN \1 |
| \1 |
(.*?)", r'
| \1 |
(?:(?!
).)*.*?
)", lambda m: (f'| {m.group(1)} | ' f''
f' |
.*?
)", lambda m: (f'