Add support for appendices: landscape diagrams, tables, and images; implement Kroki URL configurability; enhance directive parsing logic.

This commit is contained in:
Sebastian Unterschütz
2026-05-12 21:44:37 +02:00
parent 436cdcc516
commit 67f9d63f24
12 changed files with 1025 additions and 221 deletions
+284 -95
View File
@@ -1,156 +1,345 @@
# MarkdownIHK Chemnitz PDF Converter
# MarkdownToIHKChemnitz
Converts Markdown files into compliant project documentation for **IHK Chemnitz**
IT vocational exams (Verordnung 2020).
A Go CLI tool that converts a single Markdown file into a print-ready PDF compliant with the **IHK Chemnitz project documentation guidelines** (Verordnung 2020) and **DIN 5008** formatting rules.
## Features
- DIN 5008 page layout — correct margins, font, line spacing, Blocksatz
- Two-pass rendering for a page-accurate table of contents
- Roman/Arabic page numbering with automatic section detection
- Multi-line table cells with uniform row height per row
- Named tables (*Tabellenverzeichnis*) and captioned figures (*Abbildungsverzeichnis*)
- Numbered code blocks with line-number gutter and long-line wrapping
- Diagram rendering via [Kroki](https://kroki.io) — Mermaid, PlantUML, and more
- Inline landscape diagram pages (`@DiagrammQuer:`)
- Appendices in portrait **and landscape** — images, tables, and diagrams
- Abbreviation list and glossary from YAML front matter
- Bibliography sorted alphabetically from inline `@Quelle:` directives
- Declaration of authenticity page (pre-written legal text)
---
## IHK Formatting Requirements
## Prerequisites
All rules below are enforced automatically. Deviating from them can lead to
grade deductions (*"kann zu Punktabzug bei der Bewertung führen"*).
- Go 1.22 or later
- Internet access **or** a local Kroki instance for diagram rendering (see below)
| Rule | Value | Note |
|---|---|---|
| Font | Helvetica / Arial, black | PDF-safe equivalent |
| Body size | 12 pt | |
| Heading size | 14 pt, bold | |
| Caption / footnote size | 10 pt | |
| Line spacing | 1½ lines (6.35 mm) | |
| Alignment | Justified (Blocksatz) | |
| Left margin | 3.0 cm | |
| **Right margin** | **4.0 cm** | **Korrekturrand** — examiners write feedback here |
| Top margin | 2.0 cm | |
| Bottom margin | 2.5 cm | |
| Paper | DIN A4, single-sided | |
| Front-matter numbering | Roman, starting at **II** | Title page is unnumbered |
| Body numbering | Arabic, starting at **1** | Centered at the bottom |
---
## Document Structure
The tool produces all required sections in the mandatory IHK order:
```
1. Title page · no page number
2. Table of contents · Roman II
3. List of abbrev. · Roman (optional, from YAML)
4. Foreword / body · Roman → Arabic via Markdown headings
5. Bibliography · Arabic (auto-sorted AZ)
6. List of tables · Arabic (auto-generated)
7. List of figures · Arabic (auto-generated)
8. Appendix · Arabic (via @Anhang: directives)
9. Glossary · Arabic (optional, from YAML)
10. Declaration · no page number (pre-written legal text)
```
## Installation
Requires [Go](https://golang.org/) 1.22 or newer.
## Build & Run
```bash
git clone <repository-url>
cd MarkdownToIHKChemnits
go mod tidy
go build -o ihk-pdf .
./ihk-pdf -i report.md -o projektarbeit.pdf
```
## Usage
Or without building:
```bash
go run . -i report.md -o projektarbeit.pdf
```
---
## CLI Flags
| Flag | Default | Description |
|---|---|---|
|------|---------|-------------|
| `-i` | `report.md` | Input Markdown file |
| `-o` | `projektarbeit.pdf` | Output PDF file |
| `-kroki` | `https://kroki.io` | Kroki base URL for diagram rendering |
---
## Diagram Rendering with Kroki
Mermaid and PlantUML fenced code blocks are rendered server-side via [Kroki](https://kroki.io). The resulting PNG is cached locally using a SHA-256 hash of the diagram source, so unchanged diagrams are not re-fetched.
By default the public instance at `https://kroki.io` is used. If your network blocks it, run a local instance with Docker Compose:
**`docker-compose.yml`**
```yaml
services:
kroki:
image: yuzutech/kroki
environment:
- KROKI_MERMAID_HOST=mermaid
ports:
- "8000:8000"
depends_on:
- mermaid
mermaid:
image: yuzutech/kroki-mermaid
expose:
- "8002"
```
```bash
docker compose up -d
go run . -i report.md -kroki http://localhost:8000 -o projektarbeit.pdf
```
> **Note:** The base `yuzutech/kroki` image does not include a Mermaid renderer. The companion `yuzutech/kroki-mermaid` container is required, wired via `KROKI_MERMAID_HOST=mermaid`.
---
## YAML Front Matter
Every Markdown file starts with a YAML block that drives the title page,
abbreviation list, and glossary:
Every input file begins with a YAML block delimited by `---`. All fields are optional except where noted.
```yaml
---
student:
name: "Max Mustermann"
name: "Max Mustermann" # required
profession: "Fachinformatiker Fachrichtung Anwendungsentwicklung"
company: "Musterfirma GmbH"
supervisor: "Sabine Supervisor"
project:
title: "Title of the project"
period: "Summer 2026"
# Optional — generates Abkürzungsverzeichnis
abbreviations:
project:
title: "Titel der Projektarbeit" # required
subtitle: "Optionaler Untertitel" # optional
period: "Sommer 2026"
abbreviations: # optional → Abkürzungsverzeichnis
- abbr: "API"
meaning: "Application Programming Interface"
- abbr: "DIN"
meaning: "Deutsches Institut für Normung"
- abbr: "IHK"
meaning: "Industrie- und Handelskammer"
# Optional — generates Glossar
glossary:
glossary: # optional → Glossar (after appendices)
- term: "Goldmark"
definition: "A CommonMark-compliant Markdown parser written in Go."
definition: "Ein CommonMark-konformer Markdown-Parser für Go."
---
```
## Markdown Directives
---
Special directives inside paragraphs control bibliography, appendix, and diagrams:
## Document Structure
```markdown
# Vorwort
This section uses Roman page numbers.
The generated PDF follows the mandatory IHK Chemnitz order:
# 1. Problem Statement
Any numbered or unknown heading switches to Arabic numbering.
| # | Section | Page Numbering |
|---|---------|----------------|
| 1 | Title page | none |
| 2 | Table of contents | Roman (II, III, …) |
| 3 | Abbreviation list | Roman (from YAML, optional) |
| 4 | Foreword / front-matter sections | Roman |
| 5 | Main body chapters | Arabic (1, 2, …) |
| 6 | Bibliography | Arabic |
| 7 | List of tables | Arabic |
| 8 | List of figures | Arabic |
| 9 | Appendices | Arabic |
| 10 | Glossary | Arabic (from YAML, optional) |
| 11 | Declaration of authenticity | none |
# Inline formatting
**Bold** and *italic* text are preserved in the PDF output.
### Front-matter detection
# Bibliography entries
@Quelle: Author, Title, Publisher, Year
> Quelle: Alternative blockquote syntax also works
Level-1 headings named `Vorwort`, `Einleitung`, or `Abkürzungsverzeichnis` stay in the Roman-numbered front matter. Every other level-1 heading triggers the switch to Arabic numbering.
# Appendix images
@Anhang: Description | /path/to/image.png
---
# UML diagram as appendix (rendered via Kroki)
@AnhangUML: Sequence Diagram Title
` ``puml
## DIN 5008 / IHK Formatting Rules
| Property | Value |
|----------|-------|
| Paper | A4 |
| Left margin | 30 mm |
| Right margin (Korrekturrand) | 40 mm |
| Top margin | 20 mm |
| Bottom margin | 25 mm |
| Body font | Helvetica (Arial) 12 pt, black |
| Body line spacing | 1.5× → 6.35 mm |
| Heading font | Helvetica Bold 14 pt |
| Caption / footnote | Helvetica 10 pt |
| List line spacing | 1.2× → 5.0 mm |
| Alignment | Justified (Blocksatz) |
| Page number position | Centered at bottom |
---
## Directives
Directives are plain paragraphs starting with `@`. They are consumed by the parser and never appear in the PDF as raw text. Multiple directives may appear in a single paragraph, one per line.
---
### `@Quelle:` — Bibliography entry
```
@Quelle: Autor, Titel, Verlag, Jahr
```
Registers a bibliography entry. All entries are collected and rendered alphabetically at the end of the document in the *Literaturverzeichnis*. May appear anywhere in the document, any number of times.
---
### `@Tabelle:` — Named table
Place immediately before a Markdown table to assign it a name and record it in the *Tabellenverzeichnis*:
```
@Tabelle: Übersicht der Programmiersprachen
| Sprache | Paradigma | Typsystem |
|---------|-----------|-----------|
| Go | Imperativ | Statisch |
| Python | Multi | Dynamisch |
```
---
### `@TabelleAnhang:` — Table as portrait appendix
Sends the following table to the appendix on a **portrait** A4 page as a numbered *Anlage*:
```
@TabelleAnhang: Vollständige Fehlerliste
| Code | Beschreibung | Schwere |
|------|--------------------|----------|
| E001 | Datei nicht gefunden | Kritisch |
```
---
### `@TabelleAnhangQuer:` — Table as landscape appendix
Same as `@TabelleAnhang:` but placed on a **landscape** A4 page (297 × 210 mm, 15 mm symmetric margins). Use for wide tables that do not fit in portrait:
```
@TabelleAnhangQuer: Breite Vergleichsmatrix
| Kriterium | Option A | Option B | Option C | Option D |
|-----------|----------|----------|----------|----------|
| Leistung | Gut | Sehr gut | Befriedigend | Gut |
```
---
### `@Anhang:` — Image as portrait appendix
```
@Anhang: Netzwerkdiagramm | diagrams/network.png
```
Adds an image file as a numbered *Anlage* on a **portrait** appendix page. The format is `Title | relative/path/to/image`.
---
### `@AnhangBildQuer:` — Image as landscape appendix
```
@AnhangBildQuer: Großes Architekturdiagramm | diagrams/arch.png
```
Same as `@Anhang:` but placed on a **landscape** A4 page. Useful for wide images.
---
### `@AnhangUML:` — Diagram as portrait appendix
Renders the immediately following Mermaid or PlantUML code block via Kroki and places the result as a numbered *Anlage* on a **portrait** appendix page:
```
@AnhangUML: Datenbankschema
` ``plantuml
@startuml
A -> B: request
entity User {
+ id : int
+ name : string
}
@enduml
` ``
```
---
### `@AnhangUMLQuer:` — Diagram as landscape appendix
Same as `@AnhangUML:` but placed on a **landscape** A4 page with 15 mm symmetric margins. Use for complex diagrams that need more horizontal space:
```
@AnhangUMLQuer: Vollständige Modulübersicht
# Inline diagrams (rendered via Kroki, embedded in body)
` ``mermaid
graph TD
A --> B
A --> B --> C
` ``
```
Diagrams are rendered via [Kroki.io](https://kroki.io) and cached locally
using the SHA-256 hash of the diagram source as the cache key.
---
## Project Structure
### `@DiagrammQuer:` — Inline landscape diagram page
Renders the following diagram on a **dedicated landscape page inline** in the document (not in the appendix). A figure caption is added and the figure is recorded in the *Abbildungsverzeichnis*. A fresh portrait page opens automatically afterwards:
```
MarkdownToIHKChemnits/
├── main.go Entry point; two-pass rendering pipeline
├── config.go Config struct (student, project, abbreviations, glossary)
├── markdown_parser.go Goldmark AST walker; inline span extraction; directives
├── diagram.go Kroki.io integration (Mermaid, PlantUML, …)
├── pdf_renderer.go IHKRenderer struct; DIN 5008 constants; core helpers
├── pdf_numbering.go NumberingType enum; toRoman()
├── pdf_toc.go Table of contents; list of tables; list of figures
├── pdf_pages.go Title, declaration, bibliography, appendix, abbrev., glossary
└── pdf_content.go Headings, paragraphs, tables, images, list items
@DiagrammQuer: Systemarchitektur Zwei-Pass-Rendering
` ``mermaid
graph LR
MD[report.md] --> Parser --> AST --> Renderer --> PDF
` ``
```
---
## Directive Summary Table
| Directive | Format | Placement | Orientation |
|-----------|--------|-----------|-------------|
| `@Quelle:` | `@Quelle: Text` | Inline anywhere | — |
| `@Tabelle:` | `@Tabelle: Name` | Before a table | — |
| `@TabelleAnhang:` | `@TabelleAnhang: Name` | Before a table | Portrait appendix |
| `@TabelleAnhangQuer:` | `@TabelleAnhangQuer: Name` | Before a table | **Landscape** appendix |
| `@Anhang:` | `@Anhang: Title \| path` | Standalone | Portrait appendix |
| `@AnhangBildQuer:` | `@AnhangBildQuer: Title \| path` | Standalone | **Landscape** appendix |
| `@AnhangUML:` | `@AnhangUML: Title` | Before diagram block | Portrait appendix |
| `@AnhangUMLQuer:` | `@AnhangUMLQuer: Title` | Before diagram block | **Landscape** appendix |
| `@DiagrammQuer:` | `@DiagrammQuer: Caption` | Before diagram block | **Landscape** inline page |
---
## File Structure
```
.
├── main.go # Entry point, CLI flags, two-pass pipeline
├── config.go # Config struct matching the YAML front matter
├── markdown_parser.go # Goldmark AST walker, directive handling
├── pdf_renderer.go # IHKRenderer struct, DIN 5008 constants, appendix registry
├── pdf_content.go # Paragraphs, lists, tables, images, code blocks
├── pdf_toc.go # TOC, list of tables, list of figures
├── pdf_pages.go # Title page, declaration, bibliography, appendices, glossary
├── pdf_numbering.go # Roman/Arabic page numbering helpers
├── diagram.go # Kroki HTTP client, SHA-256 image cache
├── docker-compose.yml # Local Kroki + kroki-mermaid containers
└── report.md # Sample document demonstrating all features
```
---
## Two-Pass Rendering
The tool renders the document twice:
1. **Pass 1** — full render into a scratch PDF to collect the page number of every heading, table, and figure.
2. **Pass 2** — final render using the TOC index from pass 1, producing the output PDF.
Only `tocItems` are transferred between passes. `tableItems` and `figureItems` are rebuilt during pass 2 to avoid duplicates in the respective lists.
---
## Landscape Pages — Technical Notes
- Landscape appendices and `@DiagrammQuer:` use `fpdf.AddPageFormat("L", {Wd:297, Ht:210})` with 15 mm symmetric margins. No 40 mm Korrekturrand — examiners do not annotate diagram or data pages.
- After any landscape page, `AddPage()` reverts to the default portrait orientation (A4 "P") set at construction time — no explicit orientation restore is needed for subsequent content.
- Portrait margins (30/20/40 mm) are restored immediately after the landscape page is closed so all following content is formatted correctly.
---
## License
MIT