Add support for appendices: landscape diagrams, tables, and images; implement Kroki URL configurability; enhance directive parsing logic.
This commit is contained in:
Generated
+12
-4
@@ -4,10 +4,16 @@
|
|||||||
<option name="autoReloadType" value="ALL" />
|
<option name="autoReloadType" value="ALL" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="c64f46d5-641a-468c-8fc1-94edec1f2deb" name="Changes" comment="Remove outdated `toc_pages.txt`, add new Go modules for IHK Chemnitz PDF rendering including diagrams, tables, and TOC functionality.">
|
<list default="true" id="c64f46d5-641a-468c-8fc1-94edec1f2deb" name="Changes" comment="Refactor PDF content rendering: improve list indentation logic, add numbered code block rendering with gutter, and update text wrapping alignment.">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/MarkdownToIHKChemnits" beforeDir="false" afterPath="$PROJECT_DIR$/MarkdownToIHKChemnits" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/MarkdownToIHKChemnits" beforeDir="false" afterPath="$PROJECT_DIR$/MarkdownToIHKChemnits" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/diagram.go" beforeDir="false" afterPath="$PROJECT_DIR$/diagram.go" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/main.go" beforeDir="false" afterPath="$PROJECT_DIR$/main.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/markdown_parser.go" beforeDir="false" afterPath="$PROJECT_DIR$/markdown_parser.go" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/markdown_parser.go" beforeDir="false" afterPath="$PROJECT_DIR$/markdown_parser.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pdf_content.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_content.go" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pdf_content.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_content.go" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pdf_pages.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_pages.go" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pdf_renderer.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_renderer.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/projektarbeit.pdf" beforeDir="false" afterPath="$PROJECT_DIR$/projektarbeit.pdf" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/projektarbeit.pdf" beforeDir="false" afterPath="$PROJECT_DIR$/projektarbeit.pdf" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/report.md" beforeDir="false" afterPath="$PROJECT_DIR$/report.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/report.md" beforeDir="false" afterPath="$PROJECT_DIR$/report.md" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
@@ -17,7 +23,7 @@
|
|||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="EmbeddingIndexingInfo">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="19" />
|
<option name="cachedIndexableFilesCount" value="22" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="GOROOT" url="file:///usr/lib/go" />
|
<component name="GOROOT" url="file:///usr/lib/go" />
|
||||||
@@ -95,14 +101,16 @@
|
|||||||
<MESSAGE value="Initial commit: added Markdown to IHK Chemnitz PDF converter with core structure and features, including YAML config, Goldmark parser, and PDF renderer." />
|
<MESSAGE value="Initial commit: added Markdown to IHK Chemnitz PDF converter with core structure and features, including YAML config, Goldmark parser, and PDF renderer." />
|
||||||
<MESSAGE value="Remove outdated IHK guideline text and refactor PDF renderer for improved modularity, DIN 5008 compliance, and glossary/abbreviation support." />
|
<MESSAGE value="Remove outdated IHK guideline text and refactor PDF renderer for improved modularity, DIN 5008 compliance, and glossary/abbreviation support." />
|
||||||
<MESSAGE value="Remove outdated `toc_pages.txt`, add new Go modules for IHK Chemnitz PDF rendering including diagrams, tables, and TOC functionality." />
|
<MESSAGE value="Remove outdated `toc_pages.txt`, add new Go modules for IHK Chemnitz PDF rendering including diagrams, tables, and TOC functionality." />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Remove outdated `toc_pages.txt`, add new Go modules for IHK Chemnitz PDF rendering including diagrams, tables, and TOC functionality." />
|
<MESSAGE value="Refactor PDF content rendering: improve list indentation logic, add numbered code block rendering with gutter, and update text wrapping alignment." />
|
||||||
|
<MESSAGE value="Add table annex handling, update diagram rendering with configurable Kroki URL, and improve Markdown directive parsing" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="Add table annex handling, update diagram rendering with configurable Kroki URL, and improve Markdown directive parsing" />
|
||||||
</component>
|
</component>
|
||||||
<component name="XDebuggerManager">
|
<component name="XDebuggerManager">
|
||||||
<breakpoint-manager>
|
<breakpoint-manager>
|
||||||
<breakpoints>
|
<breakpoints>
|
||||||
<line-breakpoint enabled="true" type="DlvLineBreakpoint">
|
<line-breakpoint enabled="true" type="DlvLineBreakpoint">
|
||||||
<url>file://$PROJECT_DIR$/main.go</url>
|
<url>file://$PROJECT_DIR$/main.go</url>
|
||||||
<line>37</line>
|
<line>40</line>
|
||||||
<option name="timeStamp" value="1" />
|
<option name="timeStamp" value="1" />
|
||||||
</line-breakpoint>
|
</line-breakpoint>
|
||||||
</breakpoints>
|
</breakpoints>
|
||||||
|
|||||||
Binary file not shown.
@@ -1,155 +1,344 @@
|
|||||||
# Markdown → IHK Chemnitz PDF Converter
|
# MarkdownToIHKChemnitz
|
||||||
|
|
||||||
Converts Markdown files into compliant project documentation for **IHK Chemnitz**
|
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.
|
||||||
IT vocational exams (Verordnung 2020).
|
|
||||||
|
## 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
|
- Go 1.22 or later
|
||||||
grade deductions (*"kann zu Punktabzug bei der Bewertung führen"*).
|
- 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
|
## Build & Run
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
go build -o ihk-pdf .
|
||||||
cd MarkdownToIHKChemnits
|
./ihk-pdf -i report.md -o projektarbeit.pdf
|
||||||
go mod tidy
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
Or without building:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run . -i report.md -o projektarbeit.pdf
|
go run . -i report.md -o projektarbeit.pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Flags
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
|---|---|---|
|
|------|---------|-------------|
|
||||||
| `-i` | `report.md` | Input Markdown file |
|
| `-i` | `report.md` | Input Markdown file |
|
||||||
| `-o` | `projektarbeit.pdf` | Output PDF 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
|
## YAML Front Matter
|
||||||
|
|
||||||
Every Markdown file starts with a YAML block that drives the title page,
|
Every input file begins with a YAML block delimited by `---`. All fields are optional except where noted.
|
||||||
abbreviation list, and glossary:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
student:
|
student:
|
||||||
name: "Max Mustermann"
|
name: "Max Mustermann" # required
|
||||||
profession: "Fachinformatiker Fachrichtung Anwendungsentwicklung"
|
profession: "Fachinformatiker Fachrichtung Anwendungsentwicklung"
|
||||||
company: "Musterfirma GmbH"
|
company: "Musterfirma GmbH"
|
||||||
supervisor: "Sabine Supervisor"
|
supervisor: "Sabine Supervisor"
|
||||||
project:
|
|
||||||
title: "Title of the project"
|
|
||||||
period: "Summer 2026"
|
|
||||||
|
|
||||||
# Optional — generates Abkürzungsverzeichnis
|
project:
|
||||||
abbreviations:
|
title: "Titel der Projektarbeit" # required
|
||||||
|
subtitle: "Optionaler Untertitel" # optional
|
||||||
|
period: "Sommer 2026"
|
||||||
|
|
||||||
|
abbreviations: # optional → Abkürzungsverzeichnis
|
||||||
- abbr: "API"
|
- abbr: "API"
|
||||||
meaning: "Application Programming Interface"
|
meaning: "Application Programming Interface"
|
||||||
- abbr: "DIN"
|
- abbr: "IHK"
|
||||||
meaning: "Deutsches Institut für Normung"
|
meaning: "Industrie- und Handelskammer"
|
||||||
|
|
||||||
# Optional — generates Glossar
|
glossary: # optional → Glossar (after appendices)
|
||||||
glossary:
|
|
||||||
- term: "Goldmark"
|
- 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
|
The generated PDF follows the mandatory IHK Chemnitz order:
|
||||||
# Vorwort
|
|
||||||
This section uses Roman page numbers.
|
|
||||||
|
|
||||||
# 1. Problem Statement
|
| # | Section | Page Numbering |
|
||||||
Any numbered or unknown heading switches to Arabic 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
|
### Front-matter detection
|
||||||
**Bold** and *italic* text are preserved in the PDF output.
|
|
||||||
|
|
||||||
# Bibliography entries
|
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.
|
||||||
@Quelle: Author, Title, Publisher, Year
|
|
||||||
> Quelle: Alternative blockquote syntax also works
|
|
||||||
|
|
||||||
# Appendix images
|
---
|
||||||
@Anhang: Description | /path/to/image.png
|
|
||||||
|
|
||||||
# UML diagram as appendix (rendered via Kroki)
|
## DIN 5008 / IHK Formatting Rules
|
||||||
@AnhangUML: Sequence Diagram Title
|
|
||||||
` ``puml
|
| 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
|
@startuml
|
||||||
A -> B: request
|
entity User {
|
||||||
|
+ id : int
|
||||||
|
+ name : string
|
||||||
|
}
|
||||||
@enduml
|
@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
|
` ``mermaid
|
||||||
graph TD
|
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/
|
@DiagrammQuer: Systemarchitektur – Zwei-Pass-Rendering
|
||||||
│
|
|
||||||
├── main.go Entry point; two-pass rendering pipeline
|
` ``mermaid
|
||||||
├── config.go Config struct (student, project, abbreviations, glossary)
|
graph LR
|
||||||
│
|
MD[report.md] --> Parser --> AST --> Renderer --> PDF
|
||||||
├── 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
|
|
||||||
` ``
|
` ``
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
## License
|
||||||
|
|
||||||
|
|||||||
+17
-9
@@ -10,15 +10,22 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// krokiBaseURL is the Kroki instance to use. Override with the -kroki flag.
|
||||||
|
// Self-host: docker run -d -p 8000:8000 yuzutech/kroki
|
||||||
|
var krokiBaseURL = "https://kroki.io"
|
||||||
|
|
||||||
|
// krokiClient is a shared HTTP client with a short timeout so a dead Kroki
|
||||||
|
// backend fails fast instead of hanging for ~60 s per diagram.
|
||||||
|
var krokiClient = &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
// RenderDiagramViaKroki converts a diagram source (Mermaid, PlantUML, etc.)
|
// RenderDiagramViaKroki converts a diagram source (Mermaid, PlantUML, etc.)
|
||||||
// to a PNG image using the Kroki.io public rendering service and caches the
|
// to a PNG image via a Kroki rendering service and caches the result in the
|
||||||
// result in the OS temp directory.
|
// OS temp directory. The base URL is controlled by krokiBaseURL (-kroki flag).
|
||||||
//
|
|
||||||
// The cache key is the SHA-256 hash of the diagram source, so unchanged
|
|
||||||
// diagrams are not re-fetched between runs.
|
|
||||||
//
|
//
|
||||||
|
// Cache key: SHA-256 of the diagram source — unchanged diagrams are not re-fetched.
|
||||||
// Supported languages: "mermaid", "plantuml" / "puml"
|
// Supported languages: "mermaid", "plantuml" / "puml"
|
||||||
func RenderDiagramViaKroki(lang, code string) (string, error) {
|
func RenderDiagramViaKroki(lang, code string) (string, error) {
|
||||||
if lang == "puml" {
|
if lang == "puml" {
|
||||||
@@ -35,7 +42,7 @@ func RenderDiagramViaKroki(lang, code string) (string, error) {
|
|||||||
return "", fmt.Errorf("zlib close: %w", err)
|
return "", fmt.Errorf("zlib close: %w", err)
|
||||||
}
|
}
|
||||||
encoded := base64.URLEncoding.EncodeToString(buf.Bytes())
|
encoded := base64.URLEncoding.EncodeToString(buf.Bytes())
|
||||||
url := fmt.Sprintf("https://kroki.io/%s/png/%s", lang, encoded)
|
url := fmt.Sprintf("%s/%s/png/%s", krokiBaseURL, lang, encoded)
|
||||||
|
|
||||||
// Deterministic cache path based on content hash
|
// Deterministic cache path based on content hash
|
||||||
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(code)))
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(code)))
|
||||||
@@ -45,14 +52,14 @@ func RenderDiagramViaKroki(lang, code string) (string, error) {
|
|||||||
return cachePath, nil // cache hit
|
return cachePath, nil // cache hit
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Get(url) //nolint:gosec // URL is constructed from user content, acceptable here
|
resp, err := krokiClient.Get(url) //nolint:gosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("kroki request: %w", err)
|
return "", fmt.Errorf("kroki request failed (is %s reachable?): %w", krokiBaseURL, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return "", fmt.Errorf("kroki returned HTTP %d", resp.StatusCode)
|
return "", fmt.Errorf("kroki returned HTTP %d — try a local instance: docker run -d -p 8000:8000 yuzutech/kroki", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := os.Create(cachePath)
|
out, err := os.Create(cachePath)
|
||||||
@@ -62,6 +69,7 @@ func RenderDiagramViaKroki(lang, code string) (string, error) {
|
|||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
|
||||||
if _, err = io.Copy(out, resp.Body); err != nil {
|
if _, err = io.Copy(out, resp.Body); err != nil {
|
||||||
|
_ = os.Remove(cachePath) // remove incomplete cache file
|
||||||
return "", fmt.Errorf("cache file write: %w", err)
|
return "", fmt.Errorf("cache file write: %w", err)
|
||||||
}
|
}
|
||||||
return cachePath, nil
|
return cachePath, nil
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
kroki:
|
||||||
|
image: yuzutech/kroki
|
||||||
|
environment:
|
||||||
|
- KROKI_MERMAID_HOST=mermaid
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
- mermaid
|
||||||
|
|
||||||
|
mermaid:
|
||||||
|
image: yuzutech/kroki-mermaid
|
||||||
|
expose:
|
||||||
|
- "8002"
|
||||||
@@ -11,8 +11,11 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
inputMd := flag.String("i", "report.md", "Input Markdown file")
|
inputMd := flag.String("i", "report.md", "Input Markdown file")
|
||||||
outputPdf := flag.String("o", "projektarbeit.pdf", "Output PDF file")
|
outputPdf := flag.String("o", "projektarbeit.pdf", "Output PDF file")
|
||||||
|
kroki := flag.String("kroki", "https://kroki.io", "Kroki base URL (e.g. http://localhost:8000 for a local instance)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
krokiBaseURL = *kroki
|
||||||
|
|
||||||
config, doc, content, err := ParseMarkdown(*inputMd)
|
config, doc, content, err := ParseMarkdown(*inputMd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse input: %v", err)
|
log.Fatalf("Failed to parse input: %v", err)
|
||||||
|
|||||||
+71
-6
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -44,7 +45,13 @@ func ParseMarkdown(mdPath string) (Config, ast.Node, []byte, error) {
|
|||||||
// parserState tracks transient state during the AST walk.
|
// parserState tracks transient state during the AST walk.
|
||||||
type parserState struct {
|
type parserState struct {
|
||||||
nextCodeIsAppendix bool
|
nextCodeIsAppendix bool
|
||||||
|
nextAppendixLandscape bool // set by @AnhangUMLQuer: — landscape for diagram appendix
|
||||||
appendixTitle string
|
appendixTitle string
|
||||||
|
nextTableCaption string // set by @Tabelle: directive
|
||||||
|
nextTableIsAppendix bool // set by @TabelleAnhang: or @TabelleAnhangQuer:
|
||||||
|
nextTableIsLandscape bool // set by @TabelleAnhangQuer:
|
||||||
|
nextDiagramLandscape bool // set by @DiagrammQuer: directive
|
||||||
|
nextDiagramCaption string // caption for the landscape diagram page
|
||||||
listStack []listFrame // stack for nested list tracking
|
listStack []listFrame // stack for nested list tracking
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +114,29 @@ func RenderAST(doc ast.Node, content []byte, r *IHKRenderer) error {
|
|||||||
|
|
||||||
if lang == "mermaid" || lang == "plantuml" || lang == "puml" {
|
if lang == "mermaid" || lang == "plantuml" || lang == "puml" {
|
||||||
imgPath, err := RenderDiagramViaKroki(lang, code)
|
imgPath, err := RenderDiagramViaKroki(lang, code)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
caption := lang
|
log.Printf("warning: diagram render failed (%s): %v — falling back to code block", lang, err)
|
||||||
if state.nextCodeIsAppendix {
|
state.nextDiagramLandscape = false
|
||||||
r.AddAppendix(state.appendixTitle + " | " + imgPath)
|
state.nextDiagramCaption = ""
|
||||||
state.nextCodeIsAppendix = false
|
state.nextCodeIsAppendix = false
|
||||||
|
state.nextAppendixLandscape = false
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
switch {
|
||||||
|
case state.nextDiagramLandscape:
|
||||||
|
r.RenderLandscapeDiagram(imgPath, state.nextDiagramCaption)
|
||||||
|
state.nextDiagramLandscape = false
|
||||||
|
state.nextDiagramCaption = ""
|
||||||
|
case state.nextCodeIsAppendix:
|
||||||
|
if state.nextAppendixLandscape {
|
||||||
|
r.AddLandscapeAppendix(state.appendixTitle + " | " + imgPath)
|
||||||
|
state.nextAppendixLandscape = false
|
||||||
} else {
|
} else {
|
||||||
r.RenderImage(imgPath, "Diagram ("+caption+")")
|
r.AddAppendix(state.appendixTitle + " | " + imgPath)
|
||||||
|
}
|
||||||
|
state.nextCodeIsAppendix = false
|
||||||
|
default:
|
||||||
|
r.RenderImage(imgPath, "Diagram ("+lang+")")
|
||||||
}
|
}
|
||||||
return ast.WalkSkipChildren, nil
|
return ast.WalkSkipChildren, nil
|
||||||
}
|
}
|
||||||
@@ -162,6 +185,11 @@ func RenderAST(doc ast.Node, content []byte, r *IHKRenderer) error {
|
|||||||
if len(state.listStack) > 0 {
|
if len(state.listStack) > 0 {
|
||||||
state.listStack = state.listStack[:len(state.listStack)-1]
|
state.listStack = state.listStack[:len(state.listStack)-1]
|
||||||
}
|
}
|
||||||
|
// Add breathing room after the outermost list so the next
|
||||||
|
// paragraph is not glued to the last bullet.
|
||||||
|
if len(state.listStack) == 0 {
|
||||||
|
r.pdf.Ln(dinSpaceAfterList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
|
|
||||||
@@ -192,7 +220,20 @@ func RenderAST(doc ast.Node, content []byte, r *IHKRenderer) error {
|
|||||||
}
|
}
|
||||||
tableData = append(tableData, rowData)
|
tableData = append(tableData, rowData)
|
||||||
}
|
}
|
||||||
r.RenderTable(tableData, "")
|
if state.nextTableIsAppendix {
|
||||||
|
if state.nextTableIsLandscape {
|
||||||
|
r.AddTableAppendixLandscape(state.nextTableCaption, tableData)
|
||||||
|
state.nextTableIsLandscape = false
|
||||||
|
} else {
|
||||||
|
r.AddTableAppendix(state.nextTableCaption, tableData)
|
||||||
|
}
|
||||||
|
state.nextTableIsAppendix = false
|
||||||
|
state.nextTableCaption = ""
|
||||||
|
} else {
|
||||||
|
caption := state.nextTableCaption
|
||||||
|
state.nextTableCaption = ""
|
||||||
|
r.RenderTable(tableData, caption)
|
||||||
|
}
|
||||||
return ast.WalkSkipChildren, nil
|
return ast.WalkSkipChildren, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,10 +267,34 @@ func handleDirectives(text string, state *parserState, r *IHKRenderer) bool {
|
|||||||
case strings.HasPrefix(line, "@Anhang:"):
|
case strings.HasPrefix(line, "@Anhang:"):
|
||||||
r.AddAppendix(strings.TrimSpace(strings.TrimPrefix(line, "@Anhang:")))
|
r.AddAppendix(strings.TrimSpace(strings.TrimPrefix(line, "@Anhang:")))
|
||||||
handled = true
|
handled = true
|
||||||
|
case strings.HasPrefix(line, "@AnhangBildQuer:"):
|
||||||
|
r.AddLandscapeAppendix(strings.TrimSpace(strings.TrimPrefix(line, "@AnhangBildQuer:")))
|
||||||
|
handled = true
|
||||||
|
case strings.HasPrefix(line, "@AnhangUMLQuer:"):
|
||||||
|
state.nextCodeIsAppendix = true
|
||||||
|
state.nextAppendixLandscape = true
|
||||||
|
state.appendixTitle = strings.TrimSpace(strings.TrimPrefix(line, "@AnhangUMLQuer:"))
|
||||||
|
handled = true
|
||||||
case strings.HasPrefix(line, "@AnhangUML:"):
|
case strings.HasPrefix(line, "@AnhangUML:"):
|
||||||
state.nextCodeIsAppendix = true
|
state.nextCodeIsAppendix = true
|
||||||
state.appendixTitle = strings.TrimSpace(strings.TrimPrefix(line, "@AnhangUML:"))
|
state.appendixTitle = strings.TrimSpace(strings.TrimPrefix(line, "@AnhangUML:"))
|
||||||
handled = true
|
handled = true
|
||||||
|
case strings.HasPrefix(line, "@TabelleAnhangQuer:"):
|
||||||
|
state.nextTableIsAppendix = true
|
||||||
|
state.nextTableIsLandscape = true
|
||||||
|
state.nextTableCaption = strings.TrimSpace(strings.TrimPrefix(line, "@TabelleAnhangQuer:"))
|
||||||
|
handled = true
|
||||||
|
case strings.HasPrefix(line, "@TabelleAnhang:"):
|
||||||
|
state.nextTableIsAppendix = true
|
||||||
|
state.nextTableCaption = strings.TrimSpace(strings.TrimPrefix(line, "@TabelleAnhang:"))
|
||||||
|
handled = true
|
||||||
|
case strings.HasPrefix(line, "@Tabelle:"):
|
||||||
|
state.nextTableCaption = strings.TrimSpace(strings.TrimPrefix(line, "@Tabelle:"))
|
||||||
|
handled = true
|
||||||
|
case strings.HasPrefix(line, "@DiagrammQuer:"):
|
||||||
|
state.nextDiagramLandscape = true
|
||||||
|
state.nextDiagramCaption = strings.TrimSpace(strings.TrimPrefix(line, "@DiagrammQuer:"))
|
||||||
|
handled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return handled
|
return handled
|
||||||
|
|||||||
+162
-49
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, in
|
|||||||
|
|
||||||
// Render bullet/number; CellFormat advances X to textX automatically.
|
// Render bullet/number; CellFormat advances X to textX automatically.
|
||||||
r.pdf.SetX(lm + indentMM)
|
r.pdf.SetX(lm + indentMM)
|
||||||
r.pdf.CellFormat(prefixW, dinLineHtBody, r.tr(prefix), "", 0, "L", false, 0, "")
|
r.pdf.CellFormat(prefixW, dinLineHtList, r.tr(prefix), "", 0, "L", false, 0, "")
|
||||||
|
|
||||||
hasFormatting := false
|
hasFormatting := false
|
||||||
for _, s := range spans {
|
for _, s := range spans {
|
||||||
@@ -126,7 +127,7 @@ func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, in
|
|||||||
for _, s := range spans {
|
for _, s := range spans {
|
||||||
text += s.Text
|
text += s.Text
|
||||||
}
|
}
|
||||||
r.pdf.MultiCell(textW, dinLineHtBody, r.tr(text), "", "L", false)
|
r.pdf.MultiCell(textW, dinLineHtList, r.tr(text), "", "L", false)
|
||||||
} else {
|
} else {
|
||||||
for _, span := range spans {
|
for _, span := range spans {
|
||||||
style := fontStyle(span.Bold, span.Italic)
|
style := fontStyle(span.Bold, span.Italic)
|
||||||
@@ -135,70 +136,149 @@ func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, in
|
|||||||
} else {
|
} else {
|
||||||
r.pdf.SetFont("Helvetica", style, dinFontBody)
|
r.pdf.SetFont("Helvetica", style, dinFontBody)
|
||||||
}
|
}
|
||||||
r.pdf.Write(dinLineHtBody, r.tr(span.Text))
|
r.pdf.Write(dinLineHtList, r.tr(span.Text))
|
||||||
}
|
}
|
||||||
r.pdf.Ln(dinLineHtBody)
|
r.pdf.Ln(dinLineHtList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderTable renders a data table with a header row (grey background) and
|
// tableRowData holds pre-computed line-wrapped content for one table row.
|
||||||
// a auto-repeat header on page breaks. Each table is numbered and recorded
|
type tableRowData struct {
|
||||||
// in the Tabellenverzeichnis per IHK section 2.4.
|
cells [][]string // wrapped lines per cell
|
||||||
//
|
height float64 // total row height in mm
|
||||||
// data[0] is the header row; data[1:] are body rows.
|
}
|
||||||
|
|
||||||
|
// prepareRow measures how many lines each cell needs and returns a tableRowData
|
||||||
|
// with the wrapped content. Font must be set for measurement before calling.
|
||||||
|
func (r *IHKRenderer) prepareRow(rawCells []string, numCols int, colW, lineHt float64, bold bool) tableRowData {
|
||||||
|
style := ""
|
||||||
|
if bold {
|
||||||
|
style = "B"
|
||||||
|
}
|
||||||
|
r.pdf.SetFont("Helvetica", style, dinFontCaption)
|
||||||
|
|
||||||
|
cells := make([][]string, numCols)
|
||||||
|
maxLines := 0
|
||||||
|
for j := 0; j < numCols; j++ {
|
||||||
|
raw := ""
|
||||||
|
if j < len(rawCells) {
|
||||||
|
raw = rawCells[j]
|
||||||
|
}
|
||||||
|
split := r.pdf.SplitLines([]byte(r.tr(raw)), colW-2)
|
||||||
|
lines := make([]string, len(split))
|
||||||
|
for k, b := range split {
|
||||||
|
lines[k] = string(b)
|
||||||
|
}
|
||||||
|
if len(lines) == 0 {
|
||||||
|
lines = []string{""}
|
||||||
|
}
|
||||||
|
cells[j] = lines
|
||||||
|
if len(lines) > maxLines {
|
||||||
|
maxLines = len(lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if maxLines == 0 {
|
||||||
|
maxLines = 1
|
||||||
|
}
|
||||||
|
return tableRowData{cells: cells, height: float64(maxLines) * lineHt}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawRow renders a pre-computed row at the current Y position.
|
||||||
|
// Cell borders are drawn as rectangles so all cells share a uniform height
|
||||||
|
// regardless of how many lines they contain.
|
||||||
|
func (r *IHKRenderer) drawRow(row tableRowData, numCols int, colW, lineHt float64, isHeader bool) {
|
||||||
|
startY := r.pdf.GetY()
|
||||||
|
lm, _, _, _ := r.pdf.GetMargins()
|
||||||
|
|
||||||
|
style := ""
|
||||||
|
align := "L"
|
||||||
|
if isHeader {
|
||||||
|
style = "B"
|
||||||
|
align = "C"
|
||||||
|
r.pdf.SetFillColor(230, 230, 230)
|
||||||
|
} else {
|
||||||
|
r.pdf.SetFillColor(255, 255, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw all cell backgrounds and borders first (uniform height via Rect).
|
||||||
|
for j := 0; j < numCols; j++ {
|
||||||
|
r.pdf.Rect(lm+float64(j)*colW, startY, colW, row.height, "FD")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render text on top, line by line per cell.
|
||||||
|
r.pdf.SetFont("Helvetica", style, dinFontCaption)
|
||||||
|
for j, cellLines := range row.cells {
|
||||||
|
x := lm + float64(j)*colW
|
||||||
|
for k, line := range cellLines {
|
||||||
|
r.pdf.SetXY(x+1, startY+float64(k)*lineHt)
|
||||||
|
r.pdf.CellFormat(colW-2, lineHt, line, "", 0, align, false, 0, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.pdf.SetY(startY + row.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderTable numbers a table, records it in the Tabellenverzeichnis, and
|
||||||
|
// renders it with multi-line cell support and an auto-repeating header on
|
||||||
|
// page breaks. data[0] is the header row; data[1:] are body rows.
|
||||||
func (r *IHKRenderer) RenderTable(data [][]string, caption string) {
|
func (r *IHKRenderer) RenderTable(data [][]string, caption string) {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r.tableCount++
|
r.tableCount++
|
||||||
label := fmt.Sprintf("Tab. %d", r.tableCount)
|
label := fmt.Sprintf("Tab. %d", r.tableCount)
|
||||||
if caption != "" {
|
if caption != "" {
|
||||||
label += ": " + caption
|
label += ": " + caption
|
||||||
}
|
}
|
||||||
r.RecordTable(label)
|
r.RecordTable(label)
|
||||||
|
r.renderTableBody(data, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderTableBody renders table content without assigning a number or recording
|
||||||
|
// in the Tabellenverzeichnis. Pass an empty label to suppress the caption line.
|
||||||
|
func (r *IHKRenderer) renderTableBody(data [][]string, label string) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
header := data[0]
|
header := data[0]
|
||||||
numCols := len(header)
|
numCols := len(header)
|
||||||
|
if numCols == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
uw := r.usableWidth()
|
uw := r.usableWidth()
|
||||||
colW := uw / float64(numCols)
|
colW := uw / float64(numCols)
|
||||||
|
lineHt := dinLineHtCaption + 2
|
||||||
// renderHeaderRow draws the (grey) header row, optionally with a "(Fortsetzung)" suffix.
|
|
||||||
renderHeaderRow := func(continued bool) {
|
|
||||||
title := label
|
|
||||||
if continued {
|
|
||||||
title += " (Fortsetzung)"
|
|
||||||
}
|
|
||||||
r.pdf.SetFont("Helvetica", "B", dinFontCaption)
|
|
||||||
r.pdf.CellFormat(0, dinLineHtCaption+2, r.tr(title), "", 1, "L", false, 0, "")
|
|
||||||
|
|
||||||
r.pdf.SetFillColor(230, 230, 230)
|
|
||||||
r.pdf.SetFont("Helvetica", "B", dinFontCaption)
|
|
||||||
for _, col := range header {
|
|
||||||
r.pdf.CellFormat(colW, dinLineHtCaption+2, r.tr(col), "1", 0, "C", true, 0, "")
|
|
||||||
}
|
|
||||||
r.pdf.Ln(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHeaderRow(false)
|
|
||||||
|
|
||||||
r.pdf.SetFont("Helvetica", "", dinFontCaption)
|
|
||||||
r.pdf.SetFillColor(255, 255, 255)
|
|
||||||
|
|
||||||
_, pageH := r.pdf.GetPageSize()
|
_, pageH := r.pdf.GetPageSize()
|
||||||
_, _, _, bm := r.pdf.GetMargins()
|
_, _, _, bm := r.pdf.GetMargins()
|
||||||
rowH := dinLineHtCaption + 2
|
|
||||||
|
|
||||||
|
// Pre-compute all rows so we know heights before rendering.
|
||||||
|
headerRow := r.prepareRow(header, numCols, colW, lineHt, true)
|
||||||
|
bodyRows := make([]tableRowData, len(data)-1)
|
||||||
for i := 1; i < len(data); i++ {
|
for i := 1; i < len(data); i++ {
|
||||||
if r.pdf.GetY()+rowH > pageH-bm {
|
bodyRows[i-1] = r.prepareRow(data[i], numCols, colW, lineHt, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader := func(continued bool) {
|
||||||
|
title := label
|
||||||
|
if continued && label != "" {
|
||||||
|
title += " (Fortsetzung)"
|
||||||
|
}
|
||||||
|
if title != "" {
|
||||||
|
r.pdf.SetFont("Helvetica", "B", dinFontCaption)
|
||||||
|
r.pdf.CellFormat(0, lineHt, r.tr(title), "", 1, "L", false, 0, "")
|
||||||
|
}
|
||||||
|
r.drawRow(headerRow, numCols, colW, lineHt, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader(false)
|
||||||
|
|
||||||
|
for _, row := range bodyRows {
|
||||||
|
if r.pdf.GetY()+row.height > pageH-bm {
|
||||||
r.pdf.AddPage()
|
r.pdf.AddPage()
|
||||||
renderHeaderRow(true)
|
renderHeader(true)
|
||||||
r.pdf.SetFont("Helvetica", "", dinFontCaption)
|
|
||||||
}
|
}
|
||||||
for _, col := range data[i] {
|
r.drawRow(row, numCols, colW, lineHt, false)
|
||||||
r.pdf.CellFormat(colW, rowH, r.tr(col), "1", 0, "L", false, 0, "")
|
|
||||||
}
|
|
||||||
r.pdf.Ln(-1)
|
|
||||||
}
|
}
|
||||||
r.pdf.Ln(dinSpaceAfterParagraph)
|
r.pdf.Ln(dinSpaceAfterParagraph)
|
||||||
}
|
}
|
||||||
@@ -212,6 +292,7 @@ func (r *IHKRenderer) RenderImage(path string, caption string) {
|
|||||||
|
|
||||||
info := r.ensureImageRegistered(path)
|
info := r.ensureImageRegistered(path)
|
||||||
if info == nil {
|
if info == nil {
|
||||||
|
log.Printf("warning: image not found, rendering placeholder: %q", path)
|
||||||
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
|
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
|
||||||
r.pdf.CellFormat(0, dinLineHtCaption, r.tr("[Bild nicht gefunden: "+path+"]"),
|
r.pdf.CellFormat(0, dinLineHtCaption, r.tr("[Bild nicht gefunden: "+path+"]"),
|
||||||
"1", 1, "C", false, 0, "")
|
"1", 1, "C", false, 0, "")
|
||||||
@@ -283,11 +364,13 @@ const (
|
|||||||
// │ 1 │ package main │
|
// │ 1 │ package main │
|
||||||
// │ 2 │ │
|
// │ 2 │ │
|
||||||
// │ 3 │ func main() { … } │
|
// │ 3 │ func main() { … } │
|
||||||
|
// │ 4 │ // long line wraps here … │
|
||||||
|
// │ │ … continuation │
|
||||||
// └──────┴──────────────────────────────────┘
|
// └──────┴──────────────────────────────────┘
|
||||||
//
|
//
|
||||||
|
// Lines that exceed the column width are wrapped. Continuation rows show
|
||||||
|
// an empty gutter so the source line number stays unambiguous.
|
||||||
// The language label is shown in small italic text above the block.
|
// The language label is shown in small italic text above the block.
|
||||||
// Lines that exceed the printable width are clipped — code should be
|
|
||||||
// formatted to reasonable lengths before conversion.
|
|
||||||
func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
|
func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
|
||||||
lines := strings.Split(strings.TrimRight(code, "\n"), "\n")
|
lines := strings.Split(strings.TrimRight(code, "\n"), "\n")
|
||||||
if len(lines) == 0 {
|
if len(lines) == 0 {
|
||||||
@@ -311,22 +394,33 @@ func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
|
|||||||
|
|
||||||
r.pdf.SetFont("Courier", "", codeFont)
|
r.pdf.SetFont("Courier", "", codeFont)
|
||||||
|
|
||||||
|
// In Courier every glyph has the same advance width; one measurement suffices.
|
||||||
|
// Reserve 1 mm right padding so characters never touch the column edge.
|
||||||
|
charW := r.pdf.GetStringWidth("0")
|
||||||
|
maxChars := int((codeW-1)/charW) - 1 // -1 for the leading space we prepend
|
||||||
|
if maxChars < 1 {
|
||||||
|
maxChars = 1
|
||||||
|
}
|
||||||
|
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
// Start a new page if this line would fall below the bottom margin.
|
chunks := wrapCodeLine(line, maxChars)
|
||||||
|
for ci, chunk := range chunks {
|
||||||
if r.pdf.GetY()+codeLineHt > pageH-bm {
|
if r.pdf.GetY()+codeLineHt > pageH-bm {
|
||||||
r.pdf.AddPage()
|
r.pdf.AddPage()
|
||||||
}
|
}
|
||||||
|
// Gutter: darker grey; line number on the first chunk, blank on continuations.
|
||||||
// Gutter: darker grey, right-aligned line number.
|
|
||||||
r.pdf.SetFillColor(218, 218, 218)
|
r.pdf.SetFillColor(218, 218, 218)
|
||||||
r.pdf.SetX(lm)
|
r.pdf.SetX(lm)
|
||||||
|
if ci == 0 {
|
||||||
r.pdf.CellFormat(codeGutterW, codeLineHt,
|
r.pdf.CellFormat(codeGutterW, codeLineHt,
|
||||||
fmt.Sprintf("%4d ", i+1), "", 0, "R", true, 0, "")
|
fmt.Sprintf("%4d ", i+1), "", 0, "R", true, 0, "")
|
||||||
|
} else {
|
||||||
// Code line: lighter grey, left-aligned, small leading space.
|
r.pdf.CellFormat(codeGutterW, codeLineHt, "", "", 0, "R", true, 0, "")
|
||||||
|
}
|
||||||
|
// Code column: lighter grey, left-aligned, small leading space.
|
||||||
r.pdf.SetFillColor(246, 246, 246)
|
r.pdf.SetFillColor(246, 246, 246)
|
||||||
r.pdf.CellFormat(codeW, codeLineHt,
|
r.pdf.CellFormat(codeW, codeLineHt, r.tr(" "+chunk), "", 1, "L", true, 0, "")
|
||||||
r.tr(" "+line), "", 1, "L", true, 0, "")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset colours so subsequent content is unaffected.
|
// Reset colours so subsequent content is unaffected.
|
||||||
@@ -334,6 +428,25 @@ func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
|
|||||||
r.pdf.Ln(dinSpaceAfterParagraph)
|
r.pdf.Ln(dinSpaceAfterParagraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapCodeLine splits a source code line into chunks of at most maxChars runes.
|
||||||
|
// A single-rune minimum prevents an infinite loop on extremely narrow columns.
|
||||||
|
func wrapCodeLine(line string, maxChars int) []string {
|
||||||
|
runes := []rune(line)
|
||||||
|
if len(runes) <= maxChars {
|
||||||
|
return []string{line}
|
||||||
|
}
|
||||||
|
var chunks []string
|
||||||
|
for len(runes) > 0 {
|
||||||
|
n := maxChars
|
||||||
|
if n > len(runes) {
|
||||||
|
n = len(runes)
|
||||||
|
}
|
||||||
|
chunks = append(chunks, string(runes[:n]))
|
||||||
|
runes = runes[n:]
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
// fontStyle returns the fpdf font style string for a given bold/italic combination.
|
// fontStyle returns the fpdf font style string for a given bold/italic combination.
|
||||||
func fontStyle(bold, italic bool) string {
|
func fontStyle(bold, italic bool) string {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
+106
-2
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/go-pdf/fpdf"
|
"github.com/go-pdf/fpdf"
|
||||||
@@ -126,8 +127,8 @@ func (r *IHKRenderer) RenderBibliography() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenderAppendices renders the appendix section followed by individual annex
|
// RenderAppendices renders the appendix section followed by individual annex
|
||||||
// pages. The opening page contains the index of all annexes. Each annex image
|
// pages. The opening page contains the index of all annexes. Annexes marked
|
||||||
// is scaled to fill the available page area.
|
// Landscape=true are placed on a landscape A4 page with 15 mm symmetric margins.
|
||||||
func (r *IHKRenderer) RenderAppendices() {
|
func (r *IHKRenderer) RenderAppendices() {
|
||||||
if len(r.appendices) == 0 {
|
if len(r.appendices) == 0 {
|
||||||
return
|
return
|
||||||
@@ -144,14 +145,41 @@ func (r *IHKRenderer) RenderAppendices() {
|
|||||||
r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
|
r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save portrait dimensions and standard margins for post-landscape restoration.
|
||||||
|
portraitW, portraitH := r.pdf.GetPageSize()
|
||||||
|
lm, tm, rm, _ := r.pdf.GetMargins()
|
||||||
|
|
||||||
|
const diagMargin = 15.0
|
||||||
|
|
||||||
// Individual annex pages
|
// Individual annex pages
|
||||||
for i, app := range r.appendices {
|
for i, app := range r.appendices {
|
||||||
|
if app.Landscape {
|
||||||
|
r.pdf.SetMargins(diagMargin, diagMargin, diagMargin)
|
||||||
|
r.pdf.SetAutoPageBreak(true, diagMargin)
|
||||||
|
// portraitH > portraitW → {Wd: portraitH, Ht: portraitW} = landscape 297×210.
|
||||||
|
r.pdf.AddPageFormat("L", fpdf.SizeType{Wd: portraitH, Ht: portraitW})
|
||||||
|
} else {
|
||||||
r.pdf.AddPage()
|
r.pdf.AddPage()
|
||||||
|
}
|
||||||
|
|
||||||
r.pdf.SetFont("Helvetica", "B", dinFontBody)
|
r.pdf.SetFont("Helvetica", "B", dinFontBody)
|
||||||
r.pdf.CellFormat(0, dinLineHtBody,
|
r.pdf.CellFormat(0, dinLineHtBody,
|
||||||
r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
|
r.tr(fmt.Sprintf("Anlage %d: %s", i+1, app.Title)), "", 1, "L", false, 0, "")
|
||||||
r.pdf.Ln(dinSpaceAfterHeading)
|
r.pdf.Ln(dinSpaceAfterHeading)
|
||||||
|
|
||||||
|
switch app.Kind {
|
||||||
|
case AppendixKindImage:
|
||||||
r.renderAppendixImage(app.Path)
|
r.renderAppendixImage(app.Path)
|
||||||
|
case AppendixKindTable:
|
||||||
|
// Render without numbering/recording; the annex header already identifies the table.
|
||||||
|
r.renderTableBody(app.TableData, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Landscape {
|
||||||
|
// Restore standard margins so the next AddPage() returns to portrait.
|
||||||
|
r.pdf.SetMargins(lm, tm, rm)
|
||||||
|
r.pdf.SetAutoPageBreak(true, dinMarginBottom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +262,79 @@ func (r *IHKRenderer) RenderGlossary() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderLandscapeDiagram renders an image on a dedicated landscape A4 page,
|
||||||
|
// scaled to fill the full printable area. A figure caption is added below the
|
||||||
|
// image and recorded in the Abbildungsverzeichnis.
|
||||||
|
//
|
||||||
|
// The page uses symmetric 15 mm margins (no Korrekturrand — examiners do not
|
||||||
|
// mark up diagram pages). After rendering, a fresh portrait page is opened so
|
||||||
|
// subsequent content continues in the correct orientation.
|
||||||
|
func (r *IHKRenderer) RenderLandscapeDiagram(path, caption string) {
|
||||||
|
// Current dimensions before the landscape page (portrait A4: 210 × 297 mm).
|
||||||
|
pw, ph := r.pdf.GetPageSize()
|
||||||
|
lm, tm, rm, _ := r.pdf.GetMargins()
|
||||||
|
|
||||||
|
const diagMargin = 15.0
|
||||||
|
|
||||||
|
// Apply symmetric margins before adding the landscape page so the page
|
||||||
|
// inherits them. The Korrekturrand (40 mm) is not needed on a diagram page.
|
||||||
|
r.pdf.SetMargins(diagMargin, diagMargin, diagMargin)
|
||||||
|
r.pdf.SetAutoPageBreak(true, diagMargin)
|
||||||
|
// ph > pw for portrait A4 → {Wd: ph, Ht: pw} = landscape 297 × 210 mm.
|
||||||
|
r.pdf.AddPageFormat("L", fpdf.SizeType{Wd: ph, Ht: pw})
|
||||||
|
|
||||||
|
captionH := dinLineHtCaption + 4
|
||||||
|
availW := ph - 2*diagMargin // 297 − 30 = 267 mm
|
||||||
|
availH := pw - 2*diagMargin - captionH - 2 // 210 − 30 − ~10 ≈ 170 mm
|
||||||
|
|
||||||
|
info := r.ensureImageRegistered(path)
|
||||||
|
if info == nil {
|
||||||
|
log.Printf("warning: landscape diagram image not found: %q", path)
|
||||||
|
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
|
||||||
|
r.pdf.CellFormat(0, dinLineHtCaption,
|
||||||
|
r.tr("[Bild nicht gefunden: "+path+"]"), "1", 1, "C", false, 0, "")
|
||||||
|
} else {
|
||||||
|
imgW := info.Width() * ptToMM
|
||||||
|
imgH := info.Height() * ptToMM
|
||||||
|
|
||||||
|
// Scale to fill available width, clamp to available height.
|
||||||
|
displayW := availW
|
||||||
|
displayH := imgH * (displayW / imgW)
|
||||||
|
if displayH > availH {
|
||||||
|
displayH = availH
|
||||||
|
displayW = imgW * (displayH / imgH)
|
||||||
|
if displayW > availW {
|
||||||
|
displayW = availW
|
||||||
|
displayH = imgH * (displayW / imgW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posX := diagMargin + (availW-displayW)/2
|
||||||
|
posY := diagMargin
|
||||||
|
|
||||||
|
r.pdf.ImageOptions(path, posX, posY, displayW, displayH, false,
|
||||||
|
fpdf.ImageOptions{ReadDpi: true}, 0, "")
|
||||||
|
r.pdf.SetY(posY + displayH + 2)
|
||||||
|
|
||||||
|
r.figureCount++
|
||||||
|
label := fmt.Sprintf("Abb. %d", r.figureCount)
|
||||||
|
if caption != "" {
|
||||||
|
label += ": " + caption
|
||||||
|
}
|
||||||
|
r.RecordFigure(label)
|
||||||
|
|
||||||
|
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
|
||||||
|
r.pdf.CellFormat(0, captionH, r.tr(label), "", 1, "C", false, 0, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore portrait margins and open a fresh portrait page.
|
||||||
|
// AddPage() always uses the default orientation ("P") set at construction,
|
||||||
|
// so subsequent content is guaranteed to be in portrait.
|
||||||
|
r.pdf.SetMargins(lm, tm, rm)
|
||||||
|
r.pdf.SetAutoPageBreak(true, dinMarginBottom)
|
||||||
|
r.pdf.AddPage()
|
||||||
|
}
|
||||||
|
|
||||||
// ensureImageRegistered registers an image with fpdf if not already known
|
// ensureImageRegistered registers an image with fpdf if not already known
|
||||||
// and returns its metadata. Returns nil if the image cannot be loaded.
|
// and returns its metadata. Returns nil if the image cannot be loaded.
|
||||||
func (r *IHKRenderer) ensureImageRegistered(path string) *fpdf.ImageInfoType {
|
func (r *IHKRenderer) ensureImageRegistered(path string) *fpdf.ImageInfoType {
|
||||||
@@ -242,6 +343,9 @@ func (r *IHKRenderer) ensureImageRegistered(path string) *fpdf.ImageInfoType {
|
|||||||
r.pdf.RegisterImageOptions(path, fpdf.ImageOptions{ReadDpi: true})
|
r.pdf.RegisterImageOptions(path, fpdf.ImageOptions{ReadDpi: true})
|
||||||
info = r.pdf.GetImageInfo(path)
|
info = r.pdf.GetImageInfo(path)
|
||||||
}
|
}
|
||||||
|
if info == nil {
|
||||||
|
log.Printf("warning: could not load image %q", path)
|
||||||
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+65
-3
@@ -4,6 +4,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -36,12 +37,28 @@ const (
|
|||||||
dinSpaceBeforeHeading = 2 * dinLineHtBody // ≈ 12.7 mm
|
dinSpaceBeforeHeading = 2 * dinLineHtBody // ≈ 12.7 mm
|
||||||
dinSpaceAfterHeading = dinLineHtBody // ≈ 6.35 mm
|
dinSpaceAfterHeading = dinLineHtBody // ≈ 6.35 mm
|
||||||
dinSpaceAfterParagraph = 4.0 // between body paragraphs
|
dinSpaceAfterParagraph = 4.0 // between body paragraphs
|
||||||
|
|
||||||
|
// List items use tighter line spacing than body text (1.2× instead of 1.5×).
|
||||||
|
// This keeps lists compact while remaining legible at 12 pt.
|
||||||
|
dinLineHtList = 5.0 // 12 pt × 1.2 ≈ 5.08 mm, rounded to 5.0
|
||||||
|
dinSpaceAfterList = 3.0 // gap inserted after the outermost list exits
|
||||||
)
|
)
|
||||||
|
|
||||||
// Appendix holds the title and image path for one annex entry.
|
// AppendixKind distinguishes between image and table annexes.
|
||||||
|
type AppendixKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AppendixKindImage AppendixKind = iota
|
||||||
|
AppendixKindTable
|
||||||
|
)
|
||||||
|
|
||||||
|
// Appendix holds the content for one annex entry.
|
||||||
type Appendix struct {
|
type Appendix struct {
|
||||||
|
Kind AppendixKind
|
||||||
|
Landscape bool // true → render on a landscape A4 page (15 mm symmetric margins)
|
||||||
Title string
|
Title string
|
||||||
Path string
|
Path string // image path (Kind == AppendixKindImage)
|
||||||
|
TableData [][]string // table rows (Kind == AppendixKindTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IHKRenderer is the central PDF generator for IHK Chemnitz project documentation.
|
// IHKRenderer is the central PDF generator for IHK Chemnitz project documentation.
|
||||||
@@ -139,18 +156,63 @@ func (r *IHKRenderer) AddSource(source string) {
|
|||||||
r.sources = append(r.sources, source)
|
r.sources = append(r.sources, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAppendix registers an annex entry in "Title | /path/to/image" format.
|
// AddAppendix registers an image annex in "Title | /path/to/image" format.
|
||||||
func (r *IHKRenderer) AddAppendix(titlePath string) {
|
func (r *IHKRenderer) AddAppendix(titlePath string) {
|
||||||
parts := strings.SplitN(titlePath, "|", 2)
|
parts := strings.SplitN(titlePath, "|", 2)
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
|
log.Printf("warning: @Anhang directive missing '|' separator: %q", titlePath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.appendices = append(r.appendices, Appendix{
|
r.appendices = append(r.appendices, Appendix{
|
||||||
|
Kind: AppendixKindImage,
|
||||||
Title: strings.TrimSpace(parts[0]),
|
Title: strings.TrimSpace(parts[0]),
|
||||||
Path: strings.TrimSpace(parts[1]),
|
Path: strings.TrimSpace(parts[1]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddTableAppendix registers a table annex entry.
|
||||||
|
func (r *IHKRenderer) AddTableAppendix(title string, data [][]string) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
log.Printf("warning: @TabelleAnhang %q has no table data — skipped", title)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.appendices = append(r.appendices, Appendix{
|
||||||
|
Kind: AppendixKindTable,
|
||||||
|
Title: title,
|
||||||
|
TableData: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLandscapeAppendix registers an image annex in "Title | /path/to/image" format
|
||||||
|
// that will be rendered on a landscape A4 page with 15 mm symmetric margins.
|
||||||
|
func (r *IHKRenderer) AddLandscapeAppendix(titlePath string) {
|
||||||
|
parts := strings.SplitN(titlePath, "|", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
log.Printf("warning: @AnhangBildQuer directive missing '|' separator: %q", titlePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.appendices = append(r.appendices, Appendix{
|
||||||
|
Kind: AppendixKindImage,
|
||||||
|
Landscape: true,
|
||||||
|
Title: strings.TrimSpace(parts[0]),
|
||||||
|
Path: strings.TrimSpace(parts[1]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTableAppendixLandscape registers a table annex entry rendered on a landscape page.
|
||||||
|
func (r *IHKRenderer) AddTableAppendixLandscape(title string, data [][]string) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
log.Printf("warning: @TabelleAnhangQuer %q has no table data — skipped", title)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.appendices = append(r.appendices, Appendix{
|
||||||
|
Kind: AppendixKindTable,
|
||||||
|
Landscape: true,
|
||||||
|
Title: title,
|
||||||
|
TableData: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Save writes the completed PDF to the given file path.
|
// Save writes the completed PDF to the given file path.
|
||||||
func (r *IHKRenderer) Save(filename string) error {
|
func (r *IHKRenderer) Save(filename string) error {
|
||||||
return r.pdf.OutputFileAndClose(filename)
|
return r.pdf.OutputFileAndClose(filename)
|
||||||
|
|||||||
Binary file not shown.
@@ -19,20 +19,25 @@ abbreviations:
|
|||||||
meaning: "Abstract Syntax Tree"
|
meaning: "Abstract Syntax Tree"
|
||||||
- abbr: "DIN"
|
- abbr: "DIN"
|
||||||
meaning: "Deutsches Institut für Normung"
|
meaning: "Deutsches Institut für Normung"
|
||||||
|
- abbr: "YAML"
|
||||||
|
meaning: "YAML Ain't Markup Language"
|
||||||
glossary:
|
glossary:
|
||||||
- term: "Goldmark"
|
- term: "Goldmark"
|
||||||
definition: "Ein in Go geschriebener Markdown-Parser, der den CommonMark-Standard implementiert."
|
definition: "Ein in Go geschriebener Markdown-Parser, der den CommonMark-Standard implementiert und durch Erweiterungen (Tabellen, YAML-Frontmatter) ergänzt werden kann."
|
||||||
- term: "FPDF"
|
- term: "FPDF"
|
||||||
definition: "Eine Go-Bibliothek zur Erzeugung von PDF-Dokumenten ohne externe Abhängigkeiten."
|
definition: "Eine Go-Bibliothek zur Erzeugung von PDF-Dokumenten ohne externe Systemabhängigkeiten."
|
||||||
- term: "Kroki"
|
- term: "Kroki"
|
||||||
definition: "Ein Webdienst, der verschiedene Diagramm-Beschreibungssprachen (Mermaid, PlantUML u.a.) in Bilder umwandelt."
|
definition: "Ein Webdienst, der verschiedene Diagramm-Beschreibungssprachen (Mermaid, PlantUML u.a.) serverseitig in Bilder umwandelt."
|
||||||
|
- term: "Zwei-Pass-Rendering"
|
||||||
|
definition: "Verfahren, bei dem ein Dokument zweimal gerendert wird: Der erste Durchlauf ermittelt Seitenzahlen, der zweite nutzt diese für das Inhaltsverzeichnis."
|
||||||
---
|
---
|
||||||
|
|
||||||
# Vorwort
|
# Vorwort
|
||||||
|
|
||||||
Dieses Projekt entstand im Rahmen der Abschlussprüfung zum Fachinformatiker
|
Dieses Projekt entstand im Rahmen der Abschlussprüfung zum Fachinformatiker
|
||||||
Fachrichtung Anwendungsentwicklung. Es soll zeigen, dass technische Dokumentationen
|
Fachrichtung Anwendungsentwicklung. Es soll zeigen, dass technische
|
||||||
effizient und normgerecht erstellt werden können.
|
Dokumentationen effizient und normgerecht erstellt werden können – ohne
|
||||||
|
manuelle Nachbearbeitung in einer Textverarbeitung.
|
||||||
|
|
||||||
# 1. Problemstellung
|
# 1. Problemstellung
|
||||||
|
|
||||||
@@ -41,70 +46,303 @@ effizient und normgerecht erstellt werden können.
|
|||||||
Aktuell müssen IHK-Dokumentationen mühsam in Word formatiert werden, was
|
Aktuell müssen IHK-Dokumentationen mühsam in Word formatiert werden, was
|
||||||
fehleranfällig ist und viel Zeit kostet. Besonders die Einhaltung der
|
fehleranfällig ist und viel Zeit kostet. Besonders die Einhaltung der
|
||||||
Formvorgaben – Schriftgröße, Zeilenabstand und Seitenränder – erfordert
|
Formvorgaben – Schriftgröße, Zeilenabstand und Seitenränder – erfordert
|
||||||
manuelle Sorgfalt bei jedem Absatz.
|
manuelle Sorgfalt bei jedem Absatz. Änderungen am Inhalt erzwingen
|
||||||
|
regelmäßig manuelle Korrekturen an der Formatierung.
|
||||||
|
|
||||||
## 1.2 Zielsetzung
|
## 1.2 Zielsetzung
|
||||||
|
|
||||||
Ziel ist ein Go-Tool, das **Markdown** in PDF umwandelt und dabei alle
|
Ziel ist ein Go-Tool, das **Markdown** in PDF umwandelt und dabei alle
|
||||||
formalen Anforderungen der IHK Chemnitz erfüllt. Es soll:
|
formalen Anforderungen der IHK Chemnitz automatisch erfüllt. Das Tool soll:
|
||||||
|
|
||||||
- die Prüfungsvorbereitung erleichtern, die Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen unddie Qualität der Dokumente *einheitlich* sicherstellen und
|
- die Prüfungsvorbereitung erleichtern,
|
||||||
- die Qualität der Dokumente *einheitlich* sicherstellen und
|
- die Qualität der Dokumente *einheitlich* sicherstellen und
|
||||||
- den Prozess vollständig automatisieren.
|
- den gesamten Formatierungsprozess vollständig automatisieren.
|
||||||
|
|
||||||
|
Besondere Anforderung ist die Einhaltung der **DIN 5008**-Norm sowie der
|
||||||
|
spezifischen Vorgaben der IHK Chemnitz (Korrekturrand 4 cm rechts,
|
||||||
|
Schriftart Arial/Helvetica 12 pt, 1½-zeiliger Abstand).
|
||||||
|
|
||||||
# 2. Projektablauf
|
# 2. Projektablauf
|
||||||
|
|
||||||
## 2.1 Planung
|
## 2.1 Planung und Zeitrahmen
|
||||||
|
|
||||||
Die Planung umfasst die Analyse der IHK-Vorgaben und das Design der
|
Die Planung umfasst die Analyse der IHK-Vorgaben und das Design der
|
||||||
Software-Architektur. Der Projektzeitraum beträgt maximal 80 Stunden
|
Software-Architektur. Der Projektzeitraum beträgt maximal 80 Stunden
|
||||||
gemäß Ausbildungsverordnung.
|
gemäß Ausbildungsverordnung.
|
||||||
|
|
||||||
### Architektur-Übersicht
|
@Tabelle: Projektphasen mit Zeitplanung
|
||||||
|
|
||||||
|
| Phase | Aufgabe | Stunden |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| 1 | Anforderungsanalyse und Recherche der IHK-Vorgaben sowie DIN 5008 Normen | 8 |
|
||||||
|
| 2 | Architekturentwurf und Auswahl geeigneter Go-Bibliotheken (Goldmark, FPDF) | 10 |
|
||||||
|
| 3 | Implementierung des Markdown-Parsers mit AST-Traversierung | 20 |
|
||||||
|
| 4 | Implementierung des PDF-Renderers mit allen IHK-Elementen | 24 |
|
||||||
|
| 5 | Tests, Fehlerkorrektur und Dokumentation | 12 |
|
||||||
|
| 6 | Puffer und Abnahme | 6 |
|
||||||
|
|
||||||
|
## 2.2 Architektur
|
||||||
|
|
||||||
|
Die Software ist in Go implementiert und folgt einer klaren Trennung
|
||||||
|
zwischen Parsing und Rendering. Das Zwei-Pass-Verfahren ermöglicht ein
|
||||||
|
korrektes Inhaltsverzeichnis mit Seitenzahlen.
|
||||||
|
|
||||||
|
@DiagrammQuer: Systemarchitektur des Konverters – Zwei-Pass-Rendering
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph LR
|
||||||
A[Markdown] --> B(Go Parser)
|
subgraph Eingabe
|
||||||
B --> C{Metadaten?}
|
MD[report.md]
|
||||||
C -->|Ja| D[Config]
|
CFG[YAML Frontmatter]
|
||||||
C -->|Nein| E[Standard]
|
end
|
||||||
D --> F[PDF Renderer]
|
subgraph Parsing
|
||||||
E --> F
|
P(ParseMarkdown)
|
||||||
F --> G[IHK-konformes PDF]
|
AST[Goldmark AST]
|
||||||
|
end
|
||||||
|
subgraph Pass1[Pass 1 - Seitenzahlen ermitteln]
|
||||||
|
R1[IHKRenderer]
|
||||||
|
TOC[tocItems mit Seitenzahlen]
|
||||||
|
end
|
||||||
|
subgraph Pass2[Pass 2 - Finales PDF]
|
||||||
|
R2[IHKRenderer]
|
||||||
|
PDF[projektarbeit.pdf]
|
||||||
|
end
|
||||||
|
MD --> P
|
||||||
|
CFG --> P
|
||||||
|
P --> AST
|
||||||
|
AST --> R1
|
||||||
|
R1 --> TOC
|
||||||
|
TOC --> R2
|
||||||
|
AST --> R2
|
||||||
|
R2 --> PDF
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2.2 Realisierung
|
### Modulstruktur
|
||||||
|
|
||||||
Die Realisierung erfolgt in Go unter Verwendung von `goldmark` und `fpdf`.
|
Der Konverter ist in neun fokussierte Go-Dateien aufgeteilt:
|
||||||
Der Konverter verarbeitet die Markdown-Datei in zwei Durchläufen, um das
|
|
||||||
Inhaltsverzeichnis korrekt mit Seitenangaben zu befüllen.
|
|
||||||
|
|
||||||
| Werkzeug | Zweck | Version |
|
- **`main.go`** – Einstiegspunkt, CLI-Flags, Zwei-Pass-Pipeline
|
||||||
|----------|-------|---------|
|
- **`config.go`** – YAML-Konfigurationsstruktur
|
||||||
| Go | Programmiersprache | 1.22+ |
|
- **`markdown_parser.go`** – Goldmark-AST-Traversierung
|
||||||
| Goldmark | Markdown-Parser (AST) | v1.8 |
|
- **`pdf_renderer.go`** – Kern-Struct `IHKRenderer`, DIN-5008-Konstanten
|
||||||
| FPDF | PDF-Erzeugung | v0.9 |
|
- **`pdf_content.go`** – Inhalts-Renderer (Absätze, Listen, Tabellen, Code, Bilder)
|
||||||
| Kroki | Diagramm-Rendering | online |
|
- **`pdf_toc.go`** – Verzeichnis-Renderer (Inhalts-, Tabellen-, Abbildungsverzeichnis)
|
||||||
|
- **`pdf_pages.go`** – Seiten-Renderer (Titelseite, Anhang, Erklärung, Glossar)
|
||||||
|
- **`pdf_numbering.go`** – Seitennummerierung (Römisch / Arabisch)
|
||||||
|
- **`diagram.go`** – Kroki-Diagramm-Rendering mit SHA-256-Cache
|
||||||
|
|
||||||
|
## 2.3 Realisierung
|
||||||
|
|
||||||
|
Die Realisierung erfolgt in Go unter Verwendung von `goldmark` für das
|
||||||
|
Markdown-Parsing und `fpdf` für die PDF-Erzeugung.
|
||||||
|
|
||||||
|
@Tabelle: Eingesetzte Bibliotheken und Werkzeuge
|
||||||
|
|
||||||
|
| Werkzeug | Zweck | Version | Lizenz |
|
||||||
|
|----------|-------|---------|--------|
|
||||||
|
| Go | Programmiersprache und Laufzeitumgebung | 1.22+ | BSD |
|
||||||
|
| Goldmark | Markdown-Parser mit CommonMark-konformem AST | v1.8 | MIT |
|
||||||
|
| goldmark-meta | YAML-Frontmatter-Erweiterung für Goldmark | v1.1 | MIT |
|
||||||
|
| FPDF | PDF-Erzeugung ohne externe Systemabhängigkeiten | v0.9 | MIT |
|
||||||
|
| Kroki | Diagramm-Rendering für Mermaid und PlantUML | online | Apache 2.0 |
|
||||||
|
|
||||||
@Quelle: Goldmark Documentation, https://github.com/yuin/goldmark, 2024
|
@Quelle: Goldmark Documentation, https://github.com/yuin/goldmark, 2024
|
||||||
@Quelle: Go-PDF/Fpdf Documentation, https://github.com/go-pdf/fpdf, 2025
|
@Quelle: Go-PDF/Fpdf Documentation, https://github.com/go-pdf/fpdf, 2025
|
||||||
|
|
||||||
## 2.3 Test und Qualitätssicherung
|
### Implementierungsbeispiel: Tabellen-Renderer
|
||||||
|
|
||||||
Das Tool wurde anhand eines Musterdokuments getestet. Folgende Kriterien
|
Der folgende Ausschnitt zeigt die Kernlogik des mehrzeiligen Tabellen-Renderers.
|
||||||
|
Jede Zelle wird mit `SplitLines` vorgemessen; anschließend werden alle Zellen
|
||||||
|
einer Zeile auf einheitliche Höhe gebracht:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (r *IHKRenderer) prepareRow(rawCells []string, numCols int,
|
||||||
|
colW, lineHt float64, bold bool) tableRowData {
|
||||||
|
|
||||||
|
r.pdf.SetFont("Helvetica", map[bool]string{true: "B", false: ""}[bold], dinFontCaption)
|
||||||
|
|
||||||
|
cells := make([][]string, numCols)
|
||||||
|
maxLines := 0
|
||||||
|
for j := 0; j < numCols; j++ {
|
||||||
|
raw := ""
|
||||||
|
if j < len(rawCells) {
|
||||||
|
raw = rawCells[j]
|
||||||
|
}
|
||||||
|
split := r.pdf.SplitLines([]byte(r.tr(raw)), colW-2)
|
||||||
|
lines := make([]string, len(split))
|
||||||
|
for k, b := range split {
|
||||||
|
lines[k] = string(b)
|
||||||
|
}
|
||||||
|
if len(lines) == 0 {
|
||||||
|
lines = []string{""}
|
||||||
|
}
|
||||||
|
cells[j] = lines
|
||||||
|
if len(lines) > maxLines {
|
||||||
|
maxLines = len(lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tableRowData{cells: cells, height: float64(maxLines) * lineHt}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.4 Test und Qualitätssicherung
|
||||||
|
|
||||||
|
Das Tool wurde anhand dieses Musterdokuments getestet. Folgende Kriterien
|
||||||
wurden geprüft:
|
wurden geprüft:
|
||||||
|
|
||||||
1. Korrekte Seitenränder gemäß DIN 5008
|
1. Korrekte Seitenränder gemäß DIN 5008 (links 3 cm, rechts 4 cm Korrekturrand)
|
||||||
2. Seitennummerierung (römisch im Vorspann, arabisch im Textteil)
|
2. Seitennummerierung – römisch im Vorspann (ab II), arabisch im Textteil (ab 1)
|
||||||
3. Schriftart Helvetica, 12 Punkt, 1½-zeilig
|
3. Schriftart Helvetica, 12 Punkt, 1½-zeiliger Abstand (6,35 mm Zeilenhöhe)
|
||||||
4. Automatische Abbildungs- und Tabellenverzeichnisse
|
4. Automatisch generierte Verzeichnisse (Inhalt, Tabellen, Abbildungen)
|
||||||
|
5. Mehrzeilige Tabellenzellen mit einheitlicher Zeilenhöhe pro Tabellenzeile
|
||||||
|
6. Nummerierende Code-Blöcke mit Zeilennummern-Gutter
|
||||||
|
7. Listen mit korrektem hängendem Einzug (zweite Zeile bündig mit Text)
|
||||||
|
8. Tabellen als Anhang über die Direktive `@TabelleAnhang:`
|
||||||
|
|
||||||
# 3. Zusammenfassung
|
@Tabelle: Testergebnisse der Qualitätssicherung
|
||||||
|
|
||||||
Das Tool ermöglicht eine effiziente Erstellung von IHK-Dokumentationen unter
|
| Kriterium | Erwartet | Ergebnis | Status |
|
||||||
Einhaltung **aller** Formatvorgaben. Durch die Trennung von Inhalt (Markdown)
|
|-----------|----------|----------|--------|
|
||||||
und Formatierung (Go-Renderer) ist eine konsistente Ausgabe garantiert.
|
| Linker Seitenrand | 30 mm | 30 mm | Bestanden |
|
||||||
|
| Rechter Seitenrand (Korrekturrand) | 40 mm | 40 mm | Bestanden |
|
||||||
|
| Schriftgröße Fließtext | 12 pt | 12 pt | Bestanden |
|
||||||
|
| Zeilenabstand Fließtext | 6,35 mm (1,5-fach) | 6,35 mm | Bestanden |
|
||||||
|
| Römische Nummerierung Vorspann | ab Seite II | ab Seite II | Bestanden |
|
||||||
|
| Arabische Nummerierung Textteil | ab Seite 1 | ab Seite 1 | Bestanden |
|
||||||
|
| Mehrzeilige Tabellenzellen | einheitliche Zeilenhöhe | einheitliche Zeilenhöhe | Bestanden |
|
||||||
|
|
||||||
|
# 3. Verwendung der Direktiven
|
||||||
|
|
||||||
|
## 3.1 Quellenangaben und Bibliografie
|
||||||
|
|
||||||
|
Quellenangaben werden mit der Direktive `@Quelle:` eingefügt und am Ende
|
||||||
|
des Dokuments alphabetisch sortiert im Literaturverzeichnis ausgegeben.
|
||||||
|
Die Direktive kann an beliebiger Stelle im Dokument stehen.
|
||||||
|
|
||||||
|
## 3.2 Benannte Tabellen
|
||||||
|
|
||||||
|
Mit `@Tabelle: Tabellenname` unmittelbar vor einer Markdown-Tabelle wird
|
||||||
|
der Tabelle ein Name gegeben. Dieser erscheint im Tabellenverzeichnis:
|
||||||
|
|
||||||
|
```
|
||||||
|
@Tabelle: Übersicht der Programmiersprachen
|
||||||
|
|
||||||
|
| Sprache | Paradigma | Typsystem |
|
||||||
|
|---------|-----------|-----------|
|
||||||
|
| Go | Imperativ | Statisch |
|
||||||
|
| Python | Multi | Dynamisch |
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.3 Tabellen als Anhang
|
||||||
|
|
||||||
|
Mit `@TabelleAnhang: Anlagenname` wird die folgende Tabelle nicht im
|
||||||
|
Fließtext gerendert, sondern als eigene Anlage im Anhang platziert:
|
||||||
|
|
||||||
|
```
|
||||||
|
@TabelleAnhang: Vollständige Fehlerliste
|
||||||
|
|
||||||
|
| Code | Beschreibung | Schwere |
|
||||||
|
|------|--------------|---------|
|
||||||
|
| E001 | Datei nicht gefunden | Kritisch |
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.4 Diagramme als Anhang
|
||||||
|
|
||||||
|
Mit `@AnhangUML:` gefolgt von einem Mermaid- oder PlantUML-Codeblock wird
|
||||||
|
das Diagramm als Anlage eingebettet statt im Fließtext:
|
||||||
|
|
||||||
|
```
|
||||||
|
@AnhangUML: Datenbankschema
|
||||||
|
|
||||||
|
\`\`\`plantuml
|
||||||
|
@startuml
|
||||||
|
entity User { ... }
|
||||||
|
@enduml
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
# 4. Zusammenfassung
|
||||||
|
|
||||||
|
Das Tool ermöglicht eine effiziente Erstellung von IHK-Dokumentationen
|
||||||
|
unter Einhaltung **aller** Formatvorgaben. Durch die Trennung von Inhalt
|
||||||
|
(Markdown) und Formatierung (Go-Renderer) ist eine konsistente Ausgabe
|
||||||
|
garantiert. Künftige Erweiterungen könnten Fußnoten, Querverweise und
|
||||||
|
eine konfigurierbare Spaltenbreite für Tabellen umfassen.
|
||||||
|
|
||||||
@Quelle: IHK Chemnitz, Hinweise zur Erarbeitung der Dokumentation über die Projektarbeit, 2020
|
@Quelle: IHK Chemnitz, Hinweise zur Erarbeitung der Dokumentation über die Projektarbeit, 2020
|
||||||
|
@Quelle: DIN 5008:2020-03, Schreib- und Gestaltungsregeln für die Textverarbeitung, Beuth Verlag
|
||||||
|
|
||||||
@Anhang: Architektur-Diagramm Übersicht | test.png
|
@TabelleAnhang: Vollständige Liste der unterstützten Markdown-Direktiven
|
||||||
|
|
||||||
|
| Direktive | Syntax | Beschreibung |
|
||||||
|
|-----------|--------|--------------|
|
||||||
|
| Quelle | @Quelle: Text | Fügt einen Literaturverweis hinzu, der alphabetisch im Literaturverzeichnis erscheint |
|
||||||
|
| Tabelle | @Tabelle: Name | Gibt der folgenden Tabelle einen Namen für das Tabellenverzeichnis |
|
||||||
|
| Tabellenanhang | @TabelleAnhang: Name | Sendet die folgende Tabelle als nummerierte Anlage in den Anhang |
|
||||||
|
| Bildanhang | @Anhang: Titel \| Pfad | Fügt ein Bild als nummerierte Anlage in den Anhang ein |
|
||||||
|
| Diagrammanhang | @AnhangUML: Titel | Rendert den folgenden Mermaid/PlantUML-Block als Anlage |
|
||||||
|
|
||||||
|
@AnhangUML: Systemarchitektur – Datenflussdiagramm
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
MD[report.md] -->|ParseMarkdown| AST[Goldmark AST]
|
||||||
|
AST -->|Pass 1| R1[IHKRenderer Pass 1]
|
||||||
|
R1 -->|tocItems| R2[IHKRenderer Pass 2]
|
||||||
|
AST -->|Pass 2| R2
|
||||||
|
R2 -->|Save| OUT[projektarbeit.pdf]
|
||||||
|
```
|
||||||
|
|
||||||
|
@AnhangUMLQuer: Vollständige Modulübersicht – Querformat
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph Eingabe
|
||||||
|
MD[report.md]
|
||||||
|
CFG[YAML Frontmatter]
|
||||||
|
end
|
||||||
|
subgraph Parser
|
||||||
|
MP[markdown_parser.go]
|
||||||
|
AST[Goldmark AST]
|
||||||
|
end
|
||||||
|
subgraph Renderer
|
||||||
|
IR[pdf_renderer.go]
|
||||||
|
IC[pdf_content.go]
|
||||||
|
IT[pdf_toc.go]
|
||||||
|
IP[pdf_pages.go]
|
||||||
|
IN[pdf_numbering.go]
|
||||||
|
end
|
||||||
|
subgraph Diagramme
|
||||||
|
DG[diagram.go]
|
||||||
|
KR[Kroki API]
|
||||||
|
end
|
||||||
|
subgraph Ausgabe
|
||||||
|
PDF[projektarbeit.pdf]
|
||||||
|
end
|
||||||
|
MD --> MP
|
||||||
|
CFG --> MP
|
||||||
|
MP --> AST
|
||||||
|
AST --> IR
|
||||||
|
IR --> IC
|
||||||
|
IR --> IT
|
||||||
|
IR --> IP
|
||||||
|
IR --> IN
|
||||||
|
IC --> DG
|
||||||
|
DG --> KR
|
||||||
|
KR --> DG
|
||||||
|
IP --> PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
@TabelleAnhangQuer: Vollständige Direktiven-Referenz im Querformat
|
||||||
|
|
||||||
|
| Direktive | Syntax | Beschreibung | Querformat |
|
||||||
|
|-----------|--------|--------------|------------|
|
||||||
|
| Quelle | @Quelle: Text | Literaturverweis, alphabetisch sortiert im Literaturverzeichnis | – |
|
||||||
|
| Tabelle | @Tabelle: Name | Gibt der nächsten Tabelle einen Namen für das Tabellenverzeichnis | – |
|
||||||
|
| Tabellenanhang | @TabelleAnhang: Name | Sendet die nächste Tabelle als Anlage in den Anhang | – |
|
||||||
|
| Tabellenanhang Quer | @TabelleAnhangQuer: Name | Tabelle als Anlage im Querformat (297×210 mm, 15 mm Rand) | Ja |
|
||||||
|
| Bildanhang | @Anhang: Titel \| Pfad | Bild als nummerierte Anlage (Hochformat) | – |
|
||||||
|
| Bildanhang Quer | @AnhangBildQuer: Titel \| Pfad | Bild als nummerierte Anlage im Querformat | Ja |
|
||||||
|
| Diagrammanhang | @AnhangUML: Titel | Nächster Mermaid/PlantUML-Block als Anlage (Hochformat) | – |
|
||||||
|
| Diagrammanhang Quer | @AnhangUMLQuer: Titel | Nächster Mermaid/PlantUML-Block als Anlage im Querformat | Ja |
|
||||||
|
| Diagramm Querseite | @DiagrammQuer: Titel | Nächster Mermaid/PlantUML-Block inline als Querformat-Seite | – |
|
||||||
|
|||||||
Reference in New Issue
Block a user