Add support for code appendices: enable code blocks with language labels, line-number gutter, and directive-based integration.

This commit is contained in:
Sebastian Unterschütz
2026-05-14 21:00:58 +02:00
parent 3afdc3bf1e
commit 2d3e544d4f
7 changed files with 76 additions and 21 deletions
+6 -7
View File
@@ -4,14 +4,10 @@
<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="Refactor PDF content rendering: improve list indentation logic, add numbered code block rendering with gutter, and update text wrapping alignment."> <list default="true" id="c64f46d5-641a-468c-8fc1-94edec1f2deb" name="Changes" comment="Update MarkdownToIHKChemnitz: modify core functionality for improved PDF rendering">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <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_pages.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_pages.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$/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" />
@@ -23,7 +19,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="22" /> <option name="cachedIndexableFilesCount" value="23" />
<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" />
@@ -103,7 +99,10 @@
<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." />
<MESSAGE value="Refactor PDF content rendering: improve list indentation logic, add numbered code block rendering with gutter, and update text wrapping alignment." /> <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" /> <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" /> <MESSAGE value="Add new features: diagram rendering via Kroki, table handling improvements including appendices, and Docker Compose setup for self-hosted Kroki." />
<MESSAGE value="Add support for appendices: landscape diagrams, tables, and images; implement Kroki URL configurability; enhance directive parsing logic." />
<MESSAGE value="Update MarkdownToIHKChemnitz: modify core functionality for improved PDF rendering" />
<option name="LAST_COMMIT_MESSAGE" value="Update MarkdownToIHKChemnitz: modify core functionality for improved PDF rendering" />
</component> </component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>
Binary file not shown.
+21 -9
View File
@@ -44,15 +44,17 @@ 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 nextAppendixLandscape bool // set by @AnhangUMLQuer: — landscape for diagram appendix
appendixTitle string appendixTitle string
nextTableCaption string // set by @Tabelle: directive nextCodeBlockAppendix bool // set by @AnhangCode: — next non-diagram code block → appendix
nextTableIsAppendix bool // set by @TabelleAnhang: or @TabelleAnhangQuer: codeBlockAppendixTitle string
nextTableIsLandscape bool // set by @TabelleAnhangQuer: nextTableCaption string // set by @Tabelle: directive
nextDiagramLandscape bool // set by @DiagrammQuer: directive nextTableIsAppendix bool // set by @TabelleAnhang: or @TabelleAnhangQuer:
nextDiagramCaption string // caption for the landscape diagram page nextTableIsLandscape bool // set by @TabelleAnhangQuer:
listStack []listFrame // stack for nested list tracking nextDiagramLandscape bool // set by @DiagrammQuer: directive
nextDiagramCaption string // caption for the landscape diagram page
listStack []listFrame // stack for nested list tracking
} }
// listFrame tracks the type and item counter for one list nesting level. // listFrame tracks the type and item counter for one list nesting level.
@@ -142,6 +144,12 @@ func RenderAST(doc ast.Node, content []byte, r *IHKRenderer) error {
} }
// Fall through: render as plain code block on error // Fall through: render as plain code block on error
} }
if state.nextCodeBlockAppendix {
r.AddCodeAppendix(state.codeBlockAppendixTitle, lang, code)
state.nextCodeBlockAppendix = false
state.codeBlockAppendixTitle = ""
return ast.WalkSkipChildren, nil
}
// Render as a numbered code block (gutter + monospace body). // Render as a numbered code block (gutter + monospace body).
r.RenderCodeBlock(lang, code) r.RenderCodeBlock(lang, code)
return ast.WalkSkipChildren, nil return ast.WalkSkipChildren, nil
@@ -264,6 +272,10 @@ func handleDirectives(text string, state *parserState, r *IHKRenderer) bool {
case strings.HasPrefix(line, "@Quelle:"): case strings.HasPrefix(line, "@Quelle:"):
r.AddSource(strings.TrimSpace(strings.TrimPrefix(line, "@Quelle:"))) r.AddSource(strings.TrimSpace(strings.TrimPrefix(line, "@Quelle:")))
handled = true handled = true
case strings.HasPrefix(line, "@AnhangCode:"):
state.nextCodeBlockAppendix = true
state.codeBlockAppendixTitle = strings.TrimSpace(strings.TrimPrefix(line, "@AnhangCode:"))
handled = true
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
+2 -1
View File
@@ -171,8 +171,9 @@ func (r *IHKRenderer) RenderAppendices() {
case AppendixKindImage: case AppendixKindImage:
r.renderAppendixImage(app.Path) r.renderAppendixImage(app.Path)
case AppendixKindTable: case AppendixKindTable:
// Render without numbering/recording; the annex header already identifies the table.
r.renderTableBody(app.TableData, "") r.renderTableBody(app.TableData, "")
case AppendixKindCode:
r.RenderCodeBlock(app.Lang, app.Code)
} }
if app.Landscape { if app.Landscape {
+15 -4
View File
@@ -38,10 +38,8 @@ const (
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×). dinLineHtList = dinLineHtBody // 1.5× IHK standard, same as body text
// This keeps lists compact while remaining legible at 12 pt. dinSpaceAfterList = 3.0 // gap inserted after the outermost list exits
dinLineHtList = 5.0 // 12 pt × 1.2 ≈ 5.08 mm, rounded to 5.0
dinSpaceAfterList = 3.0 // gap inserted after the outermost list exits
) )
// AppendixKind distinguishes between image and table annexes. // AppendixKind distinguishes between image and table annexes.
@@ -50,6 +48,7 @@ type AppendixKind int
const ( const (
AppendixKindImage AppendixKind = iota AppendixKindImage AppendixKind = iota
AppendixKindTable AppendixKindTable
AppendixKindCode
) )
// Appendix holds the content for one annex entry. // Appendix holds the content for one annex entry.
@@ -59,6 +58,8 @@ type Appendix struct {
Title string Title string
Path string // image path (Kind == AppendixKindImage) Path string // image path (Kind == AppendixKindImage)
TableData [][]string // table rows (Kind == AppendixKindTable) TableData [][]string // table rows (Kind == AppendixKindTable)
Lang string // language label (Kind == AppendixKindCode)
Code string // source code (Kind == AppendixKindCode)
} }
// IHKRenderer is the central PDF generator for IHK Chemnitz project documentation. // IHKRenderer is the central PDF generator for IHK Chemnitz project documentation.
@@ -183,6 +184,16 @@ func (r *IHKRenderer) AddTableAppendix(title string, data [][]string) {
}) })
} }
// AddCodeAppendix registers a source-code annex rendered with line-number gutter.
func (r *IHKRenderer) AddCodeAppendix(title, lang, code string) {
r.appendices = append(r.appendices, Appendix{
Kind: AppendixKindCode,
Title: title,
Lang: lang,
Code: code,
})
}
// AddLandscapeAppendix registers an image annex in "Title | /path/to/image" format // 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. // that will be rendered on a landscape A4 page with 15 mm symmetric margins.
func (r *IHKRenderer) AddLandscapeAppendix(titlePath string) { func (r *IHKRenderer) AddLandscapeAppendix(titlePath string) {
BIN
View File
Binary file not shown.
+32
View File
@@ -282,6 +282,38 @@ eine konfigurierbare Spaltenbreite für Tabellen umfassen.
| Bildanhang | @Anhang: Titel \| Pfad | Fügt ein Bild als nummerierte Anlage in den Anhang ein | | 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 | | Diagrammanhang | @AnhangUML: Titel | Rendert den folgenden Mermaid/PlantUML-Block als Anlage |
@AnhangCode: Implementierung Tabellen-Renderer (prepareRow)
```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}
}
```
@AnhangUML: Systemarchitektur Datenflussdiagramm @AnhangUML: Systemarchitektur Datenflussdiagramm
```mermaid ```mermaid