From a0e533a856fb70ae76961cb3ed8119c3a3890cab Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 26 Jun 2026 09:46:39 +0200 Subject: [PATCH] feat(okr): /okr:export PDF via weasyprint + scripts/export-pdf.py (SC5) --- commands/export.md | 84 ++++++++++++++++++++++++++++++++ scripts/export-pdf.py | 108 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 commands/export.md create mode 100644 scripts/export-pdf.py diff --git a/commands/export.md b/commands/export.md new file mode 100644 index 0000000..30adde4 --- /dev/null +++ b/commands/export.md @@ -0,0 +1,84 @@ +--- +name: okr:export +description: Eksporter OKR-dokumenter (kvalitetsvurdering, gap-matrise, statusrapport, retrospektiv) til print-klar PDF +allowed-tools: Read, Bash +argument-hint: "[dokumenttype eller filsti]" +--- + +# OKR Export - Eksporter OKR-leveranser til PDF + +Generer en print-klar A4-PDF av et OKR-dokument for deling med ledelse, +Riksrevisjon eller styringsdialog. Konverterer Markdown til PDF via +`scripts/export-pdf.py` (weasyprint), med tabell-striping og fargekodede +score-celler (`.score-green` / `.score-yellow` / `.score-red`). + +## Kontekstbevissthet + +OKR-kontekst injiseres automatisk via hook. Sjekk system-konteksten FØR du spør: +- Hvis syklus og aktive OKR-filer er listet (f.eks. `.claude/okr/syklus/T1-2026/`): + tilby å eksportere dem direkte. +- Hvis `.claude/okr/dokumenter/` inneholder genererte rapporter: tilby disse. +- Spør kun om dokumenttype når konteksten ikke gjør valget åpenbart. + +## Arbeidsflyt + +1. **Velg dokumenttype** — hvilken leveranse skal eksporteres: + - **kvalitetsvurdering** — output fra `/okr:kvalitet` + - **gap-matrise** — output fra `/okr:gap` + - **statusrapport** — output fra `/okr:sporing` + - **retrospektiv** — `retrospektiv.md` fra `/okr:oppsett arkiver` + - eller en vilkårlig Markdown-fil brukeren oppgir. + +2. **Sørg for Markdown-kilde** — hvis dokumentet finnes som fil, bruk filstien. + Hvis det kun finnes i samtalen, skriv det til en `.md`-fil først (f.eks. + `.claude/okr/dokumenter/kvalitetsvurdering-T1-2026.md`). + +3. **Kjør eksporten** via Bash: + ```bash + python3 ${CLAUDE_PLUGIN_ROOT}/scripts/export-pdf.py + ``` + Eksempel: + ```bash + python3 ${CLAUDE_PLUGIN_ROOT}/scripts/export-pdf.py \ + .claude/okr/dokumenter/kvalitetsvurdering-T1-2026.md \ + .claude/okr/dokumenter/kvalitetsvurdering-T1-2026.pdf + ``` + +4. **Bekreft resultatet** — skriptet skriver `Skrev PDF: ` til stderr ved + suksess. Rapporter filstien til brukeren. + +**Tips for fargekoding:** I Markdown-tabellen kan score-celler tagges med +`attr_list`-syntaks slik at de blir fargelagt i PDF-en: + +```markdown +| KR | Score | +|----|-------| +| KR1 | 0.9 {: .score-green} | +| KR2 | 0.5 {: .score-yellow} | +| KR3 | 0.2 {: .score-red} | +``` + +## Feilhåndtering + +Eksporten har to dokumenterte forutsetninger (ikke bundlet med pluginet): + +- **Manglende Python-pakker** (`markdown`, `weasyprint`) → skriptet avslutter med + exit 1 og hintet: + ``` + pip install markdown weasyprint + ``` +- **Manglende native bibliotek** (Pango/cairo/gdk-pixbuf) → feiler ved rendring + med exit 1 og hintet: + ``` + brew install pango + ``` + +Begge feil gir en tydelig instruks (ikke en traceback). Når en forutsetning +mangler: rapporter instruksen til brukeren, ikke prøv å installere noe selv. + +## Referanser + +- `${CLAUDE_PLUGIN_ROOT}/scripts/export-pdf.py` — selve eksport-skriptet +- `${CLAUDE_PLUGIN_ROOT}/commands/kvalitet.md` — produserer kvalitetsvurdering +- `${CLAUDE_PLUGIN_ROOT}/commands/gap.md` — produserer gap-matrise +- weasyprint: https://doc.courtbouillon.org/weasyprint/stable/ diff --git a/scripts/export-pdf.py b/scripts/export-pdf.py new file mode 100644 index 0000000..42efc7a --- /dev/null +++ b/scripts/export-pdf.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Render an OKR Markdown document to a print-ready A4 PDF. + +Usage: + python3 export-pdf.py + +Dependencies (documented prerequisite, not bundled): + pip install markdown weasyprint + # native libs on macOS: brew install pango + +The script degrades gracefully: a missing Python package exits 1 with a pip +hint; a missing native library (Pango/cairo/gdk-pixbuf) surfaces at render +time as an OSError and exits 1 with a brew hint. Neither path leaks a traceback. +""" + +import sys + +PIP_HINT = ( + "Mangler Python-avhengigheter for PDF-eksport.\n" + "Installer dem (dokumentert forutsetning, ikke bundlet):\n" + " pip install markdown weasyprint\n" +) + +PANGO_HINT = ( + "Klarte ikke aa rendre PDF: native bibliotek mangler " + "(Pango/cairo/gdk-pixbuf).\n" + "Installer dem (macOS):\n" + " brew install pango\n" + "Se: https://doc.courtbouillon.org/weasyprint/stable/first_steps.html\n" +) + +try: + import markdown + import weasyprint +except ImportError: + sys.stderr.write(PIP_HINT) + sys.exit(1) + +# A4 page geometry + table striping + traffic-light score classes. +# attr_list lets the Markdown author tag cells: `{: .score-green}` etc. +PAGE_CSS = """ +@page { + size: A4; + margin: 20mm 18mm; + @bottom-right { content: counter(page) " / " counter(pages); font-size: 9pt; color: #666; } +} +body { + font-family: "Helvetica Neue", Arial, sans-serif; + font-size: 10.5pt; + line-height: 1.45; + color: #1a1a1a; +} +h1 { font-size: 20pt; border-bottom: 2px solid #2a4d69; padding-bottom: 4px; } +h2 { font-size: 15pt; color: #2a4d69; margin-top: 1.4em; } +h3 { font-size: 12.5pt; color: #4a6d89; } +table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 9.5pt; } +th, td { border: 1px solid #ccc; padding: 5px 8px; text-align: left; vertical-align: top; } +th { background: #2a4d69; color: #fff; font-weight: 600; } +tbody tr:nth-child(even) { background: #f3f6f9; } +code { font-family: "SF Mono", Menlo, monospace; font-size: 9pt; background: #f0f0f0; padding: 1px 4px; border-radius: 3px; } +pre { background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; padding: 10px; overflow-x: auto; } +pre code { background: none; padding: 0; } +.score-green { background: #d6efd6 !important; color: #1a5c1a; font-weight: 600; } +.score-yellow { background: #fbf3cf !important; color: #7a5c00; font-weight: 600; } +.score-red { background: #f6d6d6 !important; color: #8c1a1a; font-weight: 600; } +""" + + +def build_html(markdown_text): + """Convert Markdown to a full standalone HTML document with embedded CSS.""" + body = markdown.markdown( + markdown_text, + extensions=["tables", "fenced_code", "codehilite", "toc", "attr_list"], + ) + return ( + "" + "" + body + "" + ) + + +def main(): + if len(sys.argv) != 3: + sys.stderr.write("Bruk: python3 export-pdf.py \n") + sys.exit(2) + + in_path, out_path = sys.argv[1], sys.argv[2] + + try: + with open(in_path, "r", encoding="utf-8") as handle: + markdown_text = handle.read() + except OSError as err: + sys.stderr.write("Kunne ikke lese inputfil '%s': %s\n" % (in_path, err)) + sys.exit(1) + + html_doc = build_html(markdown_text) + + try: + # base_url anchors relative asset paths to the input file's directory. + weasyprint.HTML(string=html_doc, base_url=in_path).write_pdf(out_path) + except OSError: + sys.stderr.write(PANGO_HINT) + sys.exit(1) + + sys.stderr.write("Skrev PDF: %s\n" % out_path) + + +if __name__ == "__main__": + main()