#!/usr/bin/env python3 """Builds a Dify guide (.html, .pdf, .docx) from a markdown source. Run from a guide folder: python3 build.py (build.py in that folder calls this script with the name of the .md) Requires: python3-markdown, google-chrome, libreoffice. The LeX Consultancy logo sits next to this script and goes on the first page (top right) and in the footer, in PDF, HTML and Word. """ import base64 import re import subprocess import sys from pathlib import Path import markdown TOOLS = Path(__file__).resolve().parent LOGO = TOOLS / "logo-lex.png" # Licence and contact, printed as a colophon at the end of every document (HTML, PDF and Word). EIGENAAR = "Guido van Dijk, LeX Consultancy B.V." CONTACT = "guido@l-e-x.nl" BRON = "https://git.lexconsultancy.nl/guido/dify-guides" LICENTIE_URL = "https://creativecommons.org/licenses/by-sa/4.0/" 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; } 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; } """ def read_source(src: Path) -> str: text = src.read_text(encoding="utf-8") # The editor block at the top is not part of the published document. text = re.sub(r"^\s*\s*", "", text, count=1, flags=re.S) # python-markdown merges two consecutive boxes (only a blank line between them); # an empty comment keeps them apart, so a green box never ends up inside an orange one. 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' |
.*?
)", lambda m: (f'Licence: CC BY-SA 4.0 ({LICENTIE_URL}). You may copy, adapt and reuse this document, ' f'including at your own school, provided you credit the source ({EIGENAAR}, {BRON}) and share your adaptation ' f'under the same licence. The LeX Consultancy logo is excluded; product names are trademarks of their owners. ' f'Schools, people and documents in the examples are fictional.
' f'Owner and contact: {EIGENAAR} · {CONTACT}
' f'CC BY-SA 4.0 · {EIGENAAR} · {CONTACT} · {BRON} · ' f'LeX Consultancy logo excluded
| ').replace('', ' |