vendor/github.com/olekukonko/tablewriter/renderer/svg.go
package renderer
import (
"fmt"
"html"
"io"
"strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw"
)
// SVGConfig holds configuration for the SVG renderer.
// Fields include font, colors, padding, and merge rendering options.
// Used to customize SVG output appearance and behavior.
type SVGConfig struct {
FontFamily string // e.g., "Arial, sans-serif"
FontSize float64 // Base font size in SVG units
LineHeightFactor float64 // Factor for line height (e.g., 1.2)
Padding float64 // Padding inside cells
StrokeWidth float64 // Line width for borders
StrokeColor string // Color for strokes (e.g., "black")
HeaderBG string // Background color for header
RowBG string // Background color for rows
RowAltBG string // Alternating row background color
FooterBG string // Background color for footer
HeaderColor string // Text color for header
RowColor string // Text color for rows
FooterColor string // Text color for footer
ApproxCharWidthFactor float64 // Char width relative to FontSize
MinColWidth float64 // Minimum column width
RenderTWConfigOverrides bool // Override SVG alignments with tablewriter
Debug bool // Enable debug logging
ScaleFactor float64 // Scaling factor for SVG
}
// SVG implements tw.Renderer for SVG output.
// Manages SVG element generation and merge tracking.
type SVG struct {
config SVGConfig
trace []string
allVisualLineData [][][]string // [section][line][cell]
allVisualLineCtx [][]tw.Formatting // [section][line]Formatting
maxCols int
calculatedColWidths []float64
svgElements strings.Builder
currentY float64
dataRowCounter int
vMergeTrack map[int]int // Tracks vertical merge spans
numVisualRowsDrawn int
logger *ll.Logger
w io.Writer
}
const (
sectionTypeHeader = 0
sectionTypeRow = 1
sectionTypeFooter = 2
)
// NewSVG creates a new SVG renderer with configuration.
// Parameter configs provides optional SVGConfig; defaults used if empty.
// Returns a configured SVG instance.
func NewSVG(configs ...SVGConfig) *SVG {
cfg := SVGConfig{
FontFamily: "sans-serif",
FontSize: 12.0,
LineHeightFactor: 1.4,
Padding: 5.0,
StrokeWidth: 1.0,
StrokeColor: "black",
HeaderBG: "#F0F0F0",
RowBG: "white",
RowAltBG: "#F9F9F9",
FooterBG: "#F0F0F0",
HeaderColor: "black",
RowColor: "black",
FooterColor: "black",
ApproxCharWidthFactor: 0.6,
MinColWidth: 30.0,
ScaleFactor: 1.0,
RenderTWConfigOverrides: true,
Debug: false,
}
if len(configs) > 0 {
userCfg := configs[0]
if userCfg.FontFamily != tw.Empty {
cfg.FontFamily = userCfg.FontFamily
}
if userCfg.FontSize > 0 {
cfg.FontSize = userCfg.FontSize
}
if userCfg.LineHeightFactor > 0 {
cfg.LineHeightFactor = userCfg.LineHeightFactor
}
if userCfg.Padding >= 0 {
cfg.Padding = userCfg.Padding
}
if userCfg.StrokeWidth > 0 {
cfg.StrokeWidth = userCfg.StrokeWidth
}
if userCfg.StrokeColor != tw.Empty {
cfg.StrokeColor = userCfg.StrokeColor
}
if userCfg.HeaderBG != tw.Empty {
cfg.HeaderBG = userCfg.HeaderBG
}
if userCfg.RowBG != tw.Empty {
cfg.RowBG = userCfg.RowBG
}
cfg.RowAltBG = userCfg.RowAltBG
if userCfg.FooterBG != tw.Empty {
cfg.FooterBG = userCfg.FooterBG
}
if userCfg.HeaderColor != tw.Empty {
cfg.HeaderColor = userCfg.HeaderColor
}
if userCfg.RowColor != tw.Empty {
cfg.RowColor = userCfg.RowColor
}
if userCfg.FooterColor != tw.Empty {
cfg.FooterColor = userCfg.FooterColor
}
if userCfg.ApproxCharWidthFactor > 0 {
cfg.ApproxCharWidthFactor = userCfg.ApproxCharWidthFactor
}
if userCfg.MinColWidth >= 0 {
cfg.MinColWidth = userCfg.MinColWidth
}
cfg.RenderTWConfigOverrides = userCfg.RenderTWConfigOverrides
cfg.Debug = userCfg.Debug
}
r := &SVG{
config: cfg,
trace: make([]string, 0, 50),
allVisualLineData: make([][][]string, 3),
allVisualLineCtx: make([][]tw.Formatting, 3),
vMergeTrack: make(map[int]int),
logger: ll.New("svg"),
}
for i := 0; i < 3; i++ {
r.allVisualLineData[i] = make([][]string, 0)
r.allVisualLineCtx[i] = make([]tw.Formatting, 0)
}
return r
}
// calculateAllColumnWidths computes column widths based on content and merges.
// Uses content length and merge spans; handles horizontal merges by distributing width.
func (s *SVG) calculateAllColumnWidths() {
s.debug("Calculating column widths")
tempMaxCols := 0
for sectionIdx := 0; sectionIdx < 3; sectionIdx++ {
for lineIdx, lineCtx := range s.allVisualLineCtx[sectionIdx] {
if lineCtx.Row.Current != nil {
visualColCount := 0
for colIdx := 0; colIdx < len(lineCtx.Row.Current); {
cellCtx := lineCtx.Row.Current[colIdx]
if cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
colIdx++ // Skip non-start merged cells
continue
}
visualColCount++
span := 1
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
span = cellCtx.Merge.Horizontal.Span
if span <= 0 {
span = 1
}
}
colIdx += span
}
s.debug("Section %d, line %d: Visual columns = %d", sectionIdx, lineIdx, visualColCount)
if visualColCount > tempMaxCols {
tempMaxCols = visualColCount
}
} else if lineIdx < len(s.allVisualLineData[sectionIdx]) {
if rawDataLen := len(s.allVisualLineData[sectionIdx][lineIdx]); rawDataLen > tempMaxCols {
tempMaxCols = rawDataLen
}
}
}
}
s.maxCols = tempMaxCols
s.debug("Max columns: %d", s.maxCols)
if s.maxCols == 0 {
s.calculatedColWidths = []float64{}
return
}
s.calculatedColWidths = make([]float64, s.maxCols)
for i := range s.calculatedColWidths {
s.calculatedColWidths[i] = s.config.MinColWidth
}
// Structure to track max width for each merge group
type mergeKey struct {
startCol int
span int
}
maxMergeWidths := make(map[mergeKey]float64)
processSectionForWidth := func(sectionIdx int) {
for lineIdx, visualLineData := range s.allVisualLineData[sectionIdx] {
if lineIdx >= len(s.allVisualLineCtx[sectionIdx]) {
s.debug("Warning: Missing context for section %d line %d", sectionIdx, lineIdx)
continue
}
lineCtx := s.allVisualLineCtx[sectionIdx][lineIdx]
currentTableCol := 0
currentVisualCol := 0
for currentVisualCol < len(visualLineData) && currentTableCol < s.maxCols {
cellContent := visualLineData[currentVisualCol]
cellCtx := tw.CellContext{}
if lineCtx.Row.Current != nil {
if c, ok := lineCtx.Row.Current[currentTableCol]; ok {
cellCtx = c
}
}
hSpan := 1
if cellCtx.Merge.Horizontal.Present {
if cellCtx.Merge.Horizontal.Start {
hSpan = cellCtx.Merge.Horizontal.Span
if hSpan <= 0 {
hSpan = 1
}
} else {
currentTableCol++
continue
}
}
textPixelWidth := s.estimateTextWidth(cellContent)
contentAndPaddingWidth := textPixelWidth + (2 * s.config.Padding)
if hSpan == 1 {
if currentTableCol < len(s.calculatedColWidths) && contentAndPaddingWidth > s.calculatedColWidths[currentTableCol] {
s.calculatedColWidths[currentTableCol] = contentAndPaddingWidth
}
} else {
totalMergedWidth := contentAndPaddingWidth + (float64(hSpan-1) * s.config.Padding * 2)
if totalMergedWidth < s.config.MinColWidth*float64(hSpan) {
totalMergedWidth = s.config.MinColWidth * float64(hSpan)
}
if currentTableCol < len(s.calculatedColWidths) {
key := mergeKey{currentTableCol, hSpan}
if currentWidth, ok := maxMergeWidths[key]; ok {
if totalMergedWidth > currentWidth {
maxMergeWidths[key] = totalMergedWidth
}
} else {
maxMergeWidths[key] = totalMergedWidth
}
s.debug("Horizontal merge at col %d, span %d: Total width %.2f", currentTableCol, hSpan, totalMergedWidth)
}
}
currentTableCol += hSpan
currentVisualCol++
}
}
}
processSectionForWidth(sectionTypeHeader)
processSectionForWidth(sectionTypeRow)
processSectionForWidth(sectionTypeFooter)
// Apply maximum widths for merged cells
for key, width := range maxMergeWidths {
s.calculatedColWidths[key.startCol] = width
for i := 1; i < key.span && (key.startCol+i) < len(s.calculatedColWidths); i++ {
s.calculatedColWidths[key.startCol+i] = 0
}
}
for i := range s.calculatedColWidths {
if s.calculatedColWidths[i] < s.config.MinColWidth && s.calculatedColWidths[i] != 0 {
s.calculatedColWidths[i] = s.config.MinColWidth
}
}
s.debug("Column widths: %v", s.calculatedColWidths)
}
// Close finalizes SVG rendering and writes output.
// Parameter w is the output w.
// Returns an error if writing fails.
func (s *SVG) Close() error {
s.debug("Finalizing SVG output")
s.calculateAllColumnWidths()
s.renderBufferedData()
if s.numVisualRowsDrawn == 0 && s.maxCols == 0 {
fmt.Fprintf(s.w, `<svg xmlns="http://www.w3.org/2000/svg" width="%.2f" height="%.2f"></svg>`, s.config.StrokeWidth*2, s.config.StrokeWidth*2)
return nil
}
totalWidth := s.config.StrokeWidth
if len(s.calculatedColWidths) > 0 {
for _, cw := range s.calculatedColWidths {
colWidth := cw
if colWidth <= 0 {
colWidth = s.config.MinColWidth
}
totalWidth += colWidth + s.config.StrokeWidth
}
} else if s.maxCols > 0 {
for i := 0; i < s.maxCols; i++ {
totalWidth += s.config.MinColWidth + s.config.StrokeWidth
}
} else {
totalWidth = s.config.StrokeWidth * 2
}
totalHeight := s.currentY
singleVisualRowHeight := s.config.FontSize*s.config.LineHeightFactor + (2 * s.config.Padding)
if s.numVisualRowsDrawn == 0 {
if s.maxCols > 0 {
totalHeight = s.config.StrokeWidth + singleVisualRowHeight + s.config.StrokeWidth
} else {
totalHeight = s.config.StrokeWidth * 2
}
}
fmt.Fprintf(s.w, `<svg xmlns="http://www.w3.org/2000/svg" width="%.2f" height="%.2f" font-family="%s" font-size="%.2f">`,
totalWidth, totalHeight, html.EscapeString(s.config.FontFamily), s.config.FontSize)
fmt.Fprintln(s.w)
fmt.Fprintln(s.w, "<style>text { stroke: none; }</style>")
if _, err := io.WriteString(s.w, s.svgElements.String()); err != nil {
fmt.Fprintln(s.w, `</svg>`)
return fmt.Errorf("failed to write SVG elements: %w", err)
}
if s.maxCols > 0 || s.numVisualRowsDrawn > 0 {
fmt.Fprintf(s.w, ` <g class="table-borders" stroke="%s" stroke-width="%.2f" stroke-linecap="square">`,
html.EscapeString(s.config.StrokeColor), s.config.StrokeWidth)
fmt.Fprintln(s.w)
yPos := s.config.StrokeWidth / 2.0
borderRowsToDraw := s.numVisualRowsDrawn
if borderRowsToDraw == 0 && s.maxCols > 0 {
borderRowsToDraw = 1
}
lineStartX := s.config.StrokeWidth / 2.0
lineEndX := s.config.StrokeWidth / 2.0
for _, width := range s.calculatedColWidths {
lineEndX += width + s.config.StrokeWidth
}
for i := 0; i <= borderRowsToDraw; i++ {
fmt.Fprintf(s.w, ` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" />%s`,
lineStartX, yPos, lineEndX, yPos, "\n")
if i < borderRowsToDraw {
yPos += singleVisualRowHeight + s.config.StrokeWidth
}
}
xPos := s.config.StrokeWidth / 2.0
borderLineStartY := s.config.StrokeWidth / 2.0
borderLineEndY := totalHeight - (s.config.StrokeWidth / 2.0)
for visualColIdx := 0; visualColIdx <= s.maxCols; visualColIdx++ {
fmt.Fprintf(s.w, ` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" />%s`,
xPos, borderLineStartY, xPos, borderLineEndY, "\n")
if visualColIdx < s.maxCols {
colWidth := s.config.MinColWidth
if visualColIdx < len(s.calculatedColWidths) && s.calculatedColWidths[visualColIdx] > 0 {
colWidth = s.calculatedColWidths[visualColIdx]
}
xPos += colWidth + s.config.StrokeWidth
}
}
fmt.Fprintln(s.w, " </g>")
}
fmt.Fprintln(s.w, `</svg>`)
return nil
}
// Config returns the renderer's configuration.
// No parameters are required.
// Returns a Rendition with border and debug settings.
func (s *SVG) Config() tw.Rendition {
return tw.Rendition{
Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On},
Settings: tw.Settings{},
Streaming: false,
}
}
// Debug returns the renderer's debug trace.
// No parameters are required.
// Returns a slice of debug messages.
func (s *SVG) Debug() []string {
return s.trace
}
// estimateTextWidth estimates text width in SVG units.
// Parameter text is the input string to measure.
// Returns the estimated width based on font size and char factor.
func (s *SVG) estimateTextWidth(text string) float64 {
runeCount := float64(len([]rune(text)))
return runeCount * s.config.FontSize * s.config.ApproxCharWidthFactor
}
// Footer buffers footer lines for SVG rendering.
// Parameters include w (w), footers (lines), and ctx (formatting).
// No return value; stores data for later rendering.
func (s *SVG) Footer(footers [][]string, ctx tw.Formatting) {
s.debug("Buffering %d footer lines", len(footers))
for i, line := range footers {
currentCtx := ctx
currentCtx.IsSubRow = (i > 0)
s.storeVisualLine(sectionTypeFooter, line, currentCtx)
}
}
// getSVGAnchorFromTW maps tablewriter alignment to SVG text-anchor.
// Parameter align is the tablewriter alignment setting.
// Returns the corresponding SVG text-anchor value or empty string.
func (s *SVG) getSVGAnchorFromTW(align tw.Align) string {
switch align {
case tw.AlignLeft:
return "start"
case tw.AlignCenter:
return "middle"
case tw.AlignRight:
return "end"
case tw.AlignNone, tw.Skip:
return tw.Empty
}
return tw.Empty
}
// Header buffers header lines for SVG rendering.
// Parameters include w (w), headers (lines), and ctx (formatting).
// No return value; stores data for later rendering.
func (s *SVG) Header(headers [][]string, ctx tw.Formatting) {
s.debug("Buffering %d header lines", len(headers))
for i, line := range headers {
currentCtx := ctx
currentCtx.IsSubRow = i > 0
s.storeVisualLine(sectionTypeHeader, line, currentCtx)
}
}
// Line handles border rendering (ignored in SVG renderer).
// Parameters include w (w) and ctx (formatting).
// No return value; SVG borders are drawn in Close.
func (s *SVG) Line(ctx tw.Formatting) {
s.debug("Line rendering ignored")
}
// padLineSVG pads a line to the specified column count.
// Parameters include line (input strings) and numCols (target length).
// Returns the padded line with empty strings as needed.
func padLineSVG(line []string, numCols int) []string {
if numCols <= 0 {
return []string{}
}
currentLen := len(line)
if currentLen == numCols {
return line
}
if currentLen > numCols {
return line[:numCols]
}
padded := make([]string, numCols)
copy(padded, line)
return padded
}
// renderBufferedData renders all buffered lines to SVG elements.
// No parameters are required.
// No return value; populates svgElements buffer.
func (s *SVG) renderBufferedData() {
s.debug("Rendering buffered data")
s.currentY = s.config.StrokeWidth
s.dataRowCounter = 0
s.vMergeTrack = make(map[int]int)
s.numVisualRowsDrawn = 0
renderSection := func(sectionIdx int, position tw.Position) {
for visualLineIdx, visualLineData := range s.allVisualLineData[sectionIdx] {
if visualLineIdx >= len(s.allVisualLineCtx[sectionIdx]) {
s.debug("Error: Missing context for section %d line %d", sectionIdx, visualLineIdx)
continue
}
s.renderVisualLine(visualLineData, s.allVisualLineCtx[sectionIdx][visualLineIdx], position)
}
}
renderSection(sectionTypeHeader, tw.Header)
renderSection(sectionTypeRow, tw.Row)
renderSection(sectionTypeFooter, tw.Footer)
}
// renderVisualLine renders a single visual line as SVG elements.
// Parameters include lineData (cell content), ctx (formatting), and position (section type).
// No return value; handles horizontal and vertical merges.
func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, position tw.Position) {
if s.maxCols == 0 || len(s.calculatedColWidths) == 0 {
s.debug("Skipping line rendering: maxCols=%d, widths=%d", s.maxCols, len(s.calculatedColWidths))
return
}
s.numVisualRowsDrawn++
s.debug("Rendering visual row %d", s.numVisualRowsDrawn)
singleVisualRowHeight := s.config.FontSize*s.config.LineHeightFactor + (2 * s.config.Padding)
bgColor := tw.Empty
textColor := tw.Empty
defaultTextAnchor := "start"
switch position {
case tw.Header:
bgColor = s.config.HeaderBG
textColor = s.config.HeaderColor
defaultTextAnchor = "middle"
case tw.Footer:
bgColor = s.config.FooterBG
textColor = s.config.FooterColor
defaultTextAnchor = "end"
default:
textColor = s.config.RowColor
if !ctx.IsSubRow {
if s.config.RowAltBG != tw.Empty && s.dataRowCounter%2 != 0 {
bgColor = s.config.RowAltBG
} else {
bgColor = s.config.RowBG
}
s.dataRowCounter++
} else {
parentDataRowStripeIndex := max(s.dataRowCounter-1, 0)
if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 {
bgColor = s.config.RowAltBG
} else {
bgColor = s.config.RowBG
}
}
}
currentX := s.config.StrokeWidth
currentVisualCellIdx := 0
for tableColIdx := 0; tableColIdx < s.maxCols; {
if tableColIdx >= len(s.calculatedColWidths) {
s.debug("Table Col %d out of bounds for widths", tableColIdx)
tableColIdx++
continue
}
if remainingVSpan, isMerging := s.vMergeTrack[tableColIdx]; isMerging && remainingVSpan > 1 {
s.vMergeTrack[tableColIdx]--
if s.vMergeTrack[tableColIdx] <= 1 {
delete(s.vMergeTrack, tableColIdx)
}
currentX += s.calculatedColWidths[tableColIdx] + s.config.StrokeWidth
tableColIdx++
continue
}
cellContentFromVisualLine := tw.Empty
if currentVisualCellIdx < len(visualLineData) {
cellContentFromVisualLine = visualLineData[currentVisualCellIdx]
}
cellCtx := tw.CellContext{}
if ctx.Row.Current != nil {
if c, ok := ctx.Row.Current[tableColIdx]; ok {
cellCtx = c
}
}
textToRender := cellContentFromVisualLine
if cellCtx.Data != tw.Empty {
if !((cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start)) {
textToRender = cellCtx.Data
} else {
textToRender = tw.Empty
}
} else if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
textToRender = tw.Empty
}
hSpan := 1
if cellCtx.Merge.Horizontal.Present {
if cellCtx.Merge.Horizontal.Start {
hSpan = cellCtx.Merge.Horizontal.Span
if hSpan <= 0 {
hSpan = 1
}
} else {
currentX += s.calculatedColWidths[tableColIdx] + s.config.StrokeWidth
tableColIdx++
continue
}
}
vSpan := 1
isVSpanStart := false
if cellCtx.Merge.Vertical.Present && cellCtx.Merge.Vertical.Start {
vSpan = cellCtx.Merge.Vertical.Span
isVSpanStart = true
} else if cellCtx.Merge.Hierarchical.Present && cellCtx.Merge.Hierarchical.Start {
vSpan = cellCtx.Merge.Hierarchical.Span
isVSpanStart = true
}
if vSpan <= 0 {
vSpan = 1
}
rectWidth := 0.0
for hs := 0; hs < hSpan && (tableColIdx+hs) < s.maxCols; hs++ {
if (tableColIdx + hs) < len(s.calculatedColWidths) {
rectWidth += s.calculatedColWidths[tableColIdx+hs]
} else {
rectWidth += s.config.MinColWidth
}
}
if hSpan > 1 {
rectWidth += float64(hSpan-1) * s.config.StrokeWidth
}
if rectWidth <= 0 {
tableColIdx += hSpan
if hSpan > 0 {
currentVisualCellIdx++
}
continue
}
rectHeight := singleVisualRowHeight
if isVSpanStart && vSpan > 1 {
rectHeight = float64(vSpan)*singleVisualRowHeight + float64(vSpan-1)*s.config.StrokeWidth
for hs := 0; hs < hSpan && (tableColIdx+hs) < s.maxCols; hs++ {
s.vMergeTrack[tableColIdx+hs] = vSpan
}
s.debug("Vertical merge at col %d, span %d, height %.2f", tableColIdx, vSpan, rectHeight)
} else if remainingVSpan, isMerging := s.vMergeTrack[tableColIdx]; isMerging && remainingVSpan > 1 {
rectHeight = singleVisualRowHeight
textToRender = tw.Empty
}
fmt.Fprintf(&s.svgElements, ` <rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s"/>%s`,
currentX, s.currentY, rectWidth, rectHeight, html.EscapeString(bgColor), "\n")
cellTextAnchor := defaultTextAnchor
if s.config.RenderTWConfigOverrides {
if al := s.getSVGAnchorFromTW(cellCtx.Align); al != tw.Empty {
cellTextAnchor = al
}
}
textX := currentX + s.config.Padding
switch cellTextAnchor {
case "middle":
textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0
case "end":
textX = currentX + rectWidth - s.config.Padding
}
textY := s.currentY + rectHeight/2.0
escapedCell := html.EscapeString(textToRender)
fmt.Fprintf(&s.svgElements, ` <text x="%.2f" y="%.2f" fill="%s" text-anchor="%s" dominant-baseline="middle">%s</text>%s`,
textX, textY, html.EscapeString(textColor), cellTextAnchor, escapedCell, "\n")
currentX += rectWidth + s.config.StrokeWidth
tableColIdx += hSpan
currentVisualCellIdx++
}
s.currentY += singleVisualRowHeight + s.config.StrokeWidth
}
// Reset clears the renderer's internal state.
// No parameters are required.
// No return value; prepares for new rendering.
func (s *SVG) Reset() {
s.debug("Resetting state")
s.trace = make([]string, 0, 50)
for i := 0; i < 3; i++ {
s.allVisualLineData[i] = s.allVisualLineData[i][:0]
s.allVisualLineCtx[i] = s.allVisualLineCtx[i][:0]
}
s.maxCols = 0
s.calculatedColWidths = nil
s.svgElements.Reset()
s.currentY = 0
s.dataRowCounter = 0
s.vMergeTrack = make(map[int]int)
s.numVisualRowsDrawn = 0
}
// Row buffers a row line for SVG rendering.
// Parameters include w (w), rowLine (cells), and ctx (formatting).
// No return value; stores data for later rendering.
func (s *SVG) Row(rowLine []string, ctx tw.Formatting) {
s.debug("Buffering row line, IsSubRow: %v", ctx.IsSubRow)
s.storeVisualLine(sectionTypeRow, rowLine, ctx)
}
func (s *SVG) Logger(logger *ll.Logger) {
s.logger = logger.Namespace("svg")
}
// Start initializes SVG rendering.
// Parameter w is the output w.
// Returns nil; prepares internal state.
func (s *SVG) Start(w io.Writer) error {
s.w = w
s.debug("Starting SVG rendering")
s.Reset()
return nil
}
// debug logs a message if debugging is enabled.
// Parameters include format string and variadic arguments.
// No return value; appends to trace.
func (s *SVG) debug(format string, a ...interface{}) {
if s.config.Debug {
msg := fmt.Sprintf(format, a...)
s.trace = append(s.trace, "[SVG] "+msg)
}
}
// storeVisualLine stores a visual line for rendering.
// Parameters include sectionIdx, lineData (cells), and ctx (formatting).
// No return value; buffers data and context.
func (s *SVG) storeVisualLine(sectionIdx int, lineData []string, ctx tw.Formatting) {
copiedLineData := make([]string, len(lineData))
copy(copiedLineData, lineData)
s.allVisualLineData[sectionIdx] = append(s.allVisualLineData[sectionIdx], copiedLineData)
s.allVisualLineCtx[sectionIdx] = append(s.allVisualLineCtx[sectionIdx], ctx)
hasCurrent := ctx.Row.Current != nil
s.debug("Stored line in section %d, has context: %v", sectionIdx, hasCurrent)
}