summary history files

vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go
package renderer

import (
	"io"
	"strings"

	"github.com/olekukonko/ll"
	"github.com/olekukonko/tablewriter/pkg/twwidth"

	"github.com/olekukonko/tablewriter/tw"
)

// Blueprint implements a primary table rendering engine with customizable borders and alignments.
type Blueprint struct {
	config tw.Rendition // Rendering configuration for table borders and symbols
	logger *ll.Logger   // Logger for debug trace messages
	w      io.Writer
}

// NewBlueprint creates a new Blueprint instance with optional custom configurations.
func NewBlueprint(configs ...tw.Rendition) *Blueprint {
	// Initialize with default configuration
	cfg := defaultBlueprint()
	if len(configs) > 0 {
		userCfg := configs[0]
		// Override default borders if provided
		if userCfg.Borders.Left != 0 {
			cfg.Borders.Left = userCfg.Borders.Left
		}
		if userCfg.Borders.Right != 0 {
			cfg.Borders.Right = userCfg.Borders.Right
		}
		if userCfg.Borders.Top != 0 {
			cfg.Borders.Top = userCfg.Borders.Top
		}
		if userCfg.Borders.Bottom != 0 {
			cfg.Borders.Bottom = userCfg.Borders.Bottom
		}
		// Override symbols if provided
		if userCfg.Symbols != nil {
			cfg.Symbols = userCfg.Symbols
		}

		// Merge user settings with default settings
		cfg.Settings = mergeSettings(cfg.Settings, userCfg.Settings)
	}
	return &Blueprint{config: cfg, logger: ll.New("blueprint")}
}

// Close performs cleanup (no-op in this implementation).
func (f *Blueprint) Close() error {
	f.logger.Debug("Blueprint.Close() called (no-op).")
	return nil
}

// Config returns the renderer's current configuration.
func (f *Blueprint) Config() tw.Rendition {
	return f.config
}

// Footer renders the table footer section with configured formatting.
func (f *Blueprint) Footer(footers [][]string, ctx tw.Formatting) {
	f.logger.Debugf("Starting Footer render: IsSubRow=%v, Location=%v, Pos=%s", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position)
	// Render the footer line
	f.renderLine(ctx)
	f.logger.Debug("Completed Footer render")
}

// Header renders the table header section with configured formatting.
func (f *Blueprint) Header(headers [][]string, ctx tw.Formatting) {
	f.logger.Debugf("Starting Header render: IsSubRow=%v, Location=%v, Pos=%s, lines=%d, widths=%v",
		ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, len(ctx.Row.Current), ctx.Row.Widths)
	// Render the header line
	f.renderLine(ctx)
	f.logger.Debug("Completed Header render")
}

