summary history files

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)
}