#!/usr/bin/env python3 """Bouwt een 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" LOGO_LEX = TOOLS / "logo-lex.png" # kleiner, naast het LAI-logo # Licentie en contact, komen als colofon onderaan elk document (HTML, PDF en Word). EIGENAAR = "Guido van Dijk, LeX Consultancy B.V. en AI Netwerk Limburg" CONTACT = "guido@l-e-x.nl · guido@ai-netwerk-limburg.nl" BRON = "https://git.lexconsultancy.nl/guido/gemini-handleidingen" LICENTIE_URL = "https://creativecommons.org/licenses/by-sa/4.0/deed.nl" 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 .logos { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: .3em; } .kop img { width: 3.4cm; height: auto; } .kop img.lex { width: 2cm; } 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; } .colofon { margin-top: 2.2em; padding: .7em .9em; border: 1px solid #cbd5e1; border-radius: 4px; background: #f8fafc; font-size: .88em; color: #334155; page-break-inside: avoid; break-inside: avoid; } .colofon p { margin: .2em 0; } .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; } .voet img.lex { width: 1.1cm; } """ def read_source(src: Path) -> str: text = src.read_text(encoding="utf-8") # Het instructieblok voor de bewerker bovenin hoort niet in de uitgave. text = re.sub(r"^\s*\s*", "", text, count=1, flags=re.S) # Twee kaders na elkaar (alleen een witregel ertussen) plakt python-markdown aan elkaar; # een leeg commentaar ertussen houdt ze apart, zodat een groen kader niet in een oranje valt. return re.sub(r"(\n> [^\n]*\n)\n(?=> )", r"\1\n\n\n", text) def logo_src(embed: bool, pad: Path = LOGO) -> str: if embed: return "data:image/png;base64," + base64.b64encode(pad.read_bytes()).decode() return pad.as_uri() COMPACT_CSS = """ html { font-size: 9.2pt; } h1 { font-size: 17pt; } h2 { margin-top: 1em; } @page { margin: 1.5cm 2cm 1.5cm 2cm; } p { margin: .35em 0; } table { margin: .5em 0; } th, td { padding: .25em .5em; } .colofon { margin-top: .5em; padding: .3em .5em; font-size: .75em; border: 0; border-top: 1px solid #ddd; border-radius: 0; background: none; } .voet { margin-top: .6em; } .voet img { width: 1.1cm; } .voet img.lex { width: .8cm; } .kop img { width: 2.8cm; } .kop img.lex { width: 1.6cm; } 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' ' f' |
.*?
)", lambda m: (f'Licentie: CC BY-SA 4.0 ({LICENTIE_URL}). Je mag dit document kopiëren, bewerken en ' f'hergebruiken, ook op je eigen school, als je de bron noemt ({EIGENAAR}, {BRON}) en je bewerking onder ' f'dezelfde licentie deelt. Het logo van AI Netwerk Limburg valt buiten de licentie; namen van producten ' f'zijn merken van hun eigenaren. Scholen, personen en documenten in de voorbeelden zijn verzonnen.
' f'Eigenaar en contact: {EIGENAAR} · {CONTACT}
' f'CC BY-SA 4.0 · {EIGENAAR} · {CONTACT} · {BRON} · ' f'logo AI Netwerk Limburg uitgezonderd
| ').replace('', ' |