// Line renders a full horizontal row line with junctions and segments.
func (f *Blueprint) Line(ctx tw.Formatting) {
	// Initialize junction renderer
	jr := NewJunction(JunctionContext{
		Symbols:       f.config.Symbols,
		Ctx:           ctx,
		ColIdx:        0,
		Logger:        f.logger,
		BorderTint:    Tint{},
		SeparatorTint: Tint{},
	})

	var line strings.Builder
	totalLineWidth := 0 // Track total display width
	// Get sorted column indices
	sortedKeys := ctx.Row.Widths.SortedKeys()
	numCols := 0
	if len(sortedKeys) > 0 {
		numCols = sortedKeys[len(sortedKeys)-1] + 1
	}

	// Handle empty row case
	if numCols == 0 {
		prefix := tw.Empty
		suffix := tw.Empty
		if f.config.Borders.Left.Enabled() {
			prefix = jr.RenderLeft()
		}
		if f.config.Borders.Right.Enabled() {
			suffix = jr.RenderRight(-1)
		}
		if prefix != tw.Empty || suffix != tw.Empty {
			line.WriteString(prefix + suffix + tw.NewLine)
			totalLineWidth = twwidth.Width(prefix) + twwidth.Width(suffix)
			f.w.Write([]byte(line.String()))
		}
		f.logger.Debugf("Line: Handled empty row/widths case (total width %d)", totalLineWidth)
		return
	}

	// Calculate target total width based on data rows
	targetTotalWidth := 0
	for _, colIdx := range sortedKeys {
		targetTotalWidth += ctx.Row.Widths.Get(colIdx)
	}
	if f.config.Borders.Left.Enabled() {
		targetTotalWidth += twwidth.Width(f.config.Symbols.Column())
	}
	if f.config.Borders.Right.Enabled() {
		targetTotalWidth += twwidth.Width(f.config.Symbols.Column())
	}
	if f.config.Settings.Separators.BetweenColumns.Enabled() && len(sortedKeys) > 1 {
		targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) * (len(sortedKeys) - 1)
	}

	// Add left border if enabled
	leftBorderWidth := 0
	if f.config.Borders.Left.Enabled() {
		leftBorder := jr.RenderLeft()
		line.WriteString(leftBorder)
		leftBorderWidth = twwidth.Width(leftBorder)
		totalLineWidth += leftBorderWidth
		f.logger.Debugf("Line: Left border='%s' (f.width %d)", leftBorder, leftBorderWidth)
	}

	visibleColIndices := make([]int, 0)
	// Calculate visible columns
	for _, colIdx := range sortedKeys {
		colWidth := ctx.Row.Widths.Get(colIdx)
		if colWidth > 0 {
			visibleColIndices = append(visibleColIndices, colIdx)
		}
	}

	f.logger.Debugf("Line: sortedKeys=%v, Widths=%v, visibleColIndices=%v, targetTotalWidth=%d", sortedKeys, ctx.Row.Widths, visibleColIndices, targetTotalWidth)
	// Render each column segment
	for keyIndex, currentColIdx := range visibleColIndices {
		jr.colIdx = currentColIdx
		segment := jr.GetSegment()
		colWidth := ctx.Row.Widths.Get(currentColIdx)
		// Adjust colWidth to account for wider borders
		adjustedColWidth := colWidth
		if f.config.Borders.Left.Enabled() && keyIndex == 0 {
			adjustedColWidth -= leftBorderWidth - twwidth.Width(f.config.Symbols.Column())
		}
		if f.config.Borders.Right.Enabled() && keyIndex == len(visibleColIndices)-1 {
			rightBorderWidth := twwidth.Width(jr.RenderRight(currentColIdx))
			adjustedColWidth -= rightBorderWidth - twwidth.Width(f.config.Symbols.Column())
		}
		if adjustedColWidth < 0 {
			adjustedColWidth = 0
		}
		f.logger.Debugf("Line: colIdx=%d, segment='%s', adjusted colWidth=%d", currentColIdx, segment, adjustedColWidth)
		if segment == tw.Empty {
			spaces := strings.Repeat(tw.Space, adjustedColWidth)
			line.WriteString(spaces)
			totalLineWidth += adjustedColWidth
			f.logger.Debugf("Line: Rendered spaces='%s' (f.width %d) for col %d", spaces, adjustedColWidth, currentColIdx)
		} else {
			segmentWidth := twwidth.Width(segment)
			if segmentWidth == 0 {
				segmentWidth = 1 // Avoid division by zero
				f.logger.Warnf("Line: Segment='%s' has zero width, using 1", segment)
			}
			// Calculate how many full segments fit
			repeat := adjustedColWidth / segmentWidth
			if repeat < 1 && adjustedColWidth > 0 {
				repeat = 1
			}
			repeatedSegment := strings.Repeat(segment, repeat)
			actualWidth := twwidth.Width(repeatedSegment)
			if actualWidth > adjustedColWidth {
				// Truncate if too long
				repeatedSegment = twwidth.Truncate(repeatedSegment, adjustedColWidth)
				actualWidth = twwidth.Width(repeatedSegment)
				f.logger.Debugf("Line: Truncated segment='%s' to width %d", repeatedSegment, actualWidth)
			} else if actualWidth < adjustedColWidth {
				// Pad with segment character to match adjustedColWidth
				remainingWidth := adjustedColWidth - actualWidth
				for i := 0; i < remainingWidth/segmentWidth; i++ {
					repeatedSegment += segment
				}
				actualWidth = twwidth.Width(repeatedSegment)
				if actualWidth < adjustedColWidth {
					repeatedSegment = tw.PadRight(repeatedSegment, tw.Space, adjustedColWidth)
					actualWidth = adjustedColWidth
					f.logger.Debugf("Line: Padded segment with spaces='%s' to width %d", repeatedSegment, actualWidth)
				}
				f.logger.Debugf("Line: Padded segment='%s' to width %d", repeatedSegment, actualWidth)
			}
			line.WriteString(repeatedSegment)
			totalLineWidth += actualWidth
			f.logger.Debugf("Line: Rendered segment='%s' (f.width %d) for col %d", repeatedSegment, actualWidth, currentColIdx)
		}

		// Add junction between columns if not the last column
		isLast := keyIndex == len(visibleColIndices)-1
		if !isLast && f.config.Settings.Separators.BetweenColumns.Enabled() {
			nextColIdx := visibleColIndices[keyIndex+1]
			junction := jr.RenderJunction(currentColIdx, nextColIdx)
			// Use center symbol (❀) or column separator (|) to match data rows
			if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) {
				junction = f.config.Symbols.Center()
				if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) {
					junction = f.config.Symbols.Column()
				}
			}
			junctionWidth := twwidth.Width(junction)
			line.WriteString(junction)
			totalLineWidth += junctionWidth
			f.logger.Debugf("Line: Junction between %d and %d: '%s' (f.width %d)", currentColIdx, nextColIdx, junction, junctionWidth)
		}
	}

	// Add right border
	rightBorderWidth := 0
	if f.config.Borders.Right.Enabled() && len(visibleColIndices) > 0 {
		lastIdx := visibleColIndices[len(visibleColIndices)-1]
		rightBorder := jr.RenderRight(lastIdx)
		rightBorderWidth = twwidth.Width(rightBorder)
		line.WriteString(rightBorder)
		totalLineWidth += rightBorderWidth
		f.logger.Debugf("Line: Right border='%s' (f.width %d)", rightBorder, rightBorderWidth)
	}

	// Write the final line
	line.WriteString(tw.NewLine)
	f.w.Write([]byte(line.String()))
	f.logger.Debugf("Line rendered: '%s' (total width %d, target %d)", strings.TrimSuffix(line.String(), tw.NewLine), totalLineWidth, targetTotalWidth)
}

