Add support for appendices: landscape diagrams, tables, and images; implement Kroki URL configurability; enhance directive parsing logic.
This commit is contained in:
@@ -1,156 +1,345 @@
|
||||
# Markdown → IHK 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 A–Z)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user