Refactor table rendering: replace plain text with InlineSpan for rich text support, update row preparation, and improve PDF formatting logic.
This commit is contained in:
+116
-32
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user