// Logger sets the logger for the Blueprint instance.
func (f *Blueprint) Logger(logger *ll.Logger) {
	f.logger = logger.Namespace("blueprint")
}

// Row renders a table data row with configured formatting.
func (f *Blueprint) Row(row []string, ctx tw.Formatting) {
	f.logger.Debugf("Starting Row render: IsSubRow=%v, Location=%v, Pos=%s, hasFooter=%v",
		ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, ctx.HasFooter)

	// Render the row line
	f.renderLine(ctx)
	f.logger.Debug("Completed Row render")
}

// Start initializes the rendering process (no-op in this implementation).
func (f *Blueprint) Start(w io.Writer) error {
	f.w = w
	f.logger.Debug("Blueprint.Start() called (no-op).")
	return nil
}

// formatCell formats a cell's content with specified width, padding, and alignment, returning an empty string if width is non-positive.
func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, align tw.Align) string {
	if width <= 0 {
		return tw.Empty
	}

	f.logger.Debugf("Formatting cell: content='%s', width=%d, align=%s, padding={L:'%s' R:'%s'}",
		content, width, align, padding.Left, padding.Right)

	// Calculate display width of content
	runeWidth := twwidth.Width(content)

	// Set default padding characters
	leftPadChar := padding.Left
	rightPadChar := padding.Right

	// if f.config.Settings.Cushion.Enabled() || f.config.Settings.Cushion.Default() {
	//	if leftPadChar == tw.Empty {
	//		leftPadChar = tw.Space
	//	}
	//	if rightPadChar == tw.Empty {
	//		rightPadChar = tw.Space
	//	}
	//}

	// Calculate padding widths
	padLeftWidth := twwidth.Width(leftPadChar)
	padRightWidth := twwidth.Width(rightPadChar)

	// Calculate available width for content
	availableContentWidth := max(width-padLeftWidth-padRightWidth, 0)
	f.logger.Debugf("Available content width: %d", availableContentWidth)

	// Truncate content if it exceeds available width
	if runeWidth > availableContentWidth {
		content = twwidth.Truncate(content, availableContentWidth)
		runeWidth = twwidth.Width(content)
		f.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableContentWidth, content, runeWidth)
	}

	// Calculate total padding needed
	totalPaddingWidth := max(width-runeWidth, 0)
	f.logger.Debugf("Total padding width: %d", totalPaddingWidth)

	var result strings.Builder
	var leftPaddingWidth, rightPaddingWidth int

	// Apply alignment and padding
	switch align {
	case tw.AlignLeft:
		result.WriteString(leftPadChar)
		result.WriteString(content)
		rightPaddingWidth = totalPaddingWidth - padLeftWidth
		if rightPaddingWidth > 0 {
			result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth))
			f.logger.Debugf("Applied right padding: '%s' for %d width", rightPadChar, rightPaddingWidth)
		}
	case tw.AlignRight:
		leftPaddingWidth = totalPaddingWidth - padRightWidth
		if leftPaddingWidth > 0 {
			result.WriteString(tw.PadLeft(tw.Empty, leftPadChar, leftPaddingWidth))
			f.logger.Debugf("Applied left padding: '%s' for %d width", leftPadChar, leftPaddingWidth)
		}
		result.WriteString(content)
		result.WriteString(rightPadChar)
	case tw.AlignCenter:
		leftPaddingWidth = (totalPaddingWidth-padLeftWidth-padRightWidth)/2 + padLeftWidth
		rightPaddingWidth = totalPaddingWidth - leftPaddingWidth
		if leftPaddingWidth > padLeftWidth {
			result.WriteString(tw.PadLeft(tw.Empty, leftPadChar, leftPaddingWidth-padLeftWidth))
			f.logger.Debugf("Applied left centering padding: '%s' for %d width", leftPadChar, leftPaddingWidth-padLeftWidth)
		}
		result.WriteString(leftPadChar)
		result.WriteString(content)
		result.WriteString(rightPadChar)
		if rightPaddingWidth > padRightWidth {
			result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth-padRightWidth))
			f.logger.Debugf("Applied right centering padding: '%s' for %d width", rightPadChar, rightPaddingWidth-padRightWidth)
		}
	default:
		// Default to left alignment
		result.WriteString(leftPadChar)
		result.WriteString(content)
		rightPaddingWidth = totalPaddingWidth - padLeftWidth
		if rightPaddingWidth > 0 {
			result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth))
			f.logger.Debugf("Applied right padding: '%s' for %d width", rightPadChar, rightPaddingWidth)
		}
	}

	output := result.String()
	finalWidth := twwidth.Width(output)
	// Adjust output to match target width
	if finalWidth > width {
		output = twwidth.Truncate(output, width)
		f.logger.Debugf("formatCell: Truncated output to width %d", width)
	} else if finalWidth < width {
		output = tw.PadRight(output, tw.Space, width)
		f.logger.Debugf("formatCell: Padded output to meet width %d", width)
	}

	// Log warning if final width doesn't match target
	if f.logger.Enabled() && twwidth.Width(output) != width {
		f.logger.Debugf("formatCell Warning: Final width %d does not match target %d for result '%s'",
			twwidth.Width(output), width, output)
	}

	f.logger.Debugf("Formatted cell final result: '%s' (target width %d)", output, width)
	return output
}

