feat(okr): /okr:export PDF via weasyprint + scripts/export-pdf.py (SC5)

This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 09:46:39 +02:00
commit a0e533a856
2 changed files with 192 additions and 0 deletions

84
commands/export.md Normal file
View file

@ -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 <input.md> <output.pdf>
```
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: <sti>` 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/

108
scripts/export-pdf.py Normal file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Render an OKR Markdown document to a print-ready A4 PDF.
Usage:
python3 export-pdf.py <input.md> <output.pdf>
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 (
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<style>" + PAGE_CSS + "</style></head><body>" + body + "</body></html>"
)
def main():
if len(sys.argv) != 3:
sys.stderr.write("Bruk: python3 export-pdf.py <input.md> <output.pdf>\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()