Add support for appendices: landscape diagrams, tables, and images; implement Kroki URL configurability; enhance directive parsing logic.
This commit is contained in:
+169
-56
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -111,7 +112,7 @@ func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, in
|
||||
|
||||
// Render bullet/number; CellFormat advances X to textX automatically.
|
||||
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
|
||||
for _, s := range spans {
|
||||
@@ -126,7 +127,7 @@ func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, in
|
||||
for _, s := range spans {
|
||||
text += s.Text
|
||||
}
|
||||
r.pdf.MultiCell(textW, dinLineHtBody, r.tr(text), "", "L", false)
|
||||
r.pdf.MultiCell(textW, dinLineHtList, r.tr(text), "", "L", false)
|
||||
} else {
|
||||
for _, span := range spans {
|
||||
style := fontStyle(span.Bold, span.Italic)
|
||||
@@ -135,70 +136,149 @@ func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, in
|
||||
} else {
|
||||
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
|
||||
// a auto-repeat header on page breaks. Each table is numbered and recorded
|
||||
// in the Tabellenverzeichnis per IHK section 2.4.
|
||||
//
|
||||
// data[0] is the header row; data[1:] are body rows.
|
||||
// tableRowData holds pre-computed line-wrapped content for one table row.
|
||||
type tableRowData struct {
|
||||
cells [][]string // wrapped lines per cell
|
||||
height float64 // total row height in mm
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
r.tableCount++
|
||||
label := fmt.Sprintf("Tab. %d", r.tableCount)
|
||||
if caption != "" {
|
||||
label += ": " + caption
|
||||
}
|
||||
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]
|
||||
numCols := len(header)
|
||||
uw := r.usableWidth()
|
||||
colW := uw / float64(numCols)
|
||||
|
||||
// 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)
|
||||
if numCols == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
renderHeaderRow(false)
|
||||
|
||||
r.pdf.SetFont("Helvetica", "", dinFontCaption)
|
||||
r.pdf.SetFillColor(255, 255, 255)
|
||||
uw := r.usableWidth()
|
||||
colW := uw / float64(numCols)
|
||||
lineHt := dinLineHtCaption + 2
|
||||
|
||||
_, pageH := r.pdf.GetPageSize()
|
||||
_, _, _, 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++ {
|
||||
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()
|
||||
renderHeaderRow(true)
|
||||
r.pdf.SetFont("Helvetica", "", dinFontCaption)
|
||||
renderHeader(true)
|
||||
}
|
||||
for _, col := range data[i] {
|
||||
r.pdf.CellFormat(colW, rowH, r.tr(col), "1", 0, "L", false, 0, "")
|
||||
}
|
||||
r.pdf.Ln(-1)
|
||||
r.drawRow(row, numCols, colW, lineHt, false)
|
||||
}
|
||||
r.pdf.Ln(dinSpaceAfterParagraph)
|
||||
}
|
||||
@@ -212,6 +292,7 @@ func (r *IHKRenderer) RenderImage(path string, caption string) {
|
||||
|
||||
info := r.ensureImageRegistered(path)
|
||||
if info == nil {
|
||||
log.Printf("warning: image not found, rendering placeholder: %q", path)
|
||||
r.pdf.SetFont("Helvetica", "I", dinFontCaption)
|
||||
r.pdf.CellFormat(0, dinLineHtCaption, r.tr("[Bild nicht gefunden: "+path+"]"),
|
||||
"1", 1, "C", false, 0, "")
|
||||
@@ -283,11 +364,13 @@ const (
|
||||
// │ 1 │ package main │
|
||||
// │ 2 │ │
|
||||
// │ 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.
|
||||
// Lines that exceed the printable width are clipped — code should be
|
||||
// formatted to reasonable lengths before conversion.
|
||||
func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
|
||||
lines := strings.Split(strings.TrimRight(code, "\n"), "\n")
|
||||
if len(lines) == 0 {
|
||||
@@ -311,22 +394,33 @@ func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
|
||||
|
||||
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 {
|
||||
// Start a new page if this line would fall below the bottom margin.
|
||||
if r.pdf.GetY()+codeLineHt > pageH-bm {
|
||||
r.pdf.AddPage()
|
||||
chunks := wrapCodeLine(line, maxChars)
|
||||
for ci, chunk := range chunks {
|
||||
if r.pdf.GetY()+codeLineHt > pageH-bm {
|
||||
r.pdf.AddPage()
|
||||
}
|
||||
// Gutter: darker grey; line number on the first chunk, blank on continuations.
|
||||
r.pdf.SetFillColor(218, 218, 218)
|
||||
r.pdf.SetX(lm)
|
||||
if ci == 0 {
|
||||
r.pdf.CellFormat(codeGutterW, codeLineHt,
|
||||
fmt.Sprintf("%4d ", i+1), "", 0, "R", true, 0, "")
|
||||
} else {
|
||||
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.CellFormat(codeW, codeLineHt, r.tr(" "+chunk), "", 1, "L", true, 0, "")
|
||||
}
|
||||
|
||||
// Gutter: darker grey, right-aligned line number.
|
||||
r.pdf.SetFillColor(218, 218, 218)
|
||||
r.pdf.SetX(lm)
|
||||
r.pdf.CellFormat(codeGutterW, codeLineHt,
|
||||
fmt.Sprintf("%4d ", i+1), "", 0, "R", true, 0, "")
|
||||
|
||||
// Code line: lighter grey, left-aligned, small leading space.
|
||||
r.pdf.SetFillColor(246, 246, 246)
|
||||
r.pdf.CellFormat(codeW, codeLineHt,
|
||||
r.tr(" "+line), "", 1, "L", true, 0, "")
|
||||
}
|
||||
|
||||
// Reset colours so subsequent content is unaffected.
|
||||
@@ -334,6 +428,25 @@ func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
|
||||
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.
|
||||
func fontStyle(bold, italic bool) string {
|
||||
switch {
|
||||
|
||||
Reference in New Issue
Block a user