// renderLine renders a single line (header, row, or footer) with borders, separators, and merge handling.
func (f *Blueprint) renderLine(ctx tw.Formatting) {
	// Get sorted column indices
	sortedKeys := ctx.Row.Widths.SortedKeys()
	numCols := 0
	if len(sortedKeys) > 0 {
		numCols = sortedKeys[len(sortedKeys)-1] + 1
	}

	// Set column separator and borders
	columnSeparator := f.config.Symbols.Column()
	prefix := tw.Empty
	if f.config.Borders.Left.Enabled() {
		prefix = columnSeparator
	}
	suffix := tw.Empty
	if f.config.Borders.Right.Enabled() {
		suffix = columnSeparator
	}

	var output strings.Builder
	totalLineWidth := 0 // Track total display width
	if prefix != tw.Empty {
		output.WriteString(prefix)
		totalLineWidth += twwidth.Width(prefix)
		f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, twwidth.Width(prefix))
	}

	colIndex := 0
	separatorDisplayWidth := 0
	if f.config.Settings.Separators.BetweenColumns.Enabled() {
		separatorDisplayWidth = twwidth.Width(columnSeparator)
	}

	// Process each column
	for colIndex < numCols {
		visualWidth := ctx.Row.Widths.Get(colIndex)
		cellCtx, ok := ctx.Row.Current[colIndex]
		isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
		if visualWidth == 0 && !isHMergeStart {
			f.logger.Debugf("renderLine: Skipping col %d (zero width, not HMerge start)", colIndex)
			colIndex++
			continue
		}

		// Determine if a separator is needed
		shouldAddSeparator := false
		if colIndex > 0 && f.config.Settings.Separators.BetweenColumns.Enabled() {
			prevWidth := ctx.Row.Widths.Get(colIndex - 1)
			prevCellCtx, prevOk := ctx.Row.Current[colIndex-1]
			prevIsHMergeEnd := prevOk && prevCellCtx.Merge.Horizontal.Present && prevCellCtx.Merge.Horizontal.End
			if (prevWidth > 0 || prevIsHMergeEnd) && (!ok || (!cellCtx.Merge.Horizontal.Present || cellCtx.Merge.Horizontal.Start)) {
				shouldAddSeparator = true
			}
		}
		if shouldAddSeparator {
			output.WriteString(columnSeparator)
			totalLineWidth += separatorDisplayWidth
			f.logger.Debugf("renderLine: Added separator '%s' before col %d (f.width %d)", columnSeparator, colIndex, separatorDisplayWidth)
		} else if colIndex > 0 {
			f.logger.Debugf("renderLine: Skipped separator before col %d due to zero-width prev col or HMerge continuation", colIndex)
		}

		// Handle merged cells
		span := 1
		if isHMergeStart {
			span = cellCtx.Merge.Horizontal.Span
			if ctx.Row.Position == tw.Row {
				dynamicTotalWidth := 0
				for k := 0; k < span && colIndex+k < numCols; k++ {
					normWidth := max(ctx.NormalizedWidths.Get(colIndex+k), 0)
					dynamicTotalWidth += normWidth
					if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 {
						dynamicTotalWidth += separatorDisplayWidth
					}
				}
				visualWidth = dynamicTotalWidth
				f.logger.Debugf("renderLine: Row HMerge col %d, span %d, dynamic visualWidth %d", colIndex, span, visualWidth)
			} else {
				visualWidth = ctx.Row.Widths.Get(colIndex)
				f.logger.Debugf("renderLine: H/F HMerge col %d, span %d, pre-adjusted visualWidth %d", colIndex, span, visualWidth)
			}
		} else {
			visualWidth = ctx.Row.Widths.Get(colIndex)
			f.logger.Debugf("renderLine: Regular col %d, visualWidth %d", colIndex, visualWidth)
		}
		if visualWidth < 0 {
			visualWidth = 0
		}

		// Skip processing for non-start merged cells
		if ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
			f.logger.Debugf("renderLine: Skipping col %d processing (part of HMerge)", colIndex)
			colIndex++
			continue
		}

		// Handle empty cell context
		if !ok {
			if visualWidth > 0 {
				spaces := strings.Repeat(tw.Space, visualWidth)
				output.WriteString(spaces)
				totalLineWidth += visualWidth
				f.logger.Debugf("renderLine: No cell context for col %d, writing %d spaces (f.width %d)", colIndex, visualWidth, visualWidth)
			} else {
				f.logger.Debugf("renderLine: No cell context for col %d, visualWidth is 0, writing nothing", colIndex)
			}
			colIndex += span
			continue
		}

		// Set cell padding and alignment
		padding := cellCtx.Padding
		align := cellCtx.Align
		switch align {
		case tw.AlignNone:
			switch ctx.Row.Position {
			case tw.Header:
				align = tw.AlignCenter
			case tw.Footer:
				align = tw.AlignRight
			default:
				align = tw.AlignLeft
			}
			f.logger.Debugf("renderLine: col %d (data: '%s') using renderer default align '%s' for position %s.", colIndex, cellCtx.Data, align, ctx.Row.Position)
		case tw.Skip:
			switch ctx.Row.Position {
			case tw.Header:
				align = tw.AlignCenter
			case tw.Footer:
				align = tw.AlignRight
			default:
				align = tw.AlignLeft
			}
			f.logger.Debugf("renderLine: col %d (data: '%s') cellCtx.Align was Skip/empty, falling back to basic default '%s'.", colIndex, cellCtx.Data, align)
		}

		isTotalPattern := false

		// Case-insensitive check for "total"
		if isHMergeStart && colIndex > 0 {
			if prevCellCtx, ok := ctx.Row.Current[colIndex-1]; ok {
				if strings.Contains(strings.ToLower(prevCellCtx.Data), "total") {
					isTotalPattern = true
					f.logger.Debugf("renderLine: total pattern in row in %d", colIndex)
				}
			}
		}

		// Get the alignment from the configuration
		align = cellCtx.Align

		// Override alignment for footer merged cells
		if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
			if align == tw.AlignNone {
				f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align)
				align = tw.AlignRight
			}
		}

		// Handle vertical/hierarchical merges
		cellData := cellCtx.Data
		if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) ||
			(cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
			cellData = tw.Empty
			f.logger.Debugf("renderLine: Blanked data for col %d (non-start V/Hierarchical)", colIndex)
		}

		// Format and render the cell
		formattedCell := f.formatCell(cellData, visualWidth, padding, align)
		if len(formattedCell) > 0 {
			output.WriteString(formattedCell)
			cellWidth := twwidth.Width(formattedCell)
			totalLineWidth += cellWidth
			f.logger.Debugf("renderLine: Rendered col %d, formattedCell='%s' (f.width %d), totalLineWidth=%d", colIndex, formattedCell, cellWidth, totalLineWidth)
		}

		// Log rendering details
		if isHMergeStart {
			f.logger.Debugf("renderLine: Rendered HMerge START col %d (span %d, visualWidth %d, align %v): '%s'",
				colIndex, span, visualWidth, align, formattedCell)
		} else {
			f.logger.Debugf("renderLine: Rendered regular col %d (visualWidth %d, align %v): '%s'",
				colIndex, visualWidth, align, formattedCell)
		}
		colIndex += span
	}

	// Add suffix and adjust total width
	if output.Len() > len(prefix) || f.config.Borders.Right.Enabled() {
		output.WriteString(suffix)
		totalLineWidth += twwidth.Width(suffix)
		f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, twwidth.Width(suffix))
	}
	output.WriteString(tw.NewLine)
	f.w.Write([]byte(output.String()))
	f.logger.Debugf("renderLine: Final rendered line: '%s' (total width %d)", strings.TrimSuffix(output.String(), tw.NewLine), totalLineWidth)
}

// Rendition updates the Blueprint's configuration.
func (f *Blueprint) Rendition(config tw.Rendition) {
	f.config = mergeRendition(f.config, config)
	f.logger.Debugf("Blueprint.Rendition updated. New config: %+v", f.config)
}

// Ensure Blueprint implements tw.Renditioning
var _ tw.Renditioning = (*Blueprint)(nil)