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:
Generated
+5
-6
@@ -4,14 +4,12 @@
|
||||
<option name="autoReloadType" value="ALL" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<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" />
|
||||
<list default="true" id="c64f46d5-641a-468c-8fc1-94edec1f2deb" name="Changes" comment="Add support for code appendices: enable code blocks with language labels, line-number gutter, and directive-based integration.">
|
||||
<change afterPath="$PROJECT_DIR$/real_report.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/MarkdownToIHKChemnits" beforeDir="false" afterPath="$PROJECT_DIR$/MarkdownToIHKChemnits" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/markdown_parser.go" beforeDir="false" afterPath="$PROJECT_DIR$/markdown_parser.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_content.go" beforeDir="false" afterPath="$PROJECT_DIR$/pdf_content.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$/report.md" beforeDir="false" afterPath="$PROJECT_DIR$/report.md" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -102,7 +100,8 @@
|
||||
<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" />
|
||||
<MESSAGE value="Add support for code appendices: enable code blocks with language labels, line-number gutter, and directive-based integration." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Add support for code appendices: enable code blocks with language labels, line-number gutter, and directive-based integration." />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
|
||||
Binary file not shown.
+3
-3
@@ -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)
|
||||
}
|
||||
|
||||
+115
-31
@@ -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
|
||||
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)
|
||||
maxLines := 0
|
||||
for j := 0; j < numCols; j++ {
|
||||
raw := ""
|
||||
if j < len(rawCells) {
|
||||
raw = rawCells[j]
|
||||
type token struct {
|
||||
text string
|
||||
bold bool
|
||||
italic bool
|
||||
code bool
|
||||
}
|
||||
split := r.pdf.SplitLines([]byte(r.tr(raw)), colW-2)
|
||||
lines := make([]string, len(split))
|
||||
for k, b := range split {
|
||||
lines[k] = string(b)
|
||||
|
||||
// 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 = []string{""}
|
||||
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++ {
|
||||
var spans []InlineSpan
|
||||
if j < len(rawCells) {
|
||||
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
|
||||
}
|
||||
|
||||
+3
-3
@@ -57,7 +57,7 @@ type Appendix struct {
|
||||
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)
|
||||
TableData [][][]InlineSpan // table rows (Kind == AppendixKindTable)
|
||||
Lang string // language label (Kind == AppendixKindCode)
|
||||
Code string // source code (Kind == AppendixKindCode)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user