diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index c54043f..25b9f81 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,14 +4,12 @@
-
-
+
+
-
+
-
-
@@ -102,7 +100,8 @@
-
+
+
diff --git a/MarkdownToIHKChemnits b/MarkdownToIHKChemnits
index 5281f2e..09bc7cc 100755
Binary files a/MarkdownToIHKChemnits and b/MarkdownToIHKChemnits differ
diff --git a/markdown_parser.go b/markdown_parser.go
index be73a5d..34c5fd3 100644
--- a/markdown_parser.go
+++ b/markdown_parser.go
@@ -220,11 +220,11 @@ func RenderAST(doc ast.Node, content []byte, r *IHKRenderer) error {
if !entering {
return ast.WalkContinue, nil
}
- var tableData [][]string
+ var tableData [][][]InlineSpan
for row := node.FirstChild(); row != nil; row = row.NextSibling() {
- var rowData []string
+ var rowData [][]InlineSpan
for cell := row.FirstChild(); cell != nil; cell = cell.NextSibling() {
- rowData = append(rowData, extractPlainText(cell, content))
+ rowData = append(rowData, extractInlineSpans(cell, content))
}
tableData = append(tableData, rowData)
}
diff --git a/pdf_content.go b/pdf_content.go
index 7dddadd..3eab56d 100644
--- a/pdf_content.go
+++ b/pdf_content.go
@@ -144,34 +144,102 @@ func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, in
// 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
+ cells [][][]InlineSpan // wrapped lines per cell; each line is a []InlineSpan
+ 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"
+// wrapCellSpans splits a []InlineSpan into lines that fit within maxWidth mm.
+// If forceBold is true every span is rendered bold (used for header rows).
+func (r *IHKRenderer) wrapCellSpans(spans []InlineSpan, maxWidth float64, forceBold bool) [][]InlineSpan {
+ if len(spans) == 0 {
+ return [][]InlineSpan{{}}
}
- r.pdf.SetFont("Helvetica", style, dinFontCaption)
- cells := make([][]string, numCols)
+ type token struct {
+ text string
+ bold bool
+ italic bool
+ code bool
+ }
+
+ // Flatten spans into space-split tokens so we can wrap word-by-word.
+ var tokens []token
+ for _, sp := range spans {
+ b := sp.Bold || forceBold
+ parts := strings.Split(sp.Text, " ")
+ for i, part := range parts {
+ if i > 0 {
+ tokens = append(tokens, token{" ", false, false, false})
+ }
+ if part != "" {
+ tokens = append(tokens, token{part, b, sp.Italic, sp.Code})
+ }
+ }
+ }
+
+ // Measure each token with its correct font.
+ widths := make([]float64, len(tokens))
+ for i, tok := range tokens {
+ if tok.code {
+ r.pdf.SetFont("Courier", "", dinFontCaption)
+ } else {
+ r.pdf.SetFont("Helvetica", fontStyle(tok.bold, tok.italic), dinFontCaption)
+ }
+ widths[i] = r.pdf.GetStringWidth(tok.text)
+ }
+
+ // Greedily pack tokens into lines, merging consecutive tokens with equal style.
+ var lines [][]InlineSpan
+ var curLine []InlineSpan
+ curW := 0.0
+
+ pushToken := func(tok token) {
+ if len(curLine) > 0 {
+ last := &curLine[len(curLine)-1]
+ if last.Bold == tok.bold && last.Italic == tok.italic && last.Code == tok.code {
+ last.Text += tok.text
+ return
+ }
+ }
+ curLine = append(curLine, InlineSpan{Text: tok.text, Bold: tok.bold, Italic: tok.italic, Code: tok.code})
+ }
+
+ for i, tok := range tokens {
+ w := widths[i]
+ if tok.text == " " && curW == 0 {
+ continue // skip leading space on a fresh line
+ }
+ if curW+w > maxWidth && curW > 0 {
+ lines = append(lines, curLine)
+ curLine = nil
+ curW = 0
+ if tok.text == " " {
+ continue // discard the space that triggered the wrap
+ }
+ }
+ pushToken(tok)
+ curW += w
+ }
+ if len(curLine) > 0 {
+ lines = append(lines, curLine)
+ }
+ if len(lines) == 0 {
+ lines = [][]InlineSpan{{}}
+ }
+ return lines
+}
+
+// prepareRow wraps each cell's inline spans and returns a tableRowData.
+// If bold is true, all cell text is forced bold (used for the header row).
+func (r *IHKRenderer) prepareRow(rawCells [][]InlineSpan, numCols int, colW, lineHt float64, bold bool) tableRowData {
+ cells := make([][][]InlineSpan, numCols)
maxLines := 0
for j := 0; j < numCols; j++ {
- raw := ""
+ var spans []InlineSpan
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{""}
+ spans = rawCells[j]
}
+ lines := r.wrapCellSpans(spans, colW-2, bold)
cells[j] = lines
if len(lines) > maxLines {
maxLines = len(lines)
@@ -184,17 +252,13 @@ func (r *IHKRenderer) prepareRow(rawCells []string, numCols int, colW, lineHt fl
}
// 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.
+// Cell borders are drawn as rectangles so all cells share a uniform height.
+// Header cells are rendered bold and centred; body cells honour per-span formatting.
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)
@@ -206,12 +270,32 @@ func (r *IHKRenderer) drawRow(row tableRowData, numCols int, colW, lineHt float6
}
// 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, "")
+ for k, spanLine := range cellLines {
+ y := startY + float64(k)*lineHt
+ if isHeader {
+ // Header: plain bold text, centred.
+ plainText := ""
+ for _, s := range spanLine {
+ plainText += s.Text
+ }
+ r.pdf.SetFont("Helvetica", "B", dinFontCaption)
+ r.pdf.SetXY(x+1, y)
+ r.pdf.CellFormat(colW-2, lineHt, r.tr(plainText), "", 0, "C", false, 0, "")
+ } else {
+ // Body: render each span with its own font, left-aligned.
+ r.pdf.SetXY(x+1, y)
+ for _, span := range spanLine {
+ if span.Code {
+ r.pdf.SetFont("Courier", "", dinFontCaption)
+ } else {
+ r.pdf.SetFont("Helvetica", fontStyle(span.Bold, span.Italic), dinFontCaption)
+ }
+ sw := r.pdf.GetStringWidth(r.tr(span.Text))
+ r.pdf.CellFormat(sw, lineHt, r.tr(span.Text), "", 0, "L", false, 0, "")
+ }
+ }
}
}
r.pdf.SetY(startY + row.height)
@@ -220,7 +304,7 @@ func (r *IHKRenderer) drawRow(row tableRowData, numCols int, colW, lineHt float6
// 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 [][][]InlineSpan, caption string) {
if len(data) == 0 {
return
}
@@ -235,7 +319,7 @@ func (r *IHKRenderer) RenderTable(data [][]string, caption string) {
// 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) {
+func (r *IHKRenderer) renderTableBody(data [][][]InlineSpan, label string) {
if len(data) == 0 {
return
}
diff --git a/pdf_renderer.go b/pdf_renderer.go
index f426e7e..c9c1828 100644
--- a/pdf_renderer.go
+++ b/pdf_renderer.go
@@ -56,10 +56,10 @@ type Appendix struct {
Kind AppendixKind
Landscape bool // true → render on a landscape A4 page (15 mm symmetric margins)
Title string
- Path string // image path (Kind == AppendixKindImage)
- TableData [][]string // table rows (Kind == AppendixKindTable)
- Lang string // language label (Kind == AppendixKindCode)
- Code string // source code (Kind == AppendixKindCode)
+ Path string // image path (Kind == AppendixKindImage)
+ TableData [][][]InlineSpan // 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.
@@ -172,7 +172,7 @@ func (r *IHKRenderer) AddAppendix(titlePath string) {
}
// AddTableAppendix registers a table annex entry.
-func (r *IHKRenderer) AddTableAppendix(title string, data [][]string) {
+func (r *IHKRenderer) AddTableAppendix(title string, data [][][]InlineSpan) {
if len(data) == 0 {
log.Printf("warning: @TabelleAnhang %q has no table data — skipped", title)
return
@@ -211,7 +211,7 @@ func (r *IHKRenderer) AddLandscapeAppendix(titlePath string) {
}
// AddTableAppendixLandscape registers a table annex entry rendered on a landscape page.
-func (r *IHKRenderer) AddTableAppendixLandscape(title string, data [][]string) {
+func (r *IHKRenderer) AddTableAppendixLandscape(title string, data [][][]InlineSpan) {
if len(data) == 0 {
log.Printf("warning: @TabelleAnhangQuer %q has no table data — skipped", title